implemented taiko pp update

This commit is contained in:
MaxOhn
2022-10-01 23:05:19 +02:00
parent 3b54827e2c
commit fae3a0359c
22 changed files with 1716 additions and 1075 deletions
@@ -55,6 +55,8 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
PatternType::LOW_PROBABILITY
};
// ! BUG: Since `LegacyDifficultyControlPoint` are not considered while parsing,
// ! this value can be slightly off due to float arithmetics.
let beat_len = timing_point.beat_len / difficulty_point.speed_multiplier;
let span_count = (repeats + 1) as i32;
+9 -5
View File
@@ -38,7 +38,9 @@ impl Beatmap {
let edge_sound_count = edge_sounds.len().max(1);
while j <= obj.start_time + params.duration + params.tick_spacing / 8.0 {
while j
<= obj.start_time + params.duration as f64 + params.tick_spacing / 8.0
{
let h = HitObject {
pos: Default::default(),
start_time: j,
@@ -109,6 +111,8 @@ impl Beatmap {
let timing_point = self.timing_point_at(*start_time);
let difficulty_point = self.difficulty_point_at(*start_time).unwrap_or_default();
// ! BUG: Since `LegacyDifficultyControlPoint` are not considered while parsing,
// ! this value can be slightly off due to float arithmetics.
let mut beat_len = timing_point.beat_len / difficulty_point.speed_multiplier;
let slider_scoring_point_dist =
@@ -116,7 +120,7 @@ impl Beatmap {
// * The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
let taiko_vel = slider_scoring_point_dist * self.tick_rate;
*duration = (dist / taiko_vel * beat_len).floor();
*duration = (dist / taiko_vel * beat_len) as u32;
let osu_vel = taiko_vel * (1000.0_f32 as f64 / beat_len);
@@ -126,7 +130,7 @@ impl Beatmap {
}
// * If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
*tick_spacing = (beat_len / self.tick_rate).min(*duration / spans);
*tick_spacing = (beat_len / self.tick_rate).min(*duration as f64 / spans);
*tick_spacing > 0.0 && dist / osu_vel * 1000.0 < 2.0 * beat_len
}
@@ -134,7 +138,7 @@ impl Beatmap {
struct SliderParams<'c> {
curve: &'c Curve,
duration: f64,
duration: u32,
repeats: usize,
start_time: f64,
tick_spacing: f64,
@@ -146,7 +150,7 @@ impl<'c> SliderParams<'c> {
curve,
repeats,
start_time,
duration: 0.0,
duration: 0,
tick_spacing: 0.0,
}
}
+1 -1
View File
@@ -453,7 +453,7 @@ impl PerformanceAttributes {
Self::Catch(attributes) => DifficultyAttributes::Catch(attributes.difficulty.clone()),
Self::Mania(attributes) => DifficultyAttributes::Mania(attributes.difficulty),
Self::Osu(attributes) => DifficultyAttributes::Osu(attributes.difficulty.clone()),
Self::Taiko(attributes) => DifficultyAttributes::Taiko(attributes.difficulty),
Self::Taiko(attributes) => DifficultyAttributes::Taiko(attributes.difficulty.clone()),
}
}
-20
View File
@@ -1,5 +1,4 @@
use std::{
cmp::Ordering,
iter::{Cycle, Skip, Take},
ops::Index,
slice::Iter,
@@ -71,11 +70,6 @@ impl<T, const N: usize> LimitedQueue<T, N> {
}
}
pub(crate) fn clear(&mut self) {
self.end = N - 1;
self.len = 0;
}
pub(crate) fn full(&self) -> bool {
self.len == N
}
@@ -91,20 +85,6 @@ impl<T, const N: usize> LimitedQueue<T, N> {
pub(crate) type LimitedQueueIter<'a, T> = Take<Skip<Cycle<Iter<'a, T>>>>;
impl<T: PartialOrd, const N: usize> LimitedQueue<T, N> {
pub(crate) fn min(&self) -> Option<&T> {
self.queue
.iter()
.take(self.len)
.reduce(|min, next| match min.partial_cmp(next) {
Some(Ordering::Less) => min,
Some(Ordering::Equal) => min,
Some(Ordering::Greater) => next,
None => min,
})
}
}
impl<T, const N: usize> Index<usize> for LimitedQueue<T, N> {
type Output = T;
@@ -0,0 +1,72 @@
use std::{
cell::RefCell,
fmt::{Debug, Formatter, Result as FmtResult},
rc::{Rc, Weak},
};
use crate::taiko::difficulty_object::TaikoDifficultyObject;
use super::{mono_streak::MonoStreak, repeating_hit_patterns::RepeatingHitPatterns};
pub(crate) struct AlternatingMonoPattern<'o> {
pub(crate) mono_streaks: Vec<Rc<RefCell<MonoStreak<'o>>>>,
pub(crate) parent: Option<Weak<RefCell<RepeatingHitPatterns<'o>>>>,
pub(crate) idx: usize,
}
impl Debug for AlternatingMonoPattern<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
f,
"(idx={}, mono_len={}, has_parent={})",
self.idx,
self.mono_streaks.len(),
self.parent.is_some()
)
}
}
impl<'o> AlternatingMonoPattern<'o> {
pub(crate) fn new() -> Rc<RefCell<Self>> {
let this = Self {
mono_streaks: Vec::new(),
parent: None,
idx: 0,
};
Rc::new(RefCell::new(this))
}
pub(crate) fn first_hit_object(&self) -> Option<Weak<RefCell<TaikoDifficultyObject<'o>>>> {
self.mono_streaks
.first()
.and_then(|streak| streak.borrow().first_hit_object())
}
pub(crate) fn is_repetition_of(&self, other: &Self) -> bool {
self.has_identical_mono_len(other)
&& other.mono_streaks.len() == self.mono_streaks.len()
&& other
.mono_streaks
.first()
.map(|streak| streak.borrow().hit_kind())
== self
.mono_streaks
.first()
.map(|streak| streak.borrow().hit_kind())
}
pub(crate) fn has_identical_mono_len(&self, other: &Self) -> bool {
let other_len = other
.mono_streaks
.first()
.map(|streak| streak.borrow().run_len());
let self_len = self
.mono_streaks
.first()
.map(|streak| streak.borrow().run_len());
other_len == self_len
}
}
+27
View File
@@ -0,0 +1,27 @@
use std::{
cell::RefCell,
rc::{Rc, Weak},
};
pub(crate) use self::{
alternating_mono_pattern::AlternatingMonoPattern, mono_streak::MonoStreak,
preprocessor::ColourDifficultyPreprocessor, repeating_hit_patterns::RepeatingHitPatterns,
};
mod alternating_mono_pattern;
mod mono_streak;
mod preprocessor;
mod repeating_hit_patterns;
#[derive(Clone, Debug, Default)]
pub(crate) struct TaikoDifficultyColour<'o> {
pub(crate) mono_streak: Option<Weak<RefCell<MonoStreak<'o>>>>,
pub(crate) alternating_mono_pattern: Option<Weak<RefCell<AlternatingMonoPattern<'o>>>>,
pub(crate) repeating_hit_patterns: Option<Rc<RefCell<RepeatingHitPatterns<'o>>>>,
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub(crate) enum HitKind {
Centre,
Rim,
}
+58
View File
@@ -0,0 +1,58 @@
use std::{
cell::RefCell,
fmt::{Debug, Formatter, Result as FmtResult},
rc::{Rc, Weak},
};
use crate::taiko::difficulty_object::{MonoIndex, TaikoDifficultyObject};
use super::{alternating_mono_pattern::AlternatingMonoPattern, HitKind};
pub(crate) struct MonoStreak<'o> {
pub(crate) hit_objects: Vec<Weak<RefCell<TaikoDifficultyObject<'o>>>>,
pub(crate) parent: Option<Weak<RefCell<AlternatingMonoPattern<'o>>>>,
pub(crate) idx: usize,
}
impl Debug for MonoStreak<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
f,
"(idx={}, obj_len={}, has_parent={})",
self.idx,
self.hit_objects.len(),
self.parent.is_some()
)
}
}
impl<'o> MonoStreak<'o> {
pub(crate) fn new() -> Rc<RefCell<Self>> {
let this = Self {
hit_objects: Vec::new(),
parent: None,
idx: 0,
};
Rc::new(RefCell::new(this))
}
pub(crate) fn first_hit_object(&self) -> Option<Weak<RefCell<TaikoDifficultyObject<'o>>>> {
self.hit_objects.first().map(Weak::clone)
}
pub(crate) fn hit_kind(&self) -> Option<HitKind> {
self.hit_objects
.first()
.and_then(Weak::upgrade)
.and_then(|obj| match obj.borrow().mono_idx {
MonoIndex::Centre(_) => Some(HitKind::Centre),
MonoIndex::Rim(_) => Some(HitKind::Rim),
MonoIndex::None => None,
})
}
pub(crate) fn run_len(&self) -> usize {
self.hit_objects.len()
}
}
+205
View File
@@ -0,0 +1,205 @@
use std::{
cell::RefCell,
collections::VecDeque,
rc::{Rc, Weak},
};
use crate::taiko::difficulty_object::ObjectLists;
use super::{
alternating_mono_pattern::AlternatingMonoPattern, mono_streak::MonoStreak,
repeating_hit_patterns::RepeatingHitPatterns,
};
pub(crate) struct ColourDifficultyPreprocessor {}
impl<'o> ColourDifficultyPreprocessor {
pub(crate) fn process_and_assign(lists: &mut ObjectLists<'o>) {
// * Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is
// * assigned with the relevant encodings.
for repeating_hit_pattern in Self::encode(lists) {
if let Some(obj) = repeating_hit_pattern
.borrow()
.first_hit_object()
.as_ref()
.and_then(Weak::upgrade)
{
obj.borrow_mut().colour.repeating_hit_patterns =
Some(Rc::clone(&repeating_hit_pattern));
}
// * The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to
// * keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with
// * documentation.
for i in 0..repeating_hit_pattern
.borrow()
.alternating_mono_patterns
.len()
{
let borrowed_repeating_hit_pattern = repeating_hit_pattern.borrow();
let mono_pattern = &borrowed_repeating_hit_pattern.alternating_mono_patterns[i];
{
let mut borrowed = mono_pattern.borrow_mut();
borrowed.parent = Some(Rc::downgrade(&repeating_hit_pattern));
borrowed.idx = i;
}
if let Some(obj) = mono_pattern
.borrow()
.first_hit_object()
.as_ref()
.and_then(Weak::upgrade)
{
obj.borrow_mut().colour.alternating_mono_pattern =
Some(Rc::downgrade(mono_pattern));
}
for j in 0..mono_pattern.borrow().mono_streaks.len() {
let borrowed_mono_pattern = mono_pattern.borrow();
let mono_streak = &borrowed_mono_pattern.mono_streaks[j];
{
let mut borrowed = mono_streak.borrow_mut();
borrowed.parent = Some(Rc::downgrade(mono_pattern));
borrowed.idx = j;
}
if let Some(obj) = mono_streak
.borrow()
.first_hit_object()
.as_ref()
.and_then(Weak::upgrade)
{
obj.borrow_mut().colour.mono_streak = Some(Rc::downgrade(mono_streak));
};
}
}
}
}
fn encode(data: &mut ObjectLists<'o>) -> Vec<Rc<RefCell<RepeatingHitPatterns<'o>>>> {
let mono_streaks = Self::encode_mono_streak(data);
let alternating_mono_patterns = Self::encode_alternating_mono_pattern(mono_streaks);
Self::encode_repeating_hit_pattern(alternating_mono_patterns)
}
fn encode_mono_streak(data: &mut ObjectLists<'o>) -> Vec<Rc<RefCell<MonoStreak<'o>>>> {
let mut mono_streaks = vec![MonoStreak::new()];
let mut curr_mono_streak = mono_streaks.last_mut();
let mut data_iter = data.all.iter();
if let (Some(curr), Some(taiko_obj)) = (&curr_mono_streak, data_iter.next()) {
curr.borrow_mut().hit_objects.push(Rc::downgrade(taiko_obj));
}
for taiko_obj in data_iter {
// * This ignores all non-note objects, which may or may not be the desired behaviour
let prev = data.prev_note(taiko_obj.borrow().idx, 0);
// * If this is the first object in the list or the colour changed, create a new mono streak
let condition = prev.filter(|prev| {
!(taiko_obj.borrow().base.is_hit()
&& prev.borrow().base.is_hit()
&& (taiko_obj.borrow().base.is_rim() != prev.borrow().base.is_rim()))
});
if condition.is_none() {
mono_streaks.push(MonoStreak::new());
curr_mono_streak = mono_streaks.last_mut();
}
// * Add the current object to the encoded payload.
if let Some(ref curr) = curr_mono_streak {
curr.borrow_mut().hit_objects.push(Rc::downgrade(taiko_obj));
}
}
mono_streaks
}
fn encode_alternating_mono_pattern(
data: Vec<Rc<RefCell<MonoStreak<'o>>>>,
) -> VecDeque<Rc<RefCell<AlternatingMonoPattern<'o>>>> {
let mut mono_patterns = VecDeque::new();
mono_patterns.push_back(AlternatingMonoPattern::new());
let mut curr_mono_pattern = mono_patterns.back_mut();
if let (Some(curr), Some(mono)) = (&curr_mono_pattern, data.first()) {
curr.borrow_mut().mono_streaks.push(Rc::clone(mono));
}
for (prev, curr) in data.iter().zip(data.iter().skip(1)) {
// * Start a new AlternatingMonoPattern if the previous MonoStreak has a different mono length,
// * or if this is the first MonoStreak in the list.
if curr.borrow().run_len() != prev.borrow().run_len() {
mono_patterns.push_back(AlternatingMonoPattern::new());
curr_mono_pattern = mono_patterns.back_mut();
}
// * Add the current MonoStreak to the encoded payload.
if let Some(ref curr_mono_pattern) = curr_mono_pattern {
curr_mono_pattern
.borrow_mut()
.mono_streaks
.push(Rc::clone(curr));
}
}
mono_patterns
}
fn encode_repeating_hit_pattern(
mut data: VecDeque<Rc<RefCell<AlternatingMonoPattern<'o>>>>,
) -> Vec<Rc<RefCell<RepeatingHitPatterns<'o>>>> {
let mut hit_patterns = Vec::new();
let mut curr_hit_pattern: Option<Rc<std::cell::RefCell<_>>> = None;
while !data.is_empty() {
let old = curr_hit_pattern.as_ref().map(Rc::downgrade);
let curr_hit_pattern = curr_hit_pattern.insert(RepeatingHitPatterns::new(old));
let mut is_coupled = data.get(2).map_or(false, |other| {
data[0].borrow().is_repetition_of(&other.borrow())
});
if is_coupled {
// * If so, add the current AlternatingMonoPattern to the encoded payload and start repeatedly checking if the
// * subsequent AlternatingMonoPatterns should be grouped by increasing i and doing the appropriate isCoupled check.
while is_coupled {
curr_hit_pattern
.borrow_mut()
.alternating_mono_patterns
.push(data.pop_front().unwrap());
is_coupled = data.get(2).map_or(false, |other| {
data[0].borrow().is_repetition_of(&other.borrow())
});
}
// * Skip over viewed data and add the rest to the payload
for front in data.drain(..2) {
curr_hit_pattern
.borrow_mut()
.alternating_mono_patterns
.push(front);
}
} else {
// * If not, add the current AlternatingMonoPattern to the encoded payload and continue.
curr_hit_pattern
.borrow_mut()
.alternating_mono_patterns
.push(data.pop_front().unwrap());
}
hit_patterns.push(Rc::clone(&*curr_hit_pattern));
}
hit_patterns
.iter_mut()
.for_each(|pattern| pattern.borrow_mut().find_repetition_interval());
hit_patterns
}
}
@@ -0,0 +1,90 @@
use std::{
cell::RefCell,
fmt::{Debug, Formatter, Result as FmtResult},
rc::{Rc, Weak},
};
use crate::taiko::difficulty_object::TaikoDifficultyObject;
use super::alternating_mono_pattern::AlternatingMonoPattern;
pub(crate) struct RepeatingHitPatterns<'o> {
pub(crate) alternating_mono_patterns: Vec<Rc<RefCell<AlternatingMonoPattern<'o>>>>,
pub(crate) prev: Option<Weak<RefCell<Self>>>,
pub(crate) repetition_interval: usize,
}
impl Debug for RepeatingHitPatterns<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
f,
"(interval={}, alt_len={}, has_prev={})",
self.repetition_interval,
self.alternating_mono_patterns.len(),
self.prev.is_some()
)
}
}
impl<'o> RepeatingHitPatterns<'o> {
const MAX_REPETITION_INTERVAL: usize = 16;
pub(crate) fn new(prev: Option<Weak<RefCell<Self>>>) -> Rc<RefCell<Self>> {
let this = Self {
alternating_mono_patterns: Vec::new(),
prev,
repetition_interval: 0,
};
Rc::new(RefCell::new(this))
}
pub(crate) fn first_hit_object(&self) -> Option<Weak<RefCell<TaikoDifficultyObject<'o>>>> {
self.alternating_mono_patterns
.first()
.and_then(|pattern| pattern.borrow().first_hit_object())
}
fn is_repetition_of(&self, other: &Self) -> bool {
if self.alternating_mono_patterns.len() != other.alternating_mono_patterns.len() {
return false;
}
self.alternating_mono_patterns
.iter()
.zip(other.alternating_mono_patterns.iter())
.take(2)
.all(|(self_pat, other_pat)| {
self_pat
.borrow()
.has_identical_mono_len(&other_pat.borrow())
})
}
pub(crate) fn find_repetition_interval(&mut self) {
let mut other = match self.prev.as_ref().and_then(Weak::upgrade) {
Some(prev) => prev,
None => return self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1,
};
let mut interval = 1;
while interval < Self::MAX_REPETITION_INTERVAL {
if self.is_repetition_of(&other.borrow()) {
return self.repetition_interval = interval.min(Self::MAX_REPETITION_INTERVAL);
}
let next = match other.borrow().prev.as_ref().and_then(Weak::upgrade) {
Some(prev) => prev,
None => break,
};
// gotta love NLL...
other = next;
interval += 1;
}
self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1;
}
}
+178 -20
View File
@@ -1,34 +1,192 @@
use super::{closest_rhythm, taiko_object::TaikoObject, HitObjectRhythm};
use std::{cell::RefCell, cmp::Ordering, rc::Rc};
#[derive(Clone, Debug)]
pub(crate) struct DifficultyObject<'o> {
pub(crate) idx: usize,
pub(crate) base: TaikoObject<'o>,
pub(crate) prev: TaikoObject<'o>,
pub(crate) delta: f64,
pub(crate) rhythm: &'static HitObjectRhythm,
pub(crate) start_time: f64,
use crate::parse::HitObject;
use super::{colours::TaikoDifficultyColour, rim::Rim, taiko_object::TaikoObject};
#[derive(Clone, Debug, Default)]
pub(crate) struct ObjectLists<'o> {
pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject<'o>>>>,
pub(crate) centres: Vec<usize>,
pub(crate) rims: Vec<usize>,
pub(crate) notes: Vec<usize>,
}
impl<'o> DifficultyObject<'o> {
#[inline]
impl<'o> ObjectLists<'o> {
pub(crate) fn prev_mono(
&self,
curr: usize,
backwards_idx: usize,
) -> Option<&'_ Rc<RefCell<TaikoDifficultyObject<'o>>>> {
let curr = &self.all[curr];
let prev = match curr.borrow().mono_idx {
MonoIndex::Centre(idx) => idx
.checked_sub(backwards_idx + 1)
.and_then(|idx| self.centres.get(idx))?,
MonoIndex::Rim(idx) => idx
.checked_sub(backwards_idx + 1)
.and_then(|idx| self.rims.get(idx))?,
MonoIndex::None => return None,
};
self.all.get(*prev)
}
#[allow(unused)]
pub(crate) fn next_mono(
&self,
curr: usize,
forwards_idx: usize,
) -> Option<&'_ Rc<RefCell<TaikoDifficultyObject<'o>>>> {
let curr = &self.all[curr];
let next = match curr.borrow().mono_idx {
MonoIndex::Centre(idx) => self.centres.get(idx + (forwards_idx + 1))?,
MonoIndex::Rim(idx) => self.rims.get(idx + (forwards_idx + 1))?,
MonoIndex::None => return None,
};
self.all.get(*next)
}
pub(crate) fn prev_note(
&self,
curr: usize,
backwards_idx: usize,
) -> Option<&'_ Rc<RefCell<TaikoDifficultyObject<'o>>>> {
let curr = &self.all[curr];
let note_idx = curr.borrow().note_idx?;
let idx = note_idx.checked_sub(backwards_idx + 1)?;
let prev = self.notes.get(idx)?;
self.all.get(*prev)
}
#[allow(unused)]
pub(crate) fn next_note(
&self,
curr: usize,
forwards_idx: usize,
) -> Option<&'_ Rc<RefCell<TaikoDifficultyObject<'o>>>> {
let curr = &self.all[curr];
let note_idx = curr.borrow().note_idx?;
let idx = note_idx + (forwards_idx + 1);
let prev = self.notes.get(idx)?;
self.all.get(*prev)
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum MonoIndex {
Centre(usize),
Rim(usize),
None,
}
#[derive(Clone, Debug)]
pub(crate) struct TaikoDifficultyObject<'o> {
pub(crate) base: TaikoObject<'o>,
pub(crate) prev_time: f64,
pub(crate) colour: TaikoDifficultyColour<'o>,
pub(crate) rhythm: &'static HitObjectRhythm,
pub(crate) mono_idx: MonoIndex,
pub(crate) note_idx: Option<usize>,
pub(crate) idx: usize,
pub(crate) delta: f64,
}
impl<'o> TaikoDifficultyObject<'o> {
pub(crate) fn new(
idx: usize,
base: TaikoObject<'o>,
prev: TaikoObject<'o>,
prev_prev: TaikoObject<'o>,
last: TaikoObject<'o>,
last_last: TaikoObject<'o>,
clock_rate: f64,
lists: &ObjectLists<'o>,
idx: usize,
) -> Self {
let delta = (base.h.start_time - prev.h.start_time) / clock_rate;
let rhythm = closest_rhythm(delta, prev.h, prev_prev.h, clock_rate);
// * Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
let colour = TaikoDifficultyColour::default();
let delta = (base.h.start_time - last.h.start_time) / clock_rate;
let rhythm = closest_rhythm(delta, last.h, last_last.h, clock_rate);
let mono_idx = if !base.is_hit() {
MonoIndex::None
} else if base.sound.is_rim() {
MonoIndex::Rim(lists.rims.len())
} else {
MonoIndex::Centre(lists.centres.len())
};
let note_idx = base.is_hit().then_some(lists.notes.len());
Self {
idx,
base,
prev,
delta,
prev_time: last.h.start_time / clock_rate,
colour,
rhythm,
start_time: base.h.start_time / clock_rate,
mono_idx,
note_idx,
idx,
delta,
}
}
}
#[rustfmt::skip]
pub(crate) static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [
HitObjectRhythm { id: 0, ratio: 1.0, difficulty: 0.0 },
HitObjectRhythm { id: 1, ratio: 2.0 / 1.0, difficulty: 0.3 },
HitObjectRhythm { id: 2, ratio: 1.0 / 2.0, difficulty: 0.5 },
HitObjectRhythm { id: 3, ratio: 3.0 / 1.0, difficulty: 0.3 },
HitObjectRhythm { id: 4, ratio: 1.0 / 3.0, difficulty: 0.35 },
// * purposefully higher (requires hand switch in full alternating gameplay style)
HitObjectRhythm { id: 5, ratio: 3.0 / 2.0, difficulty: 0.6 },
HitObjectRhythm { id: 6, ratio: 2.0 / 3.0, difficulty: 0.4 },
HitObjectRhythm { id: 7, ratio: 5.0 / 4.0, difficulty: 0.5 },
HitObjectRhythm { id: 8, ratio: 4.0 / 5.0, difficulty: 0.7 },
];
#[derive(Copy, Clone, Debug)]
pub(crate) struct HitObjectRhythm {
id: u8,
pub(crate) ratio: f64,
pub(crate) difficulty: f64,
}
impl HitObjectRhythm {
pub(crate) fn static_ref() -> &'static Self {
&COMMON_RHYTHMS[0]
}
}
impl PartialEq for HitObjectRhythm {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for HitObjectRhythm {}
fn closest_rhythm(
delta_time: f64,
last: &HitObject,
last_last: &HitObject,
clock_rate: f64,
) -> &'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()
}
+99 -297
View File
@@ -1,22 +1,13 @@
use std::{
cmp::Ordering,
iter::{self, Enumerate, Skip, Zip},
};
use std::{cell::RefCell, rc::Rc, vec::IntoIter};
use crate::{
parse::{HitObject, HitObjectKind},
taiko::{
difficulty_object::DifficultyObject, norm, rescale, simple_color_penalty,
stamina_cheese::StaminaCheeseDetector, COLOR_SKILL_MULTIPLIER, RHYTHM_SKILL_MULTIPLIER,
SECTION_LEN, STAMINA_SKILL_MULTIPLIER,
},
Beatmap, Mods,
};
use crate::{beatmap::BeatmapHitWindows, taiko::rescale, Beatmap, Mods};
use super::{
skill::Skills,
taiko_object::{IntoTaikoObjectIter, TaikoObjectIter},
TaikoDifficultyAttributes,
colours::ColourDifficultyPreprocessor,
difficulty_object::{MonoIndex, ObjectLists, TaikoDifficultyObject},
skills::{Peaks, PeaksDifficultyValues, Skill},
taiko_object::IntoTaikoObjectIter,
TaikoDifficultyAttributes, DIFFICULTY_MULTIPLIER,
};
/// Gradually calculate the difficulty attributes of an osu!taiko map.
@@ -52,205 +43,122 @@ use super::{
#[derive(Clone, Debug)]
pub struct TaikoGradualDifficultyAttributes<'map> {
pub(crate) idx: usize,
difficulty_objects: GradualTaikoObjectIter<'map>,
cheese: Vec<bool>,
skills: Skills,
curr_section_end: f64,
strain_peak_buf: Vec<f64>,
attrs: TaikoDifficultyAttributes,
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject<'map>>>>,
lists: ObjectLists<'map>,
peaks: Peaks,
}
impl<'map> TaikoGradualDifficultyAttributes<'map> {
/// Create a new difficulty attributes iterator for osu!taiko maps.
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
// True if the object at that index is stamina cheese
let cheese = map.find_cheese();
let skills = Skills::new();
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let peaks = Peaks::new();
let clock_rate = mods.clock_rate();
let difficulty_objects = GradualTaikoObjectIter::new(map, clock_rate);
let BeatmapHitWindows { od: hit_window, .. } = map
.attributes()
.mods(mods)
.clock_rate(clock_rate)
.hit_windows();
let attrs = TaikoDifficultyAttributes {
stamina: 0.0,
rhythm: 0.0,
colour: 0.0,
peak: 0.0,
hit_window,
stars: 0.0,
max_combo: 0,
};
let mut diff_objects = map
.taiko_objects()
.enumerate()
.skip(2)
.zip(map.taiko_objects().skip(1))
.zip(map.taiko_objects())
.fold(
ObjectLists::default(),
|mut lists, (((idx, base), last), last_last)| {
let diff_obj =
TaikoDifficultyObject::new(base, last, last_last, clock_rate, &lists, idx);
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx),
MonoIndex::None => {}
}
if diff_obj.note_idx.is_some() {
lists.notes.push(idx);
}
lists.all.push(Rc::new(RefCell::new(diff_obj)));
lists
},
);
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
Self {
idx: 0,
difficulty_objects,
cheese,
skills,
curr_section_end: 0.0,
strain_peak_buf: Vec::new(),
hit_objects: diff_objects.all.clone().into_iter(),
lists: diff_objects,
peaks,
attrs,
}
}
fn locally_combined_difficulty(&mut self, stamina_penalty: f64) -> f64 {
let iter = self
.skills
.color
.strain_peaks
.iter()
.zip(self.skills.rhythm.strain_peaks.iter())
.zip(self.skills.stamina_right.strain_peaks.iter())
.zip(self.skills.stamina_left.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,
)
});
self.strain_peak_buf.clear();
self.strain_peak_buf.extend(iter);
let last = norm(
2.0,
self.skills.color.curr_section_peak * COLOR_SKILL_MULTIPLIER,
self.skills.rhythm.curr_section_peak * RHYTHM_SKILL_MULTIPLIER,
(self.skills.stamina_right.curr_section_peak
+ self.skills.stamina_left.curr_section_peak)
* STAMINA_SKILL_MULTIPLIER
* stamina_penalty,
);
self.strain_peak_buf.push(last);
self.strain_peak_buf
.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 &self.strain_peak_buf {
difficulty += strain * weight;
weight *= 0.9;
}
difficulty
}
}
impl Iterator for TaikoGradualDifficultyAttributes<'_> {
type Item = TaikoDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> {
self.idx = self.idx.saturating_add(1);
let curr = self.hit_objects.next()?;
if self.idx == 1 {
if self.difficulty_objects.first_object.is_empty() {
return None;
}
self.difficulty_objects.max_combo +=
self.difficulty_objects.first_object.is_circle() as usize;
let attributes = TaikoDifficultyAttributes {
stars: 0.0,
max_combo: self.difficulty_objects.max_combo,
};
return Some(attributes);
} else if self.idx == 2 {
if self.difficulty_objects.second_object.is_empty() {
return None;
}
self.difficulty_objects.max_combo +=
self.difficulty_objects.second_object.is_circle() as usize;
let attributes = TaikoDifficultyAttributes {
stars: 0.0,
max_combo: self.difficulty_objects.max_combo,
};
return Some(attributes);
{
let curr = curr.borrow();
self.peaks.process(&curr, &self.lists);
self.attrs.max_combo += curr.base.h.is_circle() as usize;
}
let h = self.difficulty_objects.next()?;
let PeaksDifficultyValues {
mut colour_rating,
mut rhythm_rating,
mut stamina_rating,
mut combined_rating,
} = self.peaks.clone().difficulty_values();
if self.idx == 3 {
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
} else {
while h.start_time > self.curr_section_end {
self.skills
.save_peak_and_start_new_section(self.curr_section_end);
self.curr_section_end += SECTION_LEN;
colour_rating *= DIFFICULTY_MULTIPLIER;
rhythm_rating *= DIFFICULTY_MULTIPLIER;
stamina_rating *= DIFFICULTY_MULTIPLIER;
combined_rating *= DIFFICULTY_MULTIPLIER;
let mut star_rating = rescale(combined_rating * 1.4);
// TODO: adjust once converts are available for gradual processing
let is_convert = false;
// * TODO: This is temporary measure as we don't detect abuse of multiple-input
// * playstyles of converts within the current system.
if is_convert {
star_rating *= 0.925;
// * For maps with low colour variance and high stamina requirement,
// * multiple inputs are more likely to be abused.
if colour_rating < 2.0 && stamina_rating > 8.0 {
star_rating *= 0.8;
}
}
self.skills.process(&h, &self.cheese);
self.attrs.stamina = stamina_rating;
self.attrs.colour = colour_rating;
self.attrs.rhythm = rhythm_rating;
self.attrs.peak = combined_rating;
self.attrs.stars = star_rating;
let len = self.skills.strain_peaks_len();
let missing = len + 1 - self.strain_peak_buf.len();
self.strain_peak_buf.extend(iter::repeat(0.0).take(missing));
self.skills
.color
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
if let Some(last) = self.strain_peak_buf.last_mut() {
*last = self.skills.color.curr_section_peak;
}
let color_rating = self
.skills
.color
.difficulty_value(&mut self.strain_peak_buf)
* COLOR_SKILL_MULTIPLIER;
self.skills
.rhythm
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
if let Some(last) = self.strain_peak_buf.last_mut() {
*last = self.skills.rhythm.curr_section_peak;
}
let rhythm_rating = self
.skills
.rhythm
.difficulty_value(&mut self.strain_peak_buf)
* RHYTHM_SKILL_MULTIPLIER;
self.skills
.stamina_right
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
if let Some(last) = self.strain_peak_buf.last_mut() {
*last = self.skills.stamina_right.curr_section_peak;
}
let stamina_right = self
.skills
.stamina_right
.difficulty_value(&mut self.strain_peak_buf);
self.skills
.stamina_left
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
if let Some(last) = self.strain_peak_buf.last_mut() {
*last = self.skills.stamina_left.curr_section_peak;
}
let stamina_left = self
.skills
.stamina_left
.difficulty_value(&mut self.strain_peak_buf);
let mut stamina_rating = (stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER;
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
stamina_rating *= stamina_penalty;
let combined_rating = self.locally_combined_difficulty(stamina_penalty);
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
let attributes = TaikoDifficultyAttributes {
stars,
max_combo: self.difficulty_objects.max_combo,
};
Some(attributes)
Some(self.attrs.clone())
}
#[inline]
@@ -262,112 +170,6 @@ impl Iterator for TaikoGradualDifficultyAttributes<'_> {
}
impl ExactSizeIterator for TaikoGradualDifficultyAttributes<'_> {
#[inline]
fn len(&self) -> usize {
let mut len = self.difficulty_objects.len();
if self.idx == 0 && !self.difficulty_objects.first_object.is_empty() {
len += 1 + !self.difficulty_objects.second_object.is_empty() as usize;
} else if self.idx == 1 && !self.difficulty_objects.second_object.is_empty() {
len += 1;
}
len
}
}
type InnerIter<'map> = Zip<
Zip<Skip<Enumerate<TaikoObjectIter<'map>>>, Skip<TaikoObjectIter<'map>>>,
TaikoObjectIter<'map>,
>;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
enum SimpleObject {
Circle,
Empty,
NonCircle,
}
impl From<&HitObject> for SimpleObject {
fn from(h: &HitObject) -> Self {
match h.kind {
HitObjectKind::Circle => Self::Circle,
_ => Self::NonCircle,
}
}
}
impl SimpleObject {
fn is_empty(self) -> bool {
self == Self::Empty
}
fn is_circle(self) -> bool {
self == Self::Circle
}
}
#[derive(Clone, Debug)]
struct GradualTaikoObjectIter<'map> {
hit_objects: InnerIter<'map>,
max_combo: usize,
clock_rate: f64,
first_object: SimpleObject,
second_object: SimpleObject,
}
impl<'map> GradualTaikoObjectIter<'map> {
fn new(map: &'map Beatmap, clock_rate: f64) -> Self {
let first_object = map
.hit_objects
.get(0)
.map_or(SimpleObject::Empty, From::from);
let second_object = map
.hit_objects
.get(1)
.map_or(SimpleObject::Empty, From::from);
let hit_objects = map
.taiko_objects()
.enumerate()
.skip(2)
.zip(map.taiko_objects().skip(1))
.zip(map.taiko_objects());
Self {
hit_objects,
max_combo: 0,
clock_rate,
first_object,
second_object,
}
}
}
impl<'map> Iterator for GradualTaikoObjectIter<'map> {
type Item = DifficultyObject<'map>;
fn next(&mut self) -> Option<Self::Item> {
let (((idx, base), prev), prev_prev) = self.hit_objects.next()?;
self.max_combo += base.h.is_circle() as usize;
Some(DifficultyObject::new(
idx,
base,
prev,
prev_prev,
self.clock_rate,
))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.hit_objects.size_hint()
}
}
impl ExactSizeIterator for GradualTaikoObjectIter<'_> {
#[inline]
fn len(&self) -> usize {
self.hit_objects.len()
-94
View File
@@ -1,94 +0,0 @@
use crate::parse::HitObject;
use std::cmp::Ordering;
static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [
HitObjectRhythm {
id: 0,
ratio: 1.0,
difficulty: 0.0,
},
HitObjectRhythm {
id: 1,
ratio: 2.0 / 1.0,
difficulty: 0.3,
},
HitObjectRhythm {
id: 2,
ratio: 1.0 / 2.0,
difficulty: 0.5,
},
HitObjectRhythm {
id: 3,
ratio: 3.0 / 1.0,
difficulty: 0.3,
},
HitObjectRhythm {
id: 4,
ratio: 1.0 / 3.0,
difficulty: 0.35,
},
HitObjectRhythm {
id: 5,
ratio: 3.0 / 2.0,
difficulty: 0.6,
},
HitObjectRhythm {
id: 6,
ratio: 2.0 / 3.0,
difficulty: 0.4,
},
HitObjectRhythm {
id: 7,
ratio: 5.0 / 4.0,
difficulty: 0.5,
},
HitObjectRhythm {
id: 8,
ratio: 4.0 / 5.0,
difficulty: 0.7,
},
];
#[derive(Copy, Clone, Debug)]
pub(crate) struct HitObjectRhythm {
id: u8,
ratio: f64,
pub(crate) difficulty: f64,
}
impl HitObjectRhythm {
pub(crate) fn static_ref() -> &'static Self {
&COMMON_RHYTHMS[0]
}
}
impl PartialEq for HitObjectRhythm {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for HitObjectRhythm {}
#[inline]
pub(crate) fn closest_rhythm(
delta_time: f64,
last: &HitObject,
last_last: &HitObject,
clock_rate: f64,
) -> &'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()
}
+118 -121
View File
@@ -1,36 +1,31 @@
mod colours;
mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod hitobject_rhythm;
mod pp;
mod rim;
mod skill;
mod skill_kind;
mod skills;
mod stamina_cheese;
mod taiko_object;
use difficulty_object::DifficultyObject;
pub use gradual_difficulty::*;
pub use gradual_performance::*;
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
pub use pp::*;
use rim::Rim;
use skill_kind::SkillKind;
use stamina_cheese::StaminaCheeseDetector;
use taiko_object::IntoTaikoObjectIter;
use crate::taiko::skill::Skills;
use crate::beatmap::BeatmapHitWindows;
use crate::{Beatmap, GameMode, Mods, OsuStars};
use std::borrow::Cow;
use std::cmp::Ordering;
use std::f64::consts::PI;
use std::{borrow::Cow, cell::RefCell, rc::Rc};
const SECTION_LEN: f64 = 400.0;
use self::colours::ColourDifficultyPreprocessor;
use self::difficulty_object::{MonoIndex, ObjectLists, TaikoDifficultyObject};
use self::skills::{Peaks, PeaksDifficultyValues, PeaksRaw, Skill};
const COLOR_SKILL_MULTIPLIER: f64 = 0.01;
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
const SECTION_LEN: usize = 400;
const DIFFICULTY_MULTIPLIER: f64 = 1.35;
/// Difficulty calculator on osu!taiko maps.
///
@@ -56,6 +51,7 @@ pub struct TaikoStars<'map> {
mods: u32,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
is_convert: bool,
}
impl<'map> TaikoStars<'map> {
@@ -67,6 +63,7 @@ impl<'map> TaikoStars<'map> {
mods: 0,
passed_objects: None,
clock_rate: None,
is_convert: false,
}
}
@@ -102,35 +99,65 @@ impl<'map> TaikoStars<'map> {
self
}
/// Specify whether the map is a convert i.e. an osu!standard map.
#[inline]
pub fn is_convert(mut self, is_convert: bool) -> Self {
self.is_convert = is_convert;
self
}
/// Calculate all difficulty related values, including stars.
#[inline]
pub fn calculate(self) -> TaikoDifficultyAttributes {
let (skills, max_combo) = calculate_skills(self);
let mut buf = vec![0.0; skills.strain_peaks_len()];
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
skills.color.copy_strain_peaks(&mut buf);
let color_rating = skills.color.difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
let BeatmapHitWindows { od: hit_window, .. } = self
.map
.attributes()
.mods(self.mods)
.clock_rate(clock_rate)
.hit_windows();
skills.rhythm.copy_strain_peaks(&mut buf);
let rhythm_rating = skills.rhythm.difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
let is_convert = self.is_convert || matches!(self.map, Cow::Owned(_));
skills.stamina_right.copy_strain_peaks(&mut buf);
let stamina_right = skills.stamina_right.difficulty_value(&mut buf);
let (peaks, max_combo) = calculate_skills(self);
skills.stamina_left.copy_strain_peaks(&mut buf);
let stamina_left = skills.stamina_left.difficulty_value(&mut buf);
let PeaksDifficultyValues {
mut colour_rating,
mut rhythm_rating,
mut stamina_rating,
mut combined_rating,
} = peaks.difficulty_values();
let mut stamina_rating = (stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER;
colour_rating *= DIFFICULTY_MULTIPLIER;
rhythm_rating *= DIFFICULTY_MULTIPLIER;
stamina_rating *= DIFFICULTY_MULTIPLIER;
combined_rating *= DIFFICULTY_MULTIPLIER;
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
stamina_rating *= stamina_penalty;
let mut star_rating = rescale(combined_rating * 1.4);
let combined_rating = locally_combined_difficulty(&mut buf, &skills, stamina_penalty);
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
// * TODO: This is temporary measure as we don't detect abuse of multiple-input
// * playstyles of converts within the current system.
if is_convert {
star_rating *= 0.925;
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
// * For maps with low colour variance and high stamina requirement,
// * multiple inputs are more likely to be abused.
if colour_rating < 2.0 && stamina_rating > 8.0 {
star_rating *= 0.8;
}
}
TaikoDifficultyAttributes { stars, max_combo }
TaikoDifficultyAttributes {
stamina: stamina_rating,
rhythm: rhythm_rating,
colour: colour_rating,
peak: combined_rating,
hit_window,
stars: star_rating,
max_combo,
}
}
/// Calculate the skill strains.
@@ -139,14 +166,19 @@ impl<'map> TaikoStars<'map> {
#[inline]
pub fn strains(self) -> TaikoStrains {
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
let (skills, _) = calculate_skills(self);
let (peaks, _) = calculate_skills(self);
let PeaksRaw {
colour,
rhythm,
stamina,
} = peaks.into_raw();
TaikoStrains {
section_len: SECTION_LEN * clock_rate,
color: skills.color.strain_peaks,
rhythm: skills.rhythm.strain_peaks,
stamina_right: skills.stamina_right.strain_peaks,
stamina_left: skills.stamina_left.strain_peaks,
section_len: SECTION_LEN as f64 * clock_rate,
color: colour,
rhythm,
stamina,
}
}
}
@@ -161,10 +193,8 @@ pub struct TaikoStrains {
pub color: Vec<f64>,
/// Strain peaks of the rhythm skill.
pub rhythm: Vec<f64>,
/// Strain peaks of the left-stamina skill.
pub stamina_right: Vec<f64>,
/// Strain peaks of the right-stamina skill.
pub stamina_left: Vec<f64>,
/// Strain peaks of the stamina skill.
pub stamina: Vec<f64>,
}
impl TaikoStrains {
@@ -176,67 +206,68 @@ impl TaikoStrains {
}
}
fn calculate_skills(params: TaikoStars<'_>) -> (Skills, usize) {
fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
let TaikoStars {
map,
mods,
passed_objects,
clock_rate,
..
} = params;
let take = passed_objects.unwrap_or(map.hit_objects.len());
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
// True if the object at that index is stamina cheese
let cheese = map.find_cheese();
let mut skills = Skills::new();
let mut peaks = Peaks::new();
let mut max_combo = 0;
match map.hit_objects.get(0) {
Some(h) => max_combo += h.is_circle() as usize,
None => return (skills, max_combo),
None => return (peaks, max_combo),
}
match map.hit_objects.get(1) {
Some(h) => max_combo += h.is_circle() as usize,
None => return (skills, max_combo),
None => return (peaks, max_combo),
}
let mut hit_objects = map
let mut diff_objects = map
.taiko_objects()
.take(take)
.enumerate()
.skip(2)
.zip(map.taiko_objects().skip(1))
.zip(map.taiko_objects())
.inspect(|(((_, base), _), _)| max_combo += base.h.is_circle() as usize)
.map(|(((idx, base), prev), prev_prev)| {
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
});
.enumerate()
.inspect(|(_, ((base, _), _))| max_combo += base.h.is_circle() as usize)
.fold(
ObjectLists::default(),
|mut lists, (idx, ((base, last), last_last))| {
let diff_obj =
TaikoDifficultyObject::new(base, last, last_last, clock_rate, &lists, idx);
// Handle first element distinctly
let h = match hit_objects.next() {
Some(h) => h,
None => return (skills, max_combo),
};
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx),
MonoIndex::None => {}
}
// No strain for first object
let mut curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
skills.process(&h, &cheese);
if diff_obj.note_idx.is_some() {
lists.notes.push(idx);
}
// Handle all other objects
for h in hit_objects {
while h.start_time > curr_section_end {
skills.save_peak_and_start_new_section(curr_section_end);
curr_section_end += SECTION_LEN;
}
lists.all.push(Rc::new(RefCell::new(diff_obj)));
skills.process(&h, &cheese);
lists
},
);
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
for hit_object in diff_objects.all.iter() {
peaks.process(&hit_object.borrow(), &diff_objects);
}
skills.save_current_peak();
(skills, max_combo)
(peaks, max_combo)
}
#[inline]
@@ -248,56 +279,19 @@ fn rescale(stars: f64) -> f64 {
}
}
#[inline]
fn simple_color_penalty(stamina: f64, color: f64) -> f64 {
if color <= 0.0 {
0.79 - 0.25
} else {
0.79 - (stamina / color - 12.0).atan() / PI / 2.0
}
}
fn locally_combined_difficulty(peaks: &mut Vec<f64>, skills: &Skills, stamina_penalty: f64) -> f64 {
peaks.clear();
let iter = skills
.color
.strain_peaks
.iter()
.zip(skills.rhythm.strain_peaks.iter())
.zip(skills.stamina_right.strain_peaks.iter())
.zip(skills.stamina_left.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: f64, a: f64, b: f64, c: f64) -> f64 {
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
}
/// The result of a difficulty calculation on an osu!taiko map.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TaikoDifficultyAttributes {
/// The difficulty corresponding to the stamina skill.
pub stamina: f64,
/// The difficulty corresponding to the rhythm skill.
pub rhythm: f64,
/// The difficulty corresponding to the colour skill.
pub colour: f64,
/// The difficulty corresponding to the hardest parts of the map.
pub peak: f64,
/// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc)
pub hit_window: f64,
/// The final star rating.
pub stars: f64,
/// The maximum combo.
@@ -322,7 +316,9 @@ pub struct TaikoPerformanceAttributes {
/// The accuracy portion of the final pp.
pub pp_acc: f64,
/// The strain portion of the final pp.
pub pp_strain: f64,
pub pp_difficulty: f64,
/// Scaled miss count based on total hits.
pub effective_miss_count: f64,
}
impl TaikoPerformanceAttributes {
@@ -367,6 +363,7 @@ impl<'map> From<OsuStars<'map>> for TaikoStars<'map> {
mods,
passed_objects,
clock_rate,
is_convert: true,
}
}
}
+120 -53
View File
@@ -38,7 +38,7 @@ use crate::{
#[derive(Clone, Debug)]
#[allow(clippy::upper_case_acronyms)]
pub struct TaikoPP<'map> {
map: Cow<'map, Beatmap>,
pub(crate) map: Cow<'map, Beatmap>,
attributes: Option<TaikoDifficultyAttributes>,
mods: u32,
combo: Option<usize>,
@@ -176,7 +176,9 @@ impl<'map> TaikoPP<'map> {
/// Calculate all performance related values, including pp and stars.
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
let attributes = self.attributes.take().unwrap_or_else(|| {
let mut calculator = TaikoStars::new(self.map.as_ref()).mods(self.mods);
let mut calculator = TaikoStars::new(self.map.as_ref())
.mods(self.mods)
.is_convert(matches!(self.map, Cow::Owned(_)));
if let Some(passed_objects) = self.passed_objects {
calculator = calculator.passed_objects(passed_objects);
@@ -189,36 +191,65 @@ impl<'map> TaikoPP<'map> {
calculator.calculate()
});
if self.n300.or(self.n100).is_some() {
let total = self.map.n_circles as usize;
let misses = self.n_misses;
self.assert_hitresults(attributes).calculate()
}
let mut n300 = self.n300.unwrap_or(0).min(total - misses);
let mut n100 = self.n100.unwrap_or(0).min(total - n300 - misses);
fn assert_hitresults(&'map self, attributes: TaikoDifficultyAttributes) -> TaikoPPInner<'map> {
let total_result_count = attributes.max_combo();
let misses = self.n_misses;
let given = n300 + n100 + misses;
let missing = total - given;
let (n300, n100) = match (self.n300, self.n100) {
(Some(n300), Some(n100)) => {
let n300 = n300.min(total_result_count - misses);
let n100 = n100.min(total_result_count - n300 - misses);
match (self.n300, self.n100) {
(Some(_), Some(_)) => n300 += missing,
(Some(_), None) => n100 += missing,
(None, Some(_)) => n300 += missing,
(None, None) => unreachable!(),
};
let given = n300 + n100 + misses;
let missing = total_result_count - given;
self.acc = (2 * n300 + n100) as f64 / (2 * (n300 + n100 + misses)) as f64;
}
(n300 + missing, n100)
}
(Some(n300), None) => {
let n300 = n300.min(total_result_count - misses);
let inner = TaikoPPInner {
map: self.map.as_ref(),
attributes,
mods: self.mods,
acc: self.acc,
n_misses: self.n_misses,
clock_rate: self.clock_rate.unwrap_or_else(|| self.mods.clock_rate()),
let n100 = total_result_count
.saturating_sub(n300)
.saturating_sub(misses);
(n300, n100)
}
(None, Some(n100)) => {
let n100 = n100.min(total_result_count - misses);
let n300 = total_result_count
.saturating_sub(n100)
.saturating_sub(misses);
(n300, n100)
}
(None, None) => {
let target_total = (self.acc * (total_result_count * 2) as f64) as usize;
let n300 = target_total - total_result_count.saturating_sub(misses);
let n100 = total_result_count
.saturating_sub(n300)
.saturating_sub(misses);
(n300, n100)
}
};
inner.calculate()
let acc = (2 * n300 + n100) as f64 / (2 * (n300 + n100 + misses)) as f64;
TaikoPPInner {
map: self.map.as_ref(),
attributes,
acc,
n300,
n100,
mods: self.mods,
n_misses: misses,
clock_rate: self.clock_rate.unwrap_or_else(|| self.mods.clock_rate()),
}
}
}
@@ -229,57 +260,73 @@ struct TaikoPPInner<'map> {
acc: f64,
n_misses: usize,
clock_rate: f64,
n300: usize,
n100: usize,
}
impl<'map> TaikoPPInner<'map> {
fn calculate(self) -> TaikoPerformanceAttributes {
let mut multiplier = 1.1;
// * The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits
// * and increasing the miss penalty for shorter object counts lower than 1000.
let total_successful_hits = self.total_successful_hits();
if self.mods.nf() {
multiplier *= 0.9;
}
let effective_miss_count = if total_successful_hits > 0 {
(1000.0 / (total_successful_hits as f64)).max(1.0) * self.n_misses as f64
} else {
0.0
};
let mut multiplier = 1.13;
if self.mods.hd() {
multiplier *= 1.1;
multiplier *= 1.075;
}
let strain_value = self.compute_strain_value();
if self.mods.ez() {
multiplier *= 0.975;
}
let diff_value = self.compute_difficulty_value(effective_miss_count);
let acc_value = self.compute_accuracy_value();
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
let pp = (diff_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
TaikoPerformanceAttributes {
difficulty: self.attributes,
pp,
pp_acc: acc_value,
pp_strain: strain_value,
pp_difficulty: diff_value,
effective_miss_count,
}
}
fn compute_strain_value(&self) -> f64 {
let attributes = &self.attributes;
let exp_base = 5.0 * (attributes.stars / 0.0075).max(1.0) - 4.0;
let mut strain = exp_base * exp_base / 100_000.0;
fn compute_difficulty_value(&self, effective_miss_count: f64) -> f64 {
let attrs = &self.attributes;
let exp_base = 5.0 * (attrs.stars / 0.115).max(1.0) - 4.0;
let mut diff_value = exp_base.powf(2.25) / 1150.0;
// Longer maps are worth more
let len_bonus = 1.0 + 0.1 * (attributes.max_combo as f64 / 1500.0).min(1.0);
strain *= len_bonus;
let len_bonus = 1.0 + 0.1 * (attrs.max_combo as f64 / 1500.0).min(1.0);
diff_value *= len_bonus;
// Penalize misses exponentially
strain *= 0.985_f64.powi(self.n_misses as i32);
diff_value *= 0.986_f64.powf(effective_miss_count);
if self.mods.ez() {
diff_value *= 0.985;
}
// HD bonus
if self.mods.hd() {
strain *= 1.025;
diff_value *= 1.025;
}
if self.mods.hr() {
diff_value *= 1.05;
}
// FL bonus
if self.mods.fl() {
strain *= 1.05 * len_bonus;
diff_value *= 1.05 * len_bonus;
}
// Scale with accuracy
strain * self.acc
diff_value * self.acc * self.acc
}
#[inline]
@@ -291,12 +338,32 @@ impl<'map> TaikoPPInner<'map> {
.clock_rate(self.clock_rate)
.hit_windows();
let max_combo = self.attributes.max_combo;
if hit_window <= 0.0 {
return 0.0;
}
(150.0 / hit_window).powf(1.1)
* self.acc.powi(15)
* 22.0
* (max_combo as f64 / 1500.0).powf(0.3).min(1.15)
let mut acc_value = (60.0 / hit_window).powf(1.1)
* self.acc.powi(8)
* self.attributes.stars.powf(0.4)
* 27.0;
let len_bonus = (self.total_hits() as f64 / 1500.0).powf(0.3).min(1.15);
acc_value *= len_bonus;
// * Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values
if self.mods.hd() && self.mods.fl() {
acc_value *= (1.075 * len_bonus).max(1.05);
}
acc_value
}
fn total_hits(&self) -> usize {
self.n300 + self.n100 + self.n_misses
}
fn total_successful_hits(&self) -> usize {
self.n300 + self.n100
}
}
-153
View File
@@ -1,153 +0,0 @@
use super::{DifficultyObject, SkillKind};
use std::cmp::Ordering;
const DECAY_WEIGHT: f64 = 0.9;
const COLOR_SKILL_MULTIPLIER: f64 = 1.0;
const COLOR_STRAIN_DECAY_BASE: f64 = 0.4;
const RHYTHM_SKILL_MULTIPLIER: f64 = 10.0;
const RHYTHM_STRAIN_DECAY_BASE: f64 = 0.0;
const STAMINA_SKILL_MULTIPLIER: f64 = 1.0;
const STAMINA_STRAIN_DECAY_BASE: f64 = 0.4;
#[derive(Clone, Debug)]
pub(crate) struct Skills {
pub(crate) color: Skill,
pub(crate) rhythm: Skill,
pub(crate) stamina_right: Skill,
pub(crate) stamina_left: Skill,
}
impl Skills {
pub(crate) fn new() -> Self {
Self {
color: Skill::new(SkillKind::color()),
rhythm: Skill::new(SkillKind::rhythm()),
stamina_right: Skill::new(SkillKind::stamina(true)),
stamina_left: Skill::new(SkillKind::stamina(false)),
}
}
pub(crate) fn save_peak_and_start_new_section(&mut self, time: f64) {
self.color.save_current_peak();
self.color.start_new_section_from(time);
self.rhythm.save_current_peak();
self.rhythm.start_new_section_from(time);
self.stamina_right.save_current_peak();
self.stamina_right.start_new_section_from(time);
self.stamina_left.save_current_peak();
self.stamina_left.start_new_section_from(time);
}
pub(crate) fn save_current_peak(&mut self) {
self.color.save_current_peak();
self.rhythm.save_current_peak();
self.stamina_right.save_current_peak();
self.stamina_left.save_current_peak();
}
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>, cheese: &[bool]) {
self.color.process(curr, cheese);
self.rhythm.process(curr, cheese);
self.stamina_right.process(curr, cheese);
self.stamina_left.process(curr, cheese);
}
pub(crate) fn strain_peaks_len(&self) -> usize {
self.color.strain_peaks.len()
}
}
#[derive(Clone, Debug)]
pub(crate) struct Skill {
pub(crate) current_strain: f64,
pub(crate) curr_section_peak: f64,
kind: SkillKind,
pub(crate) strain_peaks: Vec<f64>,
prev_time: Option<f64>,
}
impl Skill {
#[inline]
pub(crate) fn new(kind: SkillKind) -> Self {
Self {
current_strain: 1.0,
curr_section_peak: 1.0,
kind,
strain_peaks: Vec::with_capacity(128),
prev_time: None,
}
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
self.strain_peaks.push(self.curr_section_peak);
}
#[inline]
pub(crate) fn start_new_section_from(&mut self, time: f64) {
self.curr_section_peak = self.peak_strain(time - self.prev_time.unwrap());
}
#[inline]
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>, cheese: &[bool]) {
self.current_strain *= self.strain_decay(curr.delta);
self.current_strain += self.kind.strain_value_of(curr, cheese) * self.skill_multiplier();
self.curr_section_peak = self.curr_section_peak.max(self.current_strain);
self.prev_time.replace(curr.start_time);
}
pub(crate) fn copy_strain_peaks(&self, buf: &mut [f64]) {
buf.copy_from_slice(&self.strain_peaks);
}
#[inline]
pub(crate) fn difficulty_value(&self, peaks: &mut [f64]) -> f64 {
let mut difficulty = 0.0;
let mut weight = 1.0;
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in peaks.iter() {
difficulty += strain * weight;
weight *= DECAY_WEIGHT;
}
difficulty
}
#[inline]
fn skill_multiplier(&self) -> f64 {
match self.kind {
SkillKind::Color { .. } => COLOR_SKILL_MULTIPLIER,
SkillKind::Rhythm { .. } => RHYTHM_SKILL_MULTIPLIER,
SkillKind::Stamina { .. } => STAMINA_SKILL_MULTIPLIER,
}
}
#[inline]
fn strain_decay_base(&self) -> f64 {
match self.kind {
SkillKind::Color { .. } => COLOR_STRAIN_DECAY_BASE,
SkillKind::Rhythm { .. } => RHYTHM_STRAIN_DECAY_BASE,
SkillKind::Stamina { .. } => STAMINA_STRAIN_DECAY_BASE,
}
}
#[inline]
fn peak_strain(&self, delta_time: f64) -> f64 {
self.current_strain * self.strain_decay(delta_time)
}
#[inline]
fn strain_decay(&self, ms: f64) -> f64 {
self.strain_decay_base().powf(ms / 1000.0)
}
}
-311
View File
@@ -1,311 +0,0 @@
use crate::limited_queue::LimitedQueue;
use super::{DifficultyObject, HitObjectRhythm, Rim};
const RHYTHM_STRAIN_DECAY: f64 = 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;
#[derive(Copy, Clone, Debug)]
pub(crate) struct RhythmHistoryElement {
idx: usize,
rhythm: &'static HitObjectRhythm,
}
impl RhythmHistoryElement {
fn new(difficulty_object: &DifficultyObject<'_>) -> Self {
Self {
idx: difficulty_object.idx,
rhythm: difficulty_object.rhythm,
}
}
}
impl Default for RhythmHistoryElement {
fn default() -> Self {
Self {
idx: 0,
rhythm: HitObjectRhythm::static_ref(),
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum SkillKind {
Color {
mono_history: LimitedQueue<usize, MONO_HISTORY_MAX_LEN>,
prev_is_rim: Option<bool>,
current_mono_len: usize,
},
Rhythm {
rhythm_history: LimitedQueue<RhythmHistoryElement, RHYTHM_HISTORY_MAX_LEN>,
notes_since_rhythm_change: usize,
current_strain: f64,
},
Stamina {
note_pair_duration_history: LimitedQueue<f64, STAMINA_HISTORY_MAX_LEN>,
hand: u8,
off_hand_object_duration: f64,
},
}
impl SkillKind {
#[inline]
pub(crate) fn color() -> Self {
Self::Color {
mono_history: LimitedQueue::new(),
prev_is_rim: None,
current_mono_len: 0,
}
}
#[inline]
pub(crate) fn rhythm() -> Self {
Self::Rhythm {
rhythm_history: LimitedQueue::new(),
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(),
hand: right_hand as u8,
off_hand_object_duration: f64::MAX,
}
}
pub(crate) fn strain_value_of(
&mut self,
current: &DifficultyObject<'_>,
cheese: &[bool],
) -> f64 {
match self {
Self::Color {
mono_history,
prev_is_rim,
current_mono_len,
} => {
let prev_is_circle = current.prev.h.is_circle();
let base_is_circle = current.base.h.is_circle();
let curr_is_rim = current.base.sound.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| {
let to_compare =
mono_history.len() + i - MOST_RECENT_PATTERNS_TO_COMPARE;
mono_history[start + i] != mono_history[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.h.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() <= f64::EPSILON {
return 0.0;
}
let mut strain = current.rhythm.difficulty;
rhythm_history.push(RhythmHistoryElement::new(current));
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| {
let to_compare =
rhythm_history.len() + i - most_recent_patterns_to_compare;
rhythm_history[start + i].rhythm != rhythm_history[to_compare].rhythm
});
if different_pattern {
continue;
}
reps_penalty *= repetition_penalty(current.idx - rhythm_history[start].idx);
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.h.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) -> f64 {
let pattern_len = pattern_len as f64;
let short_pattern_penalty = (0.15 * pattern_len).min(1.0);
let long_pattern_penalty = (2.5 - 0.15 * pattern_len).max(0.0).min(1.0);
short_pattern_penalty.min(long_pattern_penalty)
}
#[inline]
fn cheese_penalty(note_pair_duration: f64) -> f64 {
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: f64) -> f64 {
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) -> f64 {
(0.032 * notes_since as f64).min(1.0)
}
+154
View File
@@ -0,0 +1,154 @@
use std::{
cell::RefCell,
rc::{Rc, Weak},
};
use crate::taiko::{
colours::{AlternatingMonoPattern, MonoStreak, RepeatingHitPatterns},
difficulty_object::{ObjectLists, TaikoDifficultyObject},
};
use super::{Skill, StrainDecaySkill, StrainSkill};
#[derive(Clone, Debug)]
pub(crate) struct Colour {
curr_strain: f64,
curr_section_peak: f64,
curr_section_end: f64,
pub(crate) strain_peaks: Vec<f64>,
}
impl Colour {
pub(crate) fn new() -> Self {
Self {
curr_strain: 0.0,
curr_section_peak: 0.0,
curr_section_end: 0.0,
strain_peaks: Vec::new(),
}
}
}
impl Skill for Colour {
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
<Self as StrainSkill>::process(self, curr, hit_objects)
}
fn difficulty_value(self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
}
impl StrainSkill for Colour {
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
fn strain_value_at(
&mut self,
curr: &TaikoDifficultyObject<'_>,
hit_objects: &ObjectLists<'_>,
) -> f64 {
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
}
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64 {
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
}
}
impl StrainDecaySkill for Colour {
const SKILL_MULTIPLIER: f64 = 0.12;
const STRAIN_DECAY_BASE: f64 = 0.8;
fn curr_strain(&self) -> f64 {
self.curr_strain
}
fn curr_strain_mut(&mut self) -> &mut f64 {
&mut self.curr_strain
}
fn strain_value_of(&mut self, curr: &TaikoDifficultyObject<'_>, _: &ObjectLists<'_>) -> f64 {
ColourEvaluator::evaluate_diff_of(curr)
}
}
struct ColourEvaluator;
impl ColourEvaluator {
fn sigmoid(val: f64, center: f64, width: f64, middle: f64, height: f64) -> f64 {
let sigmoid = (std::f64::consts::E * -(val - center) / width).tanh();
sigmoid * (height / 2.0) + middle
}
fn evaluate_diff_of_mono_streak(mono_streak: Rc<RefCell<MonoStreak<'_>>>) -> f64 {
let mono_streak = mono_streak.borrow();
let parent_eval = mono_streak
.parent
.as_ref()
.and_then(Weak::upgrade)
.map_or(1.0, Self::evaluate_diff_of_alternating_mono_pattern);
Self::sigmoid(mono_streak.idx as f64, 2.0, 2.0, 0.5, 1.0) * parent_eval * 0.5
}
fn evaluate_diff_of_alternating_mono_pattern(
alternating_mono_pattern: Rc<RefCell<AlternatingMonoPattern<'_>>>,
) -> f64 {
let alternating_mono_pattern = alternating_mono_pattern.borrow();
let parent_eval = alternating_mono_pattern
.parent
.as_ref()
.and_then(Weak::upgrade)
.map_or(1.0, Self::evaluate_diff_of_repeating_hit_patterns);
Self::sigmoid(alternating_mono_pattern.idx as f64, 2.0, 2.0, 0.5, 1.0) * parent_eval
}
fn evaluate_diff_of_repeating_hit_patterns(
repeating_hit_patterns: Rc<RefCell<RepeatingHitPatterns<'_>>>,
) -> f64 {
let repetition_interval = repeating_hit_patterns.borrow().repetition_interval as f64;
2.0 * (1.0 - Self::sigmoid(repetition_interval, 2.0, 2.0, 0.5, 1.0))
}
fn evaluate_diff_of(hit_object: &TaikoDifficultyObject<'_>) -> f64 {
let colour = &hit_object.colour;
let mut difficulty = 0.0;
// * Difficulty for MonoStreak
if let Some(mono_streak) = colour.mono_streak.as_ref().and_then(Weak::upgrade) {
difficulty += Self::evaluate_diff_of_mono_streak(mono_streak);
}
// * Difficulty for AlternatingMonoPattern
if let Some(alternating_mono_pattern) = colour
.alternating_mono_pattern
.as_ref()
.and_then(Weak::upgrade)
{
difficulty += Self::evaluate_diff_of_alternating_mono_pattern(alternating_mono_pattern);
}
// * Difficulty for RepeatingHitPattern
if let Some(repeating_hit_patterns) = colour.repeating_hit_patterns.as_ref().map(Rc::clone)
{
difficulty += Self::evaluate_diff_of_repeating_hit_patterns(repeating_hit_patterns);
}
difficulty
}
}
+131
View File
@@ -0,0 +1,131 @@
mod colour;
mod peaks;
mod rhythm;
mod stamina;
use std::{cmp::Ordering, mem};
pub(crate) use self::peaks::{Peaks, PeaksDifficultyValues, PeaksRaw};
use super::{
difficulty_object::{ObjectLists, TaikoDifficultyObject},
SECTION_LEN,
};
pub(crate) trait Skill: Sized {
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>);
fn difficulty_value(self) -> f64;
}
pub(crate) trait StrainSkill: Skill {
const DECAY_WEIGHT: f64 = 0.9;
fn strain_peaks_mut(&mut self) -> &mut Vec<f64>;
fn curr_section_peak(&mut self) -> &mut f64;
fn curr_section_end(&mut self) -> &mut f64;
fn strain_value_at(
&mut self,
curr: &TaikoDifficultyObject<'_>,
hit_objects: &ObjectLists<'_>,
) -> f64;
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64;
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
// * The first object doesn't generate a strain, so we begin with an incremented section end
if curr.idx == 0 {
let section_len = SECTION_LEN as f64;
*self.curr_section_end() = (curr.base.h.start_time / section_len).ceil() * section_len;
}
while curr.base.h.start_time > *self.curr_section_end() {
self.save_curr_peak();
{
let section_end = *self.curr_section_end();
self.start_new_section_from(section_end, curr);
}
*self.curr_section_end() += SECTION_LEN as f64;
}
*self.curr_section_peak() = self
.strain_value_at(curr, hit_objects)
.max(*self.curr_section_peak());
}
fn save_curr_peak(&mut self) {
let peak = *self.curr_section_peak();
self.strain_peaks_mut().push(peak);
}
fn start_new_section_from(&mut self, time: f64, curr: &TaikoDifficultyObject<'_>) {
// * The maximum strain of the new section is not zero by default
// * This means we need to capture the strain level at the beginning of the new section,
// * and use that as the initial peak level.
*self.curr_section_peak() = self.calculate_initial_strain(time, curr);
}
fn difficulty_value(self) -> f64 {
let mut difficulty = 0.0;
let mut weight = 1.0;
// * Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// * These sections will not contribute to the difficulty.
let mut peaks = self.get_curr_strain_peaks();
peaks.retain(|&peak| peak > 0.0);
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
// * Difficulty is the weighted sum of the highest strains from every section.
// * We're sorting from highest to lowest strain.
for strain in peaks {
difficulty += strain * weight;
weight *= Self::DECAY_WEIGHT;
}
difficulty
}
fn get_curr_strain_peaks(mut self) -> Vec<f64> {
let curr_peak = *self.curr_section_peak();
let mut strain_peaks = mem::take(self.strain_peaks_mut());
strain_peaks.push(curr_peak);
strain_peaks
}
}
pub(crate) trait StrainDecaySkill: StrainSkill {
const SKILL_MULTIPLIER: f64;
const STRAIN_DECAY_BASE: f64;
fn curr_strain(&self) -> f64;
fn curr_strain_mut(&mut self) -> &mut f64;
fn strain_value_of(
&mut self,
curr: &TaikoDifficultyObject<'_>,
hit_objects: &ObjectLists<'_>,
) -> f64;
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64 {
self.curr_strain() * self.strain_decay(time - curr.prev_time)
}
fn strain_value_at(
&mut self,
curr: &TaikoDifficultyObject<'_>,
hit_objects: &ObjectLists<'_>,
) -> f64 {
*self.curr_strain_mut() *= self.strain_decay(curr.delta);
*self.curr_strain_mut() += self.strain_value_of(curr, hit_objects) * Self::SKILL_MULTIPLIER;
self.curr_strain()
}
fn strain_decay(&self, ms: f64) -> f64 {
Self::STRAIN_DECAY_BASE.powf(ms / 1000.0)
}
}
+122
View File
@@ -0,0 +1,122 @@
use std::cmp::Ordering;
use crate::taiko::difficulty_object::{ObjectLists, TaikoDifficultyObject};
use super::{colour::Colour, rhythm::Rhythm, stamina::Stamina, Skill, StrainSkill};
#[derive(Clone, Debug)]
pub(crate) struct Peaks {
colour: Colour,
rhythm: Rhythm,
stamina: Stamina,
}
impl Peaks {
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.2 * Self::FINAL_MULTIPLIER;
const COLOUR_SKILL_MULTIPLIER: f64 = 0.375 * Self::FINAL_MULTIPLIER;
const STAMINA_SKILL_MULTIPLIER: f64 = 0.375 * Self::FINAL_MULTIPLIER;
const FINAL_MULTIPLIER: f64 = 0.0625;
pub(crate) fn new() -> Self {
Self {
colour: Colour::new(),
rhythm: Rhythm::new(),
stamina: Stamina::new(),
}
}
pub(crate) fn difficulty_values(self) -> PeaksDifficultyValues {
let colour_rating =
StrainSkill::difficulty_value(self.colour) * Self::COLOUR_SKILL_MULTIPLIER;
// let rhythm_rating =
// StrainSkill::difficulty_value(self.rhythm.clone()) * Self::RHYTHM_SKILL_MULTIPLIER;
// let stamina_rating =
// StrainSkill::difficulty_value(self.stamina.clone()) * Self::STAMINA_SKILL_MULTIPLIER;
PeaksDifficultyValues {
colour_rating,
rhythm_rating: 0.0,
stamina_rating: 0.0,
// combined_rating: self.difficulty_value(),
combined_rating: 0.0,
}
}
pub(crate) fn into_raw(self) -> PeaksRaw {
PeaksRaw {
colour: self.colour.strain_peaks,
rhythm: self.rhythm.strain_peaks,
stamina: self.stamina.strain_peaks,
}
}
fn norm(p: f64, values: impl IntoIterator<Item = f64>) -> f64 {
values
.into_iter()
.fold(0.0, |sum, x| sum + x.powf(p))
.powf(p.recip())
}
}
impl Skill for Peaks {
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
StrainSkill::process(&mut self.colour, curr, hit_objects);
StrainSkill::process(&mut self.rhythm, curr, hit_objects);
StrainSkill::process(&mut self.stamina, curr, hit_objects);
}
fn difficulty_value(self) -> f64 {
let mut peaks = Vec::new();
let colour_peaks = self.colour.get_curr_strain_peaks();
let rhythm_peaks = self.rhythm.get_curr_strain_peaks();
let stamina_peaks = self.stamina.get_curr_strain_peaks();
let zip = colour_peaks
.into_iter()
.zip(rhythm_peaks)
.zip(stamina_peaks);
for ((mut colour_peak, mut rhythm_peak), mut stamina_peak) in zip {
colour_peak *= Self::COLOUR_SKILL_MULTIPLIER;
rhythm_peak *= Self::RHYTHM_SKILL_MULTIPLIER;
stamina_peak *= Self::STAMINA_SKILL_MULTIPLIER;
let mut peak = Self::norm(1.5, [colour_peak, stamina_peak]);
peak = Self::norm(2.0, [peak, rhythm_peak]);
// * Sections with 0 strain are excluded to avoid worst-case
// * time complexity of the following sort (e.g. /b/2351871).
// * These sections will not contribute to the difficulty.
if peak > 0.0 {
peaks.push(peak);
}
}
let mut difficulty = 0.0;
let mut weight = 1.0;
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for strain in peaks {
difficulty += strain * weight;
weight *= 0.9;
}
difficulty
}
}
pub(crate) struct PeaksDifficultyValues {
pub(crate) colour_rating: f64,
pub(crate) rhythm_rating: f64,
pub(crate) stamina_rating: f64,
pub(crate) combined_rating: f64,
}
pub(crate) struct PeaksRaw {
pub(crate) colour: Vec<f64>,
pub(crate) rhythm: Vec<f64>,
pub(crate) stamina: Vec<f64>,
}
+205
View File
@@ -0,0 +1,205 @@
use crate::{
limited_queue::LimitedQueue,
taiko::difficulty_object::{HitObjectRhythm, ObjectLists, TaikoDifficultyObject},
};
use super::{Skill, StrainDecaySkill, StrainSkill};
const HISTORY_MAX_LEN: usize = 8;
#[derive(Clone, Debug)]
pub(crate) struct Rhythm {
// Equal to osu's CurrentStrain from abstract class StrainDecaySkill
curr_decay_strain: f64,
// Equal to osu's currentStrain from class Rhythm
curr_strain: f64,
notes_since_rhythm_change: usize,
history: LimitedQueue<HistoryElement, HISTORY_MAX_LEN>,
curr_section_peak: f64,
curr_section_end: f64,
pub(crate) strain_peaks: Vec<f64>,
}
impl Rhythm {
const STRAIN_DECAY: f64 = 0.96;
pub(crate) fn new() -> Self {
Self {
curr_decay_strain: 0.0,
curr_strain: 0.0,
notes_since_rhythm_change: 0,
history: LimitedQueue::new(),
curr_section_peak: 0.0,
curr_section_end: 0.0,
strain_peaks: Vec::new(),
}
}
fn reset_rhythm_and_strain(&mut self) {
self.curr_strain = 0.0;
self.notes_since_rhythm_change = 0;
}
fn repetition_penalties(&mut self, hit_object: &TaikoDifficultyObject<'_>) -> f64 {
let mut penalty = 1.0;
self.history.push(HistoryElement::new(hit_object));
for most_recent_patterns_to_compare in 2..=(HISTORY_MAX_LEN / 2).min(self.history.len()) {
for start in (0..self.history.len() - most_recent_patterns_to_compare).rev() {
if !self.same_pattern(start, most_recent_patterns_to_compare) {
continue;
}
let notes_since = hit_object.idx - self.history[start].idx;
penalty *= Self::repetition_penalty(notes_since);
break;
}
}
penalty
}
fn same_pattern(&self, start: usize, most_recent_patterns_to_compare: usize) -> bool {
let start = self.history.iter().skip(start);
let most_recent_patterns_to_compare = self
.history
.iter()
.skip(self.history.len() - most_recent_patterns_to_compare);
start
.zip(most_recent_patterns_to_compare)
.all(|(a, b)| a.rhythm == b.rhythm)
}
fn repetition_penalty(notes_since: usize) -> f64 {
(0.032 * notes_since as f64).min(1.0)
}
fn pattern_len_penalty(pattern_len: usize) -> f64 {
let pattern_len = pattern_len as f64;
let short_pattern_penalty = (0.15 * pattern_len).min(1.0);
let long_pattern_penalty = (2.5 - 0.15 * pattern_len).clamp(0.0, 1.0);
short_pattern_penalty.min(long_pattern_penalty)
}
fn speed_penalty(&mut self, delta: f64) -> f64 {
if delta < 80.0 {
return 1.0;
} else if delta < 210.0 {
return (1.4 - 0.005 * delta).max(0.0);
}
self.reset_rhythm_and_strain();
0.0
}
}
impl Skill for Rhythm {
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
<Self as StrainSkill>::process(self, curr, hit_objects)
}
fn difficulty_value(self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
}
impl StrainSkill for Rhythm {
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
fn strain_value_at(
&mut self,
curr: &TaikoDifficultyObject<'_>,
hit_objects: &ObjectLists<'_>,
) -> f64 {
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
}
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64 {
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
}
}
impl StrainDecaySkill for Rhythm {
const SKILL_MULTIPLIER: f64 = 10.0;
const STRAIN_DECAY_BASE: f64 = 0.0;
fn curr_strain(&self) -> f64 {
self.curr_decay_strain
}
fn curr_strain_mut(&mut self) -> &mut f64 {
&mut self.curr_decay_strain
}
fn strain_value_of(&mut self, curr: &TaikoDifficultyObject<'_>, _: &ObjectLists<'_>) -> f64 {
let base_is_circle = curr.base.h.is_circle();
// * drum rolls and swells are exempt.
if !base_is_circle {
self.reset_rhythm_and_strain();
return 0.0;
}
self.curr_strain *= Self::STRAIN_DECAY;
self.notes_since_rhythm_change += 1;
// * rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
if curr.rhythm.difficulty.abs() <= f64::EPSILON {
return 0.0;
}
let mut obj_strain = curr.rhythm.difficulty;
obj_strain *= self.repetition_penalties(curr);
obj_strain *= Self::pattern_len_penalty(self.notes_since_rhythm_change);
obj_strain *= self.speed_penalty(curr.delta);
// * careful - needs to be done here since calls above read this value
self.notes_since_rhythm_change = 0;
self.curr_strain += obj_strain;
self.curr_strain
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct HistoryElement {
idx: usize,
rhythm: &'static HitObjectRhythm,
}
impl HistoryElement {
fn new(difficulty_object: &TaikoDifficultyObject<'_>) -> Self {
Self {
idx: difficulty_object.idx,
rhythm: difficulty_object.rhythm,
}
}
}
impl Default for HistoryElement {
fn default() -> Self {
Self {
idx: 0,
rhythm: HitObjectRhythm::static_ref(),
}
}
}
+113
View File
@@ -0,0 +1,113 @@
use crate::taiko::difficulty_object::{ObjectLists, TaikoDifficultyObject};
use super::{Skill, StrainDecaySkill, StrainSkill};
#[derive(Clone, Debug)]
pub(crate) struct Stamina {
curr_strain: f64,
curr_section_peak: f64,
curr_section_end: f64,
pub(crate) strain_peaks: Vec<f64>,
}
impl Stamina {
pub(crate) fn new() -> Self {
Self {
curr_strain: 0.0,
curr_section_peak: 0.0,
curr_section_end: 0.0,
strain_peaks: Vec::new(),
}
}
}
impl Skill for Stamina {
fn process(&mut self, curr: &TaikoDifficultyObject<'_>, hit_objects: &ObjectLists<'_>) {
<Self as StrainSkill>::process(self, curr, hit_objects)
}
fn difficulty_value(self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
}
impl StrainSkill for Stamina {
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
fn strain_value_at(
&mut self,
curr: &TaikoDifficultyObject<'_>,
hit_objects: &ObjectLists<'_>,
) -> f64 {
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
}
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject<'_>) -> f64 {
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
}
}
impl StrainDecaySkill for Stamina {
const SKILL_MULTIPLIER: f64 = 1.1;
const STRAIN_DECAY_BASE: f64 = 0.4;
fn curr_strain(&self) -> f64 {
self.curr_strain
}
fn curr_strain_mut(&mut self) -> &mut f64 {
&mut self.curr_strain
}
fn strain_value_of(
&mut self,
curr: &TaikoDifficultyObject<'_>,
hit_objects: &ObjectLists<'_>,
) -> f64 {
StaminaEvaluator::evaluate_diff_of(curr, hit_objects)
}
}
struct StaminaEvaluator;
impl StaminaEvaluator {
fn speed_bonus(mut interval: f64) -> f64 {
// * Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
// * Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
// * TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
interval = interval.max(50.0);
30.0 / interval
}
fn evaluate_diff_of(
hit_object: &TaikoDifficultyObject<'_>,
hit_objects: &ObjectLists<'_>,
) -> f64 {
if !hit_object.base.is_hit() {
return 0.0;
}
// * Find the previous hit object hit by the current key, which is two notes of the same colour prior.
let curr = hit_object;
let key_prev = hit_objects.prev_mono(curr.idx, 1);
if let Some(key_prev) = key_prev {
// * Add a base strain to all objects
0.5 + Self::speed_bonus(curr.base.h.start_time - key_prev.borrow().base.h.start_time)
} else {
// * There is no previous hit object hit by the current key
0.0
}
}
}
+12
View File
@@ -2,12 +2,24 @@ use std::slice::Iter;
use crate::{parse::HitObject, Beatmap};
use super::rim::Rim;
#[derive(Copy, Clone, Debug)]
pub(crate) struct TaikoObject<'h> {
pub(crate) h: &'h HitObject,
pub(crate) sound: u8,
}
impl TaikoObject<'_> {
pub(crate) fn is_rim(&self) -> bool {
self.sound.is_rim()
}
pub(crate) fn is_hit(&self) -> bool {
self.h.is_circle()
}
}
pub(crate) trait IntoTaikoObjectIter {
fn taiko_objects(&self) -> TaikoObjectIter<'_>;
}