finished mania + restructured a little

This commit is contained in:
MaxOhn
2022-10-19 11:48:11 +02:00
parent 044fe986c9
commit 6ff46367ca
24 changed files with 737 additions and 350 deletions
+1 -1
View File
@@ -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]
-34
View File
@@ -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;
}
}
+9 -10
View File
@@ -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);
+10 -10
View File
@@ -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(
+1 -1
View File
@@ -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
View File
@@ -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
-100
View File
@@ -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
View File
@@ -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() {
+5 -16
View File
@@ -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,
}
}
}
+37 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
-4
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
use crate::{
limited_queue::LimitedQueue,
taiko::difficulty_object::{HitObjectRhythm, ObjectLists, TaikoDifficultyObject},
util::LimitedQueue,
};
use super::{Skill, StrainDecaySkill, StrainSkill};
+54
View File
@@ -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);
}
}
+65
View File
@@ -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);
}
+12
View File
@@ -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,
};
+145
View File
@@ -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);
}
}
+91
View File
@@ -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']
);
}
}