Line | Hits | Source |
---|---|---|
1 | 1 | module.exports = { |
2 | ||
3 | name : {m: true, t: 'String'}, | |
4 | start : {m: true, t: 'Function'}, | |
5 | minPlayers : {m: true, t: 'Integer'}, | |
6 | maxPlayers : {m: true, t: 'Integer'}, | |
7 | ||
8 | init : {t: 'Function'}, | |
9 | firstStage : {t: 'String'}, | |
10 | parameters : {t: 'Array'}, | |
11 | stages : {t: 'Object'}, | |
12 | processMessage : {t: 'Function'}, | |
13 | onDisconnect : {t: 'Function'}, | |
14 | onReconnect : {t: 'Function'}, | |
15 | version : {t: 'String'}, | |
16 | description : {t: 'String'}, | |
17 | opVersion : {t: 'String'}, | |
18 | css : {t: 'Array'}, | |
19 | sounds : {t: 'Array'}, | |
20 | reconnectDelay : {t: 'Integer'}, | |
21 | ||
22 | }; | |
23 |
Line | Hits | Source |
---|---|---|
1 | 1 | var openpgp = require('openpgp'); |
2 | 1 | var uuid = require('node-uuid'); |
3 | ||
4 | 1 | module.exports = { |
5 | ||
6 | generateChallenge: function() { | |
7 | 2018 | return uuid.v4(); |
8 | }, | |
9 | ||
10 | verifyChallenge: function(challenge, key, message, callback) { | |
11 | ||
12 | 5 | var m, k; |
13 | 5 | try { |
14 | 5 | k = openpgp.key.readArmored(key).keys[0]; |
15 | 5 | m = openpgp.cleartext.readArmored(message); |
16 | } catch(e) { | |
17 | 1 | return callback(e, { success: false }); |
18 | } | |
19 | ||
20 | 4 | openpgp.verify({ |
21 | publicKeys: k, | |
22 | message: m, | |
23 | }) | |
24 | .then(function(r) { | |
25 | ||
26 | 3 | var rawUsername = k.users[0].userId.userid; |
27 | ||
28 | 3 | callback(null, { |
29 | success: challenge === r.data, | |
30 | username: rawUsername.substr(0, rawUsername.length - 3), // remove ugly ' <>' added by openpgp | |
31 | fingerprint: k.primaryKey.fingerprint, | |
32 | }); | |
33 | ||
34 | }) | |
35 | 2 | .catch(function(e) { callback(e, { success: false }); }); |
36 | }, | |
37 | ||
38 | }; | |
39 | ||
40 | /* | |
41 | console.log(module.exports.generateChallenge()); | |
42 | ||
43 | ||
44 | var options = { | |
45 | userIds: { name:'My Username', email:'' }, | |
46 | numBits: 1024, | |
47 | unlocked: true, | |
48 | }; | |
49 | ||
50 | var k; | |
51 | var p; | |
52 | var challenge = 'Random challenge'; | |
53 | ||
54 | openpgp.generateKey(options) | |
55 | .then(function(key) { | |
56 | ||
57 | p = key.publicKeyArmored; | |
58 | console.log(p); | |
59 | k = key; | |
60 | return openpgp.sign({data: challenge, privateKeys: k.key}); | |
61 | ||
62 | }) | |
63 | .then(function(message) { | |
64 | ||
65 | var key = openpgp.key.readArmored(p).keys; | |
66 | console.log(message.data); | |
67 | message = openpgp.cleartext.readArmored(message.data); | |
68 | return openpgp.verify({publicKeys: key, message: message}); | |
69 | ||
70 | }) | |
71 | .then(function(result) { | |
72 | ||
73 | var key = openpgp.key.readArmored(p).keys; | |
74 | console.log('Fingerprint:', key[0].primaryKey.fingerprint); | |
75 | ||
76 | }) | |
77 | .catch(function(e) { | |
78 | console.log(e); | |
79 | }); | |
80 | ||
81 | */ | |
82 |
Line | Hits | Source |
---|---|---|
1 | 1 | var path = require('path'); |
2 | 1 | var fs = require('fs'); |
3 | 1 | var sem = require('semver'); |
4 | 1 | var utils = require('./utils'); |
5 | 1 | var attrs = require('./attrs'); |
6 | 1 | require('colors'); |
7 | ||
8 | 1 | module.exports = function(basePath, callback) { |
9 | ||
10 | 8 | if(!basePath) |
11 | 1 | basePath = path.join(__dirname, '..', 'data'); |
12 | 8 | if(!callback) |
13 | 1 | callback = function(){}; |
14 | ||
15 | 8 | var gametypes = {}; |
16 | ||
17 | 8 | fs.readdir(basePath, function(err, files) { |
18 | ||
19 | 8 | if(err) { |
20 | 1 | return callback( '!! Unable to load game definitions\n'.red + |
21 | ' Install gameplay definitions in data/ subdirectory\n'.red + | |
22 | (' Was: ' + err).red, null); | |
23 | } | |
24 | ||
25 | 7 | var ok = false; |
26 | ||
27 | 7 | files.forEach(function(filePath) { |
28 | 32 | if(fs.statSync(path.join(basePath, filePath)).isDirectory()) { |
29 | ||
30 | 21 | var modulePath = path.join(basePath, filePath, 'definition.js'); |
31 | 21 | if(!fs.existsSync(modulePath)) |
32 | 10 | return; |
33 | ||
34 | 11 | var plugin; |
35 | 11 | if((plugin = checkFile(modulePath))) { |
36 | 5 | ok = true; |
37 | 5 | gametypes[filePath] = plugin; |
38 | ||
39 | // Has public files ? | |
40 | 5 | var publicPath = path.join(basePath, filePath, 'public'); |
41 | 5 | if(fs.existsSync(publicPath)) { |
42 | 5 | __app.use('/' + filePath, __app.express.static(publicPath)); |
43 | } | |
44 | } | |
45 | } | |
46 | }); | |
47 | ||
48 | 7 | if(!ok) { |
49 | 2 | return callback('!! No gameplay definition was correct. Aborting.'.red, null); |
50 | } | |
51 | ||
52 | 5 | callback(null, gametypes); |
53 | ||
54 | }); | |
55 | }; | |
56 | ||
57 | 1 | var checkFile = function(path) { |
58 | ||
59 | 11 | var errorLog = []; |
60 | ||
61 | 11 | try { |
62 | 11 | var plugin = require(path); |
63 | 10 | var instance = new plugin(); |
64 | ||
65 | 9 | checkMissingAttributes(instance, errorLog); |
66 | 9 | checkWrongAttributes(instance, errorLog); |
67 | ||
68 | 9 | if(errorLog.length === 1) { |
69 | 2 | throw new Error('One semantic failure'); |
70 | 7 | } else if(errorLog.length > 1) { |
71 | 2 | throw new Error(errorLog.length + ' semantic failures'); |
72 | } | |
73 | ||
74 | // Everything is ok! | |
75 | 5 | return plugin; |
76 | } catch(e) { | |
77 | 6 | console.error(('~~ Cannot load "' + path + '"\n ' + e).yellow); |
78 | 6 | errorLog.forEach(function(a) { |
79 | 10 | console.error((' - ' + a).gray.italic); |
80 | }); | |
81 | 6 | return null; |
82 | } | |
83 | }; | |
84 | ||
85 | 1 | var checkMissingAttributes = function(instance, errorLog) { |
86 | ||
87 | 9 | for(var attr in attrs) { |
88 | 153 | if(!attrs[attr].m) { |
89 | 117 | continue; |
90 | } | |
91 | ||
92 | 36 | if(!instance[attr]) { |
93 | 4 | errorLog.push('Missing "' + attr + '" mandatory attribute (' + attrs[attr].t + ')'); |
94 | } | |
95 | } | |
96 | ||
97 | }; | |
98 | ||
99 | 1 | var checkType = function(e, t) { |
100 | ||
101 | 82 | var fn; |
102 | 82 | switch(t) { |
103 | 40 | case 'String': fn = utils.isString; break; |
104 | 54 | case 'Function': fn = utils.isFunction; break; |
105 | 40 | case 'Integer': fn = utils.isInteger; break; |
106 | 22 | case 'Array': fn = utils.isArray; break; |
107 | 8 | case 'Object': fn = utils.isObject; break; |
108 | } | |
109 | 82 | return fn(e); |
110 | ||
111 | }; | |
112 | ||
113 | 1 | var checkWrongAttributes = function(instance, errorLog) { |
114 | ||
115 | 9 | for(var attr in attrs) { |
116 | 153 | if(instance[attr] && !checkType(instance[attr], attrs[attr].t)) { |
117 | 2 | errorLog.push('"' + attr + '" must be of type (' + attrs[attr].t + ')'); |
118 | } | |
119 | } | |
120 | ||
121 | 9 | if(instance.maxPlayers <= 0) { |
122 | 1 | errorLog.push('"maxPlayers" must be positive'); |
123 | } | |
124 | ||
125 | 9 | if(instance.minPlayers <= 0) { |
126 | 1 | errorLog.push('"minPlayers" must be positive'); |
127 | } | |
128 | ||
129 | 9 | if(instance.maxPlayers < instance.minPlayers) { |
130 | 1 | errorLog.push('"minPlayers" is greater than "maxPlayers"'); |
131 | } | |
132 | ||
133 | 9 | if(instance.opVersion && !sem.satisfies(__version, instance.opVersion)) { |
134 | 1 | errorLog.push('This module is not ready for this version of OpenParty (' + __version + ' does not satisfy ' + instance.opVersion + ')'); |
135 | } | |
136 | }; | |
137 |
Line | Hits | Source |
---|---|---|
1 | 1 | var utils = require('./utils.js'); |
2 | ||
3 | 1 | var Player = function(socket, room) { |
4 | ||
5 | /** | |
6 | * roles = { | |
7 | * <name>: {channels: {}, actions: {}}, | |
8 | * ... | |
9 | * } | |
10 | * | |
11 | */ | |
12 | 8 | this.roles = {}; |
13 | ||
14 | 8 | this.channels = {}; |
15 | 8 | this.actions = {}; |
16 | ||
17 | 8 | this.socket = socket; // circular |
18 | 8 | this.room = room; // circular |
19 | 8 | this.username = socket.username; |
20 | ||
21 | }; | |
22 | ||
23 | 26 | Player.prototype.join = function(channel) { this.socket.join('room_' + this.room.id + '_' + channel); }; |
24 | 6 | Player.prototype.leave = function(channel) { this.socket.leave('room_' + this.room.id + '_' + channel); }; |
25 | ||
26 | 1 | Player.prototype.setRole = function(role, value) { |
27 | 5 | if(!value && this.roles[role]) { |
28 | // Remove all registered channels | |
29 | 1 | for(var channel in this.roles[role].channels) { |
30 | 2 | if(this.roles[role].channels[channel].r && !this.channels[channel]) { |
31 | 2 | this.leave(channel); |
32 | } | |
33 | } | |
34 | 4 | } else if(value) { |
35 | // Join all channels | |
36 | 3 | for(var channel in value.channels) { |
37 | 5 | if(value.channels[channel].r) { |
38 | 4 | this.join(channel); |
39 | } | |
40 | } | |
41 | } | |
42 | 5 | setAttributeObject(this, 'roles', role, value); |
43 | 5 | this.sendWriteChannels(); |
44 | }; | |
45 | ||
46 | 1 | Player.prototype.setAction = function(name, value) { |
47 | 3 | setAttributeObject(this, 'actions', name, value); |
48 | }; | |
49 | ||
50 | 1 | Player.prototype.setChannel = function(name, rights, silent) { |
51 | ||
52 | 20 | if(!rights) { |
53 | 1 | if(this.channels[name]) { |
54 | 1 | this.leave(name); |
55 | 1 | delete this.channels[name]; |
56 | // Refreshing channels | |
57 | 1 | for(var role in this.roles) { |
58 | 1 | for(var channel in this.roles[role].channels) { |
59 | 2 | if(channel === name && this.roles[role].channels[channel].r) |
60 | 1 | this.join(channel); |
61 | } | |
62 | } | |
63 | } | |
64 | } | |
65 | ||
66 | else { | |
67 | 19 | if(rights.r) { |
68 | 17 | this.join(name); |
69 | } | |
70 | else { | |
71 | 2 | this.leave(name); |
72 | } | |
73 | 19 | this.channels[name] = rights; |
74 | } | |
75 | 20 | if(!silent) { |
76 | 4 | this.sendWriteChannels(); |
77 | } | |
78 | }; | |
79 | ||
80 | 1 | Player.prototype.getWriteChannels = function() { |
81 | 35 | var channels = {}; |
82 | ||
83 | 35 | for(var role in this.roles) { |
84 | 23 | for(var channel in this.roles[role].channels) { |
85 | 41 | if(this.roles[role].channels[channel].w) { |
86 | 23 | channels[channel] = this.roles[role].channels[channel]; |
87 | } | |
88 | } | |
89 | } | |
90 | ||
91 | 35 | for(var channel in this.channels) { // override 'default' behavior |
92 | 82 | if(this.channels[channel].w) { |
93 | 40 | channels[channel] = this.channels[channel]; |
94 | } | |
95 | else { | |
96 | 42 | delete channels[channel]; |
97 | } | |
98 | } | |
99 | ||
100 | 35 | return channels; |
101 | }; | |
102 | ||
103 | 1 | Player.prototype.sendWriteChannels = function() { |
104 | 18 | return this.emit('setAllowedChannels', this.getWriteChannels()); |
105 | }; | |
106 | ||
107 | 1 | Player.prototype.getAvailableActions = function(clone) { |
108 | 36 | var output = {}; |
109 | ||
110 | // role actions | |
111 | 36 | for(var role in this.roles) { |
112 | 25 | for(var action in this.roles[role].actions) { |
113 | 76 | if(this.roles[role].actions[action].isAvailable(this)) { |
114 | 71 | output[action] = this.roles[role].actions[action]; |
115 | } | |
116 | } | |
117 | } | |
118 | ||
119 | // personal actions (override) | |
120 | 36 | for(var action in this.actions) { |
121 | 6 | if(this.actions[action].isAvailable && this.actions[action].isAvailable(this)) { |
122 | 5 | output[action] = this.actions[action]; |
123 | } else { | |
124 | 1 | delete output[action]; |
125 | } | |
126 | } | |
127 | ||
128 | 36 | for(var action in output) { |
129 | 75 | if(clone) { |
130 | 23 | output[action] = { |
131 | type: output[action].type, | |
132 | options: output[action].options | |
133 | }; | |
134 | } | |
135 | ||
136 | 75 | processActionOptions(output[action], this.room, this); |
137 | } | |
138 | ||
139 | 36 | return output; |
140 | }; | |
141 | ||
142 | 1 | Player.prototype.sendAvailableActions = function() { |
143 | 21 | return this.emit('setAvailableActions', this.getAvailableActions(true)); |
144 | }; | |
145 | ||
146 | 1 | Player.prototype.emit = function(event, data) { |
147 | 41 | if(!this.disconnected) { |
148 | 40 | this.socket.emit(event,data); |
149 | 40 | return true; |
150 | } else { | |
151 | 1 | return false; |
152 | } | |
153 | }; | |
154 | ||
155 | 1 | Player.prototype.message = function(m) { |
156 | 1 | return this.emit('chatMessage', {message: m}); |
157 | }; | |
158 | ||
159 | 1 | module.exports = { |
160 | ||
161 | init: function(room) { | |
162 | 2 | room.players.forEach(function(p) { |
163 | 8 | p.player = new Player(p, room); |
164 | 8 | p.player.setChannel('general', {r:true, w:true, n:'General'}, true); |
165 | 8 | p.player.setChannel('player-' + p.player.username, {r: true, w: false}, true); |
166 | 8 | p.player.sendWriteChannels(); |
167 | }); | |
168 | }, | |
169 | ||
170 | Player: Player | |
171 | ||
172 | }; | |
173 | ||
174 | /** PRIVATE FUNCTIONS **/ | |
175 | ||
176 | 1 | function setAttributeObject(p, attr, name, value) { |
177 | 8 | if(value === null || value === undefined) { |
178 | 3 | delete p[attr][name]; |
179 | } | |
180 | ||
181 | else { | |
182 | 5 | p[attr][name] = value; |
183 | } | |
184 | } | |
185 | ||
186 | 1 | function processActionOptions(action, room, player) { |
187 | ||
188 | 75 | switch(action.type) { |
189 | ||
190 | case 'select': | |
191 | ||
192 | 53 | if(!action.options.choices) { |
193 | 1 | action.options.choices = 'players'; |
194 | } | |
195 | ||
196 | 53 | if(utils.isString(action.options.choices)) { |
197 | ||
198 | 19 | var out = []; |
199 | 19 | room.players.forEach(function(e) { |
200 | 76 | out.push(e.username); |
201 | }); | |
202 | 19 | action.options.safeChoices = out; |
203 | ||
204 | 34 | } else if(Array.isArray(action.options.choices)) { |
205 | 19 | action.options.safeChoices = action.options.choices; |
206 | } else { | |
207 | 15 | action.options.safeChoices = action.options.choices(room, player); |
208 | } | |
209 | ||
210 | 53 | break; |
211 | ||
212 | default: | |
213 | 22 | break; |
214 | } | |
215 | ||
216 | } | |
217 |
Line | Hits | Source |
---|---|---|
1 | 1 | var rooms = require('./rooms'); |
2 | ||
3 | // Try to reconnect a socket to left room after unexpected disconnection | |
4 | 1 | module.exports = function(socket) { |
5 | 12 | if(!socket.session.identifier) { |
6 | 0 | return; |
7 | } | |
8 | ||
9 | 12 | var found = false; |
10 | 12 | for(var i = 0; i < rooms.rooms.length; i++) { |
11 | 1 | var room = rooms.rooms[i]; |
12 | 1 | for(var j = 0; j < room.players.length; j++) { |
13 | 2 | var so = room.players[j]; |
14 | 2 | if(so.player && so.player.disconnected |
15 | && so.session.identifier === socket.session.identifier | |
16 | && so.session.username === socket.session.username) { | |
17 | ||
18 | // Update the player | |
19 | 1 | clearTimeout(so.player.disconnectionTimer); |
20 | 1 | so.player.disconnected = undefined; |
21 | 1 | socket.currentRoom = room; |
22 | 1 | socket.player = so.player; |
23 | 1 | socket.player.socket = socket; // Update circular reference |
24 | 1 | socket.player.username = socket.username; |
25 | 1 | room.players[j] = socket; // This is the magic part! |
26 | ||
27 | // Refresh current game state | |
28 | 1 | socket.emit('roomJoined', room.getPublicInfo()); |
29 | 1 | socket.emit('gameStarted'); |
30 | 1 | socket.join('room_' + room.id); |
31 | 1 | socket.player.sendWriteChannels(); |
32 | 1 | socket.player.sendAvailableActions(); |
33 | ||
34 | // Subscribe to lost channels | |
35 | /// Subscribe to personal channel | |
36 | 1 | socket.player.join('player-' + socket.username); |
37 | /// Subscribe to role channels | |
38 | 1 | for(var role in socket.player.roles) { |
39 | 0 | for(var channel in socket.player.roles[role].channels) { |
40 | 0 | if(socket.player.roles[role].channels[channel].r) { |
41 | 0 | socket.player.join(channel); |
42 | } | |
43 | } | |
44 | } | |
45 | ||
46 | /// Subscribe or unsubscribe with personal channels (overrides default role behavior | |
47 | 1 | for(var channel in socket.player.channels) { |
48 | 2 | if(socket.player.channels[channel].r) { |
49 | 2 | socket.player.join(channel); |
50 | } else { | |
51 | 0 | socket.player.leave(channel); |
52 | } | |
53 | } | |
54 | ||
55 | // Inform player about the current situation | |
56 | 1 | socket.player.message(__i18n({phrase: "You have been reconnected after a network issue. Sorry, some information may not have been retrieved.", locale: socket.session.locale})) |
57 | ||
58 | // Notify gameplay if needed | |
59 | 1 | if(room.gameplay.onReconnect) |
60 | 1 | room.gameplay.onReconnect(room, socket.player); |
61 | ||
62 | 1 | break; |
63 | } | |
64 | } | |
65 | 1 | if(found) break; |
66 | } | |
67 | }; | |
68 |
Line | Hits | Source |
---|---|---|
1 | 1 | var utils = require('./utils.js'); |
2 | 1 | var players = require('./players.js'); |
3 | 1 | var Player = players.Player; |
4 | ||
5 | ||
6 | 1 | var Room = function(name, password, gameplay) { |
7 | ||
8 | 3 | this.isRoom = true; |
9 | 3 | this.id = utils.randomString(20); // TODO make it better... |
10 | 3 | this.players = []; |
11 | 3 | this.name = name; |
12 | 3 | this.size = gameplay.minPlayers; |
13 | 3 | this.creationDate = new Date(); |
14 | 3 | this.password = password; |
15 | ||
16 | 3 | this.gameplay = gameplay; |
17 | 3 | this.gameplay.room = this; // circular |
18 | ||
19 | // Stages | |
20 | 3 | this.started = false; |
21 | 3 | this.timeout = null; |
22 | 3 | this.currentStage = null; |
23 | ||
24 | 3 | if(this.gameplay.init) { |
25 | 3 | this.gameplay.init(this); |
26 | } | |
27 | ||
28 | }; | |
29 | ||
30 | /** | |
31 | * Broadcast a message to several sockets | |
32 | * @param {String} [channel] | |
33 | * @param {String} event | |
34 | * @param {*} [data] | |
35 | */ | |
36 | 1 | Room.prototype.broadcast = function(channel, event, data) { |
37 | ||
38 | 47 | if(data === undefined && channel) { |
39 | 31 | data = event; |
40 | 31 | event = channel; |
41 | 31 | channel = ''; |
42 | 16 | } else if(channel !== '') { |
43 | 12 | channel = '_' + channel; |
44 | } | |
45 | 47 | __app.io.to('room_' + this.id + channel).emit(event, data); |
46 | }; | |
47 | ||
48 | /** | |
49 | * Send more information about a player. | |
50 | * Client-side, it will be displayed in players list. | |
51 | * | |
52 | * @param {String} [channel] | |
53 | * @param {Object} player (socket or Player object) | |
54 | * @param {String} value | |
55 | */ | |
56 | 1 | Room.prototype.playerInfo = function(channel, player, value) { |
57 | ||
58 | 4 | if(!value) { |
59 | 2 | if(channel instanceof Player) { |
60 | 1 | channel = channel.socket; |
61 | } | |
62 | 2 | this.broadcast('playerInfo', {username: channel.username, value: player}); |
63 | } else { | |
64 | 2 | if(player instanceof Player) { |
65 | 1 | player = player.socket; |
66 | } | |
67 | 2 | this.broadcast(channel, 'playerInfo', {username: player.username, value: value}); |
68 | } | |
69 | }; | |
70 | ||
71 | /** | |
72 | * Send a chat message to players (system message) | |
73 | * @param {String} [channel] | |
74 | * @param {String} message | |
75 | */ | |
76 | 1 | Room.prototype.message = function(channel, message) { |
77 | 6 | if(!message) { |
78 | 4 | this.broadcast('chatMessage', {message:channel}); |
79 | } | |
80 | else { | |
81 | 2 | this.broadcast(channel, 'chatMessage', {message:message}); |
82 | } | |
83 | }; | |
84 | ||
85 | /** | |
86 | * Get public information about the room and its players | |
87 | * @return {Object} | |
88 | */ | |
89 | 1 | Room.prototype.getPublicInfo = function() { |
90 | 36 | var output = {}; |
91 | ||
92 | 36 | output.id = this.id; |
93 | 36 | output.isRoom = true; |
94 | 36 | output.name = this.name; |
95 | 36 | output.players = []; |
96 | 36 | output.size = this.size; |
97 | 36 | output.started = this.started; |
98 | 36 | output.password = !!this.password; |
99 | ||
100 | 36 | for(var i = 0; i < this.players.length; i++) { |
101 | 75 | output.players.push({ |
102 | username: this.players[i].username, | |
103 | authentication: this.players[i].authentication, | |
104 | }); | |
105 | } | |
106 | ||
107 | 36 | var parameters = []; |
108 | 36 | this.gameplay.parameters.forEach(function(p) { |
109 | 72 | parameters.push({ |
110 | name : p.name, | |
111 | value : p.value, | |
112 | help : p.help, | |
113 | isBoolean : p.type === Boolean | |
114 | }); | |
115 | }); | |
116 | ||
117 | 36 | output.gameplay = { |
118 | name : this.gameplay.name, | |
119 | description : this.gameplay.description, | |
120 | parameters : parameters, | |
121 | maxPlayers : this.gameplay.maxPlayers, | |
122 | minPlayers : this.gameplay.minPlayers, | |
123 | sounds : this.gameplay.sounds | |
124 | }; | |
125 | ||
126 | 36 | return output; |
127 | ||
128 | }; | |
129 | ||
130 | /** | |
131 | * Set room size | |
132 | * @param {Number} size | |
133 | */ | |
134 | 1 | Room.prototype.setSize = function(size) { |
135 | 3 | if(size >= this.gameplay.minPlayers && size <= this.gameplay.maxPlayers) { |
136 | 2 | this.size = size; |
137 | 2 | sendUpdateRoom(this); |
138 | } | |
139 | }; | |
140 | ||
141 | /** | |
142 | * Set room parameter | |
143 | * @param {} parameter | |
144 | * @param {*} value | |
145 | * @param {Socket} socket | |
146 | */ | |
147 | 1 | Room.prototype.setParameter = function(parameter, value, socket) { |
148 | 3 | this.gameplay.parameters.forEach(function(e) { |
149 | 6 | if(e.name === parameter) { |
150 | 2 | e.value = e.type(value); //deal with it. |
151 | 2 | sendUpdateRoom(this, socket); |
152 | 2 | return; |
153 | } | |
154 | }.bind(this)); | |
155 | }; | |
156 | ||
157 | 1 | Room.prototype.sendMessage = function(channel, message, socket) { |
158 | 24 | var allowed = false; |
159 | ||
160 | 24 | if(channel === 'preChat') { |
161 | 5 | if(this.started) { |
162 | 1 | return; |
163 | } | |
164 | 4 | allowed = true; |
165 | 4 | channel = ''; |
166 | } else { | |
167 | 19 | if(!this.started) { |
168 | 2 | return; |
169 | } | |
170 | 17 | var channels = socket.player.getWriteChannels(); |
171 | 17 | allowed = (channels[channel] ? (true === channels[channel].w) : false); |
172 | } | |
173 | ||
174 | 21 | if(allowed) { |
175 | 15 | socket.emit('messageSent'); |
176 | 15 | if(this.gameplay.processMessage && this.started) { |
177 | 11 | message = this.gameplay.processMessage(channel, message, socket.player); |
178 | 11 | if(!message) { |
179 | 4 | return; |
180 | } | |
181 | } | |
182 | 11 | this.broadcast(channel, 'chatMessage', {message: message, sender: socket.username}); |
183 | } | |
184 | }; | |
185 | ||
186 | 1 | Room.prototype.start = function() { |
187 | 2 | players.init(this); |
188 | 2 | this.gameplay.start(this, function(err) { |
189 | ||
190 | 2 | if(err) { |
191 | 1 | return this.broadcast('chatMessage', { message: err }); |
192 | } | |
193 | ||
194 | 1 | this.started = true; |
195 | 1 | sendUpdateRoom(this); |
196 | 1 | this.broadcast('gameStarted'); |
197 | ||
198 | 1 | if(this.gameplay.firstStage) { |
199 | 1 | this.nextStage(this.gameplay.firstStage); |
200 | } | |
201 | ||
202 | }.bind(this)); | |
203 | }; | |
204 | ||
205 | /** | |
206 | * End the current stage and start another one | |
207 | * @param {String} stage The new stage name | |
208 | * @param {Function} [callback] | |
209 | */ | |
210 | 1 | Room.prototype.nextStage = function(stage, callback) { |
211 | 5 | this.currentStage = stage; |
212 | 5 | this.gameplay.stages[stage].start(this, function(err, duration) { |
213 | 5 | this.setStageDuration(duration); |
214 | ||
215 | // Send actions to players | |
216 | ||
217 | 5 | this.players.forEach(function(player) { |
218 | 20 | player.player.sendAvailableActions(); |
219 | }); | |
220 | ||
221 | 5 | if(callback) { |
222 | 1 | callback(null); |
223 | } | |
224 | }.bind(this)); | |
225 | }; | |
226 | ||
227 | /** | |
228 | * End the current stage, without starting another one | |
229 | */ | |
230 | 1 | Room.prototype.endStage = function() { |
231 | 1 | clearTimeout(this.timeout); |
232 | 1 | var endFn = this.gameplay.stages[this.currentStage].end; |
233 | 1 | if(endFn) |
234 | 1 | endFn(this, function() {}); |
235 | }; | |
236 | ||
237 | /** | |
238 | * Change current stage duration | |
239 | * @param {Number} duration (sec) | |
240 | */ | |
241 | 1 | Room.prototype.setStageDuration = function(duration) { |
242 | 6 | clearTimeout(this.timeout); |
243 | ||
244 | 6 | if(duration < 0) { |
245 | 3 | this.broadcast('clearTimer'); |
246 | 3 | this.currentStageEnd = Infinity; |
247 | 3 | return; |
248 | } | |
249 | ||
250 | 3 | this.currentStageEnd = new Date().getTime() + duration * 1000; |
251 | 3 | this.broadcast('setTimer', duration); |
252 | 3 | this.timeout = setTimeout(this.endStage.bind(this), duration * 1000); |
253 | }; | |
254 | ||
255 | /** | |
256 | * @return Number Milliseconds before next stage. Can be 'Infinity'. | |
257 | */ | |
258 | 1 | Room.prototype.getRemainingTime = function() { |
259 | 2 | return this.currentStageEnd - new Date().getTime(); |
260 | }; | |
261 | ||
262 | /** | |
263 | * Get player object from username | |
264 | * @param {String} username | |
265 | * @return {Socket} | |
266 | */ | |
267 | 1 | Room.prototype.resolveUsername = function(username) { |
268 | 3 | for(var i = 0; i < this.players.length; i++) { |
269 | 6 | if(this.players[i].username === username) { |
270 | 2 | return this.players[i]; |
271 | } | |
272 | } | |
273 | 1 | return null; |
274 | }; | |
275 | ||
276 | 1 | module.exports = { |
277 | ||
278 | rooms: [], | |
279 | ||
280 | getRooms: function() { | |
281 | ||
282 | 1 | var output = []; |
283 | 1 | for(var i = 0; i < this.rooms.length; i++) { |
284 | 1 | if(!this.rooms[i].started) { |
285 | 1 | output.push(this.rooms[i].getPublicInfo()); |
286 | } | |
287 | } | |
288 | ||
289 | 1 | return output; |
290 | }, | |
291 | ||
292 | createRoom: function(name, password, type, socket) { | |
293 | ||
294 | 4 | if(__conf.maxRooms && this.rooms.length >= __conf.maxRooms) { |
295 | 0 | return; |
296 | } | |
297 | ||
298 | 4 | if(!type || type === 'default') { |
299 | 2 | for(var i in __gametypes) { |
300 | 2 | type = i; |
301 | 2 | break; |
302 | } | |
303 | } | |
304 | ||
305 | 4 | if(!__gametypes[type]) { |
306 | 1 | return; |
307 | } | |
308 | ||
309 | 3 | var gameplay = new __gametypes[type](); |
310 | ||
311 | // Set sounds path | |
312 | 3 | if(gameplay.sounds) { |
313 | 3 | var sounds = []; |
314 | 3 | gameplay.sounds.forEach(function(s){ |
315 | 3 | if(!s.distant) |
316 | 3 | sounds.push({ |
317 | id : s.id, | |
318 | path : '/' + type + '/' + s.path | |
319 | }); | |
320 | else | |
321 | 0 | sounds.push(s); |
322 | }); | |
323 | 3 | gameplay.sounds = sounds; |
324 | } | |
325 | ||
326 | 3 | var room = new Room(name, password, gameplay); |
327 | ||
328 | 3 | this.rooms.push(room); |
329 | 3 | __app.io.to('lobby').emit('roomCreated', room.getPublicInfo()); |
330 | 3 | this.joinRoom(room.id, password, socket); |
331 | }, | |
332 | ||
333 | getRoom: function(id) { | |
334 | ||
335 | 12 | for(var i = 0; i < this.rooms.length; i++) { |
336 | 12 | if(this.rooms[i].id === id) { |
337 | 11 | return this.rooms[i]; |
338 | } | |
339 | } | |
340 | ||
341 | 1 | return null; |
342 | ||
343 | }, | |
344 | ||
345 | joinRoom: function(id, password, socket) { | |
346 | ||
347 | 12 | var room = this.getRoom(id); |
348 | 12 | if(!room) { |
349 | 1 | return; |
350 | } | |
351 | ||
352 | 11 | if(room.players.length >= room.size) { |
353 | 1 | var d = (new Date()).getTime(); |
354 | 1 | if(!room.lastFullNotice || room.lastFullNotice < (d - 60*1000)) { |
355 | 1 | room.lastFullNotice = d; |
356 | 1 | room.broadcast('chatMessage', {message: '<span class="glyphicon glyphicon-user"></span> <strong>Someone</strong> would like to join this room. Could you add some slots?'}); |
357 | } | |
358 | 1 | return; |
359 | } | |
360 | ||
361 | 10 | if(room.password && room.password !== password) { |
362 | 0 | socket.emit('invalidRoomPassword'); |
363 | 0 | return; |
364 | } | |
365 | ||
366 | 10 | if(room.players.indexOf(socket) < 0) { |
367 | 10 | room.players.push(socket); |
368 | } | |
369 | ||
370 | 10 | sendUpdateRoom(room); |
371 | 10 | socket.emit('roomJoined', room.getPublicInfo()); |
372 | 10 | socket.join('room_' + id); |
373 | 10 | socket.currentRoom = room; |
374 | ||
375 | 10 | room.broadcast('chatMessage', {message: '<span class="glyphicon glyphicon-ok-circle"></span> <strong>' + socket.username + '</strong> has joined the room'}); |
376 | ||
377 | }, | |
378 | ||
379 | leaveRoom: function(socket) { | |
380 | ||
381 | 13 | var room = socket.currentRoom; |
382 | 13 | if(!room) { |
383 | 4 | return; |
384 | } | |
385 | ||
386 | 9 | var i = room.players.indexOf(socket); |
387 | 9 | if(i < 0) { |
388 | 1 | return; |
389 | } | |
390 | ||
391 | 8 | room.players.splice(i, 1); |
392 | ||
393 | 8 | if(room.gameplay.onDisconnect && socket.player) { |
394 | 2 | room.gameplay.onDisconnect(room, socket.player); |
395 | } | |
396 | ||
397 | 8 | if(!room.started && socket.username) { |
398 | 6 | room.broadcast('chatMessage', {message: '<span class="glyphicon glyphicon-remove-circle"></span> <strong>' + socket.username + '</strong> has left the room'}); |
399 | } | |
400 | ||
401 | 8 | if(room) |
402 | ||
403 | 8 | if(room.players.length === 0) { |
404 | 2 | this.removeRoom(room); |
405 | } else { | |
406 | 6 | sendUpdateRoom(room); |
407 | } | |
408 | ||
409 | 8 | socket.player = null; |
410 | 8 | socket.currentRoom = null; |
411 | ||
412 | // Leave concerned socket rooms | |
413 | // Buffered, because socket.rooms is dynamically updated by socket.io | |
414 | 8 | var regex = /^room\_/; |
415 | 8 | var buffer = []; |
416 | 8 | for(var r in socket.rooms) { |
417 | 17 | if(regex.test(socket.rooms[r])) { |
418 | 7 | buffer.push(r); |
419 | } | |
420 | } | |
421 | ||
422 | 8 | try { |
423 | 8 | buffer.forEach(function(r) { |
424 | 7 | socket.leave(r); |
425 | }); | |
426 | 8 | socket.emit('roomLeft'); |
427 | } catch(e) {} | |
428 | ||
429 | }, | |
430 | ||
431 | removeRoom: function(room) { | |
432 | ||
433 | 2 | for(var i = 0; i < this.rooms.length; i++) { |
434 | 2 | if(this.rooms[i].id === room.id) { |
435 | 2 | clearTimeout(this.rooms[i].timeout); |
436 | 2 | this.rooms.splice(i, 1); |
437 | 2 | __app.io.to('lobby').emit('roomRemoved', room.id); |
438 | 2 | return; |
439 | } | |
440 | } | |
441 | ||
442 | } | |
443 | ||
444 | }; | |
445 | ||
446 | /** PRIVATE FUNCTIONS **/ | |
447 | ||
448 | 1 | function sendUpdateRoom(room, socket) { |
449 | 21 | if(!socket) { |
450 | 19 | __app.io.to('lobby').emit('roomUpdated', room.getPublicInfo()); |
451 | } | |
452 | else { | |
453 | 2 | socket.broadcast.to('lobby').emit('roomUpdated', room.getPublicInfo()); |
454 | } | |
455 | } | |
456 |
Line | Hits | Source |
---|---|---|
1 | 1 | var utils = require('./utils'); |
2 | 1 | var rooms = require('./rooms'); |
3 | 1 | var crypto = require('./crypto'); |
4 | 1 | var git = require('git-repo-info'); |
5 | ||
6 | // we want to define this only one time | |
7 | 1 | var repo = git('.git'); |
8 | 1 | if(repo.branch) { |
9 | 1 | repo.url = require('../package.json').repository.url.replace(/\.git$/, '') + '/commit/' + repo.sha; |
10 | } | |
11 | ||
12 | 1 | module.exports = function(app) { |
13 | ||
14 | /** | |
15 | * ROOT | |
16 | */ | |
17 | 1 | app.get('/', function(req, res) { |
18 | 17 | req.session.locale = req.getLocale(); |
19 | 17 | res.render('index', {passwordRequired: __conf.password !== null }); |
20 | }); | |
21 | ||
22 | /*** | |
23 | * ABOUT | |
24 | */ | |
25 | 1 | app.get('/about', function(req, res) { |
26 | 1 | res.render('about', {repo: repo, version: __version, locales: __conf.locales, gamemodes: __staticGametypes }); |
27 | }); | |
28 | ||
29 | /** | |
30 | * IO : Connect | |
31 | */ | |
32 | 1 | app.io.on('connection', function(socket) { |
33 | 17 | socket.challenge = crypto.generateChallenge(); |
34 | 17 | socket.emit('challenge', socket.challenge); |
35 | ||
36 | // Check for reconnection | |
37 | 17 | if(socket.session.identifier && socket.session.username) { |
38 | 1 | socket.emit('reconnectInvitation', socket.session.username); |
39 | 16 | } else if(!socket.session.identifier) { |
40 | 16 | socket.session.identifier = utils.randomString(20); |
41 | 16 | socket.session.save(); |
42 | } | |
43 | }); | |
44 | ||
45 | /** | |
46 | * IO : Ping | |
47 | */ | |
48 | 1 | app.io.route('o-ping', function(socket) { |
49 | 1 | socket.emit('o-pong'); |
50 | }); | |
51 | ||
52 | /** | |
53 | * IO : Disconnect | |
54 | */ | |
55 | 1 | app.io.route('disconnect', function(socket) { |
56 | 5 | var room = utils.isInGame(socket); |
57 | 6 | if(!room) return; |
58 | 4 | if(!socket.session.identifier || !socket.player) { |
59 | 2 | rooms.leaveRoom(socket); |
60 | } else { | |
61 | 2 | socket.player.disconnected = true; |
62 | 2 | socket.player.disconnectionTimer = setTimeout(function() { |
63 | 1 | rooms.leaveRoom(socket); |
64 | }, (room.gameplay.reconnectDelay || 60) * 1000); | |
65 | } | |
66 | }); | |
67 | ||
68 | /** | |
69 | * IO : Login | |
70 | */ | |
71 | 1 | app.io.route('login', function(socket, data) { |
72 | 18 | var locale = socket.session.locale || __conf.defaultLocale; |
73 | 18 | var nbLogged = 0; |
74 | 18 | for(var s in app.io.sockets.sockets) { |
75 | 257 | if(app.io.sockets.sockets[s].username) nbLogged++; |
76 | } | |
77 | ||
78 | 18 | if(__conf.maxPlayers && nbLogged >= __conf.maxPlayers) { |
79 | 0 | return socket.emit('loginResult', {err: __i18n({phrase: 'This server is full, sorry. Please try again later!', locale: locale})}); |
80 | } | |
81 | ||
82 | 18 | if(data.password !== __conf.password && __conf.password !== null || !data.username) { |
83 | 4 | return socket.emit('loginResult', {err: __i18n({phrase: 'Bad Credentials', locale: locale})}); |
84 | } | |
85 | ||
86 | // Check username | |
87 | 14 | var username; |
88 | 14 | try { |
89 | 14 | username = utils.checkUsername(data.username); |
90 | } catch(e) { | |
91 | 2 | return socket.emit('loginResult', {err: __i18n({phrase: e.message, locale: locale})}); |
92 | } | |
93 | ||
94 | 12 | socket.join('lobby'); |
95 | 12 | socket.username = username; |
96 | 12 | socket.emit('loginResult', {err: null, username: username, gametypes: __staticGametypes}); |
97 | ||
98 | // Check session | |
99 | 12 | socket.session.username = data.username; |
100 | 12 | socket.session.save(); |
101 | ||
102 | // Check authentication (if provided) | |
103 | 12 | if(data.key && data.message) { |
104 | 0 | crypto.verifyChallenge(socket.challenge, data.key, data.message, function(err, res) { |
105 | 0 | if(err || !res.success || data.username !== res.username) { |
106 | 0 | return; |
107 | } | |
108 | 0 | socket.authentication = { |
109 | username: res.username, | |
110 | fingerprint: res.fingerprint, | |
111 | }; | |
112 | }); | |
113 | } | |
114 | ||
115 | // Try to reconnect to left room | |
116 | 12 | require('./recover')(socket); |
117 | }); | |
118 | ||
119 | /** | |
120 | * IO : Logout (ask for session reset) | |
121 | */ | |
122 | 1 | app.io.route('logout', function(socket) { |
123 | 0 | socket.session.identifier = undefined; |
124 | 0 | socket.session.username = undefined; |
125 | 0 | socket.session.save(); |
126 | 0 | socket.emit('reconnect'); |
127 | }); | |
128 | ||
129 | /** | |
130 | * IO : Get Rooms | |
131 | */ | |
132 | 1 | app.io.route('getRooms', function(socket) { |
133 | 2 | if(!utils.isInRoom(socket, 'lobby')) |
134 | 1 | return; |
135 | 1 | socket.emit('roomsList', rooms.getRooms()); |
136 | }); | |
137 | ||
138 | /** | |
139 | * IO : Create Room | |
140 | */ | |
141 | 1 | app.io.route('createRoom', function(socket, data) { |
142 | 6 | if(!utils.isInRoom(socket, 'lobby')) |
143 | 1 | return; |
144 | 5 | if(utils.isInGame(socket)) |
145 | 1 | return; |
146 | 4 | rooms.createRoom(data.name, data.password, data.type, socket); |
147 | }); | |
148 | ||
149 | /** | |
150 | * IO : Join Room | |
151 | */ | |
152 | 1 | app.io.route('joinRoom', function(socket, data) { |
153 | 11 | if(!utils.isInRoom(socket, 'lobby')) |
154 | 1 | return; |
155 | 10 | if(utils.isInGame(socket)) |
156 | 1 | return; |
157 | 9 | rooms.joinRoom(data.id, data.password, socket); |
158 | }); | |
159 | ||
160 | /** | |
161 | * IO : Leave Room | |
162 | */ | |
163 | 1 | app.io.route('leaveRoom', function(socket) { |
164 | 9 | rooms.leaveRoom(socket); |
165 | }); | |
166 | ||
167 | /** | |
168 | * IO : Kick Player | |
169 | */ | |
170 | 1 | app.io.route('kickPlayer', function(socket, username) { |
171 | ||
172 | 0 | var room = utils.isInGame(socket); |
173 | 0 | var player; |
174 | 0 | if( !room |
175 | || room.started | |
176 | || room.players[0] !== socket | |
177 | || !(player = room.resolveUsername(username))) { | |
178 | 0 | return; |
179 | } | |
180 | 0 | room.message(username + ' has been kicked out of this room.'); |
181 | 0 | rooms.leaveRoom(player); |
182 | }); | |
183 | ||
184 | /** | |
185 | * IO : Room Parameters | |
186 | */ | |
187 | 1 | app.io.route('setRoomSize', function(socket, data) { |
188 | 4 | var room = utils.isInGame(socket); |
189 | 4 | if(room && room.players[0] === socket && room.players.length <= +data) |
190 | 3 | room.setSize(+data); |
191 | }); | |
192 | ||
193 | 1 | app.io.route('setRoomParameter', function(socket, data) { |
194 | 4 | var room = utils.isInGame(socket); |
195 | 4 | if(room && room.players[0] === socket) |
196 | 3 | room.setParameter(data.parameter, data.value, socket); |
197 | }); | |
198 | ||
199 | /** | |
200 | * IO : Chat Management | |
201 | */ | |
202 | 1 | app.io.route('sendMessage', function(socket, data) { |
203 | 28 | var room = utils.isInGame(socket); |
204 | 28 | var message = utils.sanitizeHtml(data.message); |
205 | 28 | if(!message) |
206 | 3 | return; |
207 | 25 | if(room) |
208 | 24 | room.sendMessage(data.channel, message, socket); |
209 | 1 | else if(socket.username) { |
210 | 1 | socket.emit('messageSent'); |
211 | 1 | app.io.to('lobby').emit('chatMessage', {message: message, sender: socket.username, lobby: true}); |
212 | } | |
213 | }); | |
214 | ||
215 | /** | |
216 | * IO : Start Game ! | |
217 | */ | |
218 | 1 | app.io.route('startRoom', function(socket) { |
219 | 3 | var room = utils.isInGame(socket); |
220 | 3 | if(room && room.players[0] === socket && !room.started && room.size === room.players.length) |
221 | 2 | room.start(); |
222 | }); | |
223 | ||
224 | /** | |
225 | * IO : Execute Action | |
226 | */ | |
227 | 1 | app.io.route('executeAction', function(socket, data) { |
228 | ||
229 | 18 | if(!socket.player || !data || !data.action) |
230 | 3 | return; // Not authorized |
231 | ||
232 | 15 | var actions = socket.player.getAvailableActions(false); |
233 | ||
234 | 15 | if(!actions[data.action] || actions[data.action].locked) |
235 | 2 | return; // Not ready for this action |
236 | ||
237 | 13 | if(actions[data.action].type === 'select' |
238 | && actions[data.action].options.safeChoices.indexOf(data.value) < 0) | |
239 | 7 | return; // Bad value |
240 | ||
241 | 6 | if(actions[data.action].type === 'select' |
242 | && actions[data.action].options.choices === 'players') { | |
243 | 1 | data.value = socket.player.room.resolveUsername(data.value).player; |
244 | } | |
245 | ||
246 | 6 | actions[data.action].locked = true; |
247 | 6 | actions[data.action].execute(socket.player, data.value); |
248 | 6 | actions[data.action].locked = false; |
249 | }); | |
250 | }; | |
251 |
Line | Hits | Source |
---|---|---|
1 | 1 | var crypto = require('crypto'); |
2 | ||
3 | 1 | global.GET_RANDOM = function(from, to) { |
4 | 1027 | return Math.floor(Math.random() * (to - from + 1)) + from; |
5 | }; | |
6 | ||
7 | 1 | module.exports = { |
8 | ||
9 | randomString: function(len) { | |
10 | 19 | return crypto.randomBytes(Math.ceil(len * 3 / 4)) |
11 | .toString('base64') // convert to base64 format | |
12 | .slice(0, len) // return required number of characters | |
13 | .replace(/\+/g, '0') // replace '+' with '0' | |
14 | .replace(/\//g, '0'); // replace '/' with '0' | |
15 | }, | |
16 | ||
17 | isInRoom: function(socket, room) { | |
18 | 19 | return !!socket.rooms[room]; |
19 | }, | |
20 | ||
21 | isInGame: function(socket) { | |
22 | ||
23 | 59 | if(!socket.currentRoom) |
24 | 15 | return false; |
25 | else | |
26 | 44 | return socket.currentRoom; |
27 | ||
28 | }, | |
29 | ||
30 | sanitizeHtml: function(string) { | |
31 | 41 | if(!string) |
32 | 3 | return ''; |
33 | 38 | string = string.replace(/</ig,'<'); |
34 | 38 | string = string.replace(/>/ig,'>'); |
35 | 38 | return string; |
36 | }, | |
37 | ||
38 | checkUsername: function(username) { | |
39 | 14 | if(username.length > 20) |
40 | 1 | throw new Error('Username is too long.'); |
41 | ||
42 | 13 | username = this.sanitizeHtml(username); |
43 | 13 | username = username.trim(); |
44 | ||
45 | 13 | if(username.length === 0) |
46 | 1 | throw new Error('Username is invalid.'); |
47 | ||
48 | // already connected ? | |
49 | 12 | var index = 2; |
50 | 12 | var newUsername = username; |
51 | 12 | var sockets = __app.io.sockets.sockets; |
52 | 12 | while(true) { |
53 | 15 | var ok = true; |
54 | 15 | for(var socket in sockets) { |
55 | 146 | var s_username = sockets[socket].username; |
56 | 146 | if(s_username && s_username.toLowerCase() === newUsername.toLowerCase()) { |
57 | 3 | ok = false; |
58 | 3 | newUsername = username + this.intToExpString(index++); |
59 | 3 | break; |
60 | } | |
61 | } | |
62 | 27 | if(ok) break; |
63 | } | |
64 | 12 | return newUsername; |
65 | }, | |
66 | ||
67 | intToExpString: function(n) { | |
68 | 3 | var data = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹']; |
69 | ||
70 | 3 | if(n <= 0) { |
71 | 0 | return data[0]; |
72 | } | |
73 | ||
74 | 3 | var out = ''; |
75 | 3 | while(n > 0) { |
76 | 3 | out = data[n%10] + out; |
77 | 3 | n = Math.floor(n/10); |
78 | } | |
79 | 3 | return out; |
80 | }, | |
81 | ||
82 | /** TYPES */ | |
83 | ||
84 | isString: function(s) { | |
85 | 78 | return typeof s === 'string' || s instanceof String; |
86 | }, | |
87 | ||
88 | isFunction: function(f) { | |
89 | 27 | return typeof f === 'function'; |
90 | }, | |
91 | ||
92 | isInteger: function(i) { | |
93 | 20 | return i === parseInt(i, 10); |
94 | }, | |
95 | ||
96 | isArray: function(a) { | |
97 | 11 | return require('util').isArray(a); |
98 | }, | |
99 | ||
100 | isObject: function(o) { | |
101 | 4 | return typeof o === 'object'; |
102 | } | |
103 | }; | |
104 |