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__:
|
- __Additions__:
|
||||||
- Added the `ControlPoint` and `ControlPointerIter` types to the public interface, aswell as the `Beatmap::control_points` method
|
- 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:__
|
- __Breaking changes:__
|
||||||
- Moved some types to a different module. The following types can now be found in `rosu_pp::beatmap`:
|
- Moved some types to a different module. The following types can now be found in `rosu_pp::beatmap`:
|
||||||
- `Beatmap`
|
- `Beatmap`
|
||||||
@@ -11,6 +17,9 @@
|
|||||||
- `DifficultyPoint`
|
- `DifficultyPoint`
|
||||||
- `GameMode`
|
- `GameMode`
|
||||||
- `TimingPoint`
|
- `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)
|
# 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.
|
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.
|
Async is supported through features, see below.
|
||||||
|
|
||||||
### Usage
|
### 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,
|
pub beat_len: f64,
|
||||||
/// The start time of this timing section
|
/// The start time of this timing section
|
||||||
pub time: f64,
|
pub time: f64,
|
||||||
|
/// Whether the section between this and the
|
||||||
|
/// next timing points is a kiai section
|
||||||
|
pub kiai: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for TimingPoint {
|
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.
|
/// [`TimingPoint`] that depends on a previous one.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub struct DifficultyPoint {
|
pub struct DifficultyPoint {
|
||||||
@@ -24,6 +37,9 @@ pub struct DifficultyPoint {
|
|||||||
pub time: f64,
|
pub time: f64,
|
||||||
/// The speed multiplier until the next timing point
|
/// The speed multiplier until the next timing point
|
||||||
pub speed_multiplier: f64,
|
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 {
|
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`].
|
/// Control point for a [`Beatmap`].
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum ControlPoint {
|
pub enum ControlPoint {
|
||||||
@@ -118,24 +144,29 @@ mod test {
|
|||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 1.0,
|
time: 1.0,
|
||||||
beat_len: 10.0,
|
beat_len: 10.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 3.0,
|
time: 3.0,
|
||||||
beat_len: 10.0,
|
beat_len: 10.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 4.0,
|
time: 4.0,
|
||||||
beat_len: 10.0,
|
beat_len: 10.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
difficulty_points: vec![
|
difficulty_points: vec![
|
||||||
DifficultyPoint {
|
DifficultyPoint {
|
||||||
time: 2.0,
|
time: 2.0,
|
||||||
speed_multiplier: 10.0,
|
speed_multiplier: 10.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
DifficultyPoint {
|
DifficultyPoint {
|
||||||
time: 5.0,
|
time: 5.0,
|
||||||
speed_multiplier: 10.0,
|
speed_multiplier: 10.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..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;
|
use crate::parse::HitObject;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
attributes::BeatmapAttributes,
|
attributes::BeatmapAttributes,
|
||||||
|
breaks::Break,
|
||||||
control_points::{ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint},
|
control_points::{ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint},
|
||||||
mode::GameMode,
|
mode::GameMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod attributes;
|
mod attributes;
|
||||||
|
mod breaks;
|
||||||
mod control_points;
|
mod control_points;
|
||||||
|
mod converts;
|
||||||
mod mode;
|
mod mode;
|
||||||
|
|
||||||
/// The main beatmap struct containing all data relevant
|
/// 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 leniency that is used to calculate
|
||||||
/// the stack offset for stacked positions.
|
/// the stack offset for stacked positions.
|
||||||
pub stack_leniency: f32,
|
pub stack_leniency: f32,
|
||||||
|
|
||||||
|
/// All break points of the beatmap.
|
||||||
|
pub breaks: Vec<Break>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Beatmap {
|
impl Beatmap {
|
||||||
@@ -76,4 +84,73 @@ impl Beatmap {
|
|||||||
pub fn control_points(&self) -> ControlPointIter<'_> {
|
pub fn control_points(&self) -> ControlPointIter<'_> {
|
||||||
ControlPointIter::new(self)
|
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,
|
pixel_len,
|
||||||
repeats,
|
repeats,
|
||||||
control_points,
|
control_points,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
// HR business
|
// HR business
|
||||||
params.last_pos = Some(h.pos.x + control_points[control_points.len() - 1].pos.x);
|
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::*;
|
pub use pp::*;
|
||||||
use slider_state::SliderState;
|
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 SECTION_LENGTH: f64 = 750.0;
|
||||||
const STAR_SCALING_FACTOR: f64 = 0.153;
|
const STAR_SCALING_FACTOR: f64 = 0.153;
|
||||||
@@ -128,7 +130,6 @@ fn calculate_movement(params: CatchStars<'_>) -> (Movement, CatchDifficultyAttri
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or(usize::MAX);
|
let take = passed_objects.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
let map_attributes = map.attributes().mods(mods);
|
let map_attributes = map.attributes().mods(mods);
|
||||||
let clock_rate = clock_rate.unwrap_or(map_attributes.clock_rate);
|
let clock_rate = clock_rate.unwrap_or(map_attributes.clock_rate);
|
||||||
|
|
||||||
@@ -274,7 +275,27 @@ impl CatchPerformanceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<CatchPerformanceAttributes> for CatchDifficultyAttributes {
|
impl From<CatchPerformanceAttributes> for CatchDifficultyAttributes {
|
||||||
|
#[inline]
|
||||||
fn from(attributes: CatchPerformanceAttributes) -> Self {
|
fn from(attributes: CatchPerformanceAttributes) -> Self {
|
||||||
attributes.difficulty
|
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 super::{CatchDifficultyAttributes, CatchPerformanceAttributes, CatchScoreState, CatchStars};
|
||||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
|
||||||
|
|
||||||
/// Performance calculator on osu!catch maps.
|
/// 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.
|
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||||
pub trait CatchAttributeProvider {
|
pub trait CatchAttributeProvider {
|
||||||
/// Provide the actual difficulty attributes.
|
/// Provide the actual difficulty attributes.
|
||||||
|
|||||||
@@ -56,24 +56,29 @@ mod test {
|
|||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 1.0,
|
time: 1.0,
|
||||||
beat_len: 10.0,
|
beat_len: 10.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 3.0,
|
time: 3.0,
|
||||||
beat_len: 20.0,
|
beat_len: 20.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 4.0,
|
time: 4.0,
|
||||||
beat_len: 30.0,
|
beat_len: 30.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
difficulty_points: vec![
|
difficulty_points: vec![
|
||||||
DifficultyPoint {
|
DifficultyPoint {
|
||||||
time: 2.0,
|
time: 2.0,
|
||||||
speed_multiplier: 15.0,
|
speed_multiplier: 15.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
DifficultyPoint {
|
DifficultyPoint {
|
||||||
time: 5.0,
|
time: 5.0,
|
||||||
speed_multiplier: 45.0,
|
speed_multiplier: 45.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ impl From<ScoreState> for TaikoScoreState {
|
|||||||
/// assert!(gradual_perf.process_next_object(state).is_none());
|
/// assert!(gradual_perf.process_next_object(state).is_none());
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum GradualPerformanceAttributes<'map> {
|
pub enum GradualPerformanceAttributes<'map> {
|
||||||
/// Gradual osu!catch performance attributes.
|
/// Gradual osu!catch performance attributes.
|
||||||
Catch(CatchGradualPerformanceAttributes<'map>),
|
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.
|
//! 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.
|
//! Async is supported through features, see below.
|
||||||
//!
|
//!
|
||||||
//! ## Usage
|
//! ## Usage
|
||||||
@@ -208,6 +206,7 @@ mod stars;
|
|||||||
pub use stars::AnyStars;
|
pub use stars::AnyStars;
|
||||||
|
|
||||||
mod curve;
|
mod curve;
|
||||||
|
mod limited_queue;
|
||||||
mod mods;
|
mod mods;
|
||||||
|
|
||||||
pub use catch::{CatchPP, CatchStars};
|
pub use catch::{CatchPP, CatchStars};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::iter::{Cycle, Skip, Take};
|
|||||||
use std::ops::Index;
|
use std::ops::Index;
|
||||||
use std::slice::Iter;
|
use std::slice::Iter;
|
||||||
|
|
||||||
|
// TODO: make generic over const size
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct LimitedQueue<T> {
|
pub(crate) struct LimitedQueue<T> {
|
||||||
queue: Vec<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> {
|
pub(crate) struct LimitedQueueIter<'a, T> {
|
||||||
iter: Take<Skip<Cycle<Iter<'a, T>>>>,
|
iter: Take<Skip<Cycle<Iter<'a, T>>>>,
|
||||||
}
|
}
|
||||||
+26
-7
@@ -3,12 +3,14 @@ mod gradual_performance;
|
|||||||
mod pp;
|
mod pp;
|
||||||
mod strain;
|
mod strain;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub use gradual_difficulty::*;
|
pub use gradual_difficulty::*;
|
||||||
pub use gradual_performance::*;
|
pub use gradual_performance::*;
|
||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use strain::Strain;
|
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 SECTION_LEN: f64 = 400.0;
|
||||||
const STAR_SCALING_FACTOR: f64 = 0.018;
|
const STAR_SCALING_FACTOR: f64 = 0.018;
|
||||||
@@ -33,7 +35,7 @@ const STAR_SCALING_FACTOR: f64 = 0.018;
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ManiaStars<'map> {
|
pub struct ManiaStars<'map> {
|
||||||
map: &'map Beatmap,
|
map: Cow<'map, Beatmap>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
clock_rate: Option<f64>,
|
clock_rate: Option<f64>,
|
||||||
@@ -44,7 +46,7 @@ impl<'map> ManiaStars<'map> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(map: &'map Beatmap) -> Self {
|
pub fn new(map: &'map Beatmap) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map,
|
map: Cow::Borrowed(map),
|
||||||
mods: 0,
|
mods: 0,
|
||||||
passed_objects: None,
|
passed_objects: None,
|
||||||
clock_rate: None,
|
clock_rate: None,
|
||||||
@@ -189,12 +191,9 @@ pub(crate) struct DifficultyHitObject<'o> {
|
|||||||
impl<'o> DifficultyHitObject<'o> {
|
impl<'o> DifficultyHitObject<'o> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn new(base: &'o HitObject, prev: &'o HitObject, columns: f32, clock_rate: f64) -> Self {
|
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 {
|
Self {
|
||||||
base,
|
base,
|
||||||
column,
|
column: base.column(columns) as usize,
|
||||||
delta: (base.start_time - prev.start_time) / clock_rate,
|
delta: (base.start_time - prev.start_time) / clock_rate,
|
||||||
start_time: base.start_time / clock_rate,
|
start_time: base.start_time / clock_rate,
|
||||||
}
|
}
|
||||||
@@ -236,7 +235,27 @@ impl ManiaPerformanceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes {
|
impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes {
|
||||||
|
#[inline]
|
||||||
fn from(attributes: ManiaPerformanceAttributes) -> Self {
|
fn from(attributes: ManiaPerformanceAttributes) -> Self {
|
||||||
attributes.difficulty
|
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 super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaStars};
|
||||||
use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
use crate::{Beatmap, DifficultyAttributes, GameMode, Mods, OsuPP, PerformanceAttributes};
|
||||||
|
|
||||||
/// Performance calculator on osu!mania maps.
|
/// Performance calculator on osu!mania maps.
|
||||||
///
|
///
|
||||||
@@ -31,7 +33,7 @@ use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub struct ManiaPP<'map> {
|
pub struct ManiaPP<'map> {
|
||||||
map: &'map Beatmap,
|
map: Cow<'map, Beatmap>,
|
||||||
stars: Option<f64>,
|
stars: Option<f64>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
pub(crate) score: Option<f64>,
|
pub(crate) score: Option<f64>,
|
||||||
@@ -44,7 +46,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(map: &'map Beatmap) -> Self {
|
pub fn new(map: &'map Beatmap) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map,
|
map: Cow::Borrowed(map),
|
||||||
stars: None,
|
stars: None,
|
||||||
mods: 0,
|
mods: 0,
|
||||||
score: None,
|
score: None,
|
||||||
@@ -112,7 +114,7 @@ impl<'map> ManiaPP<'map> {
|
|||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(self) -> ManiaPerformanceAttributes {
|
pub fn calculate(self) -> ManiaPerformanceAttributes {
|
||||||
let stars = self.stars.unwrap_or_else(|| {
|
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 {
|
if let Some(passed_objects) = self.passed_objects {
|
||||||
calculator = calculator.passed_objects(passed_objects);
|
calculator = calculator.passed_objects(passed_objects);
|
||||||
@@ -154,7 +156,19 @@ impl<'map> ManiaPP<'map> {
|
|||||||
od *= 1.4;
|
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 strain_value = self.compute_strain(scaled_score, stars);
|
||||||
let acc_value = self.compute_accuracy_value(scaled_score, strain_value, hit_window);
|
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.
|
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||||
pub trait ManiaAttributeProvider {
|
pub trait ManiaAttributeProvider {
|
||||||
/// Provide the star rating (only difficulty attribute for osu!mania).
|
/// Provide the star rating (only difficulty attribute for osu!mania).
|
||||||
|
|||||||
+17
-5
@@ -20,7 +20,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
use crate::{curve::CurveBuffers, AnyStars, Beatmap, GameMode, Mods, Strains};
|
||||||
|
|
||||||
use self::skill::Skills;
|
use self::skill::Skills;
|
||||||
|
|
||||||
@@ -49,10 +49,10 @@ const STACK_DISTANCE: f32 = 3.0;
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct OsuStars<'map> {
|
pub struct OsuStars<'map> {
|
||||||
map: &'map Beatmap,
|
pub(crate) map: &'map Beatmap,
|
||||||
mods: u32,
|
pub(crate) mods: u32,
|
||||||
passed_objects: Option<usize>,
|
pub(crate) passed_objects: Option<usize>,
|
||||||
clock_rate: Option<f64>,
|
pub(crate) clock_rate: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> OsuStars<'map> {
|
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.
|
/// Specify mods through their bit values.
|
||||||
///
|
///
|
||||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
/// 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 {
|
impl From<OsuPerformanceAttributes> for OsuDifficultyAttributes {
|
||||||
|
#[inline]
|
||||||
fn from(attributes: OsuPerformanceAttributes) -> Self {
|
fn from(attributes: OsuPerformanceAttributes) -> Self {
|
||||||
attributes.difficulty
|
attributes.difficulty
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ impl OsuObject {
|
|||||||
pixel_len,
|
pixel_len,
|
||||||
repeats,
|
repeats,
|
||||||
control_points,
|
control_points,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
attributes.n_sliders += 1;
|
attributes.n_sliders += 1;
|
||||||
|
|
||||||
|
|||||||
+19
-6
@@ -1,5 +1,7 @@
|
|||||||
use super::{OsuDifficultyAttributes, OsuPerformanceAttributes, OsuScoreState};
|
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.
|
/// Performance calculator on osu!standard maps.
|
||||||
///
|
///
|
||||||
@@ -33,10 +35,10 @@ use crate::{Beatmap, DifficultyAttributes, Mods, OsuStars, PerformanceAttributes
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub struct OsuPP<'map> {
|
pub struct OsuPP<'map> {
|
||||||
map: &'map Beatmap,
|
pub(crate) map: &'map Beatmap,
|
||||||
attributes: Option<OsuDifficultyAttributes>,
|
pub(crate) attributes: Option<OsuDifficultyAttributes>,
|
||||||
mods: u32,
|
pub(crate) mods: u32,
|
||||||
acc: Option<f64>,
|
pub(crate) acc: Option<f64>,
|
||||||
pub(crate) combo: Option<usize>,
|
pub(crate) combo: Option<usize>,
|
||||||
|
|
||||||
pub(crate) n300: Option<usize>,
|
pub(crate) n300: Option<usize>,
|
||||||
@@ -44,7 +46,7 @@ pub struct OsuPP<'map> {
|
|||||||
pub(crate) n50: Option<usize>,
|
pub(crate) n50: Option<usize>,
|
||||||
pub(crate) n_misses: usize,
|
pub(crate) n_misses: usize,
|
||||||
pub(crate) passed_objects: Option<usize>,
|
pub(crate) passed_objects: Option<usize>,
|
||||||
clock_rate: Option<f64>,
|
pub(crate) clock_rate: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> OsuPP<'map> {
|
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.
|
/// Provide the result of a previous difficulty or performance calculation.
|
||||||
/// If you already calculated the attributes for the current map-mod combination,
|
/// 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.
|
/// be sure to put them in here so that they don't have to be recalculated.
|
||||||
|
|||||||
@@ -55,24 +55,29 @@ mod test {
|
|||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 1.0,
|
time: 1.0,
|
||||||
beat_len: 10.0,
|
beat_len: 10.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 3.0,
|
time: 3.0,
|
||||||
beat_len: 20.0,
|
beat_len: 20.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
TimingPoint {
|
TimingPoint {
|
||||||
time: 4.0,
|
time: 4.0,
|
||||||
beat_len: 30.0,
|
beat_len: 30.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
difficulty_points: vec![
|
difficulty_points: vec![
|
||||||
DifficultyPoint {
|
DifficultyPoint {
|
||||||
time: 2.0,
|
time: 2.0,
|
||||||
speed_multiplier: 15.0,
|
speed_multiplier: 15.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
DifficultyPoint {
|
DifficultyPoint {
|
||||||
time: 5.0,
|
time: 5.0,
|
||||||
speed_multiplier: 45.0,
|
speed_multiplier: 45.0,
|
||||||
|
kiai: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ impl HitObject {
|
|||||||
pub fn is_spinner(&self) -> bool {
|
pub fn is_spinner(&self) -> bool {
|
||||||
matches!(self.kind, HitObjectKind::Spinner { .. })
|
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 {
|
impl PartialOrd for HitObject {
|
||||||
@@ -66,6 +74,9 @@ pub enum HitObjectKind {
|
|||||||
repeats: usize,
|
repeats: usize,
|
||||||
/// The control points of the slider.
|
/// The control points of the slider.
|
||||||
control_points: Vec<PathControlPoint>,
|
control_points: Vec<PathControlPoint>,
|
||||||
|
/// Sample sounds for the slider head, end, and repeat points.
|
||||||
|
/// Required for converts.
|
||||||
|
edge_sounds: Vec<u8>,
|
||||||
},
|
},
|
||||||
/// A spinner object.
|
/// A spinner object.
|
||||||
Spinner {
|
Spinner {
|
||||||
|
|||||||
+170
-159
@@ -12,9 +12,9 @@ pub use pos2::Pos2;
|
|||||||
pub use slider_parsing::*;
|
pub use slider_parsing::*;
|
||||||
|
|
||||||
use reader::FileReader;
|
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")))]
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
use std::{fs::File, io::Read};
|
use std::{fs::File, io::Read};
|
||||||
@@ -28,7 +28,7 @@ use std::path::Path;
|
|||||||
#[cfg(feature = "async_std")]
|
#[cfg(feature = "async_std")]
|
||||||
use async_std::{fs::File, io::Read as AsyncRead, path::Path};
|
use async_std::{fs::File, io::Read as AsyncRead, path::Path};
|
||||||
|
|
||||||
use crate::beatmap::{Beatmap, DifficultyPoint, GameMode, TimingPoint};
|
use crate::beatmap::{Beatmap, Break, DifficultyPoint, GameMode, TimingPoint};
|
||||||
|
|
||||||
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
||||||
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
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 {
|
macro_rules! parse_difficulty_body {
|
||||||
($self:ident, $reader:ident, $section:ident) => {{
|
($self:ident, $reader:ident, $section:ident) => {{
|
||||||
let mut ar = None;
|
let mut ar = None;
|
||||||
@@ -187,26 +165,34 @@ macro_rules! parse_difficulty_body {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_difficulty {
|
macro_rules! parse_events_body {
|
||||||
() => {
|
($self:ident, $reader:ident, $section:ident) => {{
|
||||||
fn parse_difficulty<R: Read>(
|
let mut empty = true;
|
||||||
&mut self,
|
|
||||||
reader: &mut FileReader<R>,
|
|
||||||
section: &mut Section,
|
|
||||||
) -> ParseResult<bool> {
|
|
||||||
parse_difficulty_body!(self, reader, section)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(async) => {
|
while next_line!($reader)? != 0 {
|
||||||
async fn parse_difficulty<R: AsyncRead + Unpin>(
|
if let Some(bytes) = $reader.get_section() {
|
||||||
&mut self,
|
*$section = Section::from_bytes(bytes);
|
||||||
reader: &mut FileReader<R>,
|
empty = false;
|
||||||
section: &mut Section,
|
break;
|
||||||
) -> ParseResult<bool> {
|
|
||||||
parse_difficulty_body!(self, reader, section)
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
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 {
|
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 beat_len: f64 = split.next().next_field("beat len")?.trim().parse()?;
|
||||||
let timing_change = split.nth(4).and_then(|value| value.bytes().next());
|
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) {
|
if matches!(timing_change, Some(b'1') | None) {
|
||||||
let beat_len = beat_len.clamp(6.0, 60_000.0);
|
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 {
|
if time < prev_time {
|
||||||
unsorted_timings = true;
|
unsorted_timings = true;
|
||||||
@@ -258,6 +254,7 @@ macro_rules! parse_timingpoints_body {
|
|||||||
let point = DifficultyPoint {
|
let point = DifficultyPoint {
|
||||||
time,
|
time,
|
||||||
speed_multiplier,
|
speed_multiplier,
|
||||||
|
kiai,
|
||||||
};
|
};
|
||||||
|
|
||||||
$self.difficulty_points.push(point);
|
$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 {
|
macro_rules! parse_hitobjects_body {
|
||||||
($self:ident, $reader:ident, $section:ident) => {{
|
($self:ident, $reader:ident, $section:ident) => {{
|
||||||
let mut unsorted = false;
|
let mut unsorted = false;
|
||||||
@@ -430,10 +405,25 @@ macro_rules! parse_hitobjects_body {
|
|||||||
.max(0.0)
|
.max(0.0)
|
||||||
.min(MAX_COORDINATE_VALUE);
|
.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 {
|
HitObjectKind::Slider {
|
||||||
repeats,
|
repeats,
|
||||||
pixel_len,
|
pixel_len,
|
||||||
control_points,
|
control_points,
|
||||||
|
edge_sounds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if kind & Self::SPINNER_FLAG > 0 {
|
} 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 {
|
macro_rules! parse_body {
|
||||||
($input:ident) => {{
|
($input:ident) => {{
|
||||||
let mut reader = FileReader::new($input);
|
let mut reader = FileReader::new($input);
|
||||||
@@ -526,6 +494,7 @@ macro_rules! parse_body {
|
|||||||
match section {
|
match section {
|
||||||
Section::General => section!(map, parse_general, reader, section),
|
Section::General => section!(map, parse_general, reader, section),
|
||||||
Section::Difficulty => section!(map, parse_difficulty, 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::TimingPoints => section!(map, parse_timingpoints, reader, section),
|
||||||
Section::HitObjects => section!(map, parse_hitobjects, reader, section),
|
Section::HitObjects => section!(map, parse_hitobjects, reader, section),
|
||||||
Section::None => {
|
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 {
|
impl Beatmap {
|
||||||
const CIRCLE_FLAG: u8 = 1 << 0;
|
const CIRCLE_FLAG: u8 = 1 << 0;
|
||||||
const SLIDER_FLAG: u8 = 1 << 1;
|
const SLIDER_FLAG: u8 = 1 << 1;
|
||||||
@@ -751,35 +673,122 @@ mod slider_parsing {
|
|||||||
|
|
||||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
impl Beatmap {
|
impl Beatmap {
|
||||||
parse!();
|
/// Parse a beatmap from a `.osu` file.
|
||||||
parse_general!();
|
///
|
||||||
parse_difficulty!();
|
/// As argument you can give anything that implements [`std::io::Read`].
|
||||||
parse_timingpoints!();
|
/// You'll likely want to pass (a reference of) a [`File`](std::fs::File)
|
||||||
parse_hitobjects!();
|
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||||
|
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
|
||||||
from_path!();
|
parse_body!(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async_tokio")]
|
fn parse_general<R: Read>(
|
||||||
impl Beatmap {
|
&mut self,
|
||||||
parse!(async);
|
reader: &mut FileReader<R>,
|
||||||
parse_general!(async);
|
section: &mut Section,
|
||||||
parse_difficulty!(async);
|
) -> ParseResult<bool> {
|
||||||
parse_timingpoints!(async);
|
parse_general_body!(self, reader, section)
|
||||||
parse_hitobjects!(async);
|
|
||||||
|
|
||||||
from_path!(async);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async_std")]
|
fn parse_difficulty<R: Read>(
|
||||||
impl Beatmap {
|
&mut self,
|
||||||
parse!(async);
|
reader: &mut FileReader<R>,
|
||||||
parse_general!(async);
|
section: &mut Section,
|
||||||
parse_difficulty!(async);
|
) -> ParseResult<bool> {
|
||||||
parse_timingpoints!(async);
|
parse_difficulty_body!(self, reader, section)
|
||||||
parse_hitobjects!(async);
|
}
|
||||||
|
|
||||||
from_path!(async);
|
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(any(feature = "async_tokio", feature = "async_std"))]
|
||||||
|
impl Beatmap {
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_general<R: AsyncRead + Unpin>(
|
||||||
|
&mut self,
|
||||||
|
reader: &mut FileReader<R>,
|
||||||
|
section: &mut Section,
|
||||||
|
) -> ParseResult<bool> {
|
||||||
|
parse_general_body!(self, reader, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_difficulty<R: AsyncRead + Unpin>(
|
||||||
|
&mut self,
|
||||||
|
reader: &mut FileReader<R>,
|
||||||
|
section: &mut Section,
|
||||||
|
) -> ParseResult<bool> {
|
||||||
|
parse_difficulty_body!(self, reader, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
@@ -789,6 +798,7 @@ enum Section {
|
|||||||
Difficulty,
|
Difficulty,
|
||||||
TimingPoints,
|
TimingPoints,
|
||||||
HitObjects,
|
HitObjects,
|
||||||
|
Events,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Section {
|
impl Section {
|
||||||
@@ -798,6 +808,7 @@ impl Section {
|
|||||||
b"Difficulty" => Self::Difficulty,
|
b"Difficulty" => Self::Difficulty,
|
||||||
b"TimingPoints" => Self::TimingPoints,
|
b"TimingPoints" => Self::TimingPoints,
|
||||||
b"HitObjects" => Self::HitObjects,
|
b"HitObjects" => Self::HitObjects,
|
||||||
|
b"Events" => Self::Events,
|
||||||
_ => Self::None,
|
_ => Self::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-4
@@ -43,6 +43,7 @@ macro_rules! read_until {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused_macro_rules)]
|
||||||
macro_rules! impl_reader {
|
macro_rules! impl_reader {
|
||||||
() => {
|
() => {
|
||||||
impl<R: Read> FileReader<R> {
|
impl<R: Read> FileReader<R> {
|
||||||
@@ -96,10 +97,7 @@ macro_rules! impl_reader {
|
|||||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
impl_reader!();
|
impl_reader!();
|
||||||
|
|
||||||
#[cfg(feature = "async_tokio")]
|
#[cfg(any(feature = "async_tokio", feature = "async_std"))]
|
||||||
impl_reader!(async);
|
|
||||||
|
|
||||||
#[cfg(feature = "async_std")]
|
|
||||||
impl_reader!(async);
|
impl_reader!(async);
|
||||||
|
|
||||||
impl<R> FileReader<R> {
|
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.
|
/// Specify mods through their bit values.
|
||||||
///
|
///
|
||||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
/// 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.
|
/// Specify mods through their bit values.
|
||||||
///
|
///
|
||||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
/// 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_difficulty;
|
||||||
mod gradual_performance;
|
mod gradual_performance;
|
||||||
mod hitobject_rhythm;
|
mod hitobject_rhythm;
|
||||||
mod limited_queue;
|
|
||||||
mod pp;
|
mod pp;
|
||||||
mod rim;
|
mod rim;
|
||||||
mod skill;
|
mod skill;
|
||||||
@@ -14,7 +13,6 @@ use difficulty_object::DifficultyObject;
|
|||||||
pub use gradual_difficulty::*;
|
pub use gradual_difficulty::*;
|
||||||
pub use gradual_performance::*;
|
pub use gradual_performance::*;
|
||||||
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
||||||
use limited_queue::LimitedQueue;
|
|
||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use rim::Rim;
|
use rim::Rim;
|
||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
@@ -22,8 +20,9 @@ use stamina_cheese::StaminaCheeseDetector;
|
|||||||
use taiko_object::IntoTaikoObjectIter;
|
use taiko_object::IntoTaikoObjectIter;
|
||||||
|
|
||||||
use crate::taiko::skill::Skills;
|
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::cmp::Ordering;
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
@@ -53,7 +52,7 @@ const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TaikoStars<'map> {
|
pub struct TaikoStars<'map> {
|
||||||
map: &'map Beatmap,
|
map: Cow<'map, Beatmap>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
clock_rate: Option<f64>,
|
clock_rate: Option<f64>,
|
||||||
@@ -64,7 +63,7 @@ impl<'map> TaikoStars<'map> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(map: &'map Beatmap) -> Self {
|
pub fn new(map: &'map Beatmap) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map,
|
map: Cow::Borrowed(map),
|
||||||
mods: 0,
|
mods: 0,
|
||||||
passed_objects: None,
|
passed_objects: None,
|
||||||
clock_rate: None,
|
clock_rate: None,
|
||||||
@@ -331,7 +330,27 @@ impl TaikoPerformanceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<TaikoPerformanceAttributes> for TaikoDifficultyAttributes {
|
impl From<TaikoPerformanceAttributes> for TaikoDifficultyAttributes {
|
||||||
|
#[inline]
|
||||||
fn from(attributes: TaikoPerformanceAttributes) -> Self {
|
fn from(attributes: TaikoPerformanceAttributes) -> Self {
|
||||||
attributes.difficulty
|
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 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.
|
/// Performance calculator on osu!taiko maps.
|
||||||
///
|
///
|
||||||
@@ -33,7 +35,7 @@ use crate::{Beatmap, DifficultyAttributes, Mods, PerformanceAttributes};
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub struct TaikoPP<'map> {
|
pub struct TaikoPP<'map> {
|
||||||
map: &'map Beatmap,
|
map: Cow<'map, Beatmap>,
|
||||||
attributes: Option<TaikoDifficultyAttributes>,
|
attributes: Option<TaikoDifficultyAttributes>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
combo: Option<usize>,
|
combo: Option<usize>,
|
||||||
@@ -51,7 +53,7 @@ impl<'map> TaikoPP<'map> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(map: &'map Beatmap) -> Self {
|
pub fn new(map: &'map Beatmap) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map,
|
map: Cow::Borrowed(map),
|
||||||
attributes: None,
|
attributes: None,
|
||||||
mods: 0,
|
mods: 0,
|
||||||
combo: None,
|
combo: None,
|
||||||
@@ -171,7 +173,7 @@ impl<'map> TaikoPP<'map> {
|
|||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
||||||
let attributes = self.attributes.take().unwrap_or_else(|| {
|
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 {
|
if let Some(passed_objects) = self.passed_objects {
|
||||||
calculator = calculator.passed_objects(passed_objects);
|
calculator = calculator.passed_objects(passed_objects);
|
||||||
@@ -205,7 +207,7 @@ impl<'map> TaikoPP<'map> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let inner = TaikoPPInner {
|
let inner = TaikoPPInner {
|
||||||
map: self.map,
|
map: self.map.as_ref(),
|
||||||
attributes,
|
attributes,
|
||||||
mods: self.mods,
|
mods: self.mods,
|
||||||
acc: self.acc,
|
acc: self.acc,
|
||||||
@@ -302,6 +304,37 @@ fn difficulty_range_od(od: f64) -> f64 {
|
|||||||
crate::difficulty_range(od, 20.0, 35.0, 50.0)
|
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.
|
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||||
pub trait TaikoAttributeProvider {
|
pub trait TaikoAttributeProvider {
|
||||||
/// Provide the actual difficulty attributes.
|
/// 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;
|
use std::ops::Index;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::{LimitedQueue, Rim};
|
use super::Rim;
|
||||||
use crate::Beatmap;
|
use crate::{limited_queue::LimitedQueue, Beatmap};
|
||||||
|
|
||||||
const ROLL_MIN_REPETITIONS: usize = 12;
|
const ROLL_MIN_REPETITIONS: usize = 12;
|
||||||
const TL_MIN_REPETITIONS: isize = 16;
|
const TL_MIN_REPETITIONS: isize = 16;
|
||||||
|
|||||||
Reference in New Issue
Block a user