initial rewrite
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user