UNPKG

slp-enforcer

Version:

Finds violations of the Melee Controller Ruleset by inspecting SLP files

337 lines (336 loc) 11.6 kB
"use strict"; function __export(m) { for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; } var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const semver = __importStar(require("semver")); const disallowed_analog_values_1 = require("./disallowed_analog_values"); const travel_time_1 = require("./travel_time"); const uptilt_rounding_1 = require("./uptilt_rounding"); const crouch_uptilt_1 = require("./crouch_uptilt"); const sdi_1 = require("./sdi"); const goomwave_1 = require("./goomwave"); const control_stick_viz_1 = require("./control_stick_viz"); var disallowed_analog_values_2 = require("./disallowed_analog_values"); exports.hasDisallowedCStickCoords = disallowed_analog_values_2.hasDisallowedCStickCoords; exports.getCStickViolations = disallowed_analog_values_2.getCStickViolations; var travel_time_2 = require("./travel_time"); exports.averageTravelCoordHitRate = travel_time_2.averageTravelCoordHitRate; exports.hasIllegalTravelTime = travel_time_2.hasIllegalTravelTime; var uptilt_rounding_2 = require("./uptilt_rounding"); exports.hasIllegalUptiltRounding = uptilt_rounding_2.hasIllegalUptiltRounding; var sdi_2 = require("./sdi"); exports.hasIllegalSDI = sdi_2.hasIllegalSDI; var goomwave_2 = require("./goomwave"); exports.isGoomwave = goomwave_2.isGoomwave; var crouch_uptilt_2 = require("./crouch_uptilt"); exports.hasIllegalCrouchUptilt = crouch_uptilt_2.hasIllegalCrouchUptilt; var control_stick_viz_2 = require("./control_stick_viz"); exports.controlStickViz = control_stick_viz_2.controlStickViz; __export(require("./slippi")); // Holds the overall results of a check against a single player on a single game class CheckResult { constructor(result, violations = []) { this.result = result; this.violations = violations; } } exports.CheckResult = CheckResult; // Represents a single violation of a rule class Violation { constructor(metric, reason, evidence = []) { this.metric = metric; this.reason = reason; this.evidence = evidence; } } exports.Violation = Violation; // Provide an array of strings that describe the available Checks function ListChecks() { var checks = []; checks.push({ name: "Box Travel Time", checkFunction: travel_time_1.hasIllegalTravelTime }); checks.push({ name: "Disallowed Analog C-Stick Values", checkFunction: disallowed_analog_values_1.hasDisallowedCStickCoords }); checks.push({ name: "Uptilt Rounding", checkFunction: uptilt_rounding_1.hasIllegalUptiltRounding }); checks.push({ name: "Fast Crouch Uptilt", checkFunction: crouch_uptilt_1.hasIllegalCrouchUptilt }); checks.push({ name: "Illegal SDI", checkFunction: sdi_1.hasIllegalSDI }); checks.push({ name: "GoomWave Clamping", checkFunction: goomwave_1.isGoomwave }); checks.push({ name: "Control Stick Visualization", checkFunction: control_stick_viz_1.controlStickViz }); return checks; } exports.ListChecks = ListChecks; function jsonToCoord(json) { var parsed = JSON.parse(json); return { x: parsed.x, y: parsed.y }; } exports.jsonToCoord = jsonToCoord; function isEqual(one, other) { return (FloatEquals(one.x, other.x) && FloatEquals(one.y, other.y)); } exports.isEqual = isEqual; function FloatEquals(a, b) { if (Math.abs(a - b) < 0.0001) { return true; } return false; } exports.FloatEquals = FloatEquals; function toArrayBuffer(buffer) { const arrayBuffer = new ArrayBuffer(buffer.length); const view = new Uint8Array(arrayBuffer); for (let i = 0; i < buffer.length; ++i) { view[i] = buffer[i]; } return arrayBuffer; } exports.toArrayBuffer = toArrayBuffer; var JoystickRegion; (function (JoystickRegion) { JoystickRegion[JoystickRegion["DZ"] = 0] = "DZ"; JoystickRegion[JoystickRegion["NE"] = 1] = "NE"; JoystickRegion[JoystickRegion["SE"] = 2] = "SE"; JoystickRegion[JoystickRegion["SW"] = 3] = "SW"; JoystickRegion[JoystickRegion["NW"] = 4] = "NW"; JoystickRegion[JoystickRegion["N"] = 5] = "N"; JoystickRegion[JoystickRegion["E"] = 6] = "E"; JoystickRegion[JoystickRegion["S"] = 7] = "S"; JoystickRegion[JoystickRegion["W"] = 8] = "W"; })(JoystickRegion = exports.JoystickRegion || (exports.JoystickRegion = {})); function getJoystickRegion(x, y) { let region = JoystickRegion.DZ; if (x >= 0.2875 && y >= 0.2875) { region = JoystickRegion.NE; } else if (x >= 0.2875 && y <= -0.2875) { region = JoystickRegion.SE; } else if (x <= -0.2875 && y <= -0.2875) { region = JoystickRegion.SW; } else if (x <= -0.2875 && y >= 0.2875) { region = JoystickRegion.NW; } else if (y >= 0.2875) { region = JoystickRegion.N; } else if (x >= 0.2875) { region = JoystickRegion.E; } else if (y <= -0.2875) { region = JoystickRegion.S; } else if (x <= -0.2875) { region = JoystickRegion.W; } return region; } exports.getJoystickRegion = getJoystickRegion; // Uniques a set of coords function getUniqueCoords(coordinates) { const targetsSet = new Set(); for (let coord of coordinates) { targetsSet.add(JSON.stringify(coord)); } // All of this just so we can have a set of an object by value // Get your shit together, JavaScript var targets = []; for (let target of targetsSet) { targets.push(jsonToCoord(target)); } return targets; } exports.getUniqueCoords = getUniqueCoords; // Gets a list of "target" coordinates, defined as having dwelled there // for at least two consecutive frames. // IE: Removing travel time coords function getTargetCoords(coordinates) { const targetsSet = new Set(); var lastCoord = null; for (let coord of coordinates) { if (lastCoord != null) { if (isEqual(lastCoord, coord)) { targetsSet.add(JSON.stringify(coord)); } } lastCoord = coord; } // All of this just so we can have a set of an object by value // Get your shit together, JavaScript var targets = []; for (let target of targetsSet) { targets.push(jsonToCoord(target)); } return targets; } exports.getTargetCoords = getTargetCoords; function processAnalogStick(coord, deadzone) { let magnitudeSquared = (coord.x * coord.x) + (coord.y * coord.y); if (magnitudeSquared < 1e-3) { return { x: 0, y: 0 }; } let magnitude = Math.sqrt(magnitudeSquared); const threshold = 80; let fX = coord.x; let fY = coord.y; if (magnitude > threshold) { let shrinkFactor = threshold / magnitude; if (fX > 0) { fX = Math.floor(fX * shrinkFactor); fY = Math.floor(fY * shrinkFactor); } else { fX = Math.ceil(fX * shrinkFactor); fY = Math.ceil(fY * shrinkFactor); } } // Apply deadzone if applicable if (deadzone) { if (Math.abs(fX) < 23) { fX = 0; } if (Math.abs(fY) < 23) { fY = 0; } } // Round to the nearest integer (pixel) fX = Math.round(fX); fY = Math.round(fY); return { x: Math.floor(fX) / 80, y: Math.floor(fY) / 80 }; } exports.processAnalogStick = processAnalogStick; function getCoordListFromGame(game, playerIndex, isMainStick) { var _a, _b, _c, _d; var frames = game.getFrames(); var coords = []; var frame = -123; while (true) { try { var coord = { x: 0, y: 0 }; var x = 0; if (isMainStick) { x = (_a = frames[frame].players[playerIndex]) === null || _a === void 0 ? void 0 : _a.pre.rawJoystickX; } else { x = (_b = frames[frame].players[playerIndex]) === null || _b === void 0 ? void 0 : _b.pre.cStickX; } if (x !== undefined && x !== null) { coord.x = x; } var y = 0; if (isMainStick) { y = (_c = frames[frame].players[playerIndex]) === null || _c === void 0 ? void 0 : _c.pre.rawJoystickY; } else { y = (_d = frames[frame].players[playerIndex]) === null || _d === void 0 ? void 0 : _d.pre.cStickY; } if (y !== undefined && y !== null) { coord.y = y; } if (isMainStick) { coord = processAnalogStick(coord, false); } coords.push(coord); } catch (err) { break; } frame += 1; } return coords; } exports.getCoordListFromGame = getCoordListFromGame; function isBoxController(coordinates) { let RIM_COORD_MAX = 432; let rimCount = countRimCoords(coordinates); let rimProportion = rimCount / RIM_COORD_MAX; // Let's call 10,800 coordinates a "normal" game length (3 minutes) // then give a boost to shorter games // Or else we risk calling a box controller on a 1 minute game just because // they didn't move around much on the rim yet let THREE_MINUTES = 10800; if (coordinates.length < THREE_MINUTES) { let boost = 1 + ((THREE_MINUTES - coordinates.length) / THREE_MINUTES); rimProportion *= boost; } // If you hit less than 50% of the rim, then it's a box controller if (rimProportion < 0.50) { return true; } return false; } exports.isBoxController = isBoxController; // Count the number of coordinates that are on the rim of the joystick // allowing for being within .0125 of the rim function countRimCoords(coords) { let rimCoords = new Set(); for (let coord of coords) { // If the coord is within one unit of the rim, then it's a rim coord. let distance = Math.sqrt(((Math.abs(coord.x) + 0.0125) ** 2) + ((Math.abs(coord.y) + 0.0125) ** 2)); if (distance >= 1) { rimCoords.add(JSON.stringify(coord)); } } return rimCoords.size; } // Is this a supported SLP replay version? function isSlpMinVersion(game) { return semver.lt(game.getSettings().slpVersion, '3.15.0'); } exports.isSlpMinVersion = isSlpMinVersion; function isHandwarmer(game) { for (let playerIndex in game.getFrames()[0].players) { let coords = getCoordListFromGame(game, parseInt(playerIndex), true); // If the game is less than a minute long, then it's a handwarmer if (coords.length < 3600) { return true; } // If a player went 10 straight seconds in the deadzone, then it's a handwarmer // *while alive let frameIndex = -123; let deadzoneFrames = 0; for (let coord of coords) { if (getJoystickRegion(coord.x, coord.y) === JoystickRegion.DZ) { deadzoneFrames++; } else { deadzoneFrames = 0; } if (!(playerIndex in game.getFrames()[frameIndex].players)) { deadzoneFrames = 0; } if (deadzoneFrames > 600) { return true; } frameIndex++; } } return false; } exports.isHandwarmer = isHandwarmer;