Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions libenforcer-wasm/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub fn is_box_controller(coordinates: &[Coord]) -> bool {
const THREE_MINUTES: usize = 10800; // frames
const MIN_ANALOG_SMALL_OFF_AXIS_RATIO: f64 = 0.02;
const MIN_ANALOG_SMALL_OFF_AXIS_UNIQUE: usize = 32;
const MIN_ANALOG_CARDINAL_UNIQUE: usize = 100;
const MIN_ANALOG_RIM_UNIQUE: usize = 40;

let (small_off_axis_count, small_off_axis_unique_count) =
count_small_off_axis_coords(coordinates);
Expand All @@ -35,6 +37,15 @@ pub fn is_box_controller(coordinates: &[Coord]) -> bool {
}

let rim_count = count_rim_coords(coordinates);

if count_strict_rim_coords(coordinates) >= MIN_ANALOG_RIM_UNIQUE {
let (_, cardinal_analog_unique_count) = count_cardinal_analog_coords(coordinates);

if cardinal_analog_unique_count >= MIN_ANALOG_CARDINAL_UNIQUE {
return false;
}
}

let mut rim_proportion = rim_count as f64 / RIM_COORD_MAX as f64;

// Boost proportion for shorter games to avoid false positives
Expand Down Expand Up @@ -68,6 +79,43 @@ fn count_small_off_axis_coords(coords: &[Coord]) -> (usize, usize) {
(count, unique_coords.len())
}

/// Count unique coordinates that are actually on the rim, without fuzz tolerance.
/// The box fallback uses tolerant rim detection; this stricter version avoids
/// treating normal one-step fuzz around full cardinals as analog rim coverage.
fn count_strict_rim_coords(coords: &[Coord]) -> usize {
let mut rim_coords = HashSet::new();

for coord in coords {
let distance = (coord.x.powi(2) + coord.y.powi(2)).sqrt();

if distance >= 1.0 {
rim_coords.insert((coord.x.to_bits(), coord.y.to_bits()));
}
}

rim_coords.len()
}

/// Count axis-aligned coordinates with analog magnitudes below full cardinal.
/// This catches analog-button traces that have little off-axis noise, while the
/// rim-coordinate requirement keeps ordinary fuzzed box inputs classified as box.
fn count_cardinal_analog_coords(coords: &[Coord]) -> (usize, usize) {
let mut count = 0;
let mut unique_coords = HashSet::new();

for coord in coords {
let min_abs_axis = coord.x.abs().min(coord.y.abs());
let max_abs_axis = coord.x.abs().max(coord.y.abs());

if min_abs_axis == 0.0 && max_abs_axis > 0.0 && max_abs_axis < 1.0 {
count += 1;
unique_coords.insert((coord.x.to_bits(), coord.y.to_bits()));
}
}

(count, unique_coords.len())
}

/// Count unique coordinates on the rim of the joystick
/// A coordinate is on the rim if its distance from center is >= 1.0
fn count_rim_coords(coords: &[Coord]) -> usize {
Expand Down
101 changes: 101 additions & 0 deletions libenforcer-wasm/tests/utils_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,107 @@ fn test_analyze_player_issue15_orca_replays_as_analog() {
}
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_is_box_inputs_additional_orca_false_positive_replays() {
let cases = [
(
"legal/analog/orca/travel_time_20251228_230022.slp",
0,
"travel time 20251228 230022",
),
(
"legal/analog/orca/travel_time_20251228_230302.slp",
0,
"travel time 20251228 230302",
),
(
"legal/analog/orca/travel_time_20260227_104159.slp",
0,
"travel time 20260227 104159",
),
(
"legal/analog/orca/illegal_sdi_20260418_224946.slp",
1,
"illegal SDI 20260418 224946",
),
];

for (path, player_index, label) in cases {
let data = read_slp_file(path);
let game = read_slippi(&mut Cursor::new(&data), None).unwrap();
let player_data = parser::extract_player_data(&game, player_index).unwrap();

assert_eq!(
utils::is_box_controller(&player_data.main_coords),
false,
"{} Orca player should not be detected as box",
label
);
}
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_analyze_player_additional_orca_false_positive_replays_as_analog() {
let cases = [
(
"legal/analog/orca/travel_time_20251228_230022.slp",
0,
"travel time 20251228 230022",
),
(
"legal/analog/orca/travel_time_20251228_230302.slp",
0,
"travel time 20251228 230302",
),
(
"legal/analog/orca/travel_time_20260227_104159.slp",
0,
"travel time 20260227 104159",
),
(
"legal/analog/orca/illegal_sdi_20260418_224946.slp",
1,
"illegal SDI 20260418 224946",
),
];

for (path, player_index, label) in cases {
let data = read_slp_file(path);
let game = read_slippi(&mut Cursor::new(&data), None).unwrap();
let player_data = parser::extract_player_data(&game, player_index).unwrap();
let analysis = checks::analyze_player(&player_data);

assert_eq!(
analysis.controller_type,
ControllerType::Analog,
"{} Orca player should be analyzed as analog",
label
);
assert!(
analysis.is_legal,
"{} Orca player should pass aggregate legality",
label
);
assert!(
analysis.travel_time.is_none(),
"{} Orca player should skip box-only travel time checks",
label
);
assert!(
analysis.sdi.is_none(),
"{} Orca player should skip box-only SDI checks",
label
);
assert!(
analysis.input_fuzzing.is_none(),
"{} Orca player should skip box-only input fuzzing checks",
label
);
}
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_is_box_inputs_xbox_controller_a() {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading