slp-enforcer
Version:
Finds violations of the Melee Controller Ruleset by inspecting SLP files
337 lines (336 loc) • 11.6 kB
JavaScript
;
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;