finished mania + restructured a little
This commit is contained in:
@@ -109,7 +109,7 @@ pub struct EffectPoint {
|
|||||||
|
|
||||||
impl EffectPoint {
|
impl EffectPoint {
|
||||||
/// The default slider velocity for a [`DifficultyPoint`]
|
/// The default slider velocity for a [`DifficultyPoint`]
|
||||||
pub const DEFAULT_KIAI: bool = true;
|
pub const DEFAULT_KIAI: bool = false;
|
||||||
|
|
||||||
/// Create a new [`EffectPoint`].
|
/// Create a new [`EffectPoint`].
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
use std::hash::{BuildHasher, Hasher};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
|
||||||
pub(crate) struct BuildByteHasher;
|
|
||||||
|
|
||||||
impl BuildHasher for BuildByteHasher {
|
|
||||||
type Hasher = ByteHasher;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn build_hasher(&self) -> Self::Hasher {
|
|
||||||
ByteHasher { byte: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct ByteHasher {
|
|
||||||
byte: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hasher for ByteHasher {
|
|
||||||
#[inline]
|
|
||||||
fn finish(&self) -> u64 {
|
|
||||||
self.byte as u64
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn write(&mut self, _: &[u8]) {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn write_u8(&mut self, byte: u8) {
|
|
||||||
self.byte = byte;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,8 @@ use std::cmp::Ordering;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
curve::{Curve, CurveBuffers},
|
curve::{Curve, CurveBuffers},
|
||||||
limited_queue::LimitedQueue,
|
|
||||||
parse::{legacy_sort, HitObjectKind, Pos2},
|
parse::{legacy_sort, HitObjectKind, Pos2},
|
||||||
|
util::{FloatExt, LimitedQueue},
|
||||||
Beatmap, GameMode,
|
Beatmap, GameMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,7 +17,6 @@ use self::{
|
|||||||
pattern_type::PatternType,
|
pattern_type::PatternType,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod byte_hasher;
|
|
||||||
mod legacy_random;
|
mod legacy_random;
|
||||||
mod pattern;
|
mod pattern;
|
||||||
mod pattern_generator;
|
mod pattern_generator;
|
||||||
@@ -32,12 +31,14 @@ impl Beatmap {
|
|||||||
let mut n_circles = 0;
|
let mut n_circles = 0;
|
||||||
let mut n_sliders = 0;
|
let mut n_sliders = 0;
|
||||||
|
|
||||||
let seed =
|
let seed = (map.hp + map.cs).round_even() as i32 * 20
|
||||||
(map.hp + map.cs).round() as i32 * 20 + (map.od * 41.2) as i32 + map.ar.round() as i32;
|
+ (map.od * 41.2) as i32
|
||||||
|
+ map.ar.round_even() as i32;
|
||||||
|
|
||||||
let mut random = Random::new(seed);
|
let mut random = Random::new(seed);
|
||||||
|
|
||||||
let rounded_cs = map.cs.round();
|
let rounded_cs = map.cs.round_even();
|
||||||
let rounded_od = map.od.round();
|
let rounded_od = map.od.round_even();
|
||||||
|
|
||||||
let slider_or_spinner_count = self
|
let slider_or_spinner_count = self
|
||||||
.hit_objects
|
.hit_objects
|
||||||
@@ -78,9 +79,7 @@ impl Beatmap {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let total_columns = map.cs as i32;
|
let total_columns = map.cs as i32;
|
||||||
|
|
||||||
let mut last_values = PrevValues::default();
|
let mut last_values = PrevValues::default();
|
||||||
|
|
||||||
let mut curve_bufs = CurveBuffers::default();
|
let mut curve_bufs = CurveBuffers::default();
|
||||||
|
|
||||||
for (obj, sound) in self.hit_objects.iter().zip(self.sounds.iter()) {
|
for (obj, sound) in self.hit_objects.iter().zip(self.sounds.iter()) {
|
||||||
@@ -167,8 +166,8 @@ impl Beatmap {
|
|||||||
&last_values.pattern,
|
&last_values.pattern,
|
||||||
);
|
);
|
||||||
|
|
||||||
last_values.time = obj.start_time;
|
last_values.time = end_time;
|
||||||
last_values.pos = obj.pos;
|
last_values.pos = Pos2 { x: 256.0, y: 192.0 };
|
||||||
|
|
||||||
compute_density(end_time, &mut density);
|
compute_density(end_time, &mut density);
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::parse::{HitObject, HitObjectKind, Pos2};
|
use crate::{
|
||||||
|
parse::{HitObject, HitObjectKind, Pos2},
|
||||||
|
util::ByteHasher,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::pattern_generator::{
|
||||||
byte_hasher::BuildByteHasher,
|
distance_object::DistanceObjectPatternGenerator,
|
||||||
pattern_generator::{
|
end_time_object::EndTimeObjectPatternGenerator, hit_object::HitObjectPatternGenerator,
|
||||||
distance_object::DistanceObjectPatternGenerator,
|
|
||||||
end_time_object::EndTimeObjectPatternGenerator, hit_object::HitObjectPatternGenerator,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct Pattern {
|
pub(crate) struct Pattern {
|
||||||
pub(crate) hit_objects: Vec<HitObject>,
|
pub(crate) hit_objects: Vec<HitObject>,
|
||||||
contained_columns: HashSet<u8, BuildByteHasher>,
|
contained_columns: HashSet<u8, ByteHasher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pattern {
|
impl Pattern {
|
||||||
pub(crate) fn with_capacity(capacity: usize) -> Self {
|
pub(crate) fn with_capacity(capacity: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
hit_objects: Vec::with_capacity(capacity),
|
hit_objects: Vec::with_capacity(capacity),
|
||||||
contained_columns: HashSet::with_hasher(BuildByteHasher),
|
contained_columns: HashSet::with_hasher(ByteHasher),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_single(hit_object: HitObject, column: u8) -> Self {
|
fn new_single(hit_object: HitObject, column: u8) -> Self {
|
||||||
let mut contained_columns = HashSet::with_capacity_and_hasher(1, BuildByteHasher);
|
let mut contained_columns = HashSet::with_capacity_and_hasher(1, ByteHasher);
|
||||||
contained_columns.insert(column);
|
contained_columns.insert(column);
|
||||||
let hit_objects = vec![hit_object];
|
let hit_objects = vec![hit_object];
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::{
|
|||||||
curve::Curve,
|
curve::Curve,
|
||||||
mania::ManiaObject,
|
mania::ManiaObject,
|
||||||
parse::{HitObject, HitSound},
|
parse::{HitObject, HitSound},
|
||||||
|
util::FloatExt,
|
||||||
Beatmap,
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
|
|||||||
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;
|
||||||
let start_time = hit_object.start_time.round() as i32;
|
let start_time = hit_object.start_time.round_even() as i32;
|
||||||
|
|
||||||
// * This matches stable's calculation.
|
// * This matches stable's calculation.
|
||||||
let end_time = (start_time as f64
|
let end_time = (start_time as f64
|
||||||
@@ -97,12 +98,12 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
|
|||||||
let mut end_time_pattern = Pattern::default();
|
let mut end_time_pattern = Pattern::default();
|
||||||
|
|
||||||
for obj in orig_pattern.hit_objects {
|
for obj in orig_pattern.hit_objects {
|
||||||
let column = ManiaObject::new(&obj).column(self.total_columns as f32) as u8;
|
let col = ManiaObject::column(obj.pos.x, self.total_columns as f32) as u8;
|
||||||
|
|
||||||
if self.end_time != obj.end_time().round() as i32 {
|
if self.end_time != obj.end_time().round_even() as i32 {
|
||||||
intermediate_pattern.add_object(obj, column);
|
intermediate_pattern.add_object(obj, col);
|
||||||
} else {
|
} else {
|
||||||
end_time_pattern.add_object(obj, column);
|
end_time_pattern.add_object(obj, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,15 +98,14 @@ impl<'h> HitObjectPatternGenerator<'h> {
|
|||||||
pub(crate) fn generate(&mut self) -> Pattern {
|
pub(crate) fn generate(&mut self) -> Pattern {
|
||||||
let pattern = self.generate_core();
|
let pattern = self.generate_core();
|
||||||
|
|
||||||
for obj in pattern.hit_objects.iter().map(ManiaObject::new) {
|
for obj in pattern.hit_objects.iter() {
|
||||||
if self.convert_type.contains(PatternType::STAIR)
|
let col = ManiaObject::column(obj.pos.x, self.total_columns as f32) as i32;
|
||||||
&& obj.column(self.total_columns as f32) as i32 == self.total_columns - 1
|
|
||||||
{
|
if self.convert_type.contains(PatternType::STAIR) && col == self.total_columns - 1 {
|
||||||
self.stair_type = PatternType::REVERSE_STAIR;
|
self.stair_type = PatternType::REVERSE_STAIR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.convert_type.contains(PatternType::REVERSE_STAIR)
|
if self.convert_type.contains(PatternType::REVERSE_STAIR) && col == self.random_start()
|
||||||
&& obj.column(self.total_columns as f32) as i32 == self.random_start()
|
|
||||||
{
|
{
|
||||||
self.stair_type = PatternType::STAIR;
|
self.stair_type = PatternType::STAIR;
|
||||||
}
|
}
|
||||||
@@ -120,12 +119,9 @@ impl<'h> HitObjectPatternGenerator<'h> {
|
|||||||
return Pattern::new_note(self, 0);
|
return Pattern::new_note(self, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let last_column = self
|
let last_column = self.prev_pattern.hit_objects.last().map_or(0, |h| {
|
||||||
.prev_pattern
|
ManiaObject::column(h.pos.x, self.total_columns as f32) as u8
|
||||||
.hit_objects
|
});
|
||||||
.last()
|
|
||||||
.map(ManiaObject::new)
|
|
||||||
.map_or(0, |h| h.column(self.total_columns as f32) as u8);
|
|
||||||
|
|
||||||
let random_start = self.random_start() as u8;
|
let random_start = self.random_start() as u8;
|
||||||
|
|
||||||
|
|||||||
@@ -21,15 +21,13 @@ trait PatternGenerator {
|
|||||||
fn get_column(&self, allow_special: Option<bool>) -> u8 {
|
fn get_column(&self, allow_special: Option<bool>) -> u8 {
|
||||||
let allow_special = allow_special.unwrap_or(false);
|
let allow_special = allow_special.unwrap_or(false);
|
||||||
|
|
||||||
let res = if allow_special && self.total_columns() == 8 {
|
if allow_special && self.total_columns() == 8 {
|
||||||
const LOCAL_X_DIVISOR: f32 = 512.0 / 7.0;
|
const LOCAL_X_DIVISOR: f32 = 512.0 / 7.0;
|
||||||
|
|
||||||
((self.hit_object().pos.x / LOCAL_X_DIVISOR).floor() as u8).clamp(0, 6) + 1
|
((self.hit_object().pos.x / LOCAL_X_DIVISOR).floor() as u8).clamp(0, 6) + 1
|
||||||
} else {
|
} else {
|
||||||
ManiaObject::new(self.hit_object()).column(self.total_columns() as f32) as u8
|
ManiaObject::column(self.hit_object().pos.x, self.total_columns() as f32) as u8
|
||||||
};
|
}
|
||||||
|
|
||||||
res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_random_note_count(
|
fn get_random_note_count(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ impl fmt::Display for PatternType {
|
|||||||
$(
|
$(
|
||||||
if $self.contains(Self::$pat) {
|
if $self.contains(Self::$pat) {
|
||||||
if $written {
|
if $written {
|
||||||
$f.write_str(" | ")?;
|
$f.write_str(", ")?;
|
||||||
} else {
|
} else {
|
||||||
$written = true;
|
$written = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -1,13 +1,12 @@
|
|||||||
use std::{borrow::Cow, cmp::Ordering};
|
use std::{borrow::Cow, cmp::Ordering};
|
||||||
|
|
||||||
use crate::parse::HitObject;
|
use crate::{parse::HitObject, util::SortedVec};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, BeatmapHitWindows},
|
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, BeatmapHitWindows},
|
||||||
breaks::Break,
|
breaks::Break,
|
||||||
control_points::{DifficultyPoint, EffectPoint, TimingPoint},
|
control_points::{DifficultyPoint, EffectPoint, TimingPoint},
|
||||||
mode::GameMode,
|
mode::GameMode,
|
||||||
sorted_vec::SortedVec,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mod attributes;
|
mod attributes;
|
||||||
@@ -15,7 +14,6 @@ mod breaks;
|
|||||||
mod control_points;
|
mod control_points;
|
||||||
mod converts;
|
mod converts;
|
||||||
mod mode;
|
mod mode;
|
||||||
mod sorted_vec;
|
|
||||||
|
|
||||||
/// The main beatmap struct containing all data relevant
|
/// The main beatmap struct containing all data relevant
|
||||||
/// for difficulty and performance calculation
|
/// for difficulty and performance calculation
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cmp::Ordering,
|
|
||||||
convert::identity,
|
|
||||||
fmt::{Debug, Formatter, Result as FmtResult},
|
|
||||||
ops::Deref,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{control_points::EffectPoint, DifficultyPoint, TimingPoint};
|
|
||||||
|
|
||||||
/// A [`Vec`] whose elements are guaranteed to be in order based on the given comparator.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SortedVec<T> {
|
|
||||||
inner: Vec<T>,
|
|
||||||
cmp: fn(&T, &T) -> Ordering,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> SortedVec<T> {
|
|
||||||
/// If the value is found then [`Result::Ok`] is returned, containing the
|
|
||||||
/// index of the matching element. If there are multiple matches, then any
|
|
||||||
/// one of the matches could be returned.
|
|
||||||
/// If the value is not found then [`Result::Err`] is returned, containing
|
|
||||||
/// the index where a matching element could be inserted while maintaining
|
|
||||||
/// sorted order.
|
|
||||||
pub fn find(&self, value: &T) -> Result<usize, usize> {
|
|
||||||
self.inner
|
|
||||||
.binary_search_by(|probe| (self.cmp)(probe, value))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn push(&mut self, value: T) {
|
|
||||||
let idx = self.find(&value).map_or_else(identity, identity);
|
|
||||||
|
|
||||||
self.inner.insert(idx, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn dedup_by_key<F, K>(&mut self, mut key: F)
|
|
||||||
where
|
|
||||||
F: FnMut(&mut T) -> K,
|
|
||||||
K: PartialEq,
|
|
||||||
{
|
|
||||||
self.inner.dedup_by(|a, b| key(a) == key(b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Deref for SortedVec<T> {
|
|
||||||
type Target = Vec<T>;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Debug> Debug for SortedVec<T> {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
||||||
Debug::fmt(&self.inner, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SortedVec<TimingPoint> {
|
|
||||||
#[inline]
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Vec::new(),
|
|
||||||
cmp: |a, b| a.time.partial_cmp(&b.time).unwrap_or(Ordering::Equal),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SortedVec<DifficultyPoint> {
|
|
||||||
#[inline]
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Vec::new(),
|
|
||||||
cmp: |a, b| a.time.partial_cmp(&b.time).unwrap_or(Ordering::Equal),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
Ok(idx) | Err(Some(idx)) => value.is_redundant(&self[idx]),
|
|
||||||
Err(None) => value.is_redundant(&DifficultyPoint::default()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_redundant {
|
|
||||||
self.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-2
@@ -206,8 +206,8 @@ mod stars;
|
|||||||
pub use stars::AnyStars;
|
pub use stars::AnyStars;
|
||||||
|
|
||||||
mod curve;
|
mod curve;
|
||||||
mod limited_queue;
|
|
||||||
mod mods;
|
mod mods;
|
||||||
|
mod util;
|
||||||
|
|
||||||
pub use catch::{CatchPP, CatchStars};
|
pub use catch::{CatchPP, CatchStars};
|
||||||
pub use mania::{ManiaPP, ManiaStars};
|
pub use mania::{ManiaPP, ManiaStars};
|
||||||
@@ -216,6 +216,7 @@ pub use taiko::{TaikoPP, TaikoStars};
|
|||||||
|
|
||||||
pub use mods::Mods;
|
pub use mods::Mods;
|
||||||
pub use parse::{ParseError, ParseResult};
|
pub use parse::{ParseError, ParseResult};
|
||||||
|
pub use util::SortedVec;
|
||||||
|
|
||||||
/// Provides some additional methods on [`Beatmap`](crate::Beatmap).
|
/// Provides some additional methods on [`Beatmap`](crate::Beatmap).
|
||||||
pub trait BeatmapExt {
|
pub trait BeatmapExt {
|
||||||
@@ -520,7 +521,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom() {
|
fn custom() {
|
||||||
let path = "F:/osu!/beatmaps/1000168.osu";
|
let path = "F:/osu!/beatmaps/34881.osu";
|
||||||
let map = Beatmap::from_path(path).unwrap();
|
let map = Beatmap::from_path(path).unwrap();
|
||||||
|
|
||||||
let attrs = match OsuPP::new(&map).mode(GameMode::Mania).mods(0).calculate() {
|
let attrs = match OsuPP::new(&map).mode(GameMode::Mania).mods(0).calculate() {
|
||||||
|
|||||||
@@ -10,24 +10,13 @@ pub(crate) struct ManiaDifficultyObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ManiaDifficultyObject {
|
impl ManiaDifficultyObject {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(base: &ManiaObject, last: &ManiaObject, clock_rate: f64, idx: usize) -> Self {
|
||||||
base: ManiaObject<'_>,
|
|
||||||
last: ManiaObject<'_>,
|
|
||||||
clock_rate: f64,
|
|
||||||
total_columns: f32,
|
|
||||||
idx: usize,
|
|
||||||
) -> Self {
|
|
||||||
let delta_time = (base.start_time() - last.start_time()) / clock_rate;
|
|
||||||
let start_time = base.start_time() / clock_rate;
|
|
||||||
let end_time = base.end_time() / clock_rate;
|
|
||||||
let base_column = base.column(total_columns);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
idx,
|
idx,
|
||||||
base_column,
|
base_column: base.column,
|
||||||
delta_time,
|
delta_time: (base.start_time - last.start_time) / clock_rate,
|
||||||
start_time,
|
start_time: base.start_time / clock_rate,
|
||||||
end_time,
|
end_time: base.end_time / clock_rate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::{beatmap::BeatmapHitWindows, parse::HitObjectKind, Beatmap, Mods};
|
use crate::{beatmap::BeatmapHitWindows, parse::HitObjectKind, util::FloatExt, Beatmap, Mods};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
difficulty_object::ManiaDifficultyObject,
|
difficulty_object::ManiaDifficultyObject,
|
||||||
|
mania_object::ObjectParameters,
|
||||||
skills::{Skill, Strain},
|
skills::{Skill, Strain},
|
||||||
ManiaDifficultyAttributes, ManiaObject, STAR_SCALING_FACTOR,
|
ManiaDifficultyAttributes, ManiaObject, STAR_SCALING_FACTOR,
|
||||||
};
|
};
|
||||||
@@ -44,12 +45,13 @@ pub struct ManiaGradualDifficultyAttributes<'map> {
|
|||||||
strain: Strain,
|
strain: Strain,
|
||||||
diff_objects: Vec<ManiaDifficultyObject>,
|
diff_objects: Vec<ManiaDifficultyObject>,
|
||||||
curr_combo: usize,
|
curr_combo: usize,
|
||||||
|
clock_rate: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||||
/// Create a new difficulty attributes iterator for osu!mania maps.
|
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||||
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||||
let total_columns = map.cs.round().max(1.0);
|
let total_columns = map.cs.round_even().max(1.0);
|
||||||
|
|
||||||
let clock_rate = mods.clock_rate();
|
let clock_rate = mods.clock_rate();
|
||||||
let strain = Strain::new(total_columns as usize);
|
let strain = Strain::new(total_columns as usize);
|
||||||
@@ -62,16 +64,31 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
|||||||
.clock_rate(clock_rate)
|
.clock_rate(clock_rate)
|
||||||
.hit_windows();
|
.hit_windows();
|
||||||
|
|
||||||
let diff_objects_iter = map
|
let mut params = ObjectParameters::new(map);
|
||||||
.hit_objects
|
let mut hit_objects = map.hit_objects.iter();
|
||||||
.iter()
|
|
||||||
.skip(1)
|
let first = match hit_objects.next() {
|
||||||
.map(ManiaObject::new)
|
Some(h) => ManiaObject::new(h, total_columns, &mut params),
|
||||||
.enumerate()
|
None => {
|
||||||
.zip(map.hit_objects.iter().map(ManiaObject::new))
|
return Self {
|
||||||
.map(|((i, base), prev)| {
|
idx: 0,
|
||||||
ManiaDifficultyObject::new(base, prev, clock_rate, total_columns, i)
|
map,
|
||||||
});
|
hit_window,
|
||||||
|
strain,
|
||||||
|
diff_objects: Vec::new(),
|
||||||
|
curr_combo: 0,
|
||||||
|
clock_rate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
|
||||||
|
let base = ManiaObject::new(h, total_columns, &mut params);
|
||||||
|
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
|
||||||
|
*last = base;
|
||||||
|
|
||||||
|
Some(diff_object)
|
||||||
|
});
|
||||||
|
|
||||||
let curr_combo = if let Some(h) = map.hit_objects.first() {
|
let curr_combo = if let Some(h) = map.hit_objects.first() {
|
||||||
match &h.kind {
|
match &h.kind {
|
||||||
@@ -94,6 +111,7 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
|||||||
strain,
|
strain,
|
||||||
diff_objects,
|
diff_objects,
|
||||||
curr_combo,
|
curr_combo,
|
||||||
|
clock_rate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,10 +125,14 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
|||||||
|
|
||||||
if let Some(h) = self.map.hit_objects.get(self.idx) {
|
if let Some(h) = self.map.hit_objects.get(self.idx) {
|
||||||
match &h.kind {
|
match &h.kind {
|
||||||
HitObjectKind::Hold { end_time } => {
|
HitObjectKind::Circle => self.curr_combo += 1,
|
||||||
self.curr_combo += 1 + ((*end_time - h.start_time) / 100.0) as usize
|
_ => {
|
||||||
|
let start_time = curr.start_time * self.clock_rate;
|
||||||
|
let end_time = curr.end_time * self.clock_rate;
|
||||||
|
let duration = end_time - start_time;
|
||||||
|
|
||||||
|
self.curr_combo += 1 + (duration / 100.0) as usize;
|
||||||
}
|
}
|
||||||
_ => self.curr_combo += 1,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+88
-17
@@ -1,27 +1,98 @@
|
|||||||
use crate::parse::HitObject;
|
use crate::{
|
||||||
|
curve::{Curve, CurveBuffers},
|
||||||
|
parse::{HitObject, HitObjectKind},
|
||||||
|
Beatmap,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) struct ManiaObject<'h> {
|
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||||
hit_object: &'h HitObject,
|
|
||||||
|
pub(crate) struct ObjectParameters<'a> {
|
||||||
|
pub(crate) map: &'a Beatmap,
|
||||||
|
pub(crate) max_combo: usize,
|
||||||
|
pub(crate) curve_bufs: CurveBuffers,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'h> ManiaObject<'h> {
|
impl<'a> ObjectParameters<'a> {
|
||||||
pub(crate) fn new(hit_object: &'h HitObject) -> Self {
|
pub(crate) fn new(map: &'a Beatmap) -> Self {
|
||||||
Self { hit_object }
|
Self {
|
||||||
|
map,
|
||||||
|
max_combo: 0,
|
||||||
|
curve_bufs: CurveBuffers::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn start_time(&self) -> f64 {
|
pub(crate) struct ManiaObject {
|
||||||
self.hit_object.start_time
|
pub(crate) start_time: f64,
|
||||||
}
|
pub(crate) end_time: f64,
|
||||||
|
pub(crate) column: usize,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn end_time(&self) -> f64 {
|
impl ManiaObject {
|
||||||
self.hit_object.end_time()
|
pub(crate) fn column(x: f32, total_columns: f32) -> usize {
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn column(&self, total_columns: f32) -> usize {
|
|
||||||
let x_divisor = 512.0 / total_columns;
|
let x_divisor = 512.0 / total_columns;
|
||||||
|
|
||||||
(self.hit_object.pos.x / x_divisor)
|
(x / x_divisor).floor().min(total_columns - 1.0) as usize
|
||||||
.floor()
|
}
|
||||||
.min(total_columns - 1.0) as usize
|
|
||||||
|
pub(crate) fn new(
|
||||||
|
h: &HitObject,
|
||||||
|
total_columns: f32,
|
||||||
|
params: &mut ObjectParameters<'_>,
|
||||||
|
) -> Self {
|
||||||
|
let ObjectParameters {
|
||||||
|
map,
|
||||||
|
max_combo,
|
||||||
|
curve_bufs,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
let column = Self::column(h.pos.x, total_columns);
|
||||||
|
*max_combo += 1;
|
||||||
|
|
||||||
|
match &h.kind {
|
||||||
|
HitObjectKind::Circle => Self {
|
||||||
|
start_time: h.start_time,
|
||||||
|
end_time: h.start_time,
|
||||||
|
column,
|
||||||
|
},
|
||||||
|
HitObjectKind::Slider {
|
||||||
|
pixel_len,
|
||||||
|
repeats,
|
||||||
|
control_points,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let span_count = *repeats as f64 + 1.0;
|
||||||
|
|
||||||
|
let curve = Curve::new(control_points, *pixel_len, curve_bufs);
|
||||||
|
let dist = curve.dist();
|
||||||
|
|
||||||
|
let timing_point = map.timing_point_at(h.start_time);
|
||||||
|
let difficulty_point = map.difficulty_point_at(h.start_time).unwrap_or_default();
|
||||||
|
|
||||||
|
let scoring_dist =
|
||||||
|
BASE_SCORING_DISTANCE * map.slider_mult * difficulty_point.slider_vel;
|
||||||
|
|
||||||
|
let vel = scoring_dist / timing_point.beat_len;
|
||||||
|
let duration = span_count * dist / vel;
|
||||||
|
let end_time = h.start_time + duration;
|
||||||
|
|
||||||
|
*max_combo += (duration / 100.0) as usize;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
start_time: h.start_time,
|
||||||
|
end_time,
|
||||||
|
column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HitObjectKind::Spinner { end_time } | HitObjectKind::Hold { end_time } => {
|
||||||
|
*max_combo += ((*end_time - h.start_time) / 100.0) as usize;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
start_time: h.start_time,
|
||||||
|
end_time: *end_time,
|
||||||
|
column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-20
@@ -7,7 +7,7 @@ mod skills;
|
|||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::{beatmap::BeatmapHitWindows, parse::HitObjectKind, Beatmap, GameMode, Mods, OsuStars};
|
use crate::{beatmap::BeatmapHitWindows, util::FloatExt, Beatmap, GameMode, Mods, OsuStars};
|
||||||
|
|
||||||
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
|
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ pub(crate) use self::mania_object::ManiaObject;
|
|||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
difficulty_object::ManiaDifficultyObject,
|
difficulty_object::ManiaDifficultyObject,
|
||||||
|
mania_object::ObjectParameters,
|
||||||
skills::{Skill, Strain},
|
skills::{Skill, Strain},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -169,29 +170,30 @@ fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
||||||
let total_columns = map.cs.round().max(1.0);
|
let total_columns = map.cs.round_even().max(1.0);
|
||||||
|
|
||||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||||
let mut strain = Strain::new(total_columns as usize);
|
let mut strain = Strain::new(total_columns as usize);
|
||||||
let mut max_combo = 0;
|
let mut params = ObjectParameters::new(map.as_ref());
|
||||||
|
let mut hit_objects = map.hit_objects.iter();
|
||||||
|
|
||||||
let diff_objects_iter = map
|
let first = match hit_objects.next() {
|
||||||
.hit_objects
|
Some(h) => ManiaObject::new(h, total_columns, &mut params),
|
||||||
.iter()
|
None => {
|
||||||
.take(take)
|
return ManiaResult {
|
||||||
.inspect(|h| match &h.kind {
|
strain,
|
||||||
HitObjectKind::Hold { end_time } => {
|
max_combo: 0,
|
||||||
max_combo += 1 + ((*end_time - h.start_time) / 100.0) as usize
|
|
||||||
}
|
}
|
||||||
_ => max_combo += 1,
|
}
|
||||||
})
|
};
|
||||||
.skip(1)
|
|
||||||
.map(ManiaObject::new)
|
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
|
||||||
.enumerate()
|
let base = ManiaObject::new(h, total_columns, &mut params);
|
||||||
.zip(map.hit_objects.iter().map(ManiaObject::new))
|
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
|
||||||
.map(|((i, base), prev)| {
|
*last = base;
|
||||||
ManiaDifficultyObject::new(base, prev, clock_rate, total_columns, i)
|
|
||||||
});
|
Some(diff_object)
|
||||||
|
});
|
||||||
|
|
||||||
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().min(take).saturating_sub(1));
|
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().min(take).saturating_sub(1));
|
||||||
diff_objects.extend(diff_objects_iter);
|
diff_objects.extend(diff_objects_iter);
|
||||||
@@ -200,7 +202,10 @@ fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
|
|||||||
strain.process(curr, &diff_objects);
|
strain.process(curr, &diff_objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
ManiaResult { strain, max_combo }
|
ManiaResult {
|
||||||
|
strain,
|
||||||
|
max_combo: params.max_combo,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ManiaResult {
|
struct ManiaResult {
|
||||||
|
|||||||
@@ -96,12 +96,8 @@ impl<'map> ManiaPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: update
|
|
||||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||||
///
|
///
|
||||||
/// Be sure you also set [`score`](ManiaPP::score) or the final values
|
|
||||||
/// won't be correct because it will incorrectly assume a score of 1,000,000.
|
|
||||||
///
|
|
||||||
/// If you want to calculate the performance after every few objects, instead of
|
/// If you want to calculate the performance after every few objects, instead of
|
||||||
/// using [`ManiaPP`] multiple times with different `passed_objects`, you should use
|
/// using [`ManiaPP`] multiple times with different `passed_objects`, you should use
|
||||||
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes).
|
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes).
|
||||||
|
|||||||
+171
-93
@@ -14,7 +14,7 @@ pub use slider_parsing::*;
|
|||||||
use reader::FileReader;
|
use reader::FileReader;
|
||||||
pub(crate) use sort::legacy_sort;
|
pub(crate) use sort::legacy_sort;
|
||||||
|
|
||||||
use std::{cmp::Ordering, ops::Neg};
|
use std::{cmp::Ordering, ops::Neg, str::FromStr};
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
use std::{fs::File, io::Read};
|
use std::{fs::File, io::Read};
|
||||||
@@ -28,11 +28,10 @@ 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, EffectPoint, GameMode, TimingPoint};
|
use crate::{
|
||||||
|
beatmap::{Beatmap, Break, DifficultyPoint, EffectPoint, GameMode, TimingPoint},
|
||||||
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
util::TandemSorter,
|
||||||
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
};
|
||||||
}
|
|
||||||
|
|
||||||
trait OptionExt<T> {
|
trait OptionExt<T> {
|
||||||
fn next_field(self, field: &'static str) -> Result<T, ParseError>;
|
fn next_field(self, field: &'static str) -> Result<T, ParseError>;
|
||||||
@@ -44,13 +43,27 @@ impl<T> OptionExt<T> for Option<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait InRange: Sized + Copy + Neg<Output = Self> + PartialOrd {
|
trait InRange: Sized + Copy + Neg<Output = Self> + PartialOrd + FromStr {
|
||||||
const LIMIT: Self;
|
const LIMIT: Self;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn parse_in_range(s: &str) -> Option<Self> {
|
||||||
|
s.parse().ok().filter(<Self as InRange>::is_in_range)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn parse_in_custom_range(s: &str, limit: Self) -> Option<Self> {
|
||||||
|
s.parse()
|
||||||
|
.ok()
|
||||||
|
.filter(|this| <Self as InRange>::is_in_custom_range(this, limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn is_in_range(&self) -> bool {
|
fn is_in_range(&self) -> bool {
|
||||||
(-Self::LIMIT..=Self::LIMIT).contains(self)
|
(-Self::LIMIT..=Self::LIMIT).contains(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn is_in_custom_range(&self, limit: Self) -> bool {
|
fn is_in_custom_range(&self, limit: Self) -> bool {
|
||||||
(-limit..=limit).contains(self)
|
(-limit..=limit).contains(self)
|
||||||
}
|
}
|
||||||
@@ -125,7 +138,7 @@ macro_rules! parse_general_body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if key == b"StackLeniency" {
|
if key == b"StackLeniency" {
|
||||||
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
if let Some(val) = f32::parse_in_range(value) {
|
||||||
stack_leniency = Some(val);
|
stack_leniency = Some(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,32 +173,32 @@ macro_rules! parse_difficulty_body {
|
|||||||
|
|
||||||
match key {
|
match key {
|
||||||
b"ApproachRate" => {
|
b"ApproachRate" => {
|
||||||
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
if let Some(val) = f32::parse_in_range(value) {
|
||||||
ar = Some(val);
|
ar = Some(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b"OverallDifficulty" => {
|
b"OverallDifficulty" => {
|
||||||
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
if let Some(val) = f32::parse_in_range(value) {
|
||||||
od = Some(val);
|
od = Some(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b"CircleSize" => {
|
b"CircleSize" => {
|
||||||
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
if let Some(val) = f32::parse_in_range(value) {
|
||||||
cs = Some(val);
|
cs = Some(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b"HPDrainRate" => {
|
b"HPDrainRate" => {
|
||||||
if let Some(val) = value.parse().ok().filter(f32::is_in_range) {
|
if let Some(val) = f32::parse_in_range(value) {
|
||||||
hp = Some(val);
|
hp = Some(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b"SliderTickRate" => {
|
b"SliderTickRate" => {
|
||||||
if let Some(val) = value.parse().ok().filter(f64::is_in_range) {
|
if let Some(val) = f64::parse_in_range(value) {
|
||||||
tick_rate = Some(val);
|
tick_rate = Some(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b"SliderMultiplier" => {
|
b"SliderMultiplier" => {
|
||||||
if let Some(val) = value.parse().ok().filter(f64::is_in_range) {
|
if let Some(val) = f64::parse_in_range(value) {
|
||||||
sv = Some(val);
|
sv = Some(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,17 +241,13 @@ macro_rules! parse_events_body {
|
|||||||
if let Some(b'2') = split.next().and_then(|value| value.bytes().next()) {
|
if let Some(b'2') = split.next().and_then(|value| value.bytes().next()) {
|
||||||
let start_time = split
|
let start_time = split
|
||||||
.next()
|
.next()
|
||||||
.next_field("break start")?
|
.next_field("break start")
|
||||||
.parse()
|
.map(f64::parse_in_range)?;
|
||||||
.ok()
|
|
||||||
.filter(f64::is_in_range);
|
|
||||||
|
|
||||||
let end_time = split
|
let end_time = split
|
||||||
.next()
|
.next()
|
||||||
.next_field("break end")?
|
.next_field("break end")
|
||||||
.parse()
|
.map(f64::parse_in_range)?;
|
||||||
.ok()
|
|
||||||
.filter(f64::is_in_range);
|
|
||||||
|
|
||||||
if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
|
if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
|
||||||
$self.breaks.push(Break {
|
$self.breaks.push(Break {
|
||||||
@@ -270,15 +279,16 @@ macro_rules! parse_timingpoints_body {
|
|||||||
let line = $reader.get_line()?;
|
let line = $reader.get_line()?;
|
||||||
let mut split = line.split(',');
|
let mut split = line.split(',');
|
||||||
|
|
||||||
let time: f64 = split
|
let time_opt = split
|
||||||
.next()
|
.next()
|
||||||
.next_field("timing point time")?
|
.next_field("timing point time")
|
||||||
.trim()
|
.map(str::trim)
|
||||||
.parse()?;
|
.map(f64::parse_in_range)?;
|
||||||
|
|
||||||
if !time.is_in_range() {
|
let time = match time_opt {
|
||||||
continue;
|
Some(time) => time,
|
||||||
}
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
// * beatLength is allowed to be NaN to handle an edge case in which
|
// * beatLength is allowed to be NaN to handle an edge case in which
|
||||||
// * some beatmaps use NaN slider velocity to disable slider tick
|
// * some beatmaps use NaN slider velocity to disable slider tick
|
||||||
@@ -308,35 +318,30 @@ macro_rules! parse_timingpoints_body {
|
|||||||
match split
|
match split
|
||||||
.next()
|
.next()
|
||||||
.filter(|&sig| !sig.starts_with('0'))
|
.filter(|&sig| !sig.starts_with('0'))
|
||||||
.map(str::parse::<i32>)
|
.map(i32::parse_in_range)
|
||||||
{
|
{
|
||||||
Some(Ok(time_sig)) if !time_sig.is_in_range() || time_sig < 1 => {
|
Some(Some(time_sig)) if time_sig < 1 => return Status::Err,
|
||||||
return Status::Err
|
Some(Some(_)) => {}
|
||||||
}
|
|
||||||
Some(Ok(_)) => {}
|
|
||||||
None => return Status::Ok,
|
None => return Status::Ok,
|
||||||
Some(Err(_)) => return Status::Err,
|
Some(None) => return Status::Err,
|
||||||
}
|
}
|
||||||
|
|
||||||
match split.next().map(str::parse::<i32>) {
|
match split.next().map(i32::parse_in_range) {
|
||||||
Some(Ok(sample_set)) if !sample_set.is_in_range() => return Status::Err,
|
Some(Some(_)) => {}
|
||||||
Some(Ok(_)) => {}
|
Some(None) => return Status::Err,
|
||||||
None => return Status::Ok,
|
None => return Status::Ok,
|
||||||
Some(Err(_)) => return Status::Err,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match split.next().map(str::parse::<i32>) {
|
match split.next().map(i32::parse_in_range) {
|
||||||
Some(Ok(custom_sample)) if !custom_sample.is_in_range() => return Status::Err,
|
Some(Some(_)) => {}
|
||||||
Some(Ok(_)) => {}
|
Some(None) => return Status::Err,
|
||||||
None => return Status::Ok,
|
None => return Status::Ok,
|
||||||
Some(Err(_)) => return Status::Err,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match split.next().map(str::parse::<i32>) {
|
match split.next().map(i32::parse_in_range) {
|
||||||
Some(Ok(sample_volume)) if !sample_volume.is_in_range() => return Status::Err,
|
Some(Some(_)) => {}
|
||||||
Some(Ok(_)) => {}
|
Some(None) => return Status::Err,
|
||||||
None => return Status::Ok,
|
None => return Status::Ok,
|
||||||
Some(Err(_)) => return Status::Err,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(byte) = split.next().and_then(|value| value.bytes().next()) {
|
if let Some(byte) = split.next().and_then(|value| value.bytes().next()) {
|
||||||
@@ -345,11 +350,10 @@ macro_rules! parse_timingpoints_body {
|
|||||||
return Status::Ok;
|
return Status::Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
match split.next().map(str::parse::<i32>) {
|
match split.next().map(i32::parse_in_range) {
|
||||||
Some(Ok(effect_flags)) if !effect_flags.is_in_range() => return Status::Err,
|
Some(Some(effect_flags)) => *kiai = (effect_flags & KIAI_FLAG) > 0,
|
||||||
Some(Ok(effect_flags)) => *kiai = (effect_flags & KIAI_FLAG) > 0,
|
Some(None) => return Status::Err,
|
||||||
None => return Status::Ok,
|
None => return Status::Ok,
|
||||||
Some(Err(_)) => return Status::Err,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Status::Ok
|
Status::Ok
|
||||||
@@ -393,10 +397,6 @@ macro_rules! parse_timingpoints_body {
|
|||||||
$self.difficulty_points.push_if_not_redundant(point);
|
$self.difficulty_points.push_if_not_redundant(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
$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)
|
Ok(empty)
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
@@ -428,18 +428,14 @@ macro_rules! parse_hitobjects_body {
|
|||||||
|
|
||||||
let x = split
|
let x = split
|
||||||
.next()
|
.next()
|
||||||
.next_field("x pos")?
|
.next_field("x pos")
|
||||||
.parse()
|
.map(|s| f32::parse_in_custom_range(s, MAX_COORDINATE_VALUE as f32))?
|
||||||
.ok()
|
|
||||||
.filter(|x| f32::is_in_custom_range(x, MAX_COORDINATE_VALUE as f32))
|
|
||||||
.map(|x| x as i32 as f32);
|
.map(|x| x as i32 as f32);
|
||||||
|
|
||||||
let y = split
|
let y = split
|
||||||
.next()
|
.next()
|
||||||
.next_field("y pos")?
|
.next_field("y pos")
|
||||||
.parse()
|
.map(|s| f32::parse_in_custom_range(s, MAX_COORDINATE_VALUE as f32))?
|
||||||
.ok()
|
|
||||||
.filter(|x| f32::is_in_custom_range(x, MAX_COORDINATE_VALUE as f32))
|
|
||||||
.map(|x| x as i32 as f32);
|
.map(|x| x as i32 as f32);
|
||||||
|
|
||||||
let pos = if let (Some(x), Some(y)) = (x, y) {
|
let pos = if let (Some(x), Some(y)) = (x, y) {
|
||||||
@@ -450,11 +446,9 @@ macro_rules! parse_hitobjects_body {
|
|||||||
|
|
||||||
let time_opt = split
|
let time_opt = split
|
||||||
.next()
|
.next()
|
||||||
.next_field("hitobject time")?
|
.next_field("hitobject time")
|
||||||
.trim()
|
.map(str::trim)
|
||||||
.parse()
|
.map(f64::parse_in_range)?;
|
||||||
.ok()
|
|
||||||
.filter(f64::is_in_range);
|
|
||||||
|
|
||||||
let time = match time_opt {
|
let time = match time_opt {
|
||||||
Some(time) => time,
|
Some(time) => time,
|
||||||
@@ -470,12 +464,61 @@ macro_rules! parse_hitobjects_body {
|
|||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let sound: u8 = match split.next().next_field("sound")?.parse() {
|
let mut sound: u8 = match split.next().next_field("sound")?.parse() {
|
||||||
Ok(sound) => sound,
|
Ok(sound) => sound,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Status {
|
||||||
|
Ok(bool),
|
||||||
|
Skip,
|
||||||
|
Err(ParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_custom_sound_file(bank_info: Option<&str>) -> Status {
|
||||||
|
let mut split = match bank_info {
|
||||||
|
Some(s) if !s.is_empty() => s.split(':'),
|
||||||
|
_ => return Status::Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
match split.next().map(i32::parse_in_range) {
|
||||||
|
Some(Some(_)) => {}
|
||||||
|
Some(None) => return Status::Skip,
|
||||||
|
None => return Status::Err(ParseError::MissingField("normal set")),
|
||||||
|
}
|
||||||
|
|
||||||
|
match split.next().map(i32::parse_in_range) {
|
||||||
|
Some(Some(_)) => {}
|
||||||
|
Some(None) => return Status::Skip,
|
||||||
|
None => return Status::Err(ParseError::MissingField("additional set")),
|
||||||
|
}
|
||||||
|
|
||||||
|
match split.next().map(i32::parse_in_range) {
|
||||||
|
Some(Some(_)) => {}
|
||||||
|
None => return Status::Ok(false),
|
||||||
|
Some(None) => return Status::Skip,
|
||||||
|
}
|
||||||
|
|
||||||
|
match split.next().map(i32::parse_in_range) {
|
||||||
|
Some(Some(_)) => {}
|
||||||
|
None => return Status::Ok(false),
|
||||||
|
Some(None) => return Status::Skip,
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = split.next().filter(|filename| !filename.is_empty());
|
||||||
|
|
||||||
|
Status::Ok(filename.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
let kind = if kind & Self::CIRCLE_FLAG > 0 {
|
let kind = if kind & Self::CIRCLE_FLAG > 0 {
|
||||||
|
match has_custom_sound_file(split.next()) {
|
||||||
|
Status::Ok(false) => {}
|
||||||
|
Status::Ok(true) => sound = 0,
|
||||||
|
Status::Skip => continue,
|
||||||
|
Status::Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
|
||||||
$self.n_circles += 1;
|
$self.n_circles += 1;
|
||||||
|
|
||||||
HitObjectKind::Circle
|
HitObjectKind::Circle
|
||||||
@@ -548,14 +591,16 @@ macro_rules! parse_hitobjects_body {
|
|||||||
if control_points.is_empty() {
|
if control_points.is_empty() {
|
||||||
HitObjectKind::Circle
|
HitObjectKind::Circle
|
||||||
} else {
|
} else {
|
||||||
let pixel_len = match split.next().map(str::parse::<f64>) {
|
let pixel_len = match split
|
||||||
Some(Ok(len)) if len.is_in_custom_range(MAX_COORDINATE_VALUE as f64) => {
|
.next()
|
||||||
(len > 0.0).then_some(len)
|
.map(|s| f64::parse_in_custom_range(s, MAX_COORDINATE_VALUE as f64))
|
||||||
}
|
{
|
||||||
Some(_) => continue,
|
Some(Some(len)) => (len > 0.0).then_some(len),
|
||||||
|
Some(None) => continue,
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Note: Edge sets are currently not considered, seems to be fine though.
|
||||||
let edge_sounds_opt = split.next().map(|sounds| {
|
let edge_sounds_opt = split.next().map(|sounds| {
|
||||||
sounds
|
sounds
|
||||||
.split('|')
|
.split('|')
|
||||||
@@ -569,6 +614,13 @@ macro_rules! parse_hitobjects_body {
|
|||||||
Some(sounds) => sounds,
|
Some(sounds) => sounds,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
match has_custom_sound_file(split.nth(1)) {
|
||||||
|
Status::Ok(false) => {}
|
||||||
|
Status::Ok(true) => sound = 0,
|
||||||
|
Status::Skip => continue,
|
||||||
|
Status::Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
|
||||||
HitObjectKind::Slider {
|
HitObjectKind::Slider {
|
||||||
repeats,
|
repeats,
|
||||||
pixel_len,
|
pixel_len,
|
||||||
@@ -580,21 +632,37 @@ macro_rules! parse_hitobjects_body {
|
|||||||
$self.n_spinners += 1;
|
$self.n_spinners += 1;
|
||||||
|
|
||||||
let end_time = match split.next().next_field("spinner endtime")?.parse::<f64>() {
|
let end_time = match split.next().next_field("spinner endtime")?.parse::<f64>() {
|
||||||
Ok(end_time) => end_time.max(0.0),
|
Ok(end_time) => end_time.max(time),
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
match has_custom_sound_file(split.next()) {
|
||||||
|
Status::Ok(false) => {}
|
||||||
|
Status::Ok(true) => sound = 0,
|
||||||
|
Status::Skip => continue,
|
||||||
|
Status::Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
|
||||||
HitObjectKind::Spinner { end_time }
|
HitObjectKind::Spinner { end_time }
|
||||||
} else if kind & Self::HOLD_FLAG > 0 {
|
} else if kind & Self::HOLD_FLAG > 0 {
|
||||||
$self.n_sliders += 1;
|
$self.n_sliders += 1;
|
||||||
|
|
||||||
let end_time = match split
|
let end_time = match split.next().and_then(|s| s.split_once(':')) {
|
||||||
.next()
|
Some((head, tail)) => {
|
||||||
.and_then(|next| next.split(':').next())
|
let parsed = match f64::parse_in_range(head) {
|
||||||
.map(str::parse::<f64>)
|
Some(time_) => time_.max(time),
|
||||||
{
|
None => continue,
|
||||||
Some(Ok(time_)) if time_.is_in_range() => time_.max(time),
|
};
|
||||||
Some(_) => continue,
|
|
||||||
|
match has_custom_sound_file(Some(tail)) {
|
||||||
|
Status::Ok(false) => {}
|
||||||
|
Status::Ok(true) => sound = 0,
|
||||||
|
Status::Skip => continue,
|
||||||
|
Status::Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed
|
||||||
|
}
|
||||||
None => time,
|
None => time,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -614,18 +682,28 @@ macro_rules! parse_hitobjects_body {
|
|||||||
prev_time = time;
|
prev_time = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG: If [General] section comes after [HitObjects] then the mode
|
match $self.mode {
|
||||||
// won't be set yet so mania objects won't be sorted properly
|
GameMode::Osu | GameMode::Taiko | GameMode::Catch if !unsorted => {}
|
||||||
if $self.mode == GameMode::Mania {
|
GameMode::Osu | GameMode::Taiko => {
|
||||||
// First a _stable_ sort by time
|
// Sort both hitobjects and hitsounds
|
||||||
$self
|
let mut sorter = TandemSorter::new(&$self.hit_objects);
|
||||||
.hit_objects
|
sorter.sort(&mut $self.hit_objects);
|
||||||
.sort_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
sorter.toggle_marks();
|
||||||
|
sorter.sort(&mut $self.sounds);
|
||||||
|
}
|
||||||
|
// No need to sort hitsounds for the rest
|
||||||
|
GameMode::Mania => {
|
||||||
|
// First a _stable_ sort by time
|
||||||
|
$self
|
||||||
|
.hit_objects
|
||||||
|
.sort_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
||||||
|
|
||||||
// Then the legacy sort for correct position order
|
// Then the legacy sort for correct position order
|
||||||
legacy_sort(&mut $self.hit_objects);
|
legacy_sort(&mut $self.hit_objects);
|
||||||
} else if unsorted {
|
}
|
||||||
sort_unstable(&mut $self.hit_objects);
|
GameMode::Catch => $self
|
||||||
|
.hit_objects
|
||||||
|
.sort_unstable_by(|h1, h2| h1.partial_cmp(h2).unwrap_or(Ordering::Equal)),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(empty)
|
Ok(empty)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
limited_queue::LimitedQueue,
|
|
||||||
taiko::difficulty_object::{HitObjectRhythm, ObjectLists, TaikoDifficultyObject},
|
taiko::difficulty_object::{HitObjectRhythm, ObjectLists, TaikoDifficultyObject},
|
||||||
|
util::LimitedQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Skill, StrainDecaySkill, StrainSkill};
|
use super::{Skill, StrainDecaySkill, StrainSkill};
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use std::hash::{BuildHasher, Hasher};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Default)]
|
||||||
|
pub(crate) struct ByteHasher;
|
||||||
|
|
||||||
|
impl BuildHasher for ByteHasher {
|
||||||
|
type Hasher = ByteHash;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn build_hasher(&self) -> Self::Hasher {
|
||||||
|
ByteHash { byte: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ByteHash {
|
||||||
|
byte: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hasher for ByteHash {
|
||||||
|
#[inline]
|
||||||
|
fn finish(&self) -> u64 {
|
||||||
|
self.byte as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn write(&mut self, _: &[u8]) {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn write_u8(&mut self, byte: u8) {
|
||||||
|
self.byte = byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::hash::{BuildHasher, Hash};
|
||||||
|
|
||||||
|
use super::ByteHasher;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hashes_byte() {
|
||||||
|
let mut state = ByteHasher.build_hasher();
|
||||||
|
42_u8.hash(&mut state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn doesnt_hash_int() {
|
||||||
|
let mut state = ByteHasher.build_hasher();
|
||||||
|
42_i32.hash(&mut state);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
pub(crate) trait FloatExt: Sized {
|
||||||
|
// Workaround since rust rounds ties away from 0.0
|
||||||
|
// while C# rounds them to the nearest even integer.
|
||||||
|
// See github
|
||||||
|
// - https://github.com/rust-lang/rust/issues/96710
|
||||||
|
// - https://github.com/rust-lang/rust/pull/82273
|
||||||
|
fn round_even(self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FloatExt for f32 {
|
||||||
|
#[inline]
|
||||||
|
fn round_even(self) -> Self {
|
||||||
|
if (0.5 - self.fract().abs()).abs() <= f32::EPSILON {
|
||||||
|
2.0 * (self / 2.0).round()
|
||||||
|
} else {
|
||||||
|
self.round()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FloatExt for f64 {
|
||||||
|
#[inline]
|
||||||
|
fn round_even(self) -> Self {
|
||||||
|
if (0.5 - self.fract().abs()).abs() <= f64::EPSILON {
|
||||||
|
2.0 * (self / 2.0).round()
|
||||||
|
} else {
|
||||||
|
self.round()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn round_even() {
|
||||||
|
let values = vec![
|
||||||
|
(3.0, 3.0),
|
||||||
|
(3.5, 4.0),
|
||||||
|
(4.5, 4.0),
|
||||||
|
(4.500001, 5.0),
|
||||||
|
(3.499999, 3.0),
|
||||||
|
(3.500001, 4.0),
|
||||||
|
(100_000_000_002.5, 100_000_000_002.0),
|
||||||
|
(-3.0, -3.0),
|
||||||
|
(-3.5, -4.0),
|
||||||
|
(-4.5, -4.0),
|
||||||
|
(-4.500001, -5.0),
|
||||||
|
(-3.499999, -3.0),
|
||||||
|
(-3.500001, -4.0),
|
||||||
|
(-100_000_000_002.5, -100_000_000_002.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (value, expected) in values {
|
||||||
|
let rounded = <f32 as super::FloatExt>::round_even(value);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(rounded - expected).abs() <= f32::EPSILON,
|
||||||
|
"expected {} for {}; got {}",
|
||||||
|
expected,
|
||||||
|
value,
|
||||||
|
rounded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,7 +132,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(queue.last(), Some(&5));
|
assert_eq!(queue.last(), Some(&5));
|
||||||
assert!(queue.iter().eq(vec![2, 3, 4, 5].iter()));
|
assert!(queue.iter().eq(&[2, 3, 4, 5]));
|
||||||
assert_eq!(queue[0], 2);
|
assert_eq!(queue[0], 2);
|
||||||
assert_eq!(queue[3], 5);
|
assert_eq!(queue[3], 5);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
mod byte_hasher;
|
||||||
|
mod float_ext;
|
||||||
|
mod limited_queue;
|
||||||
|
mod sorted_vec;
|
||||||
|
mod tandem_sort;
|
||||||
|
|
||||||
|
pub use self::sorted_vec::SortedVec;
|
||||||
|
|
||||||
|
pub(crate) use self::{
|
||||||
|
byte_hasher::ByteHasher, float_ext::FloatExt, limited_queue::LimitedQueue,
|
||||||
|
tandem_sort::TandemSorter,
|
||||||
|
};
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
fmt::{Debug, Formatter, Result as FmtResult},
|
||||||
|
ops::{Deref, Index},
|
||||||
|
slice::SliceIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::beatmap::{DifficultyPoint, EffectPoint, TimingPoint};
|
||||||
|
|
||||||
|
/// A [`Vec`] whose elements are guaranteed to be in order based on the given comparator.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SortedVec<T> {
|
||||||
|
inner: Vec<T>,
|
||||||
|
cmp: fn(&T, &T) -> Ordering,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SortedVec<T> {
|
||||||
|
/// Same as [`slice::binary_search_by`] with the internal compare function
|
||||||
|
#[inline]
|
||||||
|
pub fn find(&self, value: &T) -> Result<usize, usize> {
|
||||||
|
self.inner
|
||||||
|
.binary_search_by(|probe| (self.cmp)(probe, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the inner [`Vec`].
|
||||||
|
#[inline]
|
||||||
|
pub fn into_inner(self) -> Vec<T> {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a new value into the sorted list.
|
||||||
|
/// If there is already an element that matches the new value,
|
||||||
|
/// the old element will be replaced.
|
||||||
|
pub(crate) fn push(&mut self, value: T) {
|
||||||
|
match self.find(&value) {
|
||||||
|
Ok(i) => self.inner[i] = value,
|
||||||
|
Err(i) if i == self.inner.len() => self.inner.push(value),
|
||||||
|
Err(i) => self.inner.insert(i, value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for SortedVec<T> {
|
||||||
|
type Target = [T];
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
<Vec<T> as Deref>::deref(&self.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, I> Index<I> for SortedVec<T>
|
||||||
|
where
|
||||||
|
I: SliceIndex<[T]>,
|
||||||
|
{
|
||||||
|
type Output = <I as SliceIndex<[T]>>::Output;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn index(&self, index: I) -> &Self::Output {
|
||||||
|
<Vec<T> as Index<I>>::index(&self.inner, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> Debug for SortedVec<T> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
<Vec<T> as Debug>::fmt(&self.inner, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for SortedVec<T>
|
||||||
|
where
|
||||||
|
T: Ord,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Vec::default(),
|
||||||
|
cmp: <T as Ord>::cmp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_default_control_point {
|
||||||
|
( $( $ty:ident ),* ) => {
|
||||||
|
$(
|
||||||
|
impl Default for SortedVec<$ty> {
|
||||||
|
#[inline]
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Vec::default(),
|
||||||
|
cmp: |a, b| a.time.partial_cmp(&b.time).unwrap_or(Ordering::Equal),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_default_control_point!(TimingPoint, DifficultyPoint, EffectPoint);
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
Ok(idx) | Err(Some(idx)) => value.is_redundant(&self[idx]),
|
||||||
|
Err(None) => value.is_redundant(&DifficultyPoint::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_redundant {
|
||||||
|
self.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::beatmap::DifficultyPoint;
|
||||||
|
|
||||||
|
use super::SortedVec;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sorts_on_push() {
|
||||||
|
let mut v = SortedVec {
|
||||||
|
inner: Vec::new(),
|
||||||
|
cmp: <i32 as Ord>::cmp,
|
||||||
|
};
|
||||||
|
|
||||||
|
v.push(42);
|
||||||
|
v.push(13);
|
||||||
|
v.push(20);
|
||||||
|
v.push(0);
|
||||||
|
|
||||||
|
assert_eq!(&v[..], &[0_i32, 13, 20, 42]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_push_if_redundant() {
|
||||||
|
let mut v = SortedVec::default();
|
||||||
|
|
||||||
|
v.push(DifficultyPoint::default());
|
||||||
|
assert_eq!(v.len(), 1);
|
||||||
|
|
||||||
|
v.push_if_not_redundant(DifficultyPoint::default());
|
||||||
|
assert_eq!(v.len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
/// Stores the sorted order for an initial list so that multiple
|
||||||
|
/// lists can be sorted based on that order.
|
||||||
|
pub(crate) struct TandemSorter {
|
||||||
|
indices: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TandemSorter {
|
||||||
|
/// Sort indices based on the given slice.
|
||||||
|
/// Note that this does **not** sort the given slice.
|
||||||
|
pub(crate) fn new<T>(slice: &[T]) -> Self
|
||||||
|
where
|
||||||
|
T: PartialOrd,
|
||||||
|
{
|
||||||
|
let mut indices: Vec<_> = (0..).take(slice.len()).collect();
|
||||||
|
|
||||||
|
indices
|
||||||
|
.sort_unstable_by(|&i, &j| slice[i].partial_cmp(&slice[j]).unwrap_or(Ordering::Equal));
|
||||||
|
|
||||||
|
Self { indices }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort the given slice based on the internal ordering.
|
||||||
|
///
|
||||||
|
/// If you intend to sort another slice afterwards,
|
||||||
|
/// don't forget to call [`Self::toggle_marks`] first.
|
||||||
|
pub(crate) fn sort<T>(&mut self, slice: &mut [T]) {
|
||||||
|
for i in 0..self.indices.len() {
|
||||||
|
let i_idx = self.indices[i];
|
||||||
|
|
||||||
|
if Self::idx_is_marked(i_idx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut j = i;
|
||||||
|
let mut j_idx = i_idx;
|
||||||
|
|
||||||
|
// When we loop back to the first index, we stop
|
||||||
|
while j_idx != i {
|
||||||
|
self.indices[j] = Self::toggle_mark_idx(j_idx);
|
||||||
|
slice.swap(j, j_idx);
|
||||||
|
j = j_idx;
|
||||||
|
j_idx = self.indices[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.indices[j] = Self::toggle_mark_idx(j_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method must be called inbetween sorting slices.
|
||||||
|
pub(crate) fn toggle_marks(&mut self) {
|
||||||
|
for idx in self.indices.iter_mut() {
|
||||||
|
*idx = Self::toggle_mark_idx(*idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn idx_is_marked(idx: usize) -> bool {
|
||||||
|
// Check if first bit is set
|
||||||
|
idx.leading_zeros() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn toggle_mark_idx(idx: usize) -> usize {
|
||||||
|
// Flip the first bit
|
||||||
|
idx ^ !(usize::MAX >> 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::TandemSorter;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sort() {
|
||||||
|
let mut base = vec![9, 7, 8, 1, 4, 3, 5, 2];
|
||||||
|
let mut sorter = TandemSorter::new(&base);
|
||||||
|
|
||||||
|
sorter.sort(&mut base);
|
||||||
|
assert_eq!(base, vec![1, 2, 3, 4, 5, 7, 8, 9]);
|
||||||
|
|
||||||
|
sorter.toggle_marks();
|
||||||
|
let mut other = vec!['h', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'];
|
||||||
|
sorter.sort(&mut other);
|
||||||
|
assert_eq!(
|
||||||
|
other,
|
||||||
|
vec!['l', 'o', ' ', 'o', 'W', 'e', 'l', 'h', 'r', 'l', 'd']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user