import type { WriteStream } from "fs" ; import fs from "fs" ; import { forEach } from "lodash" ; import type { Moment } from "moment" ; import moment from "moment" ; import type { WritableOptions } from "stream" ; import { Writable } from "stream" ; import type { GameStartType , PostFrameUpdateType } from "../types" ; import { Command } from "../types" ; import type { SlpCommandEventPayload } from "./slpStream" ; import { SlpStream , SlpStreamEvent , SlpStreamMode } from "./slpStream" ; const DEFAULT_NICKNAME = "unknown" ; export interface SlpFileMetadata { startTime : Moment ; lastFrame : number ; players : { [ playerIndex ] : { characterUsage : { [ internalCharacterId ] : number ; } ; names : { netplay : string ; code : string ; } ; } ; } ; consoleNickname ? : string ; } class extends Writable export class SlpFile extends Writable { private filePath : string ; private metadata : SlpFileMetadata ; private fileStream : WriteStream | null = null ; private rawDataLength = 0 ; private slpStream : SlpStream ; private usesExternalStream = false ; param { string } filePath param { WritableOptions } opts memberof public constructor ( filePath , slpStream ? , opts ? ) { super ( opts ) ; this . filePath = filePath ; this . metadata = { consoleNickname : DEFAULT_NICKNAME , startTime : moment ( ) , lastFrame : - 124 , players : { } , } ; this . usesExternalStream = Boolean ( slpStream ) ; this . slpStream = slpStream ? slpStream : new SlpStream ( { mode : SlpStreamMode . MANUAL } ) ; this . _setupListeners ( ) ; this . _initializeNewGame ( this . filePath ) ; } returns { string } memberof public path ( ) { return this . filePath ; } param metadata public setMetadata ( metadata ) { this . metadata = Object . assign ( { } , this . metadata , metadata ) ; } public _write ( chunk , encoding , callback ) { if ( encoding !== "buffer" ) { throw new Error ( `Unsupported stream encoding. Expected 'buffer' got '${ encoding }'.` ) ; } if ( this . fileStream ) { this . fileStream . write ( chunk ) ; } if ( ! this . usesExternalStream ) { this . slpStream . write ( chunk ) ; } this . rawDataLength += chunk . length ; callback ( ) ; } param data private _onCommand ( data ) { const { command , payload } = data ; switch ( command ) { case Command . GAME_START : const { players } = payload as GameStartType ; forEach ( players , ( player , i ) => { if ( player . type === 3 ) { return ; } this . metadata . players [ i ] = { characterUsage : { } , names : { netplay : player . displayName , code : player . connectCode , } , } ; } ) ; break ; case Command . POST_FRAME_UPDATE : const { frame , playerIndex , isFollower , internalCharacterId } = payload as PostFrameUpdateType ; if ( isFollower ) { break ; } this . metadata . lastFrame = frame ! ; const prevPlayer = this . metadata . players [ playerIndex ! ] ; const characterUsage = prevPlayer . characterUsage ; const curCharFrames = characterUsage [ internalCharacterId ! ] || 0 ; const player = { ... prevPlayer , characterUsage : { ... characterUsage , [ internalCharacterId ! ] : curCharFrames + 1 , } , } ; this . metadata . players [ playerIndex ! ] = player ; break ; } } private _setupListeners ( ) { const streamListener = ( data ) : void => { this . _onCommand ( data ) ; } ; this . slpStream . on ( SlpStreamEvent . COMMAND , streamListener ) ; this . on ( "finish" , ( ) => { const fd = fs . openSync ( this . filePath , "r+" ) ; fs . writeSync ( fd , createUInt32Buffer ( this . rawDataLength ) , 0 , 4 , 11 ) ; fs . closeSync ( fd ) ; this . slpStream . removeListener ( SlpStreamEvent . COMMAND , streamListener ) ; if ( ! this . usesExternalStream ) { this . slpStream . end ( ) ; } } ) ; } private _initializeNewGame ( filePath ) { this . fileStream = fs . createWriteStream ( filePath , { encoding : "binary" , } ) ; const header = Buffer . concat ( [ Buffer . from ( "{U" ) , Buffer . from ( [ 3 ] ) , Buffer . from ( "raw[$U#l" ) , Buffer . from ( [ 0 , 0 , 0 , 0 ] ) , ] ) ; this . fileStream . write ( header ) ; } public _final ( callback ) { let footer = Buffer . concat ( [ Buffer . from ( "U" ) , Buffer . from ( [ 8 ] ) , Buffer . from ( "metadata{" ) ] ) ; const startTimeStr = this . metadata . startTime . toISOString ( ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 7 ] ) , Buffer . from ( "startAtSU" ) , Buffer . from ( [ startTimeStr . length ] ) , Buffer . from ( startTimeStr ) , ] ) ; const lastFrame = this . metadata . lastFrame ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 9 ] ) , Buffer . from ( "lastFramel" ) , createInt32Buffer ( lastFrame ) , ] ) ; const consoleNick = this . metadata . consoleNickname || DEFAULT_NICKNAME ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 11 ] ) , Buffer . from ( "consoleNickSU" ) , Buffer . from ( [ consoleNick . length ] ) , Buffer . from ( consoleNick ) , ] ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 7 ] ) , Buffer . from ( "players{" ) ] ) ; const players = this . metadata . players ; forEach ( players , ( player , index ) => { footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ index . length ] ) , Buffer . from ( `${ index }{` ) ] ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 10 ] ) , Buffer . from ( "characters{" ) ] ) ; forEach ( player . characterUsage , ( usage , internalId ) => { footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ internalId . length ] ) , Buffer . from ( `${ internalId }l` ) , createUInt32Buffer ( usage ) , ] ) ; } ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "}" ) ] ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 5 ] ) , Buffer . from ( "names{" ) ] ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 7 ] ) , Buffer . from ( "netplaySU" ) , Buffer . from ( [ player . names . netplay . length ] ) , Buffer . from ( `${ player . names . netplay }` ) , ] ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 4 ] ) , Buffer . from ( "codeSU" ) , Buffer . from ( [ player . names . code . length ] ) , Buffer . from ( `${ player . names . code }` ) , ] ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "}}" ) ] ) ; } ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "}" ) ] ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "U" ) , Buffer . from ( [ 8 ] ) , Buffer . from ( "playedOnSU" ) , Buffer . from ( [ 7 ] ) , Buffer . from ( "network" ) , ] ) ; footer = Buffer . concat ( [ footer , Buffer . from ( "}}" ) ] ) ; if ( this . fileStream ) { this . fileStream . write ( footer , callback ) ; } } } const createInt32Buffer = ( number ) : Buffer => { const buf = Buffer . alloc ( 4 ) ; buf . writeInt32BE ( number , 0 ) ; return buf ; } ; const createUInt32Buffer = ( number ) : Buffer => { const buf = Buffer . alloc ( 4 ) ; buf . writeUInt32BE ( number , 0 ) ; return buf ; } ;