l)return m.push(c),m.push(v),m.push(f),!1;b&&(t=v,q=l-p,0<=p||q<2*l)&&(b.bInA=!1)}t&&b&&Math.abs(q) 0;)p--}0===h(t[n],d)?r(t,n,p):(p++,r(t,p,a)),p<=i&&(n=p+1),i<=p&&(a=p-1)}}function r(t,i,n){var e=t[i];t[i]=t[n],t[n]=e}i.exports=e},{}]},{},[1])(1)});
2 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var argv = require('minimist')(process.argv.slice(2));
3 | var SocketCluster = require('socketcluster').SocketCluster;
4 | var scHotReboot = require('sc-hot-reboot');
5 |
6 | var workerControllerPath = argv.wc || process.env.SOCKETCLUSTER_WORKER_CONTROLLER;
7 | var brokerControllerPath = argv.bc || process.env.SOCKETCLUSTER_BROKER_CONTROLLER;
8 | var initControllerPath = argv.ic || process.env.SOCKETCLUSTER_INIT_CONTROLLER;
9 | var environment = process.env.ENV || 'dev';
10 |
11 | var options = {
12 | workers: Number(argv.w) || Number(process.env.SOCKETCLUSTER_WORKERS) || 1,
13 | brokers: Number(argv.b) || Number(process.env.SOCKETCLUSTER_BROKERS) || 1,
14 | port: Number(argv.p) || Number(process.env.SOCKETCLUSTER_PORT) || Number(process.env.PORT) || 8000,
15 | // If your system doesn't support 'uws', you can switch to 'ws' (which is slower but works on older systems).
16 | wsEngine: process.env.SOCKETCLUSTER_WS_ENGINE || 'ws',
17 | appName: argv.n || process.env.SOCKETCLUSTER_APP_NAME || null,
18 | workerController: workerControllerPath || __dirname + '/worker.js',
19 | brokerController: brokerControllerPath || __dirname + '/broker.js',
20 | initController: initControllerPath || null,
21 | socketChannelLimit: Number(process.env.SOCKETCLUSTER_SOCKET_CHANNEL_LIMIT) || 1000,
22 | clusterStateServerHost: argv.cssh || process.env.SCC_STATE_SERVER_HOST || null,
23 | clusterStateServerPort: process.env.SCC_STATE_SERVER_PORT || null,
24 | clusterAuthKey: process.env.SCC_AUTH_KEY || null,
25 | clusterStateServerConnectTimeout: Number(process.env.SCC_STATE_SERVER_CONNECT_TIMEOUT) || null,
26 | clusterStateServerAckTimeout: Number(process.env.SCC_STATE_SERVER_ACK_TIMEOUT) || null,
27 | clusterStateServerReconnectRandomness: Number(process.env.SCC_STATE_SERVER_RECONNECT_RANDOMNESS) || null,
28 | crashWorkerOnError: argv['auto-reboot'] != false,
29 | // If using nodemon, set this to true, and make sure that environment is 'dev'.
30 | killMasterOnSignal: false,
31 | instanceId: 'realm-1',
32 | pubSubBatchDuration: null,
33 | environment: environment
34 | };
35 |
36 | var SOCKETCLUSTER_OPTIONS;
37 |
38 | if (process.env.SOCKETCLUSTER_OPTIONS) {
39 | SOCKETCLUSTER_OPTIONS = JSON.parse(process.env.SOCKETCLUSTER_OPTIONS);
40 | }
41 |
42 | for (var i in SOCKETCLUSTER_OPTIONS) {
43 | if (SOCKETCLUSTER_OPTIONS.hasOwnProperty(i)) {
44 | options[i] = SOCKETCLUSTER_OPTIONS[i];
45 | }
46 | }
47 |
48 | var masterControllerPath = argv.mc || process.env.SOCKETCLUSTER_MASTER_CONTROLLER;
49 |
50 | var start = function () {
51 | var socketCluster = new SocketCluster(options);
52 |
53 | if (masterControllerPath) {
54 | var masterController = require(masterControllerPath);
55 | masterController.run(socketCluster);
56 | }
57 |
58 | if (environment == 'dev') {
59 | // This will cause SC workers to reboot when code changes anywhere in the app directory.
60 | // The second options argument here is passed directly to chokidar.
61 | // See https://github.com/paulmillr/chokidar#api for details.
62 | console.log(` !! The sc-hot-reboot plugin is watching for code changes in the ${__dirname} directory`);
63 | scHotReboot.attach(socketCluster, {
64 | cwd: __dirname,
65 | ignored: ['public', 'node_modules', 'README.md', 'Dockerfile', 'server.js', 'broker.js', /[\/\\]\./]
66 | });
67 | }
68 | };
69 |
70 | var bootCheckInterval = Number(process.env.SOCKETCLUSTER_BOOT_CHECK_INTERVAL) || 200;
71 |
72 | if (workerControllerPath) {
73 | // Detect when Docker volumes are ready.
74 | var startWhenFileIsReady = (filePath) => {
75 | return new Promise((resolve) => {
76 | if (!filePath) {
77 | resolve();
78 | return;
79 | }
80 | var checkIsReady = () => {
81 | fs.exists(filePath, (exists) => {
82 | if (exists) {
83 | resolve();
84 | } else {
85 | setTimeout(checkIsReady, bootCheckInterval);
86 | }
87 | });
88 | };
89 | checkIsReady();
90 | });
91 | };
92 | var filesReadyPromises = [
93 | startWhenFileIsReady(masterControllerPath),
94 | startWhenFileIsReady(workerControllerPath),
95 | startWhenFileIsReady(brokerControllerPath),
96 | startWhenFileIsReady(initControllerPath)
97 | ];
98 | Promise.all(filesReadyPromises).then(() => {
99 | start();
100 | });
101 | } else {
102 | start();
103 | }
104 |
--------------------------------------------------------------------------------
/state-manager.js:
--------------------------------------------------------------------------------
1 | var StateManager = function (options) {
2 | this.channelGrid = options.channelGrid;
3 | this.stateRefs = options.stateRefs;
4 | };
5 |
6 | StateManager.prototype.create = function (state) {
7 | var stateCellIndex = this.channelGrid.getCellIndex(state);
8 | var stateRef = {
9 | id: state.id,
10 | tcid: stateCellIndex, // Target cell index.
11 | type: state.type,
12 | create: state
13 | };
14 | if (state.swid != null) {
15 | stateRef.swid = state.swid;
16 | }
17 | this.stateRefs[state.id] = stateRef;
18 | return stateRef;
19 | };
20 |
21 | // You can only update through operations which must be interpreted
22 | // by your cell controllers (cell.js).
23 | StateManager.prototype.update = function (stateRef, operation) {
24 | this.stateRefs[stateRef.id].op = operation;
25 | };
26 |
27 | StateManager.prototype.delete = function (stateRef) {
28 | this.stateRefs[stateRef.id].delete = 1;
29 | };
30 |
31 | module.exports.StateManager = StateManager;
32 |
--------------------------------------------------------------------------------
/util.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 |
3 | var Util = function (options) {
4 | this.cellData = options.cellData;
5 | };
6 |
7 | Util.prototype.groupStates = function (stateList) {
8 | stateList.forEach(function (state) {
9 | if (!state.external) {
10 | if (!state.pendingGroup) {
11 | state.pendingGroup = {};
12 | }
13 | stateList.forEach(function (memberState) {
14 | state.pendingGroup[memberState.id] = memberState;
15 | });
16 | }
17 | });
18 | };
19 |
20 | Util.prototype.ungroupStates = function (stateList) {
21 | var self = this;
22 |
23 | stateList.forEach(function (state) {
24 | if (!state.external && state.pendingGroup) {
25 |
26 | stateList.forEach(function (memberState) {
27 | delete state.pendingGroup[memberState.id];
28 | if (_.isEmpty(state.pendingGroup)) {
29 | delete state.pendingGroup;
30 | }
31 | });
32 | }
33 | });
34 | };
35 |
36 | Util.prototype.ungroupStateFromAll = function (state) {
37 | var self = this;
38 |
39 | var groupMembers = state.pendingGroup || {};
40 | var stateUngroupList = [];
41 |
42 | Object.keys(groupMembers).forEach(function (memberId) {
43 | var cellIndex = state.ccid;
44 | var type = state.type;
45 |
46 | var memberSimpleState = groupMembers[memberId];
47 | if (self.cellData[cellIndex] && self.cellData[cellIndex][type]) {
48 | var memberState = self.cellData[cellIndex][type][memberId];
49 | if (memberState) {
50 | stateUngroupList.push(memberState);
51 | }
52 | }
53 | });
54 | self.ungroupStates(stateUngroupList);
55 | };
56 |
57 | module.exports.Util = Util;
58 |
--------------------------------------------------------------------------------
/worker.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var _ = require('lodash');
3 | var express = require('express');
4 | var serveStatic = require('serve-static');
5 | var path = require('path');
6 | var morgan = require('morgan');
7 | var healthChecker = require('sc-framework-health-check');
8 | var StateManager = require('./state-manager').StateManager;
9 | var uuid = require('uuid');
10 | var ChannelGrid = require('./public/channel-grid').ChannelGrid;
11 | var Util = require('./util').Util;
12 | var SAT = require('sat');
13 | var rbush = require('rbush');
14 | var scCodecMinBin = require('sc-codec-min-bin');
15 |
16 | var config = require('./config');
17 | var CellController = require('./cell');
18 |
19 | var WORLD_WIDTH = config.WORLD_WIDTH;
20 | var WORLD_HEIGHT = config.WORLD_HEIGHT;
21 | var WORLD_CELL_WIDTH = config.WORLD_CELL_WIDTH;
22 | var WORLD_CELL_HEIGHT = config.WORLD_CELL_HEIGHT;
23 | var WORLD_COLS = Math.ceil(WORLD_WIDTH / WORLD_CELL_WIDTH);
24 | var WORLD_ROWS = Math.ceil(WORLD_HEIGHT / WORLD_CELL_HEIGHT);
25 | var WORLD_CELLS = WORLD_COLS * WORLD_ROWS;
26 | var WORLD_CELL_OVERLAP_DISTANCE = config.WORLD_CELL_OVERLAP_DISTANCE;
27 | var WORLD_UPDATE_INTERVAL = config.WORLD_UPDATE_INTERVAL;
28 | var WORLD_STALE_TIMEOUT = config.WORLD_STALE_TIMEOUT;
29 | var SPECIAL_UPDATE_INTERVALS = config.SPECIAL_UPDATE_INTERVALS;
30 |
31 | var PLAYER_DIAMETER = config.PLAYER_DIAMETER;
32 | var PLAYER_MASS = config.PLAYER_MASS;
33 |
34 | var OUTBOUND_STATE_TRANSFORMERS = config.OUTBOUND_STATE_TRANSFORMERS;
35 |
36 | var CHANNEL_INBOUND_CELL_PROCESSING = 'internal/cell-processing-inbound';
37 | var CHANNEL_CELL_TRANSITION = 'internal/cell-transition';
38 |
39 | var game = {
40 | stateRefs: {}
41 | };
42 |
43 | function getRandomPosition(spriteWidth, spriteHeight) {
44 | var halfSpriteWidth = spriteWidth / 2;
45 | var halfSpriteHeight = spriteHeight / 2;
46 | var widthRandomness = WORLD_WIDTH - spriteWidth;
47 | var heightRandomness = WORLD_HEIGHT - spriteHeight;
48 | return {
49 | x: Math.round(halfSpriteWidth + widthRandomness * Math.random()),
50 | y: Math.round(halfSpriteHeight + heightRandomness * Math.random())
51 | };
52 | }
53 |
54 | module.exports.run = function (worker) {
55 | console.log(' >> Worker PID:', process.pid);
56 |
57 | // We use a codec for SC to compress messages between clients and the server
58 | // to a lightweight binary format to reduce bandwidth consumption.
59 | // We should probably make our own codec (on top of scCodecMinBin) to compress
60 | // world-specific entities. For example, instead of emitting the JSON:
61 | // {id: '...', width: 200, height: 200}
62 | // We could compress it down to something like: {id: '...', w: 200, h: 200, c: 1000}
63 | worker.scServer.setCodecEngine(scCodecMinBin);
64 |
65 | var environment = worker.options.environment;
66 | var serverWorkerId = worker.options.instanceId + ':' + worker.id;
67 |
68 | var app = express();
69 |
70 | var httpServer = worker.httpServer;
71 | var scServer = worker.scServer;
72 |
73 | if (environment == 'dev') {
74 | // Log every HTTP request. See https://github.com/expressjs/morgan for other
75 | // available formats.
76 | app.use(morgan('dev'));
77 | }
78 | app.use(serveStatic(path.resolve(__dirname, 'public')));
79 |
80 | // Add GET /health-check express route
81 | healthChecker.attach(worker, app);
82 |
83 | httpServer.on('request', app);
84 |
85 | scServer.addMiddleware(scServer.MIDDLEWARE_SUBSCRIBE, function (req, next) {
86 | if (req.channel.indexOf('internal/') == 0) {
87 | var err = new Error('Clients are not allowed to subscribe to the ' + req.channel + ' channel.');
88 | err.name = 'ForbiddenSubscribeError';
89 | next(err);
90 | } else {
91 | next();
92 | }
93 | });
94 |
95 | scServer.addMiddleware(scServer.MIDDLEWARE_PUBLISH_IN, function (req, next) {
96 | // Only allow clients to publish to channels whose names start with 'external/'
97 | if (req.channel.indexOf('external/') == 0) {
98 | next();
99 | } else {
100 | var err = new Error('Clients are not allowed to publish to the ' + req.channel + ' channel.');
101 | err.name = 'ForbiddenPublishError';
102 | next(err);
103 | }
104 | });
105 |
106 | // This allows us to break up our channels into a grid of cells which we can
107 | // watch and publish to individually.
108 | // It handles most of the data distribution automatically so that it reaches
109 | // the intended cells.
110 | var channelGrid = new ChannelGrid({
111 | worldWidth: WORLD_WIDTH,
112 | worldHeight: WORLD_HEIGHT,
113 | cellOverlapDistance: WORLD_CELL_OVERLAP_DISTANCE,
114 | rows: WORLD_ROWS,
115 | cols: WORLD_COLS,
116 | exchange: scServer.exchange
117 | });
118 |
119 | var stateManager = new StateManager({
120 | stateRefs: game.stateRefs,
121 | channelGrid: channelGrid
122 | });
123 |
124 | if (WORLD_CELLS % worker.options.workers != 0) {
125 | var errorMessage = 'The number of cells in your world (determined by WORLD_WIDTH, WORLD_HEIGHT, WORLD_CELL_WIDTH, WORLD_CELL_HEIGHT)' +
126 | ' should share a common factor with the number of workers or else the workload might get duplicated for some cells.';
127 | console.error(errorMessage);
128 | }
129 |
130 | var cellsPerWorker = WORLD_CELLS / worker.options.workers;
131 |
132 | var cellData = {};
133 | var cellPendingDeletes = {};
134 | var cellExternalStates = {};
135 |
136 | var util = new Util({
137 | cellData: cellData
138 | });
139 |
140 | var cellControllers = {};
141 | var updateIntervals = {};
142 | var cellSpecialIntervalTypes = {};
143 |
144 | for (var h = 0; h < cellsPerWorker; h++) {
145 | var cellIndex = worker.id + h * worker.options.workers;
146 | cellData[cellIndex] = {};
147 | cellPendingDeletes[cellIndex] = {};
148 | cellExternalStates[cellIndex] = {};
149 |
150 | cellControllers[cellIndex] = new CellController({
151 | cellIndex: cellIndex,
152 | cellData: cellData[cellIndex],
153 | cellBounds: channelGrid.getCellBounds(cellIndex),
154 | worker: worker
155 | }, util);
156 |
157 | channelGrid.watchCellAtIndex(CHANNEL_INBOUND_CELL_PROCESSING, cellIndex, gridCellDataHandler.bind(null, cellIndex));
158 | channelGrid.watchCellAtIndex(CHANNEL_CELL_TRANSITION, cellIndex, gridCellTransitionHandler.bind(null, cellIndex));
159 | }
160 |
161 | function applyOutboundStateTransformer(state) {
162 | var type = state.type;
163 | if (OUTBOUND_STATE_TRANSFORMERS[type]) {
164 | return OUTBOUND_STATE_TRANSFORMERS[type](state);
165 | }
166 | return state;
167 | }
168 |
169 | function setUpdateIntervals(intervalMap) {
170 | Object.keys(intervalMap).forEach(function (interval) {
171 | var intervalNumber = parseInt(interval);
172 |
173 | intervalMap[interval].forEach(function (type) {
174 | cellSpecialIntervalTypes[type] = true;
175 | });
176 |
177 | updateIntervals[interval] = setInterval(function () {
178 | var transformedStateList = [];
179 |
180 | Object.keys(cellData).forEach(function (cellIndex) {
181 | var currentCellData = cellData[cellIndex];
182 |
183 | intervalMap[interval].forEach(function (type) {
184 | Object.keys(currentCellData[type] || {}).forEach(function (id) {
185 | transformedStateList.push(
186 | applyOutboundStateTransformer(currentCellData[type][id])
187 | );
188 | });
189 | });
190 | });
191 | // External channel which clients can subscribe to.
192 | // It will publish to multiple channels based on each state's
193 | // (x, y) coordinates.
194 | if (transformedStateList.length) {
195 | channelGrid.publish('cell-data', transformedStateList);
196 | }
197 | }, intervalNumber);
198 | });
199 | }
200 |
201 | setUpdateIntervals(SPECIAL_UPDATE_INTERVALS);
202 |
203 | function getSimplifiedState(state) {
204 | return {
205 | type: state.type,
206 | x: Math.round(state.x),
207 | y: Math.round(state.y)
208 | };
209 | }
210 |
211 | function isGroupABetterThanGroupB(groupA, groupB) {
212 | // If both groups are the same size, the one that has the leader
213 | // with the lowest alphabetical id wins.
214 | return groupA.leader.id <= groupB.leader.id;
215 | }
216 |
217 | /*
218 | Groups are not passed around between cells/processes. Their purpose is to allow
219 | states to seamlessly interact with one another across cell boundaries.
220 |
221 | When one state affects another state across cell boundaries (e.g. one player
222 | pushing another player into a different cell), there is a slight delay for
223 | the position information to be shared across processes/CPU cores; as a
224 | result of this, the states may not show up in the exact same position in both cells.
225 | When two cells report slightly different positions for the same set of
226 | states, it may cause overlapping and flickering on the front end since the
227 | front end doesn't know which data to trust.
228 |
229 | A group allows two cells to agree on which cell is responsible for broadcasting the
230 | position of states that are within the group by considering the group's average position
231 | instead of looking at the position of member states individually.
232 | */
233 | function getStateGroups() {
234 | var groupMap = {};
235 | Object.keys(cellData).forEach(function (cellIndex) {
236 | if (!groupMap[cellIndex]) {
237 | groupMap[cellIndex] = {};
238 | }
239 | var currentCellData = cellData[cellIndex];
240 | var currentGroupMap = groupMap[cellIndex];
241 | Object.keys(currentCellData).forEach(function (type) {
242 | var cellDataStates = currentCellData[type] || {};
243 | Object.keys(cellDataStates).forEach(function (id) {
244 | var state = cellDataStates[id];
245 | if (state.group) {
246 | var groupSimpleStateMap = {};
247 | Object.keys(state.group).forEach(function (stateId) {
248 | groupSimpleStateMap[stateId] = state.group[stateId];
249 | });
250 |
251 | var groupStateIdList = Object.keys(groupSimpleStateMap).sort();
252 | var groupId = groupStateIdList.join(',');
253 |
254 | var leaderClone = _.cloneDeep(state);
255 | leaderClone.x = groupSimpleStateMap[leaderClone.id].x;
256 | leaderClone.y = groupSimpleStateMap[leaderClone.id].y;
257 |
258 | var group = {
259 | id: groupId,
260 | leader: state,
261 | members: [],
262 | size: 0,
263 | x: 0,
264 | y: 0,
265 | };
266 | var expectedMemberCount = groupStateIdList.length;
267 |
268 | for (var i = 0; i < expectedMemberCount; i++) {
269 | var memberId = groupStateIdList[i];
270 | var memberSimplifiedState = groupSimpleStateMap[memberId];
271 | var memberState = currentCellData[memberSimplifiedState.type][memberId];
272 | if (memberState) {
273 | var memberStateClone = _.cloneDeep(memberState);
274 | memberStateClone.x = memberSimplifiedState.x;
275 | memberStateClone.y = memberSimplifiedState.y;
276 | group.members.push(memberStateClone);
277 | group.x += memberStateClone.x;
278 | group.y += memberStateClone.y;
279 | group.size++;
280 | }
281 | }
282 | if (group.size) {
283 | group.x = Math.round(group.x / group.size);
284 | group.y = Math.round(group.y / group.size);
285 | }
286 |
287 | var allGroupMembersAreAvailableToThisCell = group.size >= expectedMemberCount;
288 | var existingGroup = currentGroupMap[groupId];
289 | if (allGroupMembersAreAvailableToThisCell &&
290 | (!existingGroup || isGroupABetterThanGroupB(group, existingGroup))) {
291 |
292 | group.tcid = channelGrid.getCellIndex(group);
293 | currentGroupMap[groupId] = group;
294 | }
295 | }
296 | });
297 | });
298 | });
299 | return groupMap;
300 | }
301 |
302 | function prepareStatesForProcessing(cellIndex) {
303 | var currentCellData = cellData[cellIndex];
304 | var currentCellExternalStates = cellExternalStates[cellIndex];
305 |
306 | Object.keys(currentCellData).forEach(function (type) {
307 | var cellDataStates = currentCellData[type] || {};
308 | Object.keys(cellDataStates).forEach(function (id) {
309 | var state = cellDataStates[id];
310 |
311 | if (state.external) {
312 | if (!currentCellExternalStates[type]) {
313 | currentCellExternalStates[type] = {};
314 | }
315 | currentCellExternalStates[type][id] = _.cloneDeep(state);
316 | }
317 | });
318 | });
319 | }
320 |
321 | // We should never modify states which belong to other cells or
322 | // else it will result in conflicts and lost states. This function
323 | // restores them to their pre-processed condition.
324 | function restoreExternalStatesBeforeDispatching(cellIndex) {
325 | var currentCellData = cellData[cellIndex];
326 | var currentCellExternalStates = cellExternalStates[cellIndex];
327 |
328 | Object.keys(currentCellExternalStates).forEach(function (type) {
329 | var externalStatesList = currentCellExternalStates[type];
330 | Object.keys(externalStatesList).forEach(function (id) {
331 | currentCellData[type][id] = externalStatesList[id];
332 | delete externalStatesList[id];
333 | });
334 | });
335 | }
336 |
337 | function prepareGroupStatesBeforeDispatching(cellIndex) {
338 | var currentCellData = cellData[cellIndex];
339 | var currentCellExternalStates = cellExternalStates[cellIndex];
340 |
341 | Object.keys(currentCellData).forEach(function (type) {
342 | var cellDataStates = currentCellData[type] || {};
343 | Object.keys(cellDataStates).forEach(function (id) {
344 | var state = cellDataStates[id];
345 | if (state.pendingGroup) {
346 | var serializedMemberList = {};
347 | Object.keys(state.pendingGroup).forEach(function (memberId) {
348 | var memberState = state.pendingGroup[memberId];
349 | serializedMemberList[memberId] = getSimplifiedState(memberState);
350 | });
351 | state.group = serializedMemberList;
352 | delete state.pendingGroup;
353 | } else if (state.group) {
354 | delete state.group;
355 | }
356 | });
357 | });
358 | }
359 |
360 | // Remove decorator functions which were added to the states temporarily
361 | // for use within the cell controller.
362 | function cleanupStatesBeforeDispatching(cellIndex) {
363 | var currentCellData = cellData[cellIndex];
364 |
365 | Object.keys(currentCellData).forEach(function (type) {
366 | var cellDataStates = currentCellData[type] || {};
367 | Object.keys(cellDataStates).forEach(function (id) {
368 | var state = cellDataStates[id];
369 |
370 | if (state.op) {
371 | delete state.op;
372 | }
373 | });
374 | });
375 | }
376 |
377 | // Main world update loop.
378 | setInterval(function () {
379 | var cellIndexList = Object.keys(cellData);
380 | var transformedStateList = [];
381 |
382 | cellIndexList.forEach(function (cellIndex) {
383 | cellIndex = Number(cellIndex);
384 | prepareStatesForProcessing(cellIndex);
385 | cellControllers[cellIndex].run(cellData[cellIndex]);
386 | prepareGroupStatesBeforeDispatching(cellIndex);
387 | cleanupStatesBeforeDispatching(cellIndex);
388 | restoreExternalStatesBeforeDispatching(cellIndex);
389 | dispatchProcessedData(cellIndex);
390 | });
391 |
392 | var groupMap = getStateGroups();
393 |
394 | cellIndexList.forEach(function (cellIndex) {
395 | cellIndex = Number(cellIndex);
396 | var currentCellData = cellData[cellIndex];
397 | Object.keys(currentCellData).forEach(function (type) {
398 | if (!cellSpecialIntervalTypes[type]) {
399 | var cellDataStates = currentCellData[type] || {};
400 | Object.keys(cellDataStates).forEach(function (id) {
401 | var state = cellDataStates[id];
402 | if (!state.group && !state.external &&
403 | (!cellPendingDeletes[cellIndex][type] || !cellPendingDeletes[cellIndex][type][id])) {
404 |
405 | transformedStateList.push(
406 | applyOutboundStateTransformer(state)
407 | );
408 | }
409 | });
410 | }
411 | });
412 | });
413 |
414 | // Deletions are processed as part of WORLD_UPDATE_INTERVAL even if
415 | // that type has its own special interval.
416 | Object.keys(cellPendingDeletes).forEach(function (cellIndex) {
417 | cellIndex = Number(cellIndex);
418 | var currentCellDeletes = cellPendingDeletes[cellIndex];
419 | Object.keys(currentCellDeletes).forEach(function (type) {
420 | var cellDeleteStates = currentCellDeletes[type] || {};
421 | Object.keys(cellDeleteStates).forEach(function (id) {
422 | // These states should already have a delete property which
423 | // can be used on the client-side to delete items from the view.
424 | transformedStateList.push(
425 | applyOutboundStateTransformer(cellDeleteStates[id])
426 | );
427 | delete cellDeleteStates[id];
428 | });
429 | });
430 | });
431 |
432 | Object.keys(groupMap).forEach(function (cellIndex) {
433 | cellIndex = Number(cellIndex);
434 | var currentGroupMap = groupMap[cellIndex];
435 | Object.keys(currentGroupMap).forEach(function (groupId) {
436 | var group = currentGroupMap[groupId];
437 | var memberList = group.members;
438 | if (group.tcid == cellIndex) {
439 | memberList.forEach(function (member) {
440 | transformedStateList.push(
441 | applyOutboundStateTransformer(member)
442 | );
443 | });
444 | }
445 | });
446 | });
447 |
448 | // External channel which clients can subscribe to.
449 | // It will publish to multiple channels based on each state's
450 | // (x, y) coordinates.
451 | if (transformedStateList.length) {
452 | channelGrid.publish('cell-data', transformedStateList);
453 | }
454 |
455 | }, WORLD_UPDATE_INTERVAL);
456 |
457 | function forEachStateInDataTree(dataTree, callback) {
458 | var typeList = Object.keys(dataTree);
459 |
460 | typeList.forEach(function (type) {
461 | var stateList = dataTree[type];
462 | var ids = Object.keys(stateList);
463 |
464 | ids.forEach(function (id) {
465 | callback(stateList[id]);
466 | });
467 | });
468 | }
469 |
470 | function updateStateExternalTag(state, cellIndex) {
471 | if (state.ccid != cellIndex || state.tcid != cellIndex) {
472 | state.external = true;
473 | } else {
474 | delete state.external;
475 | }
476 | }
477 |
478 | // Share states with adjacent cells when those states get near
479 | // other cells' boundaries and prepare for transition to other cells.
480 | // This logic is quite complex so be careful when changing any code here.
481 | function dispatchProcessedData(cellIndex) {
482 | var now = Date.now();
483 | var currentCellData = cellData[cellIndex];
484 | var workerStateRefList = {};
485 | var statesForNearbyCells = {};
486 |
487 | forEachStateInDataTree(currentCellData, function (state) {
488 | var id = state.id;
489 | var swid = state.swid;
490 | var type = state.type;
491 |
492 | if (!state.external) {
493 | if (state.version != null) {
494 | state.version++;
495 | }
496 | state.processed = now;
497 | }
498 |
499 | // The target cell id
500 | state.tcid = channelGrid.getCellIndex(state);
501 |
502 | // For newly created states (those created from inside the cell).
503 | if (state.ccid == null) {
504 | state.ccid = cellIndex;
505 | state.version = 1;
506 | }
507 | updateStateExternalTag(state, cellIndex);
508 |
509 | if (state.ccid == cellIndex) {
510 | var nearbyCellIndexes = channelGrid.getAllCellIndexes(state);
511 | nearbyCellIndexes.forEach(function (nearbyCellIndex) {
512 | if (!statesForNearbyCells[nearbyCellIndex]) {
513 | statesForNearbyCells[nearbyCellIndex] = [];
514 | }
515 | // No need for the cell to send states to itself.
516 | if (nearbyCellIndex != cellIndex) {
517 | statesForNearbyCells[nearbyCellIndex].push(state);
518 | }
519 | });
520 |
521 | if (state.tcid != cellIndex && swid) {
522 | if (!workerStateRefList[swid]) {
523 | workerStateRefList[swid] = [];
524 | }
525 | var stateRef = {
526 | id: state.id,
527 | swid: state.swid,
528 | tcid: state.tcid,
529 | type: state.type
530 | };
531 |
532 | if (state.delete) {
533 | stateRef.delete = state.delete;
534 | }
535 | workerStateRefList[swid].push(stateRef);
536 | }
537 | }
538 |
539 | if (state.delete) {
540 | if (!cellPendingDeletes[cellIndex][type]) {
541 | cellPendingDeletes[cellIndex][type] = {};
542 | }
543 | cellPendingDeletes[cellIndex][type][id] = state;
544 | delete currentCellData[type][id];
545 | }
546 | if (now - state.processed > WORLD_STALE_TIMEOUT) {
547 | delete currentCellData[type][id];
548 | }
549 | });
550 |
551 | var workerCellTransferIds = Object.keys(workerStateRefList);
552 | workerCellTransferIds.forEach(function (swid) {
553 | scServer.exchange.publish('internal/input-cell-transition/' + swid, workerStateRefList[swid]);
554 | });
555 |
556 | // Pass states off to adjacent cells as they move across grid cells.
557 | var allNearbyCellIndexes = Object.keys(statesForNearbyCells);
558 | allNearbyCellIndexes.forEach(function (nearbyCellIndex) {
559 | channelGrid.publishToCells(CHANNEL_CELL_TRANSITION, statesForNearbyCells[nearbyCellIndex], [nearbyCellIndex]);
560 | });
561 | }
562 |
563 | // Receive states which are in other cells and *may* transition to this cell later.
564 | // We don't manage these states, we just keep a copy so that they are visible
565 | // inside our cellController (cell.js) - This allows states to interact across
566 | // cell partitions (which may be hosted on a different process/CPU core).
567 | function gridCellTransitionHandler(cellIndex, stateList) {
568 | var currentCellData = cellData[cellIndex];
569 |
570 | stateList.forEach(function (state) {
571 | var type = state.type;
572 | var id = state.id;
573 |
574 | if (!currentCellData[type]) {
575 | currentCellData[type] = {};
576 | }
577 | var existingState = currentCellData[type][id];
578 |
579 | if (!existingState || state.version > existingState.version) {
580 | // Do not overwrite a state which is in the middle of
581 | // being synchronized with a different cell.
582 | if (state.tcid == cellIndex) {
583 | // This is a full transition to our current cell.
584 | state.ccid = cellIndex;
585 | currentCellData[type][id] = state;
586 | } else {
587 | // This is just external state for us to track but not
588 | // a complete transition, the state will still be managed by
589 | // a different cell.
590 | currentCellData[type][id] = state;
591 | }
592 | updateStateExternalTag(state, cellIndex);
593 | }
594 |
595 | existingState = currentCellData[type][id];
596 | existingState.processed = Date.now();
597 | });
598 | }
599 |
600 | // Here we handle and prepare data for a single cell within our game grid to be
601 | // processed by our cell controller.
602 | function gridCellDataHandler(cellIndex, stateList) {
603 | var currentCellData = cellData[cellIndex];
604 |
605 | stateList.forEach(function (stateRef) {
606 | var id = stateRef.id;
607 | var type = stateRef.type;
608 |
609 | if (!currentCellData[type]) {
610 | currentCellData[type] = {};
611 | }
612 |
613 | if (!currentCellData[type][id]) {
614 | var state;
615 | if (stateRef.create) {
616 | // If it is a stateRef, we get the state from the create property.
617 | state = stateRef.create;
618 | } else if (stateRef.x != null && stateRef.y != null) {
619 | // If we have x and y properties, then we know that
620 | // this is a full state already (probably created directly inside the cell).
621 | state = stateRef;
622 | } else {
623 | throw new Error('Received an invalid state reference');
624 | }
625 | state.ccid = cellIndex;
626 | state.version = 1;
627 | currentCellData[type][id] = state;
628 | }
629 | var cachedState = currentCellData[type][id];
630 | if (cachedState) {
631 | if (stateRef.op) {
632 | cachedState.op = stateRef.op;
633 | }
634 | if (stateRef.delete) {
635 | cachedState.delete = stateRef.delete;
636 | }
637 | if (stateRef.data) {
638 | cachedState.data = stateRef.data;
639 | }
640 | cachedState.tcid = channelGrid.getCellIndex(cachedState);
641 | updateStateExternalTag(cachedState, cellIndex);
642 | cachedState.processed = Date.now();
643 | }
644 | });
645 | }
646 |
647 | scServer.exchange.subscribe('internal/input-cell-transition/' + serverWorkerId)
648 | .watch(function (stateList) {
649 | stateList.forEach(function (state) {
650 | game.stateRefs[state.id] = state;
651 | });
652 | });
653 |
654 | // This is the main input loop which feeds states into various cells
655 | // based on their (x, y) coordinates.
656 | function processInputStates() {
657 | var stateList = [];
658 | var stateIds = Object.keys(game.stateRefs);
659 |
660 | stateIds.forEach(function (id) {
661 | var state = game.stateRefs[id];
662 | // Don't include bots.
663 | stateList.push(state);
664 | });
665 |
666 | // Publish to internal channels for processing (e.g. Collision
667 | // detection and resolution, scoring, etc...)
668 | // These states will be processed by a cell controllers depending
669 | // on each state's target cell index (tcid) within the world grid.
670 | var gridPublishOptions = {
671 | cellIndexesFactory: function (state) {
672 | return [state.tcid];
673 | }
674 | };
675 | channelGrid.publish(CHANNEL_INBOUND_CELL_PROCESSING, stateList, gridPublishOptions);
676 |
677 | stateList.forEach(function (state) {
678 | if (state.op) {
679 | delete state.op;
680 | }
681 | if (state.delete) {
682 | delete game.stateRefs[state.id];
683 | }
684 | });
685 | }
686 |
687 | setInterval(processInputStates, WORLD_UPDATE_INTERVAL);
688 |
689 | /*
690 | In here we handle our incoming realtime connections and listen for events.
691 | */
692 | scServer.on('connection', function (socket) {
693 |
694 | socket.on('getWorldInfo', function (data, respond) {
695 | // The first argument to respond can optionally be an Error object.
696 | respond(null, {
697 | width: WORLD_WIDTH,
698 | height: WORLD_HEIGHT,
699 | cols: WORLD_COLS,
700 | rows: WORLD_ROWS,
701 | cellWidth: WORLD_CELL_WIDTH,
702 | cellHeight: WORLD_CELL_HEIGHT,
703 | cellOverlapDistance: WORLD_CELL_OVERLAP_DISTANCE,
704 | serverWorkerId: serverWorkerId,
705 | environment: environment
706 | });
707 | });
708 |
709 | socket.on('join', function (playerOptions, respond) {
710 | var startingPos = getRandomPosition(PLAYER_DIAMETER, PLAYER_DIAMETER);
711 | var player = {
712 | id: uuid.v4(),
713 | type: 'player',
714 | swid: serverWorkerId,
715 | name: playerOptions.name,
716 | x: startingPos.x,
717 | y: startingPos.y,
718 | diam: PLAYER_DIAMETER,
719 | mass: PLAYER_MASS,
720 | score: 0
721 | };
722 |
723 | socket.player = stateManager.create(player);
724 |
725 | respond(null, player);
726 | });
727 |
728 | socket.on('action', function (playerOp) {
729 | if (socket.player) {
730 | stateManager.update(socket.player, playerOp);
731 | }
732 | });
733 |
734 | socket.on('disconnect', function () {
735 | if (socket.player) {
736 | stateManager.delete(socket.player);
737 | }
738 | });
739 | });
740 | };
741 |
--------------------------------------------------------------------------------