restructured workspace into single crate
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user