Skip to content

Commit

Permalink
Add stadium support (#93)
Browse files Browse the repository at this point in the history
* expand game info block

* add event stages

* add timer calculation and test

* Expand GameStartType

* resolve review changes

* resolve review changes

* update game timer and tests

* rectify gameMode and inGameMode

* return enums instead of strings

* fix test pointing to removed attribute

* small fixes

* rename constant

Co-authored-by: Vince Au <vince@canva.com>
  • Loading branch information
cnkeats and vinceau committed Nov 3, 2022
1 parent 33cee2c commit 5a46672
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 9 deletions.
6 changes: 6 additions & 0 deletions src/SlippiGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StockComputer,
} from "./stats";
import type {
EnabledItemType,
EventCallbackFunc,
FrameEntryType,
FramesType,
Expand Down Expand Up @@ -110,6 +111,11 @@ export class SlippiGame {
return this.parser.getSettings();
}

public getItems(): EnabledItemType[] | null {
this._process();
return this.parser.getItems();
}

public getLatestFrame(): FrameEntryType | null {
this._process();
return this.parser.getLatestFrame();
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./stats";
export * from "./types";

// Utils
export * from "./utils/gameTimer";
export * from "./utils/slpFile";
export * from "./utils/slpFileWriter";
export * from "./utils/slpParser";
Expand Down
76 changes: 76 additions & 0 deletions src/melee/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,81 @@ export enum Stage {
TARGET_TEST_GAME_AND_WATCH = 56,
TARGET_TEST_ROY = 57,
TARGET_TEST_GANONDORF = 58,
RACE_TO_THE_FINISH = 82,
GRAB_THE_TROPHIES = 83,
HOME_RUN_CONTEST = 84,
ALL_STAR_LOBBY = 85,
EVENT_ONE = 202,
EVENT_EIGHTEEN = 203,
EVENT_THREE = 204,
EVENT_FOUR = 205,
EVENT_FIVE = 206,
EVENT_SIX = 207,
EVENT_SEVEN = 208,
EVENT_EIGHT = 209,
EVENT_NINE = 210,
EVENT_TEN_PART_ONE = 211,
EVENT_ELEVEN = 212,
EVENT_TWELVE = 213,
EVENT_THIRTEEN = 214,
EVENT_FOURTEEN = 215,
EVENT_THIRTY_SEVEN = 216,
EVENT_SIXTEEN = 217,
EVENT_SEVENTEEN = 218,
EVENT_TWO = 219,
EVENT_NINETEEN = 220,
EVENT_TWENTY_PART_ONE = 221,
EVENT_TWENTY_ONE = 222,
EVENT_TWENTY_TWO = 223,
EVENT_TWENTY_SEVEN = 224,
EVENT_TWENTY_FOUR = 225,
EVENT_TWENTY_FIVE = 226,
EVENT_TWENTY_SIX = 227,
EVENT_TWENTY_THREE = 228,
EVENT_TWENTY_EIGHT = 229,
EVENT_TWENTY_NINE = 230,
EVENT_THIRTY_PART_ONE = 231,
EVENT_THIRTY_ONE = 232,
EVENT_THIRTY_TWO = 233,
EVENT_THIRTY_THREE = 234,
EVENT_THIRTY_FOUR = 235,
EVENT_FORTY_EIGHT = 236,
EVENT_THIRTY_SIX_PART_ONE = 237,
EVENT_FIFTEEN = 238,
EVENT_THIRTY_EIGHT = 239,
EVENT_THIRTY_NINE = 240,
EVENT_FORTY_PART_ONE = 241,
EVENT_FORTY_ONE = 242,
EVENT_FORTY_TWO = 243,
EVENT_FORTY_THREE = 244,
EVENT_FORTY_FOUR = 245,
EVENT_FORTY_FIVE = 246,
EVENT_FORTY_SIX = 247,
EVENT_FORTY_SEVEN = 248,
EVENT_THIRTY_FIVE = 249,
EVENT_FORTY_NINE_PART_ONE = 250,
EVENT_FIFTY = 251,
EVENT_FIFTY_ONE = 252,
EVENT_TEN_PART_TWO = 253,
EVENT_TEN_PART_THREE = 254,
EVENT_TEN_PART_FOUR = 255,
EVENT_TEN_PART_FIVE = 256,
EVENT_TWENTY_PART_TWO = 257,
EVENT_TWENTY_PART_THREE = 258,
EVENT_TWENTY_PART_FOUR = 259,
EVENT_TWENTY_PART_FIVE = 260,
EVENT_THIRTY_PART_TWO = 261,
EVENT_THIRTY_PART_THREE = 262,
EVENT_THIRTY_PART_FOUR = 263,
EVENT_FORTY_PART_TWO = 264,
EVENT_FORTY_PART_THREE = 265,
EVENT_FORTY_PART_FOUR = 266,
EVENT_FORTY_PART_FIVE = 267,
EVENT_FORTY_NINE_PART_TWO = 268,
EVENT_FORTY_NINE_PART_THREE = 269,
EVENT_FORTY_NINE_PART_FOUR = 270,
EVENT_FORTY_NINE_PART_FIVE = 271,
EVENT_FORTY_NINE_PART_SIX = 272,
EVENT_THIRTY_SIX_PART_TWO = 273,
MULTI_MAN_MELEE = 285,
}
105 changes: 102 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,24 @@ export interface PlayerType {
playerIndex: number;
port: number;
characterId: number | null;
characterColor: number | null;
startStocks: number | null;
type: number | null;
startStocks: number | null;
characterColor: number | null;
teamShade: number | null;
handicap: number | null;
teamId: number | null;
staminaMode: boolean | null;
silentCharacter: boolean | null;
invisible: boolean | null;
lowGravity: boolean | null;
blackStockIcon: boolean | null;
metal: boolean | null;
startOnAngelPlatform: boolean | null;
rumbleEnabled: boolean | null;
cpuLevel: number | null;
offenseRatio: number | null;
defenseRatio: number | null;
modelScale: number | null;
controllerFix: string | null;
nametag: string | null;
displayName: string;
Expand All @@ -29,6 +43,8 @@ export interface PlayerType {
export enum GameMode {
VS = 0x02,
ONLINE = 0x08,
TARGET_TEST = 0x0f,
HOME_RUN_CONTEST = 0x20,
}

export enum Language {
Expand All @@ -38,13 +54,22 @@ export enum Language {

export interface GameStartType {
slpVersion: string | null;
timerType: TimerType | null;
inGameMode: number | null;
friendlyFireEnabled: boolean | null;
isTeams: boolean | null;
isPAL: boolean | null;
stageId: number | null;
startingTimerSeconds: number | null;
itemSpawnBehavior: ItemSpawnType | null;
enabledItems: number | null;
players: PlayerType[];
scene: number | null;
gameMode: GameMode | null;
language: Language | null;
gameInfoBlock: GameInfoType | null;
randomSeed: number | null;
isPAL: boolean | null;
isFrozenPS: boolean | null;
}

export interface FrameStartType {
Expand All @@ -53,6 +78,80 @@ export interface FrameStartType {
sceneFrameCounter: number | null;
}

export interface GameInfoType {
gameBitfield1: number | null;
gameBitfield2: number | null;
gameBitfield3: number | null;
gameBitfield4: number | null;
bombRainEnabled: boolean | null;
selfDestructScoreValue: number | null;
itemSpawnBitfield1: number | null;
itemSpawnBitfield2: number | null;
itemSpawnBitfield3: number | null;
itemSpawnBitfield4: number | null;
itemSpawnBitfield5: number | null;
damageRatio: number | null;
}

export enum TimerType {
NONE = 0b00,
DECREASING = 0b10,
INCREASING = 0b11,
}

export enum ItemSpawnType {
OFF = 0xff,
VERY_LOW = 0x00,
LOW = 0x01,
MEDIUM = 0x02,
HIGH = 0x03,
VERY_HIGH = 0x04,
}

export enum EnabledItemType {
METAL_BOX = 2 ** 0,
CLOAKING_DEVICE = 2 ** 1,
POKEBALL = 2 ** 2,
// Bits 4 through 8 of item bitfield 1 are unknown
UNKNOWN_ITEM_BIT_4 = 2 ** 3,
UNKNOWN_ITEM_BIT_5 = 2 ** 4,
UNKNOWN_ITEM_BIT_6 = 2 ** 5,
UNKNOWN_ITEM_BIT_7 = 2 ** 6,
UNKNOWN_ITEM_BIT_8 = 2 ** 7,
FAN = 2 ** 8,
FIRE_FLOWER = 2 ** 9,
SUPER_MUSHROOM = 2 ** 10,
POISON_MUSHROOM = 2 ** 11,
HAMMER = 2 ** 12,
WARP_STAR = 2 ** 13,
SCREW_ATTACK = 2 ** 14,
BUNNY_HOOD = 2 ** 15,
RAY_GUN = 2 ** 16,
FREEZIE = 2 ** 17,
FOOD = 2 ** 18,
MOTION_SENSOR_BOMB = 2 ** 19,
FLIPPER = 2 ** 20,
SUPER_SCOPE = 2 ** 21,
STAR_ROD = 2 ** 22,
LIPS_STICK = 2 ** 23,
HEART_CONTAINER = 2 ** 24,
MAXIM_TOMATO = 2 ** 25,
STARMAN = 2 ** 26,
HOME_RUN_BAT = 2 ** 27,
BEAM_SWORD = 2 ** 28,
PARASOL = 2 ** 29,
GREEN_SHELL = 2 ** 30,
RED_SHELL = 2 ** 31,
CAPSULE = 2 ** 32,
BOX = 2 ** 33,
BARREL = 2 ** 34,
EGG = 2 ** 35,
PARTY_BALL = 2 ** 36,
BARREL_CANNON = 2 ** 37,
BOMB_OMB = 2 ** 38,
MR_SATURN = 2 ** 39,
}

export interface PreFrameUpdateType {
frame: number | null;
playerIndex: number | null;
Expand Down
7 changes: 7 additions & 0 deletions src/utils/exists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Based on https://github.com/wilsonzlin/edgesearch/blob/d03816dd4b18d3d2eb6d08cb1ae14f96f046141d/demo/wiki/client/src/util/util.ts

// Ensures value is not null or undefined.
// != does no type validation so we don't need to explcitly check for undefined.
export function exists<T>(value: T | null | undefined): value is T {
return value != null;
}
38 changes: 38 additions & 0 deletions src/utils/gameTimer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { TimerType } from "../types";
import { frameToGameTimer } from "./gameTimer";

describe("when calculating the in-game timer", () => {
it("should return unknown if no starting timer is provided", () => {
const gameTimer = frameToGameTimer(1234, {
timerType: TimerType.DECREASING,
startingTimerSeconds: null,
});
expect(gameTimer).toBe("Unknown");
});

it("should support increasing timers", () => {
const gameTimer = frameToGameTimer(2014, {
timerType: TimerType.INCREASING,
startingTimerSeconds: 0,
});
expect(gameTimer).toBe("00:33.57");
});

it("should support decreasing timers", () => {
const gameTimer = frameToGameTimer(4095, {
timerType: TimerType.DECREASING,
startingTimerSeconds: 180,
});

expect(gameTimer).toBe("01:51.76");
});

it("should support when the exact limit is hit", () => {
const gameTimer = frameToGameTimer(10800, {
timerType: TimerType.DECREASING,
startingTimerSeconds: 180,
});

expect(gameTimer).toBe("00:00.00");
});
});
29 changes: 29 additions & 0 deletions src/utils/gameTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { format } from "date-fns";

import type { GameStartType } from "../types";
import { TimerType } from "../types";
import { exists } from "./exists";

export function frameToGameTimer(
frame: number,
options: Pick<GameStartType, "timerType" | "startingTimerSeconds">,
): string {
const { timerType, startingTimerSeconds } = options;

if (timerType === TimerType.DECREASING) {
if (!exists(startingTimerSeconds)) {
return "Unknown";
}
const centiseconds = Math.ceil((((60 - (frame % 60)) % 60) * 99) / 59);
const date = new Date(0, 0, 0, 0, 0, startingTimerSeconds - frame / 60, centiseconds * 10);
return format(date, "mm:ss.SS");
}

if (timerType === TimerType.INCREASING) {
const centiseconds = Math.floor(((frame % 60) * 99) / 59);
const date = new Date(0, 0, 0, 0, 0, frame / 60, centiseconds * 10);
return format(date, "mm:ss.SS");
}

return "Infinite";
}
27 changes: 27 additions & 0 deletions src/utils/slpParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ import type {
PreFrameUpdateType,
RollbackFrames,
} from "../types";
import { EnabledItemType, ItemSpawnType } from "../types";
import { Command, Frames, GameMode } from "../types";
import { exists } from "./exists";
import { RollbackCounter } from "./rollbackCounter";

// There are 5 bytes of item bitfields that can be enabled
const ITEM_SETTINGS_BIT_COUNT = 40;
export const MAX_ROLLBACK_FRAMES = 7;

export enum SlpParserEvent {
Expand Down Expand Up @@ -125,6 +129,29 @@ export class SlpParser extends EventEmitter {
return this.settingsComplete ? this.settings : null;
}

public getItems(): EnabledItemType[] | null {
if (this.settings?.itemSpawnBehavior === ItemSpawnType.OFF) {
return null;
}

const itemBitfield = this.settings?.enabledItems;
if (!exists(itemBitfield)) {
return null;
}

const enabledItems: EnabledItemType[] = [];

// Ideally we would be able to do this with bitshifting instead, but javascript
// truncates numbers after 32 bits when doing bitwise operations
for (let i = 0; i < ITEM_SETTINGS_BIT_COUNT; i++) {
if (Math.floor(itemBitfield / 2 ** i) & 1) {
enabledItems.push(2 ** i);
}
}

return enabledItems;
}

public getGameEnd(): GameEndType | null {
return this.gameEnd;
}
Expand Down
Loading

0 comments on commit 5a46672

Please sign in to comment.