From 4327ea2c3dcd4bce28dd24ad017178188fc2143f Mon Sep 17 00:00:00 2001 From: Jeremy Lee Date: Sun, 10 Jan 2021 12:51:35 +0900 Subject: [PATCH 1/6] fix test lint and include test files in yarn lint --- package.json | 2 +- test/filewriter.spec.ts | 7 ++----- test/game.spec.ts | 26 +++++++++----------------- test/realtime.spec.ts | 4 ++-- 4 files changed, 14 insertions(+), 25 deletions(-) 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/test/filewriter.spec.ts b/test/filewriter.spec.ts index 8e2df021..38ad1b34 100644 --- a/test/filewriter.spec.ts +++ b/test/filewriter.spec.ts @@ -14,12 +14,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) => { + await new Promise((resolve): void => { // 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; @@ -59,13 +57,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..0430e938 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"); @@ -71,21 +71,6 @@ it("should be able to read nametags", () => { expect(settings3.players[1].nametag).toBe(". 。"); }); -it("should be able to read netplay names and codes", () => { - const game = new SlippiGame("slp/finalizedFrame.slp"); - const metadata = game.getMetadata(); - expect(metadata.players[0].names.netplay).toBe("V"); - expect(metadata.players[0].names.code).toBe("VA#0"); - expect(metadata.players[1].names.netplay).toBe("Fizzi"); - expect(metadata.players[1].names.code).toBe("FIZZI#36"); -}); - -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"); -}); - it("should support PAL version", () => { const palGame = new SlippiGame("slp/pal.slp"); const ntscGame = new SlippiGame("slp/ntsc.slp"); @@ -119,7 +104,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(); From 920489bde8be2776817948822cadc1648bfb0795 Mon Sep 17 00:00:00 2001 From: Jeremy Lee Date: Sun, 10 Jan 2021 14:14:36 +0900 Subject: [PATCH 2/6] fix slpFile.ts lint and add test to verify correctness --- src/utils/slpFile.ts | 18 ++++++++++++------ test/filewriter.spec.ts | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 12 deletions(-) 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 38ad1b34..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"; @@ -18,10 +22,6 @@ describe("when ending SlpFileWriter", () => { pipeAllEvents(testFd, newPos, dataPos + dataLength, slpFileWriter, slpFile.messageSizes); await new Promise((resolve): void => { - // 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; - setTimeout(() => { const writtenDataLength = openSlpFile({ source: SlpInputSource.FILE, filePath: newFilename }).rawDataLength; fs.unlinkSync(newFilename); @@ -29,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); }); }); }); From 1856e3a2d5d6753253acee02767e474a21c41a61 Mon Sep 17 00:00:00 2001 From: Jeremy Lee Date: Sun, 10 Jan 2021 15:43:41 +0900 Subject: [PATCH 3/6] Fix lint in combos.ts, conversions.ts, stocks.ts --- src/stats/combos.ts | 26 ++++++++++++++++---------- src/stats/conversions.ts | 26 ++++++++++++++++---------- src/stats/stocks.ts | 10 +++++++--- 3 files changed, 39 insertions(+), 23 deletions(-) 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 From bea4b7f6a98f9b036104c63bca997fc3d3009b9e Mon Sep 17 00:00:00 2001 From: Jeremy Lee Date: Sun, 10 Jan 2021 18:08:17 +0900 Subject: [PATCH 4/6] fix any lint in actions.ts and add test --- src/stats/actions.ts | 40 +++++++++++++++++----------------------- test/game.spec.ts | 4 ++++ 2 files changed, 21 insertions(+), 23 deletions(-) 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/test/game.spec.ts b/test/game.spec.ts index 0430e938..e39ab5de 100644 --- a/test/game.spec.ts +++ b/test/game.spec.ts @@ -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); From 2615fc64f7d876464ebad27533b6924a13209270 Mon Sep 17 00:00:00 2001 From: Jeremy Lee Date: Sun, 10 Jan 2021 18:14:50 +0900 Subject: [PATCH 5/6] restore mistakenly deleted tests to game.spec.ts --- test/game.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/game.spec.ts b/test/game.spec.ts index e39ab5de..8067b676 100644 --- a/test/game.spec.ts +++ b/test/game.spec.ts @@ -75,6 +75,21 @@ it("should be able to read nametags", () => { expect(settings3.players[1].nametag).toBe(". 。"); }); +it("should be able to read netplay names and codes", () => { + const game = new SlippiGame("slp/finalizedFrame.slp"); + const metadata = game.getMetadata(); + expect(metadata.players[0].names.netplay).toBe("V"); + expect(metadata.players[0].names.code).toBe("VA#0"); + expect(metadata.players[1].names.netplay).toBe("Fizzi"); + expect(metadata.players[1].names.code).toBe("FIZZI#36"); +}); + +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"); +}); + it("should support PAL version", () => { const palGame = new SlippiGame("slp/pal.slp"); const ntscGame = new SlippiGame("slp/ntsc.slp"); From 8fef96802bcf62498b9d08bc9e897f33ac3943c7 Mon Sep 17 00:00:00 2001 From: Jeremy Lee Date: Sun, 10 Jan 2021 18:26:02 +0900 Subject: [PATCH 6/6] delete mistaken unused var in game.spec.ts --- test/game.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/game.spec.ts b/test/game.spec.ts index 8067b676..e0fbea90 100644 --- a/test/game.spec.ts +++ b/test/game.spec.ts @@ -86,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"); });