UNPKG

@slippi/slippi-js

Version:
312 lines (308 loc) 13.2 kB
'use strict'; var get = require('lodash/get'); var keyBy = require('lodash/keyBy'); var set = require('lodash/set'); var semver = require('semver'); var types = require('../types.cjs'); var exists = require('./exists.cjs'); var rollbackCounter = require('./rollbackCounter.cjs'); var typedEventEmitter = require('./typedEventEmitter.cjs'); // There are 5 bytes of item bitfields that can be enabled const ITEM_SETTINGS_BIT_COUNT = 40; const MAX_ROLLBACK_FRAMES = 7; const SlpParserEvent = { SETTINGS: "settings", END: "end", FRAME: "frame", // Emitted for every frame FINALIZED_FRAME: "finalized-frame", // Emitted for only finalized frames ROLLBACK_FRAME: "rollback-frame", // Emitted if a frame is being replaced }; // If strict mode is on, we will do strict validation checking // which could throw errors on invalid data. // Default to false though since probably only real time applications // would care about valid data. const defaultSlpParserOptions = { strict: false, }; class SlpParser extends typedEventEmitter.TypedEventEmitter { constructor(options) { super(); this.frames = {}; this.rollbackCounter = new rollbackCounter.RollbackCounter(); this.settingsComplete = false; this.lastFinalizedFrame = types.Frames.FIRST - 1; this.options = Object.assign({}, defaultSlpParserOptions, options); } // eslint-disable-next-line @typescript-eslint/no-explicit-any handleCommand(command, payload) { switch (command) { case types.Command.GAME_START: this._handleGameStart(payload); break; case types.Command.FRAME_START: this._handleFrameStart(payload); break; case types.Command.POST_FRAME_UPDATE: // We need to handle the post frame update first since that // will finalize the settings object, before we fire the frame update this._handlePostFrameUpdate(payload); this._handleFrameUpdate(command, payload); break; case types.Command.PRE_FRAME_UPDATE: this._handleFrameUpdate(command, payload); break; case types.Command.ITEM_UPDATE: this._handleItemUpdate(payload); break; case types.Command.FRAME_BOOKEND: this._handleFrameBookend(payload); break; case types.Command.GAME_END: this._handleGameEnd(payload); break; case types.Command.GECKO_LIST: this._handleGeckoList(payload); break; case types.Command.FOD_PLATFORM: this._handleStageEvent(payload); break; case types.Command.WHISPY: this._handleStageEvent(payload); break; case types.Command.STADIUM_TRANSFORMATION: this._handleStageEvent(payload); break; } } /** * Resets the parser state to their default values. */ reset() { this.frames = {}; this.settings = undefined; this.gameEnd = undefined; this.latestFrameIndex = undefined; this.settingsComplete = false; this.lastFinalizedFrame = types.Frames.FIRST - 1; } getLatestFrameNumber() { var _a; return (_a = this.latestFrameIndex) !== null && _a !== void 0 ? _a : types.Frames.FIRST - 1; } getPlayableFrameCount() { if (this.latestFrameIndex == null) { return 0; } return this.latestFrameIndex < types.Frames.FIRST_PLAYABLE ? 0 : this.latestFrameIndex - types.Frames.FIRST_PLAYABLE; } getLatestFrame() { // return this.playerFrames[this.latestFrameIndex]; // TODO: Modify this to check if we actually have all the latest frame data and return that // TODO: If we do. For now I'm just going to take a shortcut const allFrames = this.getFrames(); const frameIndex = this.latestFrameIndex != null ? this.latestFrameIndex : types.Frames.FIRST; const indexToUse = this.gameEnd ? frameIndex : frameIndex - 1; return get(allFrames, indexToUse) || undefined; } getSettings() { return this.settingsComplete ? this.settings : undefined; } getItems() { var _a, _b; if (((_a = this.settings) === null || _a === void 0 ? void 0 : _a.itemSpawnBehavior) === types.ItemSpawnType.OFF) { return undefined; } const itemBitfield = (_b = this.settings) === null || _b === void 0 ? void 0 : _b.enabledItems; if (!exists.exists(itemBitfield)) { return undefined; } const enabledItems = []; // Ideally we would be able to do this with bitshifting instead, but javascript // truncates numbers after 32 bits when doing bitwise operations for (let i = 0; i < ITEM_SETTINGS_BIT_COUNT; i++) { if (Math.floor(itemBitfield / 2 ** i) & 1) { enabledItems.push(2 ** i); } } return enabledItems; } getGameEnd() { return this.gameEnd; } getFrames() { return this.frames; } getRollbackFrames() { return { frames: this.rollbackCounter.getFrames(), count: this.rollbackCounter.getCount(), lengths: this.rollbackCounter.getLengths(), }; } getFrame(num) { return this.frames[num] || undefined; } getGeckoList() { return this.geckoList; } _handleGeckoList(payload) { this.geckoList = payload; } _handleGameEnd(payload) { // Finalize remaining frames if necessary if (this.latestFrameIndex != null && this.latestFrameIndex !== this.lastFinalizedFrame) { this._finalizeFrames(this.latestFrameIndex); } this.gameEnd = payload; this.emit(SlpParserEvent.END, this.gameEnd); } _handleGameStart(payload) { this.settings = payload; const players = payload.players; this.settings.players = players.filter((player) => player.type !== 3); // Check to see if the file was created after the sheik fix so we know // we don't have to process the first frame of the game for the full settings if (payload.slpVersion && semver.gte(payload.slpVersion, "1.6.0")) { this._completeSettings(); } } _handleFrameStart(payload) { const currentFrameNumber = payload.frame; set(this.frames, [currentFrameNumber, "start"], payload); } _handlePostFrameUpdate(payload) { if (this.settingsComplete) { return; } // Finish calculating settings if (payload.frame <= types.Frames.FIRST) { const playerIndex = payload.playerIndex; const playersByIndex = keyBy(this.settings.players, "playerIndex"); switch (payload.internalCharacterId) { case 0x7: playersByIndex[playerIndex].characterId = 0x13; // Sheik break; case 0x13: playersByIndex[playerIndex].characterId = 0x12; // Zelda break; } } if (payload.frame > types.Frames.FIRST) { this._completeSettings(); } } _handleFrameUpdate(command, payload) { const location = command === types.Command.PRE_FRAME_UPDATE ? "pre" : "post"; const field = payload.isFollower ? "followers" : "players"; const currentFrameNumber = payload.frame; this.latestFrameIndex = currentFrameNumber; if (location === "pre" && !payload.isFollower) { const currentFrame = this.frames[currentFrameNumber]; const wasRolledback = this.rollbackCounter.checkIfRollbackFrame(currentFrame, payload.playerIndex); if (wasRolledback && currentFrame) { // frame is about to be overwritten this.emit(SlpParserEvent.ROLLBACK_FRAME, currentFrame); } } set(this.frames, [currentFrameNumber, field, payload.playerIndex, location], payload); set(this.frames, [currentFrameNumber, "frame"], currentFrameNumber); // If file is from before frame bookending, add frame to stats computer here. Does a little // more processing than necessary, but it works const settings = this.getSettings(); if (settings && (!settings.slpVersion || semver.lte(settings.slpVersion, "2.2.0"))) { const frame = this.frames[currentFrameNumber]; if (frame) { this.emit(SlpParserEvent.FRAME, frame); } // Finalize the previous frame since no bookending exists this._finalizeFrames(currentFrameNumber - 1); } else { set(this.frames, [currentFrameNumber, "isTransferComplete"], false); } } _handleItemUpdate(payload) { var _a, _b; const currentFrameNumber = payload.frame; const items = (_b = (_a = this.frames[currentFrameNumber]) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : []; items.push(payload); // Set items with newest set(this.frames, [currentFrameNumber, "items"], items); } _handleFrameBookend(payload) { const latestFinalizedFrame = payload.latestFinalizedFrame; const currentFrameNumber = payload.frame; set(this.frames, [currentFrameNumber, "isTransferComplete"], true); // Fire off a normal frame event const frame = this.frames[currentFrameNumber]; if (frame) { this.emit(SlpParserEvent.FRAME, frame); } // Finalize frames if necessary const validLatestFrame = this.settings.gameMode === types.GameMode.ONLINE; if (validLatestFrame && latestFinalizedFrame >= types.Frames.FIRST) { // Ensure valid latestFinalizedFrame if (this.options.strict && latestFinalizedFrame < currentFrameNumber - MAX_ROLLBACK_FRAMES) { throw new Error(`latestFinalizedFrame should be within ${MAX_ROLLBACK_FRAMES} frames of ${currentFrameNumber}`); } this._finalizeFrames(latestFinalizedFrame); } else { // Since we don't have a valid finalized frame, just finalize the frame based on MAX_ROLLBACK_FRAMES this._finalizeFrames(currentFrameNumber - MAX_ROLLBACK_FRAMES); } } _handleStageEvent(payload) { var _a, _b; const currentFrameNumber = payload.frame; const stageEvents = (_b = (_a = this.frames[currentFrameNumber]) === null || _a === void 0 ? void 0 : _a.stageEvents) !== null && _b !== void 0 ? _b : []; stageEvents.push(payload); // Set stageEvents with newest set(this.frames, [currentFrameNumber, "stageEvents"], stageEvents); } /** * Fires off the FINALIZED_FRAME event for frames up until a certain number * @param num The frame to finalize until */ _finalizeFrames(num) { while (this.lastFinalizedFrame < num) { const frameToFinalize = this.lastFinalizedFrame + 1; const frame = this.getFrame(frameToFinalize); // If the frame doesn't exist, we can't finalize it yet // This can happen when trying to finalize frames that haven't been received if (!frame) { break; } // Check that we have all the pre and post frame data for all players if we're in strict mode if (this.options.strict) { for (const player of this.settings.players) { const playerFrameInfo = frame.players[player.playerIndex]; // Allow player frame info to be empty in non 1v1 games since // players which have been defeated will have no frame info. if (this.settings.players.length > 2 && !playerFrameInfo) { continue; } const { pre, post } = playerFrameInfo; if (!pre || !post) { const preOrPost = pre ? "pre" : "post"; throw new Error(`Could not finalize frame ${frameToFinalize} of ${num}: missing ${preOrPost}-frame update for player ${player.playerIndex}`); } } } // Our frame is complete so finalize the frame this.emit(SlpParserEvent.FINALIZED_FRAME, frame); this.lastFinalizedFrame = frameToFinalize; } } _completeSettings() { if (!this.settingsComplete) { this.settingsComplete = true; if (this.settings) { this.emit(SlpParserEvent.SETTINGS, this.settings); } } } } exports.MAX_ROLLBACK_FRAMES = MAX_ROLLBACK_FRAMES; exports.SlpParser = SlpParser; exports.SlpParserEvent = SlpParserEvent;