From b1f112415d1c2f1cfd01d95bea75615fd746514a Mon Sep 17 00:00:00 2001 From: Jeff Hobson Date: Wed, 9 Mar 2022 13:05:18 -0500 Subject: [PATCH 1/4] Send messages via serialized JSON This is to coincide with a change to the LiveSplit.Server repo --- src/LiveSplitClient.js | 65 +++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/src/LiveSplitClient.js b/src/LiveSplitClient.js index e4f12b6..d937835 100644 --- a/src/LiveSplitClient.js +++ b/src/LiveSplitClient.js @@ -94,10 +94,10 @@ class LiveSplitClient extends EventEmitter { /** * Send command to the LiveSplit Server instance. * @param {string} command - Existing LiveSplit Server command without linebreaks. - * @param {boolean} [expectResponse=true] - Expect response from the server. + * @param {object} [data] - Additional data to be sent with the command. * @returns {Promise|boolean} - Promise if answer was expected, else true. */ - send(command, expectResponse = true) { + send(command, data) { if (!this._connected) throw new Error('Client must be connected to the server!'); @@ -106,12 +106,10 @@ class LiveSplitClient extends EventEmitter { this._checkDisallowedSymbols(command); - this._socket.write(`${command}\r\n`); + var jsonString = JSON.stringify({ command, data }); + this._socket.write(`${jsonString}\r\n`); - if (expectResponse) - return this._waitForResponse(); - else - return true; + return this._waitForResponse(); } _waitForResponse() { @@ -147,7 +145,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ startTimer() { - return this.send('starttimer', false); + return this.send('starttimer'); } /** @@ -155,7 +153,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ startOrSplit() { - return this.send('startorsplit', false); + return this.send('startorsplit'); } /** @@ -163,7 +161,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ split() { - return this.send('split', false); + return this.send('split'); } /** @@ -171,7 +169,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ unsplit() { - return this.send('unsplit', false); + return this.send('unsplit'); } /** @@ -179,7 +177,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ skipSplit() { - return this.send('skipsplit', false); + return this.send('skipsplit'); } /** @@ -187,7 +185,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ pause() { - return this.send('pause', false); + return this.send('pause'); } /** @@ -195,7 +193,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ resume() { - return this.send('resume', false); + return this.send('resume'); } /** @@ -203,7 +201,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ reset() { - return this.send('reset', false); + return this.send('reset'); } /** @@ -213,7 +211,7 @@ class LiveSplitClient extends EventEmitter { initGameTime() { if (this._initGameTimeOnce) return false; this._initGameTimeOnce = true; - return this.send('initgametime', false); + return this.send('initgametime'); } /** @@ -222,7 +220,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ setGameTime(time) { - return this.send(`setgametime ${time}`, false); + return this.send('setgametime', { time }); } /** @@ -231,7 +229,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ setLoadingTimes(time) { - return this.send(`setloadingtimes ${time}`, false); + return this.send('setloadingtimes', { time }); } /** @@ -239,7 +237,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ pauseGameTime() { - return this.send('pausegametime', false); + return this.send('pausegametime'); } /** @@ -247,7 +245,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ unpauseGameTime() { - return this.send('unpausegametime', false); + return this.send('unpausegametime'); } /** @@ -256,7 +254,7 @@ class LiveSplitClient extends EventEmitter { * @returns {boolean} */ setComparison(comparison) { - return this.send(`setcomparison ${comparison}`, false); + return this.send('setcomparison', { comparison }); } /** @@ -265,8 +263,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getDelta(comparison = '') { - if (comparison) comparison = ` ${comparison}`; - return this.send(`getdelta${comparison}`, true); + return this.send('getdelta', { comparison }); } /** @@ -274,7 +271,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getLastSplitTime() { - return this.send('getlastsplittime', true); + return this.send('getlastsplittime'); } /** @@ -282,7 +279,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getComparisonSplitTime() { - return this.send('getcomparisonsplittime', true); + return this.send('getcomparisonsplittime'); } /** @@ -290,7 +287,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getCurrentTime() { - return this.send('getcurrenttime', true); + return this.send('getcurrenttime'); } /** @@ -299,8 +296,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getFinalTime(comparison = '') { - if (comparison) comparison = ` ${comparison}`; - return this.send(`getfinaltime${comparison}`, true); + return this.send('getfinaltime', { comparison }); } /** @@ -309,8 +305,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getPredictedTime(comparison = '') { - if (comparison) comparison = ` ${comparison}`; - return this.send(`getpredictedtime${comparison}`, true); + return this.send('getpredictedtime', { comparison }); } /** @@ -318,7 +313,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getBestPossibleTime() { - return this.send('getbestpossibletime', true); + return this.send('getbestpossibletime'); } /** @@ -326,7 +321,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getSplitIndex() { - return this.send('getsplitindex', true); + return this.send('getsplitindex'); } /** @@ -334,7 +329,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getCurrentSplitName() { - return this.send('getcurrentsplitname', true); + return this.send('getcurrentsplitname'); } /** @@ -342,7 +337,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getPreviousSplitName() { - return this.send('getprevioussplitname', true); + return this.send('getprevioussplitname'); } getPreviousSplitname() { @@ -354,7 +349,7 @@ class LiveSplitClient extends EventEmitter { * @returns {Promise} Command result or null on timeout. */ getCurrentTimerPhase() { - return this.send('getcurrenttimerphase', true); + return this.send('getcurrenttimerphase'); } /** From 4122afc8549ba7dab986f100413e89d94d9d7b7b Mon Sep 17 00:00:00 2001 From: Jeff Hobson Date: Thu, 10 Mar 2022 14:37:27 -0500 Subject: [PATCH 2/4] Update to new JSON messaging standard See https://github.com/LiveSplit/LiveSplit.Server/pull/39 for more details --- src/LiveSplitClient.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/LiveSplitClient.js b/src/LiveSplitClient.js index d937835..db781c3 100644 --- a/src/LiveSplitClient.js +++ b/src/LiveSplitClient.js @@ -117,7 +117,7 @@ class LiveSplitClient extends EventEmitter { const responseRecieved = new Promise((resolve) => { listener = (data) => { - resolve(data); + resolve(JSON.parse(data)); }; this.once('data', listener); @@ -142,7 +142,7 @@ class LiveSplitClient extends EventEmitter { /** * Start timer - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ startTimer() { return this.send('starttimer'); @@ -150,7 +150,7 @@ class LiveSplitClient extends EventEmitter { /** * Start or split - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ startOrSplit() { return this.send('startorsplit'); @@ -158,7 +158,7 @@ class LiveSplitClient extends EventEmitter { /** * Split - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ split() { return this.send('split'); @@ -166,7 +166,7 @@ class LiveSplitClient extends EventEmitter { /** * Unsplit - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ unsplit() { return this.send('unsplit'); @@ -174,7 +174,7 @@ class LiveSplitClient extends EventEmitter { /** * Skip split - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ skipSplit() { return this.send('skipsplit'); @@ -182,7 +182,7 @@ class LiveSplitClient extends EventEmitter { /** * Pause - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ pause() { return this.send('pause'); @@ -190,7 +190,7 @@ class LiveSplitClient extends EventEmitter { /** * Resume - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ resume() { return this.send('resume'); @@ -198,7 +198,7 @@ class LiveSplitClient extends EventEmitter { /** * Reset - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ reset() { return this.send('reset'); @@ -206,7 +206,7 @@ class LiveSplitClient extends EventEmitter { /** * Init game time. Could be called only once according to LiveSplit Server documentation. - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ initGameTime() { if (this._initGameTimeOnce) return false; @@ -217,7 +217,7 @@ class LiveSplitClient extends EventEmitter { /** * Set game time * @param {string} time - Game time - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ setGameTime(time) { return this.send('setgametime', { time }); @@ -226,7 +226,7 @@ class LiveSplitClient extends EventEmitter { /** * Set loading times * @param {string} time - Game time - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ setLoadingTimes(time) { return this.send('setloadingtimes', { time }); @@ -234,7 +234,7 @@ class LiveSplitClient extends EventEmitter { /** * Pause game time - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ pauseGameTime() { return this.send('pausegametime'); @@ -242,7 +242,7 @@ class LiveSplitClient extends EventEmitter { /** * Unpause game time - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ unpauseGameTime() { return this.send('unpausegametime'); @@ -251,7 +251,7 @@ class LiveSplitClient extends EventEmitter { /** * Set comparison * @param {string} comparison - Comparison - * @returns {boolean} + * @returns {Promise} Command result or null on timeout. */ setComparison(comparison) { return this.send('setcomparison', { comparison }); @@ -360,9 +360,8 @@ class LiveSplitClient extends EventEmitter { const output = {}; for (let method of ['getCurrentTimerPhase', 'getDelta', 'getLastSplitTime', 'getComparisonSplitTime', 'getCurrentTime', 'getFinalTime', 'getPredictedTime', 'getBestPossibleTime', 'getSplitIndex', 'getCurrentSplitName', 'getPreviousSplitName']) { - output[ - method.replace('get', '').charAt(0).toLowerCase() + method.replace('get', '').slice(1) - ] = await this[method](); + let response = await this[method](); + Object.assign(output, response.data); } return output; From 4559ebe5a7feaea79af77b3ae0ef1a50b15e81bf Mon Sep 17 00:00:00 2001 From: Jeff Hobson Date: Thu, 10 Mar 2022 14:38:16 -0500 Subject: [PATCH 3/4] Fix: Data event should handle accidental message concatenation At very fast polling intervals, the socket will sometimes send more than one message from the server in a single 'data' event. This change gracefully handles these edge cases, resulting in successful tests at as fast as polling every 5ms. --- src/LiveSplitClient.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/LiveSplitClient.js b/src/LiveSplitClient.js index db781c3..0dbb211 100644 --- a/src/LiveSplitClient.js +++ b/src/LiveSplitClient.js @@ -54,10 +54,13 @@ class LiveSplitClient extends EventEmitter { }); this._socket.on('data', (data) => { - this.emit( - 'data', - data.toString('utf-8').replace('\r\n', '') - ); + // This should catch edge cases where multiple messages are sent by the server + // so fast that this listener only fires once with all of them (concatenated). + // This allows for polling at a much faster rate with fewer errors. + const messages = data.toString('utf-8').split('\r\n'); + messages.forEach(message => { + this.emit('data', message); + }); }); this._socket.on('error', (err) => { From 5bcf9b15ff4320e4ea17554716395beb95d2c5f4 Mon Sep 17 00:00:00 2001 From: Jeff Hobson Date: Fri, 11 Mar 2022 14:08:02 -0500 Subject: [PATCH 4/4] Use a nonce to guarantee message responses LiveSplit.Server is not guaranteed to respond to messages in the same order they are received. This change uses the "nonce" field in the new JSON messaging standard to provide a unique identifier against which to match incoming and outgoing messages. Currently, `this._openRequests` is not actually used for anything. But it does provide a useful method for investigating errors or commands that never receive a response. It also may be desired to change the listening behavior of this library to use a single listener that looks up the appropriate Promise resolver in `this._openRequests` instead of the current behavior -- subscribing a new listener for every message. --- src/LiveSplitClient.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/LiveSplitClient.js b/src/LiveSplitClient.js index 0dbb211..9af5506 100644 --- a/src/LiveSplitClient.js +++ b/src/LiveSplitClient.js @@ -1,6 +1,7 @@ const net = require('net'); const EventEmitter = require('events'); const { deprecate } = require('util'); +const crypto = require('crypto'); /** * Node.js client for the LiveSplit Server running instance @@ -26,6 +27,7 @@ class LiveSplitClient extends EventEmitter { this._connected = false; this.timeout = 100; + this._openRequests = {}; /* According to: https://github.com/LiveSplit/LiveSplit.Server/blob/a4a57716dce90936606bfc8f8ac84f7623773aa5/README.md#commands @@ -57,9 +59,9 @@ class LiveSplitClient extends EventEmitter { // This should catch edge cases where multiple messages are sent by the server // so fast that this listener only fires once with all of them (concatenated). // This allows for polling at a much faster rate with fewer errors. - const messages = data.toString('utf-8').split('\r\n'); + const messages = data.toString('utf-8').split('\r\n').slice(0, -1); messages.forEach(message => { - this.emit('data', message); + this.emit('data', JSON.parse(message)); }); }); @@ -98,7 +100,7 @@ class LiveSplitClient extends EventEmitter { * Send command to the LiveSplit Server instance. * @param {string} command - Existing LiveSplit Server command without linebreaks. * @param {object} [data] - Additional data to be sent with the command. - * @returns {Promise|boolean} - Promise if answer was expected, else true. + * @returns {Promise} - Command result or null on timeout. */ send(command, data) { if (!this._connected) @@ -109,26 +111,33 @@ class LiveSplitClient extends EventEmitter { this._checkDisallowedSymbols(command); - var jsonString = JSON.stringify({ command, data }); - this._socket.write(`${jsonString}\r\n`); + const nonce = crypto.randomUUID(); + const request = { command, data, nonce }; + this._socket.write(`${JSON.stringify(request)}\r\n`); + this._openRequests[nonce] = request; - return this._waitForResponse(); + return this._waitForResponse(request); } - _waitForResponse() { + _waitForResponse(request) { let listener = false; const responseRecieved = new Promise((resolve) => { listener = (data) => { - resolve(JSON.parse(data)); + if (data.nonce === request.nonce) { + this.off('data', listener); + delete this._openRequests[request.nonce]; + resolve(data); + } }; - this.once('data', listener); + this.on('data', listener); }); const responseTimeout = new Promise((resolve) => { setTimeout(() => { - this.removeListener('data', listener); + this.off('data', listener); + delete this._openRequests[request.nonce]; resolve(null); }, this.timeout); });