slp-enforcer
Version:
Finds violations of the Melee Controller Ruleset by inspecting SLP files
217 lines (216 loc) • 8.55 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const stats_1 = require("./stats");
const stats_2 = require("./stats");
const types_1 = require("./types");
const getWinners_1 = require("./utils/getWinners");
const homeRunDistance_1 = require("./utils/homeRunDistance");
const slpParser_1 = require("./utils/slpParser");
const slpReader_1 = require("./utils/slpReader");
/**
* Slippi Game class that wraps a file
*/
class SlippiGame {
constructor(input, opts) {
this.metadata = null;
this.finalStats = null;
this.readPosition = null;
this.actionsComputer = new stats_2.ActionsComputer();
this.conversionComputer = new stats_2.ConversionComputer();
this.comboComputer = new stats_2.ComboComputer();
this.stockComputer = new stats_2.StockComputer();
this.inputComputer = new stats_2.InputComputer();
this.targetBreakComputer = new stats_1.TargetBreakComputer();
if (typeof input === "string") {
this.input = {
source: slpReader_1.SlpInputSource.FILE,
filePath: input,
};
}
else if (input instanceof ArrayBuffer) {
this.input = {
source: slpReader_1.SlpInputSource.BUFFER,
buffer: input,
};
}
else {
throw new Error("Cannot create SlippiGame with input of that type: " + typeof (input));
}
// Set up stats calculation
this.statsComputer = new stats_2.Stats(opts);
this.statsComputer.register(this.actionsComputer, this.comboComputer, this.conversionComputer, this.inputComputer, this.stockComputer, this.targetBreakComputer);
this.parser = new slpParser_1.SlpParser();
this.parser.on(slpParser_1.SlpParserEvent.SETTINGS, (settings) => {
this.statsComputer.setup(settings);
});
// Use finalized frames for stats computation
this.parser.on(slpParser_1.SlpParserEvent.FINALIZED_FRAME, (frame) => {
this.statsComputer.addFrame(frame);
});
}
_process(shouldStop = () => false, file) {
if (this.parser.getGameEnd() !== null) {
return;
}
const slpfile = file !== null && file !== void 0 ? file : slpReader_1.openSlpFile(this.input);
// Generate settings from iterating through file
this.readPosition = slpReader_1.iterateEvents(slpfile, (command, payload) => {
if (!payload) {
// If payload is falsy, keep iterating. The parser probably just doesn't know
// about this command yet
return false;
}
this.parser.handleCommand(command, payload);
return shouldStop(command, payload);
}, this.readPosition);
if (!file) {
slpReader_1.closeSlpFile(slpfile);
}
}
/**
* Gets the game settings, these are the settings that describe the starting state of
* the game such as characters, stage, etc.
*/
getSettings() {
// Settings is only complete after post-frame update
this._process(() => this.parser.getSettings() !== null);
return this.parser.getSettings();
}
getItems() {
this._process();
return this.parser.getItems();
}
getLatestFrame() {
this._process();
return this.parser.getLatestFrame();
}
getGameEnd(options = {}) {
if (options === null || options === void 0 ? void 0 : options.skipProcessing) {
// Read game end block directly
const slpfile = slpReader_1.openSlpFile(this.input);
const gameEnd = slpReader_1.getGameEnd(slpfile);
slpReader_1.closeSlpFile(slpfile);
return gameEnd;
}
this._process();
return this.parser.getGameEnd();
}
getFrames() {
this._process();
return this.parser.getFrames();
}
getRollbackFrames() {
this._process();
return this.parser.getRollbackFrames();
}
getGeckoList() {
this._process(() => this.parser.getGeckoList() !== null);
return this.parser.getGeckoList();
}
getStats() {
if (this.finalStats) {
return this.finalStats;
}
this._process();
const settings = this.parser.getSettings();
if (!settings) {
return null;
}
// Finish processing if we're not up to date
this.statsComputer.process();
const inputs = this.inputComputer.fetch();
const stocks = this.stockComputer.fetch();
const conversions = this.conversionComputer.fetch();
const playableFrameCount = this.parser.getPlayableFrameCount();
const overall = stats_2.generateOverallStats({ settings, inputs, conversions, playableFrameCount });
const gameEnd = this.parser.getGameEnd();
const gameComplete = gameEnd !== null;
const stats = {
lastFrame: this.parser.getLatestFrameNumber(),
playableFrameCount,
stocks: stocks,
conversions: conversions,
combos: this.comboComputer.fetch(),
actionCounts: this.actionsComputer.fetch(),
overall: overall,
gameComplete,
};
if (gameComplete) {
// If the game is complete, store a cached version of stats because it should not
// change anymore. Ideally the statsCompuer.process and fetch functions would simply do no
// work in this case instead but currently the conversions fetch function,
// generateOverallStats, and maybe more are doing work on every call.
this.finalStats = stats;
}
return stats;
}
getStadiumStats() {
this._process();
const settings = this.parser.getSettings();
if (!settings) {
return null;
}
const latestFrame = this.parser.getLatestFrame();
const players = latestFrame === null || latestFrame === void 0 ? void 0 : latestFrame.players;
if (!players) {
return null;
}
this.statsComputer.process();
switch (settings.gameMode) {
case types_1.GameMode.TARGET_TEST:
return {
type: "target-test",
targetBreaks: this.targetBreakComputer.fetch(),
};
case types_1.GameMode.HOME_RUN_CONTEST:
const distanceInfo = homeRunDistance_1.extractDistanceInfoFromFrame(settings, latestFrame);
if (!distanceInfo) {
return null;
}
return {
type: "home-run-contest",
distance: distanceInfo.distance,
units: distanceInfo.units,
};
default:
return null;
}
}
getMetadata() {
if (this.metadata) {
return this.metadata;
}
const slpfile = slpReader_1.openSlpFile(this.input);
this.metadata = slpReader_1.getMetadata(slpfile);
slpReader_1.closeSlpFile(slpfile);
return this.metadata;
}
getFilePath() {
var _a;
if (this.input.source !== slpReader_1.SlpInputSource.FILE) {
return null;
}
return (_a = this.input.filePath) !== null && _a !== void 0 ? _a : null;
}
getWinners() {
// Read game end block directly
const slpfile = slpReader_1.openSlpFile(this.input);
const gameEnd = slpReader_1.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
slpReader_1.closeSlpFile(slpfile);
return [];
}
// If we went to time, let's fetch the post frame updates to compute the winner
let finalPostFrameUpdates = [];
if (gameEnd.gameEndMethod === types_1.GameEndMethod.TIME) {
finalPostFrameUpdates = slpReader_1.extractFinalPostFrameUpdates(slpfile);
}
slpReader_1.closeSlpFile(slpfile);
return getWinners_1.getWinners(gameEnd, settings, finalPostFrameUpdates);
}
}
exports.SlippiGame = SlippiGame;