use std::{cmp, error, fmt, ops::ControlFlow};
use rosu_map::{section::general::GameMode, util::Pos};
use crate::{
model::hit_object::{HitObject, HitObjectKind},
util::hint::unlikely,
};
use super::Beatmap;
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub enum TooSuspicious {
Density,
Length,
ObjectCount,
RedFlag,
SliderPositions,
SliderRepeats,
}
impl TooSuspicious {
pub(crate) fn new(map: &Beatmap) -> Option<Self> {
#[inline]
const fn too_long(hit_objects: &[HitObject]) -> bool {
const DAY_MS: u32 = 60 * 60 * 24 * 1000;
if unlikely(hit_objects.len() < 2) {
return false;
}
let [first, .., last] = hit_objects else {
unreachable!()
};
(last.start_time - first.start_time) > DAY_MS as f64
}
#[inline]
const fn too_many_objects(map: &Beatmap) -> bool {
const THRESHOLD: usize = 500_000;
const THRESHOLD_TAIKO: usize = 30_000;
match map.mode {
GameMode::Taiko => map.hit_objects.len() > THRESHOLD_TAIKO,
_ => map.hit_objects.len() > THRESHOLD,
}
}
if unlikely(too_many_objects(map)) {
return Some(Self::ObjectCount);
} else if unlikely(too_long(&map.hit_objects)) {
return Some(Self::Length);
}
match map.mode {
GameMode::Osu => Self::check_osu(map),
GameMode::Taiko => Self::check_taiko(map),
GameMode::Catch => Self::check_catch(map),
GameMode::Mania => Self::check_mania(map),
}
}
fn check_osu(map: &Beatmap) -> Option<Self> {
let mut state = SliderState::new();
let per_1s = THRESHOLD_1S;
let per_10s = THRESHOLD_10S;
for (i, h) in map.hit_objects.iter().enumerate() {
if unlikely(Self::too_dense(&map.hit_objects, i, per_1s, per_10s)) {
return Some(Self::Density);
} else if unlikely(Self::suspicious_slider(h, &mut state).is_break()) {
return Some(Self::RedFlag);
}
}
state.eval()
}
fn check_taiko(map: &Beatmap) -> Option<Self> {
let per_1s = THRESHOLD_1S * 2;
let per_10s = THRESHOLD_10S * 2;
for i in 0..map.hit_objects.len() {
if unlikely(Self::too_dense(&map.hit_objects, i, per_1s, per_10s)) {
return Some(Self::Density);
}
}
None
}
fn check_catch(map: &Beatmap) -> Option<Self> {
let mut state = SliderState::new();
for h in map.hit_objects.iter() {
if unlikely(Self::suspicious_slider(h, &mut state).is_break()) {
return Some(Self::RedFlag);
}
}
state.eval()
}
fn check_mania(map: &Beatmap) -> Option<Self> {
let keys_per_hand = cmp::max(1, map.cs as usize / 2);
let per_1s = THRESHOLD_1S * keys_per_hand;
let per_10s = THRESHOLD_10S * keys_per_hand;
for i in 0..map.hit_objects.len() {
if unlikely(Self::too_dense(&map.hit_objects, i, per_1s, per_10s)) {
return Some(Self::Density);
}
}
None
}
#[inline]
fn too_dense(hit_objects: &[HitObject], i: usize, per_1s: usize, per_10s: usize) -> bool {
(hit_objects.len() > i + per_1s
&& hit_objects[i + per_1s].start_time - hit_objects[i].start_time < 1000.0)
|| (hit_objects.len() > i + per_10s
&& hit_objects[i + per_10s].start_time - hit_objects[i].start_time < 10_000.0)
}
#[inline]
const fn suspicious_slider(h: &HitObject, state: &mut SliderState) -> ControlFlow<()> {
#[inline]
const fn check_pos(pos: Pos) -> bool {
const THRESHOLD: f32 = 10_000.0;
f32::abs(pos.x) > THRESHOLD || f32::abs(pos.y) > THRESHOLD
}
#[inline]
const fn check_repeats(repeats: usize) -> bool {
const THRESHOLD: usize = 1000;
repeats > THRESHOLD
}
if let HitObjectKind::Slider(ref slider) = h.kind {
if unlikely(check_repeats(slider.repeats)) {
if unlikely(check_pos(h.pos)) {
return ControlFlow::Break(());
}
state.repeats_beyond_threshold += 1;
} else if unlikely(check_pos(h.pos)) {
state.pos_beyond_threshold += 1;
}
}
ControlFlow::Continue(())
}
}
const THRESHOLD_1S: usize = 200;
const THRESHOLD_10S: usize = 500;
struct SliderState {
repeats_beyond_threshold: usize,
pos_beyond_threshold: usize,
}
impl SliderState {
const fn new() -> Self {
Self {
repeats_beyond_threshold: 0,
pos_beyond_threshold: 0,
}
}
const fn eval(self) -> Option<TooSuspicious> {
const CUTOFF: usize = 128;
if unlikely(self.pos_beyond_threshold > CUTOFF) {
Some(TooSuspicious::SliderPositions)
} else if unlikely(self.repeats_beyond_threshold > CUTOFF) {
Some(TooSuspicious::SliderRepeats)
} else {
None
}
}
}
impl error::Error for TooSuspicious {}
impl fmt::Display for TooSuspicious {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"the map seems too suspicious for further calculation (reason={self:?})",
)
}
}