mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod osu_object;
mod pp;
mod scaling_factor;
mod skills;
use crate::{curve::CurveBuffers, parse::Pos2, AnyStars, Beatmap, GameMode, Mods};
use self::{
difficulty_object::{Distances, OsuDifficultyObject},
skills::{Skill, Skills},
};
pub use self::{gradual_difficulty::*, gradual_performance::*, osu_object::*, pp::*};
pub(crate) use self::scaling_factor::ScalingFactor;
const SECTION_LEN: f64 = 400.0;
const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
const NORMALIZED_RADIUS: f32 = 50.0;
const STACK_DISTANCE: f32 = 3.0;
const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14;
const PREEMPT_MIN: f64 = 450.0;
const FADE_IN_DURATION_MULTIPLIER: f64 = 0.4;
const PLAYFIELD_BASE_SIZE: Pos2 = Pos2 { x: 512.0, y: 384.0 };
#[derive(Clone, Debug)]
pub struct OsuStars<'map> {
pub(crate) map: &'map Beatmap,
pub(crate) mods: u32,
pub(crate) passed_objects: Option<usize>,
pub(crate) clock_rate: Option<f64>,
}
impl<'map> OsuStars<'map> {
#[inline]
pub fn new(map: &'map Beatmap) -> Self {
Self {
map,
mods: 0,
passed_objects: None,
clock_rate: None,
}
}
#[inline]
pub fn mode(self, mode: GameMode) -> AnyStars<'map> {
match mode {
GameMode::Osu => AnyStars::Osu(self),
GameMode::Taiko => AnyStars::Taiko(self.into()),
GameMode::Catch => AnyStars::Catch(self.into()),
GameMode::Mania => AnyStars::Mania(self.into()),
}
}
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
self
}
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
self
}
#[inline]
pub fn clock_rate(mut self, clock_rate: f64) -> Self {
self.clock_rate = Some(clock_rate);
self
}
#[inline]
pub fn calculate(self) -> OsuDifficultyAttributes {
let mods = self.mods;
let (skills, mut attrs) = calculate_skills(self);
let Skills {
mut aim,
mut aim_no_sliders,
mut speed,
mut flashlight,
} = skills;
let mut aim_rating = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let aim_rating_no_sliders =
aim_no_sliders.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_notes = speed.relevant_note_count();
let mut speed_rating = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let mut flashlight_rating = flashlight.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let slider_factor = if aim_rating > 0.0 {
aim_rating_no_sliders / aim_rating
} else {
1.0
};
if mods.td() {
aim_rating = aim_rating.powf(0.8);
flashlight_rating = flashlight_rating.powf(0.8);
}
if mods.rx() {
aim_rating *= 0.9;
speed_rating = 0.0;
flashlight_rating *= 0.7;
}
let base_aim_performance = (5.0 * (aim_rating / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
let base_speed_performance =
(5.0 * (speed_rating / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
let base_flashlight_performance = if mods.fl() {
flashlight_rating * flashlight_rating * 25.0
} else {
0.0
};
let base_performance = ((base_aim_performance).powf(1.1)
+ (base_speed_performance).powf(1.1)
+ (base_flashlight_performance).powf(1.1))
.powf(1.0 / 1.1);
let star_rating = if base_performance > 0.00001 {
PERFORMANCE_BASE_MULTIPLIER.cbrt()
* 0.027
* ((100_000.0 / 2.0_f64.powf(1.0 / 1.1) * base_performance).cbrt() + 4.0)
} else {
0.0
};
attrs.aim = aim_rating;
attrs.speed = speed_rating;
attrs.flashlight = flashlight_rating;
attrs.slider_factor = slider_factor;
attrs.stars = star_rating;
attrs.speed_note_count = speed_notes;
attrs
}
#[inline]
pub fn strains(self) -> OsuStrains {
let (skills, _) = calculate_skills(self);
let Skills {
aim,
aim_no_sliders,
speed,
flashlight,
} = skills;
OsuStrains {
section_len: SECTION_LEN,
aim: aim.strain_peaks,
aim_no_sliders: aim_no_sliders.strain_peaks,
speed: speed.strain_peaks,
flashlight: flashlight.strain_peaks,
}
}
}
#[derive(Clone, Debug)]
pub struct OsuStrains {
pub section_len: f64, pub aim: Vec<f64>,
pub aim_no_sliders: Vec<f64>,
pub speed: Vec<f64>,
pub flashlight: Vec<f64>,
}
impl OsuStrains {
#[inline]
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.aim.len()
}
}
fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
let OsuStars {
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());
let map_attrs = map.attributes().mods(mods).clock_rate(clock_rate).build();
let scaling_factor = ScalingFactor::new(map_attrs.cs);
let hr = mods.hr();
let hit_window = 2.0 * map_attrs.hit_windows.od;
let time_preempt = (map_attrs.hit_windows.ar * clock_rate) as f32 as f64;
let time_fade_in = if mods.hd() {
time_preempt * FADE_IN_DURATION_MULTIPLIER
} else {
400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
};
let mut attrs = OsuDifficultyAttributes {
ar: map_attrs.ar,
hp: map_attrs.hp,
od: map_attrs.od,
..Default::default()
};
let mut hit_objects =
create_osu_objects(map, &mut attrs, &scaling_factor, take, hr, time_preempt);
let mut hit_objects_iter = hit_objects.iter_mut();
let mut skills = Skills::new(
mods,
scaling_factor.radius,
time_preempt,
time_fade_in,
hit_window,
);
let last = match hit_objects_iter.next() {
Some(prev) => prev,
None => return (skills, attrs),
};
let mut last_last = None;
Distances::compute_slider_cursor_pos(last, &scaling_factor);
let mut last = &*last;
let mut diff_objects = Vec::with_capacity(hit_objects_iter.len());
for (i, curr) in hit_objects_iter.enumerate() {
let delta_time = (curr.start_time - last.start_time) / clock_rate;
let strain_time = delta_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
let dists = Distances::new(
curr,
last,
last_last,
clock_rate,
strain_time,
&scaling_factor,
);
let diff_obj = OsuDifficultyObject::new(curr, last, clock_rate, i, dists);
diff_objects.push(diff_obj);
last_last = Some(last);
last = &*curr;
}
for curr in diff_objects.iter() {
skills.process(curr, &diff_objects);
}
(skills, attrs)
}
pub(crate) fn create_osu_objects(
map: &Beatmap,
attrs: &mut OsuDifficultyAttributes,
scaling_factor: &ScalingFactor,
take: usize,
hr: bool,
time_preempt: f64,
) -> Vec<OsuObject> {
let mut params = ObjectParameters {
map,
attrs,
ticks: Vec::new(),
curve_bufs: CurveBuffers::default(),
};
let mut hit_objects: Vec<_> = map
.hit_objects
.iter()
.take(take)
.map(|h| OsuObject::new(h, &mut params))
.collect();
let stack_threshold = time_preempt * map.stack_leniency as f64;
if map.version >= 6 {
stacking(&mut hit_objects, stack_threshold);
} else {
old_stacking(&mut hit_objects, stack_threshold);
}
hit_objects
.iter_mut()
.for_each(|h| h.post_process(hr, scaling_factor));
hit_objects
}
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
let mut extended_start_idx = 0;
let extended_end_idx = match hit_objects.len().checked_sub(1) {
Some(idx) => idx,
None => return,
};
for i in (1..=extended_end_idx).rev() {
let mut n = i;
let mut obj_i_idx = i;
if hit_objects[obj_i_idx].stack_height.abs() > 0.0 || hit_objects[obj_i_idx].is_spinner() {
continue;
}
if hit_objects[obj_i_idx].is_circle() {
loop {
n = match n.checked_sub(1) {
Some(n) => n,
None => break,
};
if hit_objects[n].is_spinner() {
continue;
} else if hit_objects[obj_i_idx].start_time - hit_objects[n].end_time()
> stack_threshold
{
break; }
if n < extended_start_idx {
hit_objects[n].stack_height = 0.0;
extended_start_idx = n;
}
if hit_objects[n].is_slider()
&& hit_objects[n]
.pre_stacked_end_pos()
.distance(hit_objects[obj_i_idx].pos())
< STACK_DISTANCE
{
let offset =
hit_objects[obj_i_idx].stack_height - hit_objects[n].stack_height + 1.0;
for j in n + 1..=i {
if hit_objects[n]
.pre_stacked_end_pos()
.distance(hit_objects[j].pos())
< STACK_DISTANCE
{
hit_objects[j].stack_height -= offset;
}
}
break;
}
if hit_objects[n].pos().distance(hit_objects[obj_i_idx].pos()) < STACK_DISTANCE {
hit_objects[n].stack_height = hit_objects[obj_i_idx].stack_height + 1.0;
obj_i_idx = n;
}
}
} else if hit_objects[obj_i_idx].is_slider() {
loop {
n = match n.checked_sub(1) {
Some(n) => n,
None => break,
};
if hit_objects[n].is_spinner() {
continue;
}
if hit_objects[obj_i_idx].start_time - hit_objects[n].start_time > stack_threshold {
break; }
if hit_objects[n]
.pre_stacked_end_pos()
.distance(hit_objects[obj_i_idx].pos())
< STACK_DISTANCE
{
hit_objects[n].stack_height = hit_objects[obj_i_idx].stack_height + 1.0;
obj_i_idx = n;
}
}
}
}
}
fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
for i in 0..hit_objects.len() {
if hit_objects[i].stack_height != 0.0 && !hit_objects[i].is_slider() {
continue;
}
let mut start_time = hit_objects[i].end_time();
let pos2 = hit_objects[i].old_stacking_pos2();
let mut slider_stack = 0.0;
for j in i + 1..hit_objects.len() {
if hit_objects[j].start_time - stack_threshold > start_time {
break;
}
if hit_objects[j].pos().distance(hit_objects[i].pos()) < STACK_DISTANCE {
hit_objects[i].stack_height += 1.0;
start_time = hit_objects[j].end_time();
} else if hit_objects[j].pos().distance(pos2) < STACK_DISTANCE {
slider_stack += 1.0;
hit_objects[j].stack_height -= slider_stack;
start_time = hit_objects[j].end_time();
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct OsuDifficultyAttributes {
pub aim: f64,
pub speed: f64,
pub flashlight: f64,
pub slider_factor: f64,
pub speed_note_count: f64,
pub ar: f64,
pub od: f64,
pub hp: f64,
pub n_circles: usize,
pub n_sliders: usize,
pub n_spinners: usize,
pub stars: f64,
pub max_combo: usize,
}
impl OsuDifficultyAttributes {
#[inline]
pub fn max_combo(&self) -> usize {
self.max_combo
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct OsuPerformanceAttributes {
pub difficulty: OsuDifficultyAttributes,
pub pp: f64,
pub pp_acc: f64,
pub pp_aim: f64,
pub pp_flashlight: f64,
pub pp_speed: f64,
pub effective_miss_count: f64,
}
impl OsuPerformanceAttributes {
#[inline]
pub fn stars(&self) -> f64 {
self.difficulty.stars
}
#[inline]
pub fn pp(&self) -> f64 {
self.pp
}
#[inline]
pub fn max_combo(&self) -> usize {
self.difficulty.max_combo
}
}
impl From<OsuPerformanceAttributes> for OsuDifficultyAttributes {
#[inline]
fn from(attributes: OsuPerformanceAttributes) -> Self {
attributes.difficulty
}
}