various minor adjustments

This commit is contained in:
MaxOhn
2022-10-21 12:40:45 +02:00
parent 335f0a7dde
commit 5a129437eb
27 changed files with 268 additions and 202 deletions
+3
View File
@@ -170,10 +170,12 @@ A large portion of this repository is a port of [osu!lazer](https://github.com/p
- osu!:
- osu!lazer: Commit `85adfc2df7d931164181e145377a6ced8db2bfb3` (Wed Sep 28 18:26:36 2022 +0300)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- [Article](https://osu.ppy.sh/home/news/2022-09-30-changes-to-osu-sr-and-pp)
- taiko:
- osu!lazer: Commit `234c6ac7998fbc6742503e1a589536255554e56a` (Wed Oct 5 20:21:15 2022 +0900)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- [Article](https://osu.ppy.sh/home/news/2022-09-28-changes-to-osu-taiko-sr-and-pp)
- catch: (will be updated on the next rework)
- osu!lazer: -
@@ -182,6 +184,7 @@ A large portion of this repository is a port of [osu!lazer](https://github.com/p
- mania:
- osu!lazer: Commit `7342fb7f51b34533a42bffda89c3d6c569cc69ce` (Tue Oct 11 14:34:50 2022 +0900)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- [Article](https://osu.ppy.sh/home/news/2022-10-09-changes-to-osu-mania-sr-and-pp)
### Accuracy
-2
View File
@@ -89,8 +89,6 @@ impl Beatmap {
}
/// Return the [`TimingPoint`] for the given timestamp.
///
/// If `time` is before the first timing point, `None` is returned.
#[inline]
pub fn timing_point_at(&self, time: f64) -> TimingPoint {
let idx_result = self
+4 -2
View File
@@ -16,8 +16,8 @@ use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
/// let pp_result = CatchPP::new(&map)
/// .mods(8 + 64) // HDDT
/// .combo(1234)
/// .misses(1)
/// .accuracy(98.5)
/// .misses(1)
/// .calculate();
///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
@@ -181,7 +181,8 @@ impl<'map> CatchPP<'map> {
self
}
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
// TODO: adjust this on the next rework
/// Generate the hit results with respect to the given accuracy between `0.0` and `100.0`.
///
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
pub fn accuracy(mut self, mut acc: f64) -> Self {
@@ -438,6 +439,7 @@ impl CatchPPInner {
}
impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
#[inline]
fn from(osu: OsuPP<'map>) -> Self {
let OsuPP {
map,
+1 -46
View File
@@ -217,7 +217,7 @@ pub use mods::Mods;
pub use parse::{ParseError, ParseResult};
pub use util::SortedVec;
/// Provides some additional methods on [`Beatmap`](crate::Beatmap).
/// Provides some additional methods on [`Beatmap`].
pub trait BeatmapExt {
/// Calculate the stars and other attributes of a beatmap which are required for pp calculation.
fn stars(&self) -> AnyStars<'_>;
@@ -509,48 +509,3 @@ impl From<taiko::TaikoPerformanceAttributes> for PerformanceAttributes {
#[cfg(all(feature = "async_tokio", feature = "async_std"))]
compile_error!("Only one of the features `async_tokio` and `async_std` should be enabled");
#[cfg(test)]
mod tests {
use crate::{Beatmap, GameMode, OsuPP, PerformanceAttributes};
#[test]
fn custom() {
let path = "F:/osu!/beatmaps/1028484.osu";
let map = Beatmap::from_path(path).unwrap();
let attrs = match OsuPP::new(&map).mode(GameMode::Taiko).mods(0).calculate() {
PerformanceAttributes::Taiko(attrs) => attrs,
_ => unreachable!(),
};
println!("{attrs:#?}");
// println!(
// "difficulty:\n\
// stamina={}\n\
// rhythm={}\n\
// colour={}\n\
// peak={}\n\
// hit_window={}\n\
// max_combo={}\n\
// stars={}\n\
// performance:\n\
// difficulty={}\n\
// acc={}\n\
// effective={}\n\
// pp={}\n",
// attrs.difficulty.stamina,
// attrs.difficulty.rhythm,
// attrs.difficulty.colour,
// attrs.difficulty.peak,
// attrs.difficulty.hit_window,
// attrs.difficulty.max_combo,
// attrs.difficulty.stars,
// attrs.pp_difficulty,
// attrs.pp_acc,
// attrs.effective_miss_count,
// attrs.pp,
// );
}
}
+9 -1
View File
@@ -135,7 +135,7 @@ impl<'map> ManiaStars<'map> {
let ManiaResult { strain, .. } = calculate_result(self);
ManiaStrains {
section_len: SECTION_LEN * clock_rate, // TODO: clock_rate correct here?
section_len: SECTION_LEN * clock_rate,
strains: strain.strain_peaks,
}
}
@@ -224,6 +224,14 @@ pub struct ManiaDifficultyAttributes {
pub max_combo: usize,
}
impl ManiaDifficultyAttributes {
/// Return the maximum combo.
#[inline]
pub fn max_combo(&self) -> usize {
self.max_combo
}
}
/// The result of a performance calculation on an osu!mania map.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct ManiaPerformanceAttributes {
+4 -6
View File
@@ -5,7 +5,6 @@ use crate::{
Beatmap, DifficultyAttributes, GameMode, HitResultPriority, Mods, OsuPP, PerformanceAttributes,
};
// TODO: update
/// Performance calculator on osu!mania maps.
///
/// # Example
@@ -124,7 +123,7 @@ impl<'map> ManiaPP<'map> {
self
}
/// Specify the accuracy of a play between `0` and `100`.
/// Specify the accuracy of a play between `0.0` and `100.0`.
/// This will be used to generate matching hitresults.
#[inline]
pub fn accuracy(mut self, acc: f64) -> Self {
@@ -252,7 +251,6 @@ impl<'map> ManiaPP<'map> {
let n_misses = self.n_misses.unwrap_or(0);
if let Some(acc) = self.acc {
// TODO: test
let target_total = (acc * (n_objects * 6) as f64).round() as usize;
match (self.n320, self.n300, self.n200, self.n100, self.n50) {
@@ -530,11 +528,11 @@ impl ManiaPpInner {
}
fn compute_difficulty_value(&self) -> f64 {
// Star rating to pp curve
// * Star rating to pp curve
(self.attrs.stars - 0.15).max(0.05).powf(2.2)
// From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
// * From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
* (5.0 * self.calculate_custom_accuracy() - 4.0).max(0.0)
// Length bonus, capped at 1500 notes
// * Length bonus, capped at 1500 notes
* (1.0 + 0.1 * (self.total_hits() / 1500.0).min(1.0))
}
+5 -6
View File
@@ -25,9 +25,8 @@ pub(crate) trait StrainSkill: Sized + Skill {
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64;
fn process(&mut self, curr: &ManiaDifficultyObject, diff_objects: &[ManiaDifficultyObject]) {
// The first object doesn't generate a strain, so we begin with an incremented section end
// * The first object doesn't generate a strain, so we begin with an incremented section end
if curr.idx == 0 {
// currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
*self.curr_section_end_mut() = (curr.start_time / SECTION_LEN).ceil() * SECTION_LEN;
}
@@ -72,14 +71,14 @@ pub(crate) trait StrainSkill: Sized + Skill {
let mut difficulty = 0.0;
let mut weight = 1.0;
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
// * Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// * These sections will not contribute to the difficulty.
let mut peaks = self.get_curr_strain_peaks();
peaks.retain(|&peak| peak > 0.0);
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
// * Difficulty is the weighted sum of the highest strains from every section.
// * We're sorting from highest to lowest strain.
for strain in peaks {
difficulty += strain * weight;
weight *= Self::DECAY_WEIGHT;
+31 -20
View File
@@ -43,10 +43,12 @@ impl Strain {
}
impl Skill for Strain {
#[inline]
fn process(&mut self, curr: &ManiaDifficultyObject, diff_objects: &[ManiaDifficultyObject]) {
<Self as StrainSkill>::process(self, curr, diff_objects)
}
#[inline]
fn difficulty_value(self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
@@ -55,30 +57,37 @@ impl Skill for Strain {
impl StrainSkill for Strain {
const DECAY_WEIGHT: f64 = 0.9;
#[inline]
fn curr_section_end(&self) -> f64 {
self.curr_section_end
}
#[inline]
fn curr_section_end_mut(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
#[inline]
fn curr_section_peak(&self) -> f64 {
self.curr_section_peak
}
#[inline]
fn curr_section_peak_mut(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
#[inline]
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
#[inline]
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject) -> f64 {
<Self as StrainDecaySkill>::strain_value_at(self, curr)
}
#[inline]
fn calculate_initial_strain(
&self,
time: f64,
@@ -93,10 +102,12 @@ impl StrainDecaySkill for Strain {
const SKILL_MULTIPLIER: f64 = 1.0;
const STRAIN_DECAY_BASE: f64 = 1.0;
#[inline]
fn curr_strain(&self) -> f64 {
self.curr_strain
}
#[inline]
fn curr_strain_mut(&mut self) -> &mut f64 {
&mut self.curr_strain
}
@@ -108,19 +119,19 @@ impl StrainDecaySkill for Strain {
let col = mania_curr.base_column;
let mut is_overlapping = false;
// Lowest value we can assume with the current information
// * Lowest value we can assume with the current information
let mut closest_end_time = (end_time - start_time).abs();
// Factor to all additional strains in case something else is held
// * Factor to all additional strains in case something else is held
let mut hold_factor = 1.0;
// Addition to the current note in case it's a hold and has to be released awkwardly
// * Addition to the current note in case it's a hold and has to be released awkwardly
let mut hold_addition = 0.0;
for i in 0..self.end_times.len() {
// The current note is overlapped if a previous note or end is overlapping the current note body
// * The current note is overlapped if a previous note or end is overlapping the current note body
is_overlapping |=
self.end_times[i] > start_time + 1.0 && end_time > self.end_times[i] + 1.0;
// We give a slight bonus to everything if something is held meanwhile
// * We give a slight bonus to everything if something is held meanwhile
if self.end_times[i] > end_time + 1.0 {
hold_factor = 1.25;
}
@@ -128,22 +139,22 @@ impl StrainDecaySkill for Strain {
closest_end_time = (end_time - self.end_times[i]).abs().min(closest_end_time);
}
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition
// ^
// 1.0 + - - - - - -+-----------
// | /
// 0.5 + - - - - -/ Sigmoid Curve
// | /|
// 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold
// * The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// * Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// * holdAddition
// * ^
// * 1.0 + - - - - - -+-----------
// * | /
// * 0.5 + - - - - -/ Sigmoid Curve
// * | /|
// * 0.0 +--------+-+---------------> Release Difference / ms
// * release_threshold
if is_overlapping {
hold_addition =
(1.0 + (0.5 * (Self::RELEASE_THRESHOLD - closest_end_time)).exp()).recip();
}
// Decay and increase individualStrains in own column
// * Decay and increase individualStrains in own column
self.individual_strains[col] = Self::apply_decay(
self.individual_strains[col],
start_time - self.start_times[col],
@@ -151,14 +162,14 @@ impl StrainDecaySkill for Strain {
);
self.individual_strains[col] += 2.0 * hold_factor;
// For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
// * For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
self.individual_strain = if mania_curr.delta_time <= 1.0 {
self.individual_strain.max(self.individual_strains[col])
} else {
self.individual_strains[col]
};
// Decay and increase overallStrain
// * Decay and increase overallStrain
self.overall_strain = Self::apply_decay(
self.overall_strain,
curr.delta_time,
@@ -166,11 +177,11 @@ impl StrainDecaySkill for Strain {
);
self.overall_strain += (1.0 + hold_addition) * hold_factor;
// Update startTimes and endTimes arrays
// * Update startTimes and endTimes arrays
self.start_times[col] = start_time;
self.end_times[col] = end_time;
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
// * By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
self.individual_strain + self.overall_strain - self.curr_strain
}
+4 -5
View File
@@ -65,6 +65,7 @@ impl Debug for OsuGradualDifficultyAttributes {
.field("attrs", &self.attrs)
.field("diff_objects", &self.diff_objects)
.field("skills", &"<cannot be displayed>")
.field("hit_window", &self.hit_window)
.finish()
}
}
@@ -107,13 +108,11 @@ impl OsuGradualDifficultyAttributes {
curve_bufs: CurveBuffers::default(),
};
let hit_objects_iter = map
let mut hit_objects: Vec<_> = map
.hit_objects
.iter()
.filter_map(|h| OsuObject::new(h, &mut params));
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
hit_objects.extend(hit_objects_iter);
.map(|h| OsuObject::new(h, &mut params))
.collect();
attrs.n_circles = 0;
attrs.n_sliders = 0;
+3 -5
View File
@@ -274,14 +274,12 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
curve_bufs: CurveBuffers::default(),
};
let hit_objects_iter = map
let mut hit_objects: Vec<_> = map
.hit_objects
.iter()
.take(take)
.filter_map(|h| OsuObject::new(h, &mut params));
let mut hit_objects = Vec::with_capacity(take.min(map.hit_objects.len()));
hit_objects.extend(hit_objects_iter);
.map(|h| OsuObject::new(h, &mut params))
.collect();
let stack_threshold = time_preempt * map.stack_leniency as f64;
+4 -7
View File
@@ -92,7 +92,7 @@ pub(crate) struct ObjectParameters<'a> {
}
impl OsuObject {
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Self {
let ObjectParameters {
map,
attrs,
@@ -103,7 +103,7 @@ impl OsuObject {
attrs.max_combo += 1; // hitcircle, slider head, or spinner
let pos = h.pos;
let obj = match &h.kind {
match &h.kind {
HitObjectKind::Circle => {
attrs.n_circles += 1;
@@ -317,7 +317,7 @@ impl OsuObject {
kind: OsuObjectKind::Slider(slider),
}
}
HitObjectKind::Spinner { end_time } => {
HitObjectKind::Spinner { end_time } | HitObjectKind::Hold { end_time } => {
attrs.n_spinners += 1;
Self {
@@ -330,10 +330,7 @@ impl OsuObject {
},
}
}
HitObjectKind::Hold { .. } => return None,
};
Some(obj)
}
}
pub(crate) fn end_time(&self) -> f64 {
+2 -2
View File
@@ -21,8 +21,8 @@ use crate::{
/// let pp_result = OsuPP::new(&map)
/// .mods(8 + 64) // HDDT
/// .combo(1234)
/// .accuracy(98.5)
/// .n_misses(1)
/// .accuracy(98.5) // should be set last
/// .calculate();
///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
@@ -199,7 +199,7 @@ impl<'map> OsuPP<'map> {
self
}
/// Specify the accuracy of a play between `0` and `100`.
/// Specify the accuracy of a play between `0.0` and `100.0`.
/// This will be used to generate matching hitresults.
#[inline]
pub fn accuracy(mut self, acc: f64) -> Self {
+11
View File
@@ -37,6 +37,7 @@ impl Aim {
}
impl Skill for Aim {
#[inline]
fn process(
&mut self,
curr: &OsuDifficultyObject<'_>,
@@ -46,36 +47,44 @@ impl Skill for Aim {
<Self as StrainSkill>::process(self, curr, diff_objects, hit_window)
}
#[inline]
fn difficulty_value(&mut self) -> f64 {
<Self as OsuStrainSkill>::difficulty_value(self)
}
#[inline]
fn as_any(&self) -> &dyn Any {
self
}
#[inline]
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
#[inline]
fn take_strain_peaks(&mut self) -> Vec<f64> {
mem::take(&mut self.strain_peaks)
}
}
impl StrainSkill for Aim {
#[inline]
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
#[inline]
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
#[inline]
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
#[inline]
fn strain_value_at(
&mut self,
curr: &OsuDifficultyObject<'_>,
@@ -89,6 +98,7 @@ impl StrainSkill for Aim {
self.curr_strain
}
#[inline]
fn calculate_initial_strain(
&self,
time: f64,
@@ -98,6 +108,7 @@ impl StrainSkill for Aim {
self.curr_strain * Self::strain_decay(time - previous_start_time(diff_objects, curr.idx, 0))
}
#[inline]
fn difficulty_value(&mut self) -> f64 {
<Self as OsuStrainSkill>::difficulty_value(self)
}
+11
View File
@@ -42,6 +42,7 @@ impl Flashlight {
}
impl Skill for Flashlight {
#[inline]
fn process(
&mut self,
curr: &OsuDifficultyObject<'_>,
@@ -51,18 +52,22 @@ impl Skill for Flashlight {
<Self as StrainSkill>::process(self, curr, diff_objects, hit_window)
}
#[inline]
fn difficulty_value(&mut self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
#[inline]
fn as_any(&self) -> &dyn Any {
self
}
#[inline]
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
#[inline]
fn take_strain_peaks(&mut self) -> Vec<f64> {
mem::take(&mut self.strain_peaks)
}
@@ -71,18 +76,22 @@ impl Skill for Flashlight {
impl StrainSkill for Flashlight {
const DECAY_WEIGHT: f64 = 0.9;
#[inline]
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
#[inline]
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
#[inline]
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
#[inline]
fn strain_value_at(
&mut self,
curr: &OsuDifficultyObject<'_>,
@@ -102,6 +111,7 @@ impl StrainSkill for Flashlight {
self.curr_strain
}
#[inline]
fn calculate_initial_strain(
&self,
time: f64,
@@ -111,6 +121,7 @@ impl StrainSkill for Flashlight {
self.curr_strain * Self::strain_decay(time - previous_start_time(diff_objects, curr.idx, 0))
}
#[inline]
fn difficulty_value(&mut self) -> f64 {
self.get_curr_strain_peaks().into_iter().sum::<f64>() * Self::DIFFICULTY_MULTIPLER
}
+3
View File
@@ -70,11 +70,13 @@ pub(crate) trait StrainSkill: Skill + Sized {
.max(*self.curr_section_peak());
}
#[inline]
fn save_curr_peak(&mut self) {
let peak = *self.curr_section_peak();
self.strain_peaks_mut().push(peak);
}
#[inline]
fn start_new_section_from(
&mut self,
time: f64,
@@ -89,6 +91,7 @@ pub(crate) trait StrainSkill: Skill + Sized {
fn difficulty_value(&mut self) -> f64;
#[inline]
fn get_curr_strain_peaks(&mut self) -> Vec<f64> {
let curr_peak = *self.curr_section_peak();
let mut strain_peaks = mem::take(self.strain_peaks_mut());
+11
View File
@@ -48,6 +48,7 @@ impl Speed {
}
impl Skill for Speed {
#[inline]
fn process(
&mut self,
curr: &OsuDifficultyObject<'_>,
@@ -57,36 +58,44 @@ impl Skill for Speed {
<Self as StrainSkill>::process(self, curr, diff_objects, hit_window)
}
#[inline]
fn difficulty_value(&mut self) -> f64 {
<Self as OsuStrainSkill>::difficulty_value(self)
}
#[inline]
fn as_any(&self) -> &dyn Any {
self
}
#[inline]
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
#[inline]
fn take_strain_peaks(&mut self) -> Vec<f64> {
mem::take(&mut self.strain_peaks)
}
}
impl StrainSkill for Speed {
#[inline]
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
#[inline]
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
#[inline]
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
#[inline]
fn strain_value_at(
&mut self,
curr: &OsuDifficultyObject<'_>,
@@ -104,6 +113,7 @@ impl StrainSkill for Speed {
total_strain
}
#[inline]
fn calculate_initial_strain(
&self,
time: f64,
@@ -114,6 +124,7 @@ impl StrainSkill for Speed {
* Self::strain_decay(time - previous_start_time(diff_objects, curr.idx, 0))
}
#[inline]
fn difficulty_value(&mut self) -> f64 {
<Self as OsuStrainSkill>::difficulty_value(self)
}
+2 -2
View File
@@ -19,7 +19,7 @@ impl HitObject {
#[inline]
pub fn end_time(&self) -> f64 {
match &self.kind {
HitObjectKind::Circle { .. } => self.start_time,
HitObjectKind::Circle => self.start_time,
// incorrect, only called in mania which has no sliders though
HitObjectKind::Slider { .. } => self.start_time,
HitObjectKind::Spinner { end_time } => *end_time,
@@ -30,7 +30,7 @@ impl HitObject {
/// If the object is a circle.
#[inline]
pub fn is_circle(&self) -> bool {
matches!(self.kind, HitObjectKind::Circle { .. })
matches!(self.kind, HitObjectKind::Circle)
}
/// If the object is a slider.
+4
View File
@@ -12,18 +12,22 @@ pub trait HitSound {
}
impl HitSound for u8 {
#[inline]
fn normal(self) -> bool {
self == 0
}
#[inline]
fn whistle(self) -> bool {
self & Self::HITSOUND_WHISTLE > 0
}
#[inline]
fn finish(self) -> bool {
self & Self::HITSOUND_FINISH > 0
}
#[inline]
fn clap(self) -> bool {
self & Self::HITSOUND_CLAP > 0
}
+11 -89
View File
@@ -689,7 +689,6 @@ macro_rules! parse_hitobjects_body {
sorter.toggle_marks();
sorter.sort(&mut $self.sounds);
}
// No need to sort hitsounds for the rest
GameMode::Mania => {
// First a _stable_ sort by time
$self
@@ -975,6 +974,7 @@ impl Beatmap {
) -> ParseResult<bool> {
parse_timingpoints_body!(self, reader, section)
}
/// Pass the path to a `.osu` file.
///
/// Useful when you don't want to create the [`File`](std::fs::File) manually.
@@ -983,6 +983,11 @@ impl Beatmap {
pub fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
Self::parse(File::open(path)?)
}
/// Parse the content of a `.osu` file in form of a slice of bytes into a beatmap.
pub fn from_bytes(bytes: &[u8]) -> ParseResult<Self> {
Self::parse(bytes)
}
}
#[cfg(any(feature = "async_tokio", feature = "async_std"))]
@@ -1043,6 +1048,11 @@ impl Beatmap {
pub async fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
Self::parse(File::open(path).await?).await
}
/// Parse the content of a `.osu` file in form of a slice of bytes into a beatmap.
pub async fn from_bytes(bytes: &[u8]) -> ParseResult<Self> {
Self::parse(bytes).await
}
}
#[derive(Copy, Clone, Debug)]
@@ -1067,91 +1077,3 @@ impl Section {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
#[test]
fn parsing_sync() {
for map_id in map_ids() {
println!("map_id: {}", map_id);
let map = match Beatmap::from_path(format!("./maps/{}.osu", map_id)) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
print_info(map);
println!("---");
}
}
#[cfg(feature = "async_tokio")]
#[test]
fn parsing_async_tokio() {
use tokio::runtime::Builder;
Builder::new_current_thread()
.build()
.expect("could not start runtime")
.block_on(async {
for map_id in map_ids() {
println!("map_id: {}", map_id);
let map = match Beatmap::from_path(format!("./maps/{}.osu", map_id)).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
print_info(map);
println!("---");
}
});
}
#[cfg(feature = "async_std")]
#[test]
fn parsing_async_std() {
async_std::task::block_on(async {
for map_id in map_ids() {
println!("map_id: {}", map_id);
let map = match Beatmap::from_path(format!("./maps/{}.osu", map_id)).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
print_info(map);
println!("---");
}
});
}
fn map_ids() -> Vec<i32> {
vec![
2785319, // osu
1974394, // mania
2118524, // catch
1028484, // taiko
]
}
fn print_info(map: Beatmap) {
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!("slider_mult: {}", map.slider_mult);
println!("tick_rate: {}", map.tick_rate);
println!("hit_objects: {}", map.hit_objects.len());
println!("stack_leniency: {}", map.stack_leniency);
println!("timing_points: {}", map.timing_points.len());
println!("difficulty_points: {}", map.difficulty_points.len());
}
}
+4 -7
View File
@@ -21,15 +21,15 @@ use crate::{
/// let pp_result = AnyPP::new(&map)
/// .mods(8 + 64) // HDDT
/// .combo(1234)
/// .accuracy(98.5)
/// .n_misses(1)
/// .accuracy(98.5) // should be set last
/// .calculate();
///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
///
/// let next_result = AnyPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .accuracy(99.5)
/// .calculate();
///
@@ -151,10 +151,7 @@ impl<'map> AnyPP<'map> {
}
}
/// Set the accuracy between 0.0 and 100.0.
///
/// For some modes this method depends on previously set values.
/// Be sure to call this last before calling `calculate`.
/// Set the accuracy between `0.0` and `100.0`.
#[inline]
pub fn accuracy(self, acc: f64) -> Self {
match self {
+2 -2
View File
@@ -20,8 +20,8 @@ use crate::{
/// let pp_result = TaikoPP::new(&map)
/// .mods(8 + 64) // HDDT
/// .combo(1234)
/// .n_misses(1)
/// .accuracy(98.5)
/// .n_misses(1)
/// .calculate();
///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
@@ -134,7 +134,7 @@ impl<'map> TaikoPP<'map> {
self
}
/// Specify the accuracy of a play between `0` and `100`.
/// Specify the accuracy of a play between `0.0` and `100.0`.
/// This will be used to generate matching hitresults.
#[inline]
pub fn accuracy(mut self, acc: f64) -> Self {
+10
View File
@@ -30,32 +30,39 @@ impl Colour {
}
impl Skill for Colour {
#[inline]
fn process(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) {
<Self as StrainSkill>::process(self, curr, hit_objects)
}
#[inline]
fn difficulty_value(self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
}
impl StrainSkill for Colour {
#[inline]
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
#[inline]
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
#[inline]
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
#[inline]
fn strain_value_at(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) -> f64 {
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
}
#[inline]
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject) -> f64 {
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
}
@@ -65,14 +72,17 @@ impl StrainDecaySkill for Colour {
const SKILL_MULTIPLIER: f64 = 0.12;
const STRAIN_DECAY_BASE: f64 = 0.8;
#[inline]
fn curr_strain(&self) -> f64 {
self.curr_strain
}
#[inline]
fn curr_strain_mut(&mut self) -> &mut f64 {
&mut self.curr_strain
}
#[inline]
fn strain_value_of(&mut self, curr: &TaikoDifficultyObject, _: &ObjectLists) -> f64 {
ColourEvaluator::evaluate_diff_of(curr)
}
+6
View File
@@ -51,11 +51,13 @@ pub(crate) trait StrainSkill: Skill {
.max(*self.curr_section_peak());
}
#[inline]
fn save_curr_peak(&mut self) {
let peak = *self.curr_section_peak();
self.strain_peaks_mut().push(peak);
}
#[inline]
fn start_new_section_from(&mut self, time: f64, curr: &TaikoDifficultyObject) {
// * The maximum strain of the new section is not zero by default
// * This means we need to capture the strain level at the beginning of the new section,
@@ -84,6 +86,7 @@ pub(crate) trait StrainSkill: Skill {
difficulty
}
#[inline]
fn get_curr_strain_peaks(mut self) -> Vec<f64> {
let curr_peak = *self.curr_section_peak();
let mut strain_peaks = mem::take(self.strain_peaks_mut());
@@ -102,10 +105,12 @@ pub(crate) trait StrainDecaySkill: StrainSkill {
fn strain_value_of(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) -> f64;
#[inline]
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject) -> f64 {
self.curr_strain() * self.strain_decay(time - curr.prev_time)
}
#[inline]
fn strain_value_at(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) -> f64 {
*self.curr_strain_mut() *= self.strain_decay(curr.delta);
*self.curr_strain_mut() += self.strain_value_of(curr, hit_objects) * Self::SKILL_MULTIPLIER;
@@ -113,6 +118,7 @@ pub(crate) trait StrainDecaySkill: StrainSkill {
self.curr_strain()
}
#[inline]
fn strain_decay(&self, ms: f64) -> f64 {
Self::STRAIN_DECAY_BASE.powf(ms / 1000.0)
}
+1
View File
@@ -59,6 +59,7 @@ impl Peaks {
}
impl Skill for Peaks {
#[inline]
fn process(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) {
<Colour as Skill>::process(&mut self.colour, curr, hit_objects);
<Rhythm as Skill>::process(&mut self.rhythm, curr, hit_objects);
+9
View File
@@ -100,32 +100,39 @@ impl Rhythm {
}
impl Skill for Rhythm {
#[inline]
fn process(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) {
<Self as StrainSkill>::process(self, curr, hit_objects)
}
#[inline]
fn difficulty_value(self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
}
impl StrainSkill for Rhythm {
#[inline]
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
#[inline]
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
#[inline]
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
#[inline]
fn strain_value_at(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) -> f64 {
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
}
#[inline]
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject) -> f64 {
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
}
@@ -135,10 +142,12 @@ impl StrainDecaySkill for Rhythm {
const SKILL_MULTIPLIER: f64 = 10.0;
const STRAIN_DECAY_BASE: f64 = 0.0;
#[inline]
fn curr_strain(&self) -> f64 {
self.curr_decay_strain
}
#[inline]
fn curr_strain_mut(&mut self) -> &mut f64 {
&mut self.curr_decay_strain
}
+10
View File
@@ -22,32 +22,39 @@ impl Stamina {
}
impl Skill for Stamina {
#[inline]
fn process(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) {
<Self as StrainSkill>::process(self, curr, hit_objects)
}
#[inline]
fn difficulty_value(self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
}
impl StrainSkill for Stamina {
#[inline]
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
#[inline]
fn curr_section_peak(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
#[inline]
fn curr_section_end(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
#[inline]
fn strain_value_at(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) -> f64 {
<Self as StrainDecaySkill>::strain_value_at(self, curr, hit_objects)
}
#[inline]
fn calculate_initial_strain(&self, time: f64, curr: &TaikoDifficultyObject) -> f64 {
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr)
}
@@ -57,14 +64,17 @@ impl StrainDecaySkill for Stamina {
const SKILL_MULTIPLIER: f64 = 1.1;
const STRAIN_DECAY_BASE: f64 = 0.4;
#[inline]
fn curr_strain(&self) -> f64 {
self.curr_strain
}
#[inline]
fn curr_strain_mut(&mut self) -> &mut f64 {
&mut self.curr_strain
}
#[inline]
fn strain_value_of(&mut self, curr: &TaikoDifficultyObject, hit_objects: &ObjectLists) -> f64 {
StaminaEvaluator::evaluate_diff_of(curr, hit_objects)
}
+103
View File
@@ -0,0 +1,103 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::GameMode;
use crate::common::{Catch, Mania, Osu, Taiko};
mod common;
#[test]
fn parse_osu() {
let map = test_map!(Osu);
assert_eq!(map.mode, GameMode::Osu);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 307);
assert_eq!(map.n_sliders, 293);
assert_eq!(map.n_spinners, 1);
assert!((map.ar - 9.3).abs() <= f32::EPSILON);
assert!((map.od - 8.8).abs() <= f32::EPSILON);
assert!((map.cs - 4.5).abs() <= f32::EPSILON);
assert!((map.hp - 5.0).abs() <= f32::EPSILON);
assert!((map.slider_mult - 1.7).abs() <= f64::EPSILON);
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
assert_eq!(map.hit_objects.len(), 601);
assert_eq!(map.sounds.len(), 601);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 50);
assert_eq!(map.effect_points.len(), 131);
assert!((map.stack_leniency - 0.5).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 1)
}
#[test]
fn parse_taiko() {
let map = test_map!(Taiko);
assert_eq!(map.mode, GameMode::Taiko);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 289);
assert_eq!(map.n_sliders, 4);
assert_eq!(map.n_spinners, 2);
assert!((map.ar - 8.0).abs() <= f32::EPSILON);
assert!((map.od - 5.0).abs() <= f32::EPSILON);
assert!((map.cs - 2.0).abs() <= f32::EPSILON);
assert!((map.hp - 6.0).abs() <= f32::EPSILON);
assert!((map.slider_mult - 1.4).abs() <= f64::EPSILON);
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
assert_eq!(map.hit_objects.len(), 295);
assert_eq!(map.sounds.len(), 295);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 3);
assert_eq!(map.effect_points.len(), 8);
assert!((map.stack_leniency - 0.7).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 0)
}
#[test]
fn parse_catch() {
let map = test_map!(Catch);
assert_eq!(map.mode, GameMode::Catch);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 249);
assert_eq!(map.n_sliders, 227);
assert_eq!(map.n_spinners, 1);
assert!((map.ar - 8.0).abs() <= f32::EPSILON);
assert!((map.od - 8.0).abs() <= f32::EPSILON);
assert!((map.cs - 3.5).abs() <= f32::EPSILON);
assert!((map.hp - 5.0).abs() <= f32::EPSILON);
assert!((map.slider_mult - 1.45).abs() <= f64::EPSILON);
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
assert_eq!(map.hit_objects.len(), 477);
assert_eq!(map.sounds.len(), 477);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 0);
assert_eq!(map.effect_points.len(), 57);
assert!((map.stack_leniency - 0.7).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 0)
}
#[test]
fn parse_mania() {
let map = test_map!(Mania);
assert_eq!(map.mode, GameMode::Mania);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 2815);
assert_eq!(map.n_sliders, 423);
assert_eq!(map.n_spinners, 0);
assert!((map.ar - 5.0).abs() <= f32::EPSILON);
assert!((map.od - 8.0).abs() <= f32::EPSILON);
assert!((map.cs - 4.0).abs() <= f32::EPSILON);
assert!((map.hp - 9.0).abs() <= f32::EPSILON);
assert!((map.slider_mult - 1.4).abs() <= f64::EPSILON);
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
assert_eq!(map.hit_objects.len(), 3238);
assert_eq!(map.sounds.len(), 3238);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 1740);
assert_eq!(map.effect_points.len(), 1762);
assert!((map.stack_leniency - 0.7).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 0)
}