initial rewrite
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
use crate::mania::performance::ManiaPerformance;
|
||||
|
||||
/// The result of a difficulty calculation on an osu!mania map.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ManiaDifficultyAttributes {
|
||||
/// The final star rating.
|
||||
pub stars: f64,
|
||||
/// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
pub hit_window: f64,
|
||||
/// The amount of hitobjects in the map.
|
||||
pub n_objects: u32,
|
||||
/// The maximum achievable combo.
|
||||
pub max_combo: u32,
|
||||
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
|
||||
///
|
||||
/// [`Beatmap`]: crate::model::beatmap::Beatmap
|
||||
pub is_convert: bool,
|
||||
}
|
||||
|
||||
impl ManiaDifficultyAttributes {
|
||||
/// Return the maximum combo.
|
||||
pub const fn max_combo(&self) -> u32 {
|
||||
self.max_combo
|
||||
}
|
||||
|
||||
/// Return the amount of hitobjects.
|
||||
pub const fn n_objects(&self) -> u32 {
|
||||
self.n_objects
|
||||
}
|
||||
|
||||
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
|
||||
///
|
||||
/// [`Beatmap`]: crate::model::beatmap::Beatmap
|
||||
pub const fn is_convert(&self) -> bool {
|
||||
self.is_convert
|
||||
}
|
||||
|
||||
/// Returns a builder for performance calculation.
|
||||
pub fn pp<'a>(self) -> ManiaPerformance<'a> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a performance calculation on an osu!mania map.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ManiaPerformanceAttributes {
|
||||
/// The difficulty attributes that were used for the performance calculation.
|
||||
pub difficulty: ManiaDifficultyAttributes,
|
||||
/// The final performance points.
|
||||
pub pp: f64,
|
||||
/// The difficulty portion of the final pp.
|
||||
pub pp_difficulty: f64,
|
||||
}
|
||||
|
||||
impl ManiaPerformanceAttributes {
|
||||
/// Return the star value.
|
||||
pub const fn stars(&self) -> f64 {
|
||||
self.difficulty.stars
|
||||
}
|
||||
|
||||
/// Return the performance point value.
|
||||
pub const fn pp(&self) -> f64 {
|
||||
self.pp
|
||||
}
|
||||
|
||||
/// Return the maximum combo of the map.
|
||||
pub const fn max_combo(&self) -> u32 {
|
||||
self.difficulty.max_combo
|
||||
}
|
||||
|
||||
/// Return the amount of hitobjects.
|
||||
pub const fn n_objects(&self) -> u32 {
|
||||
self.difficulty.n_objects
|
||||
}
|
||||
|
||||
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
|
||||
///
|
||||
/// [`Beatmap`]: crate::model::beatmap::Beatmap
|
||||
pub const fn is_convert(&self) -> bool {
|
||||
self.difficulty.is_convert
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rosu_map::{
|
||||
section::{
|
||||
general::GameMode,
|
||||
hit_objects::{BorrowedCurve, CurveBuffers},
|
||||
},
|
||||
util::Pos,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
beatmap::{Beatmap, Converted},
|
||||
hit_object::{HitObjectKind, HoldNote, Spinner},
|
||||
mode::ConvertStatus,
|
||||
},
|
||||
util::{
|
||||
float_ext::FloatExt, legacy_sort::legacy_sort, limited_queue::LimitedQueue, random::Random,
|
||||
},
|
||||
};
|
||||
|
||||
use self::{
|
||||
pattern::Pattern,
|
||||
pattern_generator::{
|
||||
distance_object::DistanceObjectPatternGenerator,
|
||||
end_time_object::EndTimeObjectPatternGenerator, hit_object::HitObjectPatternGenerator,
|
||||
},
|
||||
pattern_type::PatternType,
|
||||
};
|
||||
|
||||
use super::Mania;
|
||||
|
||||
mod pattern;
|
||||
mod pattern_generator;
|
||||
mod pattern_type;
|
||||
|
||||
/// A [`Beatmap`] for [`Mania`] calculations.
|
||||
pub type ManiaBeatmap<'a> = Converted<'a, Mania>;
|
||||
|
||||
const MAX_NOTES_FOR_DENSITY: usize = 7;
|
||||
|
||||
pub fn try_convert(map: &mut Cow<'_, Beatmap>) -> ConvertStatus {
|
||||
match map.mode {
|
||||
GameMode::Osu => {
|
||||
convert(map.to_mut());
|
||||
|
||||
ConvertStatus::Done
|
||||
}
|
||||
GameMode::Mania => ConvertStatus::Noop,
|
||||
GameMode::Taiko | GameMode::Catch => ConvertStatus::Incompatible,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert(map: &mut Beatmap) {
|
||||
let seed = (map.hp + map.cs).round_even() as i32 * 20
|
||||
+ (map.od * 41.2) as i32
|
||||
+ map.ar.round_even() as i32;
|
||||
|
||||
let mut random = Random::new(seed);
|
||||
|
||||
map.cs = target_columns(map);
|
||||
|
||||
let mut prev_note_times = LimitedQueue::<f64, MAX_NOTES_FOR_DENSITY>::new();
|
||||
let mut density = f64::from(i32::MAX);
|
||||
|
||||
let mut compute_density = |new_note_time: f64, d: &mut f64| {
|
||||
prev_note_times.push(new_note_time);
|
||||
|
||||
if let ([first, ..], [.., last]) | ([], [first, .., last]) | ([first, .., last], []) =
|
||||
prev_note_times.as_slices()
|
||||
{
|
||||
*d = (last - first) / 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();
|
||||
|
||||
// TODO: find proper default
|
||||
let capacity = map.hit_objects.len() * 2;
|
||||
let mut new_hit_objects = Vec::with_capacity(capacity);
|
||||
|
||||
for (obj, sound) in map.hit_objects.iter().zip(map.hit_sounds.iter().copied()) {
|
||||
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,
|
||||
map,
|
||||
);
|
||||
|
||||
let new_pattern = gen.generate();
|
||||
|
||||
last_values.stair = gen.stair_type;
|
||||
last_values.time = obj.start_time;
|
||||
last_values.pos = obj.pos;
|
||||
|
||||
let new_hit_objects_iter = new_pattern.hit_objects.iter().cloned();
|
||||
new_hit_objects.extend(new_hit_objects_iter);
|
||||
|
||||
last_values.pattern = new_pattern;
|
||||
}
|
||||
HitObjectKind::Slider(ref slider) => {
|
||||
let curve = BorrowedCurve::new(
|
||||
&slider.control_points,
|
||||
slider.expected_dist,
|
||||
&mut curve_bufs,
|
||||
);
|
||||
|
||||
let mut gen = DistanceObjectPatternGenerator::new(
|
||||
&mut random,
|
||||
obj,
|
||||
sound,
|
||||
total_columns,
|
||||
&last_values.pattern,
|
||||
map,
|
||||
slider.repeats,
|
||||
&curve,
|
||||
&slider.node_sounds,
|
||||
);
|
||||
|
||||
let segment_duration = f64::from(gen.segment_duration);
|
||||
|
||||
for i in 0..=slider.repeats as i32 + 1 {
|
||||
let time = obj.start_time + segment_duration * f64::from(i);
|
||||
|
||||
last_values.time = time;
|
||||
last_values.pos = obj.pos;
|
||||
|
||||
compute_density(time, &mut density);
|
||||
}
|
||||
|
||||
for new_pattern in gen.generate() {
|
||||
new_hit_objects.extend_from_slice(&new_pattern.hit_objects);
|
||||
last_values.pattern = new_pattern;
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner(Spinner { end_time })
|
||||
| HitObjectKind::Hold(HoldNote { end_time }) => {
|
||||
let mut gen = EndTimeObjectPatternGenerator::new(
|
||||
&mut random,
|
||||
obj,
|
||||
end_time,
|
||||
sound,
|
||||
total_columns,
|
||||
&last_values.pattern,
|
||||
map,
|
||||
);
|
||||
|
||||
last_values.time = end_time;
|
||||
last_values.pos = Pos::new(256.0, 192.0);
|
||||
|
||||
compute_density(end_time, &mut density);
|
||||
|
||||
let new_pattern = gen.generate();
|
||||
new_hit_objects.extend(new_pattern.hit_objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map.hit_sounds.clear();
|
||||
map.hit_objects = new_hit_objects;
|
||||
map.hit_objects
|
||||
.sort_by(|a, b| a.start_time.total_cmp(&b.start_time));
|
||||
legacy_sort(&mut map.hit_objects);
|
||||
|
||||
map.mode = GameMode::Mania;
|
||||
}
|
||||
|
||||
pub struct PrevValues {
|
||||
time: f64,
|
||||
pos: Pos,
|
||||
pattern: Pattern,
|
||||
stair: PatternType,
|
||||
}
|
||||
|
||||
impl Default for PrevValues {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: 0.0,
|
||||
pos: Pos::default(),
|
||||
pattern: Pattern::default(),
|
||||
stair: PatternType::STAIR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn target_columns(map: &Beatmap) -> f32 {
|
||||
let rounded_cs = map.cs.round_even();
|
||||
let rounded_od = map.od.round_even();
|
||||
|
||||
let slider_or_spinner_count = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter(|h| matches!(h.kind, HitObjectKind::Slider(_) | HitObjectKind::Spinner(_)))
|
||||
.count();
|
||||
|
||||
let len = map.hit_objects.len();
|
||||
let percent_slider_or_spinner = f64::from(slider_or_spinner_count as f32 / len as f32);
|
||||
|
||||
if percent_slider_or_spinner < 0.2 {
|
||||
7.0
|
||||
} else if percent_slider_or_spinner < 0.3 || rounded_cs >= 5.0 {
|
||||
f32::from(6 + u8::from(rounded_od > 5.0))
|
||||
} else if percent_slider_or_spinner > 0.6 {
|
||||
f32::from(4 + u8::from(rounded_od > 4.0))
|
||||
} else {
|
||||
(rounded_od + 1.0).clamp(4.0, 7.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn convert_mania() {
|
||||
let converted = Beatmap::from_path("./resources/2785319.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted::<Mania>();
|
||||
|
||||
assert!(converted.is_convert);
|
||||
|
||||
let map = converted.map;
|
||||
|
||||
assert_eq!(map.mode, GameMode::Mania);
|
||||
assert_eq!(map.version, 14);
|
||||
assert!(map.ar.eq(9.3), "{} != 9.3", map.ar);
|
||||
assert!(map.od.eq(8.8), "{} != 8.8", map.od);
|
||||
assert!(map.cs.eq(7.0), "{} != 7.0", map.cs);
|
||||
assert!(map.hp.eq(5.0), "{} != 5.0", map.hp);
|
||||
assert!(
|
||||
map.slider_multiplier.eq(1.7),
|
||||
"{} != 1.7",
|
||||
map.slider_multiplier
|
||||
);
|
||||
assert!(
|
||||
map.slider_tick_rate.eq(1.0),
|
||||
"{} != 1.0",
|
||||
map.slider_tick_rate
|
||||
);
|
||||
assert_eq!(map.hit_objects.len(), 1046);
|
||||
assert_eq!(map.hit_sounds.len(), 0);
|
||||
assert_eq!(map.timing_points.len(), 1);
|
||||
assert_eq!(map.difficulty_points.len(), 50);
|
||||
assert_eq!(map.effect_points.len(), 0);
|
||||
assert!(map.stack_leniency.eq(0.5), "{} != 0.5", map.stack_leniency);
|
||||
assert_eq!(map.breaks.len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
use rosu_map::util::Pos;
|
||||
|
||||
use crate::model::hit_object::{HitObject, HitObjectKind, HoldNote};
|
||||
|
||||
use super::pattern_generator::{
|
||||
distance_object::DistanceObjectPatternGenerator,
|
||||
end_time_object::EndTimeObjectPatternGenerator, hit_object::HitObjectPatternGenerator,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Pattern {
|
||||
pub hit_objects: Vec<HitObject>,
|
||||
contained_columns: ContainedColumns,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
hit_objects: Vec::with_capacity(capacity),
|
||||
contained_columns: ContainedColumns::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_single(hit_object: HitObject, column: u8) -> Self {
|
||||
let mut contained_columns = ContainedColumns::default();
|
||||
contained_columns.insert(column);
|
||||
let hit_objects = vec![hit_object];
|
||||
|
||||
Self {
|
||||
hit_objects,
|
||||
contained_columns,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_note(generator: &HitObjectPatternGenerator<'_>, column: u8) -> Self {
|
||||
let pos = column_to_pos(column, generator.inner.total_columns);
|
||||
|
||||
let hit_object = HitObject {
|
||||
pos: Pos::new(pos, pos),
|
||||
start_time: generator.inner.hit_object.start_time,
|
||||
kind: HitObjectKind::Circle,
|
||||
};
|
||||
|
||||
Self::new_single(hit_object, column)
|
||||
}
|
||||
|
||||
pub fn add_note(&mut self, generator: &HitObjectPatternGenerator<'_>, column: u8) {
|
||||
let pos = column_to_pos(column, generator.inner.total_columns);
|
||||
|
||||
let hit_object = HitObject {
|
||||
pos: Pos::new(pos, pos),
|
||||
start_time: generator.inner.hit_object.start_time,
|
||||
kind: HitObjectKind::Circle,
|
||||
};
|
||||
|
||||
self.contained_columns.insert(column);
|
||||
self.hit_objects.push(hit_object);
|
||||
}
|
||||
|
||||
pub fn new_end_time_note(
|
||||
generator: &EndTimeObjectPatternGenerator<'_>,
|
||||
column: u8,
|
||||
hold_note: bool,
|
||||
) -> Self {
|
||||
let pos = column_to_pos(column, generator.inner.total_columns);
|
||||
let pos = Pos::new(pos, pos);
|
||||
|
||||
let hit_object = if hold_note {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: generator.inner.hit_object.start_time,
|
||||
kind: HitObjectKind::Hold(HoldNote {
|
||||
end_time: generator.end_time,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: generator.inner.hit_object.start_time,
|
||||
kind: HitObjectKind::Circle,
|
||||
}
|
||||
};
|
||||
|
||||
Self::new_single(hit_object, column)
|
||||
}
|
||||
|
||||
pub fn new_slider_note(
|
||||
generator: &DistanceObjectPatternGenerator<'_>,
|
||||
column: u8,
|
||||
start_time: i32,
|
||||
end_time: i32,
|
||||
) -> Self {
|
||||
let pos = column_to_pos(column, generator.inner.total_columns);
|
||||
let pos = Pos::new(pos, pos);
|
||||
|
||||
let hit_object = if start_time == end_time {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: f64::from(start_time),
|
||||
kind: HitObjectKind::Circle,
|
||||
}
|
||||
} else {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: f64::from(start_time),
|
||||
kind: HitObjectKind::Hold(HoldNote {
|
||||
end_time: f64::from(end_time),
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
Self::new_single(hit_object, column)
|
||||
}
|
||||
|
||||
pub fn add_slider_note(
|
||||
&mut self,
|
||||
generator: &DistanceObjectPatternGenerator<'_>,
|
||||
column: u8,
|
||||
start_time: i32,
|
||||
end_time: i32,
|
||||
) {
|
||||
let pos = column_to_pos(column, generator.inner.total_columns);
|
||||
let pos = Pos::new(pos, pos);
|
||||
|
||||
let hit_object = if start_time == end_time {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: f64::from(start_time),
|
||||
kind: HitObjectKind::Circle,
|
||||
}
|
||||
} else {
|
||||
HitObject {
|
||||
pos,
|
||||
start_time: f64::from(start_time),
|
||||
kind: HitObjectKind::Hold(HoldNote {
|
||||
end_time: f64::from(end_time),
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
self.contained_columns.insert(column);
|
||||
self.hit_objects.push(hit_object);
|
||||
}
|
||||
|
||||
pub fn add_object(&mut self, obj: HitObject, column: u8) {
|
||||
self.hit_objects.push(obj);
|
||||
self.contained_columns.insert(column);
|
||||
}
|
||||
|
||||
pub const fn column_has_obj(&self, column: u8) -> bool {
|
||||
self.contained_columns.contains(column)
|
||||
}
|
||||
|
||||
pub const 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 fn append(&mut self, other: &mut Self) {
|
||||
self.hit_objects.append(&mut other.hit_objects);
|
||||
self.contained_columns.extend(other.contained_columns);
|
||||
}
|
||||
}
|
||||
|
||||
fn column_to_pos(column: u8, total_columns: i32) -> f32 {
|
||||
let divisor = 512.0 / total_columns as f32;
|
||||
|
||||
(f32::from(column) * divisor).ceil()
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct ContainedColumns(u16);
|
||||
|
||||
impl ContainedColumns {
|
||||
pub fn insert(&mut self, column: u8) {
|
||||
self.0 |= 1 << column;
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: Self) {
|
||||
self.0 |= other.0;
|
||||
}
|
||||
|
||||
pub const fn len(self) -> u32 {
|
||||
self.0.count_ones()
|
||||
}
|
||||
|
||||
pub const fn contains(self, column: u8) -> bool {
|
||||
self.0 & (1 << column) != 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
use rosu_map::section::hit_objects::{hit_samples::HitSoundType, BorrowedCurve};
|
||||
|
||||
use crate::{
|
||||
mania::{
|
||||
convert::{pattern::Pattern, pattern_type::PatternType},
|
||||
object::ManiaObject,
|
||||
},
|
||||
model::{
|
||||
beatmap::{get_precision_adjusted_beat_len_taiko_mania, Beatmap},
|
||||
control_point::{EffectPoint, TimingPoint},
|
||||
hit_object::HitObject,
|
||||
},
|
||||
util::{float_ext::FloatExt, random::Random},
|
||||
};
|
||||
|
||||
use super::PatternGenerator;
|
||||
|
||||
pub struct DistanceObjectPatternGenerator<'h> {
|
||||
pub segment_duration: i32,
|
||||
pub sample: HitSoundType,
|
||||
pub inner: PatternGenerator<'h>,
|
||||
start_time: i32,
|
||||
end_time: i32,
|
||||
span_count: i32,
|
||||
prev_pattern: &'h Pattern,
|
||||
convert_type: PatternType,
|
||||
node_sounds: &'h [HitSoundType],
|
||||
}
|
||||
|
||||
impl<'h> DistanceObjectPatternGenerator<'h> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
random: &'h mut Random,
|
||||
hit_object: &'h HitObject,
|
||||
sample: HitSoundType,
|
||||
total_columns: i32,
|
||||
prev_pattern: &'h Pattern,
|
||||
orig: &'h Beatmap,
|
||||
repeats: usize,
|
||||
curve: &BorrowedCurve<'_>,
|
||||
node_sounds: &'h [HitSoundType],
|
||||
) -> Self {
|
||||
let beat_len = orig
|
||||
.timing_point_at(hit_object.start_time)
|
||||
.map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
|
||||
|
||||
let kiai = orig
|
||||
.effect_point_at(hit_object.start_time)
|
||||
.map_or(EffectPoint::DEFAULT_KIAI, |point| point.kiai);
|
||||
|
||||
let convert_type = if kiai {
|
||||
PatternType::default()
|
||||
} else {
|
||||
PatternType::LOW_PROBABILITY
|
||||
};
|
||||
|
||||
let beat_len =
|
||||
get_precision_adjusted_beat_len_taiko_mania(orig, beat_len, hit_object.start_time);
|
||||
|
||||
let span_count = (repeats + 1) as i32;
|
||||
let start_time = hit_object.start_time.round_even() as i32;
|
||||
|
||||
// * This matches stable's calculation.
|
||||
let end_time = (f64::from(start_time)
|
||||
+ curve.dist() * beat_len * f64::from(span_count) * 0.01 / orig.slider_multiplier)
|
||||
.floor() as i32;
|
||||
|
||||
let segment_duration = (end_time - start_time) / span_count;
|
||||
|
||||
let inner = PatternGenerator::new(hit_object, total_columns, random, orig);
|
||||
|
||||
Self {
|
||||
segment_duration,
|
||||
sample,
|
||||
inner,
|
||||
start_time,
|
||||
end_time,
|
||||
span_count,
|
||||
prev_pattern,
|
||||
convert_type,
|
||||
node_sounds,
|
||||
}
|
||||
}
|
||||
|
||||
pub 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 col = ManiaObject::column(obj.pos.x, self.inner.total_columns as f32) as u8;
|
||||
|
||||
// Keeping it in-sync with lazer
|
||||
#[allow(clippy::if_not_else)]
|
||||
if self.end_time != obj.end_time().round_even() as i32 {
|
||||
intermediate_pattern.add_object(obj, col);
|
||||
} else {
|
||||
end_time_pattern.add_object(obj, col);
|
||||
}
|
||||
}
|
||||
|
||||
vec![intermediate_pattern, end_time_pattern]
|
||||
}
|
||||
|
||||
fn generate_(&mut self) -> Pattern {
|
||||
let conversion_diff = self.inner.conversion_difficulty();
|
||||
|
||||
if self.inner.total_columns == 1 {
|
||||
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.inner.total_columns - 1 - self.inner.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.inner.total_columns {
|
||||
self.convert_type |= PatternType::FORCE_NOT_STACK;
|
||||
} else {
|
||||
self.convert_type &= !PatternType::FORCE_NOT_STACK;
|
||||
}
|
||||
|
||||
let note_count = 1 + i32::from(self.segment_duration >= 80);
|
||||
|
||||
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.inner.random_start();
|
||||
let usable_columns =
|
||||
self.inner.total_columns - random_start - self.prev_pattern.column_with_objs();
|
||||
let mut next_column = self.inner.get_random_column(None, None);
|
||||
|
||||
for _ in 0..usable_columns.min(note_count) {
|
||||
// * Find available column
|
||||
next_column =
|
||||
self.find_available_column(next_column, 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, &[&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.inner.get_column(Some(true));
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK)
|
||||
&& self.prev_pattern.column_with_objs() < self.inner.total_columns
|
||||
{
|
||||
next_column = self.find_available_column(next_column, 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,
|
||||
Some(&|c| c != i32::from(last_column)),
|
||||
&[],
|
||||
);
|
||||
|
||||
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 = i32::from(self.inner.get_column(Some(true)));
|
||||
let mut increasing = self.inner.random.next_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.inner.total_columns - 1 {
|
||||
increasing = false;
|
||||
column -= 1;
|
||||
} else {
|
||||
column += 1;
|
||||
}
|
||||
} else if column <= self.inner.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.inner.total_columns);
|
||||
let interval = self
|
||||
.inner
|
||||
.random
|
||||
.next_int_range(1, self.inner.total_columns - i32::from(legacy));
|
||||
|
||||
let mut next_column = i32::from(self.inner.get_column(Some(true)));
|
||||
let random_start = self.inner.random_start();
|
||||
let not_2k = self.inner.total_columns > 2;
|
||||
let mut pattern =
|
||||
Pattern::with_capacity((self.span_count as usize + 1) * (1 + usize::from(not_2k)));
|
||||
|
||||
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.inner.total_columns - random_start {
|
||||
next_column =
|
||||
next_column - self.inner.total_columns - random_start + i32::from(legacy);
|
||||
}
|
||||
|
||||
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 = i32::from(self.inner.get_random_column(None, None));
|
||||
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.inner.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: HitSoundType| sample.has_flag(HitSoundType::CLAP | HitSoundType::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
|
||||
.inner
|
||||
.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.inner.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.inner.get_column(Some(true));
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK)
|
||||
&& self.prev_pattern.column_with_objs() < self.inner.total_columns
|
||||
{
|
||||
next_column = self.find_available_column(next_column, 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, &[&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.inner.get_column(Some(true));
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK)
|
||||
&& self.prev_pattern.column_with_objs() < self.inner.total_columns
|
||||
{
|
||||
hold_column = self.find_available_column(hold_column, None, &[self.prev_pattern]);
|
||||
}
|
||||
|
||||
// * Create the hold note
|
||||
pattern.add_slider_note(self, hold_column, start_time, self.end_time);
|
||||
|
||||
let mut next_column = self.inner.get_random_column(None, None);
|
||||
|
||||
let mut note_count = if conversion_diff > 6.5 {
|
||||
self.inner
|
||||
.get_random_note_count(0.63, 0.0, None, None, None)
|
||||
} else if conversion_diff > 4.0 {
|
||||
let p2 = if self.inner.total_columns < 6 {
|
||||
0.12
|
||||
} else {
|
||||
0.45
|
||||
};
|
||||
|
||||
self.inner.get_random_note_count(p2, 0.0, None, None, None)
|
||||
} else if conversion_diff > 2.5 {
|
||||
let p2 = if self.inner.total_columns < 6 {
|
||||
0.0
|
||||
} else {
|
||||
0.24
|
||||
};
|
||||
|
||||
self.inner.get_random_note_count(p2, 0.0, None, None, None)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
note_count = note_count.min(self.inner.total_columns - 1);
|
||||
|
||||
let sample = self.sample_info_list_at(start_time);
|
||||
let ignore_head =
|
||||
!sample.has_flag(HitSoundType::WHISTLE | HitSoundType::FINISH | HitSoundType::CLAP);
|
||||
|
||||
let mut row_pattern = Pattern::default();
|
||||
let hold_column = i32::from(hold_column);
|
||||
|
||||
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,
|
||||
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) -> HitSoundType {
|
||||
self.note_samples_at(time)
|
||||
.first()
|
||||
.map_or(self.sample, |sample| *sample)
|
||||
}
|
||||
|
||||
fn note_samples_at(&self, time: i32) -> &[HitSoundType] {
|
||||
let idx = if self.segment_duration == 0 {
|
||||
0
|
||||
} else {
|
||||
((time - self.start_time) / self.segment_duration) as usize
|
||||
};
|
||||
|
||||
&self.node_sounds[idx..]
|
||||
}
|
||||
|
||||
fn find_available_column(
|
||||
&mut self,
|
||||
mut initial_column: u8,
|
||||
validation: Option<&dyn Fn(i32) -> bool>,
|
||||
patterns: &[&Pattern],
|
||||
) -> u8 {
|
||||
let lower = self.inner.random_start();
|
||||
let upper = self.inner.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(i32::from(initial_column)) {
|
||||
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 = self.inner.get_random_column(Some(lower), Some(upper));
|
||||
|
||||
!is_valid(i32::from(initial_column))
|
||||
} {}
|
||||
|
||||
initial_column
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use rosu_map::section::hit_objects::hit_samples::HitSoundType;
|
||||
|
||||
use crate::{
|
||||
mania::convert::{pattern::Pattern, pattern_type::PatternType},
|
||||
model::hit_object::HitObject,
|
||||
util::random::Random,
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::PatternGenerator;
|
||||
|
||||
pub struct EndTimeObjectPatternGenerator<'h> {
|
||||
pub end_time: f64,
|
||||
pub sample: HitSoundType,
|
||||
pub inner: PatternGenerator<'h>,
|
||||
convert_type: PatternType,
|
||||
prev_pattern: &'h Pattern,
|
||||
}
|
||||
|
||||
impl<'h> EndTimeObjectPatternGenerator<'h> {
|
||||
pub fn new(
|
||||
random: &'h mut Random,
|
||||
hit_object: &'h HitObject,
|
||||
end_time: f64,
|
||||
sample: HitSoundType,
|
||||
total_columns: i32,
|
||||
prev_pattern: &'h Pattern,
|
||||
orig: &'h Beatmap,
|
||||
) -> Self {
|
||||
let convert_type = if prev_pattern.column_with_objs() == total_columns {
|
||||
PatternType::default()
|
||||
} else {
|
||||
PatternType::FORCE_NOT_STACK
|
||||
};
|
||||
|
||||
let inner = PatternGenerator::new(hit_object, total_columns, random, orig);
|
||||
|
||||
Self {
|
||||
end_time,
|
||||
sample,
|
||||
inner,
|
||||
convert_type,
|
||||
prev_pattern,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate(&mut self) -> Pattern {
|
||||
let generate_hold = self.end_time - self.inner.hit_object.start_time >= 100.0;
|
||||
|
||||
match self.inner.total_columns {
|
||||
8 if self.sample.has_flag(HitSoundType::FINISH)
|
||||
&& self.end_time - self.inner.hit_object.start_time < 1000.0 =>
|
||||
{
|
||||
Pattern::new_end_time_note(self, 0, generate_hold)
|
||||
}
|
||||
8 => {
|
||||
let column = self.get_random_column(self.inner.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 = self.inner.get_random_column(Some(lower), None);
|
||||
|
||||
if self.convert_type.contains(PatternType::FORCE_NOT_STACK) {
|
||||
self.find_available_column(column, Some(lower), &[self.prev_pattern])
|
||||
} else {
|
||||
self.find_available_column(column, Some(lower), &[])
|
||||
}
|
||||
}
|
||||
|
||||
fn find_available_column(
|
||||
&mut self,
|
||||
mut initial_column: u8,
|
||||
lower: Option<i32>,
|
||||
// upper: Option<i32>,
|
||||
// next_column: Option<&dyn Fn(u8) -> u8>,
|
||||
// validation: Option<&dyn Fn(i32) -> bool>,
|
||||
patterns: &[&Pattern],
|
||||
) -> u8 {
|
||||
let lower = lower.unwrap_or_else(|| self.inner.random_start());
|
||||
let upper = self.inner.total_columns;
|
||||
|
||||
let is_valid = |column: i32| {
|
||||
let column = column as u8;
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.all(|pattern| !pattern.column_has_obj(column))
|
||||
};
|
||||
|
||||
// * Check for the initial column
|
||||
if is_valid(i32::from(initial_column)) {
|
||||
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 = self.inner.get_random_column(Some(lower), Some(upper));
|
||||
|
||||
!is_valid(i32::from(initial_column))
|
||||
} {}
|
||||
|
||||
initial_column
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
use rosu_map::section::hit_objects::hit_samples::HitSoundType;
|
||||
|
||||
use crate::{
|
||||
mania::{
|
||||
convert::{pattern::Pattern, pattern_type::PatternType, PrevValues},
|
||||
object::ManiaObject,
|
||||
},
|
||||
model::{
|
||||
beatmap::Beatmap,
|
||||
control_point::{EffectPoint, TimingPoint},
|
||||
hit_object::HitObject,
|
||||
},
|
||||
util::random::Random,
|
||||
};
|
||||
|
||||
use super::PatternGenerator;
|
||||
|
||||
pub struct HitObjectPatternGenerator<'h> {
|
||||
pub sample: HitSoundType,
|
||||
pub stair_type: PatternType,
|
||||
pub inner: PatternGenerator<'h>,
|
||||
convert_type: PatternType,
|
||||
prev_pattern: &'h Pattern,
|
||||
}
|
||||
|
||||
impl<'h> HitObjectPatternGenerator<'h> {
|
||||
pub fn new(
|
||||
random: &'h mut Random,
|
||||
hit_object: &'h HitObject,
|
||||
sample: HitSoundType,
|
||||
total_columns: i32,
|
||||
prev: &'h PrevValues,
|
||||
density: f64,
|
||||
orig: &'h Beatmap,
|
||||
) -> Self {
|
||||
let beat_len = orig
|
||||
.timing_point_at(hit_object.start_time)
|
||||
.map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
|
||||
|
||||
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 >= beat_len / 2.5 {
|
||||
// * Low density stream
|
||||
convert_type |= PatternType::REVERSE | PatternType::LOW_PROBABILITY;
|
||||
} else if density < beat_len / 2.5 {
|
||||
// * High density
|
||||
} else {
|
||||
let kiai = orig
|
||||
.effect_point_at(hit_object.start_time)
|
||||
.map_or(EffectPoint::DEFAULT_KIAI, |point| point.kiai);
|
||||
|
||||
if kiai {
|
||||
// * High density
|
||||
} else {
|
||||
convert_type |= PatternType::LOW_PROBABILITY;
|
||||
}
|
||||
}
|
||||
|
||||
if !convert_type.contains(PatternType::KEEP_SINGLE) {
|
||||
if sample.has_flag(HitSoundType::FINISH) && total_columns != 8 {
|
||||
convert_type |= PatternType::MIRROR;
|
||||
} else if sample.has_flag(HitSoundType::CLAP) {
|
||||
convert_type |= PatternType::GATHERED;
|
||||
}
|
||||
}
|
||||
|
||||
let inner = PatternGenerator::new(hit_object, total_columns, random, orig);
|
||||
|
||||
Self {
|
||||
stair_type: prev.stair,
|
||||
convert_type,
|
||||
sample,
|
||||
prev_pattern: &prev.pattern,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate(&mut self) -> Pattern {
|
||||
let pattern = self.generate_core();
|
||||
|
||||
for obj in pattern.hit_objects.iter() {
|
||||
let col = ManiaObject::column(obj.pos.x, self.inner.total_columns as f32) as i32;
|
||||
|
||||
if self.convert_type.contains(PatternType::STAIR) && col == self.inner.total_columns - 1
|
||||
{
|
||||
self.stair_type = PatternType::REVERSE_STAIR;
|
||||
}
|
||||
|
||||
if self.convert_type.contains(PatternType::REVERSE_STAIR)
|
||||
&& col == self.inner.random_start()
|
||||
{
|
||||
self.stair_type = PatternType::STAIR;
|
||||
}
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
fn generate_core(&mut self) -> Pattern {
|
||||
if self.inner.total_columns == 1 {
|
||||
return Pattern::new_note(self, 0);
|
||||
}
|
||||
|
||||
let last_column = self.prev_pattern.hit_objects.last().map_or(0, |h| {
|
||||
ManiaObject::column(h.pos.x, self.inner.total_columns as f32) as u8
|
||||
});
|
||||
|
||||
let random_start = self.inner.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.inner.total_columns as u8 {
|
||||
if self.prev_pattern.column_has_obj(i) {
|
||||
pattern.add_note(self, random_start + self.inner.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.inner.total_columns != 8 || last_column != 0)
|
||||
// * Make sure the last column was not the centre column
|
||||
&& (self.inner.total_columns % 2 == 0 || last_column != self.inner.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.inner.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.inner.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.inner.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.inner.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.inner.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.inner.total_columns
|
||||
- self.inner.random_start()
|
||||
- self.prev_pattern.column_with_objs())
|
||||
.min(note_count);
|
||||
}
|
||||
|
||||
let mut next_column = self.inner.get_column(Some(true));
|
||||
|
||||
for _ in 0..note_count {
|
||||
next_column = if allow_stacking {
|
||||
self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
Some(Self::get_next_column),
|
||||
&[&pattern],
|
||||
)
|
||||
} else {
|
||||
self.find_available_column(
|
||||
next_column,
|
||||
None,
|
||||
Some(Self::get_next_column),
|
||||
&[&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.inner.total_columns as u8 {
|
||||
last = self.inner.random_start() as u8;
|
||||
}
|
||||
} else {
|
||||
last = self.inner.get_random_column(None, None);
|
||||
}
|
||||
|
||||
last
|
||||
}
|
||||
|
||||
const fn has_special_column(&self) -> bool {
|
||||
self.sample.has_flag(HitSoundType::CLAP) && self.sample.has_flag(HitSoundType::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.inner.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.inner.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.has_flag(HitSoundType::CLAP) {
|
||||
p2 = 1.0;
|
||||
}
|
||||
|
||||
self.inner
|
||||
.get_random_note_count(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.inner.total_columns % 2 == 0 {
|
||||
self.inner.total_columns / 2
|
||||
} else {
|
||||
(self.inner.total_columns - 1) / 2
|
||||
};
|
||||
|
||||
let mut next_column = self.inner.get_random_column(None, Some(column_limit));
|
||||
|
||||
for _ in 0..note_count {
|
||||
next_column =
|
||||
self.find_available_column(next_column, Some(column_limit), None, &[&pattern]);
|
||||
|
||||
// * Add normal note
|
||||
pattern.add_note(self, next_column);
|
||||
|
||||
// * Add mirrored note
|
||||
let column =
|
||||
(self.inner.random_start() + self.inner.total_columns) as u8 - next_column - 1;
|
||||
pattern.add_note(self, column);
|
||||
}
|
||||
|
||||
if add_to_centre {
|
||||
pattern.add_note(self, self.inner.total_columns as u8 / 2);
|
||||
}
|
||||
|
||||
if self.inner.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.inner.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.inner.random.next_double();
|
||||
let note_count = self.inner.get_random_note_count(p2, p3, None, None, None);
|
||||
let add_to_centre = self.inner.total_columns % 2 != 0
|
||||
&& note_count != 3
|
||||
&& centre_val > 1.0 - centre_probability;
|
||||
|
||||
(note_count, add_to_centre)
|
||||
}
|
||||
|
||||
fn find_available_column(
|
||||
&mut self,
|
||||
mut initial_column: u8,
|
||||
upper: Option<i32>,
|
||||
next_column: Option<fn(&mut Self, u8) -> u8>,
|
||||
patterns: &[&Pattern],
|
||||
) -> u8 {
|
||||
let lower = self.inner.random_start();
|
||||
let upper = upper.unwrap_or(self.inner.total_columns);
|
||||
|
||||
let is_valid = |column: i32| {
|
||||
let column = column as u8;
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.all(|pattern| !pattern.column_has_obj(column))
|
||||
};
|
||||
|
||||
// * Check for the initial column
|
||||
if is_valid(i32::from(initial_column)) {
|
||||
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 {
|
||||
self.inner.get_random_column(Some(lower), Some(upper))
|
||||
};
|
||||
|
||||
!is_valid(i32::from(initial_column))
|
||||
} {}
|
||||
|
||||
initial_column
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use crate::{
|
||||
mania::object::ManiaObject,
|
||||
model::{beatmap::Beatmap, hit_object::HitObject},
|
||||
util::random::Random,
|
||||
};
|
||||
|
||||
pub(super) mod distance_object;
|
||||
pub(super) mod end_time_object;
|
||||
pub(super) mod hit_object;
|
||||
|
||||
pub struct PatternGenerator<'a> {
|
||||
pub hit_object: &'a HitObject,
|
||||
pub total_columns: i32,
|
||||
random: &'a mut Random,
|
||||
original_map: &'a Beatmap,
|
||||
}
|
||||
|
||||
impl<'a> PatternGenerator<'a> {
|
||||
fn new(
|
||||
hit_object: &'a HitObject,
|
||||
total_columns: i32,
|
||||
random: &'a mut Random,
|
||||
original_map: &'a Beatmap,
|
||||
) -> Self {
|
||||
Self {
|
||||
hit_object,
|
||||
total_columns,
|
||||
random,
|
||||
original_map,
|
||||
}
|
||||
}
|
||||
|
||||
fn random_start(&self) -> i32 {
|
||||
i32::from(self.total_columns == 8)
|
||||
}
|
||||
|
||||
fn get_column(&self, allow_special: Option<bool>) -> u8 {
|
||||
let allow_special = allow_special.unwrap_or(false);
|
||||
|
||||
if allow_special && self.total_columns == 8 {
|
||||
const LOCAL_X_DIVISOR: f32 = 512.0 / 7.0;
|
||||
|
||||
((self.hit_object.pos.x / LOCAL_X_DIVISOR).floor() as u8).clamp(0, 6) + 1
|
||||
} else {
|
||||
ManiaObject::column(self.hit_object.pos.x, self.total_columns as f32) as u8
|
||||
}
|
||||
}
|
||||
|
||||
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.next_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 + i32::from(val >= 1.0 - p2)
|
||||
}
|
||||
}
|
||||
|
||||
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 += f64::from(orig.hp + orig.ar.clamp(4.0, 7.0)) / 1.5;
|
||||
conversion_difficulty += orig.hit_objects.len() as f64 / f64::from(drain_time) * 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(self.total_columns);
|
||||
|
||||
self.random.next_int_range(lower, upper) as u8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{BitAndAssign, BitOr, BitOrAssign, Not},
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct PatternType(u16);
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl PatternType {
|
||||
pub const FORCE_STACK: Self = Self(1 << 0);
|
||||
pub const FORCE_NOT_STACK: Self = Self(1 << 1);
|
||||
pub const KEEP_SINGLE: Self = Self(1 << 2);
|
||||
pub const LOW_PROBABILITY: Self = Self(1 << 3);
|
||||
// pub const ALTERNATE: Self = Self(1 << 4);
|
||||
// pub const FORCE_SIG_SLIDER: Self = Self(1 << 5);
|
||||
// pub const FORCE_NOT_SLIDER: Self = Self(1 << 6);
|
||||
pub const GATHERED: Self = Self(1 << 7);
|
||||
pub const MIRROR: Self = Self(1 << 8);
|
||||
pub const REVERSE: Self = Self(1 << 9);
|
||||
pub const CYCLE: Self = Self(1 << 10);
|
||||
pub const STAIR: Self = Self(1 << 11);
|
||||
pub 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 const fn contains(self, other: Self) -> bool {
|
||||
self.0 & other.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr for PatternType {
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 | rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOrAssign for PatternType {
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
self.0 |= rhs.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl BitAndAssign for PatternType {
|
||||
fn bitand_assign(&mut self, rhs: Self) {
|
||||
self.0 &= rhs.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Not for PatternType {
|
||||
type Output = Self;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
Self(!self.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::{
|
||||
any::difficulty::{skills::Skill, ModeDifficulty},
|
||||
mania::{
|
||||
difficulty::{object::ManiaDifficultyObject, skills::strain::Strain},
|
||||
object::{ManiaObject, ObjectParams},
|
||||
},
|
||||
util::float_ext::FloatExt,
|
||||
};
|
||||
|
||||
use super::{attributes::ManiaDifficultyAttributes, convert::ManiaBeatmap};
|
||||
|
||||
mod object;
|
||||
mod skills;
|
||||
|
||||
const STAR_SCALING_FACTOR: f64 = 0.018;
|
||||
|
||||
pub fn difficulty(
|
||||
difficulty: &ModeDifficulty,
|
||||
converted: &ManiaBeatmap<'_>,
|
||||
) -> ManiaDifficultyAttributes {
|
||||
let hit_window = converted
|
||||
.attributes()
|
||||
.mods(difficulty.get_mods())
|
||||
.clock_rate(difficulty.get_clock_rate())
|
||||
.hit_windows()
|
||||
.od;
|
||||
|
||||
let n_objects = converted.map.hit_objects.len() as u32;
|
||||
let values = DifficultyValues::calculate(difficulty, converted);
|
||||
|
||||
ManiaDifficultyAttributes {
|
||||
stars: values.strain.difficulty_value() * STAR_SCALING_FACTOR,
|
||||
hit_window,
|
||||
max_combo: values.max_combo,
|
||||
n_objects,
|
||||
is_convert: converted.is_convert,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DifficultyValues {
|
||||
pub strain: Strain,
|
||||
pub max_combo: u32,
|
||||
}
|
||||
|
||||
impl DifficultyValues {
|
||||
pub fn calculate(difficulty: &ModeDifficulty, converted: &ManiaBeatmap<'_>) -> Self {
|
||||
let take = difficulty.get_passed_objects();
|
||||
let total_columns = converted.map.cs.round_even().max(1.0);
|
||||
let clock_rate = difficulty.get_clock_rate();
|
||||
let mut params = ObjectParams::new(converted.map.as_ref());
|
||||
|
||||
let mut mania_objects = converted
|
||||
.map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.map(|h| ManiaObject::new(h, total_columns, &mut params))
|
||||
.take(take);
|
||||
|
||||
let Some(first) = mania_objects.next() else {
|
||||
return DifficultyValues {
|
||||
strain: Strain::new(total_columns as usize),
|
||||
max_combo: 0,
|
||||
};
|
||||
};
|
||||
|
||||
let n_diff_objects = mania_objects.len();
|
||||
|
||||
let diff_objects_iter = mania_objects.enumerate().scan(first, |last, (i, base)| {
|
||||
let diff_object = ManiaDifficultyObject::new(&base, last, clock_rate, i);
|
||||
*last = base;
|
||||
|
||||
Some(diff_object)
|
||||
});
|
||||
|
||||
let mut diff_objects = Vec::with_capacity(n_diff_objects);
|
||||
diff_objects.extend(diff_objects_iter);
|
||||
|
||||
let mut strain = Skill::new(Strain::new(total_columns as usize), &diff_objects);
|
||||
|
||||
for curr in diff_objects.iter() {
|
||||
strain.process(curr);
|
||||
}
|
||||
|
||||
Self {
|
||||
strain: strain.inner,
|
||||
max_combo: params.into_max_combo(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use crate::{any::difficulty::object::IDifficultyObject, mania::object::ManiaObject};
|
||||
|
||||
pub struct ManiaDifficultyObject {
|
||||
pub idx: usize,
|
||||
pub base_column: usize,
|
||||
pub delta_time: f64,
|
||||
pub start_time: f64,
|
||||
pub end_time: f64,
|
||||
}
|
||||
|
||||
impl ManiaDifficultyObject {
|
||||
pub fn new(base: &ManiaObject, last: &ManiaObject, clock_rate: f64, idx: usize) -> Self {
|
||||
Self {
|
||||
idx,
|
||||
base_column: base.column,
|
||||
delta_time: (base.start_time - last.start_time) / clock_rate,
|
||||
start_time: base.start_time / clock_rate,
|
||||
end_time: base.end_time / clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IDifficultyObject for ManiaDifficultyObject {
|
||||
fn idx(&self) -> usize {
|
||||
self.idx
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod strain;
|
||||
@@ -0,0 +1,186 @@
|
||||
use crate::{
|
||||
any::difficulty::{
|
||||
object::IDifficultyObject,
|
||||
skills::{strain_decay, ISkill, Skill, StrainDecaySkill},
|
||||
},
|
||||
mania::difficulty::object::ManiaDifficultyObject,
|
||||
};
|
||||
|
||||
const INDIVIDUAL_DECAY_BASE: f64 = 0.125;
|
||||
const OVERALL_DECAY_BASE: f64 = 0.3;
|
||||
const RELEASE_THRESHOLD: f64 = 24.0;
|
||||
|
||||
const SKILL_MULTIPLIER: f64 = 1.0;
|
||||
const STRAIN_DECAY_BASE: f64 = 1.0;
|
||||
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct Strain {
|
||||
start_times: Box<[f64]>,
|
||||
end_times: Box<[f64]>,
|
||||
individual_strains: Box<[f64]>,
|
||||
|
||||
individual_strain: f64,
|
||||
overall_strain: f64,
|
||||
|
||||
inner: StrainDecaySkill,
|
||||
}
|
||||
|
||||
impl Strain {
|
||||
pub fn new(total_columns: usize) -> Self {
|
||||
Self {
|
||||
start_times: vec![0.0; total_columns].into_boxed_slice(),
|
||||
end_times: vec![0.0; total_columns].into_boxed_slice(),
|
||||
individual_strains: vec![0.0; total_columns].into_boxed_slice(),
|
||||
individual_strain: 0.0,
|
||||
overall_strain: 1.0,
|
||||
inner: StrainDecaySkill::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
||||
self.inner.get_curr_strain_peaks()
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self) -> f64 {
|
||||
self.inner.difficulty_value(StrainDecaySkill::DECAY_WEIGHT)
|
||||
}
|
||||
|
||||
const fn curr_strain(&self) -> f64 {
|
||||
self.inner.curr_strain
|
||||
}
|
||||
|
||||
fn curr_strain_mut(&mut self) -> &mut f64 {
|
||||
&mut self.inner.curr_strain
|
||||
}
|
||||
|
||||
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64 {
|
||||
*self.curr_strain_mut() *= strain_decay(curr.delta_time, STRAIN_DECAY_BASE);
|
||||
*self.curr_strain_mut() += self.strain_value_of(curr) * SKILL_MULTIPLIER;
|
||||
|
||||
self.curr_strain()
|
||||
}
|
||||
|
||||
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject) -> f64 {
|
||||
let mania_curr = curr;
|
||||
let start_time = mania_curr.start_time;
|
||||
let end_time = mania_curr.end_time;
|
||||
let column = mania_curr.base_column;
|
||||
let mut is_overlapping = false;
|
||||
|
||||
// * Lowest value we can assume with the current information
|
||||
let mut closest_end_time = (end_time - start_time).abs();
|
||||
// * Factor to all additional strains in case something else is held
|
||||
let mut hold_factor = 1.0;
|
||||
// * Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
let mut hold_addition = 0.0;
|
||||
|
||||
for i in 0..self.end_times.len() {
|
||||
// * The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
is_overlapping |=
|
||||
self.end_times[i] > start_time + 1.0 && end_time > self.end_times[i] + 1.0;
|
||||
|
||||
// * We give a slight bonus to everything if something is held meanwhile
|
||||
if self.end_times[i] > end_time + 1.0 {
|
||||
hold_factor = 1.25;
|
||||
}
|
||||
|
||||
closest_end_time = (end_time - self.end_times[i]).abs().min(closest_end_time);
|
||||
}
|
||||
|
||||
// * The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||
// * Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||
// * holdAddition
|
||||
// * ^
|
||||
// * 1.0 + - - - - - -+-----------
|
||||
// * | /
|
||||
// * 0.5 + - - - - -/ Sigmoid Curve
|
||||
// * | /|
|
||||
// * 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// * release_threshold
|
||||
if is_overlapping {
|
||||
hold_addition = (1.0 + (0.5 * (RELEASE_THRESHOLD - closest_end_time)).exp()).recip();
|
||||
}
|
||||
|
||||
// * Decay and increase individualStrains in own column
|
||||
self.individual_strains[column] = apply_decay(
|
||||
self.individual_strains[column],
|
||||
start_time - self.start_times[column],
|
||||
INDIVIDUAL_DECAY_BASE,
|
||||
);
|
||||
self.individual_strains[column] += 2.0 * hold_factor;
|
||||
|
||||
// * For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
|
||||
self.individual_strain = if mania_curr.delta_time <= 1.0 {
|
||||
self.individual_strain.max(self.individual_strains[column])
|
||||
} else {
|
||||
self.individual_strains[column]
|
||||
};
|
||||
|
||||
// * Decay and increase overallStrain
|
||||
self.overall_strain = apply_decay(self.overall_strain, curr.delta_time, OVERALL_DECAY_BASE);
|
||||
self.overall_strain += (1.0 + hold_addition) * hold_factor;
|
||||
|
||||
// * Update startTimes and endTimes arrays
|
||||
self.start_times[column] = start_time;
|
||||
self.end_times[column] = end_time;
|
||||
|
||||
// * By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
|
||||
self.individual_strain + self.overall_strain - self.curr_strain()
|
||||
}
|
||||
}
|
||||
|
||||
impl ISkill for Strain {
|
||||
type DifficultyObjects<'a> = [ManiaDifficultyObject];
|
||||
}
|
||||
|
||||
impl Skill<'_, Strain> {
|
||||
fn calculate_initial_strain(&mut self, offset: f64, curr: &ManiaDifficultyObject) -> f64 {
|
||||
let prev_start_time = curr
|
||||
.previous(0, self.diff_objects)
|
||||
.map_or(0.0, |prev| prev.start_time);
|
||||
|
||||
let time = offset - prev_start_time;
|
||||
|
||||
let individual = apply_decay(self.inner.individual_strain, time, INDIVIDUAL_DECAY_BASE);
|
||||
let overall = apply_decay(self.inner.overall_strain, time, OVERALL_DECAY_BASE);
|
||||
|
||||
individual + overall
|
||||
}
|
||||
|
||||
const fn curr_section_peak(&self) -> f64 {
|
||||
self.inner.inner.inner.curr_section_peak
|
||||
}
|
||||
|
||||
fn curr_section_peak_mut(&mut self) -> &mut f64 {
|
||||
&mut self.inner.inner.inner.curr_section_peak
|
||||
}
|
||||
|
||||
const fn curr_section_end(&self) -> f64 {
|
||||
self.inner.inner.inner.curr_section_end
|
||||
}
|
||||
|
||||
fn curr_section_end_mut(&mut self) -> &mut f64 {
|
||||
&mut self.inner.inner.inner.curr_section_end
|
||||
}
|
||||
|
||||
pub fn process(&mut self, curr: &ManiaDifficultyObject) {
|
||||
if curr.idx == 0 {
|
||||
*self.curr_section_end_mut() = (curr.start_time / StrainDecaySkill::SECTION_LEN).ceil()
|
||||
* StrainDecaySkill::SECTION_LEN;
|
||||
}
|
||||
|
||||
while curr.start_time > self.curr_section_end() {
|
||||
self.inner.inner.save_curr_peak();
|
||||
let initial_strain = self.calculate_initial_strain(self.curr_section_end(), curr);
|
||||
self.inner.inner.start_new_section_from(initial_strain);
|
||||
*self.curr_section_end_mut() += StrainDecaySkill::SECTION_LEN;
|
||||
}
|
||||
|
||||
let strain_value_at = self.inner.strain_value_at(curr);
|
||||
*self.curr_section_peak_mut() = strain_value_at.max(self.curr_section_peak());
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_decay(value: f64, delta_time: f64, decay_base: f64) -> f64 {
|
||||
value * decay_base.powf(delta_time / 1000.0)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use super::mania_object::ManiaObject;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ManiaDifficultyObject {
|
||||
pub(crate) idx: usize,
|
||||
pub(crate) base_column: usize,
|
||||
pub(crate) delta_time: f64,
|
||||
pub(crate) start_time: f64,
|
||||
pub(crate) end_time: f64,
|
||||
}
|
||||
|
||||
impl ManiaDifficultyObject {
|
||||
pub(crate) fn new(base: &ManiaObject, last: &ManiaObject, clock_rate: f64, idx: usize) -> Self {
|
||||
Self {
|
||||
idx,
|
||||
base_column: base.column,
|
||||
delta_time: (base.start_time - last.start_time) / clock_rate,
|
||||
start_time: base.start_time / clock_rate,
|
||||
end_time: base.end_time / clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
#![cfg(feature = "gradual")]
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
beatmap::BeatmapHitWindows,
|
||||
parse::{HitObject, HitObjectKind},
|
||||
util::FloatExt,
|
||||
Beatmap, GameMode, Mods,
|
||||
};
|
||||
|
||||
use super::{
|
||||
difficulty_object::ManiaDifficultyObject,
|
||||
mania_object::ObjectParameters,
|
||||
skills::{Skill, Strain},
|
||||
ManiaDifficultyAttributes, ManiaObject, STAR_SCALING_FACTOR,
|
||||
};
|
||||
|
||||
/// Gradually calculate the difficulty attributes of an osu!mania map.
|
||||
///
|
||||
/// Note that this struct implements [`Iterator`].
|
||||
/// On every call of [`Iterator::next`], the map's next hit object will
|
||||
/// be processed and the [`ManiaDifficultyAttributes`] will be updated and returned.
|
||||
///
|
||||
/// If you want to calculate performance attributes, use
|
||||
/// [`ManiaGradualPerformance`](crate::mania::ManiaGradualPerformance) instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, mania::ManiaGradualDifficulty};
|
||||
///
|
||||
/// # /*
|
||||
/// let map: Beatmap = ...
|
||||
/// # */
|
||||
/// # let map = Beatmap::default();
|
||||
///
|
||||
/// let mods = 64; // DT
|
||||
/// let mut iter = ManiaGradualDifficulty::new(&map, mods);
|
||||
///
|
||||
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
|
||||
/// let attrs2 = iter.next(); // after the second hit object
|
||||
///
|
||||
/// // Remaining hit objects
|
||||
/// for difficulty in iter {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ManiaGradualDifficulty<'map> {
|
||||
map: Cow<'map, Beatmap>,
|
||||
inner: ManiaGradualDifficultyInner,
|
||||
}
|
||||
|
||||
impl<'map> ManiaGradualDifficulty<'map> {
|
||||
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||
let map = map.convert_mode(GameMode::Mania);
|
||||
let is_convert = matches!(map, Cow::Owned(_));
|
||||
let inner = ManiaGradualDifficultyInner::new(map.as_ref(), is_convert, mods);
|
||||
|
||||
Self { map, inner }
|
||||
}
|
||||
|
||||
pub(crate) const fn idx(&self) -> usize {
|
||||
self.inner.idx
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ManiaGradualDifficulty<'_> {
|
||||
type Item = ManiaDifficultyAttributes;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next(&self.map.hit_objects)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
self.inner.nth(n, &self.map.hit_objects)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for ManiaGradualDifficulty<'_> {
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gradually calculate the difficulty attributes of an osu!mania map.
|
||||
///
|
||||
/// Check [`ManiaGradualDifficulty`] for more information. This struct does the same
|
||||
/// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ManiaOwnedGradualDifficulty {
|
||||
// Technically only `Beatmap::hit_objects` are required here but storing
|
||||
// the full map lets us get away with not storing the map in `ManiaOwnedGradualPerformance`.
|
||||
pub(crate) map: Beatmap,
|
||||
inner: ManiaGradualDifficultyInner,
|
||||
}
|
||||
|
||||
impl ManiaOwnedGradualDifficulty {
|
||||
/// Create a new owned difficulty attributes iterator for osu!mania maps.
|
||||
pub fn new(map: Beatmap, mods: u32) -> Self {
|
||||
let converted_map = map.convert_mode(GameMode::Mania);
|
||||
let is_convert = matches!(converted_map, Cow::Owned(_));
|
||||
let inner = ManiaGradualDifficultyInner::new(&converted_map, is_convert, mods);
|
||||
|
||||
let map = match converted_map {
|
||||
Cow::Owned(map) => map,
|
||||
Cow::Borrowed(_) => map,
|
||||
};
|
||||
|
||||
Self { map, inner }
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) const fn idx(&self) -> usize {
|
||||
self.inner.idx
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ManiaOwnedGradualDifficulty {
|
||||
type Item = ManiaDifficultyAttributes;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next(&self.map.hit_objects)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
self.inner.nth(n, &self.map.hit_objects)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for ManiaOwnedGradualDifficulty {
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ManiaGradualDifficultyInner {
|
||||
pub(crate) idx: usize,
|
||||
hit_window: f64,
|
||||
strain: Strain,
|
||||
diff_objects: Box<[ManiaDifficultyObject]>,
|
||||
curr_combo: usize,
|
||||
clock_rate: f64,
|
||||
is_convert: bool,
|
||||
}
|
||||
|
||||
impl ManiaGradualDifficultyInner {
|
||||
fn new(map: &Beatmap, is_convert: bool, mods: u32) -> Self {
|
||||
let total_columns = map.cs.round_even().max(1.0);
|
||||
let clock_rate = mods.clock_rate();
|
||||
let strain = Strain::new(total_columns as usize);
|
||||
|
||||
let BeatmapHitWindows { od: hit_window, .. } = map
|
||||
.attributes()
|
||||
.mods(mods)
|
||||
.converted(is_convert)
|
||||
.clock_rate(clock_rate)
|
||||
.hit_windows();
|
||||
|
||||
let mut params = ObjectParameters::new(map);
|
||||
let mut curr_combo = 0;
|
||||
let mut hit_objects = map.hit_objects.iter();
|
||||
|
||||
let first = match hit_objects.next() {
|
||||
Some(h) => {
|
||||
let hit_object = ManiaObject::new(h, total_columns, &mut params);
|
||||
|
||||
increment_combo_raw(
|
||||
h,
|
||||
hit_object.start_time,
|
||||
hit_object.end_time,
|
||||
&mut curr_combo,
|
||||
);
|
||||
|
||||
hit_object
|
||||
}
|
||||
None => {
|
||||
return Self {
|
||||
idx: 0,
|
||||
hit_window,
|
||||
strain,
|
||||
diff_objects: Box::from([]),
|
||||
curr_combo: 0,
|
||||
clock_rate,
|
||||
is_convert,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
|
||||
let base = ManiaObject::new(h, total_columns, &mut params);
|
||||
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
|
||||
*last = base;
|
||||
|
||||
Some(diff_object)
|
||||
});
|
||||
|
||||
let mut diff_objects = Vec::with_capacity(map.hit_objects.len() - 1);
|
||||
diff_objects.extend(diff_objects_iter);
|
||||
|
||||
debug_assert_eq!(diff_objects.len(), diff_objects.capacity());
|
||||
|
||||
Self {
|
||||
idx: 0,
|
||||
hit_window,
|
||||
strain,
|
||||
diff_objects: diff_objects.into_boxed_slice(),
|
||||
curr_combo,
|
||||
clock_rate,
|
||||
is_convert,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self, hit_objects: &[HitObject]) -> Option<ManiaDifficultyAttributes> {
|
||||
// The first difficulty object belongs to the second note since each difficulty
|
||||
// object requires the current and the last note. Hence, if we're still on the first
|
||||
// object, we don't have a difficulty object yet and just skip processing.
|
||||
if self.idx > 0 {
|
||||
let curr = self.diff_objects.get(self.idx - 1)?;
|
||||
self.strain.process(curr, &self.diff_objects);
|
||||
|
||||
let h = &hit_objects[self.idx];
|
||||
increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
|
||||
} else if hit_objects.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.idx += 1;
|
||||
|
||||
Some(ManiaDifficultyAttributes {
|
||||
stars: self.strain.clone().difficulty_value() * STAR_SCALING_FACTOR,
|
||||
hit_window: self.hit_window,
|
||||
max_combo: self.curr_combo,
|
||||
is_convert: self.is_convert,
|
||||
n_objects: self.diff_objects.len() + 1,
|
||||
})
|
||||
}
|
||||
|
||||
const fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = self.len();
|
||||
|
||||
(len, Some(len))
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize, hit_objects: &[HitObject]) -> Option<ManiaDifficultyAttributes> {
|
||||
let skip_iter = self
|
||||
.diff_objects
|
||||
.iter()
|
||||
.zip(hit_objects.iter().skip(1))
|
||||
.skip(self.idx.saturating_sub(1));
|
||||
|
||||
let mut take = n.min(self.len().saturating_sub(1));
|
||||
|
||||
// The first note has no difficulty object
|
||||
if self.idx == 0 && take > 0 {
|
||||
take -= 1;
|
||||
self.idx += 1;
|
||||
}
|
||||
|
||||
for (curr, h) in skip_iter.take(take) {
|
||||
increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
|
||||
self.strain.process(curr, &self.diff_objects);
|
||||
self.idx += 1;
|
||||
}
|
||||
|
||||
self.next(hit_objects)
|
||||
}
|
||||
|
||||
const fn len(&self) -> usize {
|
||||
self.diff_objects.len() + 1 - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_combo(
|
||||
h: &HitObject,
|
||||
diff_obj: &ManiaDifficultyObject,
|
||||
curr_combo: &mut usize,
|
||||
clock_rate: f64,
|
||||
) {
|
||||
increment_combo_raw(
|
||||
h,
|
||||
diff_obj.start_time * clock_rate,
|
||||
diff_obj.end_time * clock_rate,
|
||||
curr_combo,
|
||||
);
|
||||
}
|
||||
|
||||
fn increment_combo_raw(h: &HitObject, start_time: f64, end_time: f64, curr_combo: &mut usize) {
|
||||
match h.kind {
|
||||
HitObjectKind::Circle => *curr_combo += 1,
|
||||
_ => *curr_combo += 1 + ((end_time - start_time) / 100.0) as usize,
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
#![cfg(feature = "gradual")]
|
||||
|
||||
use crate::{Beatmap, ManiaPP};
|
||||
|
||||
use super::{
|
||||
ManiaGradualDifficulty, ManiaOwnedGradualDifficulty, ManiaPerformanceAttributes,
|
||||
ManiaScoreState,
|
||||
};
|
||||
|
||||
/// Gradually calculate the performance attributes of an osu!mania map.
|
||||
///
|
||||
/// After each hit object you can call [`next`](`ManiaGradualPerformance::next`)
|
||||
/// and it will return the resulting current [`ManiaPerformanceAttributes`].
|
||||
/// To process multiple objects at once, use [`nth`](`ManiaGradualPerformance::nth`) instead.
|
||||
///
|
||||
/// Both methods require a play's current score so far.
|
||||
/// Be sure the given score is adjusted with respect to mods.
|
||||
///
|
||||
/// If you only want to calculate difficulty attributes use
|
||||
/// [`ManiaGradualDifficulty`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformance, ManiaScoreState}};
|
||||
///
|
||||
/// # /*
|
||||
/// let map: Beatmap = ...
|
||||
/// # */
|
||||
/// # let map = Beatmap::default();
|
||||
///
|
||||
/// let mods = 64; // DT
|
||||
/// let mut gradual_perf = ManiaGradualPerformance::new(&map, mods);
|
||||
/// let mut state = ManiaScoreState::new(); // empty state, everything is on 0.
|
||||
///
|
||||
/// // The first 10 hitresults are 320s
|
||||
/// for _ in 0..10 {
|
||||
/// state.n320 += 1;
|
||||
///
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.next(score).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.next(state.clone());
|
||||
/// }
|
||||
///
|
||||
/// // Then comes a miss.
|
||||
/// state.n_misses += 1;
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.next(score).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.next(state.clone());
|
||||
///
|
||||
/// // The next 10 objects will be a mixture of 320s and 100s.
|
||||
/// // Notice how all 10 objects will be processed in one go.
|
||||
/// state.n320 += 3;
|
||||
/// state.n100 += 7;
|
||||
/// // The `nth` method takes a zero-based value.
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.nth(score, 9).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.nth(state.clone(), 9);
|
||||
///
|
||||
/// // Skip to the end
|
||||
/// # /*
|
||||
/// state.max_combo = ...
|
||||
/// state.n300 = ...
|
||||
/// state.n100 = ...
|
||||
/// state.n_misses = ...
|
||||
/// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.nth(state.clone(), usize::MAX);
|
||||
///
|
||||
/// // Once the final performance was calculated,
|
||||
/// // attempting to process further objects will return `None`.
|
||||
/// assert!(gradual_perf.next(state).is_none());
|
||||
/// ```
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ManiaGradualPerformance<'map> {
|
||||
difficulty: ManiaGradualDifficulty<'map>,
|
||||
performance: ManiaPP<'map>,
|
||||
}
|
||||
|
||||
impl<'map> ManiaGradualPerformance<'map> {
|
||||
/// Create a new gradual performance calculator for osu!mania maps.
|
||||
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||
let difficulty = ManiaGradualDifficulty::new(map, mods);
|
||||
let performance = ManiaPP::new(map).mods(mods).passed_objects(0);
|
||||
|
||||
Self {
|
||||
difficulty,
|
||||
performance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Process the next hit object and calculate the
|
||||
/// performance attributes for the resulting score.
|
||||
pub fn next(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
|
||||
self.nth(state, 0)
|
||||
}
|
||||
|
||||
/// Process all remaining hit objects and calculate the final performance attributes.
|
||||
pub fn last(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
|
||||
self.nth(state, usize::MAX)
|
||||
}
|
||||
|
||||
/// Process everything up the the next `n`th hit object and calculate the performance
|
||||
/// attributes for the resulting score state.
|
||||
///
|
||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
|
||||
/// and so on.
|
||||
pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
|
||||
let difficulty = self.difficulty.nth(n)?;
|
||||
|
||||
let performance = self
|
||||
.performance
|
||||
.clone()
|
||||
.attributes(difficulty)
|
||||
.state(state)
|
||||
.passed_objects(self.difficulty.idx())
|
||||
.calculate();
|
||||
|
||||
Some(performance)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gradually calculate the performance attributes of an osu!mania map.
|
||||
///
|
||||
/// Check [`ManiaGradualPerformance`] for more information. This struct does the same
|
||||
/// but takes ownership of [`Beatmap`] to avoid being bound to a lifetime.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "gradual")))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ManiaOwnedGradualPerformance {
|
||||
difficulty: ManiaOwnedGradualDifficulty,
|
||||
mods: u32,
|
||||
}
|
||||
|
||||
impl ManiaOwnedGradualPerformance {
|
||||
/// Create a new gradual performance calculator for osu!mania maps.
|
||||
pub fn new(map: Beatmap, mods: u32) -> Self {
|
||||
let difficulty = ManiaOwnedGradualDifficulty::new(map, mods);
|
||||
|
||||
Self { difficulty, mods }
|
||||
}
|
||||
|
||||
/// Process the next hit object and calculate the
|
||||
/// performance attributes for the resulting score.
|
||||
pub fn next(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
|
||||
self.nth(state, 0)
|
||||
}
|
||||
|
||||
/// Process all remaining hit objects and calculate the final performance attributes.
|
||||
pub fn last(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
|
||||
self.nth(state, usize::MAX)
|
||||
}
|
||||
|
||||
/// Process everything up the the next `n`th hit object and calculate the performance
|
||||
/// attributes for the resulting score state.
|
||||
///
|
||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
|
||||
/// and so on.
|
||||
pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
|
||||
let difficulty = self.difficulty.nth(n)?;
|
||||
|
||||
let performance = ManiaPP::new(&self.difficulty.map)
|
||||
.mods(self.mods)
|
||||
.attributes(difficulty)
|
||||
.state(state)
|
||||
.passed_objects(self.difficulty.idx())
|
||||
.calculate();
|
||||
|
||||
Some(performance)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use crate::{
|
||||
curve::{Curve, CurveBuffers},
|
||||
parse::{HitObject, HitObjectKind},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||
|
||||
pub(crate) struct ObjectParameters<'a> {
|
||||
pub(crate) map: &'a Beatmap,
|
||||
pub(crate) max_combo: usize,
|
||||
pub(crate) curve_bufs: CurveBuffers,
|
||||
}
|
||||
|
||||
impl<'a> ObjectParameters<'a> {
|
||||
pub(crate) fn new(map: &'a Beatmap) -> Self {
|
||||
Self {
|
||||
map,
|
||||
max_combo: 0,
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`HitObject`] that was processed for the osu!mania gamemode.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ManiaObject {
|
||||
/// Start time of the object.
|
||||
pub start_time: f64,
|
||||
/// Endtime of the object.
|
||||
pub end_time: f64,
|
||||
/// Column of the object.
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl ManiaObject {
|
||||
pub(crate) fn column(x: f32, total_columns: f32) -> usize {
|
||||
let x_divisor = 512.0 / total_columns;
|
||||
|
||||
(x / x_divisor).floor().min(total_columns - 1.0) as usize
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
h: &HitObject,
|
||||
total_columns: f32,
|
||||
params: &mut ObjectParameters<'_>,
|
||||
) -> Self {
|
||||
let ObjectParameters {
|
||||
map,
|
||||
max_combo,
|
||||
curve_bufs,
|
||||
} = params;
|
||||
|
||||
let column = Self::column(h.pos.x, total_columns);
|
||||
*max_combo += 1;
|
||||
|
||||
match &h.kind {
|
||||
HitObjectKind::Circle => Self {
|
||||
start_time: h.start_time,
|
||||
end_time: h.start_time,
|
||||
column,
|
||||
},
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
repeats,
|
||||
control_points,
|
||||
..
|
||||
} => {
|
||||
let span_count = *repeats as f64 + 1.0;
|
||||
|
||||
let curve = Curve::new(control_points, *pixel_len, curve_bufs);
|
||||
let dist = curve.dist();
|
||||
|
||||
let timing_point = map.timing_point_at(h.start_time);
|
||||
let difficulty_point = map.difficulty_point_at(h.start_time).unwrap_or_default();
|
||||
|
||||
let scoring_dist =
|
||||
BASE_SCORING_DISTANCE * map.slider_mult * difficulty_point.slider_vel;
|
||||
|
||||
let vel = scoring_dist / timing_point.beat_len;
|
||||
let duration = span_count * dist / vel;
|
||||
let end_time = h.start_time + duration;
|
||||
|
||||
*max_combo += (duration / 100.0) as usize;
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
end_time,
|
||||
column,
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { end_time } | HitObjectKind::Hold { end_time } => {
|
||||
*max_combo += ((*end_time - h.start_time) / 100.0) as usize;
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
end_time: *end_time,
|
||||
column,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-329
@@ -1,345 +1,50 @@
|
||||
mod difficulty_object;
|
||||
mod mania_object;
|
||||
mod pp;
|
||||
mod score_state;
|
||||
mod skills;
|
||||
|
||||
#[cfg(feature = "gradual")]
|
||||
mod gradual_difficulty;
|
||||
#[cfg(feature = "gradual")]
|
||||
mod gradual_performance;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{beatmap::BeatmapHitWindows, util::FloatExt, Beatmap, GameMode, Mods, OsuStars};
|
||||
use crate::{
|
||||
any::ModeDifficulty,
|
||||
model::{
|
||||
beatmap::Beatmap,
|
||||
mode::{ConvertStatus, IGameMode},
|
||||
},
|
||||
};
|
||||
|
||||
pub use self::{mania_object::ManiaObject, pp::*, score_state::ManiaScoreState};
|
||||
|
||||
#[cfg(feature = "gradual")]
|
||||
pub use self::{
|
||||
gradual_difficulty::{ManiaGradualDifficulty, ManiaOwnedGradualDifficulty},
|
||||
gradual_performance::{ManiaGradualPerformance, ManiaOwnedGradualPerformance},
|
||||
attributes::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
|
||||
convert::ManiaBeatmap,
|
||||
performance::ManiaPerformance,
|
||||
score_state::ManiaScoreState,
|
||||
strains::ManiaStrains,
|
||||
};
|
||||
|
||||
pub(crate) use self::mania_object::ObjectParameters;
|
||||
mod attributes;
|
||||
mod convert;
|
||||
mod difficulty;
|
||||
mod object;
|
||||
mod performance;
|
||||
mod score_state;
|
||||
mod strains;
|
||||
|
||||
use self::{
|
||||
difficulty_object::ManiaDifficultyObject,
|
||||
skills::{Skill, Strain},
|
||||
};
|
||||
|
||||
const SECTION_LEN: f64 = 400.0;
|
||||
const STAR_SCALING_FACTOR: f64 = 0.018;
|
||||
|
||||
/// Difficulty calculator on osu!mania maps.
|
||||
/// Marker type for [`GameMode::Mania`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{ManiaStars, Beatmap};
|
||||
///
|
||||
/// # /*
|
||||
/// let map: Beatmap = ...
|
||||
/// # */
|
||||
/// # let map = Beatmap::default();
|
||||
///
|
||||
/// let difficulty_attrs = ManiaStars::new(&map)
|
||||
/// .mods(8 + 64) // HDDT
|
||||
/// .calculate();
|
||||
///
|
||||
/// println!("Stars: {}", difficulty_attrs.stars);
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use]
|
||||
pub struct ManiaStars<'map> {
|
||||
map: Cow<'map, Beatmap>,
|
||||
mods: u32,
|
||||
passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
is_convert: bool,
|
||||
}
|
||||
/// [`GameMode::Mania`]: rosu_map::section::general::GameMode::Mania
|
||||
pub struct Mania;
|
||||
|
||||
impl<'map> ManiaStars<'map> {
|
||||
/// Create a new difficulty calculator for osu!mania maps.
|
||||
#[inline]
|
||||
pub fn new(map: &'map Beatmap) -> Self {
|
||||
let map = map.convert_mode(GameMode::Mania);
|
||||
let is_convert = matches!(map, Cow::Owned(_));
|
||||
impl IGameMode for Mania {
|
||||
type DifficultyAttributes = ManiaDifficultyAttributes;
|
||||
type Strains = ManiaStrains;
|
||||
|
||||
Self {
|
||||
map,
|
||||
mods: 0,
|
||||
passed_objects: None,
|
||||
clock_rate: None,
|
||||
is_convert,
|
||||
}
|
||||
fn try_convert(map: &mut Cow<'_, Beatmap>) -> ConvertStatus {
|
||||
convert::try_convert(map)
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
#[inline]
|
||||
pub const fn mods(mut self, mods: u32) -> Self {
|
||||
self.mods = mods;
|
||||
|
||||
self
|
||||
fn difficulty(
|
||||
difficulty: &ModeDifficulty,
|
||||
converted: &ManiaBeatmap<'_>,
|
||||
) -> Self::DifficultyAttributes {
|
||||
difficulty::difficulty(difficulty, converted)
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
///
|
||||
#[cfg_attr(
|
||||
feature = "gradual",
|
||||
doc = "If you want to calculate the difficulty after every few objects, instead of
|
||||
using [`ManiaStars`] multiple times with different `passed_objects`, you should use
|
||||
[`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficulty)."
|
||||
)]
|
||||
#[inline]
|
||||
pub const fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||
self.passed_objects = Some(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adjust the clock rate used in the calculation.
|
||||
/// If none is specified, it will take the clock rate based on the mods
|
||||
/// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
|
||||
#[inline]
|
||||
pub const fn clock_rate(mut self, clock_rate: f64) -> Self {
|
||||
self.clock_rate = Some(clock_rate);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify whether the map is a convert i.e. an osu!standard map.
|
||||
///
|
||||
/// This only needs to be specified if the map was converted manually beforehand.
|
||||
#[inline]
|
||||
pub const fn is_convert(mut self, is_convert: bool) -> Self {
|
||||
self.is_convert = is_convert;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Calculate all difficulty related values, including stars.
|
||||
#[inline]
|
||||
pub fn calculate(self) -> ManiaDifficultyAttributes {
|
||||
let is_convert = self.is_convert || matches!(self.map, Cow::Owned(_));
|
||||
|
||||
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
|
||||
|
||||
let BeatmapHitWindows { od: hit_window, .. } = self
|
||||
.map
|
||||
.attributes()
|
||||
.mods(self.mods)
|
||||
.converted(is_convert)
|
||||
.clock_rate(clock_rate)
|
||||
.hit_windows();
|
||||
|
||||
let n_objects = self.map.hit_objects.len();
|
||||
let ManiaResult { strain, max_combo } = calculate_result(self);
|
||||
|
||||
ManiaDifficultyAttributes {
|
||||
stars: strain.difficulty_value() * STAR_SCALING_FACTOR,
|
||||
hit_window,
|
||||
max_combo,
|
||||
n_objects,
|
||||
is_convert,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the skill strains.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
#[inline]
|
||||
pub fn strains(self) -> ManiaStrains {
|
||||
let ManiaResult { strain, .. } = calculate_result(self);
|
||||
|
||||
ManiaStrains {
|
||||
section_len: SECTION_LEN,
|
||||
strains: strain.strain_peaks.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of calculating the strains on a osu!taiko map.
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ManiaStrains {
|
||||
/// Time in ms inbetween two strains.
|
||||
pub section_len: f64,
|
||||
/// Strain peaks of the strain skill.
|
||||
pub strains: Vec<f64>,
|
||||
}
|
||||
|
||||
impl ManiaStrains {
|
||||
/// Returns the number of strain peaks per skill.
|
||||
#[inline]
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.strains.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
|
||||
let ManiaStars {
|
||||
map,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
is_convert: _,
|
||||
} = params;
|
||||
|
||||
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
||||
let total_columns = map.cs.round_even().max(1.0);
|
||||
|
||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||
let mut strain = Strain::new(total_columns as usize);
|
||||
let mut params = ObjectParameters::new(map.as_ref());
|
||||
let mut hit_objects = map.hit_objects.iter().take(take);
|
||||
|
||||
let first = match hit_objects.next() {
|
||||
Some(h) => ManiaObject::new(h, total_columns, &mut params),
|
||||
None => {
|
||||
return ManiaResult {
|
||||
strain,
|
||||
max_combo: 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
|
||||
let base = ManiaObject::new(h, total_columns, &mut params);
|
||||
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
|
||||
*last = base;
|
||||
|
||||
Some(diff_object)
|
||||
});
|
||||
|
||||
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().min(take).saturating_sub(1));
|
||||
diff_objects.extend(diff_objects_iter);
|
||||
|
||||
for curr in diff_objects.iter() {
|
||||
strain.process(curr, &diff_objects);
|
||||
}
|
||||
|
||||
ManiaResult {
|
||||
strain,
|
||||
max_combo: params.max_combo,
|
||||
}
|
||||
}
|
||||
|
||||
struct ManiaResult {
|
||||
strain: Strain,
|
||||
max_combo: usize,
|
||||
}
|
||||
|
||||
/// The result of a difficulty calculation on an osu!mania map.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ManiaDifficultyAttributes {
|
||||
/// The final star rating.
|
||||
pub stars: f64,
|
||||
/// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
pub hit_window: f64,
|
||||
/// The amount of hitobjects in the map.
|
||||
pub n_objects: usize,
|
||||
/// The maximum achievable combo.
|
||||
pub max_combo: usize,
|
||||
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
|
||||
pub is_convert: bool,
|
||||
}
|
||||
|
||||
impl ManiaDifficultyAttributes {
|
||||
/// Return the maximum combo.
|
||||
#[inline]
|
||||
pub const fn max_combo(&self) -> usize {
|
||||
self.max_combo
|
||||
}
|
||||
|
||||
/// Return the amount of hitobjects.
|
||||
#[inline]
|
||||
pub const fn n_objects(&self) -> usize {
|
||||
self.n_objects
|
||||
}
|
||||
|
||||
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
|
||||
#[inline]
|
||||
pub const fn is_convert(&self) -> bool {
|
||||
self.is_convert
|
||||
}
|
||||
|
||||
/// Returns a builder for performance calculation.
|
||||
#[inline]
|
||||
pub fn pp(self) -> ManiaPP<'static> {
|
||||
ManiaPP::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a performance calculation on an osu!mania map.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ManiaPerformanceAttributes {
|
||||
/// The difficulty attributes that were used for the performance calculation.
|
||||
pub difficulty: ManiaDifficultyAttributes,
|
||||
/// The final performance points.
|
||||
pub pp: f64,
|
||||
/// The difficulty portion of the final pp.
|
||||
pub pp_difficulty: f64,
|
||||
}
|
||||
|
||||
impl ManiaPerformanceAttributes {
|
||||
/// Return the star value.
|
||||
#[inline]
|
||||
pub const fn stars(&self) -> f64 {
|
||||
self.difficulty.stars
|
||||
}
|
||||
|
||||
/// Return the performance point value.
|
||||
#[inline]
|
||||
pub const fn pp(&self) -> f64 {
|
||||
self.pp
|
||||
}
|
||||
|
||||
/// Return the maximum combo of the map.
|
||||
#[inline]
|
||||
pub const fn max_combo(&self) -> usize {
|
||||
self.difficulty.max_combo
|
||||
}
|
||||
|
||||
/// Return the amount of hitobjects.
|
||||
#[inline]
|
||||
pub const fn n_objects(&self) -> usize {
|
||||
self.difficulty.n_objects
|
||||
}
|
||||
|
||||
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
|
||||
#[inline]
|
||||
pub const fn is_convert(&self) -> bool {
|
||||
self.difficulty.is_convert
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes {
|
||||
#[inline]
|
||||
fn from(attributes: ManiaPerformanceAttributes) -> Self {
|
||||
attributes.difficulty
|
||||
}
|
||||
}
|
||||
|
||||
impl<'map> From<OsuStars<'map>> for ManiaStars<'map> {
|
||||
#[inline]
|
||||
fn from(osu: OsuStars<'map>) -> Self {
|
||||
let OsuStars {
|
||||
map,
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
} = osu;
|
||||
|
||||
Self {
|
||||
map: map.convert_mode(GameMode::Mania),
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
is_convert: true,
|
||||
}
|
||||
fn strains(difficulty: &ModeDifficulty, converted: &ManiaBeatmap<'_>) -> Self::Strains {
|
||||
strains::strains(difficulty, converted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use rosu_map::section::hit_objects::CurveBuffers;
|
||||
|
||||
use crate::model::{
|
||||
beatmap::Beatmap,
|
||||
control_point::{DifficultyPoint, TimingPoint},
|
||||
hit_object::{HitObject, HitObjectKind, HoldNote, Spinner},
|
||||
};
|
||||
|
||||
pub struct ManiaObject {
|
||||
pub start_time: f64,
|
||||
pub end_time: f64,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl ManiaObject {
|
||||
pub fn new(h: &HitObject, total_columns: f32, params: &mut ObjectParams<'_>) -> Self {
|
||||
let column = Self::column(h.pos.x, total_columns);
|
||||
params.max_combo += 1;
|
||||
|
||||
match h.kind {
|
||||
HitObjectKind::Circle => Self {
|
||||
start_time: h.start_time,
|
||||
end_time: h.start_time,
|
||||
column,
|
||||
},
|
||||
HitObjectKind::Slider(ref slider) => {
|
||||
const BASE_SCORING_DIST: f32 = 100.0;
|
||||
|
||||
let dist = slider.curve(&mut params.curve_bufs).dist();
|
||||
|
||||
let beat_len = params
|
||||
.map
|
||||
.timing_point_at(h.start_time)
|
||||
.map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
|
||||
|
||||
let slider_velocity = params
|
||||
.map
|
||||
.difficulty_point_at(h.start_time)
|
||||
.map_or(DifficultyPoint::DEFAULT_SLIDER_VELOCITY, |point| {
|
||||
point.slider_velocity
|
||||
});
|
||||
|
||||
let scoring_dist =
|
||||
f64::from(BASE_SCORING_DIST) * params.map.slider_multiplier * slider_velocity;
|
||||
let velocity = scoring_dist / beat_len;
|
||||
|
||||
let duration = (slider.span_count() as f64) * dist / velocity;
|
||||
|
||||
params.max_combo += (duration / 100.0) as u32;
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
end_time: h.start_time + duration,
|
||||
column,
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner(Spinner { end_time })
|
||||
| HitObjectKind::Hold(HoldNote { end_time }) => {
|
||||
params.max_combo += ((end_time - h.start_time) / 100.0) as u32;
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
end_time,
|
||||
column,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn column(x: f32, total_columns: f32) -> usize {
|
||||
let x_divisor = 512.0 / total_columns;
|
||||
|
||||
(x / x_divisor).floor().min(total_columns - 1.0) as usize
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ObjectParams<'a> {
|
||||
map: &'a Beatmap,
|
||||
max_combo: u32,
|
||||
curve_bufs: CurveBuffers,
|
||||
}
|
||||
|
||||
impl<'a> ObjectParams<'a> {
|
||||
pub fn new(map: &'a Beatmap) -> Self {
|
||||
Self {
|
||||
map,
|
||||
max_combo: 0,
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_max_combo(self) -> u32 {
|
||||
self.max_combo
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+10
-13
@@ -1,35 +1,32 @@
|
||||
/// Aggregation for a score's current state i.e. what are the current hitresults.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
/// Aggregation for a score's current state.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ManiaScoreState {
|
||||
/// Amount of current 320s.
|
||||
pub n320: usize,
|
||||
pub n320: u32,
|
||||
/// Amount of current 300s.
|
||||
pub n300: usize,
|
||||
pub n300: u32,
|
||||
/// Amount of current 200s.
|
||||
pub n200: usize,
|
||||
pub n200: u32,
|
||||
/// Amount of current 100s.
|
||||
pub n100: usize,
|
||||
pub n100: u32,
|
||||
/// Amount of current 50s.
|
||||
pub n50: usize,
|
||||
pub n50: u32,
|
||||
/// Amount of current misses.
|
||||
pub n_misses: usize,
|
||||
pub n_misses: u32,
|
||||
}
|
||||
|
||||
impl ManiaScoreState {
|
||||
/// Create a new empty score state.
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Return the total amount of hits by adding everything up.
|
||||
#[inline]
|
||||
pub const fn total_hits(&self) -> usize {
|
||||
pub const fn total_hits(&self) -> u32 {
|
||||
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
|
||||
}
|
||||
|
||||
/// Calculate the accuracy between `0.0` and `1.0` for this state.
|
||||
#[inline]
|
||||
pub fn accuracy(&self) -> f64 {
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
@@ -40,6 +37,6 @@ impl ManiaScoreState {
|
||||
let numerator = 6 * (self.n320 + self.n300) + 4 * self.n200 + 2 * self.n100 + self.n50;
|
||||
let denominator = 6 * total_hits;
|
||||
|
||||
numerator as f64 / denominator as f64
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
mod strain;
|
||||
mod traits;
|
||||
|
||||
pub(crate) use self::{
|
||||
strain::Strain,
|
||||
traits::{Skill, StrainDecaySkill, StrainSkill},
|
||||
};
|
||||
|
||||
use super::difficulty_object::ManiaDifficultyObject;
|
||||
|
||||
fn previous(
|
||||
diff_objects: &[ManiaDifficultyObject],
|
||||
curr: usize,
|
||||
backwards_idx: usize,
|
||||
) -> Option<&ManiaDifficultyObject> {
|
||||
curr.checked_sub(backwards_idx + 1)
|
||||
.and_then(|idx| diff_objects.get(idx))
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
use crate::{mania::difficulty_object::ManiaDifficultyObject, util::CompactVec};
|
||||
|
||||
use super::{previous, Skill, StrainDecaySkill, StrainSkill};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub(crate) struct Strain {
|
||||
start_times: Vec<f64>,
|
||||
end_times: Vec<f64>,
|
||||
individual_strains: Vec<f64>,
|
||||
|
||||
individual_strain: f64,
|
||||
overall_strain: f64,
|
||||
|
||||
curr_strain: f64,
|
||||
curr_section_peak: f64,
|
||||
curr_section_end: f64,
|
||||
|
||||
pub(crate) strain_peaks: CompactVec,
|
||||
}
|
||||
|
||||
impl Strain {
|
||||
const INDIVIDUAL_DECAY_BASE: f64 = 0.125;
|
||||
const OVERALL_DECAY_BASE: f64 = 0.3;
|
||||
const RELEASE_THRESHOLD: f64 = 24.0;
|
||||
|
||||
pub(crate) fn new(total_columns: usize) -> Self {
|
||||
Self {
|
||||
start_times: vec![0.0; total_columns],
|
||||
end_times: vec![0.0; total_columns],
|
||||
individual_strains: vec![0.0; total_columns],
|
||||
individual_strain: 0.0,
|
||||
overall_strain: 1.0,
|
||||
curr_strain: 0.0,
|
||||
curr_section_peak: 0.0,
|
||||
curr_section_end: 0.0,
|
||||
strain_peaks: CompactVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_decay(value: f64, delta_time: f64, decay_base: f64) -> f64 {
|
||||
value * decay_base.powf(delta_time / 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Skill for Strain {
|
||||
#[inline]
|
||||
fn process(&mut self, curr: &ManiaDifficultyObject, diff_objects: &[ManiaDifficultyObject]) {
|
||||
<Self as StrainSkill>::process(self, curr, diff_objects);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn difficulty_value(self) -> f64 {
|
||||
<Self as StrainSkill>::difficulty_value(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl StrainSkill for Strain {
|
||||
const DECAY_WEIGHT: f64 = 0.9;
|
||||
|
||||
#[inline]
|
||||
fn curr_section_end(&self) -> f64 {
|
||||
self.curr_section_end
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn curr_section_end_mut(&mut self) -> &mut f64 {
|
||||
&mut self.curr_section_end
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn curr_section_peak(&self) -> f64 {
|
||||
self.curr_section_peak
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn curr_section_peak_mut(&mut self) -> &mut f64 {
|
||||
&mut self.curr_section_peak
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_peaks_mut(&mut self) -> &mut CompactVec {
|
||||
&mut self.strain_peaks
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64 {
|
||||
<Self as StrainDecaySkill>::strain_value_at(self, curr)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn calculate_initial_strain(
|
||||
&self,
|
||||
time: f64,
|
||||
curr: &ManiaDifficultyObject,
|
||||
diff_objects: &[ManiaDifficultyObject],
|
||||
) -> f64 {
|
||||
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr, diff_objects)
|
||||
}
|
||||
}
|
||||
|
||||
impl StrainDecaySkill for Strain {
|
||||
const SKILL_MULTIPLIER: f64 = 1.0;
|
||||
const STRAIN_DECAY_BASE: f64 = 1.0;
|
||||
|
||||
#[inline]
|
||||
fn curr_strain(&self) -> f64 {
|
||||
self.curr_strain
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn curr_strain_mut(&mut self) -> &mut f64 {
|
||||
&mut self.curr_strain
|
||||
}
|
||||
|
||||
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject) -> f64 {
|
||||
let mania_curr = curr;
|
||||
let start_time = mania_curr.start_time;
|
||||
let end_time = mania_curr.end_time;
|
||||
let col = mania_curr.base_column;
|
||||
let mut is_overlapping = false;
|
||||
|
||||
// * Lowest value we can assume with the current information
|
||||
let mut closest_end_time = (end_time - start_time).abs();
|
||||
// * Factor to all additional strains in case something else is held
|
||||
let mut hold_factor = 1.0;
|
||||
// * Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
let mut hold_addition = 0.0;
|
||||
|
||||
for i in 0..self.end_times.len() {
|
||||
// * The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
is_overlapping |=
|
||||
self.end_times[i] > start_time + 1.0 && end_time > self.end_times[i] + 1.0;
|
||||
|
||||
// * We give a slight bonus to everything if something is held meanwhile
|
||||
if self.end_times[i] > end_time + 1.0 {
|
||||
hold_factor = 1.25;
|
||||
}
|
||||
|
||||
closest_end_time = (end_time - self.end_times[i]).abs().min(closest_end_time);
|
||||
}
|
||||
|
||||
// * The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||
// * Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||
// * holdAddition
|
||||
// * ^
|
||||
// * 1.0 + - - - - - -+-----------
|
||||
// * | /
|
||||
// * 0.5 + - - - - -/ Sigmoid Curve
|
||||
// * | /|
|
||||
// * 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// * release_threshold
|
||||
if is_overlapping {
|
||||
hold_addition =
|
||||
(1.0 + (0.5 * (Self::RELEASE_THRESHOLD - closest_end_time)).exp()).recip();
|
||||
}
|
||||
|
||||
// * Decay and increase individualStrains in own column
|
||||
self.individual_strains[col] = Self::apply_decay(
|
||||
self.individual_strains[col],
|
||||
start_time - self.start_times[col],
|
||||
Self::INDIVIDUAL_DECAY_BASE,
|
||||
);
|
||||
self.individual_strains[col] += 2.0 * hold_factor;
|
||||
|
||||
// * For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
|
||||
self.individual_strain = if mania_curr.delta_time <= 1.0 {
|
||||
self.individual_strain.max(self.individual_strains[col])
|
||||
} else {
|
||||
self.individual_strains[col]
|
||||
};
|
||||
|
||||
// * Decay and increase overallStrain
|
||||
self.overall_strain = Self::apply_decay(
|
||||
self.overall_strain,
|
||||
curr.delta_time,
|
||||
Self::OVERALL_DECAY_BASE,
|
||||
);
|
||||
self.overall_strain += (1.0 + hold_addition) * hold_factor;
|
||||
|
||||
// * Update startTimes and endTimes arrays
|
||||
self.start_times[col] = start_time;
|
||||
self.end_times[col] = end_time;
|
||||
|
||||
// * By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
|
||||
self.individual_strain + self.overall_strain - self.curr_strain
|
||||
}
|
||||
|
||||
fn calculate_initial_strain(
|
||||
&self,
|
||||
offset: f64,
|
||||
curr: &ManiaDifficultyObject,
|
||||
diff_objects: &[ManiaDifficultyObject],
|
||||
) -> f64 {
|
||||
let prev_start = previous(diff_objects, curr.idx, 0).map_or(0.0, |h| h.start_time);
|
||||
|
||||
let individual_decay = Self::apply_decay(
|
||||
self.individual_strain,
|
||||
offset - prev_start,
|
||||
Self::INDIVIDUAL_DECAY_BASE,
|
||||
);
|
||||
|
||||
let overall_decay = Self::apply_decay(
|
||||
self.overall_strain,
|
||||
offset - prev_start,
|
||||
Self::OVERALL_DECAY_BASE,
|
||||
);
|
||||
|
||||
individual_decay + overall_decay
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
use std::{cmp::Ordering, mem};
|
||||
|
||||
use crate::{
|
||||
mania::{difficulty_object::ManiaDifficultyObject, SECTION_LEN},
|
||||
util::CompactVec,
|
||||
};
|
||||
|
||||
pub(crate) trait Skill {
|
||||
fn process(&mut self, curr: &ManiaDifficultyObject, diff_objects: &[ManiaDifficultyObject]);
|
||||
fn difficulty_value(self) -> f64;
|
||||
}
|
||||
|
||||
pub(crate) trait StrainSkill: Sized + Skill {
|
||||
const DECAY_WEIGHT: f64 = 0.9;
|
||||
|
||||
fn curr_section_end(&self) -> f64;
|
||||
fn curr_section_end_mut(&mut self) -> &mut f64;
|
||||
|
||||
fn curr_section_peak(&self) -> f64;
|
||||
fn curr_section_peak_mut(&mut self) -> &mut f64;
|
||||
|
||||
fn strain_peaks_mut(&mut self) -> &mut CompactVec;
|
||||
|
||||
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64;
|
||||
|
||||
fn process(&mut self, curr: &ManiaDifficultyObject, diff_objects: &[ManiaDifficultyObject]) {
|
||||
// * The first object doesn't generate a strain, so we begin with an incremented section end
|
||||
if curr.idx == 0 {
|
||||
*self.curr_section_end_mut() = (curr.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
}
|
||||
|
||||
while curr.start_time > self.curr_section_end() {
|
||||
self.save_curr_peak();
|
||||
self.start_new_section_from(self.curr_section_end(), curr, diff_objects);
|
||||
*self.curr_section_end_mut() += SECTION_LEN;
|
||||
|
||||
// Optimization to finish the loop early if
|
||||
// the current peak is 0.0 i.e. it can't decay further.
|
||||
// If final values don't coincide perfectly anymore,
|
||||
// this should be looked at and maybe adjusted.
|
||||
if self.curr_section_peak().abs() <= f64::EPSILON
|
||||
&& curr.start_time > self.curr_section_end()
|
||||
{
|
||||
let remaining_time = curr.start_time - self.curr_section_end();
|
||||
let remaining_iters = (remaining_time / SECTION_LEN).ceil();
|
||||
*self.curr_section_end_mut() += remaining_iters * SECTION_LEN;
|
||||
}
|
||||
}
|
||||
|
||||
*self.curr_section_peak_mut() = self.strain_value_at(curr).max(self.curr_section_peak());
|
||||
}
|
||||
|
||||
fn save_curr_peak(&mut self) {
|
||||
let curr_section_peak = self.curr_section_peak();
|
||||
self.strain_peaks_mut().push(curr_section_peak);
|
||||
}
|
||||
|
||||
fn start_new_section_from(
|
||||
&mut self,
|
||||
time: f64,
|
||||
curr: &ManiaDifficultyObject,
|
||||
diff_objects: &[ManiaDifficultyObject],
|
||||
) {
|
||||
*self.curr_section_peak_mut() = self.calculate_initial_strain(time, curr, diff_objects);
|
||||
}
|
||||
|
||||
fn calculate_initial_strain(
|
||||
&self,
|
||||
time: f64,
|
||||
curr: &ManiaDifficultyObject,
|
||||
diff_objects: &[ManiaDifficultyObject],
|
||||
) -> f64;
|
||||
|
||||
fn get_curr_strain_peaks(mut self) -> CompactVec {
|
||||
let mut peaks = mem::take(self.strain_peaks_mut());
|
||||
peaks.push(self.curr_section_peak());
|
||||
|
||||
peaks
|
||||
}
|
||||
|
||||
fn difficulty_value(self) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
// * Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
// * These sections will not contribute to the difficulty.
|
||||
let mut peaks = self.get_curr_strain_peaks();
|
||||
peaks.retain(|peak| peak > 0.0);
|
||||
let mut peaks = peaks.to_vec();
|
||||
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
// * Difficulty is the weighted sum of the highest strains from every section.
|
||||
// * We're sorting from highest to lowest strain.
|
||||
for strain in peaks {
|
||||
difficulty += strain * weight;
|
||||
weight *= Self::DECAY_WEIGHT;
|
||||
}
|
||||
|
||||
difficulty
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait StrainDecaySkill: StrainSkill {
|
||||
const SKILL_MULTIPLIER: f64;
|
||||
const STRAIN_DECAY_BASE: f64;
|
||||
|
||||
fn curr_strain(&self) -> f64;
|
||||
fn curr_strain_mut(&mut self) -> &mut f64;
|
||||
|
||||
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject) -> f64;
|
||||
|
||||
fn calculate_initial_strain(
|
||||
&self,
|
||||
time: f64,
|
||||
curr: &ManiaDifficultyObject,
|
||||
diff_objects: &[ManiaDifficultyObject],
|
||||
) -> f64;
|
||||
|
||||
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64 {
|
||||
*self.curr_strain_mut() *= self.strain_decay(curr.delta_time);
|
||||
*self.curr_strain_mut() += self.strain_value_of(curr) * Self::SKILL_MULTIPLIER;
|
||||
|
||||
self.curr_strain()
|
||||
}
|
||||
|
||||
fn strain_decay(&self, ms: f64) -> f64 {
|
||||
Self::STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::{any::ModeDifficulty, mania::difficulty::DifficultyValues};
|
||||
|
||||
use super::convert::ManiaBeatmap;
|
||||
|
||||
/// The result of calculating the strains on a osu!mania map.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ManiaStrains {
|
||||
/// Strain peaks of the strain skill.
|
||||
pub strains: Vec<f64>,
|
||||
}
|
||||
|
||||
impl ManiaStrains {
|
||||
/// Time between two strains in ms.
|
||||
pub const SECTION_LEN: f64 = 400.0;
|
||||
}
|
||||
|
||||
pub fn strains(difficulty: &ModeDifficulty, converted: &ManiaBeatmap<'_>) -> ManiaStrains {
|
||||
let values = DifficultyValues::calculate(difficulty, converted);
|
||||
|
||||
ManiaStrains {
|
||||
strains: values.strain.get_curr_strain_peaks(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user