added convert support (#12)
* added taiko conversion * finished converts * made clippy happy * optimized allocations for patterns * taiko convert fixes * added mode methods for osu calculators * from CRLF back to LF * updated changelog * updated readme * undo normal hitsound change * added edge_sounds to changelog
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
- __Additions__:
|
||||
- Added the `ControlPoint` and `ControlPointerIter` types to the public interface, aswell as the `Beatmap::control_points` method
|
||||
- `TimingPoint` and `DifficultyPoint` now implement `Default`
|
||||
- Added new methods to `Beatmap`:
|
||||
- `convert_mode`: Convert a map into another mode. (doesn't do anything if the starting map is not osu!standard)
|
||||
- `total_break_time`: Return the accumulated break time in milliseconds
|
||||
- `timing_point_at`: Return the timing point for the given timestamp
|
||||
- `difficulty_point_at`: Return the difficulty point for the given timestamp if available
|
||||
- __Breaking changes:__
|
||||
- Moved some types to a different module. The following types can now be found in `rosu_pp::beatmap`:
|
||||
- `Beatmap`
|
||||
@@ -11,6 +17,9 @@
|
||||
- `DifficultyPoint`
|
||||
- `GameMode`
|
||||
- `TimingPoint`
|
||||
- Added a new field `kiai: bool` to both `TimingPoint` and `DifficultyPoint` to denote whether the current timing section is in kiai mode
|
||||
- Added a new field `breaks: Vec<Break>` to `Beatmap` that contains all breaks throughout the map
|
||||
- Added a new field `edge_sounds: Vec<u8>` to the `Slider` variant of `HitObjectKind` to denote the sample played on slider heads, ends, and repeats
|
||||
|
||||
# v0.5.2 (2022-06-14)
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
A standalone crate to calculate star ratings and performance points for all [osu!](https://osu.ppy.sh/home) gamemodes.
|
||||
|
||||
Conversions between game modes (i.e. "converts") are generally not supported.
|
||||
|
||||
Async is supported through features, see below.
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/// A break point of a [`Beatmap`](crate::beatmap::Beatmap).
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub struct Break {
|
||||
/// Start timestamp of the break.
|
||||
pub start_time: f64,
|
||||
/// End timestamp of the break.
|
||||
pub end_time: f64,
|
||||
}
|
||||
|
||||
impl Break {
|
||||
/// Duration of the break.
|
||||
#[inline]
|
||||
pub fn duration(&self) -> f64 {
|
||||
self.end_time - self.start_time
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ pub struct TimingPoint {
|
||||
pub beat_len: f64,
|
||||
/// The start time of this timing section
|
||||
pub time: f64,
|
||||
/// Whether the section between this and the
|
||||
/// next timing points is a kiai section
|
||||
pub kiai: bool,
|
||||
}
|
||||
|
||||
impl PartialOrd for TimingPoint {
|
||||
@@ -17,6 +20,16 @@ impl PartialOrd for TimingPoint {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TimingPoint {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
beat_len: 60_000.0 / 60.0,
|
||||
time: 0.0,
|
||||
kiai: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`TimingPoint`] that depends on a previous one.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct DifficultyPoint {
|
||||
@@ -24,6 +37,9 @@ pub struct DifficultyPoint {
|
||||
pub time: f64,
|
||||
/// The speed multiplier until the next timing point
|
||||
pub speed_multiplier: f64,
|
||||
/// Whether the section between this and the
|
||||
/// next timing points is a kiai section
|
||||
pub kiai: bool,
|
||||
}
|
||||
|
||||
impl PartialOrd for DifficultyPoint {
|
||||
@@ -32,6 +48,16 @@ impl PartialOrd for DifficultyPoint {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DifficultyPoint {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: 0.0,
|
||||
speed_multiplier: 1.0,
|
||||
kiai: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Control point for a [`Beatmap`].
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ControlPoint {
|
||||
@@ -118,24 +144,29 @@ mod test {
|
||||
TimingPoint {
|
||||
time: 1.0,
|
||||
beat_len: 10.0,
|
||||
kiai: false,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 3.0,
|
||||
beat_len: 10.0,
|
||||
kiai: false,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 4.0,
|
||||
beat_len: 10.0,
|
||||
kiai: false,
|
||||
},
|
||||
],
|
||||
difficulty_points: vec![
|
||||
DifficultyPoint {
|
||||
time: 2.0,
|
||||
speed_multiplier: 10.0,
|
||||
kiai: false,
|
||||
},
|
||||
DifficultyPoint {
|
||||
time: 5.0,
|
||||
speed_multiplier: 10.0,
|
||||
kiai: false,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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, bytes: &[u8]) {
|
||||
// Only use this hasher for single bytes
|
||||
debug_assert_eq!(bytes.len(), 1);
|
||||
self.byte = bytes[0];
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_u8(&mut self, byte: u8) {
|
||||
self.byte = byte;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
const INT_TO_REAL: f64 = 1.0 / (i32::MAX as f64 + 1.0);
|
||||
const INT_MASK: u32 = 0x7F_FF_FF_FF;
|
||||
|
||||
pub(crate) struct Random {
|
||||
x: u32,
|
||||
y: u32,
|
||||
z: u32,
|
||||
w: u32,
|
||||
}
|
||||
|
||||
impl Random {
|
||||
pub(crate) fn new(seed: i32) -> Self {
|
||||
Self {
|
||||
x: seed as u32,
|
||||
y: 842_502_087,
|
||||
z: 3_579_807_591,
|
||||
w: 273_326_509,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn gen_unsigned(&mut self) -> u32 {
|
||||
let t = self.x ^ (self.x << 11);
|
||||
self.x = self.y;
|
||||
self.y = self.z;
|
||||
self.z = self.w;
|
||||
self.w = self.w ^ (self.w >> 19) ^ t ^ (t >> 8);
|
||||
|
||||
self.w
|
||||
}
|
||||
|
||||
pub(crate) fn gen_signed(&mut self) -> i32 {
|
||||
(INT_MASK & self.gen_unsigned()) as i32
|
||||
}
|
||||
|
||||
pub(crate) fn gen_double(&mut self) -> f64 {
|
||||
INT_TO_REAL * self.gen_signed() as f64
|
||||
}
|
||||
|
||||
pub(crate) fn gen_int_range(&mut self, min: i32, max: i32) -> i32 {
|
||||
(min as f64 + self.gen_double() * (max - min) as f64) as i32
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::{
|
||||
curve::{Curve, CurveBuffers},
|
||||
limited_queue::LimitedQueue,
|
||||
parse::{legacy_sort, HitObjectKind, Pos2},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use self::{
|
||||
legacy_random::Random,
|
||||
pattern::Pattern,
|
||||
pattern_generator::{
|
||||
distance_object::DistanceObjectPatternGenerator,
|
||||
end_time_object::EndTimeObjectPatternGenerator, hit_object::HitObjectPatternGenerator,
|
||||
},
|
||||
pattern_type::PatternType,
|
||||
};
|
||||
|
||||
mod byte_hasher;
|
||||
mod legacy_random;
|
||||
mod pattern;
|
||||
mod pattern_generator;
|
||||
mod pattern_type;
|
||||
|
||||
const MAX_NOTES_FOR_DENSITY: usize = 7;
|
||||
|
||||
impl Beatmap {
|
||||
pub(in crate::beatmap) fn convert_to_mania(&self) -> Self {
|
||||
let mut map = self.clone_without_hit_objects(false);
|
||||
|
||||
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 mut random = Random::new(seed);
|
||||
|
||||
let rounded_cs = map.cs.round();
|
||||
let rounded_od = map.od.round();
|
||||
|
||||
let slider_or_spinner_count = self
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter(|h| {
|
||||
matches!(
|
||||
h.kind,
|
||||
HitObjectKind::Slider { .. } | HitObjectKind::Spinner { .. }
|
||||
)
|
||||
})
|
||||
.count();
|
||||
|
||||
let percent_slider_or_spinner =
|
||||
slider_or_spinner_count as f32 / self.hit_objects.len() as f32;
|
||||
|
||||
let target_columns = if percent_slider_or_spinner < 0.2 {
|
||||
7.0
|
||||
} else if percent_slider_or_spinner < 0.3 || rounded_cs >= 5.0 {
|
||||
(6 + (rounded_od > 5.0) as u8) as f32
|
||||
} else if percent_slider_or_spinner > 0.6 {
|
||||
(4 + (rounded_od > 4.0) as u8) as f32
|
||||
} else {
|
||||
(rounded_od + 1.0).clamp(4.0, 7.0)
|
||||
};
|
||||
|
||||
map.cs = target_columns;
|
||||
|
||||
let mut prev_note_times = LimitedQueue::new(MAX_NOTES_FOR_DENSITY);
|
||||
let mut density = i32::MAX as f64;
|
||||
|
||||
let mut compute_density = |new_note_time: f64, d: &mut f64| {
|
||||
prev_note_times.push(new_note_time);
|
||||
|
||||
if prev_note_times.len() >= 2 {
|
||||
*d = (prev_note_times.last().unwrap() - prev_note_times[0])
|
||||
/ prev_note_times.len() as f64;
|
||||
}
|
||||
};
|
||||
|
||||
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()) {
|
||||
match obj.kind {
|
||||
HitObjectKind::Circle => {
|
||||
compute_density(obj.start_time, &mut density);
|
||||
|
||||
let mut gen = HitObjectPatternGenerator::new(
|
||||
&mut random,
|
||||
obj,
|
||||
*sound,
|
||||
total_columns,
|
||||
&last_values,
|
||||
density,
|
||||
self,
|
||||
);
|
||||
|
||||
let new_pattern = gen.generate();
|
||||
|
||||
last_values.stair = gen.stair_type;
|
||||
last_values.time = obj.start_time;
|
||||
last_values.pos = obj.pos;
|
||||
|
||||
map.hit_objects
|
||||
.extend(new_pattern.hit_objects.iter().cloned());
|
||||
|
||||
n_circles += new_pattern.hit_objects.len();
|
||||
last_values.pattern = new_pattern;
|
||||
}
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
repeats,
|
||||
ref control_points,
|
||||
ref edge_sounds,
|
||||
} => {
|
||||
let curve = Curve::new(control_points, pixel_len, &mut curve_bufs);
|
||||
|
||||
let mut gen = DistanceObjectPatternGenerator::new(
|
||||
&mut random,
|
||||
obj,
|
||||
*sound,
|
||||
total_columns,
|
||||
&last_values.pattern,
|
||||
self,
|
||||
repeats,
|
||||
&curve,
|
||||
edge_sounds,
|
||||
);
|
||||
|
||||
let segment_duration = gen.segment_duration as f64;
|
||||
|
||||
for i in 0..=repeats as i32 + 1 {
|
||||
let time = obj.start_time + segment_duration * i as f64;
|
||||
|
||||
last_values.time = time;
|
||||
last_values.pos = obj.pos;
|
||||
|
||||
compute_density(time, &mut density);
|
||||
}
|
||||
|
||||
for new_pattern in gen.generate() {
|
||||
let new_objects = new_pattern.hit_objects.iter().map(|h| {
|
||||
if obj.is_circle() {
|
||||
n_circles += 1;
|
||||
} else {
|
||||
n_sliders += 1;
|
||||
}
|
||||
|
||||
h.to_owned()
|
||||
});
|
||||
|
||||
map.hit_objects.extend(new_objects);
|
||||
|
||||
last_values.pattern = new_pattern;
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { end_time } | HitObjectKind::Hold { end_time } => {
|
||||
let mut gen = EndTimeObjectPatternGenerator::new(
|
||||
&mut random,
|
||||
obj,
|
||||
end_time,
|
||||
*sound,
|
||||
total_columns,
|
||||
&last_values.pattern,
|
||||
);
|
||||
|
||||
last_values.time = obj.start_time;
|
||||
last_values.pos = obj.pos;
|
||||
|
||||
compute_density(end_time, &mut density);
|
||||
|
||||
let new_pattern = gen.generate();
|
||||
|
||||
let new_objects = new_pattern.hit_objects.into_iter().inspect(|h| {
|
||||
if h.is_circle() {
|
||||
n_circles += 1;
|
||||
} else {
|
||||
n_sliders += 1;
|
||||
}
|
||||
});
|
||||
|
||||
map.hit_objects.extend(new_objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map.n_circles = n_circles as u32;
|
||||
map.n_sliders = n_sliders;
|
||||
|
||||
map.hit_objects
|
||||
.sort_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
||||
|
||||
legacy_sort(&mut map.hit_objects);
|
||||
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PrevValues {
|
||||
time: f64,
|
||||
pos: Pos2,
|
||||
pattern: Pattern,
|
||||
stair: PatternType,
|
||||
}
|
||||
|
||||
impl Default for PrevValues {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: 0.0,
|
||||
pos: Pos2::default(),
|
||||
pattern: Pattern::default(),
|
||||
stair: PatternType::STAIR,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::parse::{HitObject, HitObjectKind, Pos2};
|
||||
|
||||
use super::{
|
||||
byte_hasher::BuildByteHasher,
|
||||
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>,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub(crate) fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
hit_objects: Vec::with_capacity(capacity),
|
||||
contained_columns: HashSet::with_hasher(BuildByteHasher),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_single(hit_object: HitObject, column: u8) -> Self {
|
||||
let mut contained_columns = HashSet::with_capacity_and_hasher(1, BuildByteHasher);
|
||||
contained_columns.insert(column);
|
||||
let hit_objects = vec![hit_object];
|
||||
|
||||
Self {
|
||||
hit_objects,
|
||||
contained_columns,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_note(generator: &HitObjectPatternGenerator<'_>, column: u8) -> Self {
|
||||
let hit_object = HitObject {
|
||||
pos: Pos2::new(column_to_pos(column, generator.total_columns)),
|
||||
start_time: generator.hit_object.start_time,
|
||||
kind: HitObjectKind::Circle,
|
||||
};
|
||||
|
||||
Self::new_single(hit_object, column)
|
||||
}
|
||||
|
||||
pub(crate) fn add_note(&mut self, generator: &HitObjectPatternGenerator<'_>, column: u8) {
|
||||
let hit_object = HitObject {
|
||||
pos: Pos2::new(column_to_pos(column, generator.total_columns)),
|
||||
start_time: generator.hit_object.start_time,
|
||||
kind: HitObjectKind::Circle,
|
||||
};
|
||||
|
||||
self.contained_columns.insert(column);
|
||||
self.hit_objects.push(hit_object);
|
||||
}
|
||||
|
||||
pub(crate) fn new_end_time_note(
|
||||
generator: &EndTimeObjectPatternGenerator<'_>,
|
||||
column: u8,
|
||||
hold_note: bool,
|
||||
) -> Self {
|
||||
let pos = Pos2::new(column_to_pos(column, generator.total_columns));
|
||||
|
||||
let hit_object = if hold_note {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: generator.hit_object.start_time,
|
||||
kind: HitObjectKind::Hold {
|
||||
end_time: generator.end_time,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: generator.hit_object.start_time,
|
||||
kind: HitObjectKind::Circle,
|
||||
}
|
||||
};
|
||||
|
||||
Self::new_single(hit_object, column)
|
||||
}
|
||||
|
||||
pub(crate) fn new_slider_note(
|
||||
generator: &DistanceObjectPatternGenerator<'_>,
|
||||
column: u8,
|
||||
start_time: i32,
|
||||
end_time: i32,
|
||||
) -> Self {
|
||||
let pos = Pos2::new(column_to_pos(column, generator.total_columns));
|
||||
|
||||
let hit_object = if start_time == end_time {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: start_time as f64,
|
||||
kind: HitObjectKind::Circle,
|
||||
}
|
||||
} else {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: start_time as f64,
|
||||
kind: HitObjectKind::Hold {
|
||||
end_time: end_time as f64,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Self::new_single(hit_object, column)
|
||||
}
|
||||
|
||||
pub(crate) fn add_slider_note(
|
||||
&mut self,
|
||||
generator: &DistanceObjectPatternGenerator<'_>,
|
||||
column: u8,
|
||||
start_time: i32,
|
||||
end_time: i32,
|
||||
) {
|
||||
let pos = Pos2::new(column_to_pos(column, generator.total_columns));
|
||||
|
||||
let hit_object = if start_time == end_time {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: start_time as f64,
|
||||
kind: HitObjectKind::Circle,
|
||||
}
|
||||
} else {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: start_time as f64,
|
||||
kind: HitObjectKind::Hold {
|
||||
end_time: end_time as f64,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
self.contained_columns.insert(column);
|
||||
self.hit_objects.push(hit_object);
|
||||
}
|
||||
|
||||
pub(crate) fn add_object(&mut self, obj: HitObject, column: u8) {
|
||||
self.hit_objects.push(obj);
|
||||
self.contained_columns.insert(column);
|
||||
}
|
||||
|
||||
pub(crate) fn column_has_obj(&self, column: u8) -> bool {
|
||||
self.contained_columns.contains(&column)
|
||||
}
|
||||
|
||||
pub(crate) fn column_with_objs(&self) -> i32 {
|
||||
self.contained_columns.len() as i32
|
||||
}
|
||||
|
||||
/// Moves all values of `other` into `self`,
|
||||
/// leaving `other` empty but keeps the capacities.
|
||||
pub(crate) fn append(&mut self, other: &mut Self) {
|
||||
self.hit_objects.append(&mut other.hit_objects);
|
||||
self.contained_columns
|
||||
.extend(other.contained_columns.drain());
|
||||
}
|
||||
}
|
||||
|
||||
fn column_to_pos(column: u8, total_columns: i32) -> f32 {
|
||||
let divisor = 512.0 / total_columns as f32;
|
||||
|
||||
(column as f32 * divisor).ceil()
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
use crate::{
|
||||
beatmap::converts::mania::{
|
||||
legacy_random::Random, pattern::Pattern, pattern_type::PatternType,
|
||||
},
|
||||
curve::Curve,
|
||||
parse::{HitObject, HitSound},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::PatternGenerator;
|
||||
|
||||
pub(crate) struct DistanceObjectPatternGenerator<'h> {
|
||||
pub(crate) hit_object: &'h HitObject,
|
||||
pub(crate) segment_duration: i32,
|
||||
pub(crate) total_columns: i32,
|
||||
pub(crate) sample: u8,
|
||||
start_time: i32,
|
||||
end_time: i32,
|
||||
span_count: i32,
|
||||
orig: &'h Beatmap,
|
||||
prev_pattern: &'h Pattern,
|
||||
convert_type: PatternType,
|
||||
random: &'h mut Random,
|
||||
edge_sounds: &'h [u8],
|
||||
}
|
||||
|
||||
impl<'h> DistanceObjectPatternGenerator<'h> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
random: &'h mut Random,
|
||||
hit_object: &'h HitObject,
|
||||
sample: u8,
|
||||
total_columns: i32,
|
||||
prev_pattern: &'h Pattern,
|
||||
orig: &'h Beatmap,
|
||||
repeats: usize,
|
||||
curve: &Curve,
|
||||
edge_sounds: &'h [u8],
|
||||
) -> Self {
|
||||
let timing_point = orig.timing_point_at(hit_object.start_time);
|
||||
|
||||
let difficulty_point = orig
|
||||
.difficulty_point_at(hit_object.start_time)
|
||||
.unwrap_or_default();
|
||||
|
||||
let kiai = if timing_point.time < difficulty_point.time {
|
||||
difficulty_point.kiai
|
||||
} else {
|
||||
timing_point.kiai
|
||||
};
|
||||
|
||||
let convert_type = if kiai {
|
||||
PatternType::default()
|
||||
} else {
|
||||
PatternType::LOW_PROBABILITY
|
||||
};
|
||||
|
||||
let beat_len = timing_point.beat_len / difficulty_point.speed_multiplier;
|
||||
|
||||
let span_count = (repeats + 1) as i32;
|
||||
let start_time = hit_object.start_time.round() as i32;
|
||||
|
||||
// * This matches stable's calculation.
|
||||
let end_time = (start_time as f64
|
||||
+ curve.dist() * beat_len * span_count as f64 * 0.01 / orig.slider_mult)
|
||||
.floor() as i32;
|
||||
|
||||
let segment_duration = (end_time - start_time) / span_count;
|
||||
|
||||
Self {
|
||||
hit_object,
|
||||
segment_duration,
|
||||
total_columns,
|
||||
sample,
|
||||
start_time,
|
||||
end_time,
|
||||
span_count,
|
||||
orig,
|
||||
prev_pattern,
|
||||
convert_type,
|
||||
random,
|
||||
edge_sounds,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(&mut self) -> Vec<Pattern> {
|
||||
let orig_pattern = self.generate_();
|
||||
|
||||
if orig_pattern.hit_objects.len() == 1 {
|
||||
return vec![orig_pattern];
|
||||
}
|
||||
|
||||
// * We need to split the intermediate pattern into two new patterns:
|
||||
// * 1. A pattern containing all objects that do not end at our EndTime.
|
||||
// * 2. A pattern containing all objects that end at our EndTime. This will be used for further pattern generation.
|
||||
let mut intermediate_pattern = Pattern::default();
|
||||
let mut end_time_pattern = Pattern::default();
|
||||
|
||||
for obj in orig_pattern.hit_objects {
|
||||
let column = obj.column(self.total_columns as f32);
|
||||
|
||||
if self.end_time != obj.end_time().round() as i32 {
|
||||
intermediate_pattern.add_object(obj, column);
|
||||
} else {
|
||||
end_time_pattern.add_object(obj, column);
|
||||
}
|
||||
}
|
||||
|
||||
vec![intermediate_pattern, end_time_pattern]
|
||||
}
|
||||
|
||||
fn generate_(&mut self) -> Pattern {
|
||||
let conversion_diff = self.conversion_difficulty();
|
||||
|
||||
if self.total_columns == 1 {
|
||||
return Pattern::new_slider_note(self, 0, self.start_time, self.end_time);
|
||||
} else if self.span_count > 1 {
|
||||
if self.segment_duration <= 90 {
|
||||
self.generate_random_hold_notes(self.start_time, 1)
|
||||
} else if self.segment_duration <= 120 {
|
||||
self.convert_type |= PatternType::FORCE_NOT_STACK;
|
||||
|
||||
self.generate_random_notes(self.start_time, self.span_count + 1)
|
||||
} else if self.segment_duration <= 160 {
|
||||
self.generate_stair(self.start_time)
|
||||
} else if self.segment_duration <= 200 && conversion_diff > 3.0 {
|
||||
self.generate_random_multiple_notes(self.start_time)
|
||||
} else if self.end_time - self.start_time >= 4000 {
|
||||
self.generate_n_random_notes(self.start_time, 0.23, 0.0, 0.0)
|
||||
} else if self.segment_duration > 400
|
||||
&& self.span_count < self.total_columns - 1 - self.random_start()
|
||||
{
|
||||
self.generate_tiled_hold_notes(self.start_time)
|
||||
} else {
|
||||
self.generate_hold_and_normal_notes(self.start_time, conversion_diff)
|
||||
}
|
||||
} else if self.segment_duration <= 110 {
|
||||
if self.prev_pattern.column_with_objs() < self.total_columns {
|
||||
self.convert_type |= PatternType::FORCE_NOT_STACK;
|
||||
} else {
|
||||
self.convert_type &= !PatternType::FORCE_NOT_STACK;
|
||||
}
|
||||
|
||||
let note_count = 1 + (self.segment_duration >= 80) as i32;
|
||||
|
||||
self.generate_random_notes(self.start_time, note_count)
|
||||
} else if conversion_diff > 6.5 {
|
||||
if self.convert_type.contains(PatternType::LOW_PROBABILITY) {
|
||||
self.generate_n_random_notes(self.start_time, 0.78, 0.3, 0.0)
|
||||
} else {
|
||||
self.generate_n_random_notes(self.start_time, 0.85, 0.36, 0.03)
|
||||
}
|
||||
} else if conversion_diff > 4.0 {
|
||||
if self.convert_type.contains(PatternType::LOW_PROBABILITY) {
|
||||
self.generate_n_random_notes(self.start_time, 0.43, 0.08, 0.0)
|
||||
} else {
|
||||
self.generate_n_random_notes(self.start_time, 0.56, 0.18, 0.0)
|
||||
}
|
||||
} else if conversion_diff > 2.5 {
|
||||
if self.convert_type.contains(PatternType::LOW_PROBABILITY) {
|
||||
self.generate_n_random_notes(self.start_time, 0.3, 0.0, 0.0)
|
||||
} else {
|
||||
self.generate_n_random_notes(self.start_time, 0.37, 0.08, 0.0)
|
||||
}
|
||||
} else if self.convert_type.contains(PatternType::LOW_PROBABILITY) {
|
||||
self.generate_n_random_notes(self.start_time, 0.17, 0.0, 0.0)
|
||||
} else {
|
||||
self.generate_n_random_notes(self.start_time, 0.27, 0.0, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_random_hold_notes(&mut self, start_time: i32, note_count: i32) -> Pattern {
|
||||
// * - - - -
|
||||
// * ■ - ■ ■
|
||||
// * □ - □ □
|
||||
// * ■ - ■ ■
|
||||
|
||||
let mut pattern = Pattern::default();
|
||||
|
||||
let random_start = self.random_start();
|
||||
let usable_columns =
|
||||
self.total_columns - random_start - self.prev_pattern.column_with_objs();
|
||||
let mut next_column = PatternGenerator::get_random_column(self, None, None);
|
||||
|
||||
for _ in 0..usable_columns.min(note_count) {
|
||||
// * Find available column
|
||||
next_column = self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&[&pattern, self.prev_pattern],
|
||||
);
|
||||
|
||||
pattern.add_slider_note(self, next_column, start_time, self.end_time);
|
||||
}
|
||||
|
||||
// * This is can't be combined with the above loop due to RNG
|
||||
for _ in 0..note_count.saturating_sub(usable_columns) {
|
||||
next_column =
|
||||
self.find_available_column(next_column, None, None, None, None, &[&pattern]);
|
||||
|
||||
pattern.add_slider_note(self, next_column, start_time, self.end_time);
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn generate_random_notes(&mut self, mut start_time: i32, note_count: i32) -> Pattern {
|
||||
// * - - - -
|
||||
// * x - - -
|
||||
// * - - x -
|
||||
// * - - - x
|
||||
// * x - - -
|
||||
|
||||
let mut next_column = self.get_column(Some(true));
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK)
|
||||
&& self.prev_pattern.column_with_objs() < self.total_columns
|
||||
{
|
||||
next_column = self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&[self.prev_pattern],
|
||||
);
|
||||
}
|
||||
|
||||
let mut last_column = next_column;
|
||||
let mut pattern = Pattern::with_capacity(note_count as usize);
|
||||
|
||||
for _ in 0..note_count {
|
||||
pattern.add_slider_note(self, next_column, start_time, start_time);
|
||||
|
||||
next_column = self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&|c| c != last_column as i32),
|
||||
&[],
|
||||
);
|
||||
|
||||
last_column = next_column;
|
||||
start_time += self.segment_duration;
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn generate_stair(&mut self, mut start_time: i32) -> Pattern {
|
||||
// * - - - -
|
||||
// * x - - -
|
||||
// * - x - -
|
||||
// * - - x -
|
||||
// * - - - x
|
||||
// * - - x -
|
||||
// * - x - -
|
||||
// * x - - -
|
||||
|
||||
let mut column = self.get_column(Some(true)) as i32;
|
||||
let mut increasing = self.random.gen_double() > 0.5;
|
||||
let mut pattern = Pattern::with_capacity(self.span_count as usize + 1);
|
||||
|
||||
for _ in 0..=self.span_count as usize {
|
||||
pattern.add_slider_note(self, column as u8, start_time, start_time);
|
||||
start_time += self.segment_duration;
|
||||
|
||||
// * Check if we're at the borders of the stage, and invert the pattern if so
|
||||
if increasing {
|
||||
if column >= self.total_columns - 1 {
|
||||
increasing = false;
|
||||
column -= 1;
|
||||
} else {
|
||||
column += 1;
|
||||
}
|
||||
} else if column <= self.random_start() {
|
||||
increasing = true;
|
||||
column += 1;
|
||||
} else {
|
||||
column -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn generate_random_multiple_notes(&mut self, mut start_time: i32) -> Pattern {
|
||||
// * - - - -
|
||||
// * x - - -
|
||||
// * - x x -
|
||||
// * - - - x
|
||||
// * x - x -
|
||||
|
||||
let legacy = (4..=8).contains(&self.total_columns);
|
||||
let interval = self
|
||||
.random
|
||||
.gen_int_range(1, self.total_columns as i32 - (legacy as i32));
|
||||
|
||||
let mut next_column = self.get_column(Some(true)) as i32;
|
||||
let random_start = self.random_start();
|
||||
let not_2k = self.total_columns > 2;
|
||||
let mut pattern =
|
||||
Pattern::with_capacity((self.span_count as usize + 1) * (1 + not_2k as usize));
|
||||
|
||||
for _ in 0..=self.span_count as usize {
|
||||
pattern.add_slider_note(self, next_column as u8, start_time, start_time);
|
||||
|
||||
next_column += interval;
|
||||
|
||||
if next_column >= self.total_columns - random_start {
|
||||
next_column = next_column - self.total_columns - random_start + (legacy as i32);
|
||||
}
|
||||
|
||||
next_column += random_start;
|
||||
|
||||
// * If we're in 2K, let's not add many consecutive doubles
|
||||
if not_2k {
|
||||
pattern.add_slider_note(self, next_column as u8, start_time, start_time);
|
||||
}
|
||||
|
||||
next_column = PatternGenerator::get_random_column(self, None, None) as i32;
|
||||
start_time += self.segment_duration;
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn generate_n_random_notes(
|
||||
&mut self,
|
||||
start_time: i32,
|
||||
mut p2: f64,
|
||||
mut p3: f64,
|
||||
mut p4: f64,
|
||||
) -> Pattern {
|
||||
// * - - - -
|
||||
// * ■ - ■ ■
|
||||
// * □ - □ □
|
||||
// * ■ - ■ ■
|
||||
|
||||
match self.total_columns {
|
||||
2 => {
|
||||
p2 = 0.0;
|
||||
p3 = 0.0;
|
||||
p4 = 0.0;
|
||||
}
|
||||
3 => {
|
||||
p2 = p2.min(0.1);
|
||||
p3 = 0.0;
|
||||
p4 = 0.0;
|
||||
}
|
||||
4 => {
|
||||
p2 = p2.min(0.3);
|
||||
p3 = p3.min(0.04);
|
||||
p4 = 0.0;
|
||||
}
|
||||
5 => {
|
||||
p2 = p2.min(0.34);
|
||||
p3 = p3.min(0.1);
|
||||
p4 = p4.min(0.03);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let is_double_sample = |sample: u8| sample.clap() || sample.finish();
|
||||
|
||||
let can_generate_two_notes = !self.convert_type.contains(PatternType::LOW_PROBABILITY)
|
||||
&& (is_double_sample(self.sample)
|
||||
|| is_double_sample(self.sample_info_list_at(self.start_time)));
|
||||
|
||||
if can_generate_two_notes {
|
||||
p2 = 1.0;
|
||||
}
|
||||
|
||||
let note_count = self.get_random_note_count(p2, p3, Some(p4), None, None);
|
||||
|
||||
self.generate_random_hold_notes(start_time, note_count)
|
||||
}
|
||||
|
||||
fn generate_tiled_hold_notes(&mut self, mut start_time: i32) -> Pattern {
|
||||
// * - - - -
|
||||
// * ■ ■ ■ ■
|
||||
// * □ □ □ □
|
||||
// * □ □ □ □
|
||||
// * □ □ □ ■
|
||||
// * □ □ ■ -
|
||||
// * □ ■ - -
|
||||
// * ■ - - -
|
||||
|
||||
let column_repeat = self.span_count.min(self.total_columns) as usize;
|
||||
|
||||
// * Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable).
|
||||
let end_time = start_time + self.segment_duration * self.span_count;
|
||||
|
||||
let mut next_column = self.get_column(Some(true));
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK)
|
||||
&& self.prev_pattern.column_with_objs() < self.total_columns
|
||||
{
|
||||
next_column = self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&[self.prev_pattern],
|
||||
);
|
||||
}
|
||||
|
||||
let mut pattern = Pattern::with_capacity(column_repeat);
|
||||
|
||||
for _ in 0..column_repeat {
|
||||
next_column =
|
||||
self.find_available_column(next_column, None, None, None, None, &[&pattern]);
|
||||
pattern.add_slider_note(self, next_column, start_time, end_time);
|
||||
start_time += self.segment_duration;
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn generate_hold_and_normal_notes(
|
||||
&mut self,
|
||||
mut start_time: i32,
|
||||
conversion_diff: f64,
|
||||
) -> Pattern {
|
||||
// * - - - -
|
||||
// * ■ x x -
|
||||
// * ■ - x x
|
||||
// * ■ x - x
|
||||
// * ■ - x x
|
||||
|
||||
let mut pattern = Pattern::default();
|
||||
|
||||
let mut hold_column = self.get_column(Some(true));
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK)
|
||||
&& self.prev_pattern.column_with_objs() < self.total_columns
|
||||
{
|
||||
hold_column = self.find_available_column(
|
||||
hold_column,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&[self.prev_pattern],
|
||||
);
|
||||
}
|
||||
|
||||
// * Create the hold note
|
||||
pattern.add_slider_note(self, hold_column, start_time, self.end_time);
|
||||
|
||||
let mut next_column = PatternGenerator::get_random_column(self, None, None);
|
||||
|
||||
let mut note_count = if conversion_diff > 6.5 {
|
||||
self.get_random_note_count(0.63, 0.0, None, None, None)
|
||||
} else if conversion_diff > 4.0 {
|
||||
let p2 = if self.total_columns < 6 { 0.12 } else { 0.45 };
|
||||
|
||||
self.get_random_note_count(p2, 0.0, None, None, None)
|
||||
} else if conversion_diff > 2.5 {
|
||||
let p2 = if self.total_columns < 6 { 0.0 } else { 0.24 };
|
||||
|
||||
self.get_random_note_count(p2, 0.0, None, None, None)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
note_count = note_count.min(self.total_columns - 1);
|
||||
|
||||
let sample = self.sample_info_list_at(start_time);
|
||||
let ignore_head = !(sample.whistle() || sample.finish() || sample.clap());
|
||||
|
||||
let mut row_pattern = Pattern::default();
|
||||
let hold_column = hold_column as i32;
|
||||
|
||||
for _ in 0..=self.span_count as usize {
|
||||
if !(ignore_head && start_time == self.start_time) {
|
||||
for _ in 0..note_count {
|
||||
next_column = self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&|c| c != hold_column),
|
||||
&[&row_pattern],
|
||||
);
|
||||
|
||||
row_pattern.add_slider_note(self, next_column, start_time, start_time);
|
||||
}
|
||||
}
|
||||
|
||||
pattern.append(&mut row_pattern);
|
||||
start_time += self.segment_duration;
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn sample_info_list_at(&self, time: i32) -> u8 {
|
||||
self.note_samples_at(time)
|
||||
.first()
|
||||
.map_or(self.sample, |sample| *sample)
|
||||
}
|
||||
|
||||
fn note_samples_at(&self, time: i32) -> &[u8] {
|
||||
let idx = if self.segment_duration == 0 {
|
||||
0
|
||||
} else {
|
||||
((time - self.start_time) / self.segment_duration) as usize
|
||||
};
|
||||
|
||||
&self.edge_sounds[idx..]
|
||||
}
|
||||
}
|
||||
|
||||
impl PatternGenerator for DistanceObjectPatternGenerator<'_> {
|
||||
#[inline]
|
||||
fn hit_object(&self) -> &HitObject {
|
||||
self.hit_object
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_columns(&self) -> i32 {
|
||||
self.total_columns
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn random(&mut self) -> &mut Random {
|
||||
self.random
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn original_map(&self) -> &Beatmap {
|
||||
self.orig
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use crate::{
|
||||
beatmap::converts::mania::{
|
||||
legacy_random::Random, pattern::Pattern, pattern_type::PatternType,
|
||||
},
|
||||
parse::{HitObject, HitSound},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::PatternGenerator;
|
||||
|
||||
pub(crate) struct EndTimeObjectPatternGenerator<'h> {
|
||||
pub(crate) hit_object: &'h HitObject,
|
||||
pub(crate) end_time: f64,
|
||||
pub(crate) total_columns: i32,
|
||||
pub(crate) sample: u8,
|
||||
convert_type: PatternType,
|
||||
prev_pattern: &'h Pattern,
|
||||
random: &'h mut Random,
|
||||
}
|
||||
|
||||
impl<'h> EndTimeObjectPatternGenerator<'h> {
|
||||
pub(crate) fn new(
|
||||
random: &'h mut Random,
|
||||
hit_object: &'h HitObject,
|
||||
end_time: f64,
|
||||
sample: u8,
|
||||
total_columns: i32,
|
||||
prev_pattern: &'h Pattern,
|
||||
) -> Self {
|
||||
let convert_type = if prev_pattern.column_with_objs() == total_columns {
|
||||
PatternType::default()
|
||||
} else {
|
||||
PatternType::FORCE_NOT_STACK
|
||||
};
|
||||
|
||||
Self {
|
||||
hit_object,
|
||||
end_time,
|
||||
total_columns,
|
||||
sample,
|
||||
convert_type,
|
||||
prev_pattern,
|
||||
random,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(&mut self) -> Pattern {
|
||||
let generate_hold = self.end_time - self.hit_object.start_time >= 100.0;
|
||||
|
||||
match self.total_columns {
|
||||
8 if self.sample.finish() && self.end_time - self.hit_object.start_time < 1000.0 => {
|
||||
Pattern::new_end_time_note(self, 0, generate_hold)
|
||||
}
|
||||
8 => {
|
||||
let column = self.get_random_column(self.random_start());
|
||||
|
||||
Pattern::new_end_time_note(self, column, generate_hold)
|
||||
}
|
||||
_ => {
|
||||
let column = self.get_random_column(0);
|
||||
|
||||
Pattern::new_end_time_note(self, column, generate_hold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_random_column(&mut self, lower: i32) -> u8 {
|
||||
let column = PatternGenerator::get_random_column(self, Some(lower), None);
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK) {
|
||||
self.find_available_column(column, Some(lower), None, None, None, &[self.prev_pattern])
|
||||
} else {
|
||||
self.find_available_column(column, Some(lower), None, None, None, &[])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PatternGenerator for EndTimeObjectPatternGenerator<'_> {
|
||||
#[inline]
|
||||
fn hit_object(&self) -> &HitObject {
|
||||
self.hit_object
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_columns(&self) -> i32 {
|
||||
self.total_columns
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn random(&mut self) -> &mut Random {
|
||||
self.random
|
||||
}
|
||||
|
||||
fn original_map(&self) -> &Beatmap {
|
||||
panic!("trait method is not used")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
use crate::{
|
||||
beatmap::converts::mania::{
|
||||
legacy_random::Random, pattern::Pattern, pattern_type::PatternType, PrevValues,
|
||||
},
|
||||
parse::{HitObject, HitSound},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::PatternGenerator;
|
||||
|
||||
pub(crate) struct HitObjectPatternGenerator<'h> {
|
||||
pub(crate) hit_object: &'h HitObject,
|
||||
pub(crate) total_columns: i32,
|
||||
pub(crate) sample: u8,
|
||||
pub(crate) stair_type: PatternType,
|
||||
convert_type: PatternType,
|
||||
prev_pattern: &'h Pattern,
|
||||
random: &'h mut Random,
|
||||
orig: &'h Beatmap,
|
||||
}
|
||||
|
||||
impl<'h> HitObjectPatternGenerator<'h> {
|
||||
pub(crate) fn new(
|
||||
random: &'h mut Random,
|
||||
hit_object: &'h HitObject,
|
||||
sample: u8,
|
||||
total_columns: i32,
|
||||
prev: &'h PrevValues,
|
||||
density: f64,
|
||||
orig: &'h Beatmap,
|
||||
) -> Self {
|
||||
let timing_point = orig.timing_point_at(hit_object.start_time);
|
||||
|
||||
let pos_separation = (hit_object.pos - prev.pos).length();
|
||||
let time_separation = hit_object.start_time - prev.time;
|
||||
|
||||
let mut convert_type = PatternType::default();
|
||||
|
||||
if time_separation <= 80.0 {
|
||||
// * More than 187 BPM
|
||||
convert_type |= PatternType::FORCE_NOT_STACK | PatternType::KEEP_SINGLE;
|
||||
} else if time_separation <= 95.0 {
|
||||
// * More than 157 BPM
|
||||
convert_type |= PatternType::FORCE_NOT_STACK | PatternType::KEEP_SINGLE | prev.stair;
|
||||
} else if time_separation <= 105.0 {
|
||||
// * More than 140 BPM
|
||||
convert_type |= PatternType::FORCE_NOT_STACK | PatternType::LOW_PROBABILITY;
|
||||
} else if time_separation <= 125.0 {
|
||||
// * More than 120 BPM
|
||||
convert_type |= PatternType::FORCE_NOT_STACK;
|
||||
} else if time_separation <= 135.0 && pos_separation < 20.0 {
|
||||
// * More than 111 BPM stream
|
||||
convert_type |= PatternType::CYCLE | PatternType::KEEP_SINGLE;
|
||||
} else if time_separation <= 150.0 && pos_separation < 20.0 {
|
||||
// * More than 100 BPM stream
|
||||
convert_type |= PatternType::FORCE_STACK | PatternType::LOW_PROBABILITY;
|
||||
} else if pos_separation < 20.0 && density >= timing_point.beat_len / 2.5 {
|
||||
// * Low density stream
|
||||
convert_type |= PatternType::REVERSE | PatternType::LOW_PROBABILITY;
|
||||
} else if density < timing_point.beat_len / 2.5 {
|
||||
// * High density
|
||||
} else {
|
||||
let difficulty_point = orig.difficulty_point_at(hit_object.start_time);
|
||||
|
||||
let kiai = match difficulty_point {
|
||||
Some(difficulty_point) => {
|
||||
if timing_point.time < difficulty_point.time {
|
||||
difficulty_point.kiai
|
||||
} else {
|
||||
timing_point.kiai
|
||||
}
|
||||
}
|
||||
None => timing_point.kiai,
|
||||
};
|
||||
|
||||
if kiai {
|
||||
// * High density
|
||||
} else {
|
||||
convert_type |= PatternType::LOW_PROBABILITY;
|
||||
}
|
||||
}
|
||||
|
||||
if !convert_type.contains(PatternType::KEEP_SINGLE) {
|
||||
if sample.finish() && total_columns != 8 {
|
||||
convert_type |= PatternType::MIRROR;
|
||||
} else if sample.clap() {
|
||||
convert_type |= PatternType::GATHERED;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
hit_object,
|
||||
stair_type: prev.stair,
|
||||
convert_type,
|
||||
total_columns,
|
||||
sample,
|
||||
prev_pattern: &prev.pattern,
|
||||
random,
|
||||
orig,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(&mut self) -> Pattern {
|
||||
let pattern = self.generate_core();
|
||||
|
||||
for obj in pattern.hit_objects.iter() {
|
||||
if self.convert_type.contains(PatternType::STAIR)
|
||||
&& obj.column(self.total_columns as f32) as i32 == 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()
|
||||
{
|
||||
self.stair_type = PatternType::STAIR;
|
||||
}
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn generate_core(&mut self) -> Pattern {
|
||||
if self.total_columns == 1 {
|
||||
return Pattern::new_note(self, 0);
|
||||
}
|
||||
|
||||
let last_column = self
|
||||
.prev_pattern
|
||||
.hit_objects
|
||||
.last()
|
||||
.map_or(0, |h| h.column(self.total_columns as f32));
|
||||
|
||||
let random_start = self.random_start() as u8;
|
||||
|
||||
if self.convert_type.contains(PatternType::REVERSE)
|
||||
&& !self.prev_pattern.hit_objects.is_empty()
|
||||
{
|
||||
let mut pattern = Pattern::default();
|
||||
|
||||
for i in random_start..self.total_columns as u8 {
|
||||
if self.prev_pattern.column_has_obj(i) {
|
||||
pattern.add_note(self, random_start + self.total_columns as u8 - i - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
if self.convert_type.contains(PatternType::CYCLE)
|
||||
&& self.prev_pattern.hit_objects.len() == 1
|
||||
// * If we convert to 7K + 1, let's not overload the special key
|
||||
&& (self.total_columns != 8 || last_column != 0)
|
||||
// * Make sure the last column was not the centre column
|
||||
&& (self.total_columns % 2 == 0 || last_column != self.total_columns as u8 / 2)
|
||||
{
|
||||
// * Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
|
||||
let column = random_start + self.total_columns as u8 - last_column - 1;
|
||||
|
||||
return Pattern::new_note(self, column);
|
||||
}
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_STACK)
|
||||
&& !self.prev_pattern.hit_objects.is_empty()
|
||||
{
|
||||
let mut pattern = Pattern::default();
|
||||
|
||||
// * Generate a new pattern by placing on the already filled columns
|
||||
for i in random_start..self.total_columns as u8 {
|
||||
if self.prev_pattern.column_has_obj(i) {
|
||||
pattern.add_note(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
if self.prev_pattern.hit_objects.len() == 1 {
|
||||
if self.convert_type.contains(PatternType::STAIR) {
|
||||
// * Generate a new pattern by placing on the next column,
|
||||
// * cycling back to the start if there is no "next"
|
||||
let mut target_column = last_column + 1;
|
||||
|
||||
if target_column == self.total_columns as u8 {
|
||||
target_column = random_start;
|
||||
}
|
||||
|
||||
return Pattern::new_note(self, target_column);
|
||||
}
|
||||
|
||||
if self.convert_type.contains(PatternType::REVERSE_STAIR) {
|
||||
// * Generate a new pattern by placing on the previous column,
|
||||
// * cycling back to the end if there is no "previous"
|
||||
let mut target_column = last_column as i8 - 1;
|
||||
|
||||
if target_column == random_start as i8 - 1 {
|
||||
target_column = self.total_columns as i8 - 1;
|
||||
}
|
||||
|
||||
return Pattern::new_note(self, target_column as u8);
|
||||
}
|
||||
}
|
||||
|
||||
if self.convert_type.contains(PatternType::KEEP_SINGLE) {
|
||||
return self.generate_random_notes(1);
|
||||
}
|
||||
|
||||
let conversion_diff = self.conversion_difficulty();
|
||||
|
||||
if self.convert_type.contains(PatternType::MIRROR) {
|
||||
if conversion_diff > 6.5 {
|
||||
self.generate_random_pattern_with_mirrored(0.12, 0.38, 0.12)
|
||||
} else if conversion_diff > 4.0 {
|
||||
self.generate_random_pattern_with_mirrored(0.12, 0.17, 0.0)
|
||||
} else {
|
||||
self.generate_random_pattern_with_mirrored(0.12, 0.0, 0.0)
|
||||
}
|
||||
} else if conversion_diff > 6.5 {
|
||||
if self.convert_type.contains(PatternType::LOW_PROBABILITY) {
|
||||
self.generate_random_pattern(0.78, 0.42, 0.0, 0.0)
|
||||
} else {
|
||||
self.generate_random_pattern(1.0, 0.62, 0.0, 0.0)
|
||||
}
|
||||
} else if conversion_diff > 4.0 {
|
||||
if self.convert_type.contains(PatternType::LOW_PROBABILITY) {
|
||||
self.generate_random_pattern(0.35, 0.08, 0.0, 0.0)
|
||||
} else {
|
||||
self.generate_random_pattern(0.52, 0.15, 0.0, 0.0)
|
||||
}
|
||||
} else if conversion_diff > 2.0 {
|
||||
if self.convert_type.contains(PatternType::LOW_PROBABILITY) {
|
||||
self.generate_random_pattern(0.18, 0.0, 0.0, 0.0)
|
||||
} else {
|
||||
self.generate_random_pattern(0.45, 0.0, 0.0, 0.0)
|
||||
}
|
||||
} else {
|
||||
self.generate_random_pattern(0.0, 0.0, 0.0, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_random_notes(&mut self, mut note_count: i32) -> Pattern {
|
||||
let mut pattern = Pattern::default();
|
||||
|
||||
let allow_stacking = !self.convert_type.contains(PatternType::FORCE_NOT_STACK);
|
||||
|
||||
if !allow_stacking {
|
||||
note_count =
|
||||
(self.total_columns - self.random_start() - self.prev_pattern.column_with_objs())
|
||||
.min(note_count);
|
||||
}
|
||||
|
||||
let mut next_column = self.get_column(Some(true));
|
||||
|
||||
for _ in 0..note_count {
|
||||
next_column = if allow_stacking {
|
||||
self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
None,
|
||||
Some(Self::get_next_column),
|
||||
None,
|
||||
&[&pattern],
|
||||
)
|
||||
} else {
|
||||
self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
None,
|
||||
Some(Self::get_next_column),
|
||||
None,
|
||||
&[&pattern, self.prev_pattern],
|
||||
)
|
||||
};
|
||||
|
||||
pattern.add_note(self, next_column);
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn get_next_column(&mut self, mut last: u8) -> u8 {
|
||||
if self.convert_type.contains(PatternType::GATHERED) {
|
||||
last += 1;
|
||||
|
||||
if last == self.total_columns as u8 {
|
||||
last = self.random_start() as u8;
|
||||
}
|
||||
} else {
|
||||
last = PatternGenerator::get_random_column(self, None, None);
|
||||
}
|
||||
|
||||
last
|
||||
}
|
||||
|
||||
fn has_special_column(&self) -> bool {
|
||||
self.sample.clap() && self.sample.finish()
|
||||
}
|
||||
|
||||
fn generate_random_pattern(&mut self, p2: f64, p3: f64, p4: f64, p5: f64) -> Pattern {
|
||||
let random_note_count = self.get_random_note_count(p2, p3, p4, p5);
|
||||
let mut pattern = self.generate_random_notes(random_note_count);
|
||||
|
||||
if self.random_start() > 0 && self.has_special_column() {
|
||||
pattern.add_note(self, 0);
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn get_random_note_count(&mut self, mut p2: f64, mut p3: f64, mut p4: f64, mut p5: f64) -> i32 {
|
||||
match self.total_columns {
|
||||
2 => {
|
||||
p2 = 0.0;
|
||||
p3 = 0.0;
|
||||
p4 = 0.0;
|
||||
p5 = 0.0;
|
||||
}
|
||||
3 => {
|
||||
p2 = p2.min(0.1);
|
||||
p3 = 0.0;
|
||||
p4 = 0.0;
|
||||
p5 = 0.0;
|
||||
}
|
||||
4 => {
|
||||
p2 = p2.min(0.23);
|
||||
p3 = p3.min(0.04);
|
||||
p4 = 0.0;
|
||||
p5 = 0.0;
|
||||
}
|
||||
5 => {
|
||||
p3 = p3.min(0.15);
|
||||
p4 = p4.min(0.03);
|
||||
p5 = 0.0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.sample.clap() {
|
||||
p2 = 1.0;
|
||||
}
|
||||
|
||||
PatternGenerator::get_random_note_count(self, p2, p3, Some(p4), Some(p5), None)
|
||||
}
|
||||
|
||||
fn generate_random_pattern_with_mirrored(
|
||||
&mut self,
|
||||
centre_probability: f64,
|
||||
p2: f64,
|
||||
p3: f64,
|
||||
) -> Pattern {
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK) {
|
||||
return self.generate_random_pattern(1.0 / 2.0 + p2 / 2.0, p2, (p2 + p3) / 2.0, p3);
|
||||
}
|
||||
|
||||
let mut pattern = Pattern::default();
|
||||
|
||||
let (note_count, add_to_centre) =
|
||||
self.get_random_note_count_mirrored(centre_probability, p2, p3);
|
||||
|
||||
let column_limit = if self.total_columns % 2 == 0 {
|
||||
self.total_columns / 2
|
||||
} else {
|
||||
(self.total_columns - 1) / 2
|
||||
};
|
||||
|
||||
let mut next_column = PatternGenerator::get_random_column(self, None, Some(column_limit));
|
||||
|
||||
for _ in 0..note_count {
|
||||
next_column = self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
Some(column_limit),
|
||||
None,
|
||||
None,
|
||||
&[&pattern],
|
||||
);
|
||||
|
||||
// * Add normal note
|
||||
pattern.add_note(self, next_column);
|
||||
|
||||
// * Add mirrored note
|
||||
let column = (self.random_start() + self.total_columns) as u8 - next_column - 1;
|
||||
pattern.add_note(self, column);
|
||||
}
|
||||
|
||||
if add_to_centre {
|
||||
pattern.add_note(self, self.total_columns as u8 / 2);
|
||||
}
|
||||
|
||||
if self.random_start() > 0 && self.has_special_column() {
|
||||
pattern.add_note(self, 0);
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn get_random_note_count_mirrored(
|
||||
&mut self,
|
||||
mut centre_probability: f64,
|
||||
mut p2: f64,
|
||||
mut p3: f64,
|
||||
) -> (i32, bool) {
|
||||
match self.total_columns {
|
||||
2 => {
|
||||
centre_probability = 0.0;
|
||||
p2 = 0.0;
|
||||
p3 = 0.0;
|
||||
}
|
||||
3 => {
|
||||
centre_probability = centre_probability.min(0.03);
|
||||
p2 = 0.0;
|
||||
p3 = 0.0;
|
||||
}
|
||||
4 => {
|
||||
centre_probability = 0.0;
|
||||
|
||||
// * Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
|
||||
// * But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
|
||||
// * so it needs to be converted to from a probability and then back after the multiplication.
|
||||
p2 = 1.0 - ((1.0 - p2) * 2.0).max(0.8);
|
||||
p3 = 0.0;
|
||||
}
|
||||
5 => {
|
||||
centre_probability = centre_probability.min(0.03);
|
||||
p3 = 0.0;
|
||||
}
|
||||
6 => {
|
||||
centre_probability = 0.0;
|
||||
|
||||
// * Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
|
||||
// * But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
|
||||
// * so it needs to be converted to from a probability and then back after the multiplication.
|
||||
p2 = 1.0 - ((1.0 - p2) * 2.0).max(0.05);
|
||||
p3 = 1.0 - ((1.0 - p3) * 2.0).max(0.85);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// * The stable values were allowed to exceed 1, which indicate <0% probability.
|
||||
// * These values needs to be clamped otherwise GetRandomNoteCount() will throw an exception.
|
||||
p2 = p2.clamp(0.0, 1.0);
|
||||
p3 = p3.clamp(0.0, 1.0);
|
||||
|
||||
let centre_val = self.random.gen_double();
|
||||
let note_count = PatternGenerator::get_random_note_count(self, p2, p3, None, None, None);
|
||||
let add_to_centre =
|
||||
self.total_columns % 2 != 0 && note_count != 3 && centre_val > 1.0 - centre_probability;
|
||||
|
||||
(note_count, add_to_centre)
|
||||
}
|
||||
}
|
||||
|
||||
impl PatternGenerator for HitObjectPatternGenerator<'_> {
|
||||
#[inline]
|
||||
fn hit_object(&self) -> &HitObject {
|
||||
self.hit_object
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_columns(&self) -> i32 {
|
||||
self.total_columns
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn random(&mut self) -> &mut Random {
|
||||
self.random
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn original_map(&self) -> &Beatmap {
|
||||
self.orig
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use crate::{parse::HitObject, Beatmap};
|
||||
|
||||
use super::{legacy_random::Random, pattern::Pattern};
|
||||
|
||||
pub(super) mod distance_object;
|
||||
pub(super) mod end_time_object;
|
||||
pub(super) mod hit_object;
|
||||
|
||||
trait PatternGenerator {
|
||||
fn hit_object(&self) -> &HitObject;
|
||||
fn total_columns(&self) -> i32;
|
||||
fn random(&mut self) -> &mut Random;
|
||||
fn original_map(&self) -> &Beatmap;
|
||||
|
||||
// ----------------------------------
|
||||
|
||||
fn random_start(&self) -> i32 {
|
||||
(self.total_columns() == 8) as i32
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
self.hit_object().column(self.total_columns() as f32)
|
||||
};
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn get_random_note_count(
|
||||
&mut self,
|
||||
p2: f64,
|
||||
p3: f64,
|
||||
p4: Option<f64>,
|
||||
p5: Option<f64>,
|
||||
p6: Option<f64>,
|
||||
) -> i32 {
|
||||
let p4 = p4.unwrap_or(0.0);
|
||||
let p5 = p5.unwrap_or(0.0);
|
||||
let p6 = p6.unwrap_or(0.0);
|
||||
|
||||
let val = self.random().gen_double();
|
||||
|
||||
if val >= 1.0 - p6 {
|
||||
6
|
||||
} else if val >= 1.0 - p5 {
|
||||
5
|
||||
} else if val >= 1.0 - p4 {
|
||||
4
|
||||
} else if val >= 1.0 - p3 {
|
||||
3
|
||||
} else {
|
||||
1 + (val >= 1.0 - p2) as i32
|
||||
}
|
||||
}
|
||||
|
||||
fn conversion_difficulty(&self) -> f64 {
|
||||
let orig = self.original_map();
|
||||
let last_obj_time = orig.hit_objects.last().map_or(0.0, |h| h.start_time);
|
||||
let first_obj_time = orig.hit_objects.first().map_or(0.0, |h| h.start_time);
|
||||
|
||||
// * Drain time in seconds
|
||||
let total_break_time = orig.total_break_time();
|
||||
let mut drain_time = ((last_obj_time - first_obj_time - total_break_time) / 1000.0) as i32;
|
||||
|
||||
if drain_time == 0 {
|
||||
drain_time = 10_000;
|
||||
}
|
||||
|
||||
let mut conversion_difficulty = 0.0;
|
||||
conversion_difficulty += (orig.hp + orig.ar.clamp(4.0, 7.0)) as f64 / 1.5;
|
||||
conversion_difficulty += orig.hit_objects.len() as f64 / drain_time as f64 * 9.0;
|
||||
conversion_difficulty /= 38.0;
|
||||
conversion_difficulty *= 5.0;
|
||||
conversion_difficulty /= 1.15;
|
||||
conversion_difficulty = conversion_difficulty.min(12.0);
|
||||
|
||||
conversion_difficulty
|
||||
}
|
||||
|
||||
fn get_random_column(&mut self, lower: Option<i32>, upper: Option<i32>) -> u8 {
|
||||
let lower = lower.unwrap_or_else(|| self.random_start());
|
||||
let upper = upper.unwrap_or_else(|| self.total_columns());
|
||||
|
||||
self.random().gen_int_range(lower, upper) as u8
|
||||
}
|
||||
|
||||
fn find_available_column(
|
||||
&mut self,
|
||||
mut initial_column: u8,
|
||||
lower: Option<i32>,
|
||||
upper: Option<i32>,
|
||||
next_column: Option<fn(&mut Self, u8) -> u8>,
|
||||
validation: Option<&dyn Fn(i32) -> bool>,
|
||||
patterns: &[&Pattern],
|
||||
) -> u8 {
|
||||
let lower = lower.unwrap_or_else(|| self.random_start());
|
||||
let upper = upper.unwrap_or_else(|| self.total_columns());
|
||||
|
||||
let is_valid = |column: i32| {
|
||||
if let Some(fun) = validation {
|
||||
if !(fun)(column) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let column = column as u8;
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.all(|pattern| !pattern.column_has_obj(column))
|
||||
};
|
||||
|
||||
// * Check for the initial column
|
||||
if is_valid(initial_column as i32) {
|
||||
return initial_column;
|
||||
}
|
||||
|
||||
// * Ensure that we have at least one free column, so that an endless loop is avoided
|
||||
let has_valid_column = (lower..upper).any(is_valid);
|
||||
assert!(has_valid_column);
|
||||
|
||||
// * Iterate until a valid column is found. This is a random iteration in the default case.
|
||||
while {
|
||||
initial_column = if let Some(fun) = next_column {
|
||||
(fun)(self, initial_column)
|
||||
} else {
|
||||
PatternGenerator::get_random_column(self, Some(lower), Some(upper))
|
||||
};
|
||||
|
||||
!is_valid(initial_column as i32)
|
||||
} {}
|
||||
|
||||
initial_column
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{BitAndAssign, BitOr, BitOrAssign, Not},
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub(crate) struct PatternType(u16);
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl PatternType {
|
||||
pub(crate) const FORCE_STACK: Self = Self(1 << 0);
|
||||
pub(crate) const FORCE_NOT_STACK: Self = Self(1 << 1);
|
||||
pub(crate) const KEEP_SINGLE: Self = Self(1 << 2);
|
||||
pub(crate) const LOW_PROBABILITY: Self = Self(1 << 3);
|
||||
// pub(crate) const ALTERNATE: Self = Self(1 << 4);
|
||||
// pub(crate) const FORCE_SIG_SLIDER: Self = Self(1 << 5);
|
||||
// pub(crate) const FORCE_NOT_SLIDER: Self = Self(1 << 6);
|
||||
pub(crate) const GATHERED: Self = Self(1 << 7);
|
||||
pub(crate) const MIRROR: Self = Self(1 << 8);
|
||||
pub(crate) const REVERSE: Self = Self(1 << 9);
|
||||
pub(crate) const CYCLE: Self = Self(1 << 10);
|
||||
pub(crate) const STAIR: Self = Self(1 << 11);
|
||||
pub(crate) const REVERSE_STAIR: Self = Self(1 << 12);
|
||||
}
|
||||
|
||||
impl fmt::Display for PatternType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut written = false;
|
||||
|
||||
macro_rules! write_pattern {
|
||||
($self:ident, $f:ident, $written:ident: $($pat:ident,)*) => {
|
||||
$(
|
||||
if $self.contains(Self::$pat) {
|
||||
if $written {
|
||||
$f.write_str(" | ")?;
|
||||
} else {
|
||||
$written = true;
|
||||
}
|
||||
|
||||
$f.write_str(stringify!($pat))?;
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
write_pattern! {
|
||||
self, f, written:
|
||||
FORCE_STACK,
|
||||
FORCE_NOT_STACK,
|
||||
KEEP_SINGLE,
|
||||
LOW_PROBABILITY,
|
||||
GATHERED,
|
||||
MIRROR,
|
||||
REVERSE,
|
||||
CYCLE,
|
||||
STAIR,
|
||||
REVERSE_STAIR,
|
||||
}
|
||||
|
||||
if !written {
|
||||
f.write_str("NONE")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PatternType {
|
||||
pub(crate) fn contains(self, other: Self) -> bool {
|
||||
self.0 & other.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr for PatternType {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 | rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOrAssign for PatternType {
|
||||
#[inline]
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
self.0 |= rhs.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl BitAndAssign for PatternType {
|
||||
#[inline]
|
||||
fn bitand_assign(&mut self, rhs: Self) {
|
||||
self.0 &= rhs.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Not for PatternType {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn not(self) -> Self::Output {
|
||||
Self(!self.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod mania;
|
||||
mod taiko;
|
||||
@@ -0,0 +1,148 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::{
|
||||
curve::{Curve, CurveBuffers},
|
||||
parse::{HitObject, HitObjectKind},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
const LEGACY_TAIKO_VELOCITY_MULTIPLIER: f32 = 1.4;
|
||||
const OSU_BASE_SCORING_DIST: f32 = 100.0;
|
||||
|
||||
impl Beatmap {
|
||||
pub(in crate::beatmap) fn convert_to_taiko(&self) -> Self {
|
||||
let mut map = self.clone_without_hit_objects(true);
|
||||
let mut curve_bufs = CurveBuffers::default();
|
||||
|
||||
for (obj, sound) in self.hit_objects.iter().zip(self.sounds.iter()) {
|
||||
match obj.kind {
|
||||
HitObjectKind::Circle => {
|
||||
map.hit_objects.push(obj.to_owned());
|
||||
map.sounds.push(*sound);
|
||||
map.n_circles += 1;
|
||||
}
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
repeats,
|
||||
ref control_points,
|
||||
ref edge_sounds,
|
||||
} => {
|
||||
let curve = Curve::new(control_points, pixel_len, &mut curve_bufs);
|
||||
let mut params = SliderParams::new(obj.start_time, repeats, &curve);
|
||||
|
||||
if self.should_convert_slider_to_taiko_hits(&mut params) {
|
||||
let mut i = 0;
|
||||
let mut j = obj.start_time;
|
||||
|
||||
let edge_sound_count = edge_sounds.len().max(1);
|
||||
|
||||
while j <= obj.start_time + params.duration + params.tick_spacing / 8.0 {
|
||||
let h = HitObject {
|
||||
pos: Default::default(),
|
||||
start_time: j,
|
||||
kind: HitObjectKind::Circle,
|
||||
};
|
||||
|
||||
map.hit_objects.push(h);
|
||||
map.sounds.push(*edge_sounds.get(i).unwrap_or(sound));
|
||||
map.n_circles += 1;
|
||||
|
||||
if params.tick_spacing.abs() <= f64::EPSILON {
|
||||
break;
|
||||
}
|
||||
|
||||
j += params.tick_spacing;
|
||||
i = (i + 1) % edge_sound_count;
|
||||
}
|
||||
} else {
|
||||
map.hit_objects.push(obj.to_owned());
|
||||
map.n_sliders += 1;
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { .. } => {
|
||||
map.hit_objects.push(obj.to_owned());
|
||||
map.sounds.push(*sound);
|
||||
map.n_spinners += 1;
|
||||
}
|
||||
// Pathological case; shouldn't realistically happen
|
||||
HitObjectKind::Hold { end_time } => {
|
||||
let obj = HitObject {
|
||||
pos: obj.pos,
|
||||
start_time: obj.start_time,
|
||||
kind: HitObjectKind::Spinner { end_time },
|
||||
};
|
||||
|
||||
map.hit_objects.push(obj);
|
||||
map.sounds.push(*sound);
|
||||
map.n_spinners += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only convert STD to TKO so we don't need to remove objects
|
||||
// with the same timestamp that would appear only in MNA
|
||||
|
||||
map.hit_objects
|
||||
.sort_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
fn should_convert_slider_to_taiko_hits(&self, params: &mut SliderParams<'_>) -> bool {
|
||||
let SliderParams {
|
||||
curve,
|
||||
duration,
|
||||
repeats,
|
||||
start_time,
|
||||
tick_spacing,
|
||||
} = params;
|
||||
|
||||
// * The true distance, accounting for any repeats. This ends up being the drum roll distance later
|
||||
let spans = (*repeats + 1) as f64;
|
||||
let dist = curve.dist() * spans * LEGACY_TAIKO_VELOCITY_MULTIPLIER as f64;
|
||||
|
||||
let timing_point = self.timing_point_at(*start_time);
|
||||
let difficulty_point = self.difficulty_point_at(*start_time).unwrap_or_default();
|
||||
|
||||
let mut beat_len = timing_point.beat_len / difficulty_point.speed_multiplier;
|
||||
|
||||
let slider_scoring_point_dist =
|
||||
OSU_BASE_SCORING_DIST as f64 * self.slider_mult / self.tick_rate;
|
||||
|
||||
// * The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
|
||||
let taiko_vel = slider_scoring_point_dist * self.tick_rate;
|
||||
*duration = (dist / taiko_vel * beat_len).floor();
|
||||
|
||||
let osu_vel = taiko_vel * (1000.0_f32 as f64 / beat_len);
|
||||
|
||||
// * osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8
|
||||
if self.version >= 8 {
|
||||
beat_len = timing_point.beat_len;
|
||||
}
|
||||
|
||||
// * If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
|
||||
*tick_spacing = (beat_len / self.tick_rate).min(*duration / spans);
|
||||
|
||||
*tick_spacing > 0.0 && dist / osu_vel * 1000.0 < 2.0 * beat_len
|
||||
}
|
||||
}
|
||||
|
||||
struct SliderParams<'c> {
|
||||
curve: &'c Curve,
|
||||
duration: f64,
|
||||
repeats: usize,
|
||||
start_time: f64,
|
||||
tick_spacing: f64,
|
||||
}
|
||||
|
||||
impl<'c> SliderParams<'c> {
|
||||
fn new(start_time: f64, repeats: usize, curve: &'c Curve) -> Self {
|
||||
Self {
|
||||
curve,
|
||||
repeats,
|
||||
start_time,
|
||||
duration: 0.0,
|
||||
tick_spacing: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
use std::{borrow::Cow, cmp::Ordering};
|
||||
|
||||
use crate::parse::HitObject;
|
||||
|
||||
pub use self::{
|
||||
attributes::BeatmapAttributes,
|
||||
breaks::Break,
|
||||
control_points::{ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint},
|
||||
mode::GameMode,
|
||||
};
|
||||
|
||||
mod attributes;
|
||||
mod breaks;
|
||||
mod control_points;
|
||||
mod converts;
|
||||
mod mode;
|
||||
|
||||
/// The main beatmap struct containing all data relevant
|
||||
@@ -53,6 +58,9 @@ pub struct Beatmap {
|
||||
/// The stack leniency that is used to calculate
|
||||
/// the stack offset for stacked positions.
|
||||
pub stack_leniency: f32,
|
||||
|
||||
/// All break points of the beatmap.
|
||||
pub breaks: Vec<Break>,
|
||||
}
|
||||
|
||||
impl Beatmap {
|
||||
@@ -76,4 +84,73 @@ impl Beatmap {
|
||||
pub fn control_points(&self) -> ControlPointIter<'_> {
|
||||
ControlPointIter::new(self)
|
||||
}
|
||||
|
||||
/// Sum up the duration of all breaks (in milliseconds).
|
||||
#[inline]
|
||||
pub fn total_break_time(&self) -> f64 {
|
||||
self.breaks.iter().map(Break::duration).sum()
|
||||
}
|
||||
|
||||
/// Return the [`TimingPoint`] for the given timestamp.
|
||||
///
|
||||
/// If `time` is before the first timing point, `None` is returned.
|
||||
#[inline]
|
||||
pub fn timing_point_at(&self, time: f64) -> TimingPoint {
|
||||
let idx_result = self
|
||||
.timing_points
|
||||
.binary_search_by(|probe| probe.time.partial_cmp(&time).unwrap_or(Ordering::Less));
|
||||
|
||||
match idx_result {
|
||||
Ok(idx) => self.timing_points[idx],
|
||||
Err(0) => self.timing_points.first().copied().unwrap_or_default(),
|
||||
Err(idx) => self.timing_points[idx - 1],
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`DifficultyPoint`] for the given timestamp.
|
||||
///
|
||||
/// If `time` is before the first difficulty point, `None` is returned.
|
||||
#[inline]
|
||||
pub fn difficulty_point_at(&self, time: f64) -> Option<DifficultyPoint> {
|
||||
self.difficulty_points
|
||||
.binary_search_by(|probe| probe.time.partial_cmp(&time).unwrap_or(Ordering::Less))
|
||||
.map_or_else(|i| i.checked_sub(1), Some)
|
||||
.map(|i| self.difficulty_points[i])
|
||||
}
|
||||
|
||||
/// Convert a [`Beatmap`] of some mode into a different mode.
|
||||
#[inline]
|
||||
pub fn convert_mode(&self, mode: GameMode) -> Cow<'_, Self> {
|
||||
if mode == self.mode {
|
||||
return Cow::Borrowed(self);
|
||||
}
|
||||
|
||||
match mode {
|
||||
GameMode::STD | GameMode::CTB => Cow::Borrowed(self),
|
||||
GameMode::TKO => Cow::Owned(self.convert_to_taiko()),
|
||||
GameMode::MNA => Cow::Owned(self.convert_to_mania()),
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_without_hit_objects(&self, with_sounds: bool) -> Self {
|
||||
Self {
|
||||
mode: self.mode,
|
||||
version: self.version,
|
||||
n_circles: 0,
|
||||
n_sliders: 0,
|
||||
n_spinners: 0,
|
||||
ar: self.ar,
|
||||
od: self.od,
|
||||
cs: self.cs,
|
||||
hp: self.hp,
|
||||
slider_mult: self.slider_mult,
|
||||
tick_rate: self.tick_rate,
|
||||
hit_objects: Vec::with_capacity(self.hit_objects.len()),
|
||||
sounds: Vec::with_capacity((with_sounds as usize) * self.sounds.len()),
|
||||
timing_points: self.timing_points.clone(),
|
||||
difficulty_points: self.difficulty_points.clone(),
|
||||
stack_leniency: self.stack_leniency,
|
||||
breaks: self.breaks.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ impl FruitOrJuice {
|
||||
pixel_len,
|
||||
repeats,
|
||||
control_points,
|
||||
..
|
||||
} => {
|
||||
// HR business
|
||||
params.last_pos = Some(h.pos.x + control_points[control_points.len() - 1].pos.x);
|
||||
|
||||
+23
-2
@@ -16,7 +16,9 @@ use movement::Movement;
|
||||
pub use pp::*;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{catch::fruit_or_juice::FruitParams, curve::CurveBuffers, Beatmap, Mods, Strains};
|
||||
use crate::{
|
||||
catch::fruit_or_juice::FruitParams, curve::CurveBuffers, Beatmap, Mods, OsuStars, Strains,
|
||||
};
|
||||
|
||||
const SECTION_LENGTH: f64 = 750.0;
|
||||
const STAR_SCALING_FACTOR: f64 = 0.153;
|
||||
@@ -128,7 +130,6 @@ fn calculate_movement(params: CatchStars<'_>) -> (Movement, CatchDifficultyAttri
|
||||
} = params;
|
||||
|
||||
let take = passed_objects.unwrap_or(usize::MAX);
|
||||
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let clock_rate = clock_rate.unwrap_or(map_attributes.clock_rate);
|
||||
|
||||
@@ -274,7 +275,27 @@ impl CatchPerformanceAttributes {
|
||||
}
|
||||
|
||||
impl From<CatchPerformanceAttributes> for CatchDifficultyAttributes {
|
||||
#[inline]
|
||||
fn from(attributes: CatchPerformanceAttributes) -> Self {
|
||||
attributes.difficulty
|
||||
}
|
||||
}
|
||||
|
||||
impl<'map> From<OsuStars<'map>> for CatchStars<'map> {
|
||||
#[inline]
|
||||
fn from(osu: OsuStars<'map>) -> Self {
|
||||
let OsuStars {
|
||||
map,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
} = osu;
|
||||
|
||||
Self {
|
||||
map,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-1
@@ -1,5 +1,5 @@
|
||||
use super::{CatchDifficultyAttributes, CatchPerformanceAttributes, CatchScoreState, CatchStars};
|
||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
||||
use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
|
||||
|
||||
/// Performance calculator on osu!catch maps.
|
||||
///
|
||||
@@ -431,6 +431,43 @@ impl CatchPPInner {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
|
||||
fn from(osu: OsuPP<'map>) -> Self {
|
||||
let OsuPP {
|
||||
map,
|
||||
mods,
|
||||
acc,
|
||||
combo,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
n_misses,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
..
|
||||
} = osu;
|
||||
|
||||
let res = Self {
|
||||
map,
|
||||
attributes: None,
|
||||
mods,
|
||||
combo,
|
||||
n_fruits: n300,
|
||||
n_droplets: n100,
|
||||
n_tiny_droplets: n50,
|
||||
n_tiny_droplet_misses: None,
|
||||
n_misses,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
};
|
||||
|
||||
match acc {
|
||||
Some(acc) => res.accuracy(acc),
|
||||
None => res,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||
pub trait CatchAttributeProvider {
|
||||
/// Provide the actual difficulty attributes.
|
||||
|
||||
@@ -56,24 +56,29 @@ mod test {
|
||||
TimingPoint {
|
||||
time: 1.0,
|
||||
beat_len: 10.0,
|
||||
kiai: false,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 3.0,
|
||||
beat_len: 20.0,
|
||||
kiai: false,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 4.0,
|
||||
beat_len: 30.0,
|
||||
kiai: false,
|
||||
},
|
||||
],
|
||||
difficulty_points: vec![
|
||||
DifficultyPoint {
|
||||
time: 2.0,
|
||||
speed_multiplier: 15.0,
|
||||
kiai: false,
|
||||
},
|
||||
DifficultyPoint {
|
||||
time: 5.0,
|
||||
speed_multiplier: 45.0,
|
||||
kiai: false,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
|
||||
@@ -268,6 +268,7 @@ impl From<ScoreState> for TaikoScoreState {
|
||||
/// assert!(gradual_perf.process_next_object(state).is_none());
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum GradualPerformanceAttributes<'map> {
|
||||
/// Gradual osu!catch performance attributes.
|
||||
Catch(CatchGradualPerformanceAttributes<'map>),
|
||||
|
||||
+1
-2
@@ -1,7 +1,5 @@
|
||||
//! A standalone crate to calculate star ratings and performance points for all [osu!](https://osu.ppy.sh/home) gamemodes.
|
||||
//!
|
||||
//! Conversions between game modes (i.e. "converts") are generally not supported.
|
||||
//!
|
||||
//! Async is supported through features, see below.
|
||||
//!
|
||||
//! ## Usage
|
||||
@@ -208,6 +206,7 @@ mod stars;
|
||||
pub use stars::AnyStars;
|
||||
|
||||
mod curve;
|
||||
mod limited_queue;
|
||||
mod mods;
|
||||
|
||||
pub use catch::{CatchPP, CatchStars};
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::iter::{Cycle, Skip, Take};
|
||||
use std::ops::Index;
|
||||
use std::slice::Iter;
|
||||
|
||||
// TODO: make generic over const size
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct LimitedQueue<T> {
|
||||
queue: Vec<T>,
|
||||
@@ -90,6 +91,7 @@ impl<T> Index<usize> for LimitedQueue<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace with simple type definition
|
||||
pub(crate) struct LimitedQueueIter<'a, T> {
|
||||
iter: Take<Skip<Cycle<Iter<'a, T>>>>,
|
||||
}
|
||||
+26
-7
@@ -3,12 +3,14 @@ mod gradual_performance;
|
||||
mod pp;
|
||||
mod strain;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use gradual_difficulty::*;
|
||||
pub use gradual_performance::*;
|
||||
pub use pp::*;
|
||||
use strain::Strain;
|
||||
|
||||
use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains};
|
||||
use crate::{parse::HitObject, Beatmap, GameMode, Mods, OsuStars, Strains};
|
||||
|
||||
const SECTION_LEN: f64 = 400.0;
|
||||
const STAR_SCALING_FACTOR: f64 = 0.018;
|
||||
@@ -33,7 +35,7 @@ const STAR_SCALING_FACTOR: f64 = 0.018;
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ManiaStars<'map> {
|
||||
map: &'map Beatmap,
|
||||
map: Cow<'map, Beatmap>,
|
||||
mods: u32,
|
||||
passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
@@ -44,7 +46,7 @@ impl<'map> ManiaStars<'map> {
|
||||
#[inline]
|
||||
pub fn new(map: &'map Beatmap) -> Self {
|
||||
Self {
|
||||
map,
|
||||
map: Cow::Borrowed(map),
|
||||
mods: 0,
|
||||
passed_objects: None,
|
||||
clock_rate: None,
|
||||
@@ -189,12 +191,9 @@ pub(crate) struct DifficultyHitObject<'o> {
|
||||
impl<'o> DifficultyHitObject<'o> {
|
||||
#[inline]
|
||||
fn new(base: &'o HitObject, prev: &'o HitObject, columns: f32, clock_rate: f64) -> Self {
|
||||
let x_divisor = 512.0 / columns;
|
||||
let column = (base.pos.x / x_divisor).floor().min(columns - 1.0) as usize;
|
||||
|
||||
Self {
|
||||
base,
|
||||
column,
|
||||
column: base.column(columns) as usize,
|
||||
delta: (base.start_time - prev.start_time) / clock_rate,
|
||||
start_time: base.start_time / clock_rate,
|
||||
}
|
||||
@@ -236,7 +235,27 @@ impl ManiaPerformanceAttributes {
|
||||
}
|
||||
|
||||
impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes {
|
||||
#[inline]
|
||||
fn from(attributes: ManiaPerformanceAttributes) -> Self {
|
||||
attributes.difficulty
|
||||
}
|
||||
}
|
||||
|
||||
impl<'map> From<OsuStars<'map>> for ManiaStars<'map> {
|
||||
#[inline]
|
||||
fn from(osu: OsuStars<'map>) -> Self {
|
||||
let OsuStars {
|
||||
map,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
} = osu;
|
||||
|
||||
Self {
|
||||
map: map.convert_mode(GameMode::MNA),
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+41
-5
@@ -1,5 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaStars};
|
||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
||||
use crate::{Beatmap, DifficultyAttributes, GameMode, Mods, OsuPP, PerformanceAttributes};
|
||||
|
||||
/// Performance calculator on osu!mania maps.
|
||||
///
|
||||
@@ -31,7 +33,7 @@ use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct ManiaPP<'map> {
|
||||
map: &'map Beatmap,
|
||||
map: Cow<'map, Beatmap>,
|
||||
stars: Option<f64>,
|
||||
mods: u32,
|
||||
pub(crate) score: Option<f64>,
|
||||
@@ -44,7 +46,7 @@ impl<'map> ManiaPP<'map> {
|
||||
#[inline]
|
||||
pub fn new(map: &'map Beatmap) -> Self {
|
||||
Self {
|
||||
map,
|
||||
map: Cow::Borrowed(map),
|
||||
stars: None,
|
||||
mods: 0,
|
||||
score: None,
|
||||
@@ -112,7 +114,7 @@ impl<'map> ManiaPP<'map> {
|
||||
/// Calculate all performance related values, including pp and stars.
|
||||
pub fn calculate(self) -> ManiaPerformanceAttributes {
|
||||
let stars = self.stars.unwrap_or_else(|| {
|
||||
let mut calculator = ManiaStars::new(self.map).mods(self.mods);
|
||||
let mut calculator = ManiaStars::new(self.map.as_ref()).mods(self.mods);
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
@@ -154,7 +156,19 @@ impl<'map> ManiaPP<'map> {
|
||||
od *= 1.4;
|
||||
}
|
||||
|
||||
let hit_window = ((od * clock_rate).floor() / clock_rate).ceil();
|
||||
let hit_window = {
|
||||
let not_converted = matches!(self.map, Cow::Borrowed(_));
|
||||
|
||||
let value = if not_converted {
|
||||
od
|
||||
} else if self.map.od > 4.0 {
|
||||
34.0
|
||||
} else {
|
||||
47.0
|
||||
};
|
||||
|
||||
((value * clock_rate).floor() / clock_rate).ceil()
|
||||
};
|
||||
|
||||
let strain_value = self.compute_strain(scaled_score, stars);
|
||||
let acc_value = self.compute_accuracy_value(scaled_score, strain_value, hit_window);
|
||||
@@ -199,6 +213,28 @@ impl<'map> ManiaPP<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'map> From<OsuPP<'map>> for ManiaPP<'map> {
|
||||
#[inline]
|
||||
fn from(osu: OsuPP<'map>) -> Self {
|
||||
let OsuPP {
|
||||
map,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
..
|
||||
} = osu;
|
||||
|
||||
Self {
|
||||
map: map.convert_mode(GameMode::MNA),
|
||||
stars: None,
|
||||
mods,
|
||||
score: None,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||
pub trait ManiaAttributeProvider {
|
||||
/// Provide the star rating (only difficulty attribute for osu!mania).
|
||||
|
||||
+17
-5
@@ -20,7 +20,7 @@ use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
||||
use crate::{curve::CurveBuffers, AnyStars, Beatmap, GameMode, Mods, Strains};
|
||||
|
||||
use self::skill::Skills;
|
||||
|
||||
@@ -49,10 +49,10 @@ const STACK_DISTANCE: f32 = 3.0;
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OsuStars<'map> {
|
||||
map: &'map Beatmap,
|
||||
mods: u32,
|
||||
passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
pub(crate) map: &'map Beatmap,
|
||||
pub(crate) mods: u32,
|
||||
pub(crate) passed_objects: Option<usize>,
|
||||
pub(crate) clock_rate: Option<f64>,
|
||||
}
|
||||
|
||||
impl<'map> OsuStars<'map> {
|
||||
@@ -67,6 +67,17 @@ impl<'map> OsuStars<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the map into another mode.
|
||||
#[inline]
|
||||
pub fn mode(self, mode: GameMode) -> AnyStars<'map> {
|
||||
match mode {
|
||||
GameMode::STD => AnyStars::Osu(self),
|
||||
GameMode::TKO => AnyStars::Taiko(self.into()),
|
||||
GameMode::CTB => AnyStars::Catch(self.into()),
|
||||
GameMode::MNA => AnyStars::Mania(self.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
@@ -578,6 +589,7 @@ impl OsuPerformanceAttributes {
|
||||
}
|
||||
|
||||
impl From<OsuPerformanceAttributes> for OsuDifficultyAttributes {
|
||||
#[inline]
|
||||
fn from(attributes: OsuPerformanceAttributes) -> Self {
|
||||
attributes.difficulty
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ impl OsuObject {
|
||||
pixel_len,
|
||||
repeats,
|
||||
control_points,
|
||||
..
|
||||
} => {
|
||||
attributes.n_sliders += 1;
|
||||
|
||||
|
||||
+19
-6
@@ -1,5 +1,7 @@
|
||||
use super::{OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState};
|
||||
use crate::{Beatmap, DifficultyAttributes, Mods, OsuStars, PerformanceAttributes};
|
||||
use crate::{
|
||||
AnyPP, Beatmap, DifficultyAttributes, GameMode, Mods, OsuStars, PerformanceAttributes,
|
||||
};
|
||||
|
||||
/// Performance calculator on osu!standard maps.
|
||||
///
|
||||
@@ -33,10 +35,10 @@ use crate::{Beatmap, DifficultyAttributes, Mods, OsuStars, PerformanceAttributes
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct OsuPP<'map> {
|
||||
map: &'map Beatmap,
|
||||
attributes: Option<OsuDifficultyAttributes>,
|
||||
mods: u32,
|
||||
acc: Option<f64>,
|
||||
pub(crate) map: &'map Beatmap,
|
||||
pub(crate) attributes: Option<OsuDifficultyAttributes>,
|
||||
pub(crate) mods: u32,
|
||||
pub(crate) acc: Option<f64>,
|
||||
pub(crate) combo: Option<usize>,
|
||||
|
||||
pub(crate) n300: Option<usize>,
|
||||
@@ -44,7 +46,7 @@ pub struct OsuPP<'map> {
|
||||
pub(crate) n50: Option<usize>,
|
||||
pub(crate) n_misses: usize,
|
||||
pub(crate) passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
pub(crate) clock_rate: Option<f64>,
|
||||
}
|
||||
|
||||
impl<'map> OsuPP<'map> {
|
||||
@@ -67,6 +69,17 @@ impl<'map> OsuPP<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the map into another mode.
|
||||
#[inline]
|
||||
pub fn mode(self, mode: GameMode) -> AnyPP<'map> {
|
||||
match mode {
|
||||
GameMode::STD => AnyPP::Osu(self),
|
||||
GameMode::TKO => AnyPP::Taiko(self.into()),
|
||||
GameMode::CTB => AnyPP::Catch(self.into()),
|
||||
GameMode::MNA => AnyPP::Mania(self.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide the result of a previous difficulty or performance calculation.
|
||||
/// If you already calculated the attributes for the current map-mod combination,
|
||||
/// be sure to put them in here so that they don't have to be recalculated.
|
||||
|
||||
@@ -55,24 +55,29 @@ mod test {
|
||||
TimingPoint {
|
||||
time: 1.0,
|
||||
beat_len: 10.0,
|
||||
kiai: false,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 3.0,
|
||||
beat_len: 20.0,
|
||||
kiai: false,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 4.0,
|
||||
beat_len: 30.0,
|
||||
kiai: false,
|
||||
},
|
||||
],
|
||||
difficulty_points: vec![
|
||||
DifficultyPoint {
|
||||
time: 2.0,
|
||||
speed_multiplier: 15.0,
|
||||
kiai: false,
|
||||
},
|
||||
DifficultyPoint {
|
||||
time: 5.0,
|
||||
speed_multiplier: 45.0,
|
||||
kiai: false,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
|
||||
+91
-80
@@ -1,80 +1,91 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::{PathControlPoint, Pos2};
|
||||
|
||||
/// "Intermediate" hitobject created through parsing.
|
||||
/// Each mode will handle them differently.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct HitObject {
|
||||
/// The position of the object.
|
||||
pub pos: Pos2,
|
||||
/// The start time of the object.
|
||||
pub start_time: f64,
|
||||
/// The type of the object.
|
||||
pub kind: HitObjectKind,
|
||||
}
|
||||
|
||||
impl HitObject {
|
||||
/// The end time of the object.
|
||||
#[inline]
|
||||
pub fn end_time(&self) -> f64 {
|
||||
match &self.kind {
|
||||
HitObjectKind::Circle { .. } => self.start_time,
|
||||
// incorrect, only called in mania which has no sliders though
|
||||
HitObjectKind::Slider { .. } => self.start_time,
|
||||
HitObjectKind::Spinner { end_time } => *end_time,
|
||||
HitObjectKind::Hold { end_time, .. } => *end_time,
|
||||
}
|
||||
}
|
||||
|
||||
/// If the object is a circle.
|
||||
#[inline]
|
||||
pub fn is_circle(&self) -> bool {
|
||||
matches!(self.kind, HitObjectKind::Circle { .. })
|
||||
}
|
||||
|
||||
/// If the object is a slider.
|
||||
#[inline]
|
||||
pub fn is_slider(&self) -> bool {
|
||||
matches!(self.kind, HitObjectKind::Slider { .. })
|
||||
}
|
||||
|
||||
/// If the object is a spinner.
|
||||
#[inline]
|
||||
pub fn is_spinner(&self) -> bool {
|
||||
matches!(self.kind, HitObjectKind::Spinner { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for HitObject {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.start_time.partial_cmp(&other.start_time)
|
||||
}
|
||||
}
|
||||
|
||||
/// Further data related to specific object types.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum HitObjectKind {
|
||||
/// A circle object.
|
||||
Circle,
|
||||
/// A full slider object.
|
||||
Slider {
|
||||
/// Total length of the slider in pixels.
|
||||
pixel_len: f64,
|
||||
/// The amount of repeat points of the slider.
|
||||
repeats: usize,
|
||||
/// The control points of the slider.
|
||||
control_points: Vec<PathControlPoint>,
|
||||
},
|
||||
/// A spinner object.
|
||||
Spinner {
|
||||
/// The end time of the spinner.
|
||||
end_time: f64,
|
||||
},
|
||||
/// A hold note object for osu!mania.
|
||||
Hold {
|
||||
/// The end time of the hold object.
|
||||
end_time: f64,
|
||||
},
|
||||
}
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::{PathControlPoint, Pos2};
|
||||
|
||||
/// "Intermediate" hitobject created through parsing.
|
||||
/// Each mode will handle them differently.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct HitObject {
|
||||
/// The position of the object.
|
||||
pub pos: Pos2,
|
||||
/// The start time of the object.
|
||||
pub start_time: f64,
|
||||
/// The type of the object.
|
||||
pub kind: HitObjectKind,
|
||||
}
|
||||
|
||||
impl HitObject {
|
||||
/// The end time of the object.
|
||||
#[inline]
|
||||
pub fn end_time(&self) -> f64 {
|
||||
match &self.kind {
|
||||
HitObjectKind::Circle { .. } => self.start_time,
|
||||
// incorrect, only called in mania which has no sliders though
|
||||
HitObjectKind::Slider { .. } => self.start_time,
|
||||
HitObjectKind::Spinner { end_time } => *end_time,
|
||||
HitObjectKind::Hold { end_time, .. } => *end_time,
|
||||
}
|
||||
}
|
||||
|
||||
/// If the object is a circle.
|
||||
#[inline]
|
||||
pub fn is_circle(&self) -> bool {
|
||||
matches!(self.kind, HitObjectKind::Circle { .. })
|
||||
}
|
||||
|
||||
/// If the object is a slider.
|
||||
#[inline]
|
||||
pub fn is_slider(&self) -> bool {
|
||||
matches!(self.kind, HitObjectKind::Slider { .. })
|
||||
}
|
||||
|
||||
/// If the object is a spinner.
|
||||
#[inline]
|
||||
pub fn is_spinner(&self) -> bool {
|
||||
matches!(self.kind, HitObjectKind::Spinner { .. })
|
||||
}
|
||||
|
||||
/// The column of this node for osu!mania
|
||||
#[inline]
|
||||
pub fn column(&self, total_columns: f32) -> u8 {
|
||||
let x_divisor = 512.0 / total_columns;
|
||||
|
||||
(self.pos.x / x_divisor).floor().min(total_columns - 1.0) as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for HitObject {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.start_time.partial_cmp(&other.start_time)
|
||||
}
|
||||
}
|
||||
|
||||
/// Further data related to specific object types.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum HitObjectKind {
|
||||
/// A circle object.
|
||||
Circle,
|
||||
/// A full slider object.
|
||||
Slider {
|
||||
/// Total length of the slider in pixels.
|
||||
pixel_len: f64,
|
||||
/// The amount of repeat points of the slider.
|
||||
repeats: usize,
|
||||
/// The control points of the slider.
|
||||
control_points: Vec<PathControlPoint>,
|
||||
/// Sample sounds for the slider head, end, and repeat points.
|
||||
/// Required for converts.
|
||||
edge_sounds: Vec<u8>,
|
||||
},
|
||||
/// A spinner object.
|
||||
Spinner {
|
||||
/// The end time of the spinner.
|
||||
end_time: f64,
|
||||
},
|
||||
/// A hold note object for osu!mania.
|
||||
Hold {
|
||||
/// The end time of the hold object.
|
||||
end_time: f64,
|
||||
},
|
||||
}
|
||||
|
||||
+168
-157
@@ -12,9 +12,9 @@ pub use pos2::Pos2;
|
||||
pub use slider_parsing::*;
|
||||
|
||||
use reader::FileReader;
|
||||
use sort::legacy_sort;
|
||||
pub(crate) use sort::legacy_sort;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::{cmp::Ordering, num::ParseIntError};
|
||||
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
use std::{fs::File, io::Read};
|
||||
@@ -28,7 +28,7 @@ use std::path::Path;
|
||||
#[cfg(feature = "async_std")]
|
||||
use async_std::{fs::File, io::Read as AsyncRead, path::Path};
|
||||
|
||||
use crate::beatmap::{Beatmap, DifficultyPoint, GameMode, TimingPoint};
|
||||
use crate::beatmap::{Beatmap, Break, DifficultyPoint, GameMode, TimingPoint};
|
||||
|
||||
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
||||
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
||||
@@ -121,28 +121,6 @@ macro_rules! parse_general_body {
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse_general {
|
||||
() => {
|
||||
fn parse_general<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_general_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
|
||||
(async) => {
|
||||
async fn parse_general<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_general_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_difficulty_body {
|
||||
($self:ident, $reader:ident, $section:ident) => {{
|
||||
let mut ar = None;
|
||||
@@ -187,26 +165,34 @@ macro_rules! parse_difficulty_body {
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse_difficulty {
|
||||
() => {
|
||||
fn parse_difficulty<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_difficulty_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
macro_rules! parse_events_body {
|
||||
($self:ident, $reader:ident, $section:ident) => {{
|
||||
let mut empty = true;
|
||||
|
||||
(async) => {
|
||||
async fn parse_difficulty<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_difficulty_body!(self, reader, section)
|
||||
while next_line!($reader)? != 0 {
|
||||
if let Some(bytes) = $reader.get_section() {
|
||||
*$section = Section::from_bytes(bytes);
|
||||
empty = false;
|
||||
break;
|
||||
}
|
||||
|
||||
let line = $reader.get_line()?;
|
||||
let mut split = line.split(',');
|
||||
|
||||
// We're only interested in breaks
|
||||
if let Some(b'2') = split.next().and_then(|value| value.bytes().next()) {
|
||||
let start_time = split.next().next_field("break start")?.parse()?;
|
||||
let end_time = split.next().next_field("break end")?.parse()?;
|
||||
|
||||
$self.breaks.push(Break {
|
||||
start_time,
|
||||
end_time,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(empty)
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse_timingpoints_body {
|
||||
@@ -238,10 +224,20 @@ macro_rules! parse_timingpoints_body {
|
||||
|
||||
let beat_len: f64 = split.next().next_field("beat len")?.trim().parse()?;
|
||||
let timing_change = split.nth(4).and_then(|value| value.bytes().next());
|
||||
let effect_flags = split.next().and_then(|value| value.bytes().next());
|
||||
|
||||
let kiai = matches!(effect_flags, Some(b'1'));
|
||||
|
||||
if matches!(timing_change, Some(b'1') | None) {
|
||||
let beat_len = beat_len.clamp(6.0, 60_000.0);
|
||||
$self.timing_points.push(TimingPoint { time, beat_len });
|
||||
|
||||
let point = TimingPoint {
|
||||
time,
|
||||
beat_len,
|
||||
kiai,
|
||||
};
|
||||
|
||||
$self.timing_points.push(point);
|
||||
|
||||
if time < prev_time {
|
||||
unsorted_timings = true;
|
||||
@@ -258,6 +254,7 @@ macro_rules! parse_timingpoints_body {
|
||||
let point = DifficultyPoint {
|
||||
time,
|
||||
speed_multiplier,
|
||||
kiai,
|
||||
};
|
||||
|
||||
$self.difficulty_points.push(point);
|
||||
@@ -282,28 +279,6 @@ macro_rules! parse_timingpoints_body {
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse_timingpoints {
|
||||
() => {
|
||||
fn parse_timingpoints<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_timingpoints_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
|
||||
(async) => {
|
||||
async fn parse_timingpoints<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_timingpoints_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_hitobjects_body {
|
||||
($self:ident, $reader:ident, $section:ident) => {{
|
||||
let mut unsorted = false;
|
||||
@@ -430,10 +405,25 @@ macro_rules! parse_hitobjects_body {
|
||||
.max(0.0)
|
||||
.min(MAX_COORDINATE_VALUE);
|
||||
|
||||
let edge_sounds_opt = split.next().map(|sounds| {
|
||||
sounds
|
||||
.split('|')
|
||||
.take(repeats + 2)
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
});
|
||||
|
||||
let edge_sounds = match edge_sounds_opt {
|
||||
None => Vec::new(),
|
||||
Some(Ok(sounds)) => sounds,
|
||||
Some(Err(err)) => return Err(ParseIntError::into(err)),
|
||||
};
|
||||
|
||||
HitObjectKind::Slider {
|
||||
repeats,
|
||||
pixel_len,
|
||||
control_points,
|
||||
edge_sounds,
|
||||
}
|
||||
}
|
||||
} else if kind & Self::SPINNER_FLAG > 0 {
|
||||
@@ -482,28 +472,6 @@ macro_rules! parse_hitobjects_body {
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse_hitobjects {
|
||||
() => {
|
||||
fn parse_hitobjects<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_hitobjects_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
|
||||
(async) => {
|
||||
async fn parse_hitobjects<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_hitobjects_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_body {
|
||||
($input:ident) => {{
|
||||
let mut reader = FileReader::new($input);
|
||||
@@ -526,6 +494,7 @@ macro_rules! parse_body {
|
||||
match section {
|
||||
Section::General => section!(map, parse_general, reader, section),
|
||||
Section::Difficulty => section!(map, parse_difficulty, reader, section),
|
||||
Section::Events => section!(map, parse_events, reader, section),
|
||||
Section::TimingPoints => section!(map, parse_timingpoints, reader, section),
|
||||
Section::HitObjects => section!(map, parse_hitobjects, reader, section),
|
||||
Section::None => {
|
||||
@@ -544,53 +513,6 @@ macro_rules! parse_body {
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse {
|
||||
() => {
|
||||
/// Parse a beatmap from a `.osu` file.
|
||||
///
|
||||
/// As argument you can give anything that implements [`std::io::Read`].
|
||||
/// You'll likely want to pass (a reference of) a [`File`](std::fs::File)
|
||||
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
|
||||
parse_body!(input)
|
||||
}
|
||||
};
|
||||
|
||||
(async) => {
|
||||
/// Parse a beatmap from a `.osu` file.
|
||||
///
|
||||
/// As argument you can give anything that implements `tokio::io::AsyncRead`
|
||||
/// or `async_std::io::Read`, depending which feature you chose.
|
||||
/// You'll likely want to pass a `File`
|
||||
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||
pub async fn parse<R: AsyncRead + Unpin>(input: R) -> ParseResult<Self> {
|
||||
parse_body!(input)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! from_path {
|
||||
() => {
|
||||
/// Pass the path to a `.osu` file.
|
||||
///
|
||||
/// Useful when you don't want to create the [`File`](std::fs::File) manually.
|
||||
/// If you have the file lying around already though (and plan on re-using it),
|
||||
/// passing `&file` to [`parse`](Beatmap::parse) should be preferred.
|
||||
pub fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
|
||||
Self::parse(File::open(path)?)
|
||||
}
|
||||
};
|
||||
|
||||
(async) => {
|
||||
/// Pass the path to a `.osu` file.
|
||||
///
|
||||
/// Useful when you don't want to create the file manually.
|
||||
pub async fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
|
||||
Self::parse(File::open(path).await?).await
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Beatmap {
|
||||
const CIRCLE_FLAG: u8 = 1 << 0;
|
||||
const SLIDER_FLAG: u8 = 1 << 1;
|
||||
@@ -751,35 +673,122 @@ mod slider_parsing {
|
||||
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
impl Beatmap {
|
||||
parse!();
|
||||
parse_general!();
|
||||
parse_difficulty!();
|
||||
parse_timingpoints!();
|
||||
parse_hitobjects!();
|
||||
/// Parse a beatmap from a `.osu` file.
|
||||
///
|
||||
/// As argument you can give anything that implements [`std::io::Read`].
|
||||
/// You'll likely want to pass (a reference of) a [`File`](std::fs::File)
|
||||
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
|
||||
parse_body!(input)
|
||||
}
|
||||
|
||||
from_path!();
|
||||
fn parse_general<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_general_body!(self, reader, section)
|
||||
}
|
||||
|
||||
fn parse_difficulty<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_difficulty_body!(self, reader, section)
|
||||
}
|
||||
|
||||
fn parse_events<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_events_body!(self, reader, section)
|
||||
}
|
||||
|
||||
fn parse_hitobjects<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_hitobjects_body!(self, reader, section)
|
||||
}
|
||||
|
||||
fn parse_timingpoints<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_timingpoints_body!(self, reader, section)
|
||||
}
|
||||
/// Pass the path to a `.osu` file.
|
||||
///
|
||||
/// Useful when you don't want to create the [`File`](std::fs::File) manually.
|
||||
/// If you have the file lying around already though (and plan on re-using it),
|
||||
/// passing `&file` to [`parse`](Beatmap::parse) should be preferred.
|
||||
pub fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
|
||||
Self::parse(File::open(path)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async_tokio")]
|
||||
#[cfg(any(feature = "async_tokio", feature = "async_std"))]
|
||||
impl Beatmap {
|
||||
parse!(async);
|
||||
parse_general!(async);
|
||||
parse_difficulty!(async);
|
||||
parse_timingpoints!(async);
|
||||
parse_hitobjects!(async);
|
||||
/// Parse a beatmap from a `.osu` file.
|
||||
///
|
||||
/// As argument you can give anything that implements `tokio::io::AsyncRead`
|
||||
/// or `async_std::io::Read`, depending which feature you chose.
|
||||
/// You'll likely want to pass a `File`
|
||||
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||
pub async fn parse<R: AsyncRead + Unpin>(input: R) -> ParseResult<Self> {
|
||||
parse_body!(input)
|
||||
}
|
||||
|
||||
from_path!(async);
|
||||
}
|
||||
async fn parse_general<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_general_body!(self, reader, section)
|
||||
}
|
||||
|
||||
#[cfg(feature = "async_std")]
|
||||
impl Beatmap {
|
||||
parse!(async);
|
||||
parse_general!(async);
|
||||
parse_difficulty!(async);
|
||||
parse_timingpoints!(async);
|
||||
parse_hitobjects!(async);
|
||||
async fn parse_difficulty<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_difficulty_body!(self, reader, section)
|
||||
}
|
||||
|
||||
from_path!(async);
|
||||
async fn parse_events<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_events_body!(self, reader, section)
|
||||
}
|
||||
|
||||
async fn parse_hitobjects<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_hitobjects_body!(self, reader, section)
|
||||
}
|
||||
|
||||
async fn parse_timingpoints<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_timingpoints_body!(self, reader, section)
|
||||
}
|
||||
|
||||
/// Pass the path to a `.osu` file.
|
||||
///
|
||||
/// Useful when you don't want to create the file manually.
|
||||
pub async fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
|
||||
Self::parse(File::open(path).await?).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -789,6 +798,7 @@ enum Section {
|
||||
Difficulty,
|
||||
TimingPoints,
|
||||
HitObjects,
|
||||
Events,
|
||||
}
|
||||
|
||||
impl Section {
|
||||
@@ -798,6 +808,7 @@ impl Section {
|
||||
b"Difficulty" => Self::Difficulty,
|
||||
b"TimingPoints" => Self::TimingPoints,
|
||||
b"HitObjects" => Self::HitObjects,
|
||||
b"Events" => Self::Events,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -43,6 +43,7 @@ macro_rules! read_until {
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macro_rules)]
|
||||
macro_rules! impl_reader {
|
||||
() => {
|
||||
impl<R: Read> FileReader<R> {
|
||||
@@ -96,10 +97,7 @@ macro_rules! impl_reader {
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
impl_reader!();
|
||||
|
||||
#[cfg(feature = "async_tokio")]
|
||||
impl_reader!(async);
|
||||
|
||||
#[cfg(feature = "async_std")]
|
||||
#[cfg(any(feature = "async_tokio", feature = "async_std"))]
|
||||
impl_reader!(async);
|
||||
|
||||
impl<R> FileReader<R> {
|
||||
|
||||
@@ -85,6 +85,20 @@ impl<'map> AnyPP<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// If the map is an osu!standard map, convert it to another mode.
|
||||
#[inline]
|
||||
pub fn mode(self, mode: GameMode) -> Self {
|
||||
match self {
|
||||
AnyPP::Osu(o) => match mode {
|
||||
GameMode::STD => AnyPP::Osu(o),
|
||||
GameMode::TKO => AnyPP::Taiko(o.into()),
|
||||
GameMode::CTB => AnyPP::Catch(o.into()),
|
||||
GameMode::MNA => AnyPP::Mania(o.into()),
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
|
||||
@@ -44,6 +44,19 @@ impl<'map> AnyStars<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// If the map is an osu!standard map, convert it to another mode.
|
||||
pub fn mode(self, mode: GameMode) -> Self {
|
||||
match self {
|
||||
AnyStars::Osu(o) => match mode {
|
||||
GameMode::STD => AnyStars::Osu(o),
|
||||
GameMode::TKO => AnyStars::Taiko(o.into()),
|
||||
GameMode::CTB => AnyStars::Catch(o.into()),
|
||||
GameMode::MNA => AnyStars::Mania(o.into()),
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
|
||||
+24
-5
@@ -2,7 +2,6 @@ mod difficulty_object;
|
||||
mod gradual_difficulty;
|
||||
mod gradual_performance;
|
||||
mod hitobject_rhythm;
|
||||
mod limited_queue;
|
||||
mod pp;
|
||||
mod rim;
|
||||
mod skill;
|
||||
@@ -14,7 +13,6 @@ use difficulty_object::DifficultyObject;
|
||||
pub use gradual_difficulty::*;
|
||||
pub use gradual_performance::*;
|
||||
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
||||
use limited_queue::LimitedQueue;
|
||||
pub use pp::*;
|
||||
use rim::Rim;
|
||||
use skill_kind::SkillKind;
|
||||
@@ -22,8 +20,9 @@ use stamina_cheese::StaminaCheeseDetector;
|
||||
use taiko_object::IntoTaikoObjectIter;
|
||||
|
||||
use crate::taiko::skill::Skills;
|
||||
use crate::{Beatmap, Mods, Strains};
|
||||
use crate::{Beatmap, GameMode, Mods, OsuStars, Strains};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::Ordering;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
@@ -53,7 +52,7 @@ const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TaikoStars<'map> {
|
||||
map: &'map Beatmap,
|
||||
map: Cow<'map, Beatmap>,
|
||||
mods: u32,
|
||||
passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
@@ -64,7 +63,7 @@ impl<'map> TaikoStars<'map> {
|
||||
#[inline]
|
||||
pub fn new(map: &'map Beatmap) -> Self {
|
||||
Self {
|
||||
map,
|
||||
map: Cow::Borrowed(map),
|
||||
mods: 0,
|
||||
passed_objects: None,
|
||||
clock_rate: None,
|
||||
@@ -331,7 +330,27 @@ impl TaikoPerformanceAttributes {
|
||||
}
|
||||
|
||||
impl From<TaikoPerformanceAttributes> for TaikoDifficultyAttributes {
|
||||
#[inline]
|
||||
fn from(attributes: TaikoPerformanceAttributes) -> Self {
|
||||
attributes.difficulty
|
||||
}
|
||||
}
|
||||
|
||||
impl<'map> From<OsuStars<'map>> for TaikoStars<'map> {
|
||||
#[inline]
|
||||
fn from(osu: OsuStars<'map>) -> Self {
|
||||
let OsuStars {
|
||||
map,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
} = osu;
|
||||
|
||||
Self {
|
||||
map: map.convert_mode(GameMode::TKO),
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-5
@@ -1,5 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{TaikoDifficultyAttributes, TaikoPerformanceAttributes, TaikoScoreState, TaikoStars};
|
||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
||||
use crate::{Beatmap, DifficultyAttributes, GameMode, Mods, OsuPP, PerformanceAttributes};
|
||||
|
||||
/// Performance calculator on osu!taiko maps.
|
||||
///
|
||||
@@ -33,7 +35,7 @@ use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct TaikoPP<'map> {
|
||||
map: &'map Beatmap,
|
||||
map: Cow<'map, Beatmap>,
|
||||
attributes: Option<TaikoDifficultyAttributes>,
|
||||
mods: u32,
|
||||
combo: Option<usize>,
|
||||
@@ -51,7 +53,7 @@ impl<'map> TaikoPP<'map> {
|
||||
#[inline]
|
||||
pub fn new(map: &'map Beatmap) -> Self {
|
||||
Self {
|
||||
map,
|
||||
map: Cow::Borrowed(map),
|
||||
attributes: None,
|
||||
mods: 0,
|
||||
combo: None,
|
||||
@@ -171,7 +173,7 @@ impl<'map> TaikoPP<'map> {
|
||||
/// Calculate all performance related values, including pp and stars.
|
||||
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
||||
let attributes = self.attributes.take().unwrap_or_else(|| {
|
||||
let mut calculator = TaikoStars::new(self.map).mods(self.mods);
|
||||
let mut calculator = TaikoStars::new(self.map.as_ref()).mods(self.mods);
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
@@ -205,7 +207,7 @@ impl<'map> TaikoPP<'map> {
|
||||
}
|
||||
|
||||
let inner = TaikoPPInner {
|
||||
map: self.map,
|
||||
map: self.map.as_ref(),
|
||||
attributes,
|
||||
mods: self.mods,
|
||||
acc: self.acc,
|
||||
@@ -302,6 +304,37 @@ fn difficulty_range_od(od: f64) -> f64 {
|
||||
crate::difficulty_range(od, 20.0, 35.0, 50.0)
|
||||
}
|
||||
|
||||
impl<'map> From<OsuPP<'map>> for TaikoPP<'map> {
|
||||
#[inline]
|
||||
fn from(osu: OsuPP<'map>) -> Self {
|
||||
let OsuPP {
|
||||
map,
|
||||
mods,
|
||||
acc,
|
||||
combo,
|
||||
n300,
|
||||
n100,
|
||||
n_misses,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
..
|
||||
} = osu;
|
||||
|
||||
Self {
|
||||
map: map.convert_mode(GameMode::TKO),
|
||||
attributes: None,
|
||||
mods,
|
||||
combo,
|
||||
acc: acc.unwrap_or(1.0),
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
n300,
|
||||
n100,
|
||||
n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||
pub trait TaikoAttributeProvider {
|
||||
/// Provide the actual difficulty attributes.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::{DifficultyObject, HitObjectRhythm, LimitedQueue, Rim};
|
||||
use crate::limited_queue::LimitedQueue;
|
||||
|
||||
use super::{DifficultyObject, HitObjectRhythm, Rim};
|
||||
|
||||
use std::ops::Index;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{LimitedQueue, Rim};
|
||||
use crate::Beatmap;
|
||||
use super::Rim;
|
||||
use crate::{limited_queue::LimitedQueue, Beatmap};
|
||||
|
||||
const ROLL_MIN_REPETITIONS: usize = 12;
|
||||
const TL_MIN_REPETITIONS: isize = 16;
|
||||
|
||||
Reference in New Issue
Block a user