Skip to content

Commit

Permalink
Merge pull request #113 from project-slippi/feature/gecko-list
Browse files Browse the repository at this point in the history
feat: extract gecko list
  • Loading branch information
JLaferri authored Sep 10, 2022
2 parents fbcd640 + 833f73e commit 95751ef
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 9 deletions.
Binary file added slp/geckoCodes.slp
Binary file not shown.
22 changes: 18 additions & 4 deletions src/SlippiGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ import {
StockComputer,
} from "./stats";
// Type imports
import type { FrameEntryType, FramesType, GameEndType, GameStartType, MetadataType, RollbackFrames } from "./types";
import type {
EventCallbackFunc,
FrameEntryType,
FramesType,
GameEndType,
GameStartType,
GeckoListType,
MetadataType,
RollbackFrames,
} from "./types";
import { SlpParser, SlpParserEvent } from "./utils/slpParser";
import type { SlpReadInput } from "./utils/slpReader";
import { closeSlpFile, getMetadata, iterateEvents, openSlpFile, SlpInputSource } from "./utils/slpReader";
Expand Down Expand Up @@ -69,7 +78,7 @@ export class SlippiGame {
});
}

private _process(settingsOnly = false): void {
private _process(shouldStop: EventCallbackFunc = () => false): void {
if (this.parser.getGameEnd() !== null) {
return;
}
Expand All @@ -84,7 +93,7 @@ export class SlippiGame {
return false;
}
this.parser.handleCommand(command, payload);
return settingsOnly && this.parser.getSettings() !== null;
return shouldStop(command, payload);
},
this.readPosition,
);
Expand All @@ -97,7 +106,7 @@ export class SlippiGame {
*/
public getSettings(): GameStartType | null {
// Settings is only complete after post-frame update
this._process(true);
this._process(() => this.parser.getSettings() !== null);
return this.parser.getSettings();
}

Expand All @@ -121,6 +130,11 @@ export class SlippiGame {
return this.parser.getRollbackFrames();
}

public getGeckoList(): GeckoListType | null {
this._process(() => this.parser.getGeckoList() !== null);
return this.parser.getGeckoList();
}

public getStats(): StatsType | null {
if (this.finalStats) {
return this.finalStats;
Expand Down
16 changes: 15 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Command {
SPLIT_MESSAGE = 0x10,
MESSAGE_SIZES = 0x35,
GAME_START = 0x36,
PRE_FRAME_UPDATE = 0x37,
Expand All @@ -7,6 +8,7 @@ export enum Command {
FRAME_START = 0x3a,
ITEM_UPDATE = 0x3b,
FRAME_BOOKEND = 0x3c,
GECKO_LIST = 0x3d,
}

export interface PlayerType {
Expand Down Expand Up @@ -136,6 +138,17 @@ export interface GameEndType {
lrasInitiatorIndex: number | null;
}

export interface GeckoListType {
codes: GeckoCodeType[];
contents: Uint8Array;
}

export interface GeckoCodeType {
type: number | null;
address: number | null;
contents: Uint8Array;
}

export interface MetadataType {
startAt?: string | null;
playedOn?: string | null;
Expand All @@ -161,7 +174,8 @@ export type EventPayloadTypes =
| PostFrameUpdateType
| ItemUpdateType
| FrameBookendType
| GameEndType;
| GameEndType
| GeckoListType;

export type EventCallbackFunc = (command: Command, payload?: EventPayloadTypes | null) => boolean;

Expand Down
13 changes: 13 additions & 0 deletions src/utils/slpParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
FramesType,
GameEndType,
GameStartType,
GeckoListType,
ItemUpdateType,
PostFrameUpdateType,
PreFrameUpdateType,
Expand Down Expand Up @@ -46,6 +47,7 @@ export class SlpParser extends EventEmitter {
private settingsComplete = false;
private lastFinalizedFrame = Frames.FIRST - 1;
private options: SlpParserOptions;
private geckoList: GeckoListType | null = null;

public constructor(options?: Partial<SlpParserOptions>) {
super();
Expand Down Expand Up @@ -79,6 +81,9 @@ export class SlpParser extends EventEmitter {
case Command.GAME_END:
this._handleGameEnd(payload as GameEndType);
break;
case Command.GECKO_LIST:
this._handleGeckoList(payload as GeckoListType);
break;
}
}

Expand Down Expand Up @@ -140,6 +145,14 @@ export class SlpParser extends EventEmitter {
return this.frames[num] || null;
}

public getGeckoList(): GeckoListType | null {
return this.geckoList;
}

private _handleGeckoList(payload: GeckoListType): void {
this.geckoList = payload;
}

private _handleGameEnd(payload: GameEndType): void {
// Finalize remaining frames if necessary
if (this.latestFrameIndex !== null && this.latestFrameIndex !== this.lastFinalizedFrame) {
Expand Down
80 changes: 76 additions & 4 deletions src/utils/slpReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import fs from "fs";
import iconv from "iconv-lite";
import { mapValues } from "lodash";

import type { EventCallbackFunc, EventPayloadTypes, MetadataType, PlayerType, SelfInducedSpeedsType } from "../types";
import type {
EventCallbackFunc,
EventPayloadTypes,
GeckoCodeType,
MetadataType,
PlayerType,
SelfInducedSpeedsType,
} from "../types";
import { Command } from "../types";
import { toHalfwidth } from "./fullwidth";

Expand Down Expand Up @@ -211,12 +218,13 @@ export function iterateEvents(

// Generate read buffers for each
const commandPayloadBuffers = mapValues(slpFile.messageSizes, (size) => new Uint8Array(size + 1));
let splitMessageBuffer = new Uint8Array(0);

const commandByteBuffer = new Uint8Array(1);
while (readPosition < stopReadingAt) {
readRef(ref, commandByteBuffer, 0, 1, readPosition);
const commandByte = commandByteBuffer[0] as number;
const buffer = commandPayloadBuffers[commandByte];
let commandByte = (commandByteBuffer[0] as number) ?? 0;
let buffer = commandPayloadBuffers[commandByte];
if (buffer === undefined) {
// If we don't have an entry for this command, return false to indicate failed read
return readPosition;
Expand All @@ -226,14 +234,46 @@ export function iterateEvents(
return readPosition;
}

const advanceAmount = buffer.length;

readRef(ref, buffer, 0, buffer.length, readPosition);
if (commandByte === Command.SPLIT_MESSAGE) {
// Here we have a split message, we will collect data from them until the last
// message of the list is received
const view = new DataView(buffer.buffer);
const size = readUint16(view, 0x201) ?? 512;
const isLastMessage = readBool(view, 0x204);
const internalCommand = readUint8(view, 0x203) ?? 0;

// If this is the first message, initialize the splitMessageBuffer
// with the internal command byte because our parseMessage function
// seems to expect a command byte at the start
if (splitMessageBuffer.length === 0) {
splitMessageBuffer = new Uint8Array(1);
splitMessageBuffer[0] = internalCommand;
}

// Collect new data into splitMessageBuffer
const appendBuf = buffer.slice(0x1, 0x1 + size);
const mergedBuf = new Uint8Array(splitMessageBuffer.length + appendBuf.length);
mergedBuf.set(splitMessageBuffer);
mergedBuf.set(appendBuf, splitMessageBuffer.length);
splitMessageBuffer = mergedBuf;

if (isLastMessage) {
commandByte = splitMessageBuffer[0] ?? 0;
buffer = splitMessageBuffer;
splitMessageBuffer = new Uint8Array(0);
}
}

const parsedPayload = parseMessage(commandByte, buffer);
const shouldStop = callback(commandByte, parsedPayload);
if (shouldStop) {
break;
}

readPosition += buffer.length;
readPosition += advanceAmount;
}

return readPosition;
Expand Down Expand Up @@ -416,6 +456,38 @@ export function parseMessage(command: Command, payload: Uint8Array): EventPayloa
gameEndMethod: readUint8(view, 0x1),
lrasInitiatorIndex: readInt8(view, 0x2),
};
case Command.GECKO_LIST:
const codes: GeckoCodeType[] = [];
let pos = 1;
while (pos < payload.length) {
const word1 = readUint32(view, pos) ?? 0;
const codetype = (word1 >> 24) & 0xfe;
const address = (word1 & 0x01ffffff) + 0x80000000;

let offset = 8; // Default code length, most codes are this length
if (codetype === 0xc0 || codetype === 0xc2) {
const lineCount = readUint32(view, pos + 4) ?? 0;
offset = 8 + lineCount * 8;
} else if (codetype === 0x06) {
const byteLen = readUint32(view, pos + 4) ?? 0;
offset = 8 + ((byteLen + 7) & 0xfffffff8);
} else if (codetype === 0x08) {
offset = 16;
}

codes.push({
type: codetype,
address: address,
contents: payload.slice(pos, pos + offset),
});

pos += offset;
}

return {
contents: payload.slice(1),
codes: codes,
};
default:
return null;
}
Expand Down
9 changes: 9 additions & 0 deletions test/game.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ it("should be able to support reading from an array buffer input", () => {
expect(_.last(settings.players).characterId).toBe(0xe);
});

it("should extract gecko list", () => {
// This code will contain every code listed here:
// https://github.com/project-slippi/slippi-ssbm-asm/blob/adf85d157dbc1cbeff1e81d4d2d2ec83cad61852/Output/InjectionLists/list_netplay.json
const game = new SlippiGame("slp/geckoCodes.slp");
const geckoList = game.getGeckoList();
expect(geckoList?.codes.length).toBe(457);
expect(geckoList?.codes?.[0]?.address).toBe(0x8015ee98);
});

it("should support realtime parsing", () => {
const fullData = fs.readFileSync("slp/realtimeTest.slp");
const buf = Buffer.alloc(100e6); // Allocate 100 MB of space
Expand Down

0 comments on commit 95751ef

Please sign in to comment.