feat: parse stage events from 3.18.0 (#135)

This commit is contained in:
Aitch
2025-05-25 21:42:44 -06:00
committed by GitHub
parent 86fd6a5ce4
commit a203b2996e
7 changed files with 168 additions and 1 deletions

BIN
slp/FodPlatforms.slp Normal file

Binary file not shown.

BIN
slp/Whispy.slp Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -9,6 +9,9 @@ export enum Command {
ITEM_UPDATE = 0x3b,
FRAME_BOOKEND = 0x3c,
GECKO_LIST = 0x3d,
FOD_PLATFORM = 0x3f,
WHISPY = 0x40,
STADIUM_TRANSFORMATION = 0x41,
}
export type PlayerType = {
@@ -274,6 +277,50 @@ export type GeckoCodeType = {
contents: Uint8Array;
};
export enum FodPlatformSide {
RIGHT = 0,
LEFT = 1,
}
export type FodPlatformType = {
frame: number | null;
platform: FodPlatformSide | null;
height: number | null;
};
export enum WhispyBlowDirection {
NONE = 0,
LEFT = 1,
RIGHT = 2,
}
export type WhispyType = {
frame: number | null;
direction: WhispyBlowDirection | null;
};
export enum StadiumTransformation {
FIRE = 3,
GRASS = 4,
NORMAL = 5,
ROCK = 6,
WATER = 9,
}
export enum StadiumTransformationEvent {
INITIATE = 2,
ON_MONITOR = 3,
RECEDING = 4,
RISING = 5,
FINISH = 6,
}
export type StadiumTransformationType = {
frame: number | null;
event: StadiumTransformationEvent | null;
transformation: StadiumTransformation | null;
};
export type MetadataType = {
startAt?: string | null;
playedOn?: string | null;
@@ -300,7 +347,10 @@ export type EventPayloadTypes =
| ItemUpdateType
| FrameBookendType
| GameEndType
| GeckoListType;
| GeckoListType
| FodPlatformType
| WhispyType
| StadiumTransformationType;
export type EventCallbackFunc = (
command: Command,
@@ -308,6 +358,8 @@ export type EventCallbackFunc = (
buffer?: Uint8Array | null,
) => boolean;
export type StageEventTypes = FodPlatformType | WhispyType | StadiumTransformationType;
export type FrameEntryType = {
frame: number;
start?: FrameStartType;
@@ -324,6 +376,7 @@ export type FrameEntryType = {
} | null;
};
items?: ItemUpdateType[];
stageEvents?: StageEventTypes[];
};
export enum Frames {

View File

@@ -16,7 +16,11 @@ import type {
ItemUpdateType,
PostFrameUpdateType,
PreFrameUpdateType,
FodPlatformType,
WhispyType,
StadiumTransformationType,
RollbackFrames,
StageEventTypes,
} from "../types";
import { ItemSpawnType } from "../types";
import { Command, Frames, GameMode } from "../types";
@@ -91,6 +95,15 @@ export class SlpParser extends EventEmitter {
case Command.GECKO_LIST:
this._handleGeckoList(payload as GeckoListType);
break;
case Command.FOD_PLATFORM:
this._handleStageEvent(payload as FodPlatformType);
break;
case Command.WHISPY:
this._handleStageEvent(payload as WhispyType);
break;
case Command.STADIUM_TRANSFORMATION:
this._handleStageEvent(payload as StadiumTransformationType);
break;
}
}
@@ -293,6 +306,15 @@ export class SlpParser extends EventEmitter {
}
}
private _handleStageEvent(payload: StageEventTypes): void {
const currentFrameNumber = payload.frame!;
const stageEvents = this.frames[currentFrameNumber]?.stageEvents ?? [];
stageEvents.push(payload);
// Set stageEvents with newest
set(this.frames, [currentFrameNumber, "stageEvents"], stageEvents);
}
/**
* Fires off the FINALIZED_FRAME event for frames up until a certain number
* @param num The frame to finalize until

View File

@@ -583,6 +583,23 @@ export function parseMessage(command: Command, payload: Uint8Array): EventPayloa
contents: payload.slice(1),
codes: codes,
};
case Command.FOD_PLATFORM:
return {
frame: readInt32(view, 0x1),
platform: readInt8(view, 0x5),
height: readFloat(view, 0x6),
};
case Command.WHISPY:
return {
frame: readInt32(view, 0x1),
direction: readInt8(view, 0x5),
};
case Command.STADIUM_TRANSFORMATION:
return {
frame: readInt32(view, 0x1),
event: readUint16(view, 0x5),
transformation: readUint16(view, 0x7),
};
default:
return null;
}

75
test/stages.spec.ts Normal file
View File

@@ -0,0 +1,75 @@
import { Frames, SlippiGame, StadiumTransformationEvent, FodPlatformSide, WhispyBlowDirection } from "../src";
describe("when extracting stadium transformation information", () => {
it("should properly increment event ids", () => {
const game = new SlippiGame("slp/stadiumTransformations.slp");
const frames = game.getFrames();
let lastEventId = -1;
let lastTransformationId = -1;
for (let frameNum = Frames.FIRST; frames[frameNum]; frameNum++) {
const frame = frames[frameNum];
if (frame.stageEvents) {
frame.stageEvents.forEach((e) => {
if (e.transformation != lastTransformationId) {
expect(e.event).toBe(StadiumTransformationEvent.INITIATE);
lastTransformationId = e.transformation;
lastEventId = e.event;
} else {
expect(e.event).toBe((lastEventId + 1) % 7);
lastEventId = e.event;
}
});
}
}
});
});
describe("when extracting FOD platform information", () => {
it("should properly parse platform height", () => {
const game = new SlippiGame("slp/FodPlatforms.slp");
const frames = game.getFrames();
let prevHeightLeft = 20.0;
let prevHeightRight = 28.0;
for (let frameNum = Frames.FIRST; frames[frameNum]; frameNum++) {
const frame = frames[frameNum];
if (frame.stageEvents) {
frame.stageEvents.forEach((e) => {
if (e.platform == FodPlatformSide.LEFT) {
expect(Math.abs(e.height - prevHeightLeft)).toBeLessThan(0.2);
prevHeightLeft = e.height;
} else {
expect(Math.abs(e.height - prevHeightRight)).toBeLessThan(0.2);
prevHeightRight = e.height;
}
});
}
}
});
});
describe("when extracting Dreamland Whispy information", () => {
it("should properly parse blow directions", () => {
const game = new SlippiGame("slp/Whispy.slp");
const frames = game.getFrames();
let prevBlowDirection = WhispyBlowDirection.NONE;
for (let frameNum = Frames.FIRST; frames[frameNum]; frameNum++) {
const frame = frames[frameNum];
if (frame.stageEvents) {
frame.stageEvents.forEach((e) => {
if (prevBlowDirection == WhispyBlowDirection.LEFT) {
expect(e.direction).toBe(WhispyBlowDirection.NONE);
} else if (prevBlowDirection == WhispyBlowDirection.RIGHT) {
expect(e.direction).toBe(WhispyBlowDirection.NONE);
} else {
expect(e.direction).not.toBe(WhispyBlowDirection.NONE);
}
prevBlowDirection = e.direction;
});
}
}
});
});