UNPKG

slp-enforcer

Version:

Finds violations of the Melee Controller Ruleset by inspecting SLP files

287 lines (286 loc) 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const get_1 = __importDefault(require("lodash/get")); const keyBy_1 = __importDefault(require("lodash/keyBy")); const set_1 = __importDefault(require("lodash/set")); const semver_1 = __importDefault(require("semver")); const types_1 = require("../types"); const types_2 = require("../types"); const exists_1 = require("./exists"); const rollbackCounter_1 = require("./rollbackCounter"); // There are 5 bytes of item bitfields that can be enabled const ITEM_SETTINGS_BIT_COUNT = 40; exports.MAX_ROLLBACK_FRAMES = 7; var SlpParserEvent; (function (SlpParserEvent) { SlpParserEvent["SETTINGS"] = "settings"; SlpParserEvent["END"] = "end"; SlpParserEvent["FRAME"] = "frame"; SlpParserEvent["FINALIZED_FRAME"] = "finalized-frame"; SlpParserEvent["ROLLBACK_FRAME"] = "rollback-frame"; })(SlpParserEvent = exports.SlpParserEvent || (exports.SlpParserEvent = {})); // 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 events_1.EventEmitter { constructor(options) { super(); this.frames = {}; this.rollbackCounter = new rollbackCounter_1.RollbackCounter(); this.settings = null; this.gameEnd = null; this.latestFrameIndex = null; this.settingsComplete = false; this.lastFinalizedFrame = types_2.Frames.FIRST - 1; this.geckoList = null; this.options = Object.assign({}, defaultSlpParserOptions, options); } // eslint-disable-next-line @typescript-eslint/no-explicit-any handleCommand(command, payload) { switch (command) { case types_2.Command.GAME_START: this._handleGameStart(payload); break; case types_2.Command.FRAME_START: this._handleFrameStart(payload); break; case types_2.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_2.Command.PRE_FRAME_UPDATE: this._handleFrameUpdate(command, payload); break; case types_2.Command.ITEM_UPDATE: this._handleItemUpdate(payload); break; case types_2.Command.FRAME_BOOKEND: this._handleFrameBookend(payload); break; case types_2.Command.GAME_END: this._handleGameEnd(payload); break; case types_2.Command.GECKO_LIST: this._handleGeckoList(payload); break; } } /** * Resets the parser state to their default values. */ reset() { this.frames = {}; this.settings = null; this.gameEnd = null; this.latestFrameIndex = null; this.settingsComplete = false; this.lastFinalizedFrame = types_2.Frames.FIRST - 1; } getLatestFrameNumber() { var _a; return (_a = this.latestFrameIndex) !== null && _a !== void 0 ? _a : types_2.Frames.FIRST - 1; } getPlayableFrameCount() { if (this.latestFrameIndex === null) { return 0; } return this.latestFrameIndex < types_2.Frames.FIRST_PLAYABLE ? 0 : this.latestFrameIndex - types_2.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_2.Frames.FIRST; const indexToUse = this.gameEnd ? frameIndex : frameIndex - 1; return get_1.default(allFrames, indexToUse) || null; } getSettings() { return this.settingsComplete ? this.settings : null; } getItems() { var _a, _b; if (((_a = this.settings) === null || _a === void 0 ? void 0 : _a.itemSpawnBehavior) === types_1.ItemSpawnType.OFF) { return null; } const itemBitfield = (_b = this.settings) === null || _b === void 0 ? void 0 : _b.enabledItems; if (!exists_1.exists(itemBitfield)) { return null; } 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] || null; } 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_1.default.gte(payload.slpVersion, "1.6.0")) { this._completeSettings(); } } _handleFrameStart(payload) { const currentFrameNumber = payload.frame; set_1.default(this.frames, [currentFrameNumber, "start"], payload); } _handlePostFrameUpdate(payload) { if (this.settingsComplete) { return; } // Finish calculating settings if (payload.frame <= types_2.Frames.FIRST) { const playerIndex = payload.playerIndex; const playersByIndex = keyBy_1.default(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_2.Frames.FIRST) { this._completeSettings(); } } _handleFrameUpdate(command, payload) { const location = command === types_2.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) { // frame is about to be overwritten this.emit(SlpParserEvent.ROLLBACK_FRAME, currentFrame); } } set_1.default(this.frames, [currentFrameNumber, field, payload.playerIndex, location], payload); set_1.default(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_1.default.lte(settings.slpVersion, "2.2.0"))) { this.emit(SlpParserEvent.FRAME, this.frames[currentFrameNumber]); // Finalize the previous frame since no bookending exists this._finalizeFrames(currentFrameNumber - 1); } else { set_1.default(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_1.default(this.frames, [currentFrameNumber, "items"], items); } _handleFrameBookend(payload) { const latestFinalizedFrame = payload.latestFinalizedFrame; const currentFrameNumber = payload.frame; set_1.default(this.frames, [currentFrameNumber, "isTransferComplete"], true); // Fire off a normal frame event this.emit(SlpParserEvent.FRAME, this.frames[currentFrameNumber]); // Finalize frames if necessary const validLatestFrame = this.settings.gameMode === types_2.GameMode.ONLINE; if (validLatestFrame && latestFinalizedFrame >= types_2.Frames.FIRST) { // Ensure valid latestFinalizedFrame if (this.options.strict && latestFinalizedFrame < currentFrameNumber - exports.MAX_ROLLBACK_FRAMES) { throw new Error(`latestFinalizedFrame should be within ${exports.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 - exports.MAX_ROLLBACK_FRAMES); } } /** * 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); // 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; this.emit(SlpParserEvent.SETTINGS, this.settings); } } } exports.SlpParser = SlpParser;