restructured workspace into single crate

This commit is contained in:
MaxOhn
2021-01-15 22:06:51 +01:00
parent ea7ff00c0a
commit 260939a7ec
85 changed files with 777 additions and 29675 deletions
+33
View File
@@ -0,0 +1,33 @@
use super::{closest_rhythm, HitObjectRhythm};
use crate::HitObject;
#[derive(Clone, Debug)]
pub(crate) struct DifficultyObject<'o> {
pub(crate) idx: usize,
pub(crate) base: &'o HitObject,
pub(crate) prev: &'o HitObject,
pub(crate) delta: f32,
pub(crate) rhythm: &'static HitObjectRhythm,
}
impl<'o> DifficultyObject<'o> {
#[inline]
pub(crate) fn new(
idx: usize,
base: &'o HitObject,
prev: &'o HitObject,
prev_prev: &HitObject,
clock_rate: f32,
) -> Self {
let delta = (base.start_time - prev.start_time) / clock_rate;
let rhythm = closest_rhythm(delta, prev, prev_prev, clock_rate);
Self {
idx,
base,
prev,
delta,
rhythm,
}
}
}
+66
View File
@@ -0,0 +1,66 @@
use crate::HitObject;
use std::cmp::Ordering;
lazy_static::lazy_static! {
/// lazy_static required for `f32` division's
/// lack of const-ness as of now.
static ref COMMON_RHYTHMS: Vec<HitObjectRhythm> = vec![
HitObjectRhythm::new(1.0, 1.0, 0.0),
HitObjectRhythm::new(2.0, 1.0, 0.3),
HitObjectRhythm::new(1.0, 2.0, 0.5),
HitObjectRhythm::new(3.0, 1.0, 0.3),
HitObjectRhythm::new(1.0, 3.0, 0.35),
HitObjectRhythm::new(3.0, 2.0, 0.6),
HitObjectRhythm::new(2.0, 3.0, 0.4),
HitObjectRhythm::new(5.0, 4.0, 0.5),
HitObjectRhythm::new(4.0, 5.0, 0.7),
];
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct HitObjectRhythm {
ratio: f32,
pub(crate) difficulty: f32,
}
impl PartialEq for HitObjectRhythm {
#[inline]
fn eq(&self, other: &Self) -> bool {
(self.ratio - other.ratio).abs() < f32::EPSILON
&& (self.difficulty - other.difficulty).abs() < f32::EPSILON
}
}
impl Eq for HitObjectRhythm {}
impl HitObjectRhythm {
#[inline]
fn new(numerator: f32, denominator: f32, difficulty: f32) -> Self {
Self {
ratio: numerator / denominator,
difficulty,
}
}
}
#[inline]
pub(crate) fn closest_rhythm(
delta_time: f32,
last: &HitObject,
last_last: &HitObject,
clock_rate: f32,
) -> &'static HitObjectRhythm {
let prev_len = (last.start_time - last_last.start_time) / clock_rate;
let ratio = delta_time / prev_len;
COMMON_RHYTHMS
.iter()
.min_by(|r1, r2| {
(r1.ratio - ratio)
.abs()
.partial_cmp(&(r2.ratio - ratio).abs())
.unwrap_or(Ordering::Equal)
})
.unwrap()
}
+80
View File
@@ -0,0 +1,80 @@
use std::cmp::Ordering;
use std::ops::Index;
pub(crate) struct LimitedQueue<T> {
queue: Vec<T>,
start: usize,
end: usize,
}
impl<T> LimitedQueue<T> {
/// Panics if `capacity` is zero.
#[inline]
pub(crate) fn new(capacity: usize) -> Self {
Self {
end: capacity - 1,
start: 0,
queue: Vec::with_capacity(capacity),
}
}
#[inline]
pub(crate) fn push(&mut self, elem: T) {
let capacity = self.queue.capacity();
self.end = (self.end + 1) % capacity;
if self.queue.len() == capacity {
self.start = (self.start + 1) % capacity;
self.queue[self.end as usize] = elem;
} else {
self.queue.push(elem);
}
}
#[inline]
pub(crate) fn len(&self) -> usize {
self.queue.len()
}
#[inline]
pub(crate) fn last(&self) -> Option<&T> {
self.queue.get(self.end as usize)
}
#[inline]
pub(crate) fn clear(&mut self) {
self.start = 0;
self.end = self.queue.capacity() - 1;
self.queue.clear();
}
#[inline]
pub(crate) fn full(&self) -> bool {
self.queue.len() == self.queue.capacity()
}
}
impl<T: PartialOrd> LimitedQueue<T> {
pub(crate) fn min(&self) -> Option<&T> {
let mut iter = self.queue.iter();
let first = iter.next()?;
let min = iter.fold(first, |min, next| match min.partial_cmp(next) {
Some(Ordering::Less) => min,
Some(Ordering::Equal) => min,
Some(Ordering::Greater) => next,
None => min,
});
Some(min)
}
}
impl<T> Index<usize> for LimitedQueue<T> {
type Output = T;
#[inline]
fn index(&self, idx: usize) -> &Self::Output {
&self.queue[(self.start + idx) % self.queue.capacity()]
}
}
+179
View File
@@ -0,0 +1,179 @@
mod difficulty_object;
mod hitobject_rhythm;
mod limited_queue;
mod pp;
mod rim;
mod skill;
mod skill_kind;
mod stamina_cheese;
use difficulty_object::DifficultyObject;
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
use limited_queue::LimitedQueue;
pub use pp::*;
use rim::Rim;
use skill::Skill;
use skill_kind::SkillKind;
use stamina_cheese::StaminaCheeseDetector;
use crate::{Beatmap, Mods};
use std::cmp::Ordering;
use std::f32::consts::PI;
const SECTION_LEN: f32 = 400.0;
const COLOR_SKILL_MULTIPLIER: f32 = 0.01;
const RHYTHM_SKILL_MULTIPLIER: f32 = 0.014;
const STAMINA_SKILL_MULTIPLIER: f32 = 0.02;
/// Star calculation for osu!taiko maps
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
if map.hit_objects.len() < 2 {
return 0.0;
}
// True if the object at that index is stamina cheese
let cheese = map.find_cheese();
let mut skills = vec![
Skill::new(SkillKind::color()),
Skill::new(SkillKind::rhythm()),
Skill::new(SkillKind::stamina(true)),
Skill::new(SkillKind::stamina(false)),
];
let clock_rate = mods.speed();
let section_len = SECTION_LEN * clock_rate;
// No strain for first object
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let hit_objects = map
.hit_objects
.iter()
.enumerate()
.skip(2)
.zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter())
.map(|(((idx, base), prev), prev_prev)| {
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
});
for h in hit_objects {
while h.base.start_time > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
}
current_section_end += section_len;
}
for skill in skills.iter_mut() {
skill.process(&h, &cheese);
}
}
for skill in skills.iter_mut() {
skill.save_current_peak();
}
let mut buf = vec![0.0; skills[0].strain_peaks.len()];
let color_rating = skills[0].difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
let rhythm_rating = skills[1].difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
let mut stamina_rating = (skills[2].difficulty_value(&mut buf)
+ skills[3].difficulty_value(&mut buf))
* STAMINA_SKILL_MULTIPLIER;
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
stamina_rating *= stamina_penalty;
let combined_rating = locally_combined_difficulty(&skills, stamina_penalty);
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
rescale(1.4 * separate_rating + 0.5 * combined_rating)
}
#[inline]
fn rescale(stars: f32) -> f32 {
if stars < 0.0 {
stars
} else {
10.43 * (stars / 8.0 + 1.0).ln()
}
}
#[inline]
fn simple_color_penalty(stamina: f32, color: f32) -> f32 {
if color <= 0.0 {
0.79 - 0.25
} else {
0.79 - (stamina / color - 12.0).atan() / PI / 2.0
}
}
fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f32) -> f32 {
let mut peaks = Vec::with_capacity(skills[0].strain_peaks.len());
let iter = skills[0]
.strain_peaks
.iter()
.zip(skills[1].strain_peaks.iter())
.zip(skills[2].strain_peaks.iter())
.zip(skills[3].strain_peaks.iter())
.map(|(((&color, &rhythm), &stamina_right), &stamina_left)| {
norm(
2.0,
color * COLOR_SKILL_MULTIPLIER,
rhythm * RHYTHM_SKILL_MULTIPLIER,
(stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER * stamina_penalty,
)
});
peaks.extend(iter);
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
let mut difficulty = 0.0;
let mut weight = 1.0;
for strain in peaks {
difficulty += strain * weight;
weight *= 0.9;
}
difficulty
}
#[inline]
fn norm(p: f32, a: f32, b: f32, c: f32) -> f32 {
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
#[ignore]
fn taiko_single() {
let file = match File::open("E:/Games/osu!/beatmaps/1097541.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let result = PpCalculator::new(&map).mods(256).calculate();
println!("Stars: {}", result.stars);
println!("PP: {}", result.pp);
}
}
+149
View File
@@ -0,0 +1,149 @@
use crate::{Beatmap, Mods};
pub struct PpResult {
pub pp: f32,
pub stars: f32,
}
pub trait PpProvider {
fn pp(&self) -> PpCalculator;
}
impl PpProvider for Beatmap {
#[inline]
fn pp(&self) -> PpCalculator {
PpCalculator::new(self)
}
}
// TODO: Allow partial plays
pub struct PpCalculator<'m> {
map: &'m Beatmap,
stars: Option<f32>,
mods: u32,
max_combo: usize,
combo: Option<usize>,
acc: f32,
n_misses: usize,
}
impl<'m> PpCalculator<'m> {
#[inline]
pub fn new(map: &'m Beatmap) -> Self {
let max_combo = map.hit_objects.iter().filter(|h| h.is_circle()).count();
Self {
map,
stars: None,
mods: 0,
max_combo,
combo: None,
acc: 100.0,
n_misses: 0,
}
}
#[inline]
pub fn stars(mut self, stars: f32) -> Self {
self.stars.replace(stars);
self
}
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
self
}
#[inline]
pub fn combo(mut self, combo: usize) -> Self {
self.combo.replace(combo);
self
}
#[inline]
pub fn misses(mut self, n_misses: usize) -> Self {
self.n_misses = n_misses;
self
}
/// Set the accuracy between 0.0 and 100.0;
#[inline]
pub fn accuracy(mut self, acc: f32) -> Self {
self.acc = acc / 100.0;
self
}
pub fn calculate(self) -> PpResult {
let stars = self
.stars
.unwrap_or_else(|| super::stars(self.map, self.mods));
let mut multiplier = 1.1;
if self.mods.nf() {
multiplier *= 0.9;
}
if self.mods.hd() {
multiplier *= 1.1;
}
// TODO: Consider HR & co?
let hit_window = difficulty_range_od(self.map.od) as i32 as f32 / self.mods.speed();
let strain_value = self.compute_strain_value(stars);
let acc_value = self.compute_accuracy_value(hit_window);
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
PpResult { stars, pp }
}
fn compute_strain_value(&self, stars: f32) -> f32 {
let exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
let mut strain = exp_base * exp_base / 100_000.0;
// Longer maps are worth more
let len_bonus = 1.0 + 0.1 * (self.max_combo as f32 / 1500.0).min(1.0);
strain *= len_bonus;
// Penalize misses exponentially
strain *= 0.985_f32.powi(self.n_misses as i32);
// HD bonus
if self.mods.hd() {
strain *= 1.025;
}
// FL bonus
if self.mods.fl() {
strain *= 1.05 * len_bonus;
}
// Scale with accuracy
strain * self.acc
}
#[inline]
fn compute_accuracy_value(&self, hit_window: f32) -> f32 {
(150.0 / hit_window).powf(1.1)
* self.acc.powi(15)
* 22.0
* (self.max_combo as f32 / 1500.0).powf(0.3).min(1.15)
}
}
const HITWINDOW_MIN: f32 = 50.0;
const HITWINDOW_AVG: f32 = 35.0;
const HITWINDOW_MAX: f32 = 20.0;
#[inline]
pub(crate) fn difficulty_range_od(ar: f32) -> f32 {
crate::difficulty_range(ar, HITWINDOW_MAX, HITWINDOW_AVG, HITWINDOW_MIN)
}
+19
View File
@@ -0,0 +1,19 @@
use crate::{HitObject, HitSound};
pub(crate) trait Rim {
fn is_rim(&self) -> bool;
}
impl Rim for HitObject {
#[inline]
fn is_rim(&self) -> bool {
self.sound.clap() || self.sound.whistle()
}
}
impl Rim for u8 {
#[inline]
fn is_rim(&self) -> bool {
self.clap() || self.whistle()
}
}
+99
View File
@@ -0,0 +1,99 @@
use super::{DifficultyObject, SkillKind};
use std::cmp::Ordering;
const DECAY_WEIGHT: f32 = 0.9;
pub(crate) struct Skill {
pub current_strain: f32,
current_section_peak: f32,
kind: SkillKind,
pub(crate) strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
impl Skill {
#[inline]
pub(crate) fn new(kind: SkillKind) -> Self {
Self {
current_strain: 1.0,
current_section_peak: 1.0,
kind,
strain_peaks: Vec::with_capacity(128),
prev_time: None,
}
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
if self.prev_time.is_some() {
self.strain_peaks.push(self.current_section_peak);
}
}
#[inline]
pub(crate) fn start_new_section_from(&mut self, time: f32) {
if let Some(prev) = self.prev_time {
self.current_section_peak = self.peak_strain(time - prev);
}
}
#[inline]
pub(crate) fn process(&mut self, current: &DifficultyObject, cheese: &[bool]) {
self.current_strain *= self.strain_decay(current.delta);
self.current_strain +=
self.kind.strain_value_of(&current, cheese) * self.skill_multiplier();
self.current_section_peak = self.current_section_peak.max(self.current_strain);
self.prev_time.replace(current.base.start_time);
}
#[inline]
pub(crate) fn difficulty_value(&self, buf: &mut [f32]) -> f32 {
let mut difficulty = 0.0;
let mut weight = 1.0;
buf.copy_from_slice(&self.strain_peaks);
buf.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in buf.iter() {
difficulty += strain * weight;
weight *= DECAY_WEIGHT;
}
difficulty
}
#[inline]
fn skill_multiplier(&self) -> f32 {
// TODO: Use constants
match self.kind {
SkillKind::Color { .. } => 1.0,
SkillKind::Rhythm { .. } => 10.0,
SkillKind::Stamina { .. } => 1.0,
}
}
#[inline]
fn strain_decay_base(&self) -> f32 {
// TODO: Use constants
match self.kind {
SkillKind::Color { .. } => 0.4,
SkillKind::Rhythm { .. } => 0.0,
SkillKind::Stamina { .. } => 0.4,
}
}
#[inline]
fn peak_strain(&self, delta_time: f32) -> f32 {
self.current_strain * self.strain_decay(delta_time)
}
#[inline]
fn strain_decay(&self, ms: f32) -> f32 {
self.strain_decay_base().powf(ms / 1000.0)
}
}
+278
View File
@@ -0,0 +1,278 @@
use super::{DifficultyObject, HitObjectRhythm, LimitedQueue, Rim};
const RHYTHM_STRAIN_DECAY: f32 = 0.96;
const MOST_RECENT_PATTERNS_TO_COMPARE: usize = 2;
const MONO_HISTORY_MAX_LEN: usize = 5;
const RHYTHM_HISTORY_MAX_LEN: usize = 8;
const STAMINA_HISTORY_MAX_LEN: usize = 2;
pub(crate) enum SkillKind {
Color {
mono_history: LimitedQueue<usize>,
prev_is_rim: Option<bool>,
current_mono_len: usize,
},
Rhythm {
rhythm_history: LimitedQueue<(usize, HitObjectRhythm)>, // (idx, rhythm)
notes_since_rhythm_change: usize,
current_strain: f32,
},
Stamina {
note_pair_duration_history: LimitedQueue<f32>,
hand: u8,
off_hand_object_duration: f32,
},
}
impl SkillKind {
#[inline]
pub(crate) fn color() -> Self {
Self::Color {
mono_history: LimitedQueue::new(MONO_HISTORY_MAX_LEN),
prev_is_rim: None,
current_mono_len: 0,
}
}
#[inline]
pub(crate) fn rhythm() -> Self {
Self::Rhythm {
rhythm_history: LimitedQueue::new(RHYTHM_HISTORY_MAX_LEN),
notes_since_rhythm_change: 0,
current_strain: 0.0,
}
}
#[inline]
pub(crate) fn stamina(right_hand: bool) -> Self {
Self::Stamina {
note_pair_duration_history: LimitedQueue::new(STAMINA_HISTORY_MAX_LEN),
hand: right_hand as u8,
off_hand_object_duration: f32::MAX,
}
}
pub(crate) fn strain_value_of(&mut self, current: &DifficultyObject, cheese: &[bool]) -> f32 {
match self {
Self::Color {
mono_history,
prev_is_rim,
current_mono_len,
} => {
let prev_is_circle = current.prev.is_circle();
let base_is_circle = current.base.is_circle();
let curr_is_rim = current.base.is_rim();
if !(current.delta < 1000.0 && prev_is_circle && base_is_circle) {
mono_history.clear();
*current_mono_len = base_is_circle as usize;
*prev_is_rim = if base_is_circle {
Some(curr_is_rim)
} else {
None
};
return 0.0;
}
let mut strain = 0.0;
if prev_is_rim
.filter(|&is_rim| is_rim != curr_is_rim)
.is_some()
{
strain = if mono_history.len() < 2
|| (*mono_history.last().unwrap() + *current_mono_len) % 2 == 0
{
0.0
} else {
1.0
};
let mut reps_penalty = 1.0;
mono_history.push(*current_mono_len);
let iter = (0..mono_history
.len()
.saturating_sub(MOST_RECENT_PATTERNS_TO_COMPARE))
.rev();
for start in iter {
let different_pattern = (0..MOST_RECENT_PATTERNS_TO_COMPARE).any(|i| {
mono_history[start + i]
!= mono_history
[mono_history.len() + i - MOST_RECENT_PATTERNS_TO_COMPARE]
});
if different_pattern {
continue;
}
let mut notes_since = 0;
for i in start..mono_history.len() {
notes_since += mono_history[i];
}
reps_penalty *= repetition_penalty(notes_since);
break;
}
strain *= reps_penalty;
*current_mono_len = 1;
} else {
*current_mono_len += 1;
}
*prev_is_rim = Some(curr_is_rim);
strain
}
Self::Rhythm {
rhythm_history,
notes_since_rhythm_change,
current_strain,
} => {
let base_is_circle = current.base.is_circle();
if !base_is_circle {
*current_strain = 0.0;
*notes_since_rhythm_change = 0;
return 0.0;
}
*current_strain *= RHYTHM_STRAIN_DECAY;
*notes_since_rhythm_change += 1;
if current.rhythm.difficulty.abs() < f32::EPSILON {
return 0.0;
}
let mut strain = current.rhythm.difficulty;
rhythm_history.push((current.idx, *current.rhythm));
let mut reps_penalty = 1.0;
for most_recent_patterns_to_compare in 2..=RHYTHM_HISTORY_MAX_LEN / 2 {
let iter = (0..rhythm_history
.len()
.saturating_sub(most_recent_patterns_to_compare))
.rev();
for start in iter {
let different_pattern = (0..most_recent_patterns_to_compare).any(|i| {
rhythm_history[start + i].1
!= rhythm_history
[rhythm_history.len() + i - most_recent_patterns_to_compare]
.1
});
if different_pattern {
continue;
}
reps_penalty *= repetition_penalty(current.idx - rhythm_history[start].0);
break;
}
}
let speed_penalty = if current.delta < 80.0 {
1.0
} else if current.delta < 210.0 {
(1.4 - 0.005 * current.delta).max(0.0)
} else {
*current_strain = 0.0;
*notes_since_rhythm_change = 0;
0.0
};
strain *= reps_penalty;
strain *= pattern_len_penalty(*notes_since_rhythm_change);
strain *= speed_penalty;
*notes_since_rhythm_change = 0;
*current_strain += strain;
*current_strain
}
Self::Stamina {
hand,
note_pair_duration_history,
off_hand_object_duration,
} => {
let base_is_circle = current.base.is_circle();
if !base_is_circle {
return 0.0;
}
if current.idx % 2 == *hand as usize {
if current.idx == 1 {
return 1.0;
}
let mut strain = 1.0;
note_pair_duration_history.push(current.delta + *off_hand_object_duration);
let shortest_recent_note = *note_pair_duration_history.min().unwrap();
strain += speed_bonus(shortest_recent_note);
if cheese[current.idx] {
let p = cheese_penalty(current.delta + *off_hand_object_duration);
strain *= p;
}
return strain;
}
*off_hand_object_duration = current.delta;
0.0
}
}
}
}
#[inline]
fn pattern_len_penalty(pattern_len: usize) -> f32 {
let short_pattern_penalty = (0.15 * pattern_len as f32).min(1.0);
let long_pattern_penalty = (2.5 - 0.15 * pattern_len as f32).max(0.0).min(1.0);
short_pattern_penalty.min(long_pattern_penalty)
}
#[inline]
fn cheese_penalty(note_pair_duration: f32) -> f32 {
if note_pair_duration > 125.0 {
1.0
} else if note_pair_duration < 100.0 {
0.6
} else {
0.6 + (note_pair_duration - 100.0) * 0.016
}
}
#[inline]
fn speed_bonus(note_pair_duration: f32) -> f32 {
if note_pair_duration > 200.0 {
return 0.0;
}
let mut bonus = 200.0 - note_pair_duration;
bonus *= bonus;
bonus / 100_000.0
}
#[inline]
fn repetition_penalty(notes_since: usize) -> f32 {
(0.032 * notes_since as f32).min(1.0)
}
+106
View File
@@ -0,0 +1,106 @@
use super::{LimitedQueue, Rim};
use crate::{Beatmap, HitObject};
const ROLL_MIN_REPETITIONS: usize = 12;
const TL_MIN_REPETITIONS: isize = 16;
pub(crate) trait StaminaCheeseDetector {
fn find_cheese(&self) -> Vec<bool>;
fn find_rolls(&self, pattern_len: usize, cheese: &mut [bool]);
fn find_tl_tap(&self, parity: usize, is_rin: bool, cheese: &mut [bool]);
}
impl StaminaCheeseDetector for Beatmap {
// TODO: Optimize
fn find_cheese(&self) -> Vec<bool> {
let mut cheese = vec![false; self.hit_objects.len()];
self.find_rolls(3, &mut cheese);
self.find_rolls(4, &mut cheese);
self.find_tl_tap(0, true, &mut cheese);
self.find_tl_tap(1, true, &mut cheese);
self.find_tl_tap(0, false, &mut cheese);
self.find_tl_tap(1, false, &mut cheese);
cheese
}
fn find_rolls(&self, pattern_len: usize, cheese: &mut [bool]) {
let mut history = LimitedQueue::new(2 * pattern_len);
let mut index_before_last_repeat = -1;
let mut last_mark_end = 0;
for i in 0..self.hit_objects.len() {
history.push(&self.hit_objects[i]);
if !history.full() {
continue;
}
let contains = contains_pattern_repeat(&history, pattern_len);
if !contains {
index_before_last_repeat = (i + 1 - history.len()) as isize;
continue;
}
let repeated_len = (i as isize - index_before_last_repeat) as usize;
if repeated_len < ROLL_MIN_REPETITIONS {
continue;
}
mark_as_cheese(last_mark_end.max(i + 1 - repeated_len), i, cheese);
last_mark_end = i;
}
}
fn find_tl_tap(&self, parity: usize, is_rin: bool, cheese: &mut [bool]) {
let mut tl_len = -2;
let mut last_mark_end = 0;
for i in (parity..self.hit_objects.len()).step_by(2) {
if self.hit_objects[i].is_rim() == is_rin {
tl_len += 2;
} else {
tl_len = -2;
}
if tl_len < TL_MIN_REPETITIONS {
continue;
}
mark_as_cheese(
(i as isize + 1 - tl_len).max(last_mark_end as isize) as usize,
i,
cheese,
);
last_mark_end = i;
}
}
}
#[inline]
fn mark_as_cheese(start: usize, end: usize, cheese: &mut [bool]) {
cheese
.iter_mut()
.take(end + 1)
.skip(start)
.for_each(|b| *b = true);
}
#[inline]
fn contains_pattern_repeat(history: &LimitedQueue<&HitObject>, pattern_len: usize) -> bool {
for j in 0..pattern_len {
if history[j].is_rim() != history[j + pattern_len].is_rim() {
return false;
}
}
true
}