UNPKG

@slippi/slippi-js

Version:
180 lines (176 loc) 6.89 kB
'use strict'; var types = require('../types.cjs'); var slpReader = require('./slpReader.cjs'); var typedEventEmitter = require('./typedEventEmitter.cjs'); const NETWORK_MESSAGE = "HELO\0"; exports.SlpStreamMode = void 0; (function (SlpStreamMode) { SlpStreamMode["AUTO"] = "AUTO"; SlpStreamMode["MANUAL"] = "MANUAL"; })(exports.SlpStreamMode || (exports.SlpStreamMode = {})); const defaultSettings = { suppressErrors: false, mode: exports.SlpStreamMode.AUTO, }; const SlpStreamEvent = { RAW: "slp-raw", COMMAND: "slp-command", }; /** * SlpStream processes a stream of Slippi data and emits events based on the commands received. * * SlpStream emits two events: "slp-raw" and "slp-command". The "slp-raw" event emits the raw buffer * bytes whenever it processes each command. You can manually parse this or write it to a * file. The "slp-command" event returns the parsed payload which you can access the attributes. * * @class SlpStream * @extends {TypedEventEmitter} */ class SlpStream extends typedEventEmitter.TypedEventEmitter { /** *Creates an instance of SlpStream. * @param {Partial<SlpStreamSettings>} [slpOptions] * @memberof SlpStream */ constructor(slpOptions) { super(); this.gameEnded = false; // True only if in manual mode and the game has completed this.previousBuffer = new Uint8Array(0); this.utf8Decoder = new TextDecoder("utf-8"); this.settings = Object.assign({}, defaultSettings, slpOptions); } restart() { this.gameEnded = false; this.payloadSizes = undefined; } /** * Process a chunk of data. This is the main entry point for feeding data * into the stream processor. */ process(newData) { var _a; // Join the current data with the old data const combinedLength = this.previousBuffer.length + newData.length; const data = new Uint8Array(combinedLength); data.set(this.previousBuffer, 0); data.set(newData, this.previousBuffer.length); // Clear previous data this.previousBuffer = new Uint8Array(0); const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength); // Iterate through the data let index = 0; while (index < data.length) { // We want to filter out the network messages const networkMsgSlice = data.subarray(index, index + 5); if (this.utf8Decoder.decode(networkMsgSlice) === NETWORK_MESSAGE) { index += 5; continue; } // Make sure we have enough data to read a full payload const command = dataView.getUint8(index); let payloadSize = 0; if (this.payloadSizes) { payloadSize = (_a = this.payloadSizes.get(command)) !== null && _a !== void 0 ? _a : 0; } const remainingLen = data.length - index; if (remainingLen < payloadSize + 1) { // If remaining length is not long enough for full payload, save the remaining // data until we receive more data. The data has been split up. this.previousBuffer = data.slice(index); break; } // Only process if the game is still going if (this.settings.mode === exports.SlpStreamMode.MANUAL && this.gameEnded) { break; } // Increment by one for the command byte index += 1; const payloadPtr = data.subarray(index); const payloadDataView = new DataView(data.buffer, data.byteOffset + index, data.byteLength - index); let payloadLen = 0; try { payloadLen = this._processCommand(command, payloadPtr, payloadDataView); } catch (err) { // Only throw the error if we're not suppressing the errors if (!this.settings.suppressErrors) { throw err; } payloadLen = 0; } index += payloadLen; } } _writeCommand(command, entirePayload, payloadSize) { const payloadBuf = entirePayload.subarray(0, payloadSize); // Concatenate command byte with payload const bufToWrite = new Uint8Array(1 + payloadBuf.length); bufToWrite[0] = command; bufToWrite.set(payloadBuf, 1); // Forward the raw buffer onwards const event = { command: command, payload: bufToWrite, }; this.emit(SlpStreamEvent.RAW, event); return bufToWrite; } _processCommand(command, entirePayload, dataView) { var _a; // Handle the message size command if (command === types.Command.MESSAGE_SIZES) { const payloadSize = dataView.getUint8(0); // Set the payload sizes this.payloadSizes = processReceiveCommands(dataView); // Emit the raw command event this._writeCommand(command, entirePayload, payloadSize); const eventPayload = { command: command, payload: this.payloadSizes, }; this.emit(SlpStreamEvent.COMMAND, eventPayload); return payloadSize; } let payloadSize = 0; if (this.payloadSizes) { payloadSize = (_a = this.payloadSizes.get(command)) !== null && _a !== void 0 ? _a : 0; } // Fetch the payload and parse it let payload; let parsedPayload = undefined; if (payloadSize > 0) { payload = this._writeCommand(command, entirePayload, payloadSize); parsedPayload = slpReader.parseMessage(command, payload); } if (!parsedPayload) { return payloadSize; } switch (command) { case types.Command.GAME_END: // Stop parsing data until we manually restart the stream if (this.settings.mode === exports.SlpStreamMode.MANUAL) { this.gameEnded = true; } break; } const eventPayload = { command: command, payload: parsedPayload, }; this.emit(SlpStreamEvent.COMMAND, eventPayload); return payloadSize; } } const processReceiveCommands = (dataView) => { const payloadSizes = new Map(); const payloadLen = dataView.getUint8(0); for (let i = 1; i < payloadLen; i += 3) { const commandByte = dataView.getUint8(i); const payloadSize = dataView.getUint16(i + 1); payloadSizes.set(commandByte, payloadSize); } return payloadSizes; }; exports.NETWORK_MESSAGE = NETWORK_MESSAGE; exports.SlpStream = SlpStream; exports.SlpStreamEvent = SlpStreamEvent;