mania convert fixes

This commit is contained in:
MaxOhn
2022-10-17 09:13:31 +02:00
parent 10a5293c76
commit 044fe986c9
10 changed files with 100 additions and 194 deletions
+35 -126
View File
@@ -1,6 +1,4 @@
use std::{cmp::Ordering, iter::Copied, slice::Iter}; use std::cmp::Ordering;
use crate::Beatmap;
/// New rhythm speed change. /// New rhythm speed change.
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
@@ -9,24 +7,27 @@ pub struct TimingPoint {
pub beat_len: f64, pub beat_len: f64,
/// The start time of this timing section /// The start time of this timing section
pub time: f64, pub time: f64,
/// Whether the section between this and the }
/// next timing points is a kiai section
pub kiai: bool, impl TimingPoint {
/// Create a new [`TimingPoint`].
#[inline]
pub fn new(time: f64, beat_len: f64) -> Self {
Self { time, beat_len }
}
} }
impl PartialOrd for TimingPoint { impl PartialOrd for TimingPoint {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.time.partial_cmp(&other.time) self.time.partial_cmp(&other.time)
} }
} }
impl Default for TimingPoint { impl Default for TimingPoint {
#[inline]
fn default() -> Self { fn default() -> Self {
Self { Self::new(0.0, 60_000.0 / 60.0)
beat_len: 60_000.0 / 60.0,
time: 0.0,
kiai: false,
}
} }
} }
@@ -37,9 +38,6 @@ pub struct DifficultyPoint {
pub time: f64, pub time: f64,
/// The slider velocity at this control point. /// The slider velocity at this control point.
pub slider_vel: f64, pub slider_vel: f64,
/// Whether the section between this and the
/// next timing points is a kiai section
pub kiai: bool,
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
pub bpm_mult: f64, pub bpm_mult: f64,
/// Whether or not slider ticks should be generated at this control point. /// Whether or not slider ticks should be generated at this control point.
@@ -56,12 +54,13 @@ impl DifficultyPoint {
/// The default for generating ticks of a [`DifficultyPoint`] /// The default for generating ticks of a [`DifficultyPoint`]
pub const DEFAULT_GENERATE_TICKS: bool = true; pub const DEFAULT_GENERATE_TICKS: bool = true;
/// Create a new [`DifficultyPoint`] /// Create a new [`DifficultyPoint`].
pub fn new(time: f64, beat_len: f64, speed_multiplier: f64, kiai: bool) -> Self { #[inline]
pub fn new(time: f64, beat_len: f64, speed_multiplier: f64) -> Self {
// * Note: In stable, the division occurs on floats, but with compiler optimisations // * Note: In stable, the division occurs on floats, but with compiler optimisations
// * turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?). // * turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
let bpm_multiplier = if beat_len < 0.0 { let bpm_multiplier = if beat_len < 0.0 {
((-beat_len) as f32).clamp(10.0, 10_000.0) ((-beat_len) as f32).clamp(10.0, 10_000.0) as f64 / 100.0
} else { } else {
1.0 1.0
}; };
@@ -69,7 +68,6 @@ impl DifficultyPoint {
Self { Self {
time, time,
slider_vel: speed_multiplier.clamp(0.1, 10.0), slider_vel: speed_multiplier.clamp(0.1, 10.0),
kiai,
bpm_mult: bpm_multiplier as f64, bpm_mult: bpm_multiplier as f64,
generate_ticks: !beat_len.is_nan(), generate_ticks: !beat_len.is_nan(),
} }
@@ -82,16 +80,17 @@ impl DifficultyPoint {
} }
impl PartialOrd for DifficultyPoint { impl PartialOrd for DifficultyPoint {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.time.partial_cmp(&other.time) self.time.partial_cmp(&other.time)
} }
} }
impl Default for DifficultyPoint { impl Default for DifficultyPoint {
#[inline]
fn default() -> Self { fn default() -> Self {
Self { Self {
time: 0.0, time: 0.0,
kiai: false,
slider_vel: Self::DEFAULT_SLIDER_VEL, slider_vel: Self::DEFAULT_SLIDER_VEL,
bpm_mult: Self::DEFAULT_BPM_MULT, bpm_mult: Self::DEFAULT_BPM_MULT,
generate_ticks: Self::DEFAULT_GENERATE_TICKS, generate_ticks: Self::DEFAULT_GENERATE_TICKS,
@@ -99,119 +98,29 @@ impl Default for DifficultyPoint {
} }
} }
/// Control point for a [`Beatmap`]. /// Control point storing effects and their timestamps.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, PartialEq)]
pub enum ControlPoint { pub struct EffectPoint {
/// A timing point containing the current beat length. /// The time at which the control point takes effect.
Timing(TimingPoint), pub time: f64,
/// A difficulty point containing the current speed multiplier. /// Whether this control point enables Kiai mode.
Difficulty(DifficultyPoint), pub kiai: bool,
} }
impl ControlPoint { impl EffectPoint {
/// Provides the timestamp of the control point. /// The default slider velocity for a [`DifficultyPoint`]
pub const DEFAULT_KIAI: bool = true;
/// Create a new [`EffectPoint`].
#[inline] #[inline]
pub fn time(&self) -> f64 { pub fn new(time: f64, kiai: bool) -> Self {
match self { Self { time, kiai }
Self::Timing(point) => point.time,
Self::Difficulty(point) => point.time,
}
} }
} }
/// Iterator for a [`Beatmap`]'s timing- and difficulty points sorted by timestamp impl Default for EffectPoint {
#[derive(Clone, Debug)]
pub struct ControlPointIter<'p> {
timing_points: Copied<Iter<'p, TimingPoint>>,
difficulty_points: Copied<Iter<'p, DifficultyPoint>>,
next_timing: Option<TimingPoint>,
next_difficulty: Option<DifficultyPoint>,
}
impl<'p> ControlPointIter<'p> {
#[inline] #[inline]
pub(crate) fn new(map: &'p Beatmap) -> Self { fn default() -> Self {
let mut timing_points = map.timing_points.iter().copied(); Self::new(0.0, Self::DEFAULT_KIAI)
let mut difficulty_points = map.difficulty_points.iter().copied();
Self {
next_timing: timing_points.next(),
next_difficulty: difficulty_points.next(),
timing_points,
difficulty_points,
}
}
}
impl<'p> Iterator for ControlPointIter<'p> {
type Item = ControlPoint;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
match (self.next_timing, self.next_difficulty) {
(Some(timing), Some(difficulty)) if timing.time <= difficulty.time => {
self.next_timing = self.timing_points.next();
Some(ControlPoint::Timing(timing))
}
(_, Some(point)) => {
self.next_difficulty = self.difficulty_points.next();
Some(ControlPoint::Difficulty(point))
}
(Some(point), None) => {
self.next_timing = self.timing_points.next();
Some(ControlPoint::Timing(point))
}
(None, None) => None,
}
}
}
#[cfg(test)]
mod test {
use crate::{
beatmap::{ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint},
Beatmap,
};
#[test]
fn control_point_iter() {
let mut map = Beatmap::default();
map.timing_points.push(TimingPoint {
time: 1.0,
beat_len: 10.0,
kiai: false,
});
map.timing_points.push(TimingPoint {
time: 3.0,
beat_len: 10.0,
kiai: false,
});
map.timing_points.push(TimingPoint {
time: 4.0,
beat_len: 10.0,
kiai: false,
});
map.difficulty_points
.push(DifficultyPoint::new(2.0, 10.0, 10.0, false));
map.difficulty_points
.push(DifficultyPoint::new(5.0, 10.0, 10.0, false));
let mut iter = ControlPointIter::new(&map);
assert!(matches!(iter.next(), Some(ControlPoint::Timing(_))));
assert!(matches!(iter.next(), Some(ControlPoint::Difficulty(_))));
assert!(matches!(iter.next(), Some(ControlPoint::Timing(_))));
assert!(matches!(iter.next(), Some(ControlPoint::Timing(_))));
assert!(matches!(iter.next(), Some(ControlPoint::Difficulty(_))));
assert!(matches!(iter.next(), None));
} }
} }
+2 -4
View File
@@ -23,10 +23,8 @@ impl Hasher for ByteHasher {
} }
#[inline] #[inline]
fn write(&mut self, bytes: &[u8]) { fn write(&mut self, _: &[u8]) {
// Only use this hasher for single bytes unreachable!()
debug_assert_eq!(bytes.len(), 1);
self.byte = bytes[0];
} }
#[inline] #[inline]
+4 -4
View File
@@ -51,13 +51,13 @@ impl Beatmap {
.count(); .count();
let percent_slider_or_spinner = let percent_slider_or_spinner =
slider_or_spinner_count as f32 / self.hit_objects.len() as f32; (slider_or_spinner_count as f32 / self.hit_objects.len() as f32) as f64;
let target_columns = if percent_slider_or_spinner < 0.2 { let target_columns = if percent_slider_or_spinner < 0.2 {
7.0 7.0
} else if percent_slider_or_spinner < 0.3 || rounded_cs >= 5.0 { } else if percent_slider_or_spinner < 0.3 || rounded_cs >= 5.0 {
(6 + (rounded_od > 5.0) as u8) as f32 (6 + (rounded_od > 5.0) as u8) as f32
} else if percent_slider_or_spinner > 0.6 { } else if percent_slider_or_spinner as f64 > 0.6 {
(4 + (rounded_od > 4.0) as u8) as f32 (4 + (rounded_od > 4.0) as u8) as f32
} else { } else {
(rounded_od + 1.0).clamp(4.0, 7.0) (rounded_od + 1.0).clamp(4.0, 7.0)
@@ -104,8 +104,8 @@ impl Beatmap {
last_values.time = obj.start_time; last_values.time = obj.start_time;
last_values.pos = obj.pos; last_values.pos = obj.pos;
map.hit_objects let new_hit_objects = new_pattern.hit_objects.iter().cloned();
.extend(new_pattern.hit_objects.iter().cloned()); map.hit_objects.extend(new_hit_objects);
n_circles += new_pattern.hit_objects.len(); n_circles += new_pattern.hit_objects.len();
last_values.pattern = new_pattern; last_values.pattern = new_pattern;
@@ -1,6 +1,7 @@
use crate::{ use crate::{
beatmap::converts::mania::{ beatmap::{
legacy_random::Random, pattern::Pattern, pattern_type::PatternType, converts::mania::{legacy_random::Random, pattern::Pattern, pattern_type::PatternType},
EffectPoint,
}, },
curve::Curve, curve::Curve,
mania::ManiaObject, mania::ManiaObject,
@@ -44,11 +45,9 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
.difficulty_point_at(hit_object.start_time) .difficulty_point_at(hit_object.start_time)
.unwrap_or_default(); .unwrap_or_default();
let kiai = if timing_point.time < difficulty_point.time { let kiai = orig
difficulty_point.kiai .effect_point_at(hit_object.start_time)
} else { .map_or(EffectPoint::DEFAULT_KIAI, |point| point.kiai);
timing_point.kiai
};
let convert_type = if kiai { let convert_type = if kiai {
PatternType::default() PatternType::default()
@@ -56,8 +55,6 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
PatternType::LOW_PROBABILITY 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.bpm_mult; let beat_len = timing_point.beat_len * difficulty_point.bpm_mult;
let span_count = (repeats + 1) as i32; let span_count = (repeats + 1) as i32;
@@ -1,7 +1,10 @@
use crate::{ use crate::{
beatmap::converts::mania::{ beatmap::{
converts::mania::{
legacy_random::Random, pattern::Pattern, pattern_type::PatternType, PrevValues, legacy_random::Random, pattern::Pattern, pattern_type::PatternType, PrevValues,
}, },
EffectPoint,
},
mania::ManiaObject, mania::ManiaObject,
parse::{HitObject, HitSound}, parse::{HitObject, HitSound},
Beatmap, Beatmap,
@@ -61,18 +64,9 @@ impl<'h> HitObjectPatternGenerator<'h> {
} else if density < timing_point.beat_len / 2.5 { } else if density < timing_point.beat_len / 2.5 {
// * High density // * High density
} else { } else {
let difficulty_point = orig.difficulty_point_at(hit_object.start_time); let kiai = orig
.effect_point_at(hit_object.start_time)
let kiai = match difficulty_point { .map_or(EffectPoint::DEFAULT_KIAI, |point| point.kiai);
Some(difficulty_point) => {
if timing_point.time < difficulty_point.time {
difficulty_point.kiai
} else {
timing_point.kiai
}
}
None => timing_point.kiai,
};
if kiai { if kiai {
// * High density // * High density
+16 -7
View File
@@ -5,7 +5,7 @@ use crate::parse::HitObject;
pub use self::{ pub use self::{
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, BeatmapHitWindows}, attributes::{BeatmapAttributes, BeatmapAttributesBuilder, BeatmapHitWindows},
breaks::Break, breaks::Break,
control_points::{ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint}, control_points::{DifficultyPoint, EffectPoint, TimingPoint},
mode::GameMode, mode::GameMode,
sorted_vec::SortedVec, sorted_vec::SortedVec,
}; };
@@ -57,6 +57,9 @@ pub struct Beatmap {
/// Timing point for the current timing section. /// Timing point for the current timing section.
pub difficulty_points: SortedVec<DifficultyPoint>, pub difficulty_points: SortedVec<DifficultyPoint>,
/// Control points for effect sections.
pub effect_points: SortedVec<EffectPoint>,
/// The stack leniency that is used to calculate /// The stack leniency that is used to calculate
/// the stack offset for stacked positions. /// the stack offset for stacked positions.
pub stack_leniency: f32, pub stack_leniency: f32,
@@ -81,12 +84,6 @@ impl Beatmap {
} }
} }
/// Create an iterator over the map's timing- and difficulty points sorted by timestamp.
#[inline]
pub fn control_points(&self) -> ControlPointIter<'_> {
ControlPointIter::new(self)
}
/// Sum up the duration of all breaks (in milliseconds). /// Sum up the duration of all breaks (in milliseconds).
#[inline] #[inline]
pub fn total_break_time(&self) -> f64 { pub fn total_break_time(&self) -> f64 {
@@ -120,6 +117,17 @@ impl Beatmap {
.map(|i| self.difficulty_points[i]) .map(|i| self.difficulty_points[i])
} }
/// Return the [`EffectPoint`] for the given timestamp.
///
/// If `time` is before the first effect point, `None` is returned.
#[inline]
pub fn effect_point_at(&self, time: f64) -> Option<EffectPoint> {
self.effect_points
.binary_search_by(|probe| probe.time.partial_cmp(&time).unwrap_or(Ordering::Less))
.map_or_else(|i| i.checked_sub(1), Some)
.map(|i| self.effect_points[i])
}
/// Convert a [`Beatmap`] of some mode into a different mode. /// Convert a [`Beatmap`] of some mode into a different mode.
/// ///
/// # Note /// # Note
@@ -157,6 +165,7 @@ impl Beatmap {
sounds: Vec::with_capacity((with_sounds as usize) * self.sounds.len()), sounds: Vec::with_capacity((with_sounds as usize) * self.sounds.len()),
timing_points: self.timing_points.clone(), timing_points: self.timing_points.clone(),
difficulty_points: self.difficulty_points.clone(), difficulty_points: self.difficulty_points.clone(),
effect_points: self.effect_points.clone(),
stack_leniency: self.stack_leniency, stack_leniency: self.stack_leniency,
breaks: self.breaks.clone(), breaks: self.breaks.clone(),
} }
+11 -1
View File
@@ -5,7 +5,7 @@ use std::{
ops::Deref, ops::Deref,
}; };
use super::{DifficultyPoint, TimingPoint}; use super::{control_points::EffectPoint, DifficultyPoint, TimingPoint};
/// A [`Vec`] whose elements are guaranteed to be in order based on the given comparator. /// A [`Vec`] whose elements are guaranteed to be in order based on the given comparator.
#[derive(Clone)] #[derive(Clone)]
@@ -76,6 +76,16 @@ impl Default for SortedVec<DifficultyPoint> {
} }
} }
impl Default for SortedVec<EffectPoint> {
#[inline]
fn default() -> Self {
Self {
inner: Vec::new(),
cmp: |a, b| a.time.partial_cmp(&b.time).unwrap_or(Ordering::Equal),
}
}
}
impl SortedVec<DifficultyPoint> { impl SortedVec<DifficultyPoint> {
pub(crate) fn push_if_not_redundant(&mut self, value: DifficultyPoint) { pub(crate) fn push_if_not_redundant(&mut self, value: DifficultyPoint) {
let is_redundant = match self.find(&value).map_err(|idx| idx.checked_sub(1)) { let is_redundant = match self.find(&value).map_err(|idx| idx.checked_sub(1)) {
+9 -19
View File
@@ -516,36 +516,26 @@ compile_error!("Only one of the features `async_tokio` and `async_std` should be
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{Beatmap, OsuPP}; use crate::{Beatmap, GameMode, OsuPP, PerformanceAttributes};
#[test] #[test]
fn custom() { fn custom() {
let path = "F:\\osu!\\beatmaps\\1529760.osu"; let path = "F:/osu!/beatmaps/1000168.osu";
let map = Beatmap::from_path(path).unwrap(); let map = Beatmap::from_path(path).unwrap();
let attrs = OsuPP::new(&map).mods(16).calculate(); let attrs = match OsuPP::new(&map).mode(GameMode::Mania).mods(0).calculate() {
PerformanceAttributes::Mania(attrs) => attrs,
_ => unreachable!(),
};
println!( println!(
"difficulty:\n\ "difficulty:\n\
aim={}\n\
speed={}\n\
flashlight={}\n\
stars={}\n\ stars={}\n\
max_combo={}\n\
performance:\n\ performance:\n\
aim={}\n\ difficulty={}\n\
speed={}\n\
acc={}\n\
flashlight={}\n\
pp={}\n", pp={}\n",
attrs.difficulty.aim, attrs.difficulty.stars, attrs.difficulty.max_combo, attrs.pp_difficulty, attrs.pp,
attrs.difficulty.speed,
attrs.difficulty.flashlight,
attrs.difficulty.stars,
attrs.pp_aim,
attrs.pp_speed,
attrs.pp_acc,
attrs.pp_flashlight,
attrs.pp,
); );
} }
} }
+2 -2
View File
@@ -226,7 +226,7 @@ impl<'map> OsuPP<'map> {
match priority { match priority {
HitResultPriority::BestCase => n300 += remaining, HitResultPriority::BestCase => n300 += remaining,
HitResultPriority::WorstCase => n100 += remaining, HitResultPriority::WorstCase => n50 += remaining,
} }
} }
(Some(_), Some(_), None) => n50 = n_objects.saturating_sub(n300 + n100 + n_misses), (Some(_), Some(_), None) => n50 = n_objects.saturating_sub(n300 + n100 + n_misses),
@@ -771,7 +771,7 @@ mod test {
} }
#[cfg(any(feature = "async_tokio", feature = "async_str"))] #[cfg(any(feature = "async_tokio", feature = "async_str"))]
async fn test_map() -> Beatmap { async fn test_map() -> (Beatmap, OsuDifficultyAttributes) {
let path = "./maps/2785319.osu"; let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).await.unwrap(); let map = Beatmap::from_path(path).await.unwrap();
+7 -8
View File
@@ -28,7 +28,7 @@ use std::path::Path;
#[cfg(feature = "async_std")] #[cfg(feature = "async_std")]
use async_std::{fs::File, io::Read as AsyncRead, path::Path}; use async_std::{fs::File, io::Read as AsyncRead, path::Path};
use crate::beatmap::{Beatmap, Break, DifficultyPoint, GameMode, TimingPoint}; use crate::beatmap::{Beatmap, Break, DifficultyPoint, EffectPoint, GameMode, TimingPoint};
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) { fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal)); slice.sort_unstable_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
@@ -374,20 +374,18 @@ macro_rules! parse_timingpoints_body {
} }
if timing_change { if timing_change {
let point = TimingPoint { let point = TimingPoint::new(time, beat_len.clamp(6.0, 60_000.0));
time,
beat_len: beat_len.clamp(6.0, 60_000.0),
kiai,
};
$self.timing_points.push(point); $self.timing_points.push(point);
} }
if !timing_change || pending_diff_point.is_none() { if !timing_change || pending_diff_point.is_none() {
pending_diff_point = pending_diff_point = Some(DifficultyPoint::new(time, beat_len, speed_multiplier));
Some(DifficultyPoint::new(time, beat_len, speed_multiplier, kiai));
} }
let effect_point = EffectPoint::new(time, kiai);
$self.effect_points.push(effect_point);
pending_diff_points_time = time; pending_diff_points_time = time;
} }
@@ -397,6 +395,7 @@ macro_rules! parse_timingpoints_body {
$self.timing_points.dedup_by_key(|point| point.time); $self.timing_points.dedup_by_key(|point| point.time);
$self.difficulty_points.dedup_by_key(|point| point.time); $self.difficulty_points.dedup_by_key(|point| point.time);
$self.effect_points.dedup_by_key(|point| point.time);
Ok(empty) Ok(empty)
}}; }};