Merge branch 'all_included' of https://github.com/MaxOhn/rosu-pp into all_included
This commit is contained in:
@@ -422,6 +422,3 @@ compile_error!("Only one of the features `no_leniency`, `no_sliders_no_leniency`
|
|||||||
)
|
)
|
||||||
))]
|
))]
|
||||||
compile_error!("The features `no_leniency`, `no_sliders_no_leniency`, and `all_included` should only be enabled in combination with the `osu` feature");
|
compile_error!("The features `no_leniency`, `no_sliders_no_leniency`, and `all_included` should only be enabled in combination with the `osu` feature");
|
||||||
|
|
||||||
#[cfg(feature = "all_included")]
|
|
||||||
compile_error!("The `all_included` version is still WIP and unavailable, use either the `no_leniency` or `no_sliders_no_leniency` feature");
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use super::OsuObject;
|
use super::OsuObject;
|
||||||
|
|
||||||
const NORMALIZED_RADIUS: f32 = 52.0;
|
pub(crate) struct DifficultyObject<'h> {
|
||||||
|
pub(crate) base: &'h OsuObject,
|
||||||
pub(crate) struct DifficultyObject {
|
|
||||||
pub(crate) base: OsuObject,
|
|
||||||
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
|
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
|
||||||
|
|
||||||
pub(crate) jump_dist: f32,
|
pub(crate) jump_dist: f32,
|
||||||
@@ -14,40 +12,33 @@ pub(crate) struct DifficultyObject {
|
|||||||
pub(crate) strain_time: f32,
|
pub(crate) strain_time: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DifficultyObject {
|
impl<'h> DifficultyObject<'h> {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
base: OsuObject,
|
base: &'h OsuObject,
|
||||||
prev: OsuObject,
|
prev: &OsuObject,
|
||||||
prev_diff: Option<DifficultyObject>,
|
prev_vals: Option<(f32, f32)>, // (jump_dist, strain_time)
|
||||||
prev_prev: Option<OsuObject>,
|
prev_prev: Option<OsuObject>,
|
||||||
clock_rate: f32,
|
clock_rate: f32,
|
||||||
|
scaling_factor: f32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let delta = (base.time() - prev.time()) / clock_rate;
|
let delta = (base.time - prev.time) / clock_rate;
|
||||||
let strain_time = delta.max(50.0);
|
let strain_time = delta.max(50.0);
|
||||||
|
|
||||||
let radius = base.radius();
|
let pos = base.pos;
|
||||||
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
|
||||||
|
|
||||||
if radius < 30.0 {
|
|
||||||
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
|
||||||
scaling_factor *= 1.0 + small_circle_bonus;
|
|
||||||
}
|
|
||||||
|
|
||||||
let travel_dist = prev.travel_dist();
|
let travel_dist = prev.travel_dist();
|
||||||
let prev_cursor_pos = prev.cursor_end_position();
|
let prev_cursor_pos = prev.end_pos();
|
||||||
|
|
||||||
// println!("travel_dist={} | prev_cursor_pos={:?}", travel_dist, prev_cursor_pos);
|
let jump_dist = if base.is_spinner() {
|
||||||
|
0.0
|
||||||
let jump_dist = match base {
|
} else {
|
||||||
OsuObject::Spinner { .. } => 0.0,
|
((pos - prev_cursor_pos) * scaling_factor).length()
|
||||||
_ => (base.stacked_pos() * scaling_factor - prev_cursor_pos * scaling_factor).length(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let angle = prev_prev.map(|prev_prev| {
|
let angle = prev_prev.map(|prev_prev| {
|
||||||
let prev_prev_cursor_pos = prev_prev.cursor_end_position();
|
let prev_prev_cursor_pos = prev_prev.end_pos();
|
||||||
|
|
||||||
let v1 = prev_prev_cursor_pos - prev.stacked_pos();
|
let v1 = prev_prev_cursor_pos - prev_cursor_pos;
|
||||||
let v2 = base.stacked_pos() - prev_cursor_pos;
|
let v2 = pos - prev_cursor_pos;
|
||||||
|
|
||||||
let dot = v1.dot(v2);
|
let dot = v1.dot(v2);
|
||||||
let det = v1.x * v2.y - v1.y * v2.x;
|
let det = v1.x * v2.y - v1.y * v2.x;
|
||||||
@@ -55,11 +46,9 @@ impl DifficultyObject {
|
|||||||
det.atan2(dot).abs()
|
det.atan2(dot).abs()
|
||||||
});
|
});
|
||||||
|
|
||||||
let prev = prev_diff.map(|o| (o.jump_dist, o.strain_time));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base,
|
base,
|
||||||
prev,
|
prev: prev_vals,
|
||||||
|
|
||||||
jump_dist,
|
jump_dist,
|
||||||
travel_dist,
|
travel_dist,
|
||||||
|
|||||||
@@ -1,153 +1,424 @@
|
|||||||
|
//! Every aspect of osu!'s pp calculation is being used.
|
||||||
|
//! This should result in the most accurate values but with
|
||||||
|
//! drawback of being slower than the other versions.
|
||||||
|
|
||||||
#![cfg(feature = "all_included")]
|
#![cfg(feature = "all_included")]
|
||||||
#![allow(unused)]
|
|
||||||
|
use super::super::DifficultyAttributes;
|
||||||
|
use crate::Pos2;
|
||||||
|
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
mod osu_object;
|
mod osu_object;
|
||||||
mod skill;
|
mod skill;
|
||||||
mod skill_kind;
|
mod skill_kind;
|
||||||
|
mod slider_state;
|
||||||
|
|
||||||
use difficulty_object::DifficultyObject;
|
use difficulty_object::DifficultyObject;
|
||||||
use osu_object::OsuObject;
|
use osu_object::OsuObject;
|
||||||
use skill::Skill;
|
use skill::Skill;
|
||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use super::super::DifficultyAttributes;
|
|
||||||
use crate::{Beatmap, Mods, StarResult, Strains};
|
use crate::{Beatmap, Mods, StarResult, Strains};
|
||||||
|
|
||||||
|
const OBJECT_RADIUS: f32 = 64.0;
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f32 = 400.0;
|
||||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||||
|
const NORMALIZED_RADIUS: f32 = 52.0;
|
||||||
|
const TIME_PREEMPT: f32 = 600.0; // TODO: Check mod influences
|
||||||
|
const STACK_DISTANCE: f32 = 3.0;
|
||||||
|
|
||||||
/// Star calculation for osu!standard maps
|
/// Star calculation for osu!standard maps.
|
||||||
|
///
|
||||||
|
/// Both slider paths and stack leniency are considered.
|
||||||
|
/// Since taking stack leniency into account is fairly expensive,
|
||||||
|
/// this version is slower than the others but in turn gives the
|
||||||
|
/// most precise results.
|
||||||
///
|
///
|
||||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||||
pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> StarResult {
|
pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> StarResult {
|
||||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
let attributes = map.attributes().mods(mods);
|
let map_attributes = map.attributes().mods(mods);
|
||||||
let hitwindow = super::difficulty_range(attributes.od).floor() / attributes.clock_rate;
|
let hitwindow = super::difficulty_range(map_attributes.od).floor() / map_attributes.clock_rate;
|
||||||
let od = (80.0 - hitwindow) / 6.0;
|
let od = (80.0 - hitwindow) / 6.0;
|
||||||
|
|
||||||
|
let mut diff_attributes = DifficultyAttributes {
|
||||||
|
ar: map_attributes.ar,
|
||||||
|
od,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
if take < 2 {
|
if take < 2 {
|
||||||
return StarResult::Osu(DifficultyAttributes {
|
return StarResult::Osu(diff_attributes);
|
||||||
ar: attributes.ar,
|
|
||||||
od,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let section_len = SECTION_LEN * attributes.clock_rate;
|
let section_len = SECTION_LEN * map_attributes.clock_rate;
|
||||||
|
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||||
|
let radius = OBJECT_RADIUS * scale;
|
||||||
|
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||||
|
|
||||||
let mut hit_objects = map
|
if radius < 30.0 {
|
||||||
|
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
||||||
|
scaling_factor *= 1.0 + small_circle_bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut slider_state = SliderState::new(map);
|
||||||
|
let mut ticks_buf = Vec::new();
|
||||||
|
|
||||||
|
let mut hit_objects: Vec<_> = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.take(take)
|
.take(take)
|
||||||
.filter_map(|h| OsuObject::new(h, map, &attributes));
|
.filter_map(|h| {
|
||||||
|
OsuObject::new(
|
||||||
|
h,
|
||||||
|
map,
|
||||||
|
radius,
|
||||||
|
&mut ticks_buf,
|
||||||
|
&mut diff_attributes,
|
||||||
|
&mut slider_state,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut skills = vec![Skill::new(SkillKind::Aim), Skill::new(SkillKind::Speed)];
|
if map.version >= 6 {
|
||||||
|
stacking(&mut hit_objects, map.stack_leniency, TIME_PREEMPT);
|
||||||
|
} else {
|
||||||
|
old_stacking(&mut hit_objects, map.stack_leniency, TIME_PREEMPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||||
|
let stack_offset = h.stack_height * scale * -6.4;
|
||||||
|
|
||||||
|
h.pos += Pos2 {
|
||||||
|
x: stack_offset,
|
||||||
|
y: stack_offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
h
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut aim = Skill::new(SkillKind::Aim);
|
||||||
|
let mut speed = Skill::new(SkillKind::Speed);
|
||||||
|
|
||||||
|
// First object has no predecessor and thus no strain, handle distinctly
|
||||||
let mut current_section_end =
|
let mut current_section_end =
|
||||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
let mut prev_prev = None;
|
let mut prev_prev = None;
|
||||||
let mut prev = hit_objects.next().unwrap();
|
let mut prev = hit_objects.next().unwrap();
|
||||||
let mut prev_diff = None;
|
let mut prev_vals = None;
|
||||||
|
|
||||||
let mut _i = 0;
|
// Handle second object separately to remove later if-branching
|
||||||
|
let curr = hit_objects.next().unwrap();
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
prev_vals,
|
||||||
|
prev_prev,
|
||||||
|
map_attributes.clock_rate,
|
||||||
|
scaling_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.process(&h);
|
||||||
|
speed.process(&h);
|
||||||
|
|
||||||
|
prev_prev = Some(prev);
|
||||||
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
|
prev = curr;
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
for curr in hit_objects {
|
for curr in hit_objects {
|
||||||
let h = DifficultyObject::new(
|
let h = DifficultyObject::new(
|
||||||
curr.clone(),
|
&curr,
|
||||||
prev.clone(),
|
&prev,
|
||||||
prev_diff,
|
prev_vals,
|
||||||
prev_prev,
|
prev_prev,
|
||||||
attributes.clock_rate,
|
map_attributes.clock_rate,
|
||||||
|
scaling_factor,
|
||||||
);
|
);
|
||||||
|
|
||||||
// println!(
|
while h.base.time > current_section_end {
|
||||||
// "strain_time={} | travel_dist={} | jump_dist={} | angle={:?}",
|
aim.save_current_peak();
|
||||||
// h.strain_time, h.travel_dist, h.jump_dist, h.angle
|
aim.start_new_section_from(current_section_end);
|
||||||
// );
|
speed.save_current_peak();
|
||||||
|
speed.start_new_section_from(current_section_end);
|
||||||
// println!("[{}] time={}", _i, curr.time());
|
|
||||||
|
|
||||||
while h.base.time() > current_section_end {
|
|
||||||
for skill in skills.iter_mut() {
|
|
||||||
skill.save_current_peak();
|
|
||||||
skill.start_new_section_from(current_section_end);
|
|
||||||
|
|
||||||
_i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
current_section_end += section_len;
|
current_section_end += section_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
for skill in skills.iter_mut() {
|
aim.process(&h);
|
||||||
skill.process(&h);
|
speed.process(&h);
|
||||||
}
|
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
prev_prev = Some(prev);
|
||||||
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
prev = curr;
|
prev = curr;
|
||||||
prev_diff = Some(h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for skill in skills.iter_mut() {
|
aim.save_current_peak();
|
||||||
skill.save_current_peak();
|
speed.save_current_peak();
|
||||||
}
|
|
||||||
|
|
||||||
// println!("Aim:");
|
let aim_strain = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
// for (i, strain) in skills[0].strain_peaks.iter().enumerate() {
|
let speed_strain = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
// println!("{}: {}", i, strain);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// println!("Speed:");
|
let stars = aim_strain + speed_strain + (aim_strain - speed_strain).abs() / 2.0;
|
||||||
// for (i, strain) in skills[1].strain_peaks.iter().enumerate() {
|
|
||||||
// println!("{}: {}", i, strain);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// println!("Aim: {:?}", skills[0].strain_peaks);
|
diff_attributes.stars = stars;
|
||||||
// println!("Speed: {:?}", skills[1].strain_peaks);
|
diff_attributes.speed_strain = speed_strain;
|
||||||
|
diff_attributes.aim_strain = aim_strain;
|
||||||
|
|
||||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
StarResult::Osu(diff_attributes)
|
||||||
// println!("After:\n{:?}", skills[0].strain_peaks);
|
|
||||||
|
|
||||||
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
|
||||||
// println!("After:\n{:?}", skills[1].strain_peaks);
|
|
||||||
|
|
||||||
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
|
|
||||||
|
|
||||||
let attributes = DifficultyAttributes {
|
|
||||||
stars,
|
|
||||||
ar: attributes.ar,
|
|
||||||
od,
|
|
||||||
speed_strain: speed_rating,
|
|
||||||
aim_strain: aim_rating,
|
|
||||||
max_combo: 0, // TODO
|
|
||||||
n_circles: 0, // TODO
|
|
||||||
n_spinners: 0, // TODO
|
|
||||||
};
|
|
||||||
|
|
||||||
StarResult::Osu(attributes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the `stars` function but instead of
|
/// Essentially the same as the `stars` function but instead of
|
||||||
/// evaluating the final strains, it just returns them as is.
|
/// evaluating the final strains, it just returns them as is.
|
||||||
///
|
///
|
||||||
/// Suitable to plot the difficulty of a map over time.
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
pub fn strains(_map: &Beatmap, _mods: impl Mods) -> Strains {
|
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||||
todo!()
|
let map_attributes = map.attributes().mods(mods);
|
||||||
|
let hitwindow = super::difficulty_range(map_attributes.od).floor() / map_attributes.clock_rate;
|
||||||
|
let od = (80.0 - hitwindow) / 6.0;
|
||||||
|
|
||||||
|
let mut diff_attributes = DifficultyAttributes {
|
||||||
|
ar: map_attributes.ar,
|
||||||
|
od,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if map.hit_objects.len() < 2 {
|
||||||
|
return Strains::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let section_len = SECTION_LEN * map_attributes.clock_rate;
|
||||||
|
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||||
|
let radius = OBJECT_RADIUS * scale;
|
||||||
|
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||||
|
|
||||||
|
if radius < 30.0 {
|
||||||
|
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
||||||
|
scaling_factor *= 1.0 + small_circle_bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut slider_state = SliderState::new(map);
|
||||||
|
let mut ticks_buf = Vec::new();
|
||||||
|
|
||||||
|
let mut hit_objects: Vec<_> = map
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.filter_map(|h| {
|
||||||
|
OsuObject::new(
|
||||||
|
h,
|
||||||
|
map,
|
||||||
|
radius,
|
||||||
|
&mut ticks_buf,
|
||||||
|
&mut diff_attributes,
|
||||||
|
&mut slider_state,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if map.version >= 6 {
|
||||||
|
stacking(&mut hit_objects, map.stack_leniency, TIME_PREEMPT);
|
||||||
|
} else {
|
||||||
|
old_stacking(&mut hit_objects, map.stack_leniency, TIME_PREEMPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||||
|
let stack_offset = h.stack_height * scale * -6.4;
|
||||||
|
|
||||||
|
h.pos += Pos2 {
|
||||||
|
x: stack_offset,
|
||||||
|
y: stack_offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
h
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut aim = Skill::new(SkillKind::Aim);
|
||||||
|
let mut speed = Skill::new(SkillKind::Speed);
|
||||||
|
|
||||||
|
// First object has no predecessor and thus no strain, handle distinctly
|
||||||
|
let mut current_section_end =
|
||||||
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
|
let mut prev_prev = None;
|
||||||
|
let mut prev = hit_objects.next().unwrap();
|
||||||
|
let mut prev_vals = None;
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let curr = hit_objects.next().unwrap();
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
prev_vals,
|
||||||
|
prev_prev,
|
||||||
|
map_attributes.clock_rate,
|
||||||
|
scaling_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.process(&h);
|
||||||
|
speed.process(&h);
|
||||||
|
|
||||||
|
prev_prev = Some(prev);
|
||||||
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
|
prev = curr;
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
|
for curr in hit_objects {
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
prev_vals,
|
||||||
|
prev_prev,
|
||||||
|
map_attributes.clock_rate,
|
||||||
|
scaling_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
aim.save_current_peak();
|
||||||
|
aim.start_new_section_from(current_section_end);
|
||||||
|
speed.save_current_peak();
|
||||||
|
speed.start_new_section_from(current_section_end);
|
||||||
|
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.process(&h);
|
||||||
|
speed.process(&h);
|
||||||
|
|
||||||
|
prev_prev = Some(prev);
|
||||||
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
|
prev = curr;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.save_current_peak();
|
||||||
|
speed.save_current_peak();
|
||||||
|
|
||||||
|
let strains = aim
|
||||||
|
.strain_peaks
|
||||||
|
.into_iter()
|
||||||
|
.zip(speed.strain_peaks.into_iter())
|
||||||
|
.map(|(aim, speed)| aim + speed)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Strains {
|
||||||
|
section_length: section_len,
|
||||||
|
strains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stacking(hit_objects: &mut [OsuObject], stack_leniency: f32, time_preempt: f32) {
|
||||||
|
let mut extended_start_idx = 0;
|
||||||
|
let extended_end_idx = hit_objects.len() - 1;
|
||||||
|
|
||||||
|
let stack_threshold = time_preempt * stack_leniency; // TODO: Calculate outside?
|
||||||
|
|
||||||
|
for mut i in (1..=extended_end_idx).rev() {
|
||||||
|
let mut n = i;
|
||||||
|
|
||||||
|
if hit_objects[i].stack_height != 0.0 || !hit_objects[i].is_slider() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hit_objects[i].is_circle() {
|
||||||
|
loop {
|
||||||
|
n = match n.checked_sub(1) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
if hit_objects[n].is_spinner() {
|
||||||
|
break;
|
||||||
|
} else 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].end_pos().distance(&hit_objects[i].pos) < STACK_DISTANCE
|
||||||
|
{
|
||||||
|
let offset = hit_objects[i].stack_height - hit_objects[n].stack_height + 1.0;
|
||||||
|
|
||||||
|
for j in n + 1..=i {
|
||||||
|
if hit_objects[n].pos.distance(&hit_objects[j].pos) < STACK_DISTANCE {
|
||||||
|
hit_objects[j].stack_height -= offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else if hit_objects[n].pos.distance(&hit_objects[i].pos) < STACK_DISTANCE {
|
||||||
|
hit_objects[n].stack_height = hit_objects[i].stack_height + 1.0;
|
||||||
|
i = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if hit_objects[i].is_slider() {
|
||||||
|
loop {
|
||||||
|
n = match n.checked_sub(1) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
if hit_objects[n].is_spinner() {
|
||||||
|
continue;
|
||||||
|
} else if hit_objects[i].time - hit_objects[n].time > stack_threshold {
|
||||||
|
break;
|
||||||
|
} else if hit_objects[n].end_pos().distance(&hit_objects[i].pos) < STACK_DISTANCE {
|
||||||
|
hit_objects[n].stack_height = hit_objects[i].stack_height + 1.0;
|
||||||
|
i = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn old_stacking(hit_objects: &mut [OsuObject], stack_leniency: f32, time_preempt: f32) {
|
||||||
|
let stack_threshold = time_preempt * stack_leniency; // TODO: Calculate outside?
|
||||||
|
|
||||||
|
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 end_pos = hit_objects[i].end_pos(); // FIXME: Wrong for sliders? Position at 100% progress.
|
||||||
|
let mut slider_stack = 0.0;
|
||||||
|
|
||||||
|
for j in i + 1..hit_objects.len() {
|
||||||
|
if hit_objects[j].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(&end_pos) < STACK_DISTANCE {
|
||||||
|
slider_stack += 1.0;
|
||||||
|
hit_objects[j].stack_height -= slider_stack;
|
||||||
|
start_time = hit_objects[j].end_time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::super::OsuPP;
|
use super::super::super::OsuPP;
|
||||||
use super::stars;
|
|
||||||
use crate::Beatmap;
|
use crate::Beatmap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn all_included_single() {
|
fn all_included_single() {
|
||||||
let file = match File::open("./maps/70090.osu") {
|
let file = match File::open("./maps/295.osu") {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(why) => panic!("Could not open file: {}", why),
|
Err(why) => panic!("Could not open file: {}", why),
|
||||||
};
|
};
|
||||||
@@ -157,7 +428,7 @@ mod tests {
|
|||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = OsuPP::new(&map).mods(64).calculate();
|
let result = OsuPP::new(&map).mods(0).calculate();
|
||||||
|
|
||||||
println!("Stars: {}", result.stars());
|
println!("Stars: {}", result.stars());
|
||||||
println!("PP: {}", result.pp());
|
println!("PP: {}", result.pp());
|
||||||
|
|||||||
@@ -1,43 +1,26 @@
|
|||||||
#![allow(unused)]
|
use super::super::super::DifficultyAttributes;
|
||||||
|
use super::slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{curve::Curve, Beatmap, BeatmapAttributes, HitObject, HitObjectKind, PathType, Pos2};
|
use crate::{curve::Curve, Beatmap, HitObject, HitObjectKind, PathType, Pos2};
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
macro_rules! binary_search {
|
|
||||||
($slice:expr, $target:expr) => {
|
|
||||||
$slice.binary_search_by(|p| p.time.partial_cmp(&$target).unwrap_or(Ordering::Equal))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const OBJECT_RADIUS: f32 = 64.0;
|
|
||||||
const STACK_DIST: f32 = 3.0;
|
|
||||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||||
|
|
||||||
#[derive(Clone)] // TODO: Remove clone
|
pub(crate) struct OsuObject {
|
||||||
pub(crate) enum OsuObject {
|
pub(crate) time: f32,
|
||||||
Circle {
|
pub(crate) pos: Pos2,
|
||||||
pos: Pos2,
|
pub(crate) stack_height: f32,
|
||||||
time: f32,
|
kind: OsuObjectKind,
|
||||||
|
}
|
||||||
|
|
||||||
scale: f32,
|
enum OsuObjectKind {
|
||||||
stack_height: f32,
|
Circle,
|
||||||
},
|
|
||||||
Slider {
|
Slider {
|
||||||
pixel_len: f32, // TODO: Redundant?
|
end_time: f32,
|
||||||
repeats: usize, // TODO: Redundant?
|
end_pos: Pos2,
|
||||||
|
travel_dist: f32,
|
||||||
objects: Vec<SliderTick>,
|
|
||||||
|
|
||||||
scale: f32,
|
|
||||||
stack_height: f32,
|
|
||||||
|
|
||||||
cursor_end_pos: Pos2,
|
|
||||||
cursor_travel_dist: f32,
|
|
||||||
},
|
},
|
||||||
Spinner {
|
Spinner {
|
||||||
pos: Pos2,
|
end_time: f32,
|
||||||
time: f32,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,74 +28,52 @@ impl OsuObject {
|
|||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
h: &HitObject,
|
h: &HitObject,
|
||||||
map: &Beatmap,
|
map: &Beatmap,
|
||||||
attributes: &BeatmapAttributes,
|
radius: f32,
|
||||||
|
ticks: &mut Vec<f32>,
|
||||||
|
attributes: &mut DifficultyAttributes,
|
||||||
|
slider_state: &mut SliderState,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let pos = h.pos;
|
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
||||||
let time = h.start_time;
|
let stack_height = 0.0;
|
||||||
|
|
||||||
let scale = (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
|
|
||||||
let mut stack_height = 0.0;
|
|
||||||
|
|
||||||
let obj = match &h.kind {
|
let obj = match &h.kind {
|
||||||
HitObjectKind::Circle => Self::Circle {
|
HitObjectKind::Circle => {
|
||||||
pos,
|
attributes.n_circles += 1;
|
||||||
time,
|
|
||||||
scale,
|
Self {
|
||||||
stack_height,
|
time: h.start_time,
|
||||||
},
|
pos: h.pos,
|
||||||
|
stack_height,
|
||||||
|
kind: OsuObjectKind::Circle,
|
||||||
|
}
|
||||||
|
}
|
||||||
HitObjectKind::Slider {
|
HitObjectKind::Slider {
|
||||||
pixel_len,
|
pixel_len,
|
||||||
repeats,
|
repeats,
|
||||||
curve_points,
|
curve_points,
|
||||||
path_type,
|
path_type,
|
||||||
} => {
|
} => {
|
||||||
let (beat_len, timing_time) = {
|
// Key values which are computed here
|
||||||
match binary_search!(map.timing_points, time) {
|
let mut end_pos = h.pos;
|
||||||
Ok(idx) => {
|
let mut travel_dist = 0.0;
|
||||||
let point = &map.timing_points[idx];
|
|
||||||
(point.beat_len, point.time)
|
|
||||||
}
|
|
||||||
Err(0) => (1000.0, 0.0),
|
|
||||||
Err(idx) => {
|
|
||||||
let point = &map.timing_points[idx - 1];
|
|
||||||
(point.beat_len, point.time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (speed_multiplier, diff_time) = {
|
// Responsible for timing point values
|
||||||
match binary_search!(map.difficulty_points, time) {
|
slider_state.update(h.start_time);
|
||||||
Ok(idx) => {
|
|
||||||
let point = &map.difficulty_points[idx];
|
|
||||||
(point.speed_multiplier, point.time)
|
|
||||||
}
|
|
||||||
Err(0) => (1.0, 0.0),
|
|
||||||
Err(idx) => {
|
|
||||||
let point = &map.difficulty_points[idx - 1];
|
|
||||||
(point.speed_multiplier, point.time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let approx_follow_circle_radius = radius * 3.0;
|
||||||
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
|
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
|
||||||
|
|
||||||
if map.version >= 8 {
|
if map.version >= 8 {
|
||||||
tick_distance /= (100.0 / speed_multiplier).max(10.0).min(1000.0) / 100.0;
|
tick_distance /=
|
||||||
|
(100.0 / slider_state.speed_mult).max(10.0).min(1000.0) / 100.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let spm = if timing_time > diff_time {
|
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
|
||||||
1.0
|
/ (map.sv * slider_state.speed_mult)
|
||||||
} else {
|
/ 100.0;
|
||||||
speed_multiplier
|
let span_duration = duration / *repeats as f32;
|
||||||
};
|
|
||||||
|
|
||||||
let duration = *repeats as f32 * beat_len * pixel_len / (map.sv * spm) / 100.0;
|
|
||||||
|
|
||||||
// let velocity = *pixel_len as f32 / duration;
|
|
||||||
|
|
||||||
// println!("duration={}", duration);
|
|
||||||
// println!("velocity={}", velocity);
|
|
||||||
|
|
||||||
|
// Ensure path type validity
|
||||||
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
|
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
|
||||||
PathType::Bezier
|
PathType::Bezier
|
||||||
} else if curve_points.len() == 2 {
|
} else if curve_points.len() == 2 {
|
||||||
@@ -121,6 +82,7 @@ impl OsuObject {
|
|||||||
*path_type
|
*path_type
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build the curve w.r.t. the curve points
|
||||||
let curve = match path_type {
|
let curve = match path_type {
|
||||||
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
|
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
|
||||||
PathType::Bezier => Curve::bezier(&curve_points),
|
PathType::Bezier => Curve::bezier(&curve_points),
|
||||||
@@ -128,122 +90,13 @@ impl OsuObject {
|
|||||||
PathType::PerfectCurve => Curve::perfect(&curve_points),
|
PathType::PerfectCurve => Curve::perfect(&curve_points),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut current_distance = tick_distance;
|
// Called on each slider object except for the head.
|
||||||
let time_add = duration * (tick_distance / (pixel_len * *repeats as f32));
|
// Increases combo and adjusts `end_pos` and `travel_dist`
|
||||||
|
// w.r.t. the object position at the given time on the slider curve.
|
||||||
|
let mut compute_vertex = |time: f32| {
|
||||||
|
attributes.max_combo += 1;
|
||||||
|
|
||||||
let target = pixel_len - tick_distance / 8.0;
|
let mut progress = (time - h.start_time) / span_duration;
|
||||||
let mut ticks = Vec::with_capacity((target / tick_distance) as usize);
|
|
||||||
|
|
||||||
while current_distance < target {
|
|
||||||
let pos = curve.point_at_distance(current_distance);
|
|
||||||
let time = h.start_time + time_add * (ticks.len() + 1) as f32;
|
|
||||||
ticks.push(SliderTick::new(pos, time));
|
|
||||||
current_distance += tick_distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
|
|
||||||
slider_objects.push(SliderTick::new(h.pos, h.start_time));
|
|
||||||
|
|
||||||
if *repeats <= 1 {
|
|
||||||
slider_objects.append(&mut ticks);
|
|
||||||
} else {
|
|
||||||
slider_objects.append(&mut ticks.clone());
|
|
||||||
|
|
||||||
for repeat_id in 1..repeats - 1 {
|
|
||||||
let dist = (repeat_id % 2) as f32 * pixel_len;
|
|
||||||
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
|
||||||
let pos = curve.point_at_distance(dist);
|
|
||||||
|
|
||||||
// Reverse tick / last legacy tick
|
|
||||||
slider_objects.push(SliderTick::new(pos, h.start_time + time_offset));
|
|
||||||
|
|
||||||
ticks.reverse();
|
|
||||||
slider_objects.extend_from_slice(&ticks); // tick time doesn't need to be adjusted for some reason
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handling last span separatly so that `ticks` vector isn't cloned again
|
|
||||||
let dist = ((repeats - 1) % 2) as f32 * pixel_len;
|
|
||||||
let time_offset = (duration / *repeats as f32) * (repeats - 1) as f32;
|
|
||||||
let pos = curve.point_at_distance(dist);
|
|
||||||
|
|
||||||
slider_objects.push(SliderTick::new(pos, h.start_time + time_offset));
|
|
||||||
|
|
||||||
ticks.reverse();
|
|
||||||
slider_objects.append(&mut ticks);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slider tail
|
|
||||||
let span_duration = duration / *repeats as f32;
|
|
||||||
let final_span_idx = repeats.saturating_sub(1);
|
|
||||||
let final_span_start_time = h.start_time + final_span_idx as f32 * span_duration;
|
|
||||||
let final_span_end_time = (h.start_time + duration / 2.0)
|
|
||||||
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
|
||||||
let mut final_progress =
|
|
||||||
(final_span_end_time - final_span_start_time) / span_duration;
|
|
||||||
|
|
||||||
if *repeats & 1 == 0 {
|
|
||||||
final_progress = 1.0 - final_progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "final_span_index={} | final_span_start_time={} | \
|
|
||||||
// final_span_end_time={} | final_progress={}",
|
|
||||||
// final_span_idx, final_span_start_time, final_span_end_time, final_progress
|
|
||||||
// );
|
|
||||||
|
|
||||||
// println!("len={}", final_progress * *pixel_len as f32);
|
|
||||||
|
|
||||||
let dist_end = (repeats % 2) as f32 * pixel_len;
|
|
||||||
|
|
||||||
let pos = curve.point_at_distance(dist_end);
|
|
||||||
slider_objects.push(SliderTick::new(pos, final_span_end_time));
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "start_time={} | span_duration={} | vel={} | \
|
|
||||||
// tick_dist={} | dist={} | span_count={} | \
|
|
||||||
// legacy_last_tick_offset={}",
|
|
||||||
// h.start_time,
|
|
||||||
// duration / *repeats as f32,
|
|
||||||
// *pixel_len as f32 / duration,
|
|
||||||
// tick_distance,
|
|
||||||
// *pixel_len,
|
|
||||||
// *repeats,
|
|
||||||
// 36
|
|
||||||
// );
|
|
||||||
|
|
||||||
// println!("> Slider: {:?}", slider_objects);
|
|
||||||
|
|
||||||
let radius = OBJECT_RADIUS * scale;
|
|
||||||
|
|
||||||
let stack_offset = {
|
|
||||||
let c = stack_height * scale * -6.4;
|
|
||||||
|
|
||||||
Pos2 { x: c, y: c }
|
|
||||||
};
|
|
||||||
|
|
||||||
// println!("radius={} | stack_offset={:?}", radius, stack_offset);
|
|
||||||
|
|
||||||
let pos = h.pos;
|
|
||||||
let stacked_pos = pos + stack_offset; // TODO: Simplify for below
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "stacked_pos = {:?} + {:?} = {:?}",
|
|
||||||
// pos, stack_offset, stacked_pos
|
|
||||||
// );
|
|
||||||
|
|
||||||
let mut cursor_end_pos = stacked_pos;
|
|
||||||
let mut cursor_travel_dist = 0.0;
|
|
||||||
let approx_follow_circle_radius = radius * 3.0;
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "stacked_pos={:?} | approx_follow_circle_radius={}",
|
|
||||||
// stacked_pos, approx_follow_circle_radius
|
|
||||||
// );
|
|
||||||
|
|
||||||
// let mut curr_offset = tick_distance;
|
|
||||||
|
|
||||||
for (i, tick) in slider_objects.iter().skip(1).enumerate() {
|
|
||||||
let mut progress = (tick.time - h.start_time) / span_duration;
|
|
||||||
|
|
||||||
if progress % 2.0 >= 1.0 {
|
if progress % 2.0 >= 1.0 {
|
||||||
progress = 1.0 - progress % 1.0;
|
progress = 1.0 - progress % 1.0;
|
||||||
@@ -254,159 +107,128 @@ impl OsuObject {
|
|||||||
let curr_dist = pixel_len * progress;
|
let curr_dist = pixel_len * progress;
|
||||||
let curr_pos = curve.point_at_distance(curr_dist);
|
let curr_pos = curve.point_at_distance(curr_dist);
|
||||||
|
|
||||||
let diff = stacked_pos + curr_pos - pos - cursor_end_pos;
|
let diff = curr_pos - end_pos;
|
||||||
let mut dist = diff.length();
|
let mut dist = diff.length();
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "position at: progress=? | d={} => {:?}",
|
|
||||||
// curr_offset, tick.pos
|
|
||||||
// );
|
|
||||||
// curr_offset += tick_distance;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"[{}] diff = {:?} + {:?} - {:?} = {:?} | dist={}",
|
|
||||||
i,
|
|
||||||
stacked_pos,
|
|
||||||
tick.pos - pos,
|
|
||||||
cursor_end_pos,
|
|
||||||
diff,
|
|
||||||
dist
|
|
||||||
);
|
|
||||||
|
|
||||||
// println!("{} > {}", dist, approx_follow_circle_radius);
|
|
||||||
|
|
||||||
if dist > approx_follow_circle_radius {
|
if dist > approx_follow_circle_radius {
|
||||||
let normalized = diff.normalize();
|
|
||||||
// println!("diff before: {:?}", diff);
|
|
||||||
// println!("diff after: {:?}", normalized);
|
|
||||||
dist -= approx_follow_circle_radius;
|
dist -= approx_follow_circle_radius;
|
||||||
cursor_end_pos += normalized * dist;
|
end_pos += diff.normalize() * dist;
|
||||||
|
travel_dist += dist;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// println!("+= {} * {} => {:?}", normalized, dist, cursor_end_pos);
|
let mut current_distance = tick_distance;
|
||||||
|
let time_add = duration * (tick_distance / (pixel_len * *repeats as f32));
|
||||||
|
|
||||||
cursor_travel_dist += dist;
|
let target = pixel_len - tick_distance / 8.0;
|
||||||
// println!("+= {} => {}", dist, cursor_travel_dist);
|
ticks.reserve((target / tick_distance) as usize);
|
||||||
|
|
||||||
|
// Tick of the first span
|
||||||
|
if current_distance < target {
|
||||||
|
for tick_idx in 1.. {
|
||||||
|
let time = h.start_time + time_add * tick_idx as f32;
|
||||||
|
compute_vertex(time);
|
||||||
|
ticks.push(time);
|
||||||
|
current_distance += tick_distance;
|
||||||
|
|
||||||
|
if current_distance >= target {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("cursor_travel_dist={}", cursor_travel_dist);
|
// Other spans
|
||||||
|
if *repeats > 1 {
|
||||||
|
for repeat_id in 1..*repeats {
|
||||||
|
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
||||||
|
|
||||||
println!("---");
|
// Reverse tick
|
||||||
|
compute_vertex(h.start_time + time_offset);
|
||||||
|
|
||||||
Self::Slider {
|
// Actual ticks
|
||||||
pixel_len: *pixel_len,
|
if repeat_id & 1 == 1 {
|
||||||
repeats: *repeats,
|
ticks.iter().rev().for_each(|&time| compute_vertex(time));
|
||||||
|
} else {
|
||||||
|
ticks.iter().for_each(|&time| compute_vertex(time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
objects: slider_objects,
|
// Slider tail
|
||||||
|
let final_span_idx = repeats.saturating_sub(1);
|
||||||
|
let final_span_start_time = h.start_time + final_span_idx as f32 * span_duration;
|
||||||
|
let final_span_end_time = (h.start_time + duration / 2.0)
|
||||||
|
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
||||||
|
compute_vertex(final_span_end_time);
|
||||||
|
|
||||||
scale,
|
ticks.clear();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
time: h.start_time,
|
||||||
|
pos: h.pos,
|
||||||
stack_height,
|
stack_height,
|
||||||
|
kind: OsuObjectKind::Slider {
|
||||||
cursor_end_pos,
|
end_time: final_span_end_time,
|
||||||
cursor_travel_dist,
|
end_pos,
|
||||||
|
travel_dist,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HitObjectKind::Spinner { end_time } => {
|
||||||
|
attributes.n_spinners += 1;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
time: h.start_time,
|
||||||
|
pos: h.pos,
|
||||||
|
stack_height,
|
||||||
|
kind: OsuObjectKind::Spinner {
|
||||||
|
end_time: *end_time,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HitObjectKind::Spinner { .. } => Self::Spinner { pos, time },
|
|
||||||
HitObjectKind::Hold { .. } => return None,
|
HitObjectKind::Hold { .. } => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(obj)
|
Some(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn time(&self) -> f32 {
|
|
||||||
match self {
|
|
||||||
Self::Circle { time, .. } => *time,
|
|
||||||
Self::Slider { objects, .. } => objects[0].time,
|
|
||||||
Self::Spinner { time, .. } => *time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn radius(&self) -> f32 {
|
|
||||||
OBJECT_RADIUS * self.scale()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn travel_dist(&self) -> f32 {
|
pub(crate) fn travel_dist(&self) -> f32 {
|
||||||
match self {
|
match &self.kind {
|
||||||
Self::Slider {
|
OsuObjectKind::Slider { travel_dist, .. } => *travel_dist,
|
||||||
cursor_travel_dist, ..
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => 0.0,
|
||||||
} => *cursor_travel_dist,
|
|
||||||
_ => 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn stacked_pos(&self) -> Pos2 {
|
pub(crate) fn end_time(&self) -> f32 {
|
||||||
self.pos() + self.stack_offset()
|
match &self.kind {
|
||||||
|
OsuObjectKind::Circle => self.time,
|
||||||
|
OsuObjectKind::Slider { end_time, .. } => *end_time,
|
||||||
|
OsuObjectKind::Spinner { end_time } => *end_time,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn cursor_end_position(&self) -> Pos2 {
|
pub(crate) fn end_pos(&self) -> Pos2 {
|
||||||
match self {
|
match &self.kind {
|
||||||
Self::Circle { pos, .. } => *pos,
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||||
Self::Slider { cursor_end_pos, .. } => *cursor_end_pos,
|
OsuObjectKind::Slider { end_pos, .. } => *end_pos,
|
||||||
Self::Spinner { pos, .. } => *pos,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn is_circle(&self) -> bool {
|
||||||
|
matches!(self.kind, OsuObjectKind::Circle)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn is_slider(&self) -> bool {
|
||||||
|
matches!(self.kind, OsuObjectKind::Slider { .. })
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn is_spinner(&self) -> bool {
|
pub(crate) fn is_spinner(&self) -> bool {
|
||||||
matches!(self, Self::Spinner { .. })
|
matches!(self.kind, OsuObjectKind::Spinner { .. })
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn scale(&self) -> f32 {
|
|
||||||
match self {
|
|
||||||
Self::Circle { scale, .. } => *scale,
|
|
||||||
Self::Slider { scale, .. } => *scale,
|
|
||||||
Self::Spinner { .. } => 1.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn stack_height(&self) -> f32 {
|
|
||||||
match self {
|
|
||||||
Self::Circle { stack_height, .. } => *stack_height,
|
|
||||||
Self::Slider { stack_height, .. } => *stack_height,
|
|
||||||
Self::Spinner { .. } => 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove pub
|
|
||||||
#[inline]
|
|
||||||
pub fn pos(&self) -> Pos2 {
|
|
||||||
match self {
|
|
||||||
Self::Circle { pos, .. } => *pos,
|
|
||||||
Self::Slider { objects, .. } => objects[0].pos,
|
|
||||||
Self::Spinner { .. } => Pos2::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn stack_offset(&self) -> Pos2 {
|
|
||||||
let c = self.stack_height() * self.scale() * -6.4;
|
|
||||||
|
|
||||||
Pos2 { x: c, y: c }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub(crate) struct SliderTick {
|
|
||||||
pos: Pos2,
|
|
||||||
time: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SliderTick {
|
|
||||||
fn new(pos: Pos2, time: f32) -> Self {
|
|
||||||
Self { pos, time }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove
|
|
||||||
impl std::fmt::Debug for SliderTick {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(f, "{{pos={:?} | time={}}}", self.pos, self.time)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
|
|||||||
const DECAY_WEIGHT: f32 = 0.9;
|
const DECAY_WEIGHT: f32 = 0.9;
|
||||||
|
|
||||||
pub(crate) struct Skill {
|
pub(crate) struct Skill {
|
||||||
pub current_strain: f32,
|
current_strain: f32,
|
||||||
current_section_peak: f32,
|
current_section_peak: f32,
|
||||||
|
|
||||||
kind: SkillKind,
|
kind: SkillKind,
|
||||||
pub strain_peaks: Vec<f32>, // TODO: Remove pub
|
pub(crate) strain_peaks: Vec<f32>,
|
||||||
|
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f32>,
|
||||||
}
|
}
|
||||||
@@ -36,16 +36,12 @@ impl Skill {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
pub(crate) fn save_current_peak(&mut self) {
|
||||||
if self.prev_time.is_some() {
|
self.strain_peaks.push(self.current_section_peak);
|
||||||
self.strain_peaks.push(self.current_section_peak);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
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 - self.prev_time.unwrap());
|
||||||
self.current_section_peak = self.peak_strain(time - prev);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -53,7 +49,7 @@ impl Skill {
|
|||||||
self.current_strain *= self.strain_decay(current.delta);
|
self.current_strain *= self.strain_decay(current.delta);
|
||||||
self.current_strain += self.kind.strain_value_of(¤t) * self.skill_multiplier();
|
self.current_strain += self.kind.strain_value_of(¤t) * self.skill_multiplier();
|
||||||
self.current_section_peak = self.current_section_peak.max(self.current_strain);
|
self.current_section_peak = self.current_section_peak.max(self.current_strain);
|
||||||
self.prev_time.replace(current.base.time());
|
self.prev_time.replace(current.base.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ impl SkillKind {
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!("pos={:?}", current.base.pos());
|
|
||||||
|
|
||||||
let mut result = 0.0;
|
let mut result = 0.0;
|
||||||
|
|
||||||
if let Some((prev_jump_dist, prev_strain_time)) = current.prev {
|
if let Some((prev_jump_dist, prev_strain_time)) = current.prev {
|
||||||
@@ -39,23 +37,14 @@ impl SkillKind {
|
|||||||
* (current.jump_dist - scale).max(0.0))
|
* (current.jump_dist - scale).max(0.0))
|
||||||
.sqrt();
|
.sqrt();
|
||||||
|
|
||||||
// println!("angle_bonus={}", angle_bonus);
|
|
||||||
|
|
||||||
result = 1.5 * apply_diminishing_exp(angle_bonus.max(0.0))
|
result = 1.5 * apply_diminishing_exp(angle_bonus.max(0.0))
|
||||||
/ (TIMING_THRESHOLD).max(prev_strain_time)
|
/ (TIMING_THRESHOLD).max(prev_strain_time)
|
||||||
} else {
|
|
||||||
// println!("nop");
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// println!("no prev");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let jump_dist_exp = apply_diminishing_exp(current.jump_dist);
|
let jump_dist_exp = apply_diminishing_exp(current.jump_dist);
|
||||||
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
|
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
|
||||||
|
|
||||||
// println!("jump_dist={} => {}", current.jump_dist, jump_dist_exp);
|
|
||||||
// println!("travel_dist={} => {}", current.travel_dist, travel_dist_exp);
|
|
||||||
|
|
||||||
let dist_exp =
|
let dist_exp =
|
||||||
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
|
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
|
||||||
|
|
||||||
@@ -79,8 +68,6 @@ impl SkillKind {
|
|||||||
|
|
||||||
let mut angle_bonus = 1.0;
|
let mut angle_bonus = 1.0;
|
||||||
|
|
||||||
// println!("angle: {:?}", current.angle);
|
|
||||||
|
|
||||||
if let Some(angle) = current.angle.filter(|a| *a < SPEED_ANGLE_BONUS_BEGIN) {
|
if let Some(angle) = current.angle.filter(|a| *a < SPEED_ANGLE_BONUS_BEGIN) {
|
||||||
let exp_base = (1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)).sin();
|
let exp_base = (1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)).sin();
|
||||||
angle_bonus = 1.0 + exp_base * exp_base / 3.57;
|
angle_bonus = 1.0 + exp_base * exp_base / 3.57;
|
||||||
@@ -88,7 +75,6 @@ impl SkillKind {
|
|||||||
if angle < PI_OVER_2 {
|
if angle < PI_OVER_2 {
|
||||||
angle_bonus = 1.28;
|
angle_bonus = 1.28;
|
||||||
|
|
||||||
// TODO: Improve ifs
|
|
||||||
if dist < 90.0 && angle < PI_OVER_4 {
|
if dist < 90.0 && angle < PI_OVER_4 {
|
||||||
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
|
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
|
||||||
} else if dist < 90.0 {
|
} else if dist < 90.0 {
|
||||||
@@ -99,11 +85,6 @@ impl SkillKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "dist={} | speed_bonus={} | angle_bonus={}",
|
|
||||||
// dist, speed_bonus, angle_bonus
|
|
||||||
// );
|
|
||||||
|
|
||||||
(1.0 + (speed_bonus - 1.0) * 0.75)
|
(1.0 + (speed_bonus - 1.0) * 0.75)
|
||||||
* angle_bonus
|
* angle_bonus
|
||||||
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
|
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use crate::{Beatmap, ControlPoint, ControlPointIter};
|
||||||
|
|
||||||
|
pub(crate) struct SliderState<'p> {
|
||||||
|
control_points: ControlPointIter<'p>,
|
||||||
|
next: Option<ControlPoint>,
|
||||||
|
pub(crate) beat_len: f32,
|
||||||
|
pub(crate) speed_mult: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'p> SliderState<'p> {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
||||||
|
let mut control_points = ControlPointIter::new(map);
|
||||||
|
|
||||||
|
let (beat_len, speed_mult) = match control_points.next() {
|
||||||
|
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
|
||||||
|
Some(ControlPoint::Difficulty { speed_mult, .. }) => (1000.0, speed_mult),
|
||||||
|
None => (1000.0, 1.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
next: control_points.next(),
|
||||||
|
control_points,
|
||||||
|
beat_len,
|
||||||
|
speed_mult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn update(&mut self, time: f32) {
|
||||||
|
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
||||||
|
match next {
|
||||||
|
ControlPoint::Timing { beat_len, .. } => {
|
||||||
|
self.beat_len = *beat_len;
|
||||||
|
self.speed_mult = 1.0;
|
||||||
|
}
|
||||||
|
ControlPoint::Difficulty { speed_mult, .. } => self.speed_mult = *speed_mult,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.next = self.control_points.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{Beatmap, DifficultyPoint, TimingPoint};
|
||||||
|
|
||||||
|
use super::SliderState;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osu_slider_state() {
|
||||||
|
let map = Beatmap {
|
||||||
|
timing_points: vec![
|
||||||
|
TimingPoint {
|
||||||
|
time: 1.0,
|
||||||
|
beat_len: 10.0,
|
||||||
|
},
|
||||||
|
TimingPoint {
|
||||||
|
time: 3.0,
|
||||||
|
beat_len: 20.0,
|
||||||
|
},
|
||||||
|
TimingPoint {
|
||||||
|
time: 4.0,
|
||||||
|
beat_len: 30.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
difficulty_points: vec![
|
||||||
|
DifficultyPoint {
|
||||||
|
time: 2.0,
|
||||||
|
speed_multiplier: 15.0,
|
||||||
|
},
|
||||||
|
DifficultyPoint {
|
||||||
|
time: 5.0,
|
||||||
|
speed_multiplier: 45.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = SliderState::new(&map);
|
||||||
|
|
||||||
|
state.update(2.0);
|
||||||
|
assert_eq!(state.beat_len, 10.0);
|
||||||
|
|
||||||
|
state.update(3.0);
|
||||||
|
assert_eq!(state.beat_len, 20.0);
|
||||||
|
assert_eq!(state.speed_mult, 1.0);
|
||||||
|
|
||||||
|
state.update(5.0);
|
||||||
|
assert_eq!(state.beat_len, 30.0);
|
||||||
|
assert_eq!(state.speed_mult, 45.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user