From b4c90132b52dae48c0faaf9ef6381511b363dac4 Mon Sep 17 00:00:00 2001 From: Vince Au Date: Thu, 3 Nov 2022 23:41:06 +1100 Subject: [PATCH] feat: add ability to quickly read game winners --- src/SlippiGame.ts | 5 ++-- src/utils/slpReader.ts | 37 +++++++++++++++++++++++++ test/slpReader.spec.ts | 63 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 test/slpReader.spec.ts diff --git a/src/SlippiGame.ts b/src/SlippiGame.ts index d985e3a3..b993ab64 100644 --- a/src/SlippiGame.ts +++ b/src/SlippiGame.ts @@ -22,6 +22,7 @@ import type { } from "./types"; import { SlpParser, SlpParserEvent } from "./utils/slpParser"; import type { SlpReadInput } from "./utils/slpReader"; +import { getGameEnd } from "./utils/slpReader"; import { closeSlpFile, getMetadata, iterateEvents, openSlpFile, SlpInputSource } from "./utils/slpReader"; /** @@ -205,9 +206,7 @@ export class SlippiGame { } public getWinners(): PlacementType[] { - this._process(); - - const gameEnd = this.getGameEnd(); + const gameEnd = getGameEnd(this.input); if (!gameEnd) { return []; } diff --git a/src/utils/slpReader.ts b/src/utils/slpReader.ts index cff3b6bf..07cf029d 100644 --- a/src/utils/slpReader.ts +++ b/src/utils/slpReader.ts @@ -6,6 +6,7 @@ import { mapValues } from "lodash"; import type { EventCallbackFunc, EventPayloadTypes, + GameEndType, GameInfoType, GeckoCodeType, MetadataType, @@ -14,6 +15,7 @@ import type { SelfInducedSpeedsType, } from "../types"; import { Command } from "../types"; +import { exists } from "./exists"; import { toHalfwidth } from "./fullwidth"; export enum SlpInputSource { @@ -642,3 +644,38 @@ export function getMetadata(slpFile: SlpFileType): MetadataType | null { // $FlowFixMe return metadata; } + +export function getGameEnd(input: SlpReadInput): GameEndType | null { + let slpFile: SlpFileType | undefined; + + try { + slpFile = openSlpFile(input); + const { rawDataPosition, rawDataLength, messageSizes } = slpFile; + const gameEndSize = messageSizes[Command.GAME_END]; + if (!exists(gameEndSize)) { + return null; + } + + // Subtract one to account for command byte + const gameEndPosition = rawDataPosition + rawDataLength - gameEndSize - 1; + + // Add one to include command byte in payload + const buffer = new Uint8Array(gameEndSize + 1); + readRef(slpFile.ref, buffer, 0, buffer.length, gameEndPosition); + if (buffer[0] !== Command.GAME_END) { + // This isn't even a game end payload + return null; + } + + const gameEndMessage = parseMessage(Command.GAME_END, buffer); + if (!gameEndMessage) { + return null; + } + + return gameEndMessage as GameEndType; + } catch (err) { + return null; + } finally { + slpFile && closeSlpFile(slpFile); + } +} diff --git a/test/slpReader.spec.ts b/test/slpReader.spec.ts new file mode 100644 index 00000000..e039f541 --- /dev/null +++ b/test/slpReader.spec.ts @@ -0,0 +1,63 @@ +import _ from "lodash"; + +import { SlippiGame } from "../src"; +import { getGameEnd, SlpInputSource } from "../src/utils/slpReader"; + +describe("when reading game end directly", () => { + it("should return the same game end object", () => { + const game = new SlippiGame("slp/test.slp"); + const gameEnd = game.getGameEnd()!; + + const manualGameEnd = getManualGameEnd("slp/test.slp")!; + expect(gameEnd.gameEndMethod).toEqual(manualGameEnd.gameEndMethod); + expect(gameEnd.lrasInitiatorIndex).toEqual(manualGameEnd.lrasInitiatorIndex); + expect(gameEnd.placements.length).toEqual(manualGameEnd.placements.length); + }); + + it("should return the correct placings for 2 player games", () => { + const manualGameEnd = getManualGameEnd("slp/placementsTest/ffa_1p2p_winner_2p.slp")!; + const placements = manualGameEnd.placements!; + expect(placements).toHaveLength(4); + console.log(JSON.stringify(placements)); + expect(placements[0].position).toBe(1); // player in port 1 is on second place + expect(placements[0].playerIndex).toBe(0); + expect(placements[1].position).toBe(0); // player in port 2 is on first place + expect(placements[1].playerIndex).toBe(1); + }); + + it("should return placings for 3 player games", () => { + let manualGameEnd = getManualGameEnd("slp/placementsTest/ffa_1p2p3p_winner_3p.slp")!; + let placements = manualGameEnd.placements!; + expect(placements).toBeDefined(); + expect(placements).toHaveLength(4); + + expect(placements[0].playerIndex).toBe(0); + expect(placements[1].playerIndex).toBe(1); + expect(placements[2].playerIndex).toBe(2); + expect(placements[3].playerIndex).toBe(3); + + expect(placements[0].position).toBe(1); // Expect player 1 to be on second place + expect(placements[1].position).toBe(2); // Expect player 2 to be on third place + expect(placements[2].position).toBe(0); // Expect player 3 to be first place + expect(placements[3].position).toBe(-1); // Expect player 4 to not be present + + manualGameEnd = getManualGameEnd("slp/placementsTest/ffa_1p2p4p_winner_4p.slp")!; + placements = manualGameEnd.placements!; + expect(placements).toBeDefined(); + expect(placements).toHaveLength(4); + + expect(placements[0].playerIndex).toBe(0); + expect(placements[1].playerIndex).toBe(1); + expect(placements[2].playerIndex).toBe(2); + expect(placements[3].playerIndex).toBe(3); + + expect(placements[0].position).toBe(1); // Expect player 1 to be on second place + expect(placements[1].position).toBe(2); // Expect player 2 to be on third place + expect(placements[2].position).toBe(-1); // Expect player 3 to not be present + expect(placements[3].position).toBe(0); // Expect player 4 to be first place + }); +}); + +function getManualGameEnd(filePath: string) { + return getGameEnd({ source: SlpInputSource.FILE, filePath }); +}