finished mania + restructured a little
This commit is contained in:
@@ -109,7 +109,7 @@ pub struct EffectPoint {
|
||||
|
||||
impl EffectPoint {
|
||||
/// The default slider velocity for a [`DifficultyPoint`]
|
||||
pub const DEFAULT_KIAI: bool = true;
|
||||
pub const DEFAULT_KIAI: bool = false;
|
||||
|
||||
/// Create a new [`EffectPoint`].
|
||||
#[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::{
|
||||
curve::{Curve, CurveBuffers},
|
||||
limited_queue::LimitedQueue,
|
||||
parse::{legacy_sort, HitObjectKind, Pos2},
|
||||
util::{FloatExt, LimitedQueue},
|
||||
Beatmap, GameMode,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ use self::{
|
||||
pattern_type::PatternType,
|
||||
};
|
||||
|
||||
mod byte_hasher;
|
||||
mod legacy_random;
|
||||
mod pattern;
|
||||
mod pattern_generator;
|
||||
@@ -32,12 +31,14 @@ impl Beatmap {
|
||||
let mut n_circles = 0;
|
||||
let mut n_sliders = 0;
|
||||
|
||||
let seed =
|
||||
(map.hp + map.cs).round() as i32 * 20 + (map.od * 41.2) as i32 + map.ar.round() as i32;
|
||||
let seed = (map.hp + map.cs).round_even() as i32 * 20
|
||||
+ (map.od * 41.2) as i32
|
||||
+ map.ar.round_even() as i32;
|
||||
|
||||
let mut random = Random::new(seed);
|
||||
|
||||
let rounded_cs = map.cs.round();
|
||||
let rounded_od = map.od.round();
|
||||
let rounded_cs = map.cs.round_even();
|
||||
let rounded_od = map.od.round_even();
|
||||
|
||||
let slider_or_spinner_count = self
|
||||
.hit_objects
|
||||
@@ -78,9 +79,7 @@ impl Beatmap {
|
||||
};
|
||||
|
||||
let total_columns = map.cs as i32;
|
||||
|
||||
let mut last_values = PrevValues::default();
|
||||
|
||||
let mut curve_bufs = CurveBuffers::default();
|
||||
|
||||
for (obj, sound) in self.hit_objects.iter().zip(self.sounds.iter()) {
|
||||
@@ -167,8 +166,8 @@ impl Beatmap {
|
||||
&last_values.pattern,
|
||||
);
|
||||
|
||||
last_values.time = obj.start_time;
|
||||
last_values.pos = obj.pos;
|
||||
last_values.time = end_time;
|
||||
last_values.pos = Pos2 { x: 256.0, y: 192.0 };
|
||||
|
||||
compute_density(end_time, &mut density);
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::parse::{HitObject, HitObjectKind, Pos2};
|
||||
use crate::{
|
||||
parse::{HitObject, HitObjectKind, Pos2},
|
||||
util::ByteHasher,
|
||||
};
|
||||
|
||||
use super::{
|
||||
byte_hasher::BuildByteHasher,
|
||||
pattern_generator::{
|
||||
distance_object::DistanceObjectPatternGenerator,
|
||||
end_time_object::EndTimeObjectPatternGenerator, hit_object::HitObjectPatternGenerator,
|
||||
},
|
||||
use super::pattern_generator::{
|
||||
distance_object::DistanceObjectPatternGenerator,
|
||||
end_time_object::EndTimeObjectPatternGenerator, hit_object::HitObjectPatternGenerator,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Pattern {
|
||||
pub(crate) hit_objects: Vec<HitObject>,
|
||||
contained_columns: HashSet<u8, BuildByteHasher>,
|
||||
contained_columns: HashSet<u8, ByteHasher>,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub(crate) fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
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 {
|
||||
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);
|
||||
let hit_objects = vec![hit_object];
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
curve::Curve,
|
||||
mania::ManiaObject,
|
||||
parse::{HitObject, HitSound},
|
||||
util::FloatExt,
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
@@ -58,7 +59,7 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
|
||||
let beat_len = timing_point.beat_len * difficulty_point.bpm_mult;
|
||||
|
||||
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.
|
||||
let end_time = (start_time as f64
|
||||
@@ -97,12 +98,12 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
|
||||
let mut end_time_pattern = Pattern::default();
|
||||
|
||||
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 {
|
||||
intermediate_pattern.add_object(obj, column);
|
||||
if self.end_time != obj.end_time().round_even() as i32 {
|
||||
intermediate_pattern.add_object(obj, col);
|
||||
} 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 {
|
||||
let pattern = self.generate_core();
|
||||
|
||||
for obj in pattern.hit_objects.iter().map(ManiaObject::new) {
|
||||
if self.convert_type.contains(PatternType::STAIR)
|
||||
&& obj.column(self.total_columns as f32) as i32 == self.total_columns - 1
|
||||
{
|
||||
for obj in pattern.hit_objects.iter() {
|
||||
let col = ManiaObject::column(obj.pos.x, self.total_columns as f32) as i32;
|
||||
|
||||
if self.convert_type.contains(PatternType::STAIR) && col == self.total_columns - 1 {
|
||||
self.stair_type = PatternType::REVERSE_STAIR;
|
||||
}
|
||||
|
||||
if self.convert_type.contains(PatternType::REVERSE_STAIR)
|
||||
&& obj.column(self.total_columns as f32) as i32 == self.random_start()
|
||||
if self.convert_type.contains(PatternType::REVERSE_STAIR) && col == self.random_start()
|
||||
{
|
||||
self.stair_type = PatternType::STAIR;
|
||||
}
|
||||
@@ -120,12 +119,9 @@ impl<'h> HitObjectPatternGenerator<'h> {
|
||||
return Pattern::new_note(self, 0);
|
||||
}
|
||||
|
||||
let last_column = self
|
||||
.prev_pattern
|
||||
.hit_objects
|
||||
.last()
|
||||
.map(ManiaObject::new)
|
||||
.map_or(0, |h| h.column(self.total_columns as f32) as u8);
|
||||
let last_column = self.prev_pattern.hit_objects.last().map_or(0, |h| {
|
||||
ManiaObject::column(h.pos.x, self.total_columns as f32) 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 {
|
||||
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;
|
||||
|
||||
((self.hit_object().pos.x / LOCAL_X_DIVISOR).floor() as u8).clamp(0, 6) + 1
|
||||
} else {
|
||||
ManiaObject::new(self.hit_object()).column(self.total_columns() as f32) as u8
|
||||
};
|
||||
|
||||
res
|
||||
ManiaObject::column(self.hit_object().pos.x, self.total_columns() as f32) as u8
|
||||
}
|
||||
}
|
||||
|
||||
fn get_random_note_count(
|
||||
|
||||
@@ -32,7 +32,7 @@ impl fmt::Display for PatternType {
|
||||
$(
|
||||
if $self.contains(Self::$pat) {
|
||||
if $written {
|
||||
$f.write_str(" | ")?;
|
||||
$f.write_str(", ")?;
|
||||
} else {
|
||||
$written = true;
|
||||
}
|
||||
|
||||
+1
-3
@@ -1,13 +1,12 @@
|
||||
use std::{borrow::Cow, cmp::Ordering};
|
||||
|
||||
use crate::parse::HitObject;
|
||||
use crate::{parse::HitObject, util::SortedVec};
|
||||
|
||||
pub use self::{
|
||||
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, BeatmapHitWindows},
|
||||
breaks::Break,
|
||||
control_points::{DifficultyPoint, EffectPoint, TimingPoint},
|
||||
mode::GameMode,
|
||||
sorted_vec::SortedVec,
|
||||
};
|
||||
|
||||
mod attributes;
|
||||
@@ -15,7 +14,6 @@ mod breaks;
|
||||
mod control_points;
|
||||
mod converts;
|
||||
mod mode;
|
||||
mod sorted_vec;
|
||||
|
||||
/// The main beatmap struct containing all data relevant
|
||||
/// 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;
|
||||
|
||||
mod curve;
|
||||
mod limited_queue;
|
||||
mod mods;
|
||||
mod util;
|
||||
|
||||
pub use catch::{CatchPP, CatchStars};
|
||||
pub use mania::{ManiaPP, ManiaStars};
|
||||
@@ -216,6 +216,7 @@ pub use taiko::{TaikoPP, TaikoStars};
|
||||
|
||||
pub use mods::Mods;
|
||||
pub use parse::{ParseError, ParseResult};
|
||||
pub use util::SortedVec;
|
||||
|
||||
/// Provides some additional methods on [`Beatmap`](crate::Beatmap).
|
||||
pub trait BeatmapExt {
|
||||
@@ -520,7 +521,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn custom() {
|
||||
let path = "F:/osu!/beatmaps/1000168.osu";
|
||||
let path = "F:/osu!/beatmaps/34881.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
|
||||
let attrs = match OsuPP::new(&map).mode(GameMode::Mania).mods(0).calculate() {
|
||||
|
||||
@@ -10,24 +10,13 @@ pub(crate) struct ManiaDifficultyObject {
|
||||
}
|
||||
|
||||
impl ManiaDifficultyObject {
|
||||
pub(crate) fn new(
|
||||
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);
|
||||
|
||||
pub(crate) fn new(base: &ManiaObject, last: &ManiaObject, clock_rate: f64, idx: usize) -> Self {
|
||||
Self {
|
||||
idx,
|
||||
base_column,
|
||||
delta_time,
|
||||
start_time,
|
||||
end_time,
|
||||
base_column: base.column,
|
||||
delta_time: (base.start_time - last.start_time) / clock_rate,
|
||||
start_time: base.start_time / clock_rate,
|
||||
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::{
|
||||
difficulty_object::ManiaDifficultyObject,
|
||||
mania_object::ObjectParameters,
|
||||
skills::{Skill, Strain},
|
||||
ManiaDifficultyAttributes, ManiaObject, STAR_SCALING_FACTOR,
|
||||
};
|
||||
@@ -44,12 +45,13 @@ pub struct ManiaGradualDifficultyAttributes<'map> {
|
||||
strain: Strain,
|
||||
diff_objects: Vec<ManiaDifficultyObject>,
|
||||
curr_combo: usize,
|
||||
clock_rate: f64,
|
||||
}
|
||||
|
||||
impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||
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 strain = Strain::new(total_columns as usize);
|
||||
@@ -62,16 +64,31 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
.clock_rate(clock_rate)
|
||||
.hit_windows();
|
||||
|
||||
let diff_objects_iter = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(ManiaObject::new)
|
||||
.enumerate()
|
||||
.zip(map.hit_objects.iter().map(ManiaObject::new))
|
||||
.map(|((i, base), prev)| {
|
||||
ManiaDifficultyObject::new(base, prev, clock_rate, total_columns, i)
|
||||
});
|
||||
let mut params = ObjectParameters::new(map);
|
||||
let mut hit_objects = map.hit_objects.iter();
|
||||
|
||||
let first = match hit_objects.next() {
|
||||
Some(h) => ManiaObject::new(h, total_columns, &mut params),
|
||||
None => {
|
||||
return Self {
|
||||
idx: 0,
|
||||
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() {
|
||||
match &h.kind {
|
||||
@@ -94,6 +111,7 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
strain,
|
||||
diff_objects,
|
||||
curr_combo,
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,10 +125,14 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
|
||||
if let Some(h) = self.map.hit_objects.get(self.idx) {
|
||||
match &h.kind {
|
||||
HitObjectKind::Hold { end_time } => {
|
||||
self.curr_combo += 1 + ((*end_time - h.start_time) / 100.0) as usize
|
||||
HitObjectKind::Circle => self.curr_combo += 1,
|
||||
_ => {
|
||||
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> {
|
||||
hit_object: &'h HitObject,
|
||||
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||
|
||||
pub(crate) struct ObjectParameters<'a> {
|
||||
pub(crate) map: &'a Beatmap,
|
||||
pub(crate) max_combo: usize,
|
||||
pub(crate) curve_bufs: CurveBuffers,
|
||||
}
|
||||
|
||||
impl<'h> ManiaObject<'h> {
|
||||
pub(crate) fn new(hit_object: &'h HitObject) -> Self {
|
||||
Self { hit_object }
|
||||
impl<'a> ObjectParameters<'a> {
|
||||
pub(crate) fn new(map: &'a Beatmap) -> Self {
|
||||
Self {
|
||||
map,
|
||||
max_combo: 0,
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_time(&self) -> f64 {
|
||||
self.hit_object.start_time
|
||||
}
|
||||
pub(crate) struct ManiaObject {
|
||||
pub(crate) start_time: f64,
|
||||
pub(crate) end_time: f64,
|
||||
pub(crate) column: usize,
|
||||
}
|
||||
|
||||
pub(crate) fn end_time(&self) -> f64 {
|
||||
self.hit_object.end_time()
|
||||
}
|
||||
|
||||
pub(crate) fn column(&self, total_columns: f32) -> usize {
|
||||
impl ManiaObject {
|
||||
pub(crate) fn column(x: f32, total_columns: f32) -> usize {
|
||||
let x_divisor = 512.0 / total_columns;
|
||||
|
||||
(self.hit_object.pos.x / x_divisor)
|
||||
.floor()
|
||||
.min(total_columns - 1.0) as usize
|
||||
(x / x_divisor).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 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::*};
|
||||
|
||||
@@ -15,6 +15,7 @@ pub(crate) use self::mania_object::ManiaObject;
|
||||
|
||||
use self::{
|
||||
difficulty_object::ManiaDifficultyObject,
|
||||
mania_object::ObjectParameters,
|
||||
skills::{Skill, Strain},
|
||||
};
|
||||
|
||||
@@ -169,29 +170,30 @@ fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
|
||||
} = params;
|
||||
|
||||
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 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
|
||||
.hit_objects
|
||||
.iter()
|
||||
.take(take)
|
||||
.inspect(|h| match &h.kind {
|
||||
HitObjectKind::Hold { end_time } => {
|
||||
max_combo += 1 + ((*end_time - h.start_time) / 100.0) as usize
|
||||
let first = match hit_objects.next() {
|
||||
Some(h) => ManiaObject::new(h, total_columns, &mut params),
|
||||
None => {
|
||||
return ManiaResult {
|
||||
strain,
|
||||
max_combo: 0,
|
||||
}
|
||||
_ => max_combo += 1,
|
||||
})
|
||||
.skip(1)
|
||||
.map(ManiaObject::new)
|
||||
.enumerate()
|
||||
.zip(map.hit_objects.iter().map(ManiaObject::new))
|
||||
.map(|((i, base), prev)| {
|
||||
ManiaDifficultyObject::new(base, prev, clock_rate, total_columns, i)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 mut diff_objects = Vec::with_capacity(map.hit_objects.len().min(take).saturating_sub(1));
|
||||
diff_objects.extend(diff_objects_iter);
|
||||
@@ -200,7 +202,10 @@ fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
|
||||
strain.process(curr, &diff_objects);
|
||||
}
|
||||
|
||||
ManiaResult { strain, max_combo }
|
||||
ManiaResult {
|
||||
strain,
|
||||
max_combo: params.max_combo,
|
||||
}
|
||||
}
|
||||
|
||||
struct ManiaResult {
|
||||
|
||||
@@ -96,12 +96,8 @@ impl<'map> ManiaPP<'map> {
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: update
|
||||
/// 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
|
||||
/// using [`ManiaPP`] multiple times with different `passed_objects`, you should use
|
||||
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes).
|
||||
|
||||
+171
-93
@@ -14,7 +14,7 @@ pub use slider_parsing::*;
|
||||
use reader::FileReader;
|
||||
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")))]
|
||||
use std::{fs::File, io::Read};
|
||||
@@ -28,11 +28,10 @@ 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, 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));
|
||||
}
|
||||
use crate::{
|
||||
beatmap::{Beatmap, Break, DifficultyPoint, EffectPoint, GameMode, TimingPoint},
|
||||
util::TandemSorter,
|
||||
};
|
||||
|
||||
trait OptionExt<T> {
|
||||
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;
|
||||
|
||||
#[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 {
|
||||
(-Self::LIMIT..=Self::LIMIT).contains(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_in_custom_range(&self, limit: Self) -> bool {
|
||||
(-limit..=limit).contains(self)
|
||||
}
|
||||
@@ -125,7 +138,7 @@ macro_rules! parse_general_body {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -160,32 +173,32 @@ macro_rules! parse_difficulty_body {
|
||||
|
||||
match key {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -228,17 +241,13 @@ macro_rules! parse_events_body {
|
||||
if let Some(b'2') = split.next().and_then(|value| value.bytes().next()) {
|
||||
let start_time = split
|
||||
.next()
|
||||
.next_field("break start")?
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(f64::is_in_range);
|
||||
.next_field("break start")
|
||||
.map(f64::parse_in_range)?;
|
||||
|
||||
let end_time = split
|
||||
.next()
|
||||
.next_field("break end")?
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(f64::is_in_range);
|
||||
.next_field("break end")
|
||||
.map(f64::parse_in_range)?;
|
||||
|
||||
if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
|
||||
$self.breaks.push(Break {
|
||||
@@ -270,15 +279,16 @@ macro_rules! parse_timingpoints_body {
|
||||
let line = $reader.get_line()?;
|
||||
let mut split = line.split(',');
|
||||
|
||||
let time: f64 = split
|
||||
let time_opt = split
|
||||
.next()
|
||||
.next_field("timing point time")?
|
||||
.trim()
|
||||
.parse()?;
|
||||
.next_field("timing point time")
|
||||
.map(str::trim)
|
||||
.map(f64::parse_in_range)?;
|
||||
|
||||
if !time.is_in_range() {
|
||||
continue;
|
||||
}
|
||||
let time = match time_opt {
|
||||
Some(time) => time,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// * beatLength is allowed to be NaN to handle an edge case in which
|
||||
// * some beatmaps use NaN slider velocity to disable slider tick
|
||||
@@ -308,35 +318,30 @@ macro_rules! parse_timingpoints_body {
|
||||
match split
|
||||
.next()
|
||||
.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 => {
|
||||
return Status::Err
|
||||
}
|
||||
Some(Ok(_)) => {}
|
||||
Some(Some(time_sig)) if time_sig < 1 => return Status::Err,
|
||||
Some(Some(_)) => {}
|
||||
None => return Status::Ok,
|
||||
Some(Err(_)) => return Status::Err,
|
||||
Some(None) => return Status::Err,
|
||||
}
|
||||
|
||||
match split.next().map(str::parse::<i32>) {
|
||||
Some(Ok(sample_set)) if !sample_set.is_in_range() => return Status::Err,
|
||||
Some(Ok(_)) => {}
|
||||
match split.next().map(i32::parse_in_range) {
|
||||
Some(Some(_)) => {}
|
||||
Some(None) => return Status::Err,
|
||||
None => return Status::Ok,
|
||||
Some(Err(_)) => return Status::Err,
|
||||
}
|
||||
|
||||
match split.next().map(str::parse::<i32>) {
|
||||
Some(Ok(custom_sample)) if !custom_sample.is_in_range() => return Status::Err,
|
||||
Some(Ok(_)) => {}
|
||||
match split.next().map(i32::parse_in_range) {
|
||||
Some(Some(_)) => {}
|
||||
Some(None) => return Status::Err,
|
||||
None => return Status::Ok,
|
||||
Some(Err(_)) => return Status::Err,
|
||||
}
|
||||
|
||||
match split.next().map(str::parse::<i32>) {
|
||||
Some(Ok(sample_volume)) if !sample_volume.is_in_range() => return Status::Err,
|
||||
Some(Ok(_)) => {}
|
||||
match split.next().map(i32::parse_in_range) {
|
||||
Some(Some(_)) => {}
|
||||
Some(None) => return Status::Err,
|
||||
None => return Status::Ok,
|
||||
Some(Err(_)) => return Status::Err,
|
||||
}
|
||||
|
||||
if let Some(byte) = split.next().and_then(|value| value.bytes().next()) {
|
||||
@@ -345,11 +350,10 @@ macro_rules! parse_timingpoints_body {
|
||||
return Status::Ok;
|
||||
}
|
||||
|
||||
match split.next().map(str::parse::<i32>) {
|
||||
Some(Ok(effect_flags)) if !effect_flags.is_in_range() => return Status::Err,
|
||||
Some(Ok(effect_flags)) => *kiai = (effect_flags & KIAI_FLAG) > 0,
|
||||
match split.next().map(i32::parse_in_range) {
|
||||
Some(Some(effect_flags)) => *kiai = (effect_flags & KIAI_FLAG) > 0,
|
||||
Some(None) => return Status::Err,
|
||||
None => return Status::Ok,
|
||||
Some(Err(_)) => return Status::Err,
|
||||
}
|
||||
|
||||
Status::Ok
|
||||
@@ -393,10 +397,6 @@ macro_rules! parse_timingpoints_body {
|
||||
$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)
|
||||
}};
|
||||
}
|
||||
@@ -428,18 +428,14 @@ macro_rules! parse_hitobjects_body {
|
||||
|
||||
let x = split
|
||||
.next()
|
||||
.next_field("x pos")?
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|x| f32::is_in_custom_range(x, MAX_COORDINATE_VALUE as f32))
|
||||
.next_field("x pos")
|
||||
.map(|s| f32::parse_in_custom_range(s, MAX_COORDINATE_VALUE as f32))?
|
||||
.map(|x| x as i32 as f32);
|
||||
|
||||
let y = split
|
||||
.next()
|
||||
.next_field("y pos")?
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|x| f32::is_in_custom_range(x, MAX_COORDINATE_VALUE as f32))
|
||||
.next_field("y pos")
|
||||
.map(|s| f32::parse_in_custom_range(s, MAX_COORDINATE_VALUE as f32))?
|
||||
.map(|x| x as i32 as f32);
|
||||
|
||||
let pos = if let (Some(x), Some(y)) = (x, y) {
|
||||
@@ -450,11 +446,9 @@ macro_rules! parse_hitobjects_body {
|
||||
|
||||
let time_opt = split
|
||||
.next()
|
||||
.next_field("hitobject time")?
|
||||
.trim()
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(f64::is_in_range);
|
||||
.next_field("hitobject time")
|
||||
.map(str::trim)
|
||||
.map(f64::parse_in_range)?;
|
||||
|
||||
let time = match time_opt {
|
||||
Some(time) => time,
|
||||
@@ -470,12 +464,61 @@ macro_rules! parse_hitobjects_body {
|
||||
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,
|
||||
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 {
|
||||
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;
|
||||
|
||||
HitObjectKind::Circle
|
||||
@@ -548,14 +591,16 @@ macro_rules! parse_hitobjects_body {
|
||||
if control_points.is_empty() {
|
||||
HitObjectKind::Circle
|
||||
} else {
|
||||
let pixel_len = match split.next().map(str::parse::<f64>) {
|
||||
Some(Ok(len)) if len.is_in_custom_range(MAX_COORDINATE_VALUE as f64) => {
|
||||
(len > 0.0).then_some(len)
|
||||
}
|
||||
Some(_) => continue,
|
||||
let pixel_len = match split
|
||||
.next()
|
||||
.map(|s| f64::parse_in_custom_range(s, MAX_COORDINATE_VALUE as f64))
|
||||
{
|
||||
Some(Some(len)) => (len > 0.0).then_some(len),
|
||||
Some(None) => continue,
|
||||
None => None,
|
||||
};
|
||||
|
||||
// Note: Edge sets are currently not considered, seems to be fine though.
|
||||
let edge_sounds_opt = split.next().map(|sounds| {
|
||||
sounds
|
||||
.split('|')
|
||||
@@ -569,6 +614,13 @@ macro_rules! parse_hitobjects_body {
|
||||
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 {
|
||||
repeats,
|
||||
pixel_len,
|
||||
@@ -580,21 +632,37 @@ macro_rules! parse_hitobjects_body {
|
||||
$self.n_spinners += 1;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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 }
|
||||
} else if kind & Self::HOLD_FLAG > 0 {
|
||||
$self.n_sliders += 1;
|
||||
|
||||
let end_time = match split
|
||||
.next()
|
||||
.and_then(|next| next.split(':').next())
|
||||
.map(str::parse::<f64>)
|
||||
{
|
||||
Some(Ok(time_)) if time_.is_in_range() => time_.max(time),
|
||||
Some(_) => continue,
|
||||
let end_time = match split.next().and_then(|s| s.split_once(':')) {
|
||||
Some((head, tail)) => {
|
||||
let parsed = match f64::parse_in_range(head) {
|
||||
Some(time_) => time_.max(time),
|
||||
None => 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,
|
||||
};
|
||||
|
||||
@@ -614,18 +682,28 @@ macro_rules! parse_hitobjects_body {
|
||||
prev_time = time;
|
||||
}
|
||||
|
||||
// BUG: If [General] section comes after [HitObjects] then the mode
|
||||
// won't be set yet so mania objects won't be sorted properly
|
||||
if $self.mode == GameMode::Mania {
|
||||
// First a _stable_ sort by time
|
||||
$self
|
||||
.hit_objects
|
||||
.sort_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
||||
match $self.mode {
|
||||
GameMode::Osu | GameMode::Taiko | GameMode::Catch if !unsorted => {}
|
||||
GameMode::Osu | GameMode::Taiko => {
|
||||
// Sort both hitobjects and hitsounds
|
||||
let mut sorter = TandemSorter::new(&$self.hit_objects);
|
||||
sorter.sort(&mut $self.hit_objects);
|
||||
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
|
||||
legacy_sort(&mut $self.hit_objects);
|
||||
} else if unsorted {
|
||||
sort_unstable(&mut $self.hit_objects);
|
||||
// Then the legacy sort for correct position order
|
||||
legacy_sort(&mut $self.hit_objects);
|
||||
}
|
||||
GameMode::Catch => $self
|
||||
.hit_objects
|
||||
.sort_unstable_by(|h1, h2| h1.partial_cmp(h2).unwrap_or(Ordering::Equal)),
|
||||
}
|
||||
|
||||
Ok(empty)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
limited_queue::LimitedQueue,
|
||||
taiko::difficulty_object::{HitObjectRhythm, ObjectLists, TaikoDifficultyObject},
|
||||
util::LimitedQueue,
|
||||
};
|
||||
|
||||
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!(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[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