import { EventEmitter } from "events" ; import _ from "lodash" ; import semver from "semver" ; import type { FrameBookendType , FrameEntryType , FramesType , GameEndType , GameStartType , ItemUpdateType , PostFrameUpdateType , PreFrameUpdateType , RollbackFrames , } from "../types" ; import { Command , Frames , GameMode } from "../types" ; import { RollbackCounter } from "./rollbackCounter" ; export const MAX_ROLLBACK_FRAMES = 7 ; export enum SlpParserEvent { SETTINGS = "settings" , END = "end" , FRAME = "frame" , FINALIZED_FRAME = "finalized-frame" , ROLLBACK_FRAME = "rollback-frame" } const defaultSlpParserOptions = { strict : false , } ; export type SlpParserOptions = typeof defaultSlpParserOptions ; export class SlpParser extends EventEmitter { private frames : FramesType = { } ; private rollbackCounter : RollbackCounter = new RollbackCounter ( ) ; private settings : GameStartType | null = null ; private gameEnd : GameEndType | null = null ; private latestFrameIndex : number | null = null ; private settingsComplete = false ; private lastFinalizedFrame = Frames . FIRST - 1 ; private options : SlpParserOptions ; public constructor ( options ? ) { super ( ) ; this . options = Object . assign ( { } , defaultSlpParserOptions , options ) ; } public handleCommand ( command , payload ) { switch ( command ) { case Command . GAME_START : this . _handleGameStart ( payload as GameStartType ) ; break ; case Command . POST_FRAME_UPDATE : this . _handlePostFrameUpdate ( payload as PostFrameUpdateType ) ; this . _handleFrameUpdate ( command , payload as PostFrameUpdateType ) ; break ; case Command . PRE_FRAME_UPDATE : this . _handleFrameUpdate ( command , payload as PreFrameUpdateType ) ; break ; case Command . ITEM_UPDATE : this . _handleItemUpdate ( payload as ItemUpdateType ) ; break ; case Command . FRAME_BOOKEND : this . _handleFrameBookend ( payload as FrameBookendType ) ; break ; case Command . GAME_END : this . _handleGameEnd ( payload as GameEndType ) ; break ; } } /**
     * Resets the parser state to their default values.
     */ public reset ( ) { this . frames = { } ; this . settings = null ; this . gameEnd = null ; this . latestFrameIndex = null ; this . settingsComplete = false ; this . lastFinalizedFrame = Frames . FIRST - 1 ; } public getLatestFrameNumber ( ) { return this . latestFrameIndex ?? Frames . FIRST - 1 ; } public getPlayableFrameCount ( ) { if ( this . latestFrameIndex === null ) { return 0 ; } return this . latestFrameIndex < Frames . FIRST_PLAYABLE ? 0 : this . latestFrameIndex - Frames . FIRST_PLAYABLE ; } public getLatestFrame ( ) { const allFrames = this . getFrames ( ) ; const frameIndex = this . latestFrameIndex !== null ? this . latestFrameIndex : Frames . FIRST ; const indexToUse = this . gameEnd ? frameIndex : frameIndex - 1 ; return _ . get ( allFrames , indexToUse ) || null ; } public getSettings ( ) { return this . settingsComplete ? this . settings : null ; } public getGameEnd ( ) { return this . gameEnd ; } public getFrames ( ) { return this . frames ; } public getRollbackFrames ( ) { return { frames : this . rollbackCounter . getFrames ( ) , count : this . rollbackCounter . getCount ( ) , lengths : this . rollbackCounter . getLengths ( ) , } ; } public getFrame ( num ) { return this . frames [ num ] || null ; } private _handleGameEnd ( payload ) { if ( this . latestFrameIndex !== null && this . latestFrameIndex !== this . lastFinalizedFrame ) { this . _finalizeFrames ( this . latestFrameIndex ) ; } payload = payload as GameEndType ; this . gameEnd = payload ; this . emit ( SlpParserEvent . END , this . gameEnd ) ; } private _handleGameStart ( payload ) { this . settings = payload ; const players = payload . players ; this . settings . players = players . filter ( ( player ) => player . type !== 3 ) ; if ( payload . slpVersion && semver . gte ( payload . slpVersion , "1.6.0" ) ) { this . _completeSettings ( ) ; } } private _handlePostFrameUpdate ( payload ) { if ( this . settingsComplete ) { return ; } if ( payload . frame ! <= Frames . FIRST ) { const playerIndex = payload . playerIndex ! ; const playersByIndex = _ . keyBy ( this . settings ! . players , "playerIndex" ) ; switch ( payload . internalCharacterId ) { case 0x7 : playersByIndex [ playerIndex ] . characterId = 0x13 ; break ; case 0x13 : playersByIndex [ playerIndex ] . characterId = 0x12 ; break ; } } if ( payload . frame ! > Frames . FIRST ) { this . _completeSettings ( ) ; } } private _handleFrameUpdate ( command , payload ) { payload = payload as PostFrameUpdateType ; const location = command === 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 ) { this . emit ( SlpParserEvent . ROLLBACK_FRAME , currentFrame ) ; } } _ . set ( this . frames , [ currentFrameNumber , field , payload . playerIndex ! , location ] , payload ) ; _ . set ( this . frames , [ currentFrameNumber , "frame" ] , currentFrameNumber ) ; const settings = this . getSettings ( ) ; if ( settings && ( ! settings . slpVersion || semver . lte ( settings . slpVersion , "2.2.0" ) ) ) { this . emit ( SlpParserEvent . FRAME , this . frames [ currentFrameNumber ] ) ; this . _finalizeFrames ( currentFrameNumber - 1 ) ; } else { _ . set ( this . frames , [ currentFrameNumber , "isTransferComplete" ] , false ) ; } } private _handleItemUpdate ( payload ) { const currentFrameNumber = payload . frame ! ; const items = this . frames [ currentFrameNumber ] . items ?? [ ] ; items . push ( payload ) ; _ . set ( this . frames , [ currentFrameNumber , "items" ] , items ) ; } private _handleFrameBookend ( payload ) { const latestFinalizedFrame = payload . latestFinalizedFrame ! ; const currentFrameNumber = payload . frame ! ; _ . set ( this . frames , [ currentFrameNumber , "isTransferComplete" ] , true ) ; this . emit ( SlpParserEvent . FRAME , this . frames [ currentFrameNumber ] ) ; const validLatestFrame = this . settings ! . gameMode === GameMode . ONLINE ; if ( validLatestFrame && latestFinalizedFrame >= Frames . FIRST ) { 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 { this . _finalizeFrames ( currentFrameNumber - MAX_ROLLBACK_FRAMES ) ; } } param num private _finalizeFrames ( num ) { while ( this . lastFinalizedFrame < num ) { const frameToFinalize = this . lastFinalizedFrame + 1 ; const frame = this . getFrame ( frameToFinalize ) ! ; if ( this . options . strict ) { for ( const player of this . settings ! . players ) { const playerFrameInfo = frame . players [ player . playerIndex ] ; 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 }` ) ; } } } this . emit ( SlpParserEvent . FINALIZED_FRAME , frame ) ; this . lastFinalizedFrame = frameToFinalize ; } } private _completeSettings ( ) { if ( ! this . settingsComplete ) { this . settingsComplete = true ; this . emit ( SlpParserEvent . SETTINGS , this . settings ) ; } } }