diff --git a/package.json b/package.json index c931d785..9dcddfb4 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "test": "jest --verbose", "coverage": "yarn run test -- --coverage", "postcoverage": "open-cli coverage/lcov-report/index.html", - "lint": "eslint \"src/**/*.ts\"", + "lint": "eslint \"src/**/*.ts\" \"test/*.ts\"", "lint:fix": "yarn run lint --fix", "clean": "rimraf dist", "prebuild": "yarn run clean", diff --git a/src/stats/actions.ts b/src/stats/actions.ts index 2dc545e7..02897833 100644 --- a/src/stats/actions.ts +++ b/src/stats/actions.ts @@ -96,14 +96,6 @@ function didStartLedgegrab(currentAnimation: State, previousAnimation: State): b function handleActionCompute(state: PlayerActionState, indices: PlayerIndexedType, frame: FrameEntryType): void { const playerFrame = frame.players[indices.playerIndex].post; - const incrementCount = (field: string, condition: boolean): void => { - if (!condition) { - return; - } - - // FIXME: ActionsCountsType should be a map of actions -> number, instead of accessing the field via string - (state.playerCounts as any)[field] += 1; - }; // Manage animation state state.animations.push(playerFrame.actionStateId); @@ -114,23 +106,25 @@ function handleActionCompute(state: PlayerActionState, indices: PlayerIndexedTyp const prevAnimation = last3Frames[last3Frames.length - 2]; // Increment counts based on conditions - const didDashDance = _.isEqual(last3Frames, dashDanceAnimations); - incrementCount("dashDanceCount", didDashDance); - - const didRoll = didStartRoll(currentAnimation, prevAnimation); - incrementCount("rollCount", didRoll); - - const didSpotDodge = didStartSpotDodge(currentAnimation, prevAnimation); - incrementCount("spotDodgeCount", didSpotDodge); - - const didAirDodge = didStartAirDodge(currentAnimation, prevAnimation); - incrementCount("airDodgeCount", didAirDodge); - - const didGrabLedge = didStartLedgegrab(currentAnimation, prevAnimation); - incrementCount("ledgegrabCount", didGrabLedge); + const playerCounts = state.playerCounts; + if (_.isEqual(last3Frames, dashDanceAnimations)) { + playerCounts.dashDanceCount++; + } + if (didStartRoll(currentAnimation, prevAnimation)) { + playerCounts.rollCount++; + } + if (didStartSpotDodge(currentAnimation, prevAnimation)) { + playerCounts.spotDodgeCount++; + } + if (didStartAirDodge(currentAnimation, prevAnimation)) { + playerCounts.airDodgeCount++; + } + if (didStartLedgegrab(currentAnimation, prevAnimation)) { + playerCounts.ledgegrabCount++; + } // Handles wavedash detection (and waveland) - handleActionWavedash(state.playerCounts, state.animations); + handleActionWavedash(playerCounts, state.animations); } function handleActionWavedash(counts: ActionCountsType, animations: State[]): void { diff --git a/src/stats/combos.ts b/src/stats/combos.ts index eec6c313..1ff9d378 100644 --- a/src/stats/combos.ts +++ b/src/stats/combos.ts @@ -49,13 +49,19 @@ function handleComboCompute( combos: ComboType[], ): void { const playerFrame: PostFrameUpdateType = frame.players[indices.playerIndex].post; - // FIXME: use type PostFrameUpdateType instead of any - // This is because the default value {} should not be casted as a type of PostFrameUpdateType - const prevPlayerFrame: any = _.get(frames, [playerFrame.frame - 1, "players", indices.playerIndex, "post"], {}); + const prevPlayerFrame: PostFrameUpdateType = _.get(frames, [ + playerFrame.frame - 1, + "players", + indices.playerIndex, + "post", + ]); const opponentFrame: PostFrameUpdateType = frame.players[indices.opponentIndex].post; - // FIXME: use type PostFrameUpdateType instead of any - // This is because the default value {} should not be casted as a type of PostFrameUpdateType - const prevOpponentFrame: any = _.get(frames, [playerFrame.frame - 1, "players", indices.opponentIndex, "post"], {}); + const prevOpponentFrame: PostFrameUpdateType = _.get(frames, [ + playerFrame.frame - 1, + "players", + indices.opponentIndex, + "post", + ]); const opntIsDamaged = isDamaged(opponentFrame.actionStateId); const opntIsGrabbed = isGrabbed(opponentFrame.actionStateId); @@ -69,7 +75,7 @@ function handleComboCompute( // null and null < null = false const actionChangedSinceHit = playerFrame.actionStateId !== state.lastHitAnimation; const actionCounter = playerFrame.actionStateCounter; - const prevActionCounter = prevPlayerFrame.actionStateCounter; + const prevActionCounter = _.get(prevPlayerFrame, "actionStateCounter"); const actionFrameCounterReset = actionCounter < prevActionCounter; if (actionChangedSinceHit || actionFrameCounterReset) { state.lastHitAnimation = null; @@ -84,7 +90,7 @@ function handleComboCompute( opponentIndex: indices.opponentIndex, startFrame: playerFrame.frame, endFrame: null, - startPercent: prevOpponentFrame.percent || 0, + startPercent: _.get(prevOpponentFrame, "percent", 0), currentPercent: opponentFrame.percent || 0, endPercent: null, moves: [], @@ -115,7 +121,7 @@ function handleComboCompute( // Store previous frame animation to consider the case of a trade, the previous // frame should always be the move that actually connected... I hope - state.lastHitAnimation = prevPlayerFrame.actionStateId; + state.lastHitAnimation = _.get(prevPlayerFrame, "actionStateId"); } } @@ -158,7 +164,7 @@ function handleComboCompute( // If combo should terminate, mark the end states and add it to list if (shouldTerminate) { state.combo.endFrame = playerFrame.frame; - state.combo.endPercent = prevOpponentFrame.percent || 0; + state.combo.endPercent = _.get(prevOpponentFrame, "percent", 0); state.combo = null; state.move = null; diff --git a/src/stats/conversions.ts b/src/stats/conversions.ts index 3ef368e4..e23425a4 100644 --- a/src/stats/conversions.ts +++ b/src/stats/conversions.ts @@ -96,13 +96,19 @@ function handleConversionCompute( conversions: ConversionType[], ): void { const playerFrame: PostFrameUpdateType = frame.players[indices.playerIndex].post; - // FIXME: use type PostFrameUpdateType instead of any - // This is because the default value {} should not be casted as a type of PostFrameUpdateType - const prevPlayerFrame: any = _.get(frames, [playerFrame.frame - 1, "players", indices.playerIndex, "post"], {}); + const prevPlayerFrame: PostFrameUpdateType = _.get(frames, [ + playerFrame.frame - 1, + "players", + indices.playerIndex, + "post", + ]); const opponentFrame: PostFrameUpdateType = frame.players[indices.opponentIndex].post; - // FIXME: use type PostFrameUpdateType instead of any - // This is because the default value {} should not be casted as a type of PostFrameUpdateType - const prevOpponentFrame: any = _.get(frames, [playerFrame.frame - 1, "players", indices.opponentIndex, "post"], {}); + const prevOpponentFrame: PostFrameUpdateType = _.get(frames, [ + playerFrame.frame - 1, + "players", + indices.opponentIndex, + "post", + ]); const opntIsDamaged = isDamaged(opponentFrame.actionStateId); const opntIsGrabbed = isGrabbed(opponentFrame.actionStateId); @@ -116,7 +122,7 @@ function handleConversionCompute( // null and null < null = false const actionChangedSinceHit = playerFrame.actionStateId !== state.lastHitAnimation; const actionCounter = playerFrame.actionStateCounter; - const prevActionCounter = prevPlayerFrame.actionStateCounter; + const prevActionCounter = _.get(prevPlayerFrame, "actionStateCounter"); const actionFrameCounterReset = actionCounter < prevActionCounter; if (actionChangedSinceHit || actionFrameCounterReset) { state.lastHitAnimation = null; @@ -131,7 +137,7 @@ function handleConversionCompute( opponentIndex: indices.opponentIndex, startFrame: playerFrame.frame, endFrame: null, - startPercent: prevOpponentFrame.percent || 0, + startPercent: _.get(prevOpponentFrame, "percent", 0), currentPercent: opponentFrame.percent || 0, endPercent: null, moves: [], @@ -163,7 +169,7 @@ function handleConversionCompute( // Store previous frame animation to consider the case of a trade, the previous // frame should always be the move that actually connected... I hope - state.lastHitAnimation = prevPlayerFrame.actionStateId; + state.lastHitAnimation = _.get(prevPlayerFrame, "actionStateId"); } } @@ -211,7 +217,7 @@ function handleConversionCompute( // If conversion should terminate, mark the end states and add it to list if (shouldTerminate) { state.conversion.endFrame = playerFrame.frame; - state.conversion.endPercent = prevOpponentFrame.percent || 0; + state.conversion.endPercent = _.get(prevOpponentFrame, "percent", 0); state.conversion = null; state.move = null; diff --git a/src/stats/stocks.ts b/src/stats/stocks.ts index 8e54da6c..1147f833 100644 --- a/src/stats/stocks.ts +++ b/src/stats/stocks.ts @@ -2,7 +2,7 @@ import _ from "lodash"; import { isDead, didLoseStock, PlayerIndexedType, StockType } from "./common"; -import { FrameEntryType, FramesType } from "../types"; +import { FrameEntryType, FramesType, PostFrameUpdateType } from "../types"; import { StatComputer } from "./stats"; interface StockState { @@ -44,8 +44,12 @@ function handleStockCompute( stocks: StockType[], ): void { const playerFrame = frame.players[indices.playerIndex].post; - // FIXME: use PostFrameUpdateType instead of any - const prevPlayerFrame: any = _.get(frames, [playerFrame.frame - 1, "players", indices.playerIndex, "post"], {}); + const prevPlayerFrame: PostFrameUpdateType = _.get(frames, [ + playerFrame.frame - 1, + "players", + indices.playerIndex, + "post", + ]); // If there is currently no active stock, wait until the player is no longer spawning. // Once the player is no longer spawning, start the stock diff --git a/src/utils/slpFile.ts b/src/utils/slpFile.ts index 1d919b6e..67ca0943 100644 --- a/src/utils/slpFile.ts +++ b/src/utils/slpFile.ts @@ -10,7 +10,13 @@ const DEFAULT_NICKNAME = "unknown"; export interface SlpFileMetadata { startTime: Moment; lastFrame: number; - players: any; + players: { + [playerIndex: number]: { + characterUsage: { + [internalCharacterId: number]: number; + }; + }; + }; consoleNickname?: string; } @@ -111,17 +117,17 @@ export class SlpFile extends Writable { this.metadata.lastFrame = frame; // Update character usage - const prevPlayer = get(this.metadata, ["players", `${playerIndex}`]) || {}; - const characterUsage = prevPlayer.characterUsage || {}; - const curCharFrames = characterUsage[internalCharacterId] || 0; + const characterUsage: { + [internalCharacterId: number]: number; + } = get(this.metadata, ["players", playerIndex, "characterUsage"]); + const curCharFrames = get(characterUsage, internalCharacterId, 0); const player = { - ...prevPlayer, characterUsage: { ...characterUsage, [internalCharacterId]: curCharFrames + 1, }, }; - this.metadata.players[`${playerIndex}`] = player; + this.metadata.players[playerIndex] = player; break; } } diff --git a/test/filewriter.spec.ts b/test/filewriter.spec.ts index 8e2df021..5d957801 100644 --- a/test/filewriter.spec.ts +++ b/test/filewriter.spec.ts @@ -1,8 +1,12 @@ import fs from "fs"; import { openSlpFile, SlpInputSource } from "../src/utils/slpReader"; -import { SlpFileWriter } from "../src"; +import { SlpFileWriter, SlippiGame } from "../src"; import { Writable } from "stream"; +// On my machine, >100 is required to give the slpFile.ts "finish" callback time to execute. +// I thought a 'yield' 0 ms setTimout would allow the callback to execute, but that's not the case. +const TIMEOUT_MS = 1000; + describe("when ending SlpFileWriter", () => { it("should write data length to file", async () => { const testFilePath = "slp/finalizedFrame.slp"; @@ -14,16 +18,10 @@ describe("when ending SlpFileWriter", () => { const testFd = fs.openSync(testFilePath, "r"); const newPos = pipeMessageSizes(testFd, dataPos, slpFileWriter); - const newFilename = slpFileWriter.getCurrentFilename(); - const buffer = Buffer.alloc(4); pipeAllEvents(testFd, newPos, dataPos + dataLength, slpFileWriter, slpFile.messageSizes); - await new Promise((resolve) => { - // On my machine, >100 is required to give the slpFile.ts "finish" callback time to execute. - // I thought a 'yield' 0 ms setTimout would allow the callback to execute, but that's not the case. - const timeoutMs = 1000; - + await new Promise((resolve): void => { setTimeout(() => { const writtenDataLength = openSlpFile({ source: SlpInputSource.FILE, filePath: newFilename }).rawDataLength; fs.unlinkSync(newFilename); @@ -31,7 +29,33 @@ describe("when ending SlpFileWriter", () => { expect(writtenDataLength).toBe(dataLength); resolve(); - }, timeoutMs); + }, TIMEOUT_MS); + }); + }); + + it("should track and write player data to metadata in file", async () => { + const testFilePath = "slp/finalizedFrame.slp"; + + const slpFileWriter = new SlpFileWriter(); + const slpFile = openSlpFile({ source: SlpInputSource.FILE, filePath: testFilePath }); + const dataPos = slpFile.rawDataPosition; + + const testFd = fs.openSync(testFilePath, "r"); + const newPos = pipeMessageSizes(testFd, dataPos, slpFileWriter); + const newFilename = slpFileWriter.getCurrentFilename(); + + pipeAllEvents(testFd, newPos, dataPos + slpFile.rawDataLength, slpFileWriter, slpFile.messageSizes); + await new Promise((resolve): void => { + setTimeout(() => { + const players = new SlippiGame(newFilename).getMetadata().players; + fs.unlinkSync(newFilename); + + expect(Object.keys(players).length).toBe(2); + expect(players[0].characters).toEqual({ 0: 17558 }); + expect(players[1].characters).toEqual({ 1: 17558 }); + + resolve(); + }, TIMEOUT_MS); }); }); }); @@ -59,13 +83,12 @@ const pipeAllEvents = function ( messageSizes: { [command: number]: number; }, -) { +): void { let pos = start; while (pos < end) { const commandByteBuffer = new Uint8Array(1); fs.readSync(fd, commandByteBuffer, 0, 1, pos); const length = messageSizes[commandByteBuffer[0]] + 1; - const commandByte = commandByteBuffer[0]; const buffer = new Uint8Array(length); fs.readSync(fd, buffer, 0, length, pos); diff --git a/test/game.spec.ts b/test/game.spec.ts index 4561e415..e0fbea90 100644 --- a/test/game.spec.ts +++ b/test/game.spec.ts @@ -1,7 +1,7 @@ import _ from "lodash"; // import path from 'path'; import fs from "fs"; -import { SlippiGame } from "../src"; +import { FrameEntryType, FramesType, GameEndType, GameStartType, MetadataType, SlippiGame, StatsType } from "../src"; it("should correctly return game settings", () => { const game = new SlippiGame("slp/sheik_vs_ics_yoshis.slp"); @@ -34,6 +34,10 @@ it("should correctly return stats", () => { expect(stats.actionCounts[0].wavedashCount).toBe(16); expect(stats.actionCounts[0].wavelandCount).toBe(1); expect(stats.actionCounts[0].airDodgeCount).toBe(3); + expect(stats.actionCounts[0].dashDanceCount).toBe(39); + expect(stats.actionCounts[0].spotDodgeCount).toBe(0); + expect(stats.actionCounts[0].ledgegrabCount).toBe(1); + expect(stats.actionCounts[0].rollCount).toBe(0); // Test overall expect(stats.overall[0].inputCounts.total).toBe(494); @@ -82,7 +86,6 @@ it("should be able to read netplay names and codes", () => { it("should be able to read console nickname", () => { const game = new SlippiGame("slp/realtimeTest.slp"); - const metadata = game.getMetadata().consoleNick; expect(game.getMetadata().consoleNick).toBe("Day 1"); }); @@ -119,7 +122,14 @@ it("should support realtime parsing", () => { let data, copyPos = 0; - const getData = () => ({ + const getData = (): { + settings: GameStartType; + frames: FramesType; + metadata: MetadataType; + gameEnd: GameEndType | null; + stats: StatsType; + latestFrame: FrameEntryType | null; + } => ({ settings: game.getSettings(), frames: game.getFrames(), metadata: game.getMetadata(), diff --git a/test/realtime.spec.ts b/test/realtime.spec.ts index 6539319b..ca2967e8 100644 --- a/test/realtime.spec.ts +++ b/test/realtime.spec.ts @@ -134,11 +134,11 @@ describe("when reading finalised frames from SlpParser", () => { }); }); -const pipeFileContents = async (filename: string, destination: Writable, options?: any): Promise => { +const pipeFileContents = async (filename: string, destination: Writable): Promise => { return new Promise((resolve): void => { const readStream = fs.createReadStream(filename); readStream.on("open", () => { - readStream.pipe(destination, options); + readStream.pipe(destination); }); readStream.on("close", () => { resolve();