Skip to content

Commit

Permalink
Merge d5ddd09 into cd81779
Browse files Browse the repository at this point in the history
  • Loading branch information
JLaferri authored Mar 10, 2023
2 parents cd81779 + d5ddd09 commit f04dc6f
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 11 deletions.
Binary file added slp/placementsTest/incorrect-winner-timeout.slp
Binary file not shown.
43 changes: 34 additions & 9 deletions src/SlippiGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,23 @@ import type {
GeckoListType,
MetadataType,
PlacementType,
PostFrameUpdateType,
RollbackFrames,
} from "./types";
import { GameMode } from "./types";
import { GameMode, GameEndMethod } from "./types";
import { getWinners } from "./utils/getWinners";
import { extractDistanceInfoFromFrame } from "./utils/homeRunDistance";
import { SlpParser, SlpParserEvent } from "./utils/slpParser";
import type { SlpReadInput } from "./utils/slpReader";
import { closeSlpFile, getGameEnd, getMetadata, iterateEvents, openSlpFile, SlpInputSource } from "./utils/slpReader";
import type { SlpFileType, SlpReadInput } from "./utils/slpReader";
import {
closeSlpFile,
getGameEnd,
getMetadata,
iterateEvents,
openSlpFile,
SlpInputSource,
extractFinalPostFrameUpdates,
} from "./utils/slpReader";

/**
* Slippi Game class that wraps a file
Expand Down Expand Up @@ -87,11 +96,11 @@ export class SlippiGame {
});
}

private _process(shouldStop: EventCallbackFunc = () => false): void {
private _process(shouldStop: EventCallbackFunc = () => false, file?: SlpFileType): void {
if (this.parser.getGameEnd() !== null) {
return;
}
const slpfile = openSlpFile(this.input);
const slpfile = file ?? openSlpFile(this.input);
// Generate settings from iterating through file
this.readPosition = iterateEvents(
slpfile,
Expand All @@ -106,7 +115,9 @@ export class SlippiGame {
},
this.readPosition,
);
closeSlpFile(slpfile);
if (!file) {
closeSlpFile(slpfile);
}
}

/**
Expand Down Expand Up @@ -260,11 +271,25 @@ export class SlippiGame {
}

public getWinners(): PlacementType[] {
const gameEnd = this.getGameEnd({ skipProcessing: true });
const settings = this.getSettings();
// Read game end block directly
const slpfile = openSlpFile(this.input);
const gameEnd = getGameEnd(slpfile);
this._process(() => this.parser.getSettings() !== null, slpfile);
const settings = this.parser.getSettings();
if (!gameEnd || !settings) {
// Technically using the final post frame updates, it should be possible to compute winners for
// replays without a gameEnd message. But I'll leave this here anyway
return [];
}
return getWinners(gameEnd, settings);

// If we went to time, let's fetch the post frame updates to compute the winner
let finalPostFrameUpdates: PostFrameUpdateType[] = [];
if (gameEnd.gameEndMethod === GameEndMethod.TIME) {
console.log("Hello?");
finalPostFrameUpdates = extractFinalPostFrameUpdates(slpfile);
}

closeSlpFile(slpfile);
return getWinners(gameEnd, settings, finalPostFrameUpdates);
}
}
29 changes: 28 additions & 1 deletion src/utils/getWinners.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { GameEndType, GameStartType, PlacementType } from "../types";
import type { GameEndType, GameStartType, PlacementType, PostFrameUpdateType } from "../types";
import { GameEndMethod } from "../types";
import { exists } from "./exists";

export function getWinners(
gameEnd: GameEndType,
settings: Pick<GameStartType, "players" | "isTeams">,
finalPostFrameUpdates: PostFrameUpdateType[],
): PlacementType[] {
const { placements, gameEndMethod, lrasInitiatorIndex } = gameEnd;
const { players, isTeams } = settings;
Expand All @@ -26,6 +27,32 @@ export function getWinners(
return [];
}

if (gameEndMethod === GameEndMethod.TIME && players.length === 2) {
const nonFollowerUpdates = finalPostFrameUpdates.filter((pfu) => !pfu.isFollower);
if (nonFollowerUpdates.length !== players.length) {
return [];
}

const p1 = nonFollowerUpdates[0]!;
const p2 = nonFollowerUpdates[1]!;
if (p1.stocksRemaining! > p2.stocksRemaining!) {
return [{ playerIndex: p1.playerIndex!, position: 0 }];
} else if (p2.stocksRemaining! > p1.stocksRemaining!) {
return [{ playerIndex: p2.playerIndex!, position: 0 }];
}

const p1Health = Math.trunc(p1.percent!);
const p2Health = Math.trunc(p2.percent!);
if (p1Health < p2Health) {
return [{ playerIndex: p1.playerIndex!, position: 0 }];
} else if (p2Health < p1Health) {
return [{ playerIndex: p2.playerIndex!, position: 0 }];
}

// If stocks and percents were tied, no winner
return [];
}

const firstPosition = placements.find((placement) => placement.position === 0);
if (!firstPosition) {
return [];
Expand Down
49 changes: 48 additions & 1 deletion src/utils/slpReader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { decode } from "@shelacek/ubjson";
import fs from "fs";
import iconv from "iconv-lite";
import { mapValues } from "lodash";
import { mapValues, isUndefined, isNull } from "lodash";

import type {
EventCallbackFunc,
Expand All @@ -12,6 +12,7 @@ import type {
MetadataType,
PlacementType,
PlayerType,
PostFrameUpdateType,
SelfInducedSpeedsType,
} from "../types";
import { Command } from "../types";
Expand Down Expand Up @@ -684,3 +685,49 @@ export function getGameEnd(slpFile: SlpFileType): GameEndType | null {

return gameEndMessage as GameEndType;
}

export function extractFinalPostFrameUpdates(slpFile: SlpFileType): PostFrameUpdateType[] {
const { ref, rawDataPosition, rawDataLength, messageSizes } = slpFile;

// The following should exist on all replay versions
const postFramePayloadSize = messageSizes[Command.POST_FRAME_UPDATE];
const gameEndPayloadSize = messageSizes[Command.GAME_END];
const frameBookendPayloadSize = messageSizes[Command.FRAME_BOOKEND];

// Technically this should not be possible
if (isUndefined(postFramePayloadSize)) {
return [];
}

const gameEndSize = gameEndPayloadSize ? gameEndPayloadSize + 1 : 0;
const postFrameSize = postFramePayloadSize + 1;
const frameBookendSize = frameBookendPayloadSize ? frameBookendPayloadSize + 1 : 0;

let frameNum = null;
let postFramePosition = rawDataPosition + rawDataLength - gameEndSize - frameBookendSize - postFrameSize;
const postFrameUpdates: PostFrameUpdateType[] = [];
do {
const buffer = new Uint8Array(postFrameSize);
readRef(ref, buffer, 0, buffer.length, postFramePosition);
if (buffer[0] !== Command.POST_FRAME_UPDATE) {
break;
}

const postFrameMessage = parseMessage(Command.POST_FRAME_UPDATE, buffer) as PostFrameUpdateType | null;
if (!postFrameMessage) {
break;
}

if (isNull(frameNum)) {
frameNum = postFrameMessage.frame;
} else if (frameNum !== postFrameMessage.frame) {
// If post frame message is found but the frame doesn't match, it's not part of the final frame
break;
}

postFrameUpdates.unshift(postFrameMessage);
postFramePosition -= postFrameSize;
} while (postFramePosition >= rawDataPosition);

return postFrameUpdates;
}

0 comments on commit f04dc6f

Please sign in to comment.