/** * Loads the game object with the given TSID from persistence. * Depending on whether the GS is "responsible" for this object, it * will be wrapped either in a {@link module:data/persProxy|persProxy} * or {@link module:data/rpcProxy|rpcProxy}. * * @param {string} tsid TSID of the object to load * @returns {GameObject|null} the requested object, or `null` if no * object data was found for the given TSID */ function load(tsid) { assert(pbe, 'persistence back-end not set'); log.debug('pers.load: %s', tsid); var data = pbe.read(tsid); if (typeof data !== 'object' || data === null) { log.info(new DummyError(), 'no or invalid data for %s', tsid); return null; } orProxy.proxify(data); var obj = gsjsBridge.create(data); if (!rpc.isLocal(obj)) { // wrap object in RPC proxy and add it to request cache obj = rpc.makeProxy(obj); RC.getContext().cache[tsid] = obj; metrics.increment('pers.load.remote'); } else { // check if object has been loaded in a concurrent request (fiber) in the meantime if (tsid in cache) { log.warn('%s already loaded, discarding redundant copy', tsid); return cache[tsid]; } // make sure any changes to the object are persisted obj = persProxy.makeProxy(obj); cache[tsid] = obj; // post-construction operations (resume timers/intervals, GSJS onLoad etc.) if (obj.gsOnLoad) { obj.gsOnLoad(); } metrics.increment('pers.load.local'); } metrics.increment('pers.load'); return obj; }
Session.prototype.send = function send(msg) { if (!this.socket) { log.debug('socket is gone, dropping %s message', msg.type); return; } if (!this.loggedIn) { if (msg.type !== 'login_start' && msg.type !== 'login_end' && msg.type !== 'relogin_start' && msg.type !== 'relogin_end' && msg.type !== 'ping') { log.debug('(re)login incomplete, postponing %s message', msg.type); this.preLoginBuffer.push(msg); return; } if (msg.type === 'login_end' || msg.type === 'relogin_end') { this.loggedIn = true; } } if (log.trace()) { log.trace({data: msg, to: this.pc ? this.pc.tsid : undefined}, 'sending %s message', msg.type); } var data; if (this.jsamf) { data = amf.js.serializer().writeObject(msg); } else { data = amf.cc.serialize(msg); } var size = Buffer.byteLength(data, 'binary'); var buf = new Buffer(4 + size); buf.writeUInt32BE(size, 0); buf.write(data, 4, size, 'binary'); this.socket.write(buf); metrics.increment('net.amf.tx', 0.01); };
pbe.del(obj, function db(err, res) { if (err) { log.error(err, 'could not delete: %s', obj.tsid); metrics.increment('pers.del.fail'); } if (callback) return callback(err, res); });
pbe.write(orProxy.refify(obj.serialize()), function cb(err, res) { if (err) { log.error(err, 'could not write: %s', obj.tsid); metrics.increment('pers.write.fail'); } if (callback) return callback(err, res); });
pbe.del(tsid, (err) => { if (err) { log.error(err, 'could not delete: %s', tsid); metrics.increment('pers.del.fail'); } return cb(); });
/** * Writes a game object to persistent storage. * * @param {GameObject} obj game object to write * @param {string} logmsg short additional info for log messages * @param {function} callback called when write operation has finished, * or in case of errors * @private */ function write(obj, logmsg, callback) { log.debug('pers.write: %s%s', obj.tsid, logmsg ? ' (' + logmsg + ')' : ''); metrics.increment('pers.write'); pbe.write(orProxy.refify(obj.serialize()), function cb(err, res) { if (err) { log.error(err, 'could not write: %s', obj.tsid); metrics.increment('pers.write.fail'); } if (callback) return callback(err, res); }); }
/** * Loads the game object with the given TSID from persistence. * Depending on whether the GS is "responsible" for this object, it * may be wrapped in an {@link module:data/rpcProxy|rpcProxy}. * * @param {string} tsid TSID of the object to load * @returns {GameObject|null} the requested object, or `null` if no * object data was found for the given TSID */ function load(tsid) { assert(pbe, 'persistence back-end not set'); log.debug('pers.load: %s', tsid); var data = pbe.read(tsid); if (data === null && (utils.isGeo(tsid) || utils.isLoc(tsid))) { log.info('no data for %s, using temp location data instead', tsid); data = pbe.read(utils.isGeo(tsid) ? 'GKZ8WU4WGMQME7CXXX' : 'LKZ8WU4WGMQME7CXXX'); if (data) data.tsid = tsid; } if (!_.isObject(data)) { log.info(new DummyError(), 'no or invalid data for %s', tsid); return null; } orProxy.proxify(data); var obj = gsjsBridge.create(data); if (!rpc.isLocal(obj)) { // wrap object in RPC proxy and add it to request cache obj = rpc.makeProxy(obj); RC.getContext().cache[tsid] = obj; metrics.increment('pers.load.remote'); } else { // check if object has been loaded in a concurrent request (fiber) in the meantime if (tsid in cache) { log.warn('%s already loaded, discarding redundant copy', tsid); return cache[tsid]; } cache[tsid] = obj; // post-construction operations (resume timers/intervals, GSJS onLoad etc.) try { if (obj.gsOnLoad) obj.gsOnLoad(); } catch (err) { log.error(err, 'failed to process onLoad event'); } metrics.increment('pers.load.local'); } metrics.increment('pers.load'); return obj; }
Session.prototype.enqueueMessage = function enqueueMessage(msg) { log.trace({data: msg}, 'queueing %s request', msg.type); metrics.increment('net.amf.rx', 0.01); if (msg.type === 'ping') { this.processRequest(msg); } else { var rq = this.pc ? this.pc.getRQ() : RQ.getGlobal('prelogin'); rq.push(msg.type, this.processRequest.bind(this, msg), this.handleAmfReqError.bind(this, msg), {session: this, obj: this.pc, timerTag: msg.type}); } };
/** * Permanently deletes a game object from persistent storage. Also * removes the object from the live object cache. * * @param {GameObject} obj game object to remove * @param {string} logmsg short additional info for log messages * @param {function} callback called when delete operation has * finished, or in case of errors * @private */ function del(obj, logmsg, callback) { log.debug('pers.del: %s%s', obj.tsid, logmsg ? ' (' + logmsg + ')' : ''); metrics.increment('pers.del'); obj.suspendGsTimers(); delete cache[obj.tsid]; pbe.del(obj, function db(err, res) { if (err) { log.error(err, 'could not delete: %s', obj.tsid); metrics.increment('pers.del.fail'); } if (callback) return callback(err, res); }); }
/** * Creates a new game object of the given type. Also calls the object's * GSJS `onCreate` handler, if there is one. * * @param {function} modelType the desired game object model type (i.e. * a constructor like `Player` or `Geo`) * @param {object} [data] additional properties for the object * @param {boolean} [upsert] when `true`, allows replacing existing objects * @returns {object} the new object, wrapped in a persistence proxy */ function create(modelType, data, upsert) { log.debug('pers.create: %s%s', modelType.name, _.isObject(data) && data.tsid ? '#' + data.tsid : ''); data = data || {}; var obj = gsjsBridge.create(data, modelType); if (!upsert) { assert(!(obj.tsid in cache), 'object already exists: ' + obj.tsid); } cache[obj.tsid] = obj; RC.getContext().setDirty(obj); obj.gsOnCreate(); metrics.increment('pers.create'); return obj; }
/** * Creates a new game object of the given type and adds it to * persistence. The returned object is wrapped in a ({@link * module:data/persProxy|persProxy}) to make sure all future changes * to the object are automatically persisted. * Also calls the object's GSJS `onCreate` handler, if there is one. * * @param {function} modelType the desired game object model type (i.e. * a constructor like `Player` or `Geo`) * @param {object} [data] additional properties for the object * @returns {object} the new object, wrapped in a persistence proxy */ function create(modelType, data) { log.debug('pers.create: %s%s', modelType.name, (typeof data === 'object' && data.tsid) ? ('#' + data.tsid) : ''); data = data || {}; var obj = gsjsBridge.create(data, modelType); assert(!(obj.tsid in cache), 'object already exists: ' + obj.tsid); obj = persProxy.makeProxy(obj); cache[obj.tsid] = obj; obj = orProxy.wrap(obj); RC.getContext().setDirty(obj, true); if (typeof obj.onCreate === 'function') { obj.onCreate(); } metrics.increment('pers.create'); return obj; }
/** * Sends an RPC request to another game server instance, taking care of * proper argument and return value (de)serialization. * Returns the result either via callback or synchronously using * {@link https://github.com/luciotato/waitfor|wait.for/fibers}. * * @param {string} gsid ID of the game server to forward the call to * @param {string} rpcFunc RPC function to call (must be `obj`, `api`, * `admin` or `gs`) * @param {array} args function arguments; the obligatory source GS ID * parameter (required for any RPC function) is prepended here * @param {function} [callback] * ``` * callback(err, res) * ``` * called with the function result (`res`) or an error (`err`) when the * RPC returns; if not supplied, the function behaves synchronously and * returns the result (or throws an exception) * @returns {*} result of the remote function call if no callback was * supplied (`undefined` otherwise) * @throws {RpcError} in case something bad happens during the RPC and * no callback was supplied */ function sendRequest(gsid, rpcFunc, args, callback) { assert(gsid !== config.getGsid(), 'RPC to self'); if (shuttingDown) { // hack - see above (preShutdown) log.info('shutdown in progress, dropping %s RPC request', rpcFunc); return; } var client = clients[gsid]; if (!client) { var err = new RpcError(util.format('no RPC client found for "%s"', gsid)); if (callback) return callback(err); throw err; } // argument marshalling (replace objref proxies with actual objrefs) args = orProxy.refify(args); var logmsg = util.format('%s(%s) @%s', rpcFunc, args.join(', '), gsid); log.debug('calling %s', logmsg); metrics.increment('net.rpc.tx'); var rpcArgs = [config.getGsid()].concat(args); if (callback) { client.request(rpcFunc, rpcArgs, function cb(err, res) { log.trace('%s returned', logmsg); // wrapping to handle the special case where res is an objref itself var wrap = {res: res}; orProxy.proxify(wrap); callback(err, res); }); } else { try { var res = wait.forMethod(client, 'request', rpcFunc, rpcArgs); // wrapping to handle the special case where res is an objref itself var wrap = {res: res}; orProxy.proxify(wrap); return wrap.res; } catch (e) { throw new RpcError('error calling ' + logmsg, e); } } }
/** * Server-side RPC request handler. Executes a function on an object * specified by TSID (within a separate request context) and returns * the result to the remote caller. * * @param {string} callerId ID of the component requesting the function * call (for logging) * @param {object} obj the object on which a function should be called * @param {string|null} tag ID tag of ongoing request process this RPC belongs * to (if any) * @param {string} fname name of the function to call on the object * @param {array} args function call arguments * @param {function} callback * ``` * callback(error, result) * ``` * callback for the RPC library, returning the result (or errors) to * the remote caller */ function handleRequest(callerId, obj, tag, fname, args, callback) { metrics.increment('net.rpc.rx'); if (!obj || !_.isFunction(obj[fname])) { var msg = util.format('no such function: %s.%s', obj, fname); return callback(new RpcError(msg)); } orProxy.proxify(args); // unmarshal arguments var logtag = util.format('%s.%s.%s', callerId, obj.tsid ? obj.tsid : obj, fname); log.debug('%s(%s)', logtag, _.isArray(args) ? args.join(', ') : args); var rpcReq = function rpcReq() { var ret = obj[fname].apply(obj, args); // convert <undefined> result to <null> so RPC lib produces a valid // response (it just omits the <result> property otherwise) if (ret === undefined) ret = null; return ret; }; var rpcCallback = function rpcCallback(err, res) { if (err) { log.error(err, 'exception in %s', logtag); } if (!_.isFunction(callback)) { log.error('%s called without a valid callback', logtag); } else { log.trace('%s finished', logtag); res = orProxy.refify(res); // marshal return value return callback(err, res); } }; try { var rq = RQ.getGlobal('rpc'); if (obj.tsid && isLocal(obj) && _.isFunction(obj.getRQ)) { rq = obj.getRQ(); } rq.push(tag, rpcReq, rpcCallback, {waitPers: true}); } catch (err) { return rpcCallback(err); } }