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 crate::Beatmap;
use std::cmp::Ordering;
/// New rhythm speed change.
#[derive(Copy, Clone, Debug, PartialEq)]
@@ -9,24 +7,27 @@ pub struct TimingPoint {
pub beat_len: f64,
/// The start time of this timing section
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 {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.time.partial_cmp(&other.time)
}
}
impl Default for TimingPoint {
#[inline]
fn default() -> Self {
Self {
beat_len: 60_000.0 / 60.0,
time: 0.0,
kiai: false,
}
Self::new(0.0, 60_000.0 / 60.0)
}
}
@@ -37,9 +38,6 @@ pub struct DifficultyPoint {
pub time: f64,
/// The slider velocity at this control point.
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.
pub bpm_mult: f64,
/// 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`]
pub const DEFAULT_GENERATE_TICKS: bool = true;
/// Create a new [`DifficultyPoint`]
pub fn new(time: f64, beat_len: f64, speed_multiplier: f64, kiai: bool) -> Self {
/// Create a new [`DifficultyPoint`].
#[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
// * turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
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 {
1.0
};
@@ -69,7 +68,6 @@ impl DifficultyPoint {
Self {
time,
slider_vel: speed_multiplier.clamp(0.1, 10.0),
kiai,
bpm_mult: bpm_multiplier as f64,
generate_ticks: !beat_len.is_nan(),
}
@@ -82,16 +80,17 @@ impl DifficultyPoint {
}
impl PartialOrd for DifficultyPoint {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.time.partial_cmp(&other.time)
}
}
impl Default for DifficultyPoint {
#[inline]
fn default() -> Self {
Self {
time: 0.0,
kiai: false,
slider_vel: Self::DEFAULT_SLIDER_VEL,
bpm_mult: Self::DEFAULT_BPM_MULT,
generate_ticks: Self::DEFAULT_GENERATE_TICKS,
@@ -99,119 +98,29 @@ impl Default for DifficultyPoint {
}
}
/// Control point for a [`Beatmap`].
#[derive(Copy, Clone, Debug)]
pub enum ControlPoint {
/// A timing point containing the current beat length.
Timing(TimingPoint),
/// A difficulty point containing the current speed multiplier.
Difficulty(DifficultyPoint),
/// Control point storing effects and their timestamps.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct EffectPoint {
/// The time at which the control point takes effect.
pub time: f64,
/// Whether this control point enables Kiai mode.
pub kiai: bool,
}
impl ControlPoint {
/// Provides the timestamp of the control point.
impl EffectPoint {
/// The default slider velocity for a [`DifficultyPoint`]
pub const DEFAULT_KIAI: bool = true;
/// Create a new [`EffectPoint`].
#[inline]
pub fn time(&self) -> f64 {
match self {
Self::Timing(point) => point.time,
Self::Difficulty(point) => point.time,
}
pub fn new(time: f64, kiai: bool) -> Self {
Self { time, kiai }
}
}
/// Iterator for a [`Beatmap`]'s timing- and difficulty points sorted by timestamp
#[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> {
impl Default for EffectPoint {
#[inline]
pub(crate) fn new(map: &'p Beatmap) -> Self {
let mut timing_points = map.timing_points.iter().copied();
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));
fn default() -> Self {
Self::new(0.0, Self::DEFAULT_KIAI)
}
}
+2 -4
View File
@@ -23,10 +23,8 @@ impl Hasher for ByteHasher {
}
#[inline]
fn write(&mut self, bytes: &[u8]) {
// Only use this hasher for single bytes
debug_assert_eq!(bytes.len(), 1);
self.byte = bytes[0];
fn write(&mut self, _: &[u8]) {
unreachable!()
}
#[inline]
+4 -4
View File
@@ -51,13 +51,13 @@ impl Beatmap {
.count();
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 {
7.0
} else if percent_slider_or_spinner < 0.3 || rounded_cs >= 5.0 {
(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
} else {
(rounded_od + 1.0).clamp(4.0, 7.0)
@@ -104,8 +104,8 @@ impl Beatmap {
last_values.time = obj.start_time;
last_values.pos = obj.pos;
map.hit_objects
.extend(new_pattern.hit_objects.iter().cloned());
let new_hit_objects = new_pattern.hit_objects.iter().cloned();
map.hit_objects.extend(new_hit_objects);
n_circles += new_pattern.hit_objects.len();
last_values.pattern = new_pattern;
@@ -1,6 +1,7 @@
use crate::{
beatmap::converts::mania::{
legacy_random::Random, pattern::Pattern, pattern_type::PatternType,
beatmap::{
converts::mania::{legacy_random::Random, pattern::Pattern, pattern_type::PatternType},
EffectPoint,
},
curve::Curve,
mania::ManiaObject,
@@ -44,11 +45,9 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
.difficulty_point_at(hit_object.start_time)
.unwrap_or_default();
let kiai = if timing_point.time < difficulty_point.time {
difficulty_point.kiai
} else {
timing_point.kiai
};
let kiai = orig
.effect_point_at(hit_object.start_time)
.map_or(EffectPoint::DEFAULT_KIAI, |point| point.kiai);
let convert_type = if kiai {
PatternType::default()
@@ -56,8 +55,6 @@ 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.bpm_mult;
let span_count = (repeats + 1) as i32;
@@ -1,6 +1,9 @@
use crate::{
beatmap::converts::mania::{
legacy_random::Random, pattern::Pattern, pattern_type::PatternType, PrevValues,
beatmap::{
converts::mania::{
legacy_random::Random, pattern::Pattern, pattern_type::PatternType, PrevValues,
},
EffectPoint,
},
mania::ManiaObject,
parse::{HitObject, HitSound},
@@ -61,18 +64,9 @@ impl<'h> HitObjectPatternGenerator<'h> {
} else if density < timing_point.beat_len / 2.5 {
// * High density
} else {
let difficulty_point = orig.difficulty_point_at(hit_object.start_time);
let kiai = match difficulty_point {
Some(difficulty_point) => {
if timing_point.time < difficulty_point.time {
difficulty_point.kiai
} else {
timing_point.kiai
}
}
None => timing_point.kiai,
};
let kiai = orig
.effect_point_at(hit_object.start_time)
.map_or(EffectPoint::DEFAULT_KIAI, |point| point.kiai);
if kiai {
// * High density
+16 -7
View File
@@ -5,7 +5,7 @@ use crate::parse::HitObject;
pub use self::{
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, BeatmapHitWindows},
breaks::Break,
control_points::{ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint},
control_points::{DifficultyPoint, EffectPoint, TimingPoint},
mode::GameMode,
sorted_vec::SortedVec,
};
@@ -57,6 +57,9 @@ pub struct Beatmap {
/// Timing point for the current timing section.
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 offset for stacked positions.
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).
#[inline]
pub fn total_break_time(&self) -> f64 {
@@ -120,6 +117,17 @@ impl Beatmap {
.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.
///
/// # Note
@@ -157,6 +165,7 @@ impl Beatmap {
sounds: Vec::with_capacity((with_sounds as usize) * self.sounds.len()),
timing_points: self.timing_points.clone(),
difficulty_points: self.difficulty_points.clone(),
effect_points: self.effect_points.clone(),
stack_leniency: self.stack_leniency,
breaks: self.breaks.clone(),
}
+11 -1
View File
@@ -5,7 +5,7 @@ use std::{
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.
#[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> {
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)) {
+9 -19
View File
@@ -516,36 +516,26 @@ compile_error!("Only one of the features `async_tokio` and `async_std` should be
#[cfg(test)]
mod tests {
use crate::{Beatmap, OsuPP};
use crate::{Beatmap, GameMode, OsuPP, PerformanceAttributes};
#[test]
fn custom() {
let path = "F:\\osu!\\beatmaps\\1529760.osu";
let path = "F:/osu!/beatmaps/1000168.osu";
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!(
"difficulty:\n\
aim={}\n\
speed={}\n\
flashlight={}\n\
stars={}\n\
max_combo={}\n\
performance:\n\
aim={}\n\
speed={}\n\
acc={}\n\
flashlight={}\n\
difficulty={}\n\
pp={}\n",
attrs.difficulty.aim,
attrs.difficulty.speed,
attrs.difficulty.flashlight,
attrs.difficulty.stars,
attrs.pp_aim,
attrs.pp_speed,
attrs.pp_acc,
attrs.pp_flashlight,
attrs.pp,
attrs.difficulty.stars, attrs.difficulty.max_combo, attrs.pp_difficulty, attrs.pp,
);
}
}
+2 -2
View File
@@ -226,7 +226,7 @@ impl<'map> OsuPP<'map> {
match priority {
HitResultPriority::BestCase => n300 += remaining,
HitResultPriority::WorstCase => n100 += remaining,
HitResultPriority::WorstCase => n50 += remaining,
}
}
(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"))]
async fn test_map() -> Beatmap {
async fn test_map() -> (Beatmap, OsuDifficultyAttributes) {
let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).await.unwrap();
+7 -8
View File
@@ -28,7 +28,7 @@ use std::path::Path;
#[cfg(feature = "async_std")]
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]) {
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 {
let point = TimingPoint {
time,
beat_len: beat_len.clamp(6.0, 60_000.0),
kiai,
};
let point = TimingPoint::new(time, beat_len.clamp(6.0, 60_000.0));
$self.timing_points.push(point);
}
if !timing_change || pending_diff_point.is_none() {
pending_diff_point =
Some(DifficultyPoint::new(time, beat_len, speed_multiplier, kiai));
pending_diff_point = Some(DifficultyPoint::new(time, beat_len, speed_multiplier));
}
let effect_point = EffectPoint::new(time, kiai);
$self.effect_points.push(effect_point);
pending_diff_points_time = time;
}
@@ -397,6 +395,7 @@ macro_rules! parse_timingpoints_body {
$self.timing_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)
}};