initial rewrite

This commit is contained in:
MaxOhn
2024-02-09 17:54:14 +01:00
parent ecf1b2c7f4
commit a8bf9601b6
190 changed files with 10892 additions and 18142 deletions
+257
View File
@@ -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);
}
}
+191
View File
@@ -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
}
}
+106
View File
@@ -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
}
}
+100
View File
@@ -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)
}
}