├── .nvmrc ├── .eslintignore ├── .istanbul.yml ├── .gitattributes ├── bench ├── fixtures │ ├── jsonmsg-out-ping.json │ ├── amfmsg-in-move_xy.bin │ ├── jsonmsg-out-map_get.json │ ├── amfmsg-in-edit_location.bin │ ├── amfmsg-out-login_start.bin │ ├── jsonmsg-out-login_start.json │ ├── amfmsg-out-location_lock_request.bin │ ├── IHFK8C8NB6J2FJ5.json │ └── jsonmsg-out-login_end.json ├── suites │ ├── utils.js │ ├── data │ │ ├── RequestContext.js │ │ ├── objrefProxy.js │ │ └── rpc.js │ ├── model │ │ ├── GameObject.js │ │ └── gsjsBridge.js │ ├── amf.js │ └── gsjs.js ├── setup.js └── runner.js ├── test ├── mocha.opts ├── func │ ├── fixtures │ │ ├── LCR177QO65T1EON.json │ │ ├── I00000000000002.json │ │ ├── IHFK8C8NB6J2FJ5.json │ │ ├── LLI32G3NUTD100I.json │ │ ├── P00000000000001.json │ │ └── P00000000000002.json │ ├── logging.js │ ├── model │ │ ├── DataContainer.js │ │ ├── Geo.js │ │ ├── Group.js │ │ ├── GameObject.js │ │ └── Quest.js │ └── data │ │ ├── RequestQueue.js │ │ └── rpcApi.js ├── mock │ ├── gsjsBridge.js │ ├── rpc.js │ ├── RequestContext.js │ ├── pers.js │ └── pbe.js ├── .eslintrc ├── helpers.js ├── unit │ ├── model │ │ ├── ItemMovement.js │ │ ├── IdObjRefMap.js │ │ ├── globalApi.js │ │ ├── gsjsBridge.js │ │ ├── OrderedHash.js │ │ └── Property.js │ ├── logging.js │ ├── data │ │ ├── rpcApi.js │ │ ├── rpcProxy.js │ │ ├── RequestContext.js │ │ └── RequestQueue.js │ ├── comm │ │ ├── abe │ │ │ └── hmac.js │ │ ├── slackNotify.js │ │ ├── slackChat.js │ │ └── sessionMgr.js │ └── fixtures │ │ └── PLI16FSFK2I91.json ├── setup.js └── int │ └── data │ └── pbe │ └── rethink.js ├── god └── customize │ ├── npc_shrine.js │ ├── geo.js │ ├── street_spirit_zutto.js │ ├── npc.js │ ├── cteb.js │ ├── street_spirit_firebog.js │ ├── home.js │ ├── town.js │ ├── quoin.js │ └── street_spirit.js ├── .gitignore ├── deploy ├── eleven-server.init-defaults.VAGRANT ├── deploy-remote.sh ├── eleven-server.service ├── eleven-server.service.VAGRANT ├── deploy.sh └── eleven-server.init ├── jsdoc-conf.json ├── .gitlab-ci.yml ├── src ├── comm │ ├── abe │ │ ├── passthrough.js │ │ └── hmac.js │ ├── amfServer.js │ ├── policyServer.js │ ├── auth.js │ ├── slackNotify.js │ ├── replServer.js │ └── sessionMgr.js ├── errors.js ├── model │ ├── PropertyApi.js │ ├── Quest.js │ ├── BagApi.js │ ├── IdObjRefMap.js │ ├── DataContainer.js │ ├── Group.js │ ├── OrderedHash.js │ └── Property.js ├── data │ ├── rpcProxy.js │ └── pbe │ │ └── rethink.js └── worker.js ├── .travis.yml ├── LICENSE ├── tools └── repl-client.js ├── config_local.js.SAMPLE_VAGRANT ├── config_local.js.SAMPLE_PROD ├── package.json ├── config_base.js ├── README.md ├── CONTRIBUTING.md └── .eslintrc /.nvmrc: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | src/gsjs/* 3 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | excludes: ['src/gsjs/**'] 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh eol=lf 2 | *.nvmrc eol=lf 3 | node_modules/** -text 4 | -------------------------------------------------------------------------------- /bench/fixtures/jsonmsg-out-ping.json: -------------------------------------------------------------------------------- 1 | {"type":"ping","success":true,"ts":1411415614} -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/setup 2 | --reporter spec 3 | --ui tdd 4 | --recursive 5 | -------------------------------------------------------------------------------- /bench/fixtures/amfmsg-in-move_xy.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElevenGiants/eleven-server/HEAD/bench/fixtures/amfmsg-in-move_xy.bin -------------------------------------------------------------------------------- /bench/fixtures/jsonmsg-out-map_get.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElevenGiants/eleven-server/HEAD/bench/fixtures/jsonmsg-out-map_get.json -------------------------------------------------------------------------------- /god/customize/npc_shrine.js: -------------------------------------------------------------------------------- 1 | exports.optionsOverride = { 2 | fields: { 3 | giant_tips: { 4 | hidden: true, 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /bench/fixtures/amfmsg-in-edit_location.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElevenGiants/eleven-server/HEAD/bench/fixtures/amfmsg-in-edit_location.bin -------------------------------------------------------------------------------- /bench/fixtures/amfmsg-out-login_start.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElevenGiants/eleven-server/HEAD/bench/fixtures/amfmsg-out-login_start.bin -------------------------------------------------------------------------------- /bench/fixtures/jsonmsg-out-login_start.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElevenGiants/eleven-server/HEAD/bench/fixtures/jsonmsg-out-login_start.json -------------------------------------------------------------------------------- /god/customize/geo.js: -------------------------------------------------------------------------------- 1 | exports.schemaOrder = { 2 | _self: ['l', 'r', 't', 'b', 'ground_y', 'rookable_type', 'swf_file', 'swf_file_versioned'], 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config_local.js 2 | /log 3 | /docs 4 | /coverage 5 | /src/gsjs 6 | /node_modules 7 | npm-debug.log 8 | builderror.log 9 | .coveralls.yml 10 | -------------------------------------------------------------------------------- /test/func/fixtures/LCR177QO65T1EON.json: -------------------------------------------------------------------------------- 1 | { 2 | "class_tsid": "town", 3 | "label": "Gregarious Grange Subway Station", 4 | "tsid": "LCR177QO65T1EON" 5 | } -------------------------------------------------------------------------------- /bench/fixtures/amfmsg-out-location_lock_request.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElevenGiants/eleven-server/HEAD/bench/fixtures/amfmsg-out-location_lock_request.bin -------------------------------------------------------------------------------- /deploy/eleven-server.init-defaults.VAGRANT: -------------------------------------------------------------------------------- 1 | NODE_USER=vagrant 2 | NODE_ARGS="--expose_gc --max_old_space_size=300" 3 | APP_DIR="/vagrant/eleven-server" 4 | LOG_DIR="/vagrant/eleven-server/log" 5 | VERBOSE=yes 6 | -------------------------------------------------------------------------------- /bench/suites/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../setup'); 4 | var suite = new (require('benchmark')).Suite; 5 | module.exports = suite; 6 | 7 | 8 | var utils = require('utils'); 9 | 10 | 11 | suite.add('makeTsid', function() { 12 | utils.makeTsid('X', 'gs01-01'); 13 | }); 14 | -------------------------------------------------------------------------------- /god/customize/street_spirit_zutto.js: -------------------------------------------------------------------------------- 1 | var street_spirit_firebog = require('./street_spirit_firebog'); 2 | 3 | // same as firebogs - hide the groddle spirits' appearance props 4 | exports.schemaOverride = street_spirit_firebog.schemaOverride; 5 | exports.optionsOverride = street_spirit_firebog.optionsOverride; 6 | -------------------------------------------------------------------------------- /jsdoc-conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "exclude": [ 4 | "src/gsjs" 5 | ] 6 | }, 7 | "opts": { 8 | "recurse": true 9 | }, 10 | "plugins": [ 11 | "plugins/markdown" 12 | ], 13 | "markdown": { 14 | "parser": "marked", 15 | "tags": ["exceptions"] 16 | }, 17 | "templates": { 18 | "cleverLinks": true 19 | } 20 | } -------------------------------------------------------------------------------- /bench/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bunyan = require('bunyan'); 4 | 5 | 6 | initGlobals(); 7 | 8 | 9 | function initGlobals() { 10 | global.log = bunyan.createLogger({ 11 | name: 'benchlog', 12 | src: true, 13 | streams: [ 14 | { 15 | level: 'error', 16 | stream: process.stderr, 17 | }, 18 | ], 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/mock/gsjsBridge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gsjsBridge = require('model/gsjsBridge'); 4 | 5 | 6 | // public interface 7 | module.exports = { 8 | create: create, 9 | isTsid: gsjsBridge.isTsid, 10 | }; 11 | 12 | 13 | function create(data, modelType) { 14 | if (modelType) { 15 | return new modelType(data); 16 | } 17 | return data; 18 | } 19 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:12.14.0 2 | 3 | services: 4 | - gcc:4.8 5 | 6 | cache: {} 7 | 8 | before_script: 9 | - cd .. 10 | - git clone https://gitlab.com/ElevenGiants/eleven-gsjs.git 11 | - cd eleven-server 12 | - npm run preproc 13 | - npm install 14 | 15 | build: 16 | script: 17 | - npm -s run test 18 | - npm -s run functest 19 | - npm -s run lint 20 | -------------------------------------------------------------------------------- /deploy/deploy-remote.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # deployment script executed on the remote/target host by deploy.sh (via SSH) 4 | 5 | set -xe 6 | 7 | rm -rf /eleven/eleven-server.old 8 | [[ ! -d /eleven/eleven-server ]] || mv /eleven/eleven-server /eleven/eleven-server.old 9 | mv /eleven/eleven-server.new /eleven/eleven-server 10 | sudo /bin/systemctl restart eleven-server 11 | -------------------------------------------------------------------------------- /test/func/fixtures/I00000000000002.json: -------------------------------------------------------------------------------- 1 | { 2 | "class_tsid": "apple", 3 | "container": { 4 | "label": "Beta", 5 | "objref": true, 6 | "tsid": "P00000000000002" 7 | }, 8 | "count": 1, 9 | "label": "Apple", 10 | "pcont": "P00000000000002", 11 | "state": "iconic", 12 | "tcont": "P00000000000002", 13 | "ts": 1355112616233, 14 | "tsid": "I00000000000002", 15 | "version": "1355086256", 16 | "x": 0, 17 | "y": 1 18 | } 19 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "assert": true, 4 | "setup": true, 5 | "suite": true, 6 | "suiteSetup": true, 7 | "suiteTeardown": true, 8 | "teardown": true, 9 | "test": true 10 | }, 11 | "rules": { 12 | "complexity": [1, 15], 13 | "func-names": 0, 14 | "max-len": 0, 15 | "max-nested-callbacks": [1, 5], 16 | "no-underscore-dangle": [1, "allow": ["super_", "__proxyTarget", "__isGO", "__isORP", "__isRP", "__get__", "__set__"]] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/comm/abe/passthrough.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Dummy authentication module that just uses player TSIDs as tokens. 5 | * Only to be used for development/testing, obviously. 6 | * 7 | * @module 8 | */ 9 | 10 | // public interface 11 | module.exports = { 12 | authenticate: authenticate, 13 | getToken: getToken, 14 | getTokenLifespan: getTokenLifespan, 15 | }; 16 | 17 | 18 | function authenticate(token) { 19 | return token; 20 | } 21 | 22 | 23 | function getToken(player) { 24 | return player.tsid; 25 | } 26 | 27 | 28 | function getTokenLifespan() { 29 | return 0; 30 | } 31 | -------------------------------------------------------------------------------- /deploy/eleven-server.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Eleven Giants Game Server 3 | Requires=rethinkdb@eleven.service 4 | After=rethinkdb@eleven.service 5 | 6 | [Service] 7 | WorkingDirectory=/eleven/eleven-server 8 | ExecStart=/usr/bin/node --expose-gc ./src/server.js 9 | Type=simple 10 | Restart=always 11 | RestartSec=1s 12 | KillMode=mixed 13 | TimeoutStopSec=180 14 | StandardOutput=syslog 15 | StandardError=syslog 16 | SyslogIdentifier=eleven-server 17 | User=eleven 18 | Group=eleven 19 | Environment=NODE_ENV=production 20 | Environment=NODE_PATH=src 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /deploy/eleven-server.service.VAGRANT: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Eleven Giants Game Server 3 | Requires=rethinkdb@eleven.service 4 | After=rethinkdb@eleven.service 5 | 6 | [Service] 7 | WorkingDirectory=/vagrant/eleven-server 8 | ExecStart=/usr/bin/node --expose-gc ./src/server.js 9 | Type=simple 10 | Restart=always 11 | RestartSec=1s 12 | KillMode=mixed 13 | TimeoutStopSec=60 14 | StandardOutput=syslog 15 | StandardError=syslog 16 | SyslogIdentifier=eleven-server 17 | User=vagrant 18 | Group=vagrant 19 | Environment=NODE_ENV=production 20 | Environment=NODE_PATH=src 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /god/customize/npc.js: -------------------------------------------------------------------------------- 1 | exports.optionsOverride = { 2 | fields: { 3 | state_stack: { 4 | hidden: true, 5 | }, 6 | waitingFor: { 7 | hidden: true, 8 | }, 9 | available_quests: { 10 | hidden: true, 11 | }, 12 | npc_can_climb: { 13 | hidden: true, 14 | }, 15 | npc_can_fall: { 16 | hidden: true, 17 | }, 18 | npc_can_jump: { 19 | hidden: true, 20 | }, 21 | npc_can_walk: { 22 | hidden: true, 23 | }, 24 | npc_climb_speed: { 25 | hidden: true, 26 | }, 27 | npc_jump_height: { 28 | hidden: true, 29 | }, 30 | npc_walk_speed: { 31 | hidden: true, 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /bench/suites/data/RequestContext.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../setup'); 4 | var suite = new (require('benchmark')).Suite; 5 | module.exports = suite; 6 | var RequestContext = require('data/RequestContext'); 7 | 8 | 9 | suite.add('run (fire&forget)', function() { 10 | new RequestContext().run( 11 | function req() { 12 | }, 13 | function cb(err) { 14 | if (err) throw err; 15 | } 16 | ); 17 | }); 18 | 19 | 20 | suite.add('run (sequential/wait for result)', function(deferred) { 21 | new RequestContext().run( 22 | function req() { 23 | }, 24 | function cb(err) { 25 | if (err) throw err; 26 | deferred.resolve(); 27 | } 28 | ); 29 | }, {defer: true}); 30 | -------------------------------------------------------------------------------- /bench/fixtures/IHFK8C8NB6J2FJ5.json: -------------------------------------------------------------------------------- 1 | { 2 | "class_tsid": "quoin", 3 | "container": { 4 | "label": "Pansanu Insanu", 5 | "objref": true, 6 | "tsid": "LHFF2N2M36J2MG3" 7 | }, 8 | "count": 1, 9 | "instanceProps": { 10 | "benefit_ceil": 6, 11 | "benefit_floor": 2, 12 | "class_name": "small random currants", 13 | "is_random": "1", 14 | "respawn_time": 180, 15 | "type": "currants" 16 | }, 17 | "label": "Coin", 18 | "spawned": 1, 19 | "state": 1, 20 | "tcont": "LHFF2N2M36J2MG3", 21 | "ts": 1390339339072, 22 | "tsid": "IHFK8C8NB6J2FJ5", 23 | "version": "1351476850", 24 | "x": 376, 25 | "y": -484 26 | } -------------------------------------------------------------------------------- /test/func/fixtures/IHFK8C8NB6J2FJ5.json: -------------------------------------------------------------------------------- 1 | { 2 | "class_tsid": "quoin", 3 | "container": { 4 | "label": "Pansanu Insanu", 5 | "objref": true, 6 | "tsid": "LHFF2N2M36J2MG3" 7 | }, 8 | "count": 1, 9 | "instanceProps": { 10 | "benefit_ceil": 6, 11 | "benefit_floor": 2, 12 | "class_name": "small random currants", 13 | "is_random": "1", 14 | "respawn_time": 180, 15 | "type": "currants" 16 | }, 17 | "label": "Coin", 18 | "spawned": 1, 19 | "state": 1, 20 | "tcont": "LHFF2N2M36J2MG3", 21 | "ts": 1390339339072, 22 | "tsid": "IHFK8C8NB6J2FJ5", 23 | "version": "1351476850", 24 | "x": 376, 25 | "y": -484 26 | } -------------------------------------------------------------------------------- /deploy/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # deployment script executed on the build host (by Jenkins) 4 | 5 | set -xe 6 | 7 | src_dir=${WORKSPACE}/eleven-server 8 | 9 | # check if source directory exists 10 | if [[ ! -d "$src_dir" ]]; then 11 | echo "source directory not found: $src_dir" 12 | exit 1 13 | fi 14 | 15 | # copy everything over to target host 16 | rsync ${DRY_RUN:+--dry-run} --compress --recursive --exclude=".git*" -e "ssh -p ${SSH_PORT}" "${src_dir}/" ${SSH_USER}@${SSH_HOST}:/eleven/eleven-server.new 17 | 18 | # run remote deployment script (moves old version out of the way, replaces it 19 | # with new one and restarts GS service) 20 | ssh -T -p ${SSH_PORT} ${SSH_USER}@${SSH_HOST} /eleven/eleven-server.new/deploy/deploy-remote.sh 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 12 5 | 6 | env: 7 | - CXX=g++-4.8 8 | 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-4.8 15 | 16 | cache: false 17 | 18 | before_script: 19 | - cd .. 20 | - git clone https://github.com/ElevenGiants/eleven-gsjs.git 21 | - cd eleven-server 22 | - npm run preproc 23 | 24 | script: 25 | - npm -s run test 26 | - npm -s run functest 27 | - npm -s run lint 28 | 29 | after_script: 30 | - npm -s run coveralls 31 | 32 | notifications: 33 | email: false 34 | slack: 35 | secure: kVNLsevaDEQrNAZ64C4HdK9qd0aka73WoDzb1vWcaIBqZQ2H+V6U20l0m3tDsBf9kLfrit2+gDJuPdYW672l2m6mVCDBUk74UW99wraOeuE2hIbHGEBTe/eF6QjPUlBt8B6usOJ3grxjQIiitV81aDGnf5usEjLPQBM1Zcp6mqY= 36 | -------------------------------------------------------------------------------- /test/mock/rpc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // public interface 4 | module.exports = { 5 | reset: reset, 6 | isLocal: isLocal, 7 | makeProxy: makeProxy, 8 | sendObjRequest: sendObjRequest, 9 | getRequests: getRequests, 10 | getGsid: getGsid, 11 | }; 12 | 13 | 14 | var local = true; 15 | var requests = []; 16 | 17 | 18 | function reset(loc) { 19 | local = !!loc; 20 | requests = []; 21 | } 22 | 23 | 24 | function isLocal(obj) { 25 | return local; 26 | } 27 | 28 | 29 | function makeProxy(obj) { 30 | obj.__isRP = true; 31 | return obj; 32 | } 33 | 34 | 35 | function sendObjRequest(obj, fname, args) { 36 | requests.push({obj: obj, fname: fname, args: args}); 37 | return obj[fname].apply(obj, args); 38 | } 39 | 40 | 41 | function getRequests() { 42 | return requests; 43 | } 44 | 45 | 46 | function getGsid() { 47 | return 'gs01-01'; 48 | } 49 | -------------------------------------------------------------------------------- /god/customize/cteb.js: -------------------------------------------------------------------------------- 1 | var pers = require('../../persistence/pers'); 2 | 3 | 4 | exports.customizeSchema = function(obj, schema) { 5 | var loc = pers.get(obj.tcont); 6 | var locEvents = Object.keys(loc.events); 7 | for (var k in schema.properties.instanceProps.properties) { 8 | if (k === 'onEnter' || k === 'onExit' || k === 'onTimer') { 9 | schema.properties.instanceProps.properties[k]['enum'] = locEvents; 10 | } 11 | } 12 | return schema; 13 | }; 14 | 15 | 16 | exports.customizeOptions = function(obj, options) { 17 | for (var k in obj.instanceProps) { 18 | // make event choice fields select boxes (radio buttons/checkboxes 19 | // don't reliably show the current value) 20 | if (k === 'onEnter' || k === 'onExit' || k === 'onTimer' || k === 'timer_fire') { 21 | options.fields.instanceProps.fields[k] = { 22 | type: 'select', 23 | }; 24 | } 25 | } 26 | return options; 27 | }; 28 | -------------------------------------------------------------------------------- /bench/suites/data/objrefProxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../setup'); 4 | var suite = new (require('benchmark')).Suite; 5 | module.exports = suite; 6 | var orproxy = require('data/objrefProxy'); 7 | 8 | 9 | function getSampleObj() { 10 | return { 11 | a: { 12 | aa: { 13 | objref: true, 14 | tsid: 'IAA', 15 | label: 'blah', 16 | }, 17 | ab: { 18 | objref: true, 19 | tsid: 'IAB', 20 | label: 'blah', 21 | }, 22 | }, 23 | b: { 24 | ba: { 25 | noObjref: true, 26 | }, 27 | bb: { 28 | objref: true, 29 | tsid: 'IBB', 30 | label: 'blah', 31 | } 32 | }, 33 | }; 34 | }; 35 | 36 | var proxiedSampleObj = getSampleObj(); 37 | orproxy.proxify(proxiedSampleObj); 38 | 39 | 40 | suite.add('proxify', function() { 41 | orproxy.proxify(getSampleObj()); 42 | }, { 43 | //minTime: 50, 44 | }); 45 | 46 | suite.add('refify', function() { 47 | orproxy.refify(proxiedSampleObj); 48 | }); 49 | -------------------------------------------------------------------------------- /src/comm/amfServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Accepts TCP connections from game clients and binds them to 5 | * {@link Session} instances. 6 | * 7 | * @module 8 | */ 9 | 10 | // public interface 11 | module.exports = { 12 | start: start, 13 | close: close, 14 | }; 15 | 16 | 17 | var net = require('net'); 18 | var WebSocket = require('ws'); 19 | var config = require('config'); 20 | var sessionMgr = require('comm/sessionMgr'); 21 | 22 | var server; 23 | 24 | 25 | function start() { 26 | sessionMgr.init(); 27 | var gsconf = config.getGSConf(); 28 | server = new WebSocket.Server({port: gsconf.port}); 29 | server.on('listening', function onListening() { 30 | log.info('%s ready (pid=%s)', config.getGsid(), process.pid); 31 | }); 32 | server.on('connection', handleConnect); 33 | } 34 | 35 | 36 | function close(callback) { 37 | log.info('WS server shutdown'); 38 | server.close(callback); 39 | sessionMgr.shutdown(); 40 | } 41 | 42 | 43 | function handleConnect(socket, req) { 44 | sessionMgr.newSession(socket, req); 45 | } 46 | -------------------------------------------------------------------------------- /bench/suites/model/GameObject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../setup'); 4 | var suite = new (require('benchmark')).Suite; 5 | module.exports = suite; 6 | var fs = require('fs'); 7 | var GameObject = require('model/GameObject'); 8 | var gsjsBridge = require('model/gsjsBridge'); 9 | 10 | 11 | var f = fs.readFileSync('bench/fixtures/PUVF8UK15083AI1XXX.json'); 12 | var data = JSON.parse(f); 13 | var go = new GameObject(data); 14 | gsjsBridge.reset(); 15 | var proto = gsjsBridge.getProto('players', 'human'); 16 | var goWithProto = gsjsBridge.create(data); 17 | 18 | 19 | suite.add('GameObject instantiation (no parent prototypes)', function() { 20 | new GameObject(data); 21 | }); 22 | 23 | suite.add('GameObject serialization (no parent prototypes)', function() { 24 | go.serialize(); 25 | }); 26 | 27 | suite.add('Player instantiation (with full prototype hierarchy)', function() { 28 | new proto.constructor(data); 29 | }); 30 | 31 | suite.add('Player serialization (with full prototype hierarchy)', function() { 32 | goWithProto.serialize(); 33 | }); 34 | -------------------------------------------------------------------------------- /test/func/logging.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rewire = require('rewire'); 4 | var logging = rewire('logging'); 5 | var RC = require('data/RequestContext'); 6 | 7 | 8 | suite('logging', function () { 9 | 10 | var origLogger; 11 | 12 | setup(function () { 13 | origLogger = logging.__get__('logger'); 14 | logging.__set__('logger', log); 15 | }); 16 | 17 | teardown(function () { 18 | logging.__set__('logger', origLogger); 19 | }); 20 | 21 | 22 | suite('custom log emitter', function () { 23 | 24 | test('does not modify passed data object', function (done) { 25 | var data = {some: 'data'}; 26 | var emitter = function () { 27 | if (!arguments.length) return true; // just to fool log level test 28 | assert.deepEqual(data, {some: 'data'}); 29 | done(); 30 | }; 31 | var wrapLogEmitter = logging.__get__('wrapLogEmitter'); 32 | var wrappedEmitter = wrapLogEmitter(emitter); 33 | new RC().run(function () { 34 | wrappedEmitter(data, 'foozux'); 35 | }, function cb(err) { 36 | if (err) return done(err); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /god/customize/street_spirit_firebog.js: -------------------------------------------------------------------------------- 1 | exports.schemaOverride = { 2 | properties: { 3 | instanceProps: { 4 | properties: { 5 | skull: { 6 | 'default': '', 7 | required: false, 8 | }, 9 | eyes: { 10 | 'default': '', 11 | required: false, 12 | }, 13 | top: { 14 | 'default': '', 15 | required: false, 16 | }, 17 | bottom: { 18 | 'default': '', 19 | required: false, 20 | }, 21 | base: { 22 | 'default': '', 23 | required: false, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }; 29 | 30 | 31 | exports.optionsOverride = { 32 | fields: { 33 | instanceProps: { 34 | fields: { 35 | skull: { 36 | removeDefaultNone: false, 37 | hidden: true, 38 | }, 39 | eyes: { 40 | removeDefaultNone: false, 41 | hidden: true, 42 | }, 43 | top: { 44 | removeDefaultNone: false, 45 | hidden: true, 46 | }, 47 | bottom: { 48 | removeDefaultNone: false, 49 | hidden: true, 50 | }, 51 | base: { 52 | removeDefaultNone: false, 53 | hidden: true, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 The Eleven Project Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bench/suites/model/gsjsBridge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../setup'); 4 | var suite = new (require('benchmark')).Suite; 5 | module.exports = suite; 6 | var fs = require('fs'); 7 | var gsjsBridge = require('model/gsjsBridge'); 8 | var config = require('config'); 9 | 10 | 11 | var data = JSON.parse(fs.readFileSync('bench/fixtures/PUVF8UK15083AI1XXX.json')); 12 | 13 | 14 | suite.asyncSetup = function(done) { 15 | config.init(true, { 16 | gsjs: { 17 | config: 'config_prod', 18 | }, 19 | }); 20 | gsjsBridge.init(false, done); 21 | }; 22 | 23 | 24 | suite.add('loadProto (cached)', function() { 25 | gsjsBridge.getProto('items', 'apple'); 26 | }); 27 | 28 | suite.add('createFromData (cached)', function() { 29 | gsjsBridge.create(data); 30 | }); 31 | 32 | 33 | suite.add('loadProto (uncached)', function() { 34 | gsjsBridge.getProto('items', 'apple'); 35 | gsjsBridge.reset(); 36 | }, { 37 | onStart: function onStart() { 38 | gsjsBridge.reset(); 39 | }, 40 | }); 41 | 42 | 43 | suite.add('createFromData (uncached)', function() { 44 | gsjsBridge.create(data); 45 | gsjsBridge.reset(); 46 | }, { 47 | onStart: function onStart() { 48 | gsjsBridge.reset(); 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /god/customize/home.js: -------------------------------------------------------------------------------- 1 | exports.optionsOverride = { 2 | fields: { 3 | players: {hidden: true}, 4 | items: {hidden: true}, 5 | action_requests: {hidden: true}, 6 | delayed_sounds: {hidden: true}, 7 | emotes: {hidden: true}, 8 | greeters_summoned: {hidden: true}, 9 | hi_sign_daily_evasion_record: {hidden: true}, 10 | hi_sign_evasion_record: {hidden: true}, 11 | hi_sign_evasion_record_history: {hidden: true}, 12 | incantations: {hidden: true}, 13 | incantations_redux: {hidden: true}, 14 | incantations_redux_step: {hidden: true}, 15 | jobs: {hidden: true}, 16 | jobs_is_locked: {hidden: true}, 17 | qurazy: {hidden: true}, 18 | streaking_increments: {hidden: true}, 19 | stun_orbs: {hidden: true}, 20 | rook_status: { 21 | lazyLoading: false, 22 | }, 23 | keys: { 24 | lazyLoading: false, 25 | }, 26 | class_tsid: {hidden: false}, 27 | label: {hidden: false}, 28 | }, 29 | } 30 | 31 | 32 | exports.schemaOrder = { 33 | _self: ['label', 'class_tsid', 'hubid', 'moteid', 'template', 'upgrade_template', 'upgrade_level', 'old_upgrade_tree', 'rook_status', 'keys', 'image', 'loading_image'], 34 | image: {_self: ['w', 'h', 'url']}, 35 | loading_image: {_self: ['w', 'h', 'url']}, 36 | } 37 | -------------------------------------------------------------------------------- /god/customize/town.js: -------------------------------------------------------------------------------- 1 | exports.optionsOverride = { 2 | fields: { 3 | players: {hidden: true}, 4 | items: {hidden: true}, 5 | action_requests: {hidden: true}, 6 | delayed_sounds: {hidden: true}, 7 | emotes: {hidden: true}, 8 | greeters_summoned: {hidden: true}, 9 | hi_sign_daily_evasion_record: {hidden: true}, 10 | hi_sign_evasion_record: {hidden: true}, 11 | hi_sign_evasion_record_history: {hidden: true}, 12 | incantations: {hidden: true}, 13 | incantations_redux: {hidden: true}, 14 | incantations_redux_step: {hidden: true}, 15 | jobs: {hidden: true}, 16 | jobs_is_locked: {hidden: true}, 17 | qurazy: {hidden: true}, 18 | streaking_increments: {hidden: true}, 19 | stun_orbs: {hidden: true}, 20 | rook_status: { 21 | lazyLoading: false, 22 | }, 23 | keys: { 24 | lazyLoading: false, 25 | }, 26 | class_tsid: {hidden: false}, 27 | label: {hidden: false}, 28 | }, 29 | } 30 | 31 | 32 | exports.schemaOrder = { 33 | _self: ['label', 'class_tsid', 'hubid', 'moteid', 'template', 'upgrade_template', 'upgrade_level', 'old_upgrade_tree', 'rook_status', 'keys', 'image', 'loading_image'], 34 | image: {_self: ['w', 'h', 'url']}, 35 | loading_image: {_self: ['w', 'h', 'url']}, 36 | } 37 | -------------------------------------------------------------------------------- /test/mock/RequestContext.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // public interface 4 | module.exports = { 5 | reset: reset, 6 | getContext: getContext, 7 | getDirtyList: getDirtyList, 8 | setDirty: setDirty, 9 | run: run, 10 | setUnload: setUnload, 11 | getUnloadList: getUnloadList, 12 | }; 13 | 14 | 15 | var wait = require('wait.for'); 16 | 17 | 18 | var cache = {}; 19 | var dirty = {}; 20 | var ulist = {}; 21 | 22 | 23 | function reset() { 24 | cache = {}; 25 | dirty = {}; 26 | ulist = {}; 27 | } 28 | 29 | 30 | function getContext() { 31 | return { 32 | cache: cache, 33 | setDirty: setDirty, 34 | }; 35 | } 36 | 37 | 38 | function getDirtyList() { 39 | return Object.keys(dirty); 40 | } 41 | 42 | 43 | function setDirty(obj) { 44 | dirty[obj.tsid] = obj; 45 | } 46 | 47 | 48 | function run(func, logtag, owner, callback) { 49 | wait.launchFiber(function persFiber() { 50 | try { 51 | var res = func(); 52 | if (callback) callback(null, res); 53 | } 54 | catch (e) { 55 | if (callback) callback(e); 56 | else throw e; 57 | } 58 | }); 59 | } 60 | 61 | 62 | function setUnload(obj) { 63 | ulist[obj.tsid] = obj; 64 | } 65 | 66 | 67 | function getUnloadList() { 68 | return ulist; 69 | } 70 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var events = require('events'); 5 | var Player = require('model/Player'); 6 | var Session = require('comm/Session'); 7 | 8 | 9 | exports.getDummySocket = function getDummySocket() { 10 | var ret = new events.EventEmitter(); 11 | ret.write = function write(data) { 12 | ret.emit('message', data); // simple echo 13 | }; 14 | ret.setNoDelay = _.noop; 15 | ret.destroy = _.noop; 16 | ret.end = _.noop; 17 | ret.readyState = 1; 18 | return ret; 19 | }; 20 | 21 | 22 | exports.getTestSession = function getTestSession(id, socket) { 23 | // creates a Session instance throwing errors (the regular error handler 24 | // obscures potential test errors, making debugging difficult) 25 | if (!socket) socket = exports.getDummySocket(); 26 | var ret = new Session(id, socket); 27 | ret.handleError = function (err) { 28 | throw err; 29 | }; 30 | ret.dom.on('error', ret.handleError.bind(ret)); 31 | return ret; 32 | }; 33 | 34 | 35 | exports.getOnlinePlayer = function getOnlinePlayer(data) { 36 | // create a "connected" player instance with a dummy session object 37 | var ret = new Player(data); 38 | ret.session = {send: _.noop}; 39 | return ret; 40 | }; 41 | -------------------------------------------------------------------------------- /bench/suites/amf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../setup'); 4 | var suite = new (require('benchmark')).Suite; 5 | module.exports = suite; 6 | 7 | 8 | var fs = require('fs'); 9 | var amf = { 10 | js: require('eleven-node-amf/node-amf/amf') 11 | }; 12 | 13 | 14 | // load test fixtures 15 | var amfData = {}; 16 | var amfDataStr = {}; 17 | [ 18 | 'in-move_xy', 19 | 'in-edit_location', 20 | 'out-location_lock_request', 21 | 'out-login_start', 22 | ].forEach(function iter(type) { 23 | amfData[type] = fs.readFileSync('bench/fixtures/amfmsg-' + type + '.bin'); 24 | amfDataStr[type] = amfData[type].toString('binary'); 25 | }); 26 | var jsonData = {}; 27 | [ 28 | 'out-ping', 29 | 'out-login_end', 30 | 'out-map_get', 31 | 'out-login_start', 32 | ].forEach(function iter(type) { 33 | jsonData[type] = JSON.parse(fs.readFileSync('bench/fixtures/jsonmsg-' + type + '.json')); 34 | }); 35 | 36 | 37 | Object.keys(amfData).forEach(function iter(type) { 38 | suite.add('amflib-js/deserialize ' + type, function () { 39 | var deser = amf.js.deserializer(amfDataStr[type]); 40 | deser.readValue(amf.js.AMF3); 41 | }); 42 | }); 43 | 44 | 45 | Object.keys(jsonData).forEach(function iter(type) { 46 | suite.add('amflib-js/serialize ' + type, function () { 47 | amf.js.serializer().writeObject(jsonData[type]); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/unit/model/ItemMovement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Geo = require('model/Geo'); 4 | var Item = require('model/Item'); 5 | var ItemMovement = require('model/ItemMovement'); 6 | 7 | 8 | suite('ItemMovement', function () { 9 | 10 | suite('buildPath', function () { 11 | 12 | function getTestGeo() { 13 | var geo = new Geo({l: -100, r: 100, t: -100, b: 0}); 14 | geo.layers.middleground.platform_lines = { 15 | plat1: { 16 | start: {x: -100, y: -10}, 17 | end: {x: 100, y: -10}, 18 | platform_item_perm: -1, 19 | platform_pc_perm: -1, 20 | }, 21 | }; 22 | return geo; 23 | } 24 | 25 | test('keeps path within geo limits for "kicked" transport', function () { 26 | var geo = getTestGeo(); 27 | var it = new Item(); 28 | it.container = {geometry: geo}; 29 | it.x = 90; 30 | it.y = -20; 31 | var im = new ItemMovement(it); 32 | im.options = {vx: 20, vy: -30}; 33 | var path = im.buildPath('kicked'); 34 | for (var i = 0; i < path.length; i++) { 35 | var segment = path[i]; 36 | assert.isTrue(segment.x >= geo.l && segment.x <= geo.r, 37 | 'path segment ends within horizontal geo boundaries'); 38 | assert.isTrue(segment.y >= geo.t && segment.y <= geo.b, 39 | 'path segment ends within vertical geo boundaries'); 40 | } 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tools/repl-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // essentially nicked from 4 | 5 | var net = require('net'); 6 | 7 | 8 | var args = process.argv; 9 | if (args.length < 3) { 10 | // eslint-disable-next-line no-console 11 | console.log('usage: %s %s ', args[0], args[1]); 12 | process.exit(1); 13 | } 14 | var socket = net.connect(args[2]); 15 | process.stdin.pipe(socket); 16 | 17 | 18 | process.stdin.on('data', function onData(buffer) { 19 | if (buffer.length === 1 && buffer[0] === 0x04) { // EOT 20 | process.stdin.emit('end'); // process.stdin will be destroyed 21 | process.stdin.setRawMode(false); 22 | process.stdin.pause(); // stop emitting 'data' event 23 | } 24 | }); 25 | 26 | 27 | // this event won't be fired if REPL is exited by '.exit' command 28 | process.stdin.on('end', function onEnd() { 29 | console.log('.exit');// eslint-disable-line no-console 30 | socket.destroy(); 31 | }); 32 | 33 | socket.pipe(process.stdout); 34 | 35 | 36 | socket.on('connect', function connect() { 37 | console.log('Connected.'); // eslint-disable-line no-console 38 | process.stdin.setRawMode(true); 39 | }); 40 | 41 | 42 | socket.on('close', function close() { 43 | console.log('Disconnected.'); // eslint-disable-line no-console 44 | socket.removeListener('close', close); 45 | }); 46 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bunyan = require('bunyan'); 4 | var chai = require('chai'); 5 | var config = require('config'); 6 | 7 | 8 | initGlobals(); 9 | initConfig(); 10 | 11 | 12 | function initGlobals() { 13 | global.assert = chai.assert; 14 | global.log = bunyan.createLogger({ 15 | name: 'testlog', 16 | src: true, 17 | streams: [ 18 | { 19 | level: 'fatal', 20 | stream: process.stderr, 21 | }, 22 | ], 23 | }); 24 | } 25 | 26 | 27 | function initConfig() { 28 | // minimal configuration just to enable tests 29 | config.init(false, { 30 | net: { 31 | gameServers: { 32 | gs01: { 33 | host: '127.0.0.1', 34 | ports: [1443], 35 | }, 36 | }, 37 | maxMsgSize: 131072, 38 | rpc: { 39 | timeout: 10000, 40 | }, 41 | amflib: 'js', 42 | }, 43 | pers: { 44 | backEnd: { 45 | config: { 46 | rethink: { 47 | dbname: 'eleven_test', 48 | dbtable: 'gamedata', 49 | dbhost: 'localhost', 50 | dbport: 28015, 51 | dbauth: 'test123', 52 | queryOpts: { 53 | durability: 'hard', 54 | noreply: false, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | gsjs: { 61 | config: 'config_prod', 62 | }, 63 | cache: { 64 | pathfinding: './test/pathfinding.json', 65 | }, 66 | }, { 67 | gsid: 'gs01-01', 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /god/customize/quoin.js: -------------------------------------------------------------------------------- 1 | exports.schemaOverride = { 2 | properties: { 3 | instanceProps: { 4 | properties: { 5 | is_random: { 6 | 'enum': ['0', '1'], 7 | 'default': '0', 8 | // hide the description (labels defined in custom options are descriptive) 9 | description: undefined, 10 | }, 11 | type: { 12 | description: undefined, // name is obvious enough 13 | }, 14 | }, 15 | }, 16 | }, 17 | } 18 | 19 | 20 | exports.optionsOverride = { 21 | fields: { 22 | instanceProps: { 23 | fields: { 24 | is_random: { 25 | optionLabels: { 26 | '0': 'no', 27 | '1': 'yes', 28 | } 29 | }, 30 | benefit: { 31 | hidden: true, 32 | }, 33 | benefit_ceil: { 34 | hidden: true, 35 | }, 36 | benefit_floor: { 37 | hidden: true, 38 | }, 39 | is_random: { 40 | hidden: true, 41 | }, 42 | location_event_id: { 43 | hidden: true, 44 | }, 45 | marker: { 46 | hidden: true, 47 | }, 48 | owner: { 49 | hidden: true, 50 | }, 51 | respawn_time: { 52 | hidden: true, 53 | }, 54 | uses_remaining: { 55 | hidden: true, 56 | }, 57 | }, 58 | }, 59 | state: { 60 | hidden: true, 61 | }, 62 | spawned: { 63 | hidden: true, 64 | }, 65 | only_visible_to: { 66 | hidden: true, 67 | }, 68 | isHidden: { 69 | hidden: true, 70 | }, 71 | }, 72 | } 73 | exports.schemaOrder = { 74 | instanceProps: {_self: ['class_name', 'type', 'giant']}, 75 | } 76 | -------------------------------------------------------------------------------- /test/unit/logging.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rewire = require('rewire'); 4 | var logging = rewire('logging'); 5 | 6 | 7 | suite('logging', function () { 8 | 9 | 10 | suite('logAction', function () { 11 | 12 | var origActionLogger; 13 | 14 | 15 | setup(function () { 16 | origActionLogger = logging.__get__('actionLogger'); 17 | }); 18 | 19 | teardown(function () { 20 | logging.__set__('actionLogger', origActionLogger); 21 | }); 22 | 23 | 24 | test('works as expected', function (done) { 25 | logging.__set__('actionLogger', { 26 | info: function info(fields, msg) { 27 | assert.strictEqual(msg, 'XYZ'); 28 | assert.deepEqual(fields, 29 | {action: 'XYZ', abc: '12', def: 'foo'}); 30 | done(); 31 | }, 32 | }); 33 | logging.logAction('XYZ', ['abc=12', 'def=foo']); 34 | }); 35 | 36 | test('handles improperly formatted fields gracefully', function (done) { 37 | logging.__set__('actionLogger', { 38 | info: function info(fields, msg) { 39 | assert.strictEqual(msg, 'meh'); 40 | assert.deepEqual(fields, {action: 'meh', 41 | 'UNKNOWN#0': 'barf', 'UNKNOWN#1': '123', 42 | 'UNKNOWN#2': 'null', 'UNKNOWN#3': 'undefined'}); 43 | done(); 44 | }, 45 | }); 46 | logging.logAction('meh', ['barf', 123, null, undefined]); 47 | }); 48 | 49 | test('fails on invalid action parameter', function () { 50 | assert.throw(function () { 51 | logging.logAction(); 52 | }, assert.AssertionError); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/func/model/DataContainer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gsjsBridge = require('model/gsjsBridge'); 4 | var RC = require('data/RequestContext'); 5 | var Group = require('model/Group'); 6 | var Geo = require('model/Geo'); 7 | var DataContainer = require('model/DataContainer'); 8 | var pers = require('data/pers'); 9 | var utils = require('utils'); 10 | var pbeMock = require('../../mock/pbe'); 11 | 12 | 13 | suite('DataContainer', function () { 14 | 15 | suite('create', function () { 16 | 17 | setup(function () { 18 | gsjsBridge.reset(); 19 | pers.init(pbeMock); 20 | }); 21 | 22 | teardown(function () { 23 | gsjsBridge.reset(); 24 | pers.init(); // disable mock back-end 25 | }); 26 | 27 | 28 | test('does its job', function (done) { 29 | new RC().run( 30 | function () { 31 | var group = Group.create(); 32 | var dc = DataContainer.create(group); 33 | assert.isTrue(utils.isDC(dc)); 34 | assert.strictEqual(dc.owner, group); 35 | }, 36 | function cb(err, res) { 37 | if (err) return done(err); 38 | var db = pbeMock.getDB(); 39 | assert.strictEqual(pbeMock.getCounts().write, 2); 40 | assert.strictEqual(Object.keys(db).length, 2); 41 | done(); 42 | } 43 | ); 44 | }); 45 | 46 | test('fails on invalid owner type', function () { 47 | assert.throw(function () { 48 | new RC().run(function () { 49 | var geo = Geo.create(); 50 | DataContainer.create(geo); 51 | }); 52 | }, assert.AssertionError); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/func/model/Geo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var pers = require('data/pers'); 5 | var RC = require('data/RequestContext'); 6 | var pbeMock = require('../../mock/pbe'); 7 | var Geo = require('model/Geo'); 8 | var gsjsBridge = require('model/gsjsBridge'); 9 | var utils = require('utils'); 10 | 11 | 12 | suite('Geo', function () { 13 | 14 | setup(function (done) { 15 | pers.init(pbeMock, {backEnd: { 16 | module: 'pbeMock', 17 | config: {pbeMock: { 18 | fixturesPath: path.resolve(path.join(__dirname, '../fixtures')), 19 | }}, 20 | }}, done); 21 | }); 22 | 23 | teardown(function () { 24 | pers.init(); // disable mock back-end 25 | }); 26 | 27 | 28 | suite('create', function () { 29 | 30 | setup(function () { 31 | gsjsBridge.reset(); 32 | }); 33 | 34 | teardown(function () { 35 | gsjsBridge.reset(); 36 | }); 37 | 38 | 39 | test('does its job', function (done) { 40 | new RC().run( 41 | function () { 42 | var g = Geo.create(); 43 | assert.isTrue(utils.isGeo(g)); 44 | }, 45 | function cb(err, res) { 46 | if (err) return done(err); 47 | var db = pbeMock.getDB(); 48 | assert.strictEqual(pbeMock.getCounts().write, 1); 49 | assert.strictEqual(Object.keys(db).length, 1); 50 | done(); 51 | } 52 | ); 53 | }); 54 | 55 | test('fails with invalid custom TSID', function () { 56 | assert.throw(function () { 57 | Geo.create({tsid: 'IXYZ'}); 58 | }, assert.AssertionError); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/data/rpcApi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rewire = require('rewire'); 4 | var auth = require('comm/auth'); 5 | var abePassthrough = require('comm/abe/passthrough'); 6 | var rpc = rewire('data/rpc'); 7 | var rpcApi = rewire('data/rpcApi'); 8 | var RQ = require('data/RequestQueue'); 9 | var pers = require('data/pers'); 10 | var persMock = require('../../mock/pers'); 11 | var Player = require('model/Player'); 12 | var Location = require('model/Location'); 13 | var Geo = require('model/Geo'); 14 | 15 | // introduce rewired components to each other 16 | rpcApi.__set__('rpc', rpc); 17 | 18 | 19 | suite('rpcApi', function () { 20 | 21 | setup(function () { 22 | rpc.__set__('pers', persMock); 23 | rpcApi.__set__('pers', persMock); 24 | persMock.reset(); 25 | RQ.init(); 26 | }); 27 | 28 | teardown(function () { 29 | persMock.reset(); 30 | rpcApi.__set__('pers', pers); 31 | rpc.__set__('pers', pers); 32 | RQ.init(); 33 | }); 34 | 35 | 36 | suite('getConnectData', function () { 37 | 38 | setup(function () { 39 | auth.init(abePassthrough); 40 | }); 41 | 42 | teardown(function () { 43 | auth.init(null); 44 | }); 45 | 46 | 47 | test('does its job', function () { 48 | var l = new Location({tsid: 'L1'}, new Geo()); 49 | persMock.preAdd(new Player({tsid: 'PXYZ', location: l}), l); 50 | var data = rpcApi.getConnectData('PXYZ'); 51 | assert.deepEqual(data, { 52 | hostPort: '127.0.0.1:1443', // from standard test config (setup.js) 53 | authToken: 'PXYZ', 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /config_local.js.SAMPLE_VAGRANT: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Sample local configuration file - copy to 'config_local.js' to "activate". 5 | * The preset values here are tailored to the Vagrant development VM. 6 | */ 7 | 8 | module.exports = { 9 | net: { 10 | gameServers: { 11 | gs01: { 12 | host: '192.168.23.23', 13 | ports: [ 14 | 1443, 15 | 1444, 16 | ], 17 | }, 18 | }, 19 | assetServer: { 20 | host: '192.168.23.23', 21 | port: 8000, 22 | }, 23 | rpc: { 24 | // moved to 6000 because Graphite's carbon daemon is sitting on 7002 25 | basePort: 6000, 26 | }, 27 | }, 28 | pers: { 29 | backEnd: { 30 | module: 'rethink', 31 | config: { 32 | rethink: { 33 | dbname: 'eleven_dev', 34 | dbtable: 'gamedata', 35 | dbhost: 'localhost', 36 | dbport: 28015, 37 | dbauth: 'test123', 38 | queryOpts: { 39 | durability: 'hard', 40 | noreply: false, 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | auth: { 47 | backEnd: { 48 | module: 'hmac', 49 | config: { 50 | hmac: { 51 | // secret used for auth token HMAC calculation (set this 52 | // to a random string): 53 | secret: 'CHANGE-ME-OR-ELSE', 54 | // minimum token lifetime in seconds: 55 | timeStep: 600, 56 | }, 57 | }, 58 | }, 59 | }, 60 | log: { 61 | level: { 62 | file: 'debug', 63 | stdout: 'error', 64 | }, 65 | includeLoc: true, 66 | }, 67 | mon: { 68 | statsd: { 69 | prefix: 'vagrant', 70 | }, 71 | }, 72 | debug: { 73 | stackTraceLimit: 100, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/comm/policyServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A very simple Flash "socket policy server". The Flash runtime needs 5 | * to read this policy from any server before allowing the SWF file 6 | * (the game client in our case) to establish a TCP socket connection 7 | * to that server. 8 | * 9 | * @see https://www.adobe.com/devnet/flashplayer/articles/socket_policy_files.html 10 | * 11 | * @module 12 | */ 13 | 14 | // public interface 15 | module.exports = { 16 | start: start, 17 | }; 18 | 19 | 20 | var net = require('net'); 21 | var config = require('config'); 22 | 23 | var POLICY = '\ 24 | \n\ 25 | \n\ 26 | \n\ 27 | \n\x00'; 28 | 29 | 30 | function start() { 31 | var host = config.get('net:gameServers:' + config.getGsid() + ':host'); 32 | var port = config.get('net:flashPolicyPort'); 33 | var server = net.createServer(handleConnect); 34 | server.listen(port, host, function onListening() { 35 | log.info('policy server listening on %s:%s', host, port); 36 | }); 37 | } 38 | 39 | 40 | function handleConnect(socket) { 41 | socket.on('data', function onData(data) { 42 | if (data.toString().slice(0, 20) === ' 0 ? minCount : undefined); 39 | }; 40 | 41 | 42 | /** 43 | * Add an item to the bag's hidden items list, making it "invisible" 44 | * for other API functions working on the bag contents. It can still be 45 | * accessed through the `hiddenItems` property. 46 | * 47 | * @param {Item} item the item to add 48 | */ 49 | BagApi.prototype.apiAddHiddenStack = function apiAddHiddenStack(item) { 50 | log.debug('%s.apiAddHiddenStack(%s)', this, item); 51 | item.setContainer(this, undefined, undefined, true); 52 | }; 53 | 54 | 55 | /** 56 | * Retrieves a list of items in the bag, i.e. an array with {@link 57 | * Item} instances and `null` for empty slots. 58 | * 59 | * @param {number} [count] length of the inventory to retrieve 60 | * (defaults to the bag capacity) 61 | * @returns {array} a list of items corresponding to the bag contents 62 | */ 63 | BagApi.prototype.apiGetSlots = function apiGetSlots(count) { 64 | log.trace('%s.apiGetSlots(%s)', this, count); 65 | return this.getSlots(count); 66 | }; 67 | -------------------------------------------------------------------------------- /test/unit/model/gsjsBridge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var rewire = require('rewire'); 5 | var gsjsBridge = rewire('model/gsjsBridge'); 6 | var Item = require('model/Item'); 7 | var Geo = require('model/Geo'); 8 | var DataContainer = require('model/DataContainer'); 9 | 10 | 11 | suite('gsjsBridge', function () { 12 | 13 | suite('create', function () { 14 | 15 | function createDummyProtos() { 16 | var ret = { 17 | items: {}, 18 | }; 19 | var Thingie = function Thingie() { 20 | Thingie.super_.apply(this, arguments); 21 | this.dummydata = 'foo'; 22 | }; 23 | util.inherits(Thingie, Item); 24 | ret.items.thingie = Thingie.prototype; 25 | return ret; 26 | } 27 | 28 | setup(function () { 29 | gsjsBridge.__set__('prototypes', createDummyProtos()); 30 | }); 31 | 32 | test('does its job', function () { 33 | var o = gsjsBridge.create({ 34 | tsid: 'IXYZ', 35 | class_tsid: 'thingie', 36 | blargh: 'oomph', 37 | }); 38 | assert.strictEqual(o.constructor.name, 'Thingie'); 39 | assert.instanceOf(o, Item); 40 | assert.strictEqual(o.tsid, 'IXYZ'); 41 | assert.property(o, 'blargh', 'property copied from supplied data'); 42 | assert.property(o, 'dummydata', 'property set in thingie constructor'); 43 | }); 44 | 45 | test('geo and DC objects are instantiated from their base classes', function () { 46 | var g = gsjsBridge.create({tsid: 'GXYZ'}); 47 | assert.instanceOf(g, Geo); 48 | var d = gsjsBridge.create({tsid: 'DXYZ'}); 49 | assert.instanceOf(d, DataContainer); 50 | }); 51 | }); 52 | 53 | 54 | suite('isTsid', function () { 55 | 56 | test('works as expected', function () { 57 | assert.isTrue(gsjsBridge.isTsid('PXYZ')); 58 | assert.isTrue(gsjsBridge.isTsid('IA')); 59 | assert.isFalse(gsjsBridge.isTsid('pXYZ')); 60 | assert.isFalse(gsjsBridge.isTsid('FOO')); 61 | assert.isFalse(gsjsBridge.isTsid(null)); 62 | assert.isFalse(gsjsBridge.isTsid(undefined)); 63 | assert.isFalse(gsjsBridge.isTsid('')); 64 | }); 65 | }); 66 | 67 | 68 | suite('getConfig', function () { 69 | 70 | setup(function () { 71 | gsjsBridge.init(true); 72 | }); 73 | 74 | teardown(function () { 75 | gsjsBridge.reset(); 76 | }); 77 | 78 | 79 | test('works as expected', function () { 80 | gsjsBridge.__get__('initDependencies')({dummy: 'config'}, {}); 81 | assert.strictEqual(gsjsBridge.getConfig().dummy, 'config'); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /bench/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Quick-and-dirty benchmark suite runner. Runs the files in SUITE_DIR 5 | * (including subdirectories) one by one as BenchmarkJS suites, each 6 | * one in a separate process. 7 | */ 8 | 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | var spawn = require('child_process').spawn; 12 | 13 | 14 | var SUITE_DIR = path.resolve(path.join(__dirname, 'suites')); 15 | var entries = fs.readdirSync(SUITE_DIR); 16 | 17 | 18 | function runSuites() { 19 | var entry = entries.shift(); 20 | if (!entry) return; 21 | var entryPath = path.resolve(path.join(SUITE_DIR, entry)); 22 | var stat = fs.statSync(entryPath); 23 | if (stat.isDirectory()) { 24 | var subEntries = fs.readdirSync(entryPath); 25 | for (var i = 0; i < subEntries.length; i++) { 26 | entries.push(path.join(entry, subEntries[i])); 27 | } 28 | return process.nextTick(runSuites); 29 | } 30 | else if (stat.isFile()) { 31 | spawnSuite(entryPath); 32 | } 33 | } 34 | 35 | 36 | function spawnSuite(suitePath) { 37 | var args = (process.execArgv || []).concat([process.argv[1], suitePath]); 38 | var child = spawn(process.execPath, args, {stdio: 'inherit'}); 39 | child.on('close', function(code) { 40 | if (code) { 41 | console.log('%s failed (%s).', suitePath, code); 42 | process.exit(code); 43 | } 44 | else { 45 | runSuites(); 46 | } 47 | }); 48 | } 49 | 50 | 51 | function runSuite(suitePath) { 52 | suitePath = path.resolve(SUITE_DIR, suitePath); 53 | var name = suitePath.slice(SUITE_DIR.length + 1); 54 | console.log('\nrunning bench suite %s...', name); 55 | var suite = require(suitePath); 56 | // workaround for asynchronous suite setup (cf. ) 57 | var setup = suite.asyncSetup || function(cb) { cb(); }; 58 | setup(function cb(err) { 59 | if (err) throw err; 60 | suite.on('cycle', onCycle); 61 | suite.on('error', onError); 62 | suite.run(); 63 | }); 64 | } 65 | 66 | 67 | function onError(event) { 68 | console.log('\terror running %s: %s', event.target.name, 69 | event.target.error.stack); 70 | } 71 | 72 | 73 | function onCycle(event) { 74 | if (!event.target.error) { 75 | console.log('\t' + String(event.target)); 76 | } 77 | } 78 | 79 | 80 | function main() { 81 | var target = process.argv[2]; 82 | if (target) { 83 | runSuite(target); 84 | } 85 | else { 86 | runSuites(); 87 | } 88 | } 89 | 90 | 91 | if (require.main === module) { 92 | main(); 93 | } 94 | -------------------------------------------------------------------------------- /src/data/rpcProxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * ECMAScript 6 direct proxy helper for transparent RPC wrapping of 5 | * function calls on game objects. On each game server instance, 6 | * copies of objects that another server is responsible for are wrapped 7 | * in RPC proxies, so the GSJS code does not have to take care of the 8 | * distributed server environment when working with game objects. 9 | * 10 | * @see {@link http://wiki.ecmascript.org/doku.php?id=harmony:direct_proxies} 11 | * @module 12 | */ 13 | 14 | // public interface 15 | module.exports = { 16 | makeProxy: makeProxy, 17 | }; 18 | 19 | 20 | var _ = require('lodash'); 21 | var assert = require('assert'); 22 | var rpc = require('data/rpc'); 23 | 24 | 25 | /** 26 | * Wraps a game object (or rather, a copy of a game object that another 27 | * server instance is responsible for) in a transparent RPC handling 28 | * proxy. 29 | * 30 | * @param {GameObject} obj the game object to wrap 31 | * @returns {Proxy} wrapped game object 32 | */ 33 | function makeProxy(obj) { 34 | assert(!obj.__isRP, 'object is already RPC-proxied: ' + obj); 35 | return new Proxy(obj, { 36 | get: proxyGet, 37 | }); 38 | } 39 | 40 | 41 | /** 42 | * Traps property read access on RPC-proxy-wrapped game objects. 43 | * Function calls are transparently forwarded to the game server 44 | * responsible for the object (see {@link module:data/rpc~sendRequest| 45 | * rpc.sendRequest}); access to properties of any other type is passed 46 | * through to the local copy of the object. Since this local copy of a 47 | * remote game object is read from persistence and only cached for the 48 | * current request, its data is expected to be up to date. 49 | * 50 | * @private 51 | */ 52 | function proxyGet(target, name, receiver) { 53 | if (name === '__isRP') return true; 54 | // only functions are called remotely 55 | if (!_.isFunction(target[name])) { 56 | return target[name]; 57 | } 58 | if (name === 'inspect' || name === 'valueOf' || name === 'toString') { 59 | return () => '^R' + target.toString(); 60 | } 61 | // call functions inherited from Object locally (e.g. hasOwnProperty(), etc) 62 | if (Object.prototype.hasOwnProperty(name)) { 63 | return target[name]; 64 | } 65 | // anything else: call on remote host 66 | return function rpcWrapper() { 67 | // convert function arguments to a proper array 68 | var args = Array.prototype.slice.call(arguments); 69 | return rpc.sendObjRequest(target, name, args); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /test/unit/comm/slackNotify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rewire = require('rewire'); 4 | var slackNotify = rewire('comm/slackNotify'); 5 | 6 | 7 | suite('slackNotify', function () { 8 | 9 | suite('send', function () { 10 | 11 | setup(function () { 12 | slackNotify.__set__('cfg', { 13 | webhookUrl: 'https://hooks.slack.com/services/FOO/bar', 14 | channel: 'server-admins-test', 15 | botName: 'gameserver TEST', 16 | }); 17 | }); 18 | 19 | teardown(function () { 20 | slackNotify.__set__('cfg', undefined); 21 | }); 22 | 23 | 24 | test('formats messages', function (done) { 25 | slackNotify.__set__('slack', {webhook: function webhook(params) { 26 | assert.strictEqual(params.text, ':rotating_light: meebleforp!'); 27 | done(); 28 | }}); 29 | slackNotify.alert('%sble%s!', 'mee', 'forp'); 30 | slackNotify.__set__('slack', undefined); // clean up 31 | }); 32 | 33 | test('sends message without icon correctly', function (done) { 34 | slackNotify.__set__('slack', {webhook: function webhook(params) { 35 | assert.strictEqual(params.text, 'aaargh'); 36 | done(); 37 | }}); 38 | slackNotify.__get__('send')(['aaargh']); 39 | slackNotify.__set__('slack', undefined); // clean up 40 | }); 41 | 42 | test('uses default parameters defined in config', function (done) { 43 | slackNotify.__set__('slack', {webhook: function webhook(params) { 44 | // config options defined in test setup 45 | assert.strictEqual(params.channel, 'server-admins-test'); 46 | assert.strictEqual(params.username, 'gameserver TEST (gs01-01)'); 47 | done(); 48 | }}); 49 | slackNotify.info('I am Professor Chaos!'); 50 | slackNotify.__set__('slack', undefined); // clean up 51 | }); 52 | 53 | test('handles optional custom channel argument', function (done) { 54 | slackNotify.__set__('slack', {webhook: function webhook(params) { 55 | assert.strictEqual(params.channel, 'gswarnings'); 56 | assert.strictEqual(params.text, ':warning: something happened'); 57 | done(); 58 | }}); 59 | slackNotify.warning({channel: 'gswarnings'}, 'something %s', 'happened'); 60 | slackNotify.__set__('slack', undefined); // clean up 61 | }); 62 | 63 | test('does not fail when integration is not configured', function () { 64 | slackNotify.__set__('cfg', undefined); 65 | slackNotify.__set__('slack', {webhook: function webhook(params) { 66 | throw new Error('should not be called'); 67 | }}); 68 | slackNotify.alert('meep'); 69 | slackNotify.__set__('slack', undefined); // clean up 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/model/IdObjRefMap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = IdObjRefMap; 4 | 5 | 6 | var _ = require('lodash'); 7 | var orProxy = require('data/objrefProxy'); 8 | var utils = require('utils'); 9 | 10 | 11 | /** 12 | * Container for a collection of {@link GameObject}s (stored as 13 | * properties with their TSIDs as names), providing a `length` 14 | * property and an iterator helper function. 15 | * 16 | * @param {array} [data] optional initial content (array elements are 17 | * shallow-copied into the map; objref descriptors are copied 18 | * without loading the referenced objects) 19 | * @constructor 20 | */ 21 | function IdObjRefMap(data) { 22 | if (data && !_.isArray(data)) { 23 | throw new TypeError('invalid data type for IdObjRefMap: ' + typeof data); 24 | } 25 | for (var i = 0; data && i < data.length; i++) { 26 | var obj = data[i]; 27 | if (!_.isObject(obj)) { 28 | // ignore values that aren't objects 29 | continue; 30 | } 31 | if (obj.__isORP) { 32 | orProxy.setupObjRefProp(obj.tsid, this, obj.tsid); 33 | } 34 | else { 35 | // if the object is already loaded, we don't need the accessor 36 | // property (and this allows tests without persistence layer) 37 | this[obj.tsid] = obj; 38 | } 39 | } 40 | } 41 | 42 | 43 | /** 44 | * the number of stored objects 45 | * 46 | * @name length 47 | * @member {number} 48 | * @memberof IdObjRefMap 49 | * @instance 50 | */ 51 | Object.defineProperty(IdObjRefMap.prototype, 'length', { 52 | get: function get() { 53 | return Object.keys(this).length; 54 | }, 55 | }); 56 | 57 | 58 | /** 59 | * Iterates over objects in this map, optionally filtering by 60 | * `class_tsid`, and calls the given function on each one. 61 | * 62 | * @param {string} [classTsid] only iterate over objects of this class 63 | * @param {function} func function to be called for each (matching) 64 | * object; signature: `func(obj)` 65 | */ 66 | IdObjRefMap.prototype.apiIterate = function apiIterate(classTsid, func) { 67 | // handle optional classTsid parameter 68 | if (_.isFunction(classTsid)) { 69 | func = classTsid; 70 | classTsid = undefined; 71 | } 72 | log.debug('IdObjRefMap.apiIterate(%s, %s)', classTsid, func.name); 73 | var keys = Object.keys(this); 74 | for (var i = 0; i < keys.length; i++) { 75 | var o = this[keys[i]]; 76 | if (typeof o !== 'object') continue; 77 | if (!classTsid || o.class_tsid === classTsid) { 78 | func(o); 79 | } 80 | } 81 | }; 82 | // hide function from for...in loops on IdObjRefMap instances: 83 | utils.makeNonEnumerable(IdObjRefMap.prototype, 'apiIterate'); 84 | -------------------------------------------------------------------------------- /test/unit/model/OrderedHash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var OrderedHash = require('model/OrderedHash'); 4 | 5 | 6 | suite('OrderedHash', function () { 7 | 8 | suite('ctor', function () { 9 | 10 | test('creates OrderedHash', function () { 11 | var o = {x: 'x', y: 'y', z: 'z'}; 12 | var oh = new OrderedHash(o); 13 | assert.strictEqual(oh.x, 'x'); 14 | oh.q = 'q'; 15 | assert.notProperty(o, 'q', 'new properties not added to source data object'); 16 | }); 17 | 18 | test('creates empty OrderedHash without parameter', function () { 19 | var oh = new OrderedHash(); 20 | assert.strictEqual(oh.length(), 0); 21 | }); 22 | 23 | test('JSON serialization skips everything but data properties', function () { 24 | var oh = new OrderedHash({x: 'x', yz: {y: 'y', z: 'z'}}); 25 | assert.strictEqual(JSON.stringify(oh), 26 | '{"x":"x","yz":{"y":"y","z":"z"}}'); 27 | }); 28 | 29 | test('makes property enumeration iterate over keys in natural order', function () { 30 | var oh = new OrderedHash({z: 'z', a: 'a'}); 31 | var keys = []; 32 | for (var k in oh) { 33 | keys.push(k); 34 | } 35 | assert.deepEqual(keys, ['a', 'z']); 36 | assert.deepEqual(Object.keys(oh), ['a', 'z']); 37 | oh.burp = 'b'; 38 | oh['234'] = '123'; 39 | keys = []; 40 | for (var j in oh) { 41 | keys.push(j); 42 | } 43 | assert.deepEqual(keys, ['234', 'a', 'burp', 'z']); 44 | assert.deepEqual(Object.keys(oh), ['234', 'a', 'burp', 'z']); 45 | }); 46 | }); 47 | 48 | 49 | suite('first', function () { 50 | 51 | test('does its job', function () { 52 | var oh = new OrderedHash({X: 'X', y: 'y', z: 'z'}); 53 | assert.strictEqual(oh.first(), 'X'); 54 | oh.A = 13; 55 | assert.strictEqual(oh.first(), 13); 56 | }); 57 | }); 58 | 59 | 60 | suite('last', function () { 61 | 62 | test('does its job', function () { 63 | var oh = new OrderedHash({X: 'X', y: 'y', z: 'z'}); 64 | assert.strictEqual(oh.last(), 'z'); 65 | oh.zzz = null; 66 | assert.strictEqual(oh.last(), null); 67 | }); 68 | }); 69 | 70 | 71 | suite('length', function () { 72 | 73 | test('does its job', function () { 74 | var oh = new OrderedHash(); 75 | assert.strictEqual(oh.length(), 0); 76 | oh.x = 'x'; 77 | assert.strictEqual(oh.length(), 1); 78 | oh.y = {y: 'y', z: 'z'}; 79 | assert.strictEqual(oh.length(), 2); 80 | oh.u = undefined; 81 | assert.strictEqual(oh.length(), 3); 82 | delete oh.x; 83 | assert.strictEqual(oh.length(), 2); 84 | }); 85 | }); 86 | 87 | 88 | suite('clear', function () { 89 | 90 | test('does its job', function () { 91 | var oh = new OrderedHash({x: 'x', y: 'y', z: 'z'}); 92 | oh.clear(); 93 | assert.strictEqual(oh.length(), 0); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/model/DataContainer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = DataContainer; 4 | 5 | 6 | var assert = require('assert'); 7 | var GameObject = require('model/GameObject'); 8 | var pers = require('data/pers'); 9 | var RC = require('data/RequestContext'); 10 | var rpc = require('data/rpc'); 11 | var RQ = require('data/RequestQueue'); 12 | var util = require('util'); 13 | var utils = require('utils'); 14 | 15 | 16 | util.inherits(DataContainer, GameObject); 17 | DataContainer.prototype.TSID_INITIAL = GameObject.prototype.TSID_INITIAL_DATA_CONTAINER; 18 | 19 | 20 | /** 21 | * Generic constructor for both instantiating an existing data 22 | * container object (from JSON data), and creating a new one. 23 | * 24 | * @param {object} [data] initialization values (properties are 25 | * shallow-copied into the object) 26 | * @constructor 27 | * @augments GameObject 28 | */ 29 | function DataContainer(data) { 30 | DataContainer.super_.call(this, data); 31 | } 32 | 33 | 34 | /** 35 | * Creates a new `DataContainer` instance and adds it to persistence. 36 | * 37 | * @param {Location|Group|Item|Bag|Player} owner top-level game object 38 | * this DC belongs to 39 | * @returns {object} a `DataContainer` object 40 | */ 41 | DataContainer.create = function create(owner) { 42 | assert(utils.isLoc(owner) || utils.isItem(owner) || utils.isGroup(owner), 43 | util.format('invalid DC owner: %s', owner)); 44 | var dc = pers.create(DataContainer, {owner: owner}); 45 | return dc; 46 | }; 47 | 48 | 49 | /** 50 | * Retrieves the request queue for this data container (typically, the queue of 51 | * its owner). 52 | * 53 | * @returns {RequestQueue} the request queue for this DC 54 | */ 55 | DataContainer.prototype.getRQ = function getRQ() { 56 | if (this.owner && rpc.isLocal(this.owner)) { 57 | return this.owner.getRQ(); 58 | } 59 | return RQ.getGlobal(); 60 | }; 61 | 62 | 63 | /** 64 | * Special helper for instance group cleanup, removing an obsolete instance 65 | * group reference from a template location's `instances` DC. 66 | * Called from {@link Group#del} (potentially from a remote GS worker via RPC) 67 | * when an instance group is cleared. 68 | * 69 | * @param {string} instId the instance template ID (e.g. "hell_one") 70 | * @param {string} instTsid TSID of the instance group that is being deleted 71 | */ 72 | DataContainer.prototype.removeInstance = function removeInstance(instId, instTsid) { 73 | var instanceList = this.instances ? this.instances[instId] : []; 74 | for (var i = 0; instanceList && i < instanceList.length; i++) { 75 | if (instanceList[i].tsid === instTsid) { 76 | instanceList.splice(i, 1); 77 | log.debug('instance %s removed from %s instance list in %s', 78 | instTsid, instId, this.tsid); 79 | RC.setDirty(this); 80 | break; 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/model/Group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Group; 4 | 5 | 6 | var GameObject = require('model/GameObject'); 7 | var pers = require('data/pers'); 8 | var rpc = require('data/rpc'); 9 | var RQ = require('data/RequestQueue'); 10 | var util = require('util'); 11 | 12 | 13 | util.inherits(Group, GameObject); 14 | Group.prototype.TSID_INITIAL = GameObject.prototype.TSID_INITIAL_GROUP; 15 | 16 | 17 | /** 18 | * Generic constructor for both instantiating an existing group object 19 | * (from JSON data), and creating a new one. 20 | * 21 | * @param {object} [data] initialization values (properties are 22 | * shallow-copied into the object) 23 | * @constructor 24 | * @augments GameObject 25 | */ 26 | function Group(data) { 27 | data = data || {}; 28 | if (!data.tsid) data.tsid = rpc.makeLocalTsid(this.TSID_INITIAL_GROUP); 29 | Group.super_.call(this, data); 30 | } 31 | 32 | 33 | /** 34 | * Creates a new `Group` instance and adds it to persistence. 35 | * 36 | * @param {string} [classTsid] specific class of the group 37 | * @param {string} [hubId] hub to attach the group to 38 | * @returns {object} a `Group` object 39 | */ 40 | Group.create = function create(classTsid, hubId) { 41 | var data = {}; 42 | if (classTsid) { 43 | data.class_tsid = classTsid; 44 | } 45 | if (hubId) { 46 | data.hubid = hubId; 47 | } 48 | return pers.create(Group, data); 49 | }; 50 | 51 | 52 | /** 53 | * Retrieves the request queue for this group. 54 | * 55 | * @returns {RequestQueue} the request queue for this group 56 | */ 57 | Group.prototype.getRQ = function getRQ() { 58 | return RQ.get(this); 59 | }; 60 | 61 | 62 | /** 63 | * Schedules this group to be released from the live object cache after all 64 | * pending requests for it have been handled. When this is called, the group's 65 | * request queue will not accept any new requests. 66 | * 67 | * @param {function} [callback] for optional error handling 68 | */ 69 | Group.prototype.unload = function unload(callback) { 70 | var self = this; 71 | this.getRQ().push('unload', function unloadReq() { 72 | Group.super_.prototype.unload.call(self); 73 | }, callback, {close: true, obj: this}); 74 | }; 75 | 76 | 77 | /** 78 | * Schedules this group for deletion after the current request. Special handling 79 | * for instance groups. 80 | */ 81 | Group.prototype.del = function del() { 82 | log.trace('del %s', this); 83 | Group.super_.prototype.del.call(this); 84 | // explicit cleanup for instance groups: remove reference from instances DC 85 | // of the corresponding template location 86 | if (this.instance_id && this.base_tsid) { 87 | log.debug('deleting instance %s of %s', this.tsid, this.base_tsid); 88 | var templateLoc = pers.get(this.base_tsid); 89 | if (templateLoc.instances) { 90 | templateLoc.instances.removeInstance(this.instance_id, this.tsid); 91 | } 92 | else { 93 | log.info('no instance list found for %s', this.base_tsid); 94 | } 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /config_local.js.SAMPLE_PROD: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Sample local configuration file for a "live" public environment. 5 | * The values here need to be adjusted for the specific target server. 6 | */ 7 | 8 | module.exports = { 9 | net: { 10 | gameServers: { 11 | gs01: { 12 | // IP/hostname the AMF and RPC services will bind to 13 | host: 'THIS.GS.HOST.LOCAL.IP', 14 | // optional externally reachable IP/hostname, if 'host' above is 15 | // not a public IP address (e.g. when using NAT) 16 | publicHost: 'THIS.GS.HOST.PUBLIC.IP', 17 | ports: [ 18 | // add as many worker processes as you like (probably best 19 | // not to add more than the number of CPU cores, though) 20 | 1443, 21 | 1444, 22 | 1445, 23 | 1446 24 | ], 25 | }, 26 | }, 27 | assetServer: { 28 | host: 'YOUR.ASSET.SERVER.IP', 29 | port: 8000, 30 | }, 31 | }, 32 | limits: { 33 | // maximum number of concurrent client connections per worker (workers 34 | // may refuse to accept new connections once this number is reached) 35 | maxSessions: 10, 36 | }, 37 | pers: { 38 | backEnd: { 39 | module: 'rethink', 40 | config: { 41 | rethink: { 42 | dbname: 'eleven_prod', 43 | dbtable: 'gamedata', 44 | dbhost: 'localhost', 45 | dbport: 28015, 46 | dbauth: 'RETHINKDB_AUTH_KEY', 47 | queryOpts: { 48 | durability: 'hard', 49 | noreply: false, 50 | }, 51 | }, 52 | }, 53 | }, 54 | }, 55 | auth: { 56 | backEnd: { 57 | module: 'hmac', 58 | config: { 59 | hmac: { 60 | // secret used for auth token HMAC calculation (set this 61 | // to a random string): 62 | secret: 'CHANGE-ME-OR-ELSE', 63 | // minimum token lifetime in seconds: 64 | timeStep: 600, 65 | }, 66 | }, 67 | }, 68 | }, 69 | log: { 70 | dir: '/var/log/eleven', 71 | level: { 72 | file: 'info', 73 | stdout: 'fatal', 74 | }, 75 | // enabling this has a significant performance impact: 76 | includeLoc: false, 77 | }, 78 | mon: { 79 | statsd: { 80 | prefix: 'prod', 81 | }, 82 | }, 83 | debug: { 84 | // consider security and stability implications before enabling the 85 | // REPL server in a PROD environment 86 | repl: { 87 | enable: false, 88 | }, 89 | }, 90 | slack: { 91 | // the in-game chat group integration requires a "bot user" to be set 92 | // up in Slack, and the resulting API token needs to be set here 93 | chat: { 94 | token: 'xoxo-1234567890-T0keNfr0mSL4cK', 95 | groups: { 96 | RA512UITCLA22AD: 'livehelp', 97 | RA9118JTCLA204I: 'global', 98 | }, 99 | }, 100 | // server notifications/alerts need an "incoming WebHook" configured 101 | // in Slack, and its URL added here 102 | notify: { 103 | webhookUrl: 'https://hooks.slack.com/services/XYZ/asdfghjkl', 104 | channel: 'server-admins', 105 | botName: 'gameserver PROD', 106 | }, 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /test/unit/comm/slackChat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var rewire = require('rewire'); 5 | var slackChat = rewire('comm/slackChat'); 6 | 7 | 8 | suite('slackChat', function () { 9 | 10 | 11 | suite('processMsgText', function () { 12 | 13 | var processMsgText = slackChat.__get__('processMsgText'); 14 | 15 | test('replaces user and channel references with names', function () { 16 | assert.strictEqual(processMsgText( 17 | 'test <@U024H9SL6|aroha> <#C024H4M2X|general>x'), 18 | 'test @aroha #generalx'); 19 | }); 20 | 21 | test('gets missing user/group/channel labels from slack client', function () { 22 | slackChat.__set__('slack', {dataStore: { 23 | getUserById: function getUserById(id) { 24 | return {name: id.toLowerCase()}; 25 | }, 26 | getChannelById: function getChannelById(id) { 27 | return; // fake unknown channel/group 28 | }, 29 | }}); 30 | assert.strictEqual(processMsgText( 31 | 'emptylabel <@U024H9SL6|> nolabel <#C024H4M2X>'), 32 | 'emptylabel @u024h9sl6 nolabel #C024H4M2X'); 33 | slackChat.__set__('slack', undefined); // clean up 34 | }); 35 | }); 36 | 37 | 38 | suite('onSlackMessage', function () { 39 | 40 | setup(function () { 41 | slackChat.__set__('channelToGroup', {C037FB4HV: 'GXYZ'}); 42 | slackChat.__set__('slack', {webClient: {users: {info: function getUsersInfoStub(id) { 43 | return {user: {id: 'PASDF', name: 'D. Ummy User'}}; 44 | }}}}); 45 | }); 46 | 47 | teardown(function () { 48 | slackChat.__set__('channelToGroup', {}); 49 | slackChat.__set__('slack', undefined); 50 | }); 51 | 52 | var onSlackMessage = slackChat.__get__('onSlackMessage'); 53 | /* 54 | // TODO: test fails due to pbe not set (loading GXYZ). 55 | test('works as expected', function (done) { 56 | var dispatchToGroup = slackChat.__get__('dispatchToGroup'); 57 | slackChat.__set__('dispatchToGroup', function dispatchToGroupStub(msg) { 58 | assert.deepEqual(msg, { 59 | type: 'pc_groups_chat', 60 | tsid: 'GXYZ', 61 | pc: {tsid: 'PSLACKPASDF', label: 'D. Ummy User'}, 62 | txt: 'this is a message that was posted in Slack', 63 | }); 64 | assert.isTrue(msg.fromSlack); 65 | return done(); 66 | }); 67 | onSlackMessage({ 68 | type: 'message', 69 | channel: 'C037FB4HV', 70 | user: 'U024H9SL6', 71 | text: 'this is a message that was posted in Slack', 72 | ts: '1452279130.000011', 73 | team: 'T024H4M2R', 74 | }); 75 | slackChat.__set__('dispatchToGroup', dispatchToGroup); 76 | }); 77 | */ 78 | test('handles missing user', function () { 79 | // for unknown reasons, the Slack lib sometimes does not return a 80 | // user; simulate this 81 | slackChat.__set__('slack', {dataStore: { 82 | getUserById: _.noop, 83 | }}); 84 | onSlackMessage({ 85 | type: 'message', 86 | channel: 'C037FB4HV', 87 | user: 'U024H9SL6', 88 | text: 'barf', 89 | }); 90 | // nothing specific to check, it just shouldn't throw an error 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/int/data/pbe/rethink.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var wait = require('wait.for'); 4 | var pbe = require('data/pbe/rethink'); 5 | var rdb = require('rethinkdb'); 6 | var config = require('config'); 7 | var Player = require('model/Player'); 8 | 9 | 10 | suite('rethink', function () { 11 | 12 | this.timeout(5000); 13 | 14 | var cfg = config.get('pers:backEnd:config:rethink'); 15 | 16 | function connect(cb) { 17 | rdb.connect({ 18 | host: cfg.dbhost, 19 | port: cfg.dbport, 20 | db: cfg.dbname, 21 | authKey: cfg.dbauth, 22 | }, cb); 23 | } 24 | 25 | function run(func) { 26 | var args = Array.prototype.slice.call(arguments, 1, arguments.length - 1); 27 | var callback = arguments[arguments.length - 1]; 28 | connect(function cb(err, conn) { 29 | if (err) return callback(err); 30 | func.apply(rdb, args).run(conn, callback); 31 | }); 32 | } 33 | 34 | suiteSetup(function (done) { 35 | run(rdb.dbCreate, cfg.dbname, function cb(err, res) { 36 | if (err) return done(err); 37 | pbe.init( 38 | config.get('pers:backEnd:config:rethink'), 39 | function cb(err, res) { 40 | return done(err); 41 | } 42 | ); 43 | }); 44 | }); 45 | 46 | suiteTeardown(function (done) { 47 | run(rdb.dbDrop, cfg.dbname, function cb(err, res) { 48 | if (err) return done(err); 49 | pbe.init(config.get('pers:backEnd:config:rethink'), done); 50 | }); 51 | }); 52 | 53 | setup(function (done) { 54 | run(rdb.tableCreate, cfg.dbtable, {primaryKey: 'tsid'}, done); 55 | }); 56 | 57 | teardown(function (done) { 58 | run(rdb.tableDrop, cfg.dbtable, done); 59 | }); 60 | 61 | 62 | suite('CRUD', function () { 63 | 64 | test('basic create/read', function (done) { 65 | wait.launchFiber(function () { 66 | var p = new Player(); 67 | wait.for(pbe.write, p.serialize()); 68 | var rp = wait.for(pbe.read, p.tsid); 69 | assert.strictEqual(rp.tsid, p.tsid); 70 | assert.property(rp, 'stats'); 71 | assert.property(rp, 'metabolics'); 72 | return done(); 73 | }); 74 | }); 75 | 76 | test('basic create/delete', function (done) { 77 | wait.launchFiber(function () { 78 | var o = {tsid: 'X', ping: 'pong'}; 79 | assert.isNull(wait.for(pbe.read, 'X')); 80 | wait.for(pbe.write, o); 81 | assert.strictEqual(wait.for(pbe.read, 'X').ping, 'pong'); 82 | wait.for(pbe.del, o.tsid); 83 | assert.isNull(wait.for(pbe.read, 'X')); 84 | return done(); 85 | }); 86 | }); 87 | 88 | test('basic create/update', function (done) { 89 | wait.launchFiber(function () { 90 | var o = {tsid: 'Y', blurp: 1, meh: true}; 91 | wait.for(pbe.write, o); 92 | assert.strictEqual(wait.for(pbe.read, 'Y').blurp, 1); 93 | o.blurp = -3; 94 | delete o.meh; 95 | wait.for(pbe.write, o); 96 | assert.strictEqual(wait.for(pbe.read, 'Y').blurp, -3); 97 | assert.notProperty(wait.for(pbe.read, 'Y'), 'meh'); 98 | return done(); 99 | }); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/comm/abe/hmac.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * SHA512 HMAC based player authentication back-end. Generates tokens 5 | * according to the following scheme: 6 | * ``` 7 | * | 8 | * ``` 9 | * The HMAC consists of the player TSID and a timestamp, hashed with a 10 | * secret key. 11 | * 12 | * Tokens are considered valid if the plain-text TSID matches the one 13 | * in the hash code, and the timestamp is within the accepted range 14 | * (see the {@link https://github.com/mixu/token|token} package 15 | * documentation for details regarding token expiry). 16 | * 17 | * Since the generated tokens contain the player TSID, there is no need 18 | * to store them in persistent storage. 19 | * 20 | * @module 21 | */ 22 | 23 | // public interface 24 | module.exports = { 25 | init: init, 26 | authenticate: authenticate, 27 | getToken: getToken, 28 | getTokenLifespan: getTokenLifespan, 29 | }; 30 | 31 | 32 | var _ = require('lodash'); 33 | var assert = require('assert'); 34 | var auth = require('comm/auth'); 35 | var token = require('token'); 36 | var utils = require('utils'); 37 | 38 | 39 | /** 40 | * Initializes the {@link https://github.com/mixu/token|token} library 41 | * with parameters from the global server configuration. 42 | * 43 | * @param {object} config configuration settings 44 | */ 45 | function init(config) { 46 | assert(_.isObject(config) && config.secret !== undefined && 47 | utils.isInt(config.timeStep), 'invalid or missing HMAC auth config'); 48 | token.defaults.secret = config.secret; 49 | token.defaults.timeStep = config.timeStep; 50 | } 51 | 52 | 53 | /** 54 | * Authenticates a client/player. 55 | * 56 | * @param {string} t authentication token supplied by the client 57 | * @returns {string} player TSID (if successfully authenticated) 58 | * @throws {AuthError} if the given token could not be parsed or is 59 | * invalid/expired 60 | */ 61 | function authenticate(t) { 62 | var tsid, tdata; 63 | try { 64 | tsid = t.split('|')[0]; 65 | tdata = t.split('|')[1]; 66 | } 67 | catch (e) { 68 | throw new auth.AuthError('invalid token data: ' + t, e); 69 | } 70 | var res = token.verify(tsid, tdata); 71 | if (!res) { 72 | throw new auth.AuthError('invalid or expired token: ' + t); 73 | } 74 | return tsid; 75 | } 76 | 77 | 78 | /** 79 | * Generates an authentication token for the given player. 80 | * 81 | * @param {Player} player the player to generate a token for 82 | * @param {object} [options] custom options, overriding those set via 83 | * {@link module:comm/abe/hmac~init|init} (for testing) 84 | * @returns {string} a valid authentication token 85 | */ 86 | function getToken(player, options) { 87 | var tdata = token.generate(player.tsid, options); 88 | var ret = player.tsid + '|' + tdata; 89 | log.debug('generated token for %s: %s', player, ret); 90 | return ret; 91 | } 92 | 93 | 94 | /** 95 | * Returns the minimal guaranteed lifespan of an authentication token 96 | * generated by this module. 97 | * 98 | * @returns {number} guaranteed minimal token lifespan **in seconds** 99 | */ 100 | function getTokenLifespan() { 101 | return token.defaults.timeStep; 102 | } 103 | -------------------------------------------------------------------------------- /bench/suites/data/rpc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../../setup'); 4 | var suite = new (require('benchmark')).Suite; 5 | module.exports = suite; 6 | var childproc = require('child_process'); 7 | var path = require('path'); 8 | var rewire = require('rewire'); 9 | var rpc = rewire('data/rpc'); 10 | var config = require('config'); 11 | var wait = require('wait.for'); 12 | var GameObject = require('model/GameObject'); 13 | 14 | 15 | // minimal GS configuration with one master and one worker process 16 | var CONFIG = {net: { 17 | gameServers: { 18 | gs01: {host: '127.0.0.1', ports: [3000]}, 19 | }, 20 | rpc: {basePort: 6000, timeout: 10000}, 21 | }}; 22 | // dummy game object to call RPC functions on 23 | var DUMMY_OBJ = new GameObject({ 24 | tsid: 'LTESTOBJECT', 25 | add: function(a, b) { return a + b; }, 26 | }); 27 | 28 | 29 | var worker; 30 | 31 | 32 | // called by the bench runner (i.e. only for the master process) 33 | suite.asyncSetup = function(done) { 34 | // fork child process as worker/RPC server 35 | worker = childproc.fork(path.join(__filename)); 36 | worker.send({cmd: 'init'}); 37 | worker.on('message', function messageHandler() { 38 | // server is ready, initialize config and RPC channels for 39 | // ourself (master, RPC client) 40 | init(true, function masterReady() { 41 | done(); 42 | }); 43 | }); 44 | }; 45 | 46 | 47 | // message handler for the (worker/RPC client) child process 48 | process.on('message', function(msg) { 49 | switch(msg.cmd) { 50 | case 'init': 51 | init(false); 52 | break; 53 | case 'shutdown': 54 | rpc.shutdown(function callback() { 55 | process.exit(0); 56 | }); 57 | break; 58 | } 59 | }); 60 | 61 | 62 | // initializes the process either as master (RPC client for the purpose of 63 | // this benchmark) or worker (RPC server, started as a child process) 64 | function init(isMaster, ready) { 65 | config.init(isMaster, CONFIG, isMaster ? {} : {gsid: 'gs01-01'}); 66 | // mock the persistence layer (dirty as fuck, but we're only messing up 67 | // the rpc module for this particular process here) 68 | rpc.__set__('pers', { 69 | get: function() { 70 | return DUMMY_OBJ; 71 | }, 72 | }); 73 | // initialize RPC channels 74 | rpc.init(function callback(err) { 75 | if (err) throw err; 76 | if (isMaster) { 77 | // master -> start benchmark 78 | ready(); 79 | } 80 | else { 81 | // worker -> signal parent process (benchmark suite) that we're ready 82 | process.send({type: 'ready'}); 83 | } 84 | }); 85 | } 86 | 87 | 88 | // cleanup/shutdown handler 89 | suite.on('complete', function() { 90 | rpc.shutdown(function callback() { 91 | worker.send({cmd: 'shutdown'}); 92 | }); 93 | }); 94 | 95 | 96 | suite.add('game object function call (w/ wait.for)', function(deferred) { 97 | wait.launchFiber(function rpcFiber() { 98 | rpc.sendObjRequest(DUMMY_OBJ, 'add', [1, 2]); 99 | deferred.resolve(); 100 | }); 101 | }, {defer: true}); 102 | 103 | 104 | suite.add('game object function call (w/ callback)', function(deferred) { 105 | rpc.sendObjRequest(DUMMY_OBJ, 'add', [1, 2], function(err, res) { 106 | if (err) throw err; 107 | deferred.resolve(); 108 | }); 109 | }, {defer: true}); 110 | -------------------------------------------------------------------------------- /src/comm/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A thin common interface layer for pluggable authentication back-end 5 | * modules. These modules must implement the following API: 6 | * ``` 7 | * init(config, callback) 8 | * authenticate(token) -> playerTsid 9 | * getToken(player) -> token 10 | * getTokenLifespan() -> number 11 | * ``` 12 | * 13 | * @module 14 | */ 15 | 16 | // public interface 17 | module.exports = { 18 | AuthError: AuthError, 19 | init: init, 20 | authenticate: authenticate, 21 | getToken: getToken, 22 | getTokenLifespan: getTokenLifespan, 23 | }; 24 | 25 | 26 | var _ = require('lodash'); 27 | var assert = require('assert'); 28 | 29 | 30 | /** 31 | * Custom authentication error type. 32 | * 33 | * @param {string} [msg] error message 34 | * @constructor 35 | */ 36 | // see , , 37 | // 38 | function AuthError(msg, cause) { 39 | this.message = msg; 40 | Error.captureStackTrace(this, AuthError); 41 | // log cause (for possible auth debugging) 42 | log.info(cause, msg); 43 | } 44 | AuthError.prototype = Object.create(Error.prototype); 45 | AuthError.prototype.constructor = AuthError; 46 | AuthError.prototype.name = 'AuthError'; 47 | 48 | 49 | // auth back-end 50 | var abe = null; 51 | 52 | 53 | /** 54 | * (Re-)initializes the authentication layer. 55 | * 56 | * @param {object} backEnd auth back-end module; must implement the API 57 | * shown in the above module docs. 58 | * @param {object} [config] configuration options for back-end module 59 | * @param {function} [callback] called when auth layer is ready, or an 60 | * error occurred during initialization 61 | */ 62 | function init(backEnd, config, callback) { 63 | abe = backEnd; 64 | if (abe && _.isFunction(abe.init)) { 65 | abe.init(config); 66 | } 67 | if (callback) return callback(); 68 | } 69 | 70 | 71 | /** 72 | * Authenticates a client/player. 73 | * 74 | * @param {string} token authentication token supplied by the client 75 | * @returns {string} player TSID (if successfully authenticated) 76 | * @throws {AuthError} if authentication failed 77 | */ 78 | function authenticate(token) { 79 | assert(abe !== undefined && abe !== null, 'no auth back-end configured'); 80 | var ret = abe.authenticate(token); 81 | log.info({token: token}, '%s successfully authenticated', ret); 82 | return ret; 83 | } 84 | 85 | 86 | /** 87 | * Retrieves an authentication token for the given player, or generates 88 | * a new one if necessary. 89 | * 90 | * @param {Player} player the player to generate a token for 91 | * @returns {string} a valid authentication token 92 | */ 93 | function getToken(player) { 94 | assert(abe !== undefined && abe !== null, 'no auth back-end configured'); 95 | var ret = abe.getToken(player); 96 | log.debug({token: ret}, 'auth token generated for %s', player); 97 | return ret; 98 | } 99 | 100 | 101 | /** 102 | * Returns the minimal guaranteed lifespan of an authentication token. 103 | * 104 | * @returns {number} guaranteed minimal token lifespan **in seconds**, 105 | * or `0` if the generated tokens are valid infinitely 106 | */ 107 | function getTokenLifespan() { 108 | assert(abe !== undefined && abe !== null, 'no auth back-end configured'); 109 | return abe.getTokenLifespan(); 110 | } 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleven-server", 3 | "version": "0.2.16", 4 | "description": "Eleven game server", 5 | "homepage": "http://elevengiants.com/", 6 | "license": "MIT", 7 | "contributors": [ 8 | { 9 | "name": "Markus Dolic", 10 | "email": "aroha@elevengiants.com", 11 | "url": "https://twitter.com/ElevenAroha" 12 | }, 13 | { 14 | "name": "Joey Thomas", 15 | "email": "josephthomas619@msn.com" 16 | }, 17 | { 18 | "name": "Aric Stewart", 19 | "email": "aricstewart@gmail.com" 20 | }, 21 | { 22 | "name": "scheijan", 23 | "email": "scheijan@gmail.com" 24 | }, 25 | { 26 | "name": "Justin Patrin", 27 | "email": "papercrane@reversefold.com" 28 | }, 29 | { 30 | "name": "Kyle Phelps", 31 | "email": "kphelps@projectdecibel.com" 32 | }, 33 | { 34 | "name": "Jim Condren", 35 | "email": "kaiyonalatar@elevengiants.com" 36 | } 37 | ], 38 | "author": "Markus Dolic (https://twitter.com/ElevenAroha)", 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/ElevenGiants/eleven-server.git" 42 | }, 43 | "main": "src/server.js", 44 | "directories": { 45 | "test": "test" 46 | }, 47 | "engines": { 48 | "node": "6.x" 49 | }, 50 | "dependencies": { 51 | "@slack/rtm-api": "^5.0.3", 52 | "async": "^0.9.0", 53 | "bunyan": "^1.0.1", 54 | "fibers": "^4.0.3", 55 | "gc-stats": "^1.4.0", 56 | "lodash": "^4.17.15", 57 | "lynx": "^0.2.0", 58 | "mathjs": "^3.20.2", 59 | "multitransport-jsonrpc": "^0.9.3", 60 | "murmurhash-js": "^1.0.0", 61 | "nconf": "^0.8.4", 62 | "node-dijkstra": "^2.5.0", 63 | "rethinkdb": "^2.3.1", 64 | "segfault-handler": "^1.3.0", 65 | "slack-node": "^0.2.0", 66 | "token": "0.0.0", 67 | "wait.for": "^0.6.4" 68 | }, 69 | "devDependencies": { 70 | "benchmark": "https://github.com/bestiejs/benchmark.js/tarball/master", 71 | "chai": "^1.9.1", 72 | "coveralls": "^3.0.9", 73 | "eslint": "^3.2.2", 74 | "eslint-plugin-lodash": "^1.10.1", 75 | "eslint-plugin-node": "^2.0.0", 76 | "jsdoc": "^3.3.0-alpha9", 77 | "mocha": "^7.0.0", 78 | "rewire": "^2.1.0" 79 | }, 80 | "scripts": { 81 | "preproc": "python tools/gsjs-preproc.py", 82 | "start": "NODE_PATH=src node --expose-gc ./src/server.js | node_modules/bunyan/bin/bunyan -o short", 83 | "test": "NODE_PATH=src node_modules/mocha/bin/mocha test/unit --exit", 84 | "functest": "NODE_PATH=src node_modules/mocha/bin/mocha test/func --exit", 85 | "inttest": "NODE_PATH=src node_modules/mocha/bin/mocha test/int --exit", 86 | "alltests": "NODE_PATH=src sh -c 'npm run test -- -R progress && npm run functest -- -R progress && npm run inttest -- -R progress && npm run lint'", 87 | "coverage": "NODE_PATH=src node node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha -- -R progress test/unit test/func", 88 | "coveralls": "NODE_PATH=src node node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha --report lcovonly -- -R progress test/unit test/func && cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js", 89 | "bench": "NODE_PATH=src node bench/runner.js", 90 | "lint": "node_modules/eslint/bin/eslint.js src test tools", 91 | "docs": "node node_modules/jsdoc/jsdoc.js --destination docs --configure jsdoc-conf.json src", 92 | "repl": "node tools/repl-client.js 7201" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /config_base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Game server configuration data. 5 | * This file contains reasonable default or dummy values and should not normally 6 | * be modified to suit a local server installation; instead, add configuration 7 | * for your specific local environment to 'config_local.js' (typically at least 8 | * the 'net' part), which is excluded from version control. 9 | * In particular, do NOT add any sensitive information (e.g. credentials or keys 10 | * for an actual public server) here. 11 | * Values in 'config_local.js' take precedence over the values in this file. 12 | */ 13 | 14 | module.exports = { 15 | net: { 16 | // If the server ID is not specified explicitly (e.g. via environment 17 | // variable), the server process will cycle through the following hash 18 | // and compare each entry's 'host' property with the list of network 19 | // interfaces returned by os.networkInterfaces. When a matching IP 20 | // address is found, the process will consider the respective config 21 | // block its own, and bind the GS instance(s) to that interface/port(s). 22 | gameServers: { 23 | gs01: { 24 | host: '127.0.0.1', 25 | ports: [ 26 | 1443, 27 | 1444, 28 | // add TCP ports here to add GS instances on this host 29 | ], 30 | }, 31 | // add entries here (e.g. gs02, gs03, ...) for additional GS hosts 32 | }, 33 | flashPolicyPort: 1843, 34 | assetServer: { 35 | host: '127.0.0.1', 36 | port: 8000, 37 | }, 38 | rpc: { 39 | // process number is added to the base port for each GS instance 40 | // (master = 0 (i.e. running on basePort), workers = 1, 2, 3, ...) 41 | basePort: 7000, 42 | timeout: 10000, // ms 43 | }, 44 | // AMF library to use ('js' or 'cc') 45 | amflib: 'js', 46 | // incoming AMF messages bigger than this are considered invalid 47 | maxMsgSize: 131072, 48 | heartbeat: { 49 | interval: 3000, 50 | timeout: 60000, 51 | }, 52 | }, 53 | proc: { 54 | // timeout (in ms) for graceful worker process shutdown: 55 | shutdownTimeout: 30000, 56 | // timeout for worker.kill() or SIGTERM, before sending SIGKILL: 57 | killTimeout: 5000, 58 | // global timeout for worker shutdown before master itself exits: 59 | masterTimeout: 45000, 60 | }, 61 | log: { 62 | // dir can be an absolute path, or relative to eleven-server directory 63 | dir: './log', 64 | level: { 65 | file: 'info', 66 | stdout: 'error', 67 | }, 68 | // include source file/line number in log messages: 69 | // (slow - do not use in production!) 70 | includeLoc: false, 71 | }, 72 | mon: { 73 | statsd: { 74 | enabled: true, 75 | host: '127.0.0.1', 76 | port: 8125, 77 | // optional prefix for the metrics names: 78 | prefix: '', 79 | }, 80 | }, 81 | debug: { 82 | // REPL server for live debugging/inspection 83 | repl: { 84 | enable: true, 85 | host: '127.0.0.1', // only local connections allowed by default 86 | basePort: 7200, 87 | }, 88 | stackTraceLimit: 20, 89 | // set to false to disable all NPC movement: 90 | npcMovement: true, 91 | }, 92 | gsjs: { 93 | // the GSJS configuration variant to load 94 | config: 'config_prod', 95 | }, 96 | god: { 97 | hidden_properties: ['ts', 'tsid', 'class_tsid', 'label', 98 | 'pcont', 99 | 'version', 'letime', 'rbtime', 'load_time', 'upd_time', 100 | 'lastUpdateTime', 'upd_gs', 101 | 'gstimers', 'gsintervals', 102 | 'package_intervals'] 103 | }, 104 | cache: { 105 | // cached data files. right now this just contains the pathfinding file 106 | pathfinding: '../pathfinding.json', 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /bench/suites/gsjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../setup'); 4 | var suite = new (require('benchmark')).Suite; 5 | module.exports = suite; 6 | var RC = require('data/RequestContext'); 7 | var pers = require('data/pers'); 8 | var pbeMock = require('../../test/mock/pbe'); 9 | var gsjsBridge = require('model/gsjsBridge'); 10 | var config = require('config'); 11 | var Group = require('model/Group'); 12 | var Geo = require('model/Geo'); 13 | var Location = require('model/Location'); 14 | var Player = require('model/Player'); 15 | var Item = require('model/Item'); 16 | 17 | var loc; 18 | var pc; 19 | var trant; 20 | var apple; 21 | 22 | 23 | // spoof a single common request context for all tests 24 | RC.getContext = function getContext() { 25 | return new RC('DUMMY_BENCH_RC'); 26 | }; 27 | 28 | 29 | suite.asyncSetup = function (done) { 30 | pers.init(pbeMock); 31 | // spoof a GS worker config (so location gets properly initialized) 32 | config.init(false, { 33 | gsid: 'gs01-01', 34 | net: { 35 | gameServers: { 36 | gs01: {host: '127.0.0.1', ports: [3000]}, 37 | }, 38 | }, 39 | gsjs: { 40 | config: 'config_prod', 41 | }, 42 | }, {}); 43 | gsjsBridge.init(true, function cb() { 44 | // hi variants tracker group required for pc login: 45 | pers.create(Group, {tsid: 'RIFUKAGPIJC358O', class_tsid: 'hi_variants_tracker'}); 46 | var geo = Geo.create({tsid: 'GXYZ', layers: {middleground: { 47 | platform_lines: {plat_1: { 48 | start: {x: -100, y: 0}, end: {x: 100, y: 0}, 49 | platform_item_perm: -1, platform_pc_perm: -1, 50 | }}}}}); 51 | pbeMock.getDB()[geo.tsid] = geo; 52 | loc = Location.create(geo); 53 | pbeMock.getDB()[loc.tsid] = loc; 54 | pc = Player.create({ 55 | tsid: 'PXYYZ', 56 | label: 'Chuck', 57 | class_tsid: 'human', 58 | skip_newux: true, 59 | location: loc, 60 | x: 0, y: -100, 61 | last_location: {}, 62 | }); 63 | pbeMock.getDB()[pc.tsid] = pc; 64 | trant = Item.create('trant_bean'); 65 | trant.setContainer(loc, 12, 34); 66 | trant.die = function () {}; // prevent dying 67 | pbeMock.getDB()[trant.tsid] = trant; 68 | apple = Item.create('apple'); 69 | apple.setContainer(pc, 0); 70 | pbeMock.getDB()[apple.tsid] = apple; 71 | done(); 72 | }); 73 | }; 74 | 75 | 76 | suite.on('complete', function () { 77 | process.exit(); 78 | }); 79 | 80 | 81 | suite.add('login_start', function () { 82 | gsjsBridge.getMain().processMessage(pc, {type: 'login_start'}); 83 | }); 84 | 85 | 86 | suite.add('login_end', function () { 87 | gsjsBridge.getMain().processMessage(pc, {type: 'login_end'}); 88 | }); 89 | 90 | 91 | suite.add('relogin_start', function () { 92 | gsjsBridge.getMain().processMessage(pc, {type: 'relogin_start'}); 93 | }); 94 | 95 | 96 | suite.add('relogin_end', function () { 97 | gsjsBridge.getMain().processMessage(pc, {type: 'relogin_end'}); 98 | }); 99 | 100 | 101 | suite.add('groups_chat', function () { 102 | gsjsBridge.getMain().processMessage(pc, {type: 'local_chat', txt: 'test!'}); 103 | }); 104 | 105 | 106 | suite.add('itemstack_verb_menu', function () { 107 | gsjsBridge.getMain().processMessage(pc, {type: 'itemstack_verb_menu', 108 | itemstack_tsid: trant.tsid}); 109 | }); 110 | 111 | 112 | suite.add('itemstack_verb', function () { 113 | gsjsBridge.getMain().processMessage(pc, {type: 'itemstack_verb', 114 | itemstack_tsid: apple.tsid, verb: 'lick'}); 115 | }); 116 | 117 | 118 | suite.add('move_xy', function () { 119 | gsjsBridge.getMain().processMessage(pc, {type: 'move_xy', x: 1, y: 1}); 120 | }); 121 | 122 | 123 | suite.add('trant.onInterval', function () { 124 | trant.onInterval(); 125 | }); 126 | -------------------------------------------------------------------------------- /test/unit/comm/sessionMgr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var rewire = require('rewire'); 5 | var sessionMgr = rewire('comm/sessionMgr'); 6 | var getDummySocket = require('../../helpers').getDummySocket; 7 | var gsjsBridge = require('model/gsjsBridge'); 8 | 9 | 10 | suite('sessionMgr', function () { 11 | 12 | suiteSetup(function () { 13 | gsjsBridge.reset({gsjsMain: { 14 | processMessage: _.noop, 15 | }}); 16 | }); 17 | 18 | suiteTeardown(function () { 19 | gsjsBridge.reset(); 20 | // just in case any other test relies on sessionMgr 21 | sessionMgr.init(); 22 | }); 23 | 24 | setup(function () { 25 | sessionMgr.init(); 26 | }); 27 | 28 | 29 | suite('newSession', function () { 30 | 31 | test('creates and adds new Session object', function () { 32 | var s = sessionMgr.newSession(getDummySocket()); 33 | var sessions = sessionMgr.__get__('sessions'); 34 | assert.isString(s.id); 35 | assert.isTrue(s.id.length >= 8); 36 | assert.strictEqual(sessionMgr.getSessionCount(), 1); 37 | assert.strictEqual(Object.keys(sessions)[0], s.id); 38 | assert.strictEqual(s.listeners('close')[0], 39 | sessionMgr.__get__('onSessionClose')); 40 | }); 41 | 42 | test('generates unique IDs in consecutive calls', function () { 43 | this.slow(400); // prevent mocha from flagging this test as slow 44 | var id, prev; 45 | for (var i = 0; i < 100; i++) { 46 | prev = id; 47 | id = sessionMgr.newSession(getDummySocket()).id; 48 | assert.notStrictEqual(id, prev); 49 | } 50 | }); 51 | }); 52 | 53 | 54 | suite('onSessionClose', function () { 55 | 56 | test('does its job', function () { 57 | var s = sessionMgr.newSession(getDummySocket()); 58 | assert.strictEqual(sessionMgr.getSessionCount(), 1); 59 | s.emit('close', s); 60 | assert.strictEqual(sessionMgr.getSessionCount(), 0); 61 | }); 62 | }); 63 | 64 | 65 | suite('forEachSession', function () { 66 | 67 | test('works as expected', function (done) { 68 | var called = []; 69 | var s1 = sessionMgr.newSession(getDummySocket()); 70 | var s2 = sessionMgr.newSession(getDummySocket()); 71 | var s3 = sessionMgr.newSession(getDummySocket()); 72 | s1.loggedIn = true; 73 | s2.loggedIn = true; 74 | s3.loggedIn = true; 75 | sessionMgr.forEachSession( 76 | function check(session, cb) { 77 | called.push(session.id); 78 | cb(); 79 | }, 80 | function cb(err, res) { 81 | assert.sameMembers(called, [s1.id, s2.id, s3.id]); 82 | return done(err); 83 | } 84 | ); 85 | }); 86 | }); 87 | 88 | 89 | suite('sendToAll', function () { 90 | 91 | test('works as expected', function (done) { 92 | var s1 = sessionMgr.newSession(getDummySocket()); 93 | s1.loggedIn = true; 94 | var s1Called = false; 95 | s1.send = function (msg) { 96 | s1Called = true; 97 | throw new Error('should be ignored'); 98 | }; 99 | var s2 = sessionMgr.newSession(getDummySocket()); 100 | s2.loggedIn = true; 101 | var s2Called = false; 102 | s2.send = function (msg) { 103 | s2Called = true; 104 | assert.deepEqual(msg, {blerg: 1, txt: 'blah'}); 105 | }; 106 | var s3 = sessionMgr.newSession(getDummySocket()); 107 | s3.loggedIn = false; // will be skipped 108 | var s3Called = false; 109 | s3.send = function (msg) { 110 | s3Called = true; 111 | }; 112 | sessionMgr.sendToAll({blerg: 1, txt: 'blah'}, function callback() { 113 | assert.isTrue(s1Called, 'message sent to s1 (or tried to)'); 114 | assert.isTrue(s2Called, 'message sent to s2'); 115 | assert.isFalse(s3Called, 'message not sent to s3'); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eleven-server # 2 | This is the game server for [Eleven Giants](http://elevengiants.com/). 3 | 4 | **Work in progress disclaimer:** 5 | _The server is currently only able to run a limited portion of the game. To 6 | actually start up the client with it, additional components are required, which 7 | are not publicly available at this time. If you want to get involved in the 8 | development process, please [let us know!](http://elevengiants.com/contact.php)_ 9 | 10 | 11 | ## Prerequisites ## 12 | Development and testing usually happens in our Debian based Vagrant VM, so that 13 | is probably the least painful way to get up and running. Setup instructions for 14 | the VM can be found in our internal wiki. 15 | 16 | For the adventurous, it should be possible to run the server on most platforms 17 | that support [Node.js](http://nodejs.org/) v6. At the moment you also need 18 | [Python 2.7](https://www.python.org/download/releases/2.7/) for the GSJS 19 | preprocessor script. 20 | 21 | 22 | ## Setup ## 23 | **Note:** _The following setup steps are **not** necessary if you are using the 24 | Vagrant box and created the VM with the _`eleven-server`_ and _`eleven-gsjs`_ 25 | repos already present._ 26 | 27 | Clone this repository and [`eleven-gsjs`](https://github.com/ElevenGiants/eleven-gsjs) 28 | in the same parent directory. Directory names are assumed to match the Git 29 | repository names. Call 30 | ```bash 31 | npm -s run preproc 32 | ``` 33 | to run the preprocessor script that prepares the GSJS code for embedding in the 34 | game server. 35 | 36 | Once that has finished successfully, compile the required non-JS npm packages: 37 | ```bash 38 | npm install 39 | ``` 40 | If you are running the Vagrant VM on Windows, add `--no-bin-links` as an 41 | argument (necessary because symlinks cannot be created in folders shared between 42 | the VM and the Windows host). 43 | 44 | The server expects environment specific parts of the configuration in a file 45 | called `config_local.js` in its root directory. Copy one of the 46 | `config_local.js.SAMPLE_*` files and adjust it according to your needs. 47 | 48 | 49 | ## Operation ## 50 | All actions are invoked via [`npm`](https://www.npmjs.org/doc/cli/npm.html). 51 | The following operations are available: 52 | 53 | * `test` run the unit tests (with [mocha](https://mochajs.org/)) 54 | * `functest` run functional tests 55 | * `inttest` run integration tests (depends on external components) 56 | * `alltests` run all tests back-to-back with reduced output (also includes the 57 | `lint` task below); handy as a basic smoke test before committing 58 | * `bench` run benchmarks 59 | * `lint` perform static code analysis with [ESLint](http://eslint.org/) 60 | * `docs` generate HTML documentation with [JSDoc](http://usejsdoc.org/) 61 | * `start` run the server 62 | 63 | These scripts can be called using `npm run-script` (or the alias `npm run`); the 64 | `-s` flag hides distracting additional output, e.g.: 65 | ```bash 66 | npm -s run test 67 | ``` 68 | 69 | To run specific tests or benchmark suites, append arguments for the test or 70 | benchmark runner with `--`, e.g.: 71 | ```bash 72 | npm -s run test -- --grep objrefProxy 73 | npm -s run bench -- utils.js 74 | ``` 75 | (this requires npm >= 2.0.0) 76 | 77 | 78 | ## Contributing ## 79 | Help is always welcome! If you are interested, please [get in touch] 80 | (http://elevengiants.com/contact.php) to get access to our [Slack] 81 | (http://slack.com/) instance, internal documentation, guidelines and other 82 | resources. 83 | 84 | (If you are in fact already signed up and ready to go, have a look 85 | [here](https://github.com/ElevenGiants/eleven-server/blob/master/CONTRIBUTING.md)). 86 | 87 | 88 | ## License ## 89 | [MIT](https://github.com/ElevenGiants/eleven-server/blob/master/LICENSE) 90 | -------------------------------------------------------------------------------- /src/model/OrderedHash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = OrderedHash; 4 | 5 | 6 | var _ = require('lodash'); 7 | var orProxy = require('data/objrefProxy'); 8 | var utils = require('utils'); 9 | 10 | 11 | /** 12 | * A class that provides sorted map functionality in JS. Entries are 13 | * sorted using natural order of keys. 14 | * 15 | * For the sake of simplicity, property keys are sorted on read access 16 | * (resp. on enumeration); this class is obviously **not** well suited 17 | * for big collections with few writes and many reads. 18 | * 19 | * @param {object} [data] optional initial content (properties are 20 | * shallow-copied into the hash) 21 | * @constructor 22 | */ 23 | function OrderedHash(data) { 24 | // wrap the actual class in a proxy that makes sure for...in loops 25 | // loop over the properties in natural order of their keys 26 | return new Proxy(new OrderedHashAux(data), { 27 | enumerate: function enumerate(target) { 28 | var sortedKeys = target.sortedKeys(); 29 | var l = sortedKeys.length; 30 | var i = 0; 31 | return { 32 | next: function next() { 33 | if (i === l) return {done: true}; 34 | return { 35 | done: false, 36 | value: sortedKeys[i++], 37 | }; 38 | }, 39 | }; 40 | }, 41 | ownKeys: function ownKeys(target) { 42 | return target.sortedKeys(); 43 | }, 44 | get: function get(target, name, receiver) { 45 | if (name === 'toJSON') { 46 | // TODO: check if this is still necessary 47 | // required to prevent weird context-less "illegal access" 48 | // errors when stringifying proxied objects (or objects with 49 | // proxied children) 50 | // see: https://github.com/tvcutsem/harmony-reflect/issues/38 51 | return function toJSON() { 52 | return target; 53 | }; 54 | } 55 | return target[name]; 56 | }, 57 | }); 58 | } 59 | 60 | 61 | function OrderedHashAux(data) { 62 | orProxy.copyOwnProps(data, this); 63 | } 64 | 65 | 66 | /** 67 | * Helper function for {@link OrderedHashAux#first|first} and {@link 68 | * OrderedHashAux#last|last}. 69 | * 70 | * @returns {array} sorted list of the hash keys 71 | * @private 72 | */ 73 | OrderedHashAux.prototype.sortedKeys = function sortedKeys() { 74 | return Object.keys(this).sort(); 75 | }; 76 | utils.makeNonEnumerable(OrderedHashAux.prototype, 'sortedKeys'); 77 | 78 | 79 | /** 80 | * Retrieves the hash entry whose key is first (according to natural 81 | * order). 82 | * 83 | * @returns {*} first value in the hash 84 | */ 85 | OrderedHashAux.prototype.first = function first() { 86 | return this[this.sortedKeys()[0]]; 87 | }; 88 | utils.makeNonEnumerable(OrderedHashAux.prototype, 'first'); 89 | 90 | 91 | /** 92 | * Retrieves the hash entry whose key is last (according to natural 93 | * order). 94 | * 95 | * @returns {*} last value in the hash 96 | */ 97 | OrderedHashAux.prototype.last = function last() { 98 | return this[this.sortedKeys().slice(-1)]; 99 | }; 100 | utils.makeNonEnumerable(OrderedHashAux.prototype, 'last'); 101 | 102 | 103 | /** 104 | * Returns the length of the hash. 105 | * 106 | * @returns {number} number of key/value pairs stored in the hash 107 | */ 108 | OrderedHashAux.prototype.length = function length() { 109 | return Object.keys(this).length; 110 | }; 111 | utils.makeNonEnumerable(OrderedHashAux.prototype, 'length'); 112 | 113 | 114 | /** 115 | * Clears the hash by removing all non-function direct properties. 116 | */ 117 | OrderedHashAux.prototype.clear = function clear() { 118 | for (var prop in this) { 119 | if (this.hasOwnProperty(prop) && !_.isFunction(this[prop])) { 120 | delete this[prop]; 121 | } 122 | } 123 | }; 124 | utils.makeNonEnumerable(OrderedHashAux.prototype, 'clear'); 125 | -------------------------------------------------------------------------------- /test/func/model/GameObject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var GameObject = require('model/GameObject'); 6 | 7 | 8 | suite('GameObject', function () { 9 | 10 | var FIXTURES_PATH = path.resolve(path.join(__dirname, '../fixtures')); 11 | 12 | function getFixtureJson(fname) { 13 | var data = fs.readFileSync(path.join(FIXTURES_PATH, fname)); 14 | return JSON.parse(data); 15 | } 16 | 17 | 18 | suite('game object loading/initialization', function () { 19 | 20 | test('keeps timestamp from data if there is one', function () { 21 | var data = getFixtureJson('GIFPV9EMLT72DP4.json'); 22 | var go = new GameObject(data); 23 | assert.strictEqual(go.ts, data.ts); 24 | }); 25 | }); 26 | 27 | 28 | suite('preparation for serialization', function () { 29 | 30 | test('serialized data is equivalent to source data', function () { 31 | var data = getFixtureJson('IHFK8C8NB6J2FJ5.json'); 32 | var go = new GameObject(data); 33 | assert.deepEqual(go.serialize(), data); 34 | }); 35 | }); 36 | 37 | 38 | suite('timers', function () { 39 | 40 | test('basic timer call', function (done) { 41 | var go = new GameObject(); 42 | var called = false; 43 | go.timerTest = function timerTest(arg) { 44 | called = true; 45 | assert.strictEqual(arg, 'grunt'); 46 | done(); 47 | }; 48 | go.setGsTimer({ 49 | fname: 'timerTest', 50 | delay: 10, 51 | args: ['grunt'], 52 | }); 53 | assert.isFalse(called); 54 | }); 55 | 56 | test('basic interval call', function (done) { 57 | var go = new GameObject(); 58 | var calls = 0; 59 | go.intTest = function intTest() { 60 | calls++; 61 | if (calls === 3) { 62 | delete go.gsTimers.intTest; // clean up 63 | done(); 64 | } 65 | }; 66 | go.setGsTimer({ 67 | fname: 'intTest', 68 | delay: 5, 69 | interval: true, 70 | }); 71 | assert.strictEqual(calls, 0); 72 | }); 73 | 74 | test('timers on stale objects are not executed', function (done) { 75 | var go = new GameObject(); 76 | var called = false; 77 | go.foo = function () { 78 | called = true; 79 | }; 80 | go.setGsTimer({fname: 'foo', delay: 5}); 81 | setTimeout(function () { 82 | assert.isTrue(go.stale); 83 | assert.isFalse(called); 84 | done(); 85 | }, 10); 86 | go.stale = true; 87 | }); 88 | 89 | test('intervals on stale objects are cleared', function (done) { 90 | var go = new GameObject(); 91 | var calledOnStale = false; 92 | var c = 0; 93 | go.foo = function () { 94 | c++; 95 | if (go.stale) calledOnStale = true; 96 | go.stale = true; 97 | }; 98 | go.setGsTimer({fname: 'foo', delay: 5, interval: true}); 99 | setTimeout(function () { 100 | assert.isTrue(go.stale); 101 | assert.strictEqual(c, 1); 102 | assert.isFalse(calledOnStale); 103 | done(); 104 | }, 20); 105 | }); 106 | 107 | test('timers/intervals are persistently removed after errors', function (done) { 108 | var go = new GameObject(); 109 | var c = 0; 110 | go.foo = function () { 111 | throw new Error('something went wrong here'); 112 | }; 113 | go.bar = function () { 114 | c++; 115 | throw new Error('something went wrong here too'); 116 | }; 117 | go.setGsTimer({fname: 'foo', delay: 5}); 118 | go.setGsTimer({fname: 'bar', delay: 5, interval: true}); 119 | assert.property(go.gsTimers, 'foo'); 120 | setTimeout(function () { 121 | assert.notProperty(go.gsTimers, 'foo', 122 | 'timer removed in spite of an execution error'); 123 | assert.notProperty(go.gsTimers, 'bar', 124 | 'interval removed in spite of an execution error'); 125 | assert.strictEqual(c, 1, 'interval only executed once'); 126 | done(); 127 | }, 20); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Eleven game server # 2 | :+1: First of all, thanks for taking the time to read this! :+1: 3 | 4 | The following is a set of guidelines for contributing to the [Eleven game server](https://github.com/ElevenGiants/eleven-server) 5 | code. These rules are not set in stone — use your best judgement, and feel free 6 | to propose changes if you think something should be improved. 7 | 8 | 9 | ## Task distribution, planning, roadmap ## 10 | Work on the game server is managed on [Trello](https://trello.com/b/ZdLBfI1l/game-server). 11 | Tasks waiting to be picked up by somebody are in the **To Do** list, and 12 | generally roughly sorted by priority (decreasing from top to bottom). 13 | Relatively easy tasks that might be a good starting point for contributing to the 14 | GS are tagged `low hanging fruit`. 15 | The **TBD** list is also pending, but those topics are waiting on more concrete 16 | specs, further discussion or similar. 17 | 18 | When you are starting to work on an item, assign yourself to the respective 19 | card and move it to the **Doing** list. If you are working on something that is 20 | not on Trello yet, please create a card for it. This makes it easy to see for 21 | everyone what is going on, and gives others the opportunity to participate and 22 | provide feedback. 23 | 24 | 25 | ## Development flow ## 26 | Development is following the [​GitHub flow](https://guides.github.com/introduction/flow/index.html) 27 | model, with dev branches being created in each developer's own fork of the main 28 | repo. It's useful to include the Trello card number in the branch name, e.g. 29 | `trello#123_awesome-feature` (you can find the number under the "Share and 30 | more..." link on each card). 31 | 32 | 33 | ## Implementation ## 34 | Please follow our [Javascript style conventions](http://trac.elevengiants.com/trac/wiki/JsCodeStyle), 35 | and generally try to maintain a consistent "feel" with the existing codebase. 36 | This is open source software — consider the people who will read and work with 37 | your code (which includes future you), and make it look nice for them. 38 | 39 | Document modules and public functions with [JSDoc](http://usejsdoc.org/) 40 | comments (*public* in a colloquial sense, i.e. anything that is not purely for 41 | module-internal use). 42 | 43 | Add unit tests for new functions, and extend/adjust tests where appropriate when 44 | modifying existing functions. For code that involves other GS modules/components 45 | that cannot be easily stubbed, add functional tests. Tests that involve 46 | "external" dependencies (like a database or network resources) should be added 47 | to the integration test suite which is not run automatically by the build 48 | system. 49 | 50 | Always write clear log messages for your commits. One-line messages are fine for 51 | small things, bigger changes should look like this: 52 | 53 | $ git commit -m "A brief summary of the commit 54 | > 55 | > A paragraph describing what changed and its impact, or a 56 | > * list 57 | > * of 58 | > * changes" 59 | 60 | * use the present tense ("add feature", not "added feature") 61 | * use the imperative mood ("fix memory leak", not "fixes memory leak") 62 | * wrap lines at 72 characters 63 | * reference relevant external resources (e.g. Trello cards, bug tickets for 64 | libraries, Slack archive links etc) 65 | 66 | ## Pull requests ## 67 | When a feature or bugfix is finished, create a pull request for your branch. 68 | Before submitting it, make sure lint and all tests pass (`npm -s run alltests`), 69 | and if your work touched any core components, check that benchmark results have 70 | not significantly degraded. 71 | 72 | Clean up the commit history if necessary (use Git squash and/or rebase). Keep 73 | your commits atomic: no single commit should break existing functionality, 74 | especially not the tests. All of this simplifies future debugging and 75 | maintenance work. 76 | -------------------------------------------------------------------------------- /test/func/model/Quest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gsjsBridge = require('model/gsjsBridge'); 4 | var RC = require('data/RequestContext'); 5 | var Geo = require('model/Geo'); 6 | var Location = require('model/Location'); 7 | var Quest = require('model/Quest'); 8 | var pers = require('data/pers'); 9 | var utils = require('utils'); 10 | var pbeMock = require('../../mock/pbe'); 11 | 12 | 13 | suite('Quest', function () { 14 | 15 | setup(function () { 16 | gsjsBridge.reset(); 17 | pers.init(pbeMock); 18 | }); 19 | 20 | teardown(function () { 21 | gsjsBridge.reset(); 22 | pers.init(); // disable mock back-end 23 | }); 24 | 25 | 26 | suite('ctor', function () { 27 | 28 | test('does not override changed prototype properties', function () { 29 | var ctor = gsjsBridge.getProto('quests', 'lightgreenthumb_1').constructor; 30 | var q = new ctor({accepted: true, class_tsid: 'lightgreenthumb_1'}); 31 | assert.strictEqual(q.accepted, true); 32 | }); 33 | }); 34 | 35 | 36 | suite('create', function () { 37 | 38 | test('does its job', function (done) { 39 | new RC().run( 40 | function () { 41 | var l = Location.create(Geo.create()); 42 | var q = Quest.create('beer_guzzle', l); 43 | assert.isTrue(utils.isQuest(q)); 44 | assert.strictEqual(q.class_tsid, 'beer_guzzle'); 45 | assert.strictEqual(q.owner, l); 46 | }, 47 | function cb(err, res) { 48 | if (err) return done(err); 49 | var db = pbeMock.getDB(); 50 | assert.strictEqual(pbeMock.getCounts().write, 3); 51 | assert.strictEqual(Object.keys(db).length, 3); 52 | return done(); 53 | } 54 | ); 55 | }); 56 | 57 | test('fails on invalid owner type', function () { 58 | assert.throw(function () { 59 | new RC().run(function () { 60 | var geo = Geo.create(); 61 | Quest.create('beer_guzzle', geo); 62 | }); 63 | }, assert.AssertionError); 64 | }); 65 | }); 66 | 67 | 68 | suite('del', function () { 69 | 70 | test('flags owner/quest DCs as dirty', function (done) { 71 | var db = pbeMock.getDB(); 72 | db.G1 = { 73 | tsid: 'G1', 74 | }; 75 | db.L1 = { 76 | tsid: 'L1', 77 | players: ['P1'], 78 | jobs: { 79 | 'proto-IDOOR': { 80 | class_ids: { 81 | job_proto_door: { 82 | class_id: 'job_proto_door', 83 | label: 'Build a New Floor', 84 | instance: {objref: true, tsid: 'Q2'}, 85 | }, 86 | }, 87 | }, 88 | }, 89 | }; 90 | db.P1 = { 91 | tsid: 'P1', 92 | label: 'a player', 93 | location: {objref: true, tsid: 'L1'}, 94 | quests: { 95 | todo: {objref: true, label: 'To Do', tsid: 'D1'}, 96 | done: {objref: true, label: 'Done', tsid: 'D2'}, 97 | }, 98 | }; 99 | db.D1 = { 100 | tsid: 'D1', 101 | owner: {objref: true, label: 'a player', tsid: 'P1'}, 102 | quests: { 103 | beer_guzzle: {objref: true, tsid: 'Q1'}, 104 | }, 105 | }; 106 | db.D2 = { 107 | tsid: 'D2', 108 | owner: {objref: true, label: 'a player', tsid: 'P1'}, 109 | quests: {}, 110 | }; 111 | db.Q1 = { 112 | tsid: 'Q1', 113 | owner: {objref: true, label: 'a player', tsid: 'P1'}, 114 | class_tsid: 'beer_guzzle', 115 | }; 116 | db.Q2 = { 117 | tsid: 'Q2', 118 | owner: {objref: true, label: 'a player\'s house', tsid: 'L1'}, 119 | class_tsid: 'job_proto_door', 120 | }; 121 | new RC().run( 122 | function () { 123 | pers.get('P1').quests.todo.quests.beer_guzzle.del(); 124 | var locJobs = pers.get('L1').jobs['proto-IDOOR'].class_ids; 125 | locJobs.job_proto_door.instance.del(); 126 | }, 127 | function cb(err) { 128 | if (err) return done(err); 129 | assert.sameMembers(pbeMock.getDeletes(), ['Q1', 'Q2']); 130 | assert.includeMembers(pbeMock.getWrites(), ['D1', 'L1']); 131 | return done(); 132 | } 133 | ); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/unit/data/RequestContext.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var rewire = require('rewire'); 5 | var RC = rewire('data/RequestContext'); 6 | var persMock = require('../../mock/pers'); 7 | 8 | 9 | suite('RequestContext', function () { 10 | 11 | setup(function () { 12 | RC.__set__('pers', persMock); 13 | persMock.reset(); 14 | }); 15 | 16 | teardown(function () { 17 | RC.__set__('pers', rewire('data/pers')); 18 | }); 19 | 20 | 21 | suite('getContext', function () { 22 | 23 | test('fails when called outside a request context', function () { 24 | assert.throw(function () { 25 | RC.getContext(); 26 | }, assert.AssertionError); 27 | }); 28 | 29 | test('does its job', function (done) { 30 | new RC('testlogtag').run(function () { 31 | var ctx = RC.getContext(); 32 | assert.isDefined(ctx); 33 | assert.strictEqual(ctx.tag, 'testlogtag'); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | 40 | suite('run', function () { 41 | 42 | test('initializes request data structures', function (done) { 43 | new RC().run(function () { 44 | var ctx = RC.getContext(); 45 | assert.property(ctx, 'cache'); 46 | assert.deepEqual(ctx.cache, {}); 47 | assert.property(ctx, 'unload'); 48 | assert.deepEqual(ctx.unload, {}); 49 | done(); 50 | }); 51 | }); 52 | 53 | test('waits for persistence operation callback if desired', function (done) { 54 | var persDone = false; 55 | RC.__set__('pers', { 56 | postRequestProc: function postRequestProc(dl, ul, logtag, callback) { 57 | // simulate an async persistence operation that takes 20ms 58 | setTimeout(function () { 59 | persDone = true; 60 | callback(); 61 | }, 20); 62 | }, 63 | }); 64 | new RC().run( 65 | function dummy() { 66 | return 7; 67 | }, 68 | function callback(err, res) { 69 | if (err) return done(err); 70 | assert.isTrue(persDone); 71 | assert.strictEqual(res, 7); 72 | return done(); 73 | }, 74 | true 75 | ); 76 | // RC.pers is restored in suite teardown 77 | }); 78 | 79 | test('unloads objects scheduled for unloading', function (done) { 80 | var rc = new RC(); 81 | rc.run( 82 | function () { 83 | rc.setUnload({tsid: 'IA'}); 84 | rc.setUnload({tsid: 'IB', deleted: true}); 85 | assert.deepEqual(Object.keys(rc.unload), ['IA', 'IB']); 86 | assert.deepEqual(persMock.getUnloadList(), {}, 87 | 'request in progress, list not processed yet'); 88 | }, 89 | function callback() { 90 | assert.deepEqual(persMock.getUnloadList(), { 91 | IA: {tsid: 'IA'}, 92 | IB: {tsid: 'IB', deleted: true}, 93 | }); 94 | } 95 | ); 96 | done(); 97 | }); 98 | 99 | test('runs request function and returns its return value in callback', function (done) { 100 | var derp = 1; 101 | new RC().run(function () { 102 | derp = 3; 103 | return 'hooray'; 104 | }, 105 | function callback(err, res) { 106 | assert.strictEqual(derp, 3); 107 | assert.strictEqual(res, 'hooray'); 108 | done(); 109 | }); 110 | }); 111 | 112 | test('passes errors thrown by the request function back in callback', function (done) { 113 | new RC().run(function () { 114 | throw new Error('meh'); 115 | }, 116 | function callback(err, res) { 117 | assert.isDefined(err); 118 | assert.strictEqual(err.message, 'meh'); 119 | done(); 120 | }); 121 | }); 122 | 123 | test('does not invoke callback twice in case of errors in callback', function () { 124 | var calls = 0; 125 | assert.throw(function () { 126 | new RC().run( 127 | _.noop, 128 | function callback(err, res) { 129 | calls++; 130 | assert.strictEqual(calls, 1); 131 | throw new Error('error in callback'); 132 | } 133 | ); 134 | }, Error, 'error in callback'); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /src/comm/slackNotify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Functions for sending notifications/alerts to Slack (using 5 | * {@link https://api.slack.com/incoming-webhooks|incoming WebHooks}). 6 | * 7 | * @module 8 | */ 9 | 10 | // public interface 11 | module.exports = { 12 | init: init, 13 | info: info, 14 | warning: warning, 15 | alert: alert, 16 | }; 17 | 18 | var _ = require('lodash'); 19 | var config = require('config'); 20 | var Slack = require('slack-node'); 21 | var util = require('util'); 22 | 23 | var cfg; 24 | var slack; 25 | 26 | 27 | function init() { 28 | slack = new Slack(); 29 | cfg = config.get('slack:notify'); 30 | slack.setWebhook(cfg.webhookUrl); 31 | } 32 | 33 | 34 | /** 35 | * Sends an informational message to Slack. 36 | * 37 | * @param {object} [options] can be used to override default webhook 38 | * options from the GS config 39 | * @param {string} [options.channel] custom Slack channel (or user or 40 | * group) to send the message to 41 | * @param {string} msg the message to send (may contain placeholders, 42 | * processed by `util.format`) 43 | * @param {...string} [vals] values for the placeholders in `msg` 44 | */ 45 | function info(options, msg) { 46 | send(arguments, ':white_check_mark:'); 47 | } 48 | 49 | 50 | /** 51 | * Sends a warning message to Slack. 52 | * 53 | * @param {object} [options] can be used to override default webhook 54 | * options from the GS config 55 | * @param {string} [options.channel] custom Slack channel (or user or 56 | * group) to send the message to 57 | * @param {string} msg the message to send (may contain placeholders, 58 | * processed by `util.format`) 59 | * @param {...string} [vals] values for the placeholders in `msg` 60 | */ 61 | function warning(options, msg) { 62 | send(arguments, ':warning:'); 63 | } 64 | 65 | 66 | /** 67 | * Sends an alert message to Slack. 68 | * 69 | * @param {object} [options] can be used to override default webhook 70 | * options from the GS config 71 | * @param {string} [options.channel] custom Slack channel (or user or 72 | * group) to send the message to 73 | * @param {string} msg the message to send (may contain placeholders, 74 | * processed by `util.format`) 75 | * @param {...string} [vals] values for the placeholders in `msg` 76 | */ 77 | function alert(options, msg) { 78 | send(arguments, ':rotating_light:'); 79 | } 80 | 81 | 82 | /** 83 | * Sends a message to a Slack incoming WebHook (if the respective 84 | * integration is configured). 85 | * 86 | * @param {array} args webhook call payload: one or more string 87 | * elements that will be formatted through `util.format`, and 88 | * optionally a leading `object` type element that may contain 89 | * a custom `channel` to send the message to 90 | * @param {string} [icon] an optional prefix for the message (typically 91 | * an emoji like `:warning:`) 92 | * @private 93 | */ 94 | function send(args, icon) { 95 | if (!cfg) { 96 | log.debug({args: args}, 'Slack webhook call skipped (not configured)'); 97 | return; 98 | } 99 | var webhookParams = { 100 | icon_emoji: ':bcroc:', 101 | username: util.format('%s (%s)', cfg.botName, config.getGsid()), 102 | channel: cfg.channel, 103 | }; 104 | // function parameter handling 105 | if (_.isObject(args[0])) { 106 | webhookParams.channel = args[0].channel || webhookParams.channel; 107 | args = Array.prototype.slice.call(args, 1); 108 | } 109 | // format message content 110 | var text = util.format.apply(null, args); 111 | if (_.isString(icon) && icon.length) { 112 | text = util.format('%s %s', icon, text); 113 | } 114 | webhookParams.text = text; 115 | // invoke webhook 116 | log.debug(webhookParams, 'calling Slack webhook'); 117 | slack.webhook(webhookParams, function callback(err, res) { 118 | if (err) { 119 | log.error(err, 'failed to call Slack webhook'); 120 | } 121 | }); 122 | } 123 | 124 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "log": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 2017, 7 | }, 8 | "env": { 9 | "node": true, 10 | "es6": true 11 | }, 12 | "extends": "eslint:recommended", 13 | "plugins": ["node", "lodash"], 14 | "settings": { 15 | "lodash": { 16 | "version": 3 17 | } 18 | }, 19 | "rules": { 20 | "array-bracket-spacing": [1, "never"], 21 | "brace-style": [1, "stroustrup"], 22 | "camelcase": [1, {"properties": "never"}], 23 | "comma-dangle": [1, "always-multiline"], 24 | "comma-spacing": 1, 25 | "comma-style": [1, "last"], 26 | "complexity": [1, 15], 27 | "computed-property-spacing": [1, "never"], 28 | "consistent-this": [1, "self"], 29 | "curly": [2, "multi-line"], 30 | "dot-location": [1, "property"], 31 | "dot-notation": 1, 32 | "eol-last": 1, 33 | "eqeqeq": 2, 34 | "func-names": 1, 35 | "indent": [1, "tab", {"SwitchCase": 1}], 36 | "key-spacing": 1, 37 | "keyword-spacing": 1, 38 | "linebreak-style": [1, "unix"], 39 | "lodash/callback-binding": 2, 40 | "lodash/chain-style": 1, 41 | "lodash/collection-method-value": 2, 42 | "lodash/collection-return": 2, 43 | "lodash/identity-shorthand": 1, 44 | "lodash/matches-prop-shorthand": 1, 45 | "lodash/matches-shorthand": 1, 46 | "lodash/no-commit": 1, 47 | "lodash/no-double-unwrap": 2, 48 | "lodash/no-extra-args": 2, 49 | "lodash/no-single-chain": 1, 50 | "lodash/path-style": [1, "string"], 51 | "lodash/prefer-chain": [1, 3], 52 | "lodash/prefer-compact": 1, 53 | "lodash/prefer-filter": 1, 54 | "lodash/prefer-flat-map": 1, 55 | "lodash/prefer-get": [1, 3], 56 | "lodash/prefer-includes": 1, 57 | "lodash/prefer-invoke-map": 1, 58 | "lodash/prefer-lodash-method": [1, {"except": ["create", "filter", "find", "forEach", "keys", "map", "reduce", "slice", "some"]}], 59 | "lodash/prefer-map": 1, 60 | "lodash/prefer-matches": 1, 61 | "lodash/prefer-noop": 1, 62 | "lodash/prefer-reject": 1, 63 | "lodash/prefer-startswith": 1, 64 | "lodash/prefer-thru": 1, 65 | "lodash/prefer-wrapper-method": 1, 66 | "lodash/preferred-alias": 1, 67 | "lodash/prop-shorthand": 1, 68 | "lodash/unwrap": 2, 69 | "max-depth": [1, 4], 70 | "max-len": [1, 90, 4, {"ignoreUrls": true}], // the goal is generally 80, just being a bit lenient here 71 | "max-nested-callbacks": [2, 3], 72 | "max-params": [1, 5], 73 | "max-statements": [1, 30], 74 | "node/no-missing-import": 2, 75 | "node/no-missing-require": 0, // does not work with non-standard NODE_PATH 76 | "node/no-unsupported-features": [2, {"version": 6}], 77 | "no-console": 1, 78 | "no-else-return": 1, 79 | "no-empty": 1, 80 | "no-eval": 2, 81 | "no-extra-parens": 1, 82 | "no-lonely-if": 1, 83 | "no-lone-blocks": 1, 84 | "no-loop-func": 1, 85 | "no-mixed-spaces-and-tabs": 1, 86 | "no-multi-spaces": 1, 87 | "no-nested-ternary": 1, 88 | "no-self-compare": 1, 89 | "no-spaced-func": 1, 90 | "no-throw-literal": 2, 91 | "no-trailing-spaces": 1, 92 | "no-underscore-dangle": [1, "allow": ["super_", "__proxyTarget", "__isGO", "__isORP", "__isRP"]], 93 | "no-unneeded-ternary": 1, 94 | "no-unused-expressions": 2, 95 | "no-unused-vars": [1, {"vars": "all", "args": "none"}], 96 | "no-use-before-define": [1, "nofunc"], 97 | "no-with": 2, 98 | "object-curly-spacing": [1, "never"], 99 | "operator-linebreak": [1, "after", {"overrides": {"?": "before", ":": "before"}}], 100 | "quotes": [1, "single", "avoid-escape"], 101 | "quote-props": [1, "as-needed"], 102 | "semi": [1, "always"], 103 | "semi-spacing": 1, 104 | "spaced-comment": [1, "always", {"line": {"markers": ["TODO", "TODO:"], "exceptions": ["TODO"]}}], 105 | "space-before-blocks": [1, "always"], 106 | "space-before-function-paren": [1, {"anonymous": "always", "named": "never"}], 107 | "space-infix-ops": 1, 108 | "space-in-parens": [1, "never"], 109 | "space-unary-ops": 1, 110 | "strict": [2, "global"], 111 | "wrap-iife": [1, "inside"] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/func/fixtures/P00000000000001.json: -------------------------------------------------------------------------------- 1 | { 2 | "tsid": "P00000000000001", 3 | "ts": 1359263650109, 4 | "label": "Alpha", 5 | "count": 1, 6 | "x": 2750, 7 | "y": -55, 8 | "deleted": false, 9 | "items": [], 10 | "hiddenItems": [], 11 | "class_tsid": "human", 12 | "load_time": "2013-01-26 21:13:59.000", 13 | "location": { 14 | "tsid": "LLI32G3NUTD100I", 15 | "label": "Gregarious Grange", 16 | "objref": true 17 | }, 18 | "lpa": "3g8ao5il", 19 | "upd_gs": "gs6", 20 | "upd_time": "2013-01-26 20:49:28.479", 21 | "userid": 1, 22 | "a2": { 23 | "coat": 0, 24 | "dress": 0, 25 | "ears": 0, 26 | "ears_height": 1, 27 | "ears_scale": 0.824, 28 | "eye_dist": -2.5266666666667, 29 | "eye_height": 0.88, 30 | "eye_scale": 0.84873333333333, 31 | "eyes": 11, 32 | "hair": 64, 33 | "hair_color": 8, 34 | "hat": 136, 35 | "mouth": 40, 36 | "mouth_height": 0.28, 37 | "mouth_scale": 0.75, 38 | "nose": 50, 39 | "nose_height": 0.50666666666667, 40 | "nose_scale": 1.0126666666667, 41 | "pants": 366, 42 | "shirt": 312, 43 | "shoes": 251, 44 | "skin_color": 63, 45 | "skirt": 0 46 | }, 47 | "av_meta": { 48 | "pending": false, 49 | "sheets": "/c2.glitch.bz/avatars/2011-03-24/2765262852ce6775fa7a497259aecb39_1301011661", 50 | "singles": "/c2.glitch.bz/avatars/2011-06-03/2765262852ce6775fa7a497259aecb39_1307145346", 51 | "version": 3 52 | }, 53 | "daily_quoin_limit": 100, 54 | "date_last_loggedin": 0, 55 | "date_last_login": 0, 56 | "date_last_logout": 0, 57 | "deaths_today": 0, 58 | "delivery_type": "auction", 59 | "do_not_disturb": false, 60 | "followers": "", 61 | "food_today": 0, 62 | "fox_preserve_intro": false, 63 | "fox_preserve_visit": false, 64 | "giant_tip_index": 11, 65 | "h": 112, 66 | "has_done_intro": false, 67 | "is_afk": false, 68 | "is_dead": 0, 69 | "is_god": 0, 70 | "jump_count": 0, 71 | "metabolics": { 72 | "energy": 100, 73 | "mood": 100, 74 | "tank": 100 75 | }, 76 | "next_delivery": -1, 77 | "s": "-7", 78 | "stacked_physics_cache": { 79 | "can_3_jump": true, 80 | "friction_air": 1, 81 | "friction_floor": 1, 82 | "friction_thresh": 1, 83 | "gravity": 1, 84 | "item_scale": 1, 85 | "keys": "", 86 | "multiplier_3_jump": 0.8, 87 | "pc_scale": 1, 88 | "vx_accel_add_in_air": 1, 89 | "vx_accel_add_in_floor": 1, 90 | "vx_max": 1, 91 | "vx_off_ladder": 1, 92 | "vy_jump": 1, 93 | "vy_max": 1, 94 | "y_cam_offset": 1 95 | }, 96 | "stats": { 97 | "xp": 0, 98 | "currants": 0, 99 | "donation_xp_today": 0, 100 | "imagination": 0, 101 | "credits": 0, 102 | "quoins_today": 0, 103 | "meditation_today": 0, 104 | "rube_trades": 0, 105 | "rube_lure_disabled": 0, 106 | "daily_count": { 107 | "cultivation_img_rewards": 0 108 | }, 109 | "has_subscription": false, 110 | "last_rube_trade": 0, 111 | "level": 1, 112 | "misc": {}, 113 | "quoin_multiplier": 1, 114 | "subscription_end": 0 115 | }, 116 | "time_played": 0, 117 | "tp_queue": [], 118 | "w": 59, 119 | "favor_points": { 120 | "alph": 0, 121 | "cosma": 0, 122 | "friendly": 0, 123 | "grendaline": 0, 124 | "humbaba": 0, 125 | "lem": 0, 126 | "mab": 0, 127 | "pot": 0, 128 | "spriggan": 0, 129 | "ti": 0, 130 | "zille": 0 131 | }, 132 | "group_invites": {}, 133 | "group_applied": {}, 134 | "group_chats": [ 135 | "RA9118JTCLA204I" 136 | ], 137 | "can_be_rooked": true, 138 | "is_player": 1, 139 | "is_bag": true, 140 | "is_limited": false, 141 | "last_location": { 142 | "tsid": "LLI32G3NUTD100I", 143 | "label": "Gregarious Grange", 144 | "objref": true 145 | }, 146 | "prefs": { 147 | "int_menu_more_quantity_buttons": false, 148 | "int_menu_default_to_one": false, 149 | "do_oneclick_pickup": true, 150 | "do_stat_count_animations": true, 151 | "do_power_saving_mode": true, 152 | "up_key_is_enter": false 153 | }, 154 | "acl_keys": {}, 155 | "houses": {}, 156 | "giant_emblems": {}, 157 | "making": {}, 158 | "making_queue": [], 159 | "familiar": { 160 | "details": {}, 161 | "stack": [] 162 | } 163 | } -------------------------------------------------------------------------------- /test/func/fixtures/P00000000000002.json: -------------------------------------------------------------------------------- 1 | { 2 | "tsid": "P00000000000002", 3 | "ts": 1359263650109, 4 | "label": "Beta", 5 | "count": 1, 6 | "x": 2750, 7 | "y": -55, 8 | "deleted": false, 9 | "items": [ 10 | { 11 | "tsid": "I00000000000002", 12 | "label": "Random Kindness", 13 | "objref": true 14 | } 15 | ], 16 | "hiddenItems": [], 17 | "class_tsid": "human", 18 | "load_time": "2013-01-26 21:13:59.000", 19 | "location": { 20 | "tsid": "LLI32G3NUTD100I", 21 | "label": "Gregarious Grange", 22 | "objref": true 23 | }, 24 | "lpa": "3g8ao5il", 25 | "upd_gs": "gs6", 26 | "upd_time": "2013-01-26 20:49:28.479", 27 | "userid": 1, 28 | "a2": { 29 | "coat": 0, 30 | "dress": 0, 31 | "ears": 0, 32 | "ears_height": 1, 33 | "ears_scale": 0.824, 34 | "eye_dist": -2.5266666666667, 35 | "eye_height": 0.88, 36 | "eye_scale": 0.84873333333333, 37 | "eyes": 11, 38 | "hair": 64, 39 | "hair_color": 8, 40 | "hat": 136, 41 | "mouth": 40, 42 | "mouth_height": 0.28, 43 | "mouth_scale": 0.75, 44 | "nose": 50, 45 | "nose_height": 0.50666666666667, 46 | "nose_scale": 1.0126666666667, 47 | "pants": 366, 48 | "shirt": 312, 49 | "shoes": 251, 50 | "skin_color": 63, 51 | "skirt": 0 52 | }, 53 | "av_meta": { 54 | "pending": false, 55 | "sheets": "/c2.glitch.bz/avatars/2011-03-24/2765262852ce6775fa7a497259aecb39_1301011661", 56 | "singles": "/c2.glitch.bz/avatars/2011-06-03/2765262852ce6775fa7a497259aecb39_1307145346", 57 | "version": 3 58 | }, 59 | "daily_quoin_limit": 100, 60 | "date_last_loggedin": 0, 61 | "date_last_login": 0, 62 | "date_last_logout": 0, 63 | "deaths_today": 0, 64 | "delivery_type": "auction", 65 | "do_not_disturb": false, 66 | "followers": "", 67 | "food_today": 0, 68 | "fox_preserve_intro": false, 69 | "fox_preserve_visit": false, 70 | "giant_tip_index": 11, 71 | "h": 112, 72 | "has_done_intro": false, 73 | "is_afk": false, 74 | "is_dead": 0, 75 | "is_god": 0, 76 | "jump_count": 0, 77 | "metabolics": { 78 | "energy": 100, 79 | "mood": 100, 80 | "tank": 100 81 | }, 82 | "next_delivery": -1, 83 | "s": "-7", 84 | "stacked_physics_cache": { 85 | "can_3_jump": true, 86 | "friction_air": 1, 87 | "friction_floor": 1, 88 | "friction_thresh": 1, 89 | "gravity": 1, 90 | "item_scale": 1, 91 | "keys": "", 92 | "multiplier_3_jump": 0.8, 93 | "pc_scale": 1, 94 | "vx_accel_add_in_air": 1, 95 | "vx_accel_add_in_floor": 1, 96 | "vx_max": 1, 97 | "vx_off_ladder": 1, 98 | "vy_jump": 1, 99 | "vy_max": 1, 100 | "y_cam_offset": 1 101 | }, 102 | "stats": { 103 | "xp": 0, 104 | "currants": 0, 105 | "donation_xp_today": 0, 106 | "imagination": 0, 107 | "credits": 0, 108 | "quoins_today": 0, 109 | "meditation_today": 0, 110 | "rube_trades": 0, 111 | "rube_lure_disabled": 0, 112 | "daily_count": { 113 | "cultivation_img_rewards": 0 114 | }, 115 | "has_subscription": false, 116 | "last_rube_trade": 0, 117 | "level": 1, 118 | "misc": {}, 119 | "quoin_multiplier": 1, 120 | "subscription_end": 0 121 | }, 122 | "time_played": 0, 123 | "tp_queue": [], 124 | "w": 59, 125 | "favor_points": { 126 | "alph": 0, 127 | "cosma": 0, 128 | "friendly": 0, 129 | "grendaline": 0, 130 | "humbaba": 0, 131 | "lem": 0, 132 | "mab": 0, 133 | "pot": 0, 134 | "spriggan": 0, 135 | "ti": 0, 136 | "zille": 0 137 | }, 138 | "group_invites": {}, 139 | "group_applied": {}, 140 | "group_chats": [ 141 | "RA9118JTCLA204I" 142 | ], 143 | "can_be_rooked": true, 144 | "is_player": 1, 145 | "is_bag": true, 146 | "is_limited": false, 147 | "last_location": { 148 | "tsid": "LLI32G3NUTD100I", 149 | "label": "Gregarious Grange", 150 | "objref": true 151 | }, 152 | "prefs": { 153 | "int_menu_more_quantity_buttons": false, 154 | "int_menu_default_to_one": false, 155 | "do_oneclick_pickup": true, 156 | "do_stat_count_animations": true, 157 | "do_power_saving_mode": true, 158 | "up_key_is_enter": false 159 | }, 160 | "acl_keys": {}, 161 | "houses": {}, 162 | "giant_emblems": {}, 163 | "making": {}, 164 | "making_queue": [], 165 | "familiar": { 166 | "details": {}, 167 | "stack": [] 168 | } 169 | } -------------------------------------------------------------------------------- /src/comm/replServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A TCP socket based live debugging/inspection interface 5 | * (essentially a glorified wrapper around node's built-in 6 | * repl module). 7 | * Gives access to a couple of core GS modules/APIs at 8 | * runtime; that well-known Spiderman quote applies. 9 | * 10 | * See `/tools/repl-client.js` for the client counterpart, 11 | * and the {@link http://nodejs.org/api/repl.html|Node.js 12 | * REPL docs} for some usage information. 13 | * 14 | * @module 15 | */ 16 | 17 | 18 | // public interface 19 | module.exports = { 20 | init: init, 21 | shutdown: shutdown, 22 | }; 23 | 24 | 25 | var _ = require('lodash'); 26 | var net = require('net'); 27 | var repl = require('repl'); 28 | var util = require('util'); 29 | var vm = require('vm'); 30 | var bunyan = require('bunyan'); 31 | var config = require('config'); 32 | var pers = require('data/pers'); 33 | var RQ = require('data/RequestQueue'); 34 | var gsjsBridge = require('model/gsjsBridge'); 35 | var globalApi = require('model/globalApi'); 36 | var rpc = require('data/rpc'); 37 | var rpcApi = require('data/rpcApi'); 38 | var slack = require('comm/slackChat'); 39 | var logging = require('logging'); 40 | var sessionMgr = require('comm/sessionMgr'); 41 | 42 | var server; 43 | var connections = []; 44 | 45 | 46 | function init() { 47 | var port = config.getServicePort('debug:repl:basePort'); 48 | var host = config.get('debug:repl:host'); 49 | server = net.createServer(handleConnect).listen(port, host); 50 | server.on('listening', function onListening() { 51 | log.info('debugging REPL listening on %s:%s', host, port); 52 | }); 53 | } 54 | 55 | 56 | function shutdown(done) { 57 | log.info('REPL server shutdown'); 58 | server.close(done); 59 | for (var k in connections) { 60 | connections[k].destroy(); 61 | } 62 | } 63 | 64 | 65 | function handleConnect(socket) { 66 | var addr = socket.remoteAddress + ':' + socket.remotePort; 67 | connections[addr] = socket; 68 | socket.on('close', function close() { 69 | delete connections[addr]; 70 | }); 71 | log.info('REPL connection opened: %s', addr); 72 | var r = repl.start({ 73 | prompt: config.getGsid() + '> ', 74 | input: socket, 75 | output: socket, 76 | terminal: true, 77 | eval: getReplEval(addr, socket), 78 | writer: function passthrough(data) { 79 | return data; 80 | }, 81 | }); 82 | r.on('exit', function onReplExit() { 83 | socket.end(); 84 | log.info('REPL connection closed: %s', addr); 85 | }); 86 | // make some things available in the REPL context 87 | r.context.socket = socket; 88 | r.context.pers = pers; 89 | r.context.admin = gsjsBridge.getAdmin(); 90 | r.context.api = globalApi; 91 | r.context.gsrpc = rpcApi; 92 | r.context.slack = slack.getClient(); 93 | r.context.rpc = rpc; 94 | r.context.config = config; 95 | r.context.logging = logging; 96 | r.context.bunyan = bunyan; 97 | r.context.sessionMgr = sessionMgr; 98 | r.context.rq = RQ; 99 | r.context.ld = _; 100 | } 101 | 102 | 103 | function getReplEval(addr, socket) { 104 | return function replEval(code, context, file, replCallback) { 105 | log.trace({client: addr}, code); 106 | // the REPL callback handler may run into unexpected problems, handle those safely 107 | var guardedCallback = function guardedCallback(err, res) { 108 | try { 109 | return replCallback(err, res); 110 | } 111 | catch (e) { 112 | log.error(e, 'unhandled error in REPL callback: %s', e.message); 113 | if (socket && _.isFunction(socket.destroy)) { 114 | log.info('closing REPL connection after error: %s', addr); 115 | socket.destroy(); 116 | } 117 | } 118 | }; 119 | // create Script object to check syntax 120 | var script; 121 | try { 122 | script = vm.createScript(code, { 123 | filename: file, 124 | displayErrors: false, 125 | }); 126 | } 127 | catch (e) { 128 | log.trace({client: addr}, 'parse error: %s', e.message); 129 | return guardedCallback(e); 130 | } 131 | // run Script in a separate request context 132 | log.info({client: addr}, code); 133 | RQ.getGlobal('repl').push('repl.' + addr, 134 | function req() { 135 | var res = script.runInContext(context, {displayErrors: false}); 136 | return util.inspect(res, {showHidden: false, depth: 1, colors: true}); 137 | }, 138 | function cb(err, res) { 139 | if (err) { 140 | log.error(err, 'error in REPL call: %s', err.message); 141 | } 142 | return guardedCallback(err, res); 143 | }, 144 | {waitPers: true} 145 | ); 146 | }; 147 | } 148 | -------------------------------------------------------------------------------- /test/unit/model/Property.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Prop = require('model/Property'); 4 | 5 | 6 | suite('Property', function () { 7 | 8 | suite('ctor', function () { 9 | 10 | test('works with data object', function () { 11 | var p = new Prop('test', {}); 12 | assert.strictEqual(p.label, 'test'); 13 | assert.strictEqual(p.value, 0); 14 | assert.strictEqual(p.bottom, 0); 15 | assert.strictEqual(p.top, 0); 16 | assert.isFalse(p.changed); 17 | p = new Prop('test', {value: -3, bottom: -7, top: 8000}); 18 | assert.strictEqual(p.value, -3); 19 | assert.strictEqual(p.bottom, -7); 20 | assert.strictEqual(p.top, 8000); 21 | assert.isFalse(p.changed); 22 | p = new Prop('', {top: 5, value: 3}); 23 | assert.strictEqual(p.bottom, 3, 'unspecified limits are set to value'); 24 | assert.isFalse(p.changed); 25 | }); 26 | 27 | test('works with single value as data', function () { 28 | var p = new Prop('test', 12); 29 | assert.strictEqual(p.value, 12); 30 | assert.strictEqual(p.bottom, 12); 31 | assert.strictEqual(p.top, 12); 32 | }); 33 | 34 | test('works without data argument', function () { 35 | var p = new Prop('test'); 36 | assert.strictEqual(p.value, 0); 37 | assert.strictEqual(p.bottom, 0); 38 | assert.strictEqual(p.top, 0); 39 | }); 40 | 41 | test('non-integer arguments are rounded', function () { 42 | var p = new Prop('test', {value: -3.76, bottom: -5.1, top: 6.1e2}); 43 | assert.strictEqual(p.value, -4); 44 | assert.strictEqual(p.bottom, -5); 45 | assert.strictEqual(p.top, 610); 46 | }); 47 | }); 48 | 49 | 50 | suite('serialize', function () { 51 | 52 | test('does its job', function () { 53 | var p = new Prop('xyz', 1); 54 | assert.deepEqual(p.serialize(), {value: 1, bottom: 1, top: 1}); 55 | p = new Prop('test', -3); 56 | p.setLimits(-7, 8000); 57 | assert.deepEqual(p.serialize(), {value: -3, bottom: -7, top: 8000}); 58 | }); 59 | }); 60 | 61 | 62 | suite('setLimits', function () { 63 | 64 | test('does its job', function () { 65 | var p = new Prop('x', {bottom: 0, top: 10}); 66 | p.setVal(11); 67 | assert.strictEqual(p.value, 0); 68 | p.setLimits(0, 11); 69 | p.setVal(11); 70 | assert.strictEqual(p.value, 11); 71 | }); 72 | 73 | test('existing value is clamped to new limits', function () { 74 | var p = new Prop('x', {bottom: 1, top: 8, value: 5}); 75 | p.setLimits(3, 4); 76 | assert.strictEqual(p.value, 4); 77 | }); 78 | 79 | test('invalid limits throw an error', function () { 80 | assert.throw(function () { 81 | new Prop('foo').setLimits(3, 2); 82 | }, assert.AssertionError); 83 | }); 84 | }); 85 | 86 | 87 | suite('setVal', function () { 88 | 89 | test('sets value within limits', function () { 90 | var p = new Prop('a', {bottom: 0, top: 20}); 91 | p.setVal(12); 92 | assert.strictEqual(p.value, 12); 93 | assert.isTrue(p.changed); 94 | p.setVal(11.0001); 95 | assert.strictEqual(p.value, 11); 96 | assert.isTrue(p.changed); 97 | }); 98 | 99 | test('silently ignores values exceeding limits', function () { 100 | var p = new Prop('a', {bottom: 0, top: 20, value: 3}); 101 | p.setVal(-12); 102 | assert.strictEqual(p.value, 3); 103 | p.setVal(22); 104 | assert.strictEqual(p.value, 3); 105 | assert.isFalse(p.changed); 106 | }); 107 | }); 108 | 109 | 110 | suite('inc/dec', function () { 111 | 112 | test('do their job', function () { 113 | var p = new Prop('test', {bottom: 0, top: 100}); 114 | var d = p.inc(12); 115 | assert.strictEqual(p.value, 12); 116 | assert.strictEqual(d, 12); 117 | assert.isTrue(p.changed); 118 | d = p.inc(0.1); 119 | assert.strictEqual(p.value, 12); 120 | assert.strictEqual(d, 0); 121 | d = p.dec(3.87); 122 | assert.strictEqual(p.value, 9, 'inc/dec don\'t round, they floor'); 123 | assert.strictEqual(d, -3); 124 | d = p.dec(30); 125 | assert.strictEqual(p.value, 0); 126 | assert.strictEqual(d, -9); 127 | d = p.inc(1000.1); 128 | assert.strictEqual(p.value, 100); 129 | assert.strictEqual(d, 100); 130 | }); 131 | }); 132 | 133 | 134 | suite('mult', function () { 135 | 136 | test('does its job', function () { 137 | var p = new Prop('test', {bottom: 0, top: 100}); 138 | var d = p.mult(3); 139 | assert.strictEqual(p.value, 0); 140 | assert.strictEqual(d, 0); 141 | assert.isFalse(p.changed); 142 | p.value = 5; 143 | d = p.mult(2.87); 144 | assert.strictEqual(p.value, 14); 145 | assert.strictEqual(d, 9); 146 | assert.isTrue(p.changed); 147 | d = p.mult(0.5); 148 | assert.strictEqual(p.value, 7); 149 | assert.strictEqual(d, -7); 150 | d = p.mult(-1.6); 151 | assert.strictEqual(p.value, 0); 152 | assert.strictEqual(d, -7); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/model/Property.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Property; 4 | 5 | 6 | var _ = require('lodash'); 7 | var assert = require('assert'); 8 | var utils = require('utils'); 9 | 10 | 11 | /** 12 | * A class for integer properties of game objects that provides atomic 13 | * manipulations on their values. 14 | * 15 | * @param {string} label name of the property 16 | * @param {number|object} data either just a numeric initial value, or 17 | * an object containing extended configuration like: 18 | * ```{val: 3, bottom: -3, top: 8000}``` 19 | * @constructor 20 | * @mixes PropertyApi 21 | */ 22 | function Property(label, data) { 23 | this.label = label; 24 | if (data === undefined) data = {}; 25 | if (_.isNumber(data)) { 26 | this.value = Math.round(data); 27 | } 28 | else if (_.isObject(data)) { 29 | this.value = _.isNumber(data.value) ? Math.round(data.value) : 0; 30 | } 31 | else { 32 | this.value = 0; 33 | } 34 | this.setLimits( 35 | _.isNumber(data.bottom) ? data.bottom : this.value, 36 | _.isNumber(data.top) ? data.top : this.value); 37 | // add a flag that indicates whether an update for this property's value 38 | // needs to be sent to the client 39 | utils.addNonEnumerable(this, 'changed', false); 40 | } 41 | 42 | utils.copyProps(require('model/PropertyApi').prototype, Property.prototype); 43 | 44 | 45 | /** 46 | * @returns {string} string representation of the property 47 | */ 48 | Property.prototype.toString = function toString() { 49 | return '[prop.' + this.label + ':' + this.value + ']'; 50 | }; 51 | 52 | 53 | /** 54 | * Creates a compact representation of this property for persistent 55 | * serialization. The result can be used to reconstruct an equivalent 56 | * property (except for the label, which is stored as the property 57 | * name in the parent object). 58 | * 59 | * @returns {object} representation of the property for persistence 60 | */ 61 | Property.prototype.serialize = function serialize() { 62 | return { 63 | value: this.value, 64 | bottom: this.bottom, 65 | top: this.top, 66 | }; 67 | }; 68 | 69 | 70 | /** 71 | * Sets new limits for values of the property. Limits are rounded 72 | * using `Math.round`. 73 | * 74 | * @param {number} bottom new bottom limit 75 | * @param {number} top new top limit 76 | */ 77 | Property.prototype.setLimits = function setLimits(bottom, top) { 78 | bottom = Math.round(bottom); 79 | top = Math.round(top); 80 | assert(top >= bottom, 'invalid limits: ' + bottom + '/' + top); 81 | this.bottom = bottom; 82 | this.top = top; 83 | // clamp value to new limits: 84 | this.value = Math.min(this.top, Math.max(this.bottom, this.value)); 85 | }; 86 | 87 | 88 | /** 89 | * Sets the value of the property. Values exceeding the current limits 90 | * are ignored (no `Error` thrown, value remains unchanged). 91 | * 92 | * @param {number} val new value (rounded using `Math.round`) 93 | */ 94 | Property.prototype.setVal = function setVal(val) { 95 | val = Math.round(val); 96 | if (val >= this.bottom && val <= this.top) { 97 | if (this.value !== val) { 98 | this.value = val; 99 | this.changed = true; 100 | } 101 | } 102 | else { 103 | log.error('invalid value for %s: %s', this, val); 104 | } 105 | }; 106 | 107 | 108 | /** 109 | * Increments the value of the property by the given amount. 110 | * 111 | * @param {number} delta increment by this much (converted to integer 112 | * using `Math.floor`) 113 | * @returns {number} actual delta (may be different from given delta 114 | * due to limits) 115 | */ 116 | Property.prototype.inc = function inc(delta) { 117 | var d = Math.min(this.top - this.value, Math.floor(delta)); 118 | if (d !== 0) { 119 | this.value += d; 120 | this.changed = true; 121 | } 122 | return d; 123 | }; 124 | 125 | 126 | /** 127 | * Decrements the value of the property by the given amount. 128 | * 129 | * @param {number} delta decrement by this much (converted to integer 130 | * using `Math.floor`) 131 | * @returns {number} actual delta (may be different from given delta 132 | * due to limits) 133 | */ 134 | Property.prototype.dec = function dec(delta) { 135 | var d = Math.min(this.value - this.bottom, Math.floor(delta)); 136 | if (d !== 0) { 137 | this.value -= d; 138 | this.changed = true; 139 | } 140 | return -d; 141 | }; 142 | 143 | 144 | /** 145 | * Multiplies the value of the property with the given factor. The 146 | * result is rounded using `Math.round`. 147 | * 148 | * @param {number} factor multiplication factor 149 | * @returns {number} value delta 150 | */ 151 | Property.prototype.mult = function mult(factor) { 152 | var newval = Math.round(this.value * factor); 153 | newval = Math.max(Math.min(newval, this.top), this.bottom); 154 | var d = newval - this.value; 155 | if (d !== 0) { 156 | this.value = newval; 157 | this.changed = true; 158 | } 159 | return d; 160 | }; 161 | -------------------------------------------------------------------------------- /test/unit/fixtures/PLI16FSFK2I91.json: -------------------------------------------------------------------------------- 1 | { 2 | "a2": { 3 | "coat": 0, 4 | "dress": 0, 5 | "ears": 0, 6 | "ears_height": 1, 7 | "ears_scale": 0.824, 8 | "eye_dist": -2.5266666666667, 9 | "eye_height": 0.88, 10 | "eye_scale": 0.84873333333333, 11 | "eyes": 11, 12 | "hair": 64, 13 | "hair_color": 8, 14 | "hat": 136, 15 | "mouth": 40, 16 | "mouth_height": 0.28, 17 | "mouth_scale": 0.75, 18 | "nose": 50, 19 | "nose_height": 0.50666666666667, 20 | "nose_scale": 1.0126666666667, 21 | "pants": 366, 22 | "shirt": 312, 23 | "shoes": 251, 24 | "skin_color": 63, 25 | "skirt": 0 26 | }, 27 | "av_meta": { 28 | "pending": false, 29 | "sheets": "/c2.glitch.bz/avatars/2012-04-08/e2de97f049a47e5d6311be19c944314a_1333924914", 30 | "singles": "/c2.glitch.bz/avatars/2012-04-08/e2de97f049a47e5d6311be19c944314a_1333924799", 31 | "version": 3 32 | }, 33 | "can_be_rooked": true, 34 | "class_tsid": "human", 35 | "count": 1, 36 | "daily_quoin_limit": 0, 37 | "date_last_loggedin": 0, 38 | "date_last_login": 0, 39 | "date_last_logout": 0, 40 | "deaths_today": 0, 41 | "deleted": false, 42 | "delivery_type": "auction", 43 | "do_not_disturb": false, 44 | "events": { 45 | "1": { 46 | "callback": "quests_give_level_do", 47 | "time": 0 48 | } 49 | }, 50 | "familiar": { 51 | "details": {}, 52 | "stack": [] 53 | }, 54 | "feeling_called_love_last_time": 0, 55 | "food_today": 0, 56 | "fox_preserve_intro": true, 57 | "fox_preserve_visit": true, 58 | "giant_tip_index": 11, 59 | "h": 112, 60 | "has_done_intro": true, 61 | "id": "PLI16FSFK2I91", 62 | "imagination": { 63 | "label": "Imagination", 64 | "objref": true, 65 | "tsid": "DLI16FSFK2I91_imagination" 66 | }, 67 | "is_afk": false, 68 | "is_bag": true, 69 | "is_dead": 0, 70 | "is_god": 1, 71 | "is_limited": false, 72 | "is_player": 1, 73 | "items": [], 74 | "jump_count": 0, 75 | "label": "stoot barfield", 76 | "location": { 77 | "label": "Gregarious Grange", 78 | "objref": true, 79 | "tsid": "LLI32G3NUTD100I" 80 | }, 81 | "lpa": "3g8ao5il", 82 | "metabolics": { 83 | "energy": 2400, 84 | "mood": 2400, 85 | "tank": 2400 86 | }, 87 | "new_player_goodbye_familiar": 0, 88 | "next_delivery": -1, 89 | "physics_new": { 90 | "imagination": { 91 | "added_time": 1342669406577, 92 | "can_3_jump": 1, 93 | "can_wall_jump": 0, 94 | "gravity": 1, 95 | "is_img": 1, 96 | "is_permanent": 1, 97 | "multiplier_3_jump": 0.8, 98 | "vx_max": 1, 99 | "vy_jump": 1 100 | } 101 | }, 102 | "s": 7, 103 | "size": 16, 104 | "skills": { 105 | "label": "Skills", 106 | "objref": true, 107 | "tsid": "DLI16FSFK2I91_skills" 108 | }, 109 | "stacked_physics_cache": { 110 | "can_3_jump": true, 111 | "friction_air": 1, 112 | "friction_floor": 1, 113 | "friction_thresh": 1, 114 | "gravity": 1, 115 | "item_scale": 1, 116 | "keys": "", 117 | "multiplier_3_jump": 0.8, 118 | "pc_scale": 1, 119 | "vx_accel_add_in_air": 1, 120 | "vx_accel_add_in_floor": 1, 121 | "vx_max": 1, 122 | "vx_off_ladder": 1, 123 | "vy_jump": 1, 124 | "vy_max": 1, 125 | "y_cam_offset": 1 126 | }, 127 | "stats": { 128 | "credits": 2323, 129 | "currants": 232323, 130 | "daily_count": { 131 | "cultivation_img_rewards": 0 132 | }, 133 | "donation_xp_today": 0, 134 | "has_subscription": true, 135 | "imagination": 232323, 136 | "last_rube_trade": 0, 137 | "level": 42, 138 | "meditation_today": 0, 139 | "misc": {}, 140 | "quoin_multiplier": 62.35, 141 | "quoins_today": 0, 142 | "rube_lure_disabled": 0, 143 | "rube_trades": 0, 144 | "subscription_end": 9999999999, 145 | "xp": 74092200 146 | }, 147 | "time_played": 0, 148 | "too_much_nostalgia_prompt": true, 149 | "ts": 0, 150 | "tsid": "PLI16FSFK2I91", 151 | "upd_gs": "gs6", 152 | "upd_time": "2013-01-26 20:49:28.479", 153 | "use_img": true, 154 | "userid": 122470, 155 | "w": 59, 156 | "x": 2750, 157 | "y": -55 158 | } -------------------------------------------------------------------------------- /test/unit/data/RequestQueue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var RQ = require('data/RequestQueue'); 5 | 6 | 7 | suite('RequestQueue', function () { 8 | 9 | setup(function () { 10 | RQ.init(); 11 | }); 12 | 13 | teardown(function () { 14 | RQ.init(); 15 | }); 16 | 17 | 18 | suite('create', function () { 19 | 20 | test('creates and registers RQs for locations and groups only', function () { 21 | RQ.create('LXYZ'); 22 | RQ.create('RXYZ'); 23 | RQ.create('IXYZ'); 24 | RQ.create('PXYZ'); 25 | RQ.create('_special'); 26 | assert.instanceOf(RQ.get('LXYZ'), RQ); 27 | assert.instanceOf(RQ.get('RXYZ'), RQ); 28 | assert.instanceOf(RQ.get('_special'), RQ); 29 | assert.isUndefined(RQ.get('IXYZ')); 30 | assert.isUndefined(RQ.get('PXYZ')); 31 | }); 32 | }); 33 | 34 | 35 | suite('push', function () { 36 | 37 | test('adds requests to queue and triggers execution', function (done) { 38 | var rq = new RQ(); 39 | var firstReqProcessed = false; 40 | var firstCallbackCalled = false; 41 | rq.push('tag1', 42 | function firstReq() { 43 | firstReqProcessed = true; 44 | }, 45 | function firstCb() { 46 | firstCallbackCalled = true; 47 | } 48 | ); 49 | rq.push('tag2', 50 | _.noop, 51 | function callback(err) { 52 | assert.isTrue(firstReqProcessed); 53 | assert.isTrue(firstCallbackCalled); 54 | return done(err); 55 | } 56 | ); 57 | }); 58 | 59 | test('handles close requests', function (done) { 60 | var rq = RQ.create('LX'); 61 | var closeReqProcessed = false; 62 | rq.push('close', 63 | _.noop, 64 | function (err) { 65 | if (err) return done(err); 66 | assert.isTrue(rq.closing, 'closing flag set'); 67 | closeReqProcessed = true; 68 | }, {close: true} 69 | ); 70 | rq.push('after', 71 | function () { 72 | throw new Error('should not be reached'); 73 | }, 74 | function (err) { 75 | assert.isUndefined(err, 'push is ignored silently'); 76 | assert.lengthOf(rq.queue, 1, 'request not queued'); 77 | setTimeout(function () { 78 | // wait for 'close' request to be processed (scheduled via setImmediate) 79 | assert.isTrue(closeReqProcessed, 'close request callback called'); 80 | assert.isUndefined(RQ.get('LX', true), 'RQ actually closed'); 81 | done(); 82 | }, 10); 83 | } 84 | ); 85 | }); 86 | }); 87 | 88 | 89 | suite('next', function () { 90 | 91 | test('dequeues requests one by one', function () { 92 | var rq = new RQ(); 93 | var checkBusy = function checkBusy(req) { 94 | assert.isNotNull(rq.inProgress); 95 | }; 96 | rq.queue = [ 97 | {func: checkBusy}, 98 | {func: checkBusy}, 99 | ]; 100 | rq.next(); 101 | assert.lengthOf(rq.queue, 1); 102 | rq.next(); 103 | assert.lengthOf(rq.queue, 0); 104 | assert.isNull(rq.inProgress); 105 | }); 106 | 107 | test('does nothing when called with empty queue', function () { 108 | var rq = new RQ(); 109 | rq.handle = function check() { 110 | throw new Error('should not happen'); 111 | }; 112 | rq.next(); 113 | }); 114 | 115 | test('does nothing when already busy processing a message', function () { 116 | var rq = new RQ(); 117 | rq.queue = [{ 118 | func: function checkNotCalled(req) { 119 | throw new Error('should not happen'); 120 | }, 121 | }]; 122 | rq.inProgress = {foo: 'fake'}; 123 | rq.next(); 124 | }); 125 | }); 126 | 127 | 128 | suite('handle', function () { 129 | 130 | test('executes a request', function (done) { 131 | var rq = new RQ(); 132 | var called = false; 133 | rq.handle({ 134 | func: function check() { 135 | called = true; 136 | }, 137 | callback: function cb(err) { 138 | if (err) return done(err); 139 | assert.isTrue(called); 140 | assert.isNull(rq.inProgress); 141 | return done(); 142 | }, 143 | }); 144 | }); 145 | 146 | test('resets busy flag in case of thrown errors', function (done) { 147 | var rq = new RQ(); 148 | rq.handle({ 149 | func: function simulateError() { 150 | throw new Error('something went wrong'); 151 | }, 152 | callback: function cb(err) { 153 | assert.strictEqual(err.message, 'something went wrong'); 154 | assert.isNull(rq.inProgress); 155 | return done(); 156 | }, 157 | }); 158 | }); 159 | 160 | test('triggers execution of next request', function (done) { 161 | var rq = new RQ(); 162 | rq.queue = [{ 163 | func: function checkNextReqCalled() { 164 | done(); 165 | }, 166 | }]; 167 | rq.handle({func: _.noop}); 168 | }); 169 | 170 | test('passes the request function result to the callback', function (done) { 171 | var rq = new RQ(); 172 | rq.handle({ 173 | func: function func() { 174 | return 28; 175 | }, 176 | callback: function cb(err, res) { 177 | assert.strictEqual(res, 28); 178 | return done(err); 179 | }, 180 | }); 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /src/comm/sessionMgr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Client session management module. 5 | * 6 | * @module 7 | */ 8 | 9 | // public interface 10 | module.exports = { 11 | init: init, 12 | shutdown: shutdown, 13 | newSession: newSession, 14 | getSessionCount: getSessionCount, 15 | getPlayerCount: getPlayerCount, 16 | getPlayerInfo: getPlayerInfo, 17 | getSessionInfo: getSessionInfo, 18 | forEachSession: forEachSession, 19 | sendToAll: sendToAll, 20 | }; 21 | 22 | 23 | var async = require('async'); 24 | var Session = require('comm/Session'); 25 | var metrics = require('metrics'); 26 | 27 | var sessions = {}; 28 | 29 | 30 | function init() { 31 | sessions = {}; 32 | metrics.setupGaugeInterval('comm.session.count', getSessionCount); 33 | metrics.setupGaugeInterval('comm.player.count', getPlayerCount); 34 | } 35 | 36 | 37 | function shutdown() { 38 | log.info('closing and disconnecting %s session(s)', getSessionCount()); 39 | forEachSession(function endSession(session, cb) { 40 | session.close(cb); 41 | }); 42 | } 43 | 44 | 45 | function newSession(socket, req) { 46 | var id; 47 | do { 48 | id = (+new Date()).toString(36); 49 | } 50 | while (id in sessions); 51 | var session = new Session(id, socket); 52 | sessions[id] = session; 53 | session.on('close', onSessionClose); 54 | session.remote = req ? req.connection.remoteAddress : 'unknown'; 55 | return session; 56 | } 57 | 58 | 59 | function onSessionClose(session) { 60 | log.info({session: session}, 'session unlink'); 61 | delete sessions[session.id]; 62 | } 63 | 64 | 65 | /** 66 | * Gets the number of currently active client sessions (active meaning 67 | * connected, not necessarily logged in). 68 | * 69 | * @returns {number} the active session count 70 | */ 71 | function getSessionCount() { 72 | if (!sessions) return 0; 73 | return Object.keys(sessions).length; 74 | } 75 | 76 | 77 | /** 78 | * Gets the number of logged in players 79 | * 80 | * @returns {number} the player count 81 | */ 82 | function getPlayerCount() { 83 | var count = 0; 84 | for (var sess in sessions) { 85 | if (sessions[sess].loggedIn) { 86 | count++; 87 | } 88 | } 89 | return count; 90 | } 91 | 92 | 93 | /** 94 | * Retrieves some data about the currently connected clients/players 95 | * from the active sessions. 96 | * 97 | * Note: The returned information is highly volatile (e.g. it does not 98 | * include players currently moving between GS workers), and should 99 | * therefore only be used for non-critical purposes. 100 | * 101 | * @returns {object} a hash with player TSIDs as keys and data records 102 | * containing player information as values 103 | */ 104 | function getPlayerInfo() { 105 | var ret = {}; 106 | for (var id in sessions) { 107 | var session = sessions[id]; 108 | if (session.pc) { 109 | var pc = session.pc; 110 | ret[pc.tsid] = { 111 | label: pc.label, 112 | loc: { 113 | tsid: pc.location.tsid, 114 | label: pc.location.label, 115 | }, 116 | }; 117 | } 118 | } 119 | return ret; 120 | } 121 | 122 | 123 | function getSessionInfo() { 124 | var ret = {}; 125 | for (var id in sessions) { 126 | var s = sessions[id]; 127 | ret[id] = {id: id}; 128 | if (s.socket) ret[id].socket = s.remote; 129 | ret[id].loggedIn = s.loggedIn; 130 | if (s.pc) ret[id].pc = s.pc.tsid; 131 | } 132 | return ret; 133 | } 134 | 135 | 136 | /** 137 | * Asynchronously calls a given function for each session. 138 | * 139 | * @param {function} func 140 | * ``` 141 | * func(session, callback) 142 | * ``` 143 | * function to call for each session; `callback(err)` must be called 144 | * once the function has completed or an error has occurred 145 | * @param {function} [callback] 146 | * ``` 147 | * callback(err) 148 | * ``` 149 | * called when all function calls have finished, or when an error 150 | * occurs in any of them; `err` is an `Error` object or `null` 151 | */ 152 | function forEachSession(func, callback) { 153 | async.eachLimit(Object.keys(sessions), 10, function iterator(id, cb) { 154 | var session = sessions[id]; 155 | func.call(session, session, cb); 156 | }, callback); 157 | } 158 | 159 | 160 | /** 161 | * Asynchronously sends a message to all logged in clients. Errors 162 | * sending to single clients do not stop the distribution process. 163 | * 164 | * @param {object} msg the message to send 165 | * @param {function} [done] called when all messages have been sent 166 | * (no feedback regarding delivery success!) 167 | */ 168 | function sendToAll(msg, done) { 169 | forEachSession( 170 | function send(session, cb) { 171 | if (session.loggedIn) { 172 | log.debug('sending god message to %s', session); 173 | try { 174 | session.send(msg); 175 | } 176 | catch (e) { 177 | log.error(e, 'error sending god message to %s', session); 178 | } 179 | } 180 | cb(); 181 | }, 182 | function callback(err) { 183 | if (err) { 184 | log.error(err, 'error sending message to connected clients'); 185 | } 186 | if (done) done(); 187 | } 188 | ); 189 | } 190 | -------------------------------------------------------------------------------- /src/data/pbe/rethink.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * RethinkDB back-end for the persistence layer. 5 | * 6 | * @module 7 | */ 8 | 9 | // public interface 10 | module.exports = { 11 | init: init, 12 | close: close, 13 | read: read, 14 | write: write, 15 | del: del, 16 | }; 17 | 18 | var rdb = require('rethinkdb'); 19 | var wait = require('wait.for'); 20 | 21 | 22 | var cfg; 23 | var conn; 24 | 25 | 26 | /** 27 | * Resets and initializes the RethinkDB persistence back-end. Does 28 | * **not** perform any shutdown/cleanup if the module has been 29 | * initialized before; {@link module:data/pbe/rethink~close|close} 30 | * needs to be called explicitly in that case. 31 | * 32 | * @param {object} config DB connection parameters 33 | * @param {function} callback called when initialization is finished 34 | * (connection to the DB has been established) 35 | */ 36 | function init(config, callback) { 37 | cfg = config; 38 | if (!cfg.queryOpts) cfg.queryOpts = {}; 39 | conn = null; 40 | getConn(callback); 41 | } 42 | 43 | 44 | /** 45 | * Returns a connection to RethinkDB (establishing the connection if it 46 | * is not available yet). 47 | * 48 | * @private 49 | */ 50 | function getConn(callback) { 51 | if (conn) return callback(null, conn); 52 | var options = { 53 | host: cfg.dbhost, 54 | port: cfg.dbport, 55 | db: cfg.dbname, 56 | authKey: cfg.dbauth, 57 | }; 58 | rdb.connect(options, function cb(err, res) { 59 | if (err) { 60 | if (callback) return callback(err); 61 | throw err; 62 | } 63 | log.info('connected to RethinkDB'); 64 | conn = res; 65 | if (callback) return callback(null, conn); 66 | }); 67 | } 68 | 69 | 70 | /** 71 | * Closes the connection to RethinkDB. 72 | * 73 | * @param {function} callback called when the shutdown process has 74 | * finished, or when an error occurred 75 | */ 76 | function close(callback) { 77 | if (conn) { 78 | log.info('RethinkDB persistence back-end shutdown'); 79 | conn.close(callback); 80 | conn = null; 81 | return; 82 | } 83 | return callback(null); 84 | } 85 | 86 | 87 | /** 88 | * Gets persistence data for a game object from the DB. Works either 89 | * synchronously or asynchronously, depending on whether a `callback` 90 | * parameter is supplied. 91 | * 92 | * @param {string} tsid ID of the requested game object 93 | * @param {object} [queryOpts] [RethinkDB query options]{@link 94 | * http://www.rethinkdb.com/api/javascript/run/}; if not 95 | * specified, the options passed to {@link 96 | * module:data/pbe/rethink~init|init} are used 97 | * @param {function} [callback] called with the data for the requested 98 | * game object (*not* a {@link GameObject} instance), or an 99 | * error; if `undefined`, the data is returned instead (or an 100 | * error thrown) 101 | * @returns {object} game object data if no `callback` function was 102 | * given (`undefined` otherwise) 103 | */ 104 | function read(tsid, queryOpts, callback) { 105 | if (!callback) { 106 | callback = queryOpts; 107 | queryOpts = null; 108 | } 109 | var query = rdb.table(cfg.dbtable).get(tsid); 110 | if (callback) { 111 | // async version 112 | runQuery(query, queryOpts, function cb(err, data) { 113 | return callback(err, data); 114 | }); 115 | } 116 | else { 117 | // sync (fibers) version 118 | return wait.for(runQuery, query, queryOpts); 119 | } 120 | } 121 | 122 | 123 | /** 124 | * Writes game object data to the DB. 125 | * 126 | * @param {object|array} data serialized game object data (*not* actual 127 | * {@link GameObject} instances); may be a single object or an array 128 | * @param {object} [queryOpts] [RethinkDB query options]{@link 129 | * http://www.rethinkdb.com/api/javascript/run/}; if not 130 | * specified, the options passed to {@link 131 | * module:data/pbe/rethink~init|init} are used 132 | * @param {function} callback called when the object was written, or an 133 | * error occurred 134 | */ 135 | function write(data, queryOpts, callback) { 136 | if (!callback) { 137 | callback = queryOpts; 138 | queryOpts = null; 139 | } 140 | var query = rdb.table(cfg.dbtable).insert(rdb.json(JSON.stringify(data)), 141 | {conflict: 'replace'}); 142 | runQuery(query, queryOpts, callback); 143 | } 144 | 145 | 146 | /** 147 | * Removes game object data from the DB. 148 | * 149 | * @param {object} tsid ID of the game object to be removed 150 | * @param {object} [queryOpts] [RethinkDB query options]{@link 151 | * http://www.rethinkdb.com/api/javascript/run/}; if not specified, the 152 | * options passed to {@link module:data/pbe/rethink~init|init} are used 153 | * @param {function} callback called when the object was deleted, or an 154 | * error occurred 155 | */ 156 | function del(tsid, queryOpts, callback) { 157 | if (!callback) { 158 | callback = queryOpts; 159 | queryOpts = null; 160 | } 161 | var query = rdb.table(cfg.dbtable).get(tsid).delete(); 162 | runQuery(query, queryOpts, callback); 163 | } 164 | 165 | 166 | function runQuery(query, opts, callback) { 167 | opts = opts || cfg.queryOpts; 168 | getConn(function cb(err, conn) { 169 | if (err) return callback(err); 170 | query.run(conn, opts, callback); 171 | }); 172 | } 173 | -------------------------------------------------------------------------------- /bench/fixtures/jsonmsg-out-login_end.json: -------------------------------------------------------------------------------- 1 | {"msg_id":"2","type":"login_end","success":true,"location":{"tsid":"LLIERMJ93DE1H25","pcs":{},"itemstacks":{"ILI2K6DGF7F107J":{"class_tsid":"quoin","x":1048,"y":-581,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K6DGF7F107J","s":"scene:1","soulbound_to":null},"ILI2K5AGF7F1QH8":{"class_tsid":"quoin","x":942,"y":-661,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K5AGF7F1QH8","s":"scene:2","soulbound_to":null},"ILI2KCVGF7F113I":{"class_tsid":"quoin","x":1612,"y":-415,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2KCVGF7F113I","s":"scene:4","soulbound_to":null},"IDORL1S5CLQ2AFS":{"class_tsid":"marker_qurazy","x":2332,"y":-285,"label":"Qurazy marker","count":1,"version":"1335984783","path_tsid":"IDORL1S5CLQ2AFS","soulbound_to":null},"ILI2K35GF7F1HVM":{"class_tsid":"quoin","x":415,"y":-649,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K35GF7F1HVM","s":"scene:3","soulbound_to":null},"ILI2KFGUF7F1KOH":{"class_tsid":"quoin","x":1819,"y":-322,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2KFGUF7F1KOH","s":"scene:3","soulbound_to":null},"IDOCLF1B1MF30RC":{"class_tsid":"trant_bean","x":220,"y":-101,"label":"Bean Tree","count":1,"version":"1354586285","path_tsid":"IDOCLF1B1MF30RC","s":{"m":10,"h":10,"f_cap":288,"f_num":288},"status":{"is_rook_verbs":false,"is_tend_verbs":true,"msg":"I have a health of 10 and a maturity of 10. I would like to be petted and watered.","verb_states":{"pet":{"enabled":true,"disabled_reason":"","warning":false}}},"soulbound_to":null,"config":{},"rs":false},"IA5OHGVE01V24E0":{"class_tsid":"npc_mailbox","x":-1468,"y":-88,"label":"Mailbox","count":1,"version":"1350083456","path_tsid":"IA5OHGVE01V24E0","soulbound_to":null,"config":{"variant":"mailboxLeft"}},"ILI2KANGF7F174D":{"class_tsid":"quoin","x":1445,"y":-499,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2KANGF7F174D","s":"scene:3","soulbound_to":null},"ILI2K23GF7F1N3G":{"class_tsid":"quoin","x":227,"y":-681,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K23GF7F1N3G","s":"scene:1","soulbound_to":null},"ILI2K12GF7F126Q":{"class_tsid":"quoin","x":183,"y":-689,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K12GF7F126Q","s":"scene:2","soulbound_to":null},"ILI14FREMJI120H":{"class_tsid":"npc_shrine_pot","x":-113,"y":-105,"label":"Shrine to Pot","count":1,"version":"1351536731","path_tsid":"ILI14FREMJI120H","soulbound_to":null},"ILI2KD2HF7F149P":{"class_tsid":"quoin","x":1711,"y":-343,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2KD2HF7F149P","s":"scene:1","soulbound_to":null},"IDO2GN6OQNF324E":{"class_tsid":"trant_fruit","x":-550,"y":-101,"label":"Fruit Tree","count":1,"version":"1354586285","path_tsid":"IDO2GN6OQNF324E","s":{"m":10,"h":10,"f_cap":288,"f_num":288},"status":{"is_rook_verbs":false,"is_tend_verbs":true,"msg":"I have a health of 10 and a maturity of 10. I would like to be petted and watered.","verb_states":{"pet":{"enabled":true,"disabled_reason":"","warning":false}}},"soulbound_to":null,"config":{},"rs":false},"IDO77SPPT8E3CKF":{"class_tsid":"trant_bubble","x":-1978,"y":-128,"label":"Bubble Tree","count":1,"version":"1354586285","path_tsid":"IDO77SPPT8E3CKF","s":{"m":10,"h":10,"f_cap":288,"f_num":288},"status":{"is_rook_verbs":false,"is_tend_verbs":true,"msg":"I have a health of 10 and a maturity of 10. I would like to be petted and watered.","verb_states":{"pet":{"enabled":true,"disabled_reason":"","warning":false}}},"soulbound_to":null,"config":{},"rs":false},"ILI2K8GGF7F1S2K":{"class_tsid":"quoin","x":1295,"y":-507,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K8GGF7F1S2K","s":"scene:2","soulbound_to":null},"IDOURRJA4HF3NK5":{"class_tsid":"trant_fruit","x":1244,"y":-101,"label":"Fruit Tree","count":1,"version":"1354586285","path_tsid":"IDOURRJA4HF3NK5","s":{"m":10,"h":10,"f_cap":288,"f_num":288},"status":{"is_rook_verbs":false,"is_tend_verbs":true,"msg":"I have a health of 10 and a maturity of 10. I would like to be petted and watered.","verb_states":{"pet":{"enabled":true,"disabled_reason":"","warning":false}}},"soulbound_to":null,"config":{},"rs":false},"ILI2K49GF7F16SK":{"class_tsid":"quoin","x":814,"y":-666,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K49GF7F16SK","s":"scene:3","soulbound_to":null},"ILI2K9LGF7F1DBR":{"class_tsid":"quoin","x":1373,"y":-517,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K9LGF7F1DBR","s":"scene:4","soulbound_to":null},"ILI2K7FGF7F14O3":{"class_tsid":"quoin","x":1156,"y":-569,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2K7FGF7F14O3","s":"scene:3","soulbound_to":null},"IA510BAPID6217F":{"class_tsid":"street_spirit_groddle","x":824,"y":-175,"label":"Street Spirit","count":1,"version":"1351816158","path_tsid":"IA510BAPID6217F","s":"turn","tooltip_label":"Street Spirit (Kitchen Tools Vendor)","soulbound_to":null,"subclass_tsid":"npc_streetspirit_kitchen_tools","config":{"skull":"skull_L1dirt","eyes":"eyes_L1eyes1","top":"top_L1woodLeafHat","bottom":"bottom_L1FlowerBush","base":"base_L1dirt"}},"ILI2KBOGF7F1V7J":{"class_tsid":"quoin","x":1531,"y":-474,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2KBOGF7F1V7J","s":"scene:2","soulbound_to":null},"ILI2KEDUF7F1CTL":{"class_tsid":"quoin","x":1768,"y":-369,"label":"Coin","count":1,"version":"1351476850","path_tsid":"ILI2KEDUF7F1CTL","s":"scene:3","soulbound_to":null}}},"teleportation":{"energy_cost":null,"has_teleportation_skill":false,"skill_level":0,"can_teleport":false,"can_set_target":false,"targets":{},"map_tokens_used":0,"map_tokens_max":5,"map_free_used":0,"map_free_max":0,"tokens_remaining":0},"previous_location":{},"is_dead":0} -------------------------------------------------------------------------------- /deploy/eleven-server.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An init.d script for the Eleven Giants game server (on Debian/Ubuntu). 4 | # 5 | # Loosely based on/inspired by: 6 | # https://gist.github.com/peterhost/715255 7 | # https://github.com/chovy/node-startup/blob/master/init.d/node-app 8 | # 9 | # Also see: 10 | # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html 11 | # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptfunc.html 12 | # 13 | ### BEGIN INIT INFO 14 | # Provides: eleven-server 15 | # Required-Start: $syslog $remote_fs 16 | # Required-Stop: $syslog $remote_fs 17 | # Should-Start: $local_fs 18 | # Should-Stop: $local_fs 19 | # Default-Start: 2 3 4 5 20 | # Default-Stop: 0 1 6 21 | # Short-Description: Eleven Giants game server 22 | # Description: Eleven Giants game server 23 | ### END INIT INFO 24 | 25 | # load rcS variables and LSB functions (depends on lsb-base >= 3.0-6) 26 | . /lib/init/vars.sh 27 | . /lib/lsb/init-functions 28 | 29 | # various process management settings; if necessary, override these according 30 | # to the local environment in /etc/default/$NAME 31 | NAME=eleven-server 32 | NODE=/usr/local/bin/node 33 | NODE_USER=eleven 34 | NODE_ARGS="" 35 | NODE_ENV="production" 36 | APP_DIR="/eleven/eleven-server" 37 | LOG_DIR="/var/log/eleven" 38 | PID_DIR="/var/run" 39 | TERM_TIMEOUT=180 40 | 41 | # include local config overrides 42 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 43 | 44 | # derived variables (should not normally need to modify these): 45 | LOG_STDOUT="$LOG_DIR/$NAME.out.log" 46 | LOG_STDERR="$LOG_DIR/$NAME.err.log" 47 | PID_FILE="$PID_DIR/$NAME.pid" 48 | SRC_DIR="$APP_DIR/src" 49 | NODE_CMDLINE="$NODE $NODE_ARGS $SRC_DIR/server.js >> $LOG_STDOUT 2>> $LOG_STDERR" 50 | PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin 51 | 52 | 53 | do_start() { 54 | # make sure we can write log files 55 | for f in "$LOG_STDOUT" "$LOG_STDERR"; do 56 | touch "$f" && chown $NODE_USER "$f" 57 | if [ "$?" -ne "0" ]; then 58 | exit 1 59 | fi 60 | done 61 | # start, unless it's already running 62 | if [ -n "$(running)" ]; then 63 | [ "$VERBOSE" != no ] && log_daemon_msg "Already running" "$NAME"; return 1; 64 | fi 65 | NODE_PATH="$SRC_DIR" NODE_ENV=$NODE_ENV start-stop-daemon \ 66 | --start --quiet --chuid $NODE_USER --pidfile $PID_FILE --background \ 67 | --make-pidfile --chdir "$APP_DIR" --startas /bin/bash -- -c "exec $NODE_CMDLINE" \ 68 | || { [ "$VERBOSE" != no ] && log_daemon_msg "Could not start" "$NAME"; return 2; } 69 | # start-stop-daemon may return 0 even in error cases (e.g. when it cannot 70 | # create the PID file), so catch that here; a nicer and safer way to make 71 | # sure that the GS is actually running would be neat 72 | sleep 1 73 | [ ! -n "$(running)" ] && return 2 74 | if [ "$VERBOSE" != no ]; then 75 | log_daemon_msg "Started" "$NAME" 76 | fi 77 | } 78 | 79 | do_stop() { 80 | start-stop-daemon --stop --quiet --retry=TERM/$TERM_TIMEOUT/KILL/5 \ 81 | --pidfile $PID_FILE --chuid $NODE_USER --exec $NODE 82 | RETVAL="$?" 83 | [ "$RETVAL" = 2 ] && return 2 84 | [ "$RETVAL" = 0 ] && rm -f $PID_FILE 85 | [ "$VERBOSE" != no -a "$RETVAL" = 1 ] && log_daemon_msg "Not running" "$NAME" 86 | [ "$VERBOSE" != no -a "$RETVAL" = 0 ] && log_daemon_msg "Stopped" "$NAME" 87 | return "$RETVAL" 88 | } 89 | 90 | do_status() { 91 | RUNNING=$(running) 92 | ispidactive=$(pidof $NAME | grep `cat $PID_FILE 2>&1` > /dev/null 2>&1) 93 | ISPIDACTIVE=$? 94 | if [ -n "$RUNNING" ]; then 95 | if [ $ISPIDACTIVE ]; then 96 | pid=$(cat "$PID_FILE") 97 | log_success_msg "$NAME is running (PID $pid)" 98 | exit 0 99 | fi 100 | else 101 | if [ -f $PID_FILE ]; then 102 | log_success_msg "$NAME is not running (phantom PID file $PID_FILE)" 103 | exit 1 104 | else 105 | log_success_msg "$NAME is not running" 106 | exit 3 107 | fi 108 | fi 109 | } 110 | 111 | running() { 112 | RUNSTAT=$(start-stop-daemon \ 113 | --start --quiet --chuid $NODE_USER --pidfile $PID_FILE --background \ 114 | --test --startas /bin/bash -- -c "exec $NODE_CMDLINE") 115 | if [ "$?" = 1 ]; then 116 | echo y 117 | fi 118 | } 119 | 120 | 121 | case "$1" in 122 | start) 123 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting" "$NAME" && echo 124 | do_start 125 | case "$?" in 126 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; # started or already running 127 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; # error 128 | esac 129 | ;; 130 | stop) 131 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping" "$NAME" && echo 132 | do_stop 133 | case "$?" in 134 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; # (already) stopped 135 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; # error 136 | esac 137 | ;; 138 | restart) 139 | [ "$VERBOSE" != no ] && log_daemon_msg "Restarting" "$NAME" && echo 140 | do_stop 141 | case "$?" in 142 | 0|1) 143 | [ "$VERBOSE" != no ] && log_end_msg 0 144 | do_start 145 | case "$?" in 146 | 0) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 147 | 1) [ "$VERBOSE" != no ] && log_end_msg 1 ;; # old process is still running (should not be possible - something is wrong) 148 | *) [ "$VERBOSE" != no ] && log_end_msg 1 ;; # failed to start 149 | esac 150 | ;; 151 | *) [ "$VERBOSE" != no ] && log_end_msg 1 ;; # failed to stop 152 | esac 153 | ;; 154 | status) 155 | do_status 156 | ;; 157 | *) 158 | echo "Usage: `basename $0` {start|stop|restart|status}" 159 | exit 1 160 | ;; 161 | esac 162 | exit 0 163 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Initialization/startup and shutdown functionality for GS worker 5 | * processes. 6 | * 7 | * @module 8 | */ 9 | 10 | // public interface 11 | module.exports = { 12 | run: run, 13 | }; 14 | 15 | 16 | var auth = require('comm/auth'); 17 | var async = require('async'); 18 | var config = require('config'); 19 | var gsjsBridge = require('model/gsjsBridge'); 20 | var pers = require('data/pers'); 21 | var rpc = require('data/rpc'); 22 | var RQ = require('data/RequestQueue'); 23 | var amfServer = require('comm/amfServer'); 24 | var metrics = require('metrics'); 25 | var replServer = require('comm/replServer'); 26 | var logging = require('logging'); 27 | var slackChat = require('comm/slackChat'); 28 | var util = require('util'); 29 | var segfaultHandler = require('segfault-handler'); 30 | 31 | 32 | var shuttingDown = false; 33 | 34 | 35 | /** 36 | * Worker process entry point. Called as soon as common low-level 37 | * infrastructure (logging etc.) has been initialized in `server.js`. 38 | */ 39 | function run() { 40 | log.info('starting cluster worker %s', config.getGsid()); 41 | segfaultHandler.registerHandler(); 42 | RQ.init(); 43 | // initialize and wait for modules required for GS operation 44 | async.series([ 45 | persInit, 46 | authInit, 47 | rpcInit, 48 | ], 49 | function callback(err, res) { 50 | if (err) throw err; // bail if anything went wrong 51 | // otherwise, start listening for requests 52 | process.on('message', onMessage); 53 | // bind SIGINT here too, because it is also sent to child processes 54 | // when running the GS from the command line and pressing ctrl+c 55 | process.on('SIGINT', shutdown); 56 | amfServer.start(); 57 | }); 58 | // gsjs bridge loads stuff in the background (don't need to wait for it) 59 | gsjsBridge.init(function callback(err) { 60 | if (err) log.error(err, 'GSJS bridge initialization failed'); 61 | else log.info('GSJS prototypes loaded'); 62 | }); 63 | // start REPL server if enabled 64 | if (config.get('debug').repl && config.get('debug:repl:enable')) { 65 | replServer.init(); 66 | } 67 | if (config.get('slack:chat:token', null)) { 68 | slackChat.init(); 69 | } 70 | startGCInterval(); 71 | } 72 | 73 | 74 | function onMessage(msg) { 75 | if (msg === 'shutdown') { 76 | log.debug('shutdown request received'); 77 | shutdown(); 78 | } 79 | } 80 | 81 | 82 | /** 83 | * Starts the explicit GC interval (if configured). 84 | * 85 | * @private 86 | */ 87 | function startGCInterval() { 88 | var gcInt = config.get('debug:gcInt', null); 89 | if (gcInt) { 90 | if (!global.gc) { 91 | log.error('GC interval configured, but global gc() not available ' + 92 | '(requires node option --expose_gc)'); 93 | } 94 | else { 95 | log.info('starting explicit GC interval (%s ms)', gcInt); 96 | setInterval(function explicitGC() { 97 | var timer = metrics.createTimer('process.gc_time'); 98 | global.gc(); 99 | timer.stop(); 100 | }, gcInt); 101 | } 102 | } 103 | } 104 | 105 | 106 | function loadPluggable(modPath, logtag) { 107 | try { 108 | return require(modPath); 109 | } 110 | catch (e) { 111 | var msg = util.format('could not load pluggable %s module "%s": %s', 112 | logtag, modPath, e.message); 113 | throw new config.ConfigError(msg); 114 | } 115 | } 116 | 117 | 118 | function persInit(callback) { 119 | var modName = config.get('pers:backEnd:module'); 120 | var pbe = loadPluggable('data/pbe/' + modName, 'persistence back-end'); 121 | pers.init(pbe, config.get('pers'), function cb(err, res) { 122 | if (err) log.error(err, 'persistence layer initialization failed'); 123 | else log.info('persistence layer initialized (%s back-end)', modName); 124 | callback(err); 125 | }); 126 | } 127 | 128 | 129 | function authInit(callback) { 130 | var modName = config.get('auth:backEnd:module'); 131 | var mod = loadPluggable('comm/abe/' + modName, 'authentication back-end'); 132 | var abeConfig; // may stay undefined (no config required for some ABEs) 133 | if (config.get('auth:backEnd').config) { 134 | abeConfig = config.get('auth:backEnd').config[modName]; 135 | } 136 | auth.init(mod, abeConfig, function cb(err) { 137 | if (err) log.error(err, 'auth layer initialization failed'); 138 | else log.info('auth layer initialized (%s back-end)', modName); 139 | callback(err); 140 | }); 141 | } 142 | 143 | 144 | function rpcInit(callback) { 145 | rpc.init(function cb(err) { 146 | if (err) log.error(err, 'RPC initialization failed'); 147 | else log.info('RPC connections established'); 148 | callback(err); 149 | }); 150 | } 151 | 152 | 153 | function shutdown() { 154 | if (shuttingDown) { 155 | return log.warn('graceful shutdown already in progress'); 156 | } 157 | log.info('initiating graceful shutdown'); 158 | shuttingDown = true; 159 | rpc.preShutdown(); 160 | async.series([ 161 | // first, close and disconnect all client sessions 162 | amfServer.close, 163 | // then close all request queues 164 | RQ.shutdown, 165 | // then shut down RPC and persistence layer 166 | rpc.shutdown, 167 | pers.shutdown, 168 | // then everything else can go (no more incoming requests possible) 169 | function finish(cb) { 170 | async.parallel([ 171 | slackChat.shutdown, 172 | replServer.shutdown, 173 | metrics.shutdown, 174 | ], cb); 175 | }, 176 | ], function done(err, results) { 177 | if (err) { 178 | log.error(err, 'graceful shutdown failed'); 179 | } 180 | else { 181 | log.info('graceful shutdown finished'); 182 | } 183 | logging.end(process.exit, err ? 1 : 0); 184 | }); 185 | } 186 | --------------------------------------------------------------------------------