restructured workspace into single crate

This commit is contained in:
MaxOhn
2021-01-15 22:06:51 +01:00
parent ea7ff00c0a
commit 260939a7ec
85 changed files with 777 additions and 29675 deletions
+86
View File
@@ -0,0 +1,86 @@
use super::Mods;
#[derive(Clone, Debug)]
pub struct BeatmapAttributes {
pub ar: f32,
pub od: f32,
pub cs: f32,
pub hp: f32,
pub clock_rate: f32,
}
impl BeatmapAttributes {
const AR0_MS: f32 = 1800.0;
const AR5_MS: f32 = 1200.0;
const AR10_MS: f32 = 450.0;
const AR_MS_STEP_1: f32 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
const AR_MS_STEP_2: f32 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
const OD0_MS: f32 = 80.0;
const OD10_MS: f32 = 20.0;
const OD_MS_STEP: f32 = (Self::OD0_MS - Self::OD10_MS) / 10.0;
#[inline]
pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self {
Self {
ar,
od,
cs,
hp,
clock_rate: 1.0,
}
}
pub fn mods(self, mods: impl Mods) -> Self {
if !mods.change_map() {
return self;
}
let clock_rate = mods.speed();
let multiplier = mods.od_ar_hp_multiplier();
// AR
let mut ar = self.ar * multiplier;
let mut ar_ms = if ar <= 5.0 {
Self::AR0_MS - Self::AR_MS_STEP_1 * ar
} else {
Self::AR5_MS - Self::AR_MS_STEP_2 * (ar - 5.0)
};
ar_ms = ar_ms.max(Self::AR10_MS).min(Self::AR0_MS);
ar_ms /= clock_rate;
ar = if ar_ms > Self::AR5_MS {
(Self::AR0_MS - ar_ms) / Self::AR_MS_STEP_1
} else {
5.0 + (Self::AR5_MS - ar_ms) / Self::AR_MS_STEP_2
};
// OD
let mut od = self.od * multiplier;
let mut od_ms = Self::OD0_MS - (Self::OD_MS_STEP * od).ceil();
od_ms = od_ms.max(Self::OD10_MS).min(Self::OD0_MS);
od_ms /= clock_rate;
od = (Self::OD0_MS - od_ms) / Self::OD_MS_STEP;
// CS
let mut cs = self.cs;
if mods.hr() {
cs *= 1.3;
} else if mods.ez() {
cs *= 0.5;
}
cs = cs.min(10.0);
// HP
let hp = (self.hp * multiplier).min(10.0);
Self {
ar,
od,
cs,
hp,
clock_rate,
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use std::cmp::Ordering;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct TimingPoint {
pub beat_len: f32,
pub bpm: f32,
pub time: f32,
}
impl PartialOrd for TimingPoint {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.time.partial_cmp(&other.time)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct DifficultyPoint {
pub time: f32,
pub speed_multiplier: f32,
}
impl PartialOrd for DifficultyPoint {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.time.partial_cmp(&other.time)
}
}
+94
View File
@@ -0,0 +1,94 @@
use super::OSU_FILE_HEADER;
use std::error::Error as StdError;
use std::fmt;
use std::io::Error as IOError;
use std::num::{ParseFloatError, ParseIntError};
pub type ParseResult<T> = Result<T, ParseError>;
#[derive(Debug)]
pub enum ParseError {
IOError(IOError),
IncorrectFileHeader,
BadLine,
InvalidCurvePoints,
InvalidInteger,
InvalidEffectFlag,
InvalidFloatingPoint,
InvalidMode,
InvalidPathType,
InvalidTimingSignature,
MissingField(usize),
UnsupportedMode,
UnknownHitObjectKind,
NoHitobjects,
UnsortedHitobjects,
UnsortedTimingPoints,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::IOError(_) => f.write_str("IO error"),
Self::IncorrectFileHeader => {
write!(f, "expected `{}` at file begin", OSU_FILE_HEADER)
}
Self::BadLine => f.write_str("line not in `Key:Value` pattern"),
Self::InvalidCurvePoints => f.write_str("invalid curve point"),
Self::InvalidInteger => f.write_str("invalid integer"),
Self::InvalidEffectFlag => f.write_str("invalid effect flag"),
Self::InvalidFloatingPoint => f.write_str("invalid floating-point number"),
Self::InvalidMode => f.write_str("invalid mode"),
Self::InvalidPathType => f.write_str("invalid path type"),
Self::InvalidTimingSignature => f.write_str("invalid timing signature"),
Self::MissingField(line) => write!(f, "missing field on line {}", line),
Self::UnsupportedMode => f.write_str("unsupported osu! mode"),
Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"),
Self::NoHitobjects => f.write_str("beatmap has no hitobjects"),
Self::UnsortedHitobjects => f.write_str("hitobjects are not sorted by time"),
Self::UnsortedTimingPoints => f.write_str("timing points are not sorted by time"),
}
}
}
impl StdError for ParseError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::IOError(inner) => Some(inner),
Self::IncorrectFileHeader => None,
Self::BadLine => None,
Self::InvalidCurvePoints => None,
Self::InvalidInteger => None,
Self::InvalidEffectFlag => None,
Self::InvalidFloatingPoint => None,
Self::InvalidMode => None,
Self::InvalidPathType => None,
Self::InvalidTimingSignature => None,
Self::MissingField(_) => None,
Self::UnsupportedMode => None,
Self::UnknownHitObjectKind => None,
Self::NoHitobjects => None,
Self::UnsortedHitobjects => None,
Self::UnsortedTimingPoints => None,
}
}
}
impl From<IOError> for ParseError {
fn from(other: IOError) -> Self {
Self::IOError(other)
}
}
impl From<ParseIntError> for ParseError {
fn from(_: ParseIntError) -> Self {
Self::InvalidInteger
}
}
impl From<ParseFloatError> for ParseError {
fn from(_: ParseFloatError) -> Self {
Self::InvalidFloatingPoint
}
}
+61
View File
@@ -0,0 +1,61 @@
use super::{PathType, Pos2};
use std::cmp::Ordering;
#[derive(Clone, Debug, PartialEq)]
pub struct HitObject {
pub pos: Pos2,
pub start_time: f32,
pub kind: HitObjectKind,
pub sound: u8,
}
impl HitObject {
#[inline]
pub fn end_time(&self) -> f32 {
match &self.kind {
HitObjectKind::Circle { .. } => self.start_time,
HitObjectKind::Slider { .. } => self.start_time, // wrong but should be unreachable
HitObjectKind::Spinner { end_time } => *end_time,
HitObjectKind::Hold { end_time, .. } => *end_time,
}
}
#[inline]
pub fn is_circle(&self) -> bool {
matches!(self.kind, HitObjectKind::Circle { .. })
}
#[inline]
pub fn is_slider(&self) -> bool {
matches!(self.kind, HitObjectKind::Slider { .. })
}
#[inline]
pub fn is_spinner(&self) -> bool {
matches!(self.kind, HitObjectKind::Spinner { .. })
}
}
impl PartialOrd for HitObject {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.start_time.partial_cmp(&other.start_time)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum HitObjectKind {
Circle,
Slider {
pixel_len: f32,
repeats: usize,
curve_points: Vec<Pos2>,
path_type: PathType,
},
Spinner {
end_time: f32,
},
Hold {
end_time: f32,
},
}
+28
View File
@@ -0,0 +1,28 @@
const HITSOUND_WHISTLE: u8 = 1 << 1;
const HITSOUND_FINISH: u8 = 1 << 2;
const HITSOUND_CLAP: u8 = 1 << 3;
pub trait HitSound {
fn normal(self) -> bool;
fn whistle(self) -> bool;
fn finish(self) -> bool;
fn clap(self) -> bool;
}
impl HitSound for u8 {
fn normal(self) -> bool {
self == 0
}
fn whistle(self) -> bool {
self & HITSOUND_WHISTLE > 0
}
fn finish(self) -> bool {
self & HITSOUND_FINISH > 0
}
fn clap(self) -> bool {
self & HITSOUND_CLAP > 0
}
}
+438
View File
@@ -0,0 +1,438 @@
mod attributes;
mod control_point;
mod error;
mod hitobject;
mod hitsound;
mod mods;
mod pos2;
mod sort;
pub use attributes::BeatmapAttributes;
pub use control_point::{DifficultyPoint, TimingPoint};
pub use error::{ParseError, ParseResult};
pub use hitobject::{HitObject, HitObjectKind};
pub use hitsound::HitSound;
pub use mods::Mods;
pub use pos2::Pos2;
use sort::sort;
use std::cmp::Ordering;
use std::io::{BufRead, BufReader, Read};
use std::str::FromStr;
macro_rules! sort {
($slice:expr) => {
$slice.sort_unstable_by(|p1, p2| p1.partial_cmp(&p2).unwrap_or(Ordering::Equal))
};
}
macro_rules! next_field {
($opt:expr, $nmbr:ident) => {
$opt.ok_or(ParseError::MissingField($nmbr))?
};
}
macro_rules! validate_float {
($x:expr) => {{
if $x.is_finite() {
$x
} else {
return Err(ParseError::InvalidFloatingPoint);
}
}};
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum GameMode {
STD = 0,
TKO = 1,
CTB = 2,
MNA = 3,
}
impl Default for GameMode {
fn default() -> Self {
Self::STD
}
}
#[derive(Clone, Default, Debug)]
pub struct Beatmap {
pub mode: GameMode,
pub version: u8,
pub n_circles: u32,
pub n_sliders: u32,
pub n_spinners: u32,
pub ar: f32,
pub od: f32,
pub cs: f32,
pub hp: f32,
pub sv: f32,
pub tick_rate: f32,
pub stack_leniency: f32,
pub hit_objects: Vec<HitObject>,
pub timing_points: Vec<TimingPoint>,
pub difficulty_points: Vec<DifficultyPoint>,
}
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
impl Beatmap {
const CIRCLE_FLAG: u8 = 1 << 0;
const SLIDER_FLAG: u8 = 1 << 1;
#[allow(unused)]
const NEW_COMBO_FLAG: u8 = 1 << 2;
const SPINNER_FLAG: u8 = 1 << 3;
#[allow(unused)]
const COMBO_OFFSET_FLAG: u8 = (1 << 4) | (1 << 5) | (1 << 6);
const HOLD_FLAG: u8 = 1 << 7;
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
let mut reader = BufReader::new(input);
let mut buf = String::new();
reader.read_line(&mut buf)?;
let mut map = Self::default();
map.version = match buf.find(OSU_FILE_HEADER) {
Some(idx) => buf[idx + OSU_FILE_HEADER.len()..].trim_end().parse()?,
None => return Err(ParseError::IncorrectFileHeader),
};
// version 4 and lower had an incorrect offset (stable has this set as 24ms off)
let offset = if map.version < 5 { 24.0 } else { 0.0 };
buf.clear();
map.hit_objects.reserve(256);
let mut mode = None;
let mut ar = None;
let mut od = None;
let mut cs = None;
let mut hp = None;
let mut sv = None;
let mut tick_rate = None;
let mut stack_leniency = None;
let mut section = Section::None;
let mut prev_time = 0.0;
let mut prev_diff = 0.0;
let mut unsorted_timings = false;
let mut unsorted_difficulties = false;
let mut unsorted_hits = false;
let mut nmbr = 1;
while reader.read_line(&mut buf)? != 0 {
let mut line = buf.trim_end();
nmbr += 1;
if line.is_empty()
|| line.starts_with("//")
|| line.starts_with(' ')
|| line.starts_with('_')
{
buf.clear();
continue;
}
if line.starts_with('[') && line.ends_with(']') {
section = Section::from_str(&line[1..line.len() - 1]);
buf.clear();
continue;
}
if let Some(idx) = line.find("//") {
line = &line[..idx];
}
match section {
Section::General => {
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
if key == "Mode" {
mode = match value {
"0" => Some(GameMode::STD),
"1" => Some(GameMode::TKO),
"2" => Some(GameMode::CTB),
"3" => Some(GameMode::MNA),
_ => return Err(ParseError::InvalidMode),
};
} else if key == "StackLeniency" {
stack_leniency = Some(value.parse()?);
}
}
Section::Difficulty => {
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
match key {
"ApproachRate" => ar = Some(value.parse()?),
"OverallDifficulty" => od = Some(value.parse()?),
"CircleSize" => cs = Some(value.parse()?),
"HPDrainRate" => hp = Some(value.parse()?),
"SliderTickRate" => tick_rate = Some(value.parse()?),
"SliderMultiplier" => sv = Some(value.parse()?),
_ => {}
}
}
Section::TimingPoints => {
let mut split = line.split(',');
let time = offset + next_field!(split.next(), nmbr).trim().parse::<f32>()?;
validate_float!(time);
let beat_len = next_field!(split.next(), nmbr).trim().parse::<f32>()?;
if beat_len.is_sign_negative() {
let point = DifficultyPoint {
time,
speed_multiplier: -100.0 / beat_len,
};
map.difficulty_points.push(point);
if time < prev_diff {
unsorted_difficulties = true;
} else {
prev_diff = time;
}
} else {
let point = TimingPoint {
time,
bpm: 60_000.0 / beat_len,
beat_len,
};
map.timing_points.push(point);
if time < prev_time {
unsorted_timings = true;
} else {
prev_time = time;
}
}
}
Section::HitObjects => {
let mut split = line.split(',');
let pos = Pos2 {
x: next_field!(split.next(), nmbr).parse()?,
y: next_field!(split.next(), nmbr).parse()?,
};
let time = offset + next_field!(split.next(), nmbr).trim().parse::<f32>()?;
validate_float!(time);
if !map.hit_objects.is_empty() && time < prev_time {
unsorted_hits = true;
}
let kind: u8 = next_field!(split.next(), nmbr).parse()?;
let sound = split.next().map(str::parse).transpose()?.unwrap_or(0);
let kind = if kind & Self::CIRCLE_FLAG > 0 {
map.n_circles += 1;
HitObjectKind::Circle
} else if kind & Self::SLIDER_FLAG > 0 {
map.n_sliders += 1;
let mut curve_points = Vec::with_capacity(16);
curve_points.push(pos);
let mut curve_point_iter = next_field!(split.next(), nmbr).split('|');
let mut path_type: PathType =
next_field!(curve_point_iter.next(), nmbr).parse()?;
for pos in curve_point_iter {
let mut v = pos.split(':').map(str::parse);
match (v.next(), v.next()) {
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
_ => return Err(ParseError::InvalidCurvePoints),
}
}
if map.version <= 6 && curve_points.len() >= 2 {
if path_type == PathType::Linear {
path_type = PathType::Bezier;
}
if curve_points.len() == 2
&& (pos == curve_points[0] || pos == curve_points[1])
{
path_type = PathType::Linear;
}
}
if curve_points.is_empty() {
HitObjectKind::Circle
} else {
let repeats = next_field!(split.next(), nmbr).parse::<usize>()?;
let len: f32 = next_field!(split.next(), nmbr).parse()?;
HitObjectKind::Slider {
repeats,
pixel_len: len,
curve_points,
path_type,
}
}
} else if kind & Self::SPINNER_FLAG > 0 {
map.n_spinners += 1;
let end_time = next_field!(split.next(), nmbr).parse()?;
HitObjectKind::Spinner { end_time }
} else if kind & Self::HOLD_FLAG > 0 {
map.n_sliders += 1;
let mut end = time;
if let Some(next) = split.next() {
end = end.max(next_field!(next.split(':').next(), nmbr).parse()?);
}
HitObjectKind::Hold { end_time: end }
} else {
return Err(ParseError::UnknownHitObjectKind);
};
map.hit_objects.push(HitObject {
pos,
start_time: time,
kind,
sound,
});
prev_time = time;
}
Section::None => {}
}
buf.clear();
}
map.mode = next_field!(mode, nmbr);
map.ar = next_field!(ar, nmbr);
map.od = next_field!(od, nmbr);
map.cs = next_field!(cs, nmbr);
map.hp = next_field!(hp, nmbr);
map.sv = next_field!(sv, nmbr);
map.tick_rate = next_field!(tick_rate, nmbr);
map.stack_leniency = next_field!(stack_leniency, nmbr);
if unsorted_timings {
sort!(map.timing_points);
}
if unsorted_difficulties {
sort!(map.difficulty_points);
}
if map.mode == GameMode::MNA {
sort(&mut map.hit_objects);
} else if unsorted_hits {
sort!(map.hit_objects);
}
Ok(map)
}
#[inline]
pub fn attributes(&self) -> BeatmapAttributes {
BeatmapAttributes::new(self.ar, self.od, self.cs, self.hp)
}
}
#[inline]
fn split_colon(line: &str) -> Option<(&str, &str)> {
let mut split = line.split(':');
Some((split.next()?, split.next()?.trim()))
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum PathType {
Catmull = 0,
Bezier = 1,
Linear = 2,
PerfectCurve = 3,
}
impl FromStr for PathType {
type Err = ParseError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"L" => Ok(Self::Linear),
"C" => Ok(Self::Catmull),
"B" => Ok(Self::Bezier),
"P" => Ok(Self::PerfectCurve),
_ => Err(ParseError::InvalidPathType),
}
}
}
#[derive(Copy, Clone)]
enum Section {
None,
General,
Difficulty,
TimingPoints,
HitObjects,
}
impl Section {
#[inline]
fn from_str(s: &str) -> Self {
match s {
"General" => Self::General,
"Difficulty" => Self::Difficulty,
"TimingPoints" => Self::TimingPoints,
"HitObjects" => Self::HitObjects,
_ => Self::None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
fn parsing_works() {
let file = match File::open("E:/Games/osu!/beatmaps/2223745.osu") {
Ok(file) => file,
Err(why) => panic!("Could not read file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
println!("Mode: {}", map.mode as u8);
println!("n_circles: {}", map.n_circles);
println!("n_sliders: {}", map.n_sliders);
println!("n_spinners: {}", map.n_spinners);
println!("ar: {}", map.ar);
println!("od: {}", map.od);
println!("cs: {}", map.cs);
println!("hp: {}", map.hp);
println!("sv: {}", map.sv);
println!("tick_rate: {}", map.tick_rate);
println!("stack_leniency: {}", map.stack_leniency);
println!("hit_objects: {}", map.hit_objects.len());
println!("timing_points: {}", map.timing_points.len());
println!("difficulty_points: {}", map.difficulty_points.len());
assert_eq!(2 + 2, 4);
}
}
+111
View File
@@ -0,0 +1,111 @@
pub trait Mods: Copy {
const NF: u32 = 1 << 0;
const EZ: u32 = 1 << 1;
const TD: u32 = 1 << 2;
const HD: u32 = 1 << 3;
const HR: u32 = 1 << 4;
const DT: u32 = 1 << 6;
const HT: u32 = 1 << 8;
const NC: u32 = Self::DT | (1 << 9);
const FL: u32 = 1 << 10;
const SO: u32 = 1 << 12;
fn change_speed(self) -> bool;
fn change_map(self) -> bool;
fn speed(self) -> f32;
fn od_ar_hp_multiplier(self) -> f32;
fn nf(self) -> bool;
fn ez(self) -> bool;
fn td(self) -> bool;
fn hd(self) -> bool;
fn hr(self) -> bool;
fn dt(self) -> bool;
fn ht(self) -> bool;
fn nc(self) -> bool;
fn fl(self) -> bool;
fn so(self) -> bool;
}
impl Mods for u32 {
#[inline]
fn change_speed(self) -> bool {
self & (Self::HT | Self::DT) > 0
}
#[inline]
fn change_map(self) -> bool {
self & (Self::HT | Self::DT | Self::HR | Self::EZ) > 0
}
#[inline]
fn speed(self) -> f32 {
if self & Self::DT > 0 {
1.5
} else if self & Self::HT > 0 {
0.75
} else {
1.0
}
}
#[inline]
fn od_ar_hp_multiplier(self) -> f32 {
if self & Self::HR > 0 {
1.4
} else if self & Self::EZ > 0 {
0.5
} else {
1.0
}
}
#[inline]
fn nf(self) -> bool {
self & Self::NF > 0
}
#[inline]
fn ez(self) -> bool {
self & Self::EZ > 0
}
#[inline]
fn td(self) -> bool {
self & Self::TD > 0
}
#[inline]
fn hd(self) -> bool {
self & Self::HD > 0
}
#[inline]
fn hr(self) -> bool {
self & Self::HR > 0
}
#[inline]
fn dt(self) -> bool {
self & Self::DT > 0
}
#[inline]
fn ht(self) -> bool {
self & Self::HT > 0
}
#[inline]
fn nc(self) -> bool {
self & Self::NC > 0
}
#[inline]
fn fl(self) -> bool {
self & Self::FL > 0
}
#[inline]
fn so(self) -> bool {
self & Self::SO > 0
}
}
+109
View File
@@ -0,0 +1,109 @@
use std::fmt;
use std::ops;
#[derive(Clone, Copy, Default, PartialEq)]
pub struct Pos2 {
pub x: f32,
pub y: f32,
}
impl Pos2 {
#[inline]
pub fn length_squared(&self) -> f32 {
self.dot(*self)
}
#[inline]
pub fn length(&self) -> f32 {
self.length_squared().sqrt()
}
#[inline]
pub fn dot(&self, other: Self) -> f32 {
self.x * other.x + self.y * other.y
}
#[inline]
pub fn distance(&self, other: &Self) -> f32 {
(*self - *other).length()
}
#[inline]
pub fn add_scaled(self, other: Pos2, factor: f32) -> Pos2 {
self + other * factor
}
#[inline]
pub fn normalize(self) -> Pos2 {
self / self.length()
}
}
impl ops::Add<Pos2> for Pos2 {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self::Output {
Self {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl ops::Sub<Pos2> for Pos2 {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self::Output {
Self {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
}
impl ops::Mul<f32> for Pos2 {
type Output = Self;
#[inline]
fn mul(self, rhs: f32) -> Self::Output {
Self {
x: self.x * rhs,
y: self.y * rhs,
}
}
}
impl ops::Div<f32> for Pos2 {
type Output = Self;
#[inline]
fn div(self, rhs: f32) -> Self::Output {
Self {
x: self.x / rhs,
y: self.y / rhs,
}
}
}
impl ops::AddAssign for Pos2 {
fn add_assign(&mut self, other: Self) {
*self = Self {
x: self.x + other.x,
y: self.y + other.y,
};
}
}
impl fmt::Display for Pos2 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl fmt::Debug for Pos2 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
+118
View File
@@ -0,0 +1,118 @@
use super::HitObject;
use std::cmp::Ordering;
const QUICK_SORT_DEPTH_THRESHOLD: usize = 32;
/// Algorithm from https://github.com/ppy/osu/blob/master/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs#L21
pub(crate) fn sort(keys: &mut [HitObject]) {
if keys.is_empty() {
return;
}
depth_limited_quick_sort(keys, 0, keys.len() - 1, QUICK_SORT_DEPTH_THRESHOLD);
}
fn depth_limited_quick_sort(
keys: &mut [HitObject],
mut left: usize,
mut right: usize,
mut depth_limit: usize,
) {
loop {
if depth_limit == 0 {
heap_sort(keys, left, right);
return;
}
let mut i = left;
let mut j = right;
let mid = i + ((j - i) >> 1);
if keys[i] > keys[mid] {
keys.swap(i, mid);
}
if keys[i] > keys[j] {
keys.swap(i, j);
}
if keys[mid] > keys[j] {
keys.swap(mid, j);
}
loop {
while keys[i] < keys[mid] {
i += 1;
}
while keys[mid] < keys[j] {
j -= 1;
}
match i.cmp(&j) {
Ordering::Less => keys.swap(i, j),
Ordering::Equal => {}
Ordering::Greater => break,
}
i += 1;
j = j.saturating_sub(1);
if i > j {
break;
}
}
depth_limit -= 1;
if j.saturating_sub(left) <= right - i {
if left < j {
depth_limited_quick_sort(keys, left, j, depth_limit);
}
left = i;
} else {
if i < right {
depth_limited_quick_sort(keys, i, right, depth_limit);
}
right = j;
}
if left >= right {
break;
}
}
}
fn heap_sort(keys: &mut [HitObject], lo: usize, hi: usize) {
let n = hi - lo + 1;
for i in (1..=n / 2).rev() {
down_heap(keys, i, n, lo);
}
for i in (2..=n).rev() {
keys.swap(lo, lo + i - 1);
down_heap(keys, 1, i - 1, lo);
}
}
fn down_heap(keys: &mut [HitObject], mut i: usize, n: usize, lo: usize) {
while i <= n / 2 {
let mut child = 2 * i;
if child < n && keys[lo + child - 1] < keys[lo + child] {
child += 1;
}
if keys[lo + i - 1] >= keys[lo + child - 1] {
break;
}
keys.swap(lo + i - 1, lo + child - 1);
i = child;
}
}