mania convert fixes
This commit is contained in:
+35
-126
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}};
|
||||
|
||||
Reference in New Issue
Block a user