feat!: overhaul convert api
This commit is contained in:
@@ -1,114 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fmt::{Debug, Formatter, Result as FmtResult},
|
|
||||||
marker::PhantomData,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
model::{beatmap::Converted, mode::IGameMode},
|
|
||||||
util::generic_fmt::GenericFormatter,
|
|
||||||
Difficulty,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Difficulty calculator on maps of a given mode.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use rosu_pp::{Beatmap, Difficulty};
|
|
||||||
/// use rosu_pp::catch::{Catch, CatchDifficultyAttributes};
|
|
||||||
///
|
|
||||||
/// let converted = Beatmap::from_path("./resources/2118524.osu")
|
|
||||||
/// .unwrap()
|
|
||||||
/// .unchecked_into_converted();
|
|
||||||
///
|
|
||||||
/// let attrs: CatchDifficultyAttributes = Difficulty::new()
|
|
||||||
/// .mods(8 + 1024) // HDFL
|
|
||||||
/// .with_mode::<Catch>() // -> `ConvertedDifficulty`
|
|
||||||
/// .calculate(&converted);
|
|
||||||
/// ```
|
|
||||||
#[must_use]
|
|
||||||
pub struct ConvertedDifficulty<'a, M> {
|
|
||||||
inner: &'a Difficulty,
|
|
||||||
_mode: PhantomData<M>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, M> ConvertedDifficulty<'a, M> {
|
|
||||||
pub(crate) const fn new(difficulty: &'a Difficulty) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: difficulty,
|
|
||||||
_mode: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the internal [`Difficulty`].
|
|
||||||
pub const fn inner(self) -> &'a Difficulty {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cast from generic mode `M` to `N`.
|
|
||||||
pub const fn cast<N: IGameMode>(self) -> ConvertedDifficulty<'a, N> {
|
|
||||||
ConvertedDifficulty {
|
|
||||||
inner: self.inner,
|
|
||||||
_mode: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M: IGameMode> ConvertedDifficulty<'_, M> {
|
|
||||||
/// Perform the difficulty calculation for a [`Converted`] beatmap and
|
|
||||||
/// process the final skill values.
|
|
||||||
pub fn calculate(self, map: &Converted<'_, M>) -> M::DifficultyAttributes {
|
|
||||||
M::difficulty(self.inner, map)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform a difficulty calculation for a [`Converted`] beatmap without
|
|
||||||
/// processing the final skill values.
|
|
||||||
pub fn strains(self, map: &Converted<'_, M>) -> M::Strains {
|
|
||||||
M::strains(self.inner, map)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a gradual difficulty calculator for a [`Converted`] beatmap.
|
|
||||||
pub fn gradual_difficulty(self, map: &Converted<'_, M>) -> M::GradualDifficulty {
|
|
||||||
M::gradual_difficulty(self.inner.to_owned(), map)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a gradual performance calculator for a [`Converted`] beatmap.
|
|
||||||
pub fn gradual_performance(self, map: &Converted<'_, M>) -> M::GradualPerformance {
|
|
||||||
M::gradual_performance(self.inner.to_owned(), map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, M: IGameMode> From<&'a Difficulty> for ConvertedDifficulty<'a, M> {
|
|
||||||
fn from(difficulty: &'a Difficulty) -> Self {
|
|
||||||
Self::new(difficulty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> AsRef<Difficulty> for ConvertedDifficulty<'_, M> {
|
|
||||||
fn as_ref(&self) -> &Difficulty {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> Copy for ConvertedDifficulty<'_, M> {}
|
|
||||||
|
|
||||||
impl<M> Clone for ConvertedDifficulty<'_, M> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
*self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> Debug for ConvertedDifficulty<'_, M> {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
||||||
f.debug_struct("ConvertedDifficulty")
|
|
||||||
.field("inner", self.inner)
|
|
||||||
.field("mode", &GenericFormatter::<M>::new())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> PartialEq for ConvertedDifficulty<'_, M> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.inner == other.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use rosu_map::section::general::GameMode;
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::DifficultyAttributes,
|
any::DifficultyAttributes,
|
||||||
catch::{Catch, CatchBeatmap, CatchGradualDifficulty},
|
catch::{Catch, CatchGradualDifficulty},
|
||||||
mania::{Mania, ManiaBeatmap, ManiaGradualDifficulty},
|
mania::{Mania, ManiaGradualDifficulty},
|
||||||
model::mode::IGameMode,
|
model::mode::{ConvertError, IGameMode},
|
||||||
osu::{Osu, OsuBeatmap, OsuGradualDifficulty},
|
osu::{Osu, OsuGradualDifficulty},
|
||||||
taiko::{Taiko, TaikoBeatmap, TaikoGradualDifficulty},
|
taiko::{Taiko, TaikoGradualDifficulty},
|
||||||
Beatmap, Converted, Difficulty,
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gradually calculate the difficulty attributes on maps of any mode.
|
/// Gradually calculate the difficulty attributes on maps of any mode.
|
||||||
@@ -51,38 +49,26 @@ pub enum GradualDifficulty {
|
|||||||
Mania(ManiaGradualDifficulty),
|
Mania(ManiaGradualDifficulty),
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! from_converted {
|
|
||||||
( $fn:ident, $mode:ident, $converted:ident ) => {
|
|
||||||
#[doc = concat!("Create a [`GradualDifficulty`] for a [`", stringify!($converted), "`]")]
|
|
||||||
pub fn $fn(difficulty: Difficulty, converted: &$converted<'_>) -> Self {
|
|
||||||
Self::$mode($mode::gradual_difficulty(difficulty, converted))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GradualDifficulty {
|
impl GradualDifficulty {
|
||||||
/// Create a [`GradualDifficulty`] for a map of any mode.
|
/// Create a [`GradualDifficulty`] for a map of any mode.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Self {
|
||||||
let map = Cow::Borrowed(map);
|
Self::new_with_mode(difficulty, map, map.mode).expect("no conversion required")
|
||||||
|
|
||||||
match map.mode {
|
|
||||||
GameMode::Osu => Self::Osu(Osu::gradual_difficulty(difficulty, &Converted::new(map))),
|
|
||||||
GameMode::Taiko => {
|
|
||||||
Self::Taiko(Taiko::gradual_difficulty(difficulty, &Converted::new(map)))
|
|
||||||
}
|
|
||||||
GameMode::Catch => {
|
|
||||||
Self::Catch(Catch::gradual_difficulty(difficulty, &Converted::new(map)))
|
|
||||||
}
|
|
||||||
GameMode::Mania => {
|
|
||||||
Self::Mania(Mania::gradual_difficulty(difficulty, &Converted::new(map)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
from_converted!(from_osu_map, Osu, OsuBeatmap);
|
/// Create a [`GradualDifficulty`] for a [`Beatmap`] on a specific [`GameMode`].
|
||||||
from_converted!(from_taiko_map, Taiko, TaikoBeatmap);
|
pub fn new_with_mode(
|
||||||
from_converted!(from_catch_map, Catch, CatchBeatmap);
|
difficulty: Difficulty,
|
||||||
from_converted!(from_mania_map, Mania, ManiaBeatmap);
|
map: &Beatmap,
|
||||||
|
mode: GameMode,
|
||||||
|
) -> Result<Self, ConvertError> {
|
||||||
|
match mode {
|
||||||
|
GameMode::Osu => Osu::gradual_difficulty(difficulty, map).map(Self::Osu),
|
||||||
|
GameMode::Taiko => Taiko::gradual_difficulty(difficulty, map).map(Self::Taiko),
|
||||||
|
GameMode::Catch => Catch::gradual_difficulty(difficulty, map).map(Self::Catch),
|
||||||
|
GameMode::Mania => Mania::gradual_difficulty(difficulty, map).map(Self::Mania),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for GradualDifficulty {
|
impl Iterator for GradualDifficulty {
|
||||||
|
|||||||
+57
-34
@@ -1,5 +1,4 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
|
||||||
fmt::{Debug, Formatter, Result as FmtResult},
|
fmt::{Debug, Formatter, Result as FmtResult},
|
||||||
num::NonZeroU64,
|
num::NonZeroU64,
|
||||||
};
|
};
|
||||||
@@ -9,20 +8,14 @@ use rosu_map::section::general::GameMode;
|
|||||||
use crate::{
|
use crate::{
|
||||||
catch::Catch,
|
catch::Catch,
|
||||||
mania::Mania,
|
mania::Mania,
|
||||||
model::{
|
model::{beatmap::Beatmap, mode::ConvertError, mods::GameMods},
|
||||||
beatmap::{Beatmap, Converted},
|
|
||||||
mods::GameMods,
|
|
||||||
},
|
|
||||||
osu::Osu,
|
osu::Osu,
|
||||||
taiko::Taiko,
|
taiko::Taiko,
|
||||||
GradualDifficulty, GradualPerformance,
|
GradualDifficulty, GradualPerformance,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::converted::ConvertedDifficulty;
|
|
||||||
|
|
||||||
use super::{attributes::DifficultyAttributes, InspectDifficulty, Strains};
|
use super::{attributes::DifficultyAttributes, InspectDifficulty, Strains};
|
||||||
|
|
||||||
pub mod converted;
|
|
||||||
pub mod gradual;
|
pub mod gradual;
|
||||||
pub mod inspect;
|
pub mod inspect;
|
||||||
pub mod object;
|
pub mod object;
|
||||||
@@ -100,14 +93,6 @@ impl Difficulty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use this [`Difficulty`] as a calculator for a specific [`IGameMode`].
|
|
||||||
///
|
|
||||||
/// Note that [`ConvertedDifficulty`] won't allow to further customize
|
|
||||||
/// fields so be sure they're all set before converting to it.
|
|
||||||
pub const fn with_mode<M: IGameMode>(&self) -> ConvertedDifficulty<'_, M> {
|
|
||||||
ConvertedDifficulty::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Turn this [`Difficulty`] into a [`InspectDifficulty`] to inspect its
|
/// Turn this [`Difficulty`] into a [`InspectDifficulty`] to inspect its
|
||||||
/// configured values.
|
/// configured values.
|
||||||
pub fn inspect(self) -> InspectDifficulty {
|
pub fn inspect(self) -> InspectDifficulty {
|
||||||
@@ -281,48 +266,86 @@ impl Difficulty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Perform the difficulty calculation.
|
/// Perform the difficulty calculation.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes {
|
pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes {
|
||||||
let map = Cow::Borrowed(map);
|
|
||||||
|
|
||||||
match map.mode {
|
match map.mode {
|
||||||
GameMode::Osu => DifficultyAttributes::Osu(Osu::difficulty(self, &Converted::new(map))),
|
GameMode::Osu => DifficultyAttributes::Osu(
|
||||||
GameMode::Taiko => {
|
Osu::difficulty(self, map).expect("no conversion required"),
|
||||||
DifficultyAttributes::Taiko(Taiko::difficulty(self, &Converted::new(map)))
|
),
|
||||||
}
|
GameMode::Taiko => DifficultyAttributes::Taiko(
|
||||||
GameMode::Catch => {
|
Taiko::difficulty(self, map).expect("no conversion required"),
|
||||||
DifficultyAttributes::Catch(Catch::difficulty(self, &Converted::new(map)))
|
),
|
||||||
}
|
GameMode::Catch => DifficultyAttributes::Catch(
|
||||||
GameMode::Mania => {
|
Catch::difficulty(self, map).expect("no conversion required"),
|
||||||
DifficultyAttributes::Mania(Mania::difficulty(self, &Converted::new(map)))
|
),
|
||||||
}
|
GameMode::Mania => DifficultyAttributes::Mania(
|
||||||
|
Mania::difficulty(self, map).expect("no conversion required"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform the difficulty calculation for a specific [`IGameMode`].
|
||||||
|
pub fn calculate_for_mode<M: IGameMode>(
|
||||||
|
&self,
|
||||||
|
map: &Beatmap,
|
||||||
|
) -> Result<M::DifficultyAttributes, ConvertError> {
|
||||||
|
M::difficulty(self, map)
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform the difficulty calculation but instead of evaluating the skill
|
/// Perform the difficulty calculation but instead of evaluating the skill
|
||||||
/// strains, return them as is.
|
/// strains, return them as is.
|
||||||
///
|
///
|
||||||
/// Suitable to plot the difficulty of a map over time.
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn strains(&self, map: &Beatmap) -> Strains {
|
pub fn strains(&self, map: &Beatmap) -> Strains {
|
||||||
let map = Cow::Borrowed(map);
|
|
||||||
|
|
||||||
match map.mode {
|
match map.mode {
|
||||||
GameMode::Osu => Strains::Osu(Osu::strains(self, &Converted::new(map))),
|
GameMode::Osu => Strains::Osu(Osu::strains(self, map).expect("no conversion required")),
|
||||||
GameMode::Taiko => Strains::Taiko(Taiko::strains(self, &Converted::new(map))),
|
GameMode::Taiko => {
|
||||||
GameMode::Catch => Strains::Catch(Catch::strains(self, &Converted::new(map))),
|
Strains::Taiko(Taiko::strains(self, map).expect("no conversion required"))
|
||||||
GameMode::Mania => Strains::Mania(Mania::strains(self, &Converted::new(map))),
|
}
|
||||||
|
GameMode::Catch => {
|
||||||
|
Strains::Catch(Catch::strains(self, map).expect("no conversion required"))
|
||||||
|
}
|
||||||
|
GameMode::Mania => {
|
||||||
|
Strains::Mania(Mania::strains(self, map).expect("no conversion required"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform the strain calculation for a specific [`IGameMode`].
|
||||||
|
pub fn strains_for_mode<M: IGameMode>(
|
||||||
|
&self,
|
||||||
|
map: &Beatmap,
|
||||||
|
) -> Result<M::Strains, ConvertError> {
|
||||||
|
M::strains(self, map)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a gradual difficulty calculator for a [`Beatmap`].
|
/// Create a gradual difficulty calculator for a [`Beatmap`].
|
||||||
pub fn gradual_difficulty(self, map: &Beatmap) -> GradualDifficulty {
|
pub fn gradual_difficulty(self, map: &Beatmap) -> GradualDifficulty {
|
||||||
GradualDifficulty::new(self, map)
|
GradualDifficulty::new(self, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a gradual difficulty calculator for a [`Beatmap`] on a specific [`IGameMode`].
|
||||||
|
pub fn gradual_difficulty_for_mode<M: IGameMode>(
|
||||||
|
self,
|
||||||
|
map: &Beatmap,
|
||||||
|
) -> Result<M::GradualDifficulty, ConvertError> {
|
||||||
|
M::gradual_difficulty(self, map)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a gradual performance calculator for a [`Beatmap`].
|
/// Create a gradual performance calculator for a [`Beatmap`].
|
||||||
pub fn gradual_performance(self, map: &Beatmap) -> GradualPerformance {
|
pub fn gradual_performance(self, map: &Beatmap) -> GradualPerformance {
|
||||||
GradualPerformance::new(self, map)
|
GradualPerformance::new(self, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a gradual performance calculator for a [`Beatmap`] on a specific [`IGameMode`].
|
||||||
|
pub fn gradual_performance_for_mode<M: IGameMode>(
|
||||||
|
self,
|
||||||
|
map: &Beatmap,
|
||||||
|
) -> Result<M::GradualPerformance, ConvertError> {
|
||||||
|
M::gradual_performance(self, map)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) const fn get_mods(&self) -> &GameMods {
|
pub(crate) const fn get_mods(&self) -> &GameMods {
|
||||||
&self.mods
|
&self.mods
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
pub use self::{
|
pub use self::{
|
||||||
attributes::{DifficultyAttributes, PerformanceAttributes},
|
attributes::{DifficultyAttributes, PerformanceAttributes},
|
||||||
difficulty::{
|
difficulty::{
|
||||||
converted::ConvertedDifficulty, gradual::GradualDifficulty, inspect::InspectDifficulty,
|
gradual::GradualDifficulty, inspect::InspectDifficulty, Difficulty, ModsDependent,
|
||||||
Difficulty, ModsDependent,
|
|
||||||
},
|
},
|
||||||
performance::{
|
performance::{
|
||||||
gradual::GradualPerformance,
|
gradual::GradualPerformance,
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use rosu_map::section::general::GameMode;
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{PerformanceAttributes, ScoreState},
|
any::{PerformanceAttributes, ScoreState},
|
||||||
catch::{Catch, CatchBeatmap, CatchGradualPerformance},
|
catch::{Catch, CatchGradualPerformance},
|
||||||
mania::{Mania, ManiaBeatmap, ManiaGradualPerformance},
|
mania::{Mania, ManiaGradualPerformance},
|
||||||
model::mode::IGameMode,
|
model::mode::{ConvertError, IGameMode},
|
||||||
osu::{Osu, OsuBeatmap, OsuGradualPerformance},
|
osu::{Osu, OsuGradualPerformance},
|
||||||
taiko::{Taiko, TaikoBeatmap, TaikoGradualPerformance},
|
taiko::{Taiko, TaikoGradualPerformance},
|
||||||
Beatmap, Converted, Difficulty,
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gradually calculate the performance attributes on maps of any mode.
|
/// Gradually calculate the performance attributes on maps of any mode.
|
||||||
@@ -100,38 +98,26 @@ pub enum GradualPerformance {
|
|||||||
Mania(ManiaGradualPerformance),
|
Mania(ManiaGradualPerformance),
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! from_converted {
|
|
||||||
( $fn:ident, $mode:ident, $converted:ident ) => {
|
|
||||||
#[doc = concat!("Create a [`GradualPerformance`] for a [`", stringify!($converted), "`]")]
|
|
||||||
pub fn $fn(difficulty: Difficulty, converted: &$converted<'_>) -> Self {
|
|
||||||
Self::$mode($mode::gradual_performance(difficulty, converted))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GradualPerformance {
|
impl GradualPerformance {
|
||||||
/// Create a [`GradualPerformance`] for a map of any mode.
|
/// Create a [`GradualPerformance`] for a map of any mode.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Self {
|
||||||
let map = Cow::Borrowed(map);
|
Self::new_with_mode(difficulty, map, map.mode).expect("no conversion required")
|
||||||
|
|
||||||
match map.mode {
|
|
||||||
GameMode::Osu => Self::Osu(Osu::gradual_performance(difficulty, &Converted::new(map))),
|
|
||||||
GameMode::Taiko => {
|
|
||||||
Self::Taiko(Taiko::gradual_performance(difficulty, &Converted::new(map)))
|
|
||||||
}
|
|
||||||
GameMode::Catch => {
|
|
||||||
Self::Catch(Catch::gradual_performance(difficulty, &Converted::new(map)))
|
|
||||||
}
|
|
||||||
GameMode::Mania => {
|
|
||||||
Self::Mania(Mania::gradual_performance(difficulty, &Converted::new(map)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
from_converted!(from_osu_map, Osu, OsuBeatmap);
|
/// Create a [`GradualPerformance`] for a [`Beatmap`] on a specific [`GameMode`].
|
||||||
from_converted!(from_taiko_map, Taiko, TaikoBeatmap);
|
pub fn new_with_mode(
|
||||||
from_converted!(from_catch_map, Catch, CatchBeatmap);
|
difficulty: Difficulty,
|
||||||
from_converted!(from_mania_map, Mania, ManiaBeatmap);
|
map: &Beatmap,
|
||||||
|
mode: GameMode,
|
||||||
|
) -> Result<Self, ConvertError> {
|
||||||
|
match mode {
|
||||||
|
GameMode::Osu => Osu::gradual_performance(difficulty, map).map(Self::Osu),
|
||||||
|
GameMode::Taiko => Taiko::gradual_performance(difficulty, map).map(Self::Taiko),
|
||||||
|
GameMode::Catch => Catch::gradual_performance(difficulty, map).map(Self::Catch),
|
||||||
|
GameMode::Mania => Mania::gradual_performance(difficulty, map).map(Self::Mania),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Process the next hit object and calculate the performance attributes
|
/// Process the next hit object and calculate the performance attributes
|
||||||
/// for the resulting score state.
|
/// for the resulting score state.
|
||||||
|
|||||||
+25
-42
@@ -1,11 +1,9 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use rosu_map::section::general::GameMode;
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{DifficultyAttributes, PerformanceAttributes},
|
any::{DifficultyAttributes, PerformanceAttributes},
|
||||||
model::mode::IGameMode,
|
model::mode::IGameMode,
|
||||||
Beatmap, Converted, Performance,
|
Beatmap, Performance,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Turning a type into the generic [`IGameMode`]'s performance calculator.
|
/// Turning a type into the generic [`IGameMode`]'s performance calculator.
|
||||||
@@ -31,18 +29,6 @@ macro_rules! impl_from_mode {
|
|||||||
() => { crate::$module::$mode };
|
() => { crate::$module::$mode };
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> IntoModePerformance<'map, mode!()> for Converted<'map, mode!()> {
|
|
||||||
fn into_performance(self) -> <mode!() as IGameMode>::Performance<'map> {
|
|
||||||
<mode!() as IGameMode>::Performance::from_map_or_attrs(self.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'map> IntoModePerformance<'map, mode!()> for &'map Converted<'_, mode!()> {
|
|
||||||
fn into_performance(self) -> <mode!() as IGameMode>::Performance<'map> {
|
|
||||||
<mode!() as IGameMode>::Performance::from_map_or_attrs(self.as_owned().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'map> IntoModePerformance<'map, mode!()> for crate::$module::$diff {
|
impl<'map> IntoModePerformance<'map, mode!()> for crate::$module::$diff {
|
||||||
fn into_performance(self) -> <mode!() as IGameMode>::Performance<'map> {
|
fn into_performance(self) -> <mode!() as IGameMode>::Performance<'map> {
|
||||||
<mode!() as IGameMode>::Performance::from_map_or_attrs(self.into())
|
<mode!() as IGameMode>::Performance::from_map_or_attrs(self.into())
|
||||||
@@ -55,22 +41,6 @@ macro_rules! impl_from_mode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> IntoPerformance<'map> for Converted<'map, mode!()> {
|
|
||||||
fn into_performance(self) -> Performance<'map> {
|
|
||||||
Performance::$mode(
|
|
||||||
<Self as IntoModePerformance<'map, mode!()>>::into_performance(self)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'map> IntoPerformance<'map> for &'map Converted<'_, mode!()> {
|
|
||||||
fn into_performance(self) -> Performance<'map> {
|
|
||||||
Performance::$mode(
|
|
||||||
<Self as IntoModePerformance<'map, mode!()>>::into_performance(self)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoPerformance<'a> for crate::$module::$diff {
|
impl<'a> IntoPerformance<'a> for crate::$module::$diff {
|
||||||
fn into_performance(self) -> Performance<'a> {
|
fn into_performance(self) -> Performance<'a> {
|
||||||
Performance::$mode(
|
Performance::$mode(
|
||||||
@@ -86,6 +56,18 @@ macro_rules! impl_from_mode {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'map> IntoModePerformance<'map, mode!()> for &'map Beatmap {
|
||||||
|
fn into_performance(self) -> <mode!() as IGameMode>::Performance<'map> {
|
||||||
|
<mode!() as IGameMode>::Performance::from_map_or_attrs(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoModePerformance<'a, mode!()> for Beatmap {
|
||||||
|
fn into_performance(self) -> <mode!() as IGameMode>::Performance<'a> {
|
||||||
|
<mode!() as IGameMode>::Performance::from_map_or_attrs(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
)*
|
)*
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -115,22 +97,23 @@ impl_from_mode!(
|
|||||||
|
|
||||||
impl<'a> IntoPerformance<'a> for Beatmap {
|
impl<'a> IntoPerformance<'a> for Beatmap {
|
||||||
fn into_performance(self) -> Performance<'a> {
|
fn into_performance(self) -> Performance<'a> {
|
||||||
map_to_performance(self.mode, Cow::Owned(self))
|
match self.mode {
|
||||||
|
GameMode::Osu => Performance::Osu(self.into()),
|
||||||
|
GameMode::Taiko => Performance::Taiko(self.into()),
|
||||||
|
GameMode::Catch => Performance::Catch(self.into()),
|
||||||
|
GameMode::Mania => Performance::Mania(self.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> IntoPerformance<'map> for &'map Beatmap {
|
impl<'map> IntoPerformance<'map> for &'map Beatmap {
|
||||||
fn into_performance(self) -> Performance<'map> {
|
fn into_performance(self) -> Performance<'map> {
|
||||||
map_to_performance(self.mode, Cow::Borrowed(self))
|
match self.mode {
|
||||||
}
|
GameMode::Osu => Performance::Osu(self.into()),
|
||||||
}
|
GameMode::Taiko => Performance::Taiko(self.into()),
|
||||||
|
GameMode::Catch => Performance::Catch(self.into()),
|
||||||
fn map_to_performance(mode: GameMode, map: Cow<'_, Beatmap>) -> Performance<'_> {
|
GameMode::Mania => Performance::Mania(self.into()),
|
||||||
match mode {
|
}
|
||||||
GameMode::Osu => Performance::Osu(Converted::new(map).into()),
|
|
||||||
GameMode::Taiko => Performance::Taiko(Converted::new(map).into()),
|
|
||||||
GameMode::Catch => Performance::Catch(Converted::new(map).into()),
|
|
||||||
GameMode::Mania => Performance::Mania(Converted::new(map).into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-18
@@ -2,7 +2,7 @@ use rosu_map::section::general::GameMode;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
catch::CatchPerformance, mania::ManiaPerformance, osu::OsuPerformance, taiko::TaikoPerformance,
|
catch::CatchPerformance, mania::ManiaPerformance, osu::OsuPerformance, taiko::TaikoPerformance,
|
||||||
Difficulty,
|
Difficulty, GameMods,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::into::IntoPerformance;
|
use self::into::IntoPerformance;
|
||||||
@@ -29,7 +29,7 @@ impl<'map> Performance<'map> {
|
|||||||
/// - previously calculated attributes ([`DifficultyAttributes`],
|
/// - previously calculated attributes ([`DifficultyAttributes`],
|
||||||
/// [`PerformanceAttributes`], or mode-specific attributes like
|
/// [`PerformanceAttributes`], or mode-specific attributes like
|
||||||
/// [`TaikoDifficultyAttributes`], [`ManiaPerformanceAttributes`], ...)
|
/// [`TaikoDifficultyAttributes`], [`ManiaPerformanceAttributes`], ...)
|
||||||
/// - a beatmap ([`Beatmap`] or [`Converted<'_, M>`])
|
/// - a [`Beatmap`] (by reference or value)
|
||||||
///
|
///
|
||||||
/// If a map is given, difficulty attributes will need to be calculated
|
/// If a map is given, difficulty attributes will need to be calculated
|
||||||
/// internally which is a costly operation. Hence, passing attributes
|
/// internally which is a costly operation. Hence, passing attributes
|
||||||
@@ -40,7 +40,6 @@ impl<'map> Performance<'map> {
|
|||||||
/// Otherwise, the final attributes will be incorrect.
|
/// Otherwise, the final attributes will be incorrect.
|
||||||
///
|
///
|
||||||
/// [`Beatmap`]: crate::model::beatmap::Beatmap
|
/// [`Beatmap`]: crate::model::beatmap::Beatmap
|
||||||
/// [`Converted<'_, M>`]: crate::model::beatmap::Converted
|
|
||||||
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
||||||
/// [`TaikoDifficultyAttributes`]: crate::taiko::TaikoDifficultyAttributes
|
/// [`TaikoDifficultyAttributes`]: crate::taiko::TaikoDifficultyAttributes
|
||||||
/// [`ManiaPerformanceAttributes`]: crate::mania::ManiaPerformanceAttributes
|
/// [`ManiaPerformanceAttributes`]: crate::mania::ManiaPerformanceAttributes
|
||||||
@@ -50,12 +49,21 @@ impl<'map> Performance<'map> {
|
|||||||
|
|
||||||
/// Consume the performance calculator and calculate
|
/// Consume the performance calculator and calculate
|
||||||
/// performance attributes for the given parameters.
|
/// performance attributes for the given parameters.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn calculate(self) -> PerformanceAttributes {
|
pub fn calculate(self) -> PerformanceAttributes {
|
||||||
match self {
|
match self {
|
||||||
Self::Osu(o) => PerformanceAttributes::Osu(o.calculate()),
|
Self::Osu(o) => {
|
||||||
Self::Taiko(t) => PerformanceAttributes::Taiko(t.calculate()),
|
PerformanceAttributes::Osu(o.calculate().expect("no conversion required"))
|
||||||
Self::Catch(f) => PerformanceAttributes::Catch(f.calculate()),
|
}
|
||||||
Self::Mania(m) => PerformanceAttributes::Mania(m.calculate()),
|
Self::Taiko(t) => {
|
||||||
|
PerformanceAttributes::Taiko(t.calculate().expect("no conversion required"))
|
||||||
|
}
|
||||||
|
Self::Catch(f) => {
|
||||||
|
PerformanceAttributes::Catch(f.calculate().expect("no conversion required"))
|
||||||
|
}
|
||||||
|
Self::Mania(m) => {
|
||||||
|
PerformanceAttributes::Mania(m.calculate().expect("no conversion required"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +116,7 @@ impl<'map> Performance<'map> {
|
|||||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||||
///
|
///
|
||||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||||
pub fn mods(self, mods: u32) -> Self {
|
pub fn mods(self, mods: impl Into<GameMods>) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Osu(o) => Self::Osu(o.mods(mods)),
|
Self::Osu(o) => Self::Osu(o.mods(mods)),
|
||||||
Self::Taiko(t) => Self::Taiko(t.mods(mods)),
|
Self::Taiko(t) => Self::Taiko(t.mods(mods)),
|
||||||
@@ -404,12 +412,13 @@ impl<'map> Performance<'map> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create the [`ScoreState`] that will be used for performance calculation.
|
/// Create the [`ScoreState`] that will be used for performance calculation.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn generate_state(&mut self) -> ScoreState {
|
pub fn generate_state(&mut self) -> ScoreState {
|
||||||
match self {
|
match self {
|
||||||
Self::Osu(o) => o.generate_state().into(),
|
Self::Osu(o) => o.generate_state().expect("no conversion required").into(),
|
||||||
Self::Taiko(t) => t.generate_state().into(),
|
Self::Taiko(t) => t.generate_state().expect("no conversion required").into(),
|
||||||
Self::Catch(f) => f.generate_state().into(),
|
Self::Catch(f) => f.generate_state().expect("no conversion required").into(),
|
||||||
Self::Mania(m) => m.generate_state().into(),
|
Self::Mania(m) => m.generate_state().expect("no conversion required").into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,7 +455,7 @@ mod tests {
|
|||||||
catch::{CatchDifficultyAttributes, CatchPerformanceAttributes},
|
catch::{CatchDifficultyAttributes, CatchPerformanceAttributes},
|
||||||
mania::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
|
mania::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
|
||||||
osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||||
taiko::{Taiko, TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
taiko::{TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
||||||
Beatmap,
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -455,10 +464,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn create() {
|
fn create() {
|
||||||
let map = Beatmap::from_path("./resources/1028484.osu").unwrap();
|
let map = Beatmap::from_path("./resources/1028484.osu").unwrap();
|
||||||
let converted = map.unchecked_as_converted::<Taiko>();
|
|
||||||
|
|
||||||
let _ = Performance::new(&converted);
|
|
||||||
let _ = Performance::new(converted.as_owned());
|
|
||||||
let _ = Performance::new(&map);
|
let _ = Performance::new(&map);
|
||||||
let _ = Performance::new(map.clone());
|
let _ = Performance::new(map.clone());
|
||||||
|
|
||||||
@@ -477,8 +483,6 @@ mod tests {
|
|||||||
TaikoPerformanceAttributes::default(),
|
TaikoPerformanceAttributes::default(),
|
||||||
));
|
));
|
||||||
|
|
||||||
let _ = Performance::from(&converted);
|
|
||||||
let _ = Performance::from(converted);
|
|
||||||
let _ = Performance::from(&map);
|
let _ = Performance::from(&map);
|
||||||
let _ = Performance::from(map);
|
let _ = Performance::from(map);
|
||||||
|
|
||||||
|
|||||||
+10
-31
@@ -2,9 +2,8 @@ use rosu_map::section::{general::GameMode, hit_objects::CurveBuffers};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
beatmap::{Beatmap, Converted},
|
beatmap::Beatmap,
|
||||||
hit_object::{HitObject, HitObjectKind, HoldNote, Spinner},
|
hit_object::{HitObject, HitObjectKind, HoldNote, Spinner},
|
||||||
mode::ConvertStatus,
|
|
||||||
mods::Reflection,
|
mods::Reflection,
|
||||||
},
|
},
|
||||||
util::{float_ext::FloatExt, random::Random},
|
util::{float_ext::FloatExt, random::Random},
|
||||||
@@ -19,37 +18,18 @@ use super::{
|
|||||||
juice_stream::{JuiceStream, JuiceStreamBufs, NestedJuiceStreamObjectKind},
|
juice_stream::{JuiceStream, JuiceStreamBufs, NestedJuiceStreamObjectKind},
|
||||||
palpable::PalpableObject,
|
palpable::PalpableObject,
|
||||||
},
|
},
|
||||||
Catch, PLAYFIELD_WIDTH,
|
PLAYFIELD_WIDTH,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RNG_SEED: i32 = 1337;
|
const RNG_SEED: i32 = 1337;
|
||||||
|
|
||||||
/// A [`Beatmap`] for [`Catch`] calculations.
|
pub fn convert(map: &mut Beatmap) {
|
||||||
pub type CatchBeatmap<'a> = Converted<'a, Catch>;
|
map.mode = GameMode::Catch;
|
||||||
|
map.is_convert = true;
|
||||||
pub const fn check_convert(map: &Beatmap) -> ConvertStatus {
|
|
||||||
match map.mode {
|
|
||||||
GameMode::Osu => ConvertStatus::Conversion,
|
|
||||||
GameMode::Catch => ConvertStatus::Noop,
|
|
||||||
GameMode::Taiko | GameMode::Mania => ConvertStatus::Incompatible,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_convert(map: &mut Beatmap) -> ConvertStatus {
|
|
||||||
match map.mode {
|
|
||||||
GameMode::Osu => {
|
|
||||||
map.mode = GameMode::Catch;
|
|
||||||
map.is_convert = true;
|
|
||||||
|
|
||||||
ConvertStatus::Conversion
|
|
||||||
}
|
|
||||||
GameMode::Catch => ConvertStatus::Noop,
|
|
||||||
GameMode::Taiko | GameMode::Mania => ConvertStatus::Incompatible,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_objects(
|
pub fn convert_objects(
|
||||||
converted: &CatchBeatmap<'_>,
|
map: &Beatmap,
|
||||||
count: &mut ObjectCountBuilder,
|
count: &mut ObjectCountBuilder,
|
||||||
reflection: Reflection,
|
reflection: Reflection,
|
||||||
hr_offsets: bool,
|
hr_offsets: bool,
|
||||||
@@ -70,8 +50,8 @@ pub fn convert_objects(
|
|||||||
let mut last_pos = None;
|
let mut last_pos = None;
|
||||||
let mut last_start_time = 0.0;
|
let mut last_start_time = 0.0;
|
||||||
|
|
||||||
for h in converted.hit_objects.iter() {
|
for h in map.hit_objects.iter() {
|
||||||
let mut new_objects = convert_object(h, converted, count, &mut bufs);
|
let mut new_objects = convert_object(h, map, count, &mut bufs);
|
||||||
|
|
||||||
apply_pos_offset(
|
apply_pos_offset(
|
||||||
&mut new_objects,
|
&mut new_objects,
|
||||||
@@ -99,7 +79,7 @@ pub fn convert_objects(
|
|||||||
|
|
||||||
fn convert_object<'a>(
|
fn convert_object<'a>(
|
||||||
h: &'a HitObject,
|
h: &'a HitObject,
|
||||||
converted: &CatchBeatmap<'_>,
|
map: &Beatmap,
|
||||||
count: &mut ObjectCountBuilder,
|
count: &mut ObjectCountBuilder,
|
||||||
bufs: &'a mut JuiceStreamBufs,
|
bufs: &'a mut JuiceStreamBufs,
|
||||||
) -> ObjectIter<'a> {
|
) -> ObjectIter<'a> {
|
||||||
@@ -107,8 +87,7 @@ fn convert_object<'a>(
|
|||||||
HitObjectKind::Circle => ObjectIterState::Fruit(Some(Fruit::new(count))),
|
HitObjectKind::Circle => ObjectIterState::Fruit(Some(Fruit::new(count))),
|
||||||
HitObjectKind::Slider(ref slider) => {
|
HitObjectKind::Slider(ref slider) => {
|
||||||
let effective_x = h.pos.x.clamp(0.0, PLAYFIELD_WIDTH);
|
let effective_x = h.pos.x.clamp(0.0, PLAYFIELD_WIDTH);
|
||||||
let stream =
|
let stream = JuiceStream::new(effective_x, h.start_time, slider, map, count, bufs);
|
||||||
JuiceStream::new(effective_x, h.start_time, slider, converted, count, bufs);
|
|
||||||
|
|
||||||
ObjectIterState::JuiceStream(stream)
|
ObjectIterState::JuiceStream(stream)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::skills::Skill,
|
any::difficulty::skills::Skill,
|
||||||
catch::{
|
catch::{
|
||||||
attributes::{GradualObjectCount, ObjectCountBuilder},
|
attributes::{GradualObjectCount, ObjectCountBuilder},
|
||||||
convert::convert_objects,
|
convert::convert_objects,
|
||||||
CatchBeatmap, CatchDifficultyAttributes,
|
CatchDifficultyAttributes,
|
||||||
},
|
},
|
||||||
Difficulty,
|
model::mode::ConvertError,
|
||||||
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -65,18 +68,20 @@ pub struct CatchGradualDifficulty {
|
|||||||
|
|
||||||
impl CatchGradualDifficulty {
|
impl CatchGradualDifficulty {
|
||||||
/// Create a new difficulty attributes iterator for osu!catch maps.
|
/// Create a new difficulty attributes iterator for osu!catch maps.
|
||||||
pub fn new(difficulty: Difficulty, converted: &CatchBeatmap<'_>) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
|
||||||
|
let map = map.convert_ref(GameMode::Catch, difficulty.get_mods())?;
|
||||||
|
|
||||||
let clock_rate = difficulty.get_clock_rate();
|
let clock_rate = difficulty.get_clock_rate();
|
||||||
|
|
||||||
let CatchDifficultySetup { map_attrs, attrs } =
|
let CatchDifficultySetup { map_attrs, attrs } =
|
||||||
CatchDifficultySetup::new(&difficulty, converted);
|
CatchDifficultySetup::new(&difficulty, &map);
|
||||||
|
|
||||||
let hr_offsets = difficulty.get_hardrock_offsets();
|
let hr_offsets = difficulty.get_hardrock_offsets();
|
||||||
let reflection = difficulty.get_mods().reflection();
|
let reflection = difficulty.get_mods().reflection();
|
||||||
let mut count = ObjectCountBuilder::new_gradual();
|
let mut count = ObjectCountBuilder::new_gradual();
|
||||||
|
|
||||||
let palpable_objects = convert_objects(
|
let palpable_objects = convert_objects(
|
||||||
converted,
|
&map,
|
||||||
&mut count,
|
&mut count,
|
||||||
reflection,
|
reflection,
|
||||||
hr_offsets,
|
hr_offsets,
|
||||||
@@ -92,14 +97,14 @@ impl CatchGradualDifficulty {
|
|||||||
let count = count.into_gradual();
|
let count = count.into_gradual();
|
||||||
let movement = Movement::new(clock_rate);
|
let movement = Movement::new(clock_rate);
|
||||||
|
|
||||||
Self {
|
Ok(Self {
|
||||||
idx: 0,
|
idx: 0,
|
||||||
difficulty,
|
difficulty,
|
||||||
attrs,
|
attrs,
|
||||||
count,
|
count,
|
||||||
diff_objects,
|
diff_objects,
|
||||||
movement,
|
movement,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,30 +173,26 @@ impl ExactSizeIterator for CatchGradualDifficulty {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::Beatmap;
|
use crate::{catch::Catch, Beatmap};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn empty() {
|
||||||
let converted = Beatmap::from_bytes(&[]).unwrap().unchecked_into_converted();
|
let map = Beatmap::from_bytes(&[]).unwrap();
|
||||||
|
let mut gradual = CatchGradualDifficulty::new(Difficulty::new(), &map).unwrap();
|
||||||
let mut gradual = CatchGradualDifficulty::new(Difficulty::new(), &converted);
|
|
||||||
|
|
||||||
assert!(gradual.next().is_none());
|
assert!(gradual.next().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_and_nth() {
|
fn next_and_nth() {
|
||||||
let converted = Beatmap::from_path("./resources/2118524.osu")
|
let map = Beatmap::from_path("./resources/2118524.osu").unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted();
|
|
||||||
|
|
||||||
let difficulty = Difficulty::new();
|
let difficulty = Difficulty::new();
|
||||||
|
|
||||||
let mut gradual = CatchGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_2nd = CatchGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual_2nd = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_3rd = CatchGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual_3rd = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
|
|
||||||
for i in 1.. {
|
for i in 1.. {
|
||||||
let Some(next_gradual) = gradual.next() else {
|
let Some(next_gradual) = gradual.next() else {
|
||||||
@@ -214,8 +215,8 @@ mod tests {
|
|||||||
let expected = difficulty
|
let expected = difficulty
|
||||||
.clone()
|
.clone()
|
||||||
.passed_objects(i as u32)
|
.passed_objects(i as u32)
|
||||||
.with_mode()
|
.calculate_for_mode::<Catch>(&map)
|
||||||
.calculate(&converted);
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(next_gradual, expected);
|
assert_eq!(next_gradual, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-18
@@ -1,16 +1,18 @@
|
|||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::{skills::Skill, Difficulty},
|
any::difficulty::{skills::Skill, Difficulty},
|
||||||
catch::{
|
catch::{
|
||||||
catcher::Catcher, convert::convert_objects, difficulty::object::CatchDifficultyObject,
|
catcher::Catcher, convert::convert_objects, difficulty::object::CatchDifficultyObject,
|
||||||
},
|
},
|
||||||
model::beatmap::BeatmapAttributes,
|
model::{beatmap::BeatmapAttributes, mode::ConvertError},
|
||||||
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::skills::movement::Movement;
|
use self::skills::movement::Movement;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
attributes::{CatchDifficultyAttributes, ObjectCountBuilder},
|
attributes::{CatchDifficultyAttributes, ObjectCountBuilder},
|
||||||
convert::CatchBeatmap,
|
|
||||||
object::palpable::PalpableObject,
|
object::palpable::PalpableObject,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,16 +24,18 @@ const DIFFICULTY_MULTIPLIER: f64 = 4.59;
|
|||||||
|
|
||||||
pub fn difficulty(
|
pub fn difficulty(
|
||||||
difficulty: &Difficulty,
|
difficulty: &Difficulty,
|
||||||
converted: &CatchBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> CatchDifficultyAttributes {
|
) -> Result<CatchDifficultyAttributes, ConvertError> {
|
||||||
|
let map = map.convert_ref(GameMode::Catch, difficulty.get_mods())?;
|
||||||
|
|
||||||
let DifficultyValues {
|
let DifficultyValues {
|
||||||
movement,
|
movement,
|
||||||
mut attrs,
|
mut attrs,
|
||||||
} = DifficultyValues::calculate(difficulty, converted);
|
} = DifficultyValues::calculate(difficulty, &map);
|
||||||
|
|
||||||
DifficultyValues::eval(&mut attrs, movement.difficulty_value());
|
DifficultyValues::eval(&mut attrs, movement.difficulty_value());
|
||||||
|
|
||||||
attrs
|
Ok(attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CatchDifficultySetup {
|
pub struct CatchDifficultySetup {
|
||||||
@@ -40,12 +44,12 @@ pub struct CatchDifficultySetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CatchDifficultySetup {
|
impl CatchDifficultySetup {
|
||||||
pub fn new(difficulty: &Difficulty, converted: &CatchBeatmap<'_>) -> Self {
|
pub fn new(difficulty: &Difficulty, map: &Beatmap) -> Self {
|
||||||
let map_attrs = converted.attributes().difficulty(difficulty).build();
|
let map_attrs = map.attributes().difficulty(difficulty).build();
|
||||||
|
|
||||||
let attrs = CatchDifficultyAttributes {
|
let attrs = CatchDifficultyAttributes {
|
||||||
ar: map_attrs.ar,
|
ar: map_attrs.ar,
|
||||||
is_convert: converted.is_convert,
|
is_convert: map.is_convert,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,26 +63,21 @@ pub struct DifficultyValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DifficultyValues {
|
impl DifficultyValues {
|
||||||
pub fn calculate(difficulty: &Difficulty, converted: &CatchBeatmap<'_>) -> Self {
|
pub fn calculate(difficulty: &Difficulty, map: &Beatmap) -> Self {
|
||||||
let take = difficulty.get_passed_objects();
|
let take = difficulty.get_passed_objects();
|
||||||
let clock_rate = difficulty.get_clock_rate();
|
let clock_rate = difficulty.get_clock_rate();
|
||||||
|
|
||||||
let CatchDifficultySetup {
|
let CatchDifficultySetup {
|
||||||
map_attrs,
|
map_attrs,
|
||||||
mut attrs,
|
mut attrs,
|
||||||
} = CatchDifficultySetup::new(difficulty, converted);
|
} = CatchDifficultySetup::new(difficulty, map);
|
||||||
|
|
||||||
let hr_offsets = difficulty.get_hardrock_offsets();
|
let hr_offsets = difficulty.get_hardrock_offsets();
|
||||||
let reflection = difficulty.get_mods().reflection();
|
let reflection = difficulty.get_mods().reflection();
|
||||||
let mut count = ObjectCountBuilder::new_regular(take);
|
let mut count = ObjectCountBuilder::new_regular(take);
|
||||||
|
|
||||||
let palpable_objects = convert_objects(
|
let palpable_objects =
|
||||||
converted,
|
convert_objects(map, &mut count, reflection, hr_offsets, map_attrs.cs as f32);
|
||||||
&mut count,
|
|
||||||
reflection,
|
|
||||||
hr_offsets,
|
|
||||||
map_attrs.cs as f32,
|
|
||||||
);
|
|
||||||
|
|
||||||
let diff_objects = Self::create_difficulty_objects(
|
let diff_objects = Self::create_difficulty_objects(
|
||||||
&map_attrs,
|
&map_attrs,
|
||||||
|
|||||||
+20
-20
@@ -1,14 +1,15 @@
|
|||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
beatmap::Beatmap,
|
beatmap::Beatmap,
|
||||||
mode::{ConvertStatus, IGameMode},
|
mode::{ConvertError, IGameMode},
|
||||||
},
|
},
|
||||||
Difficulty,
|
Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
attributes::{CatchDifficultyAttributes, CatchPerformanceAttributes},
|
attributes::{CatchDifficultyAttributes, CatchPerformanceAttributes},
|
||||||
convert::CatchBeatmap,
|
|
||||||
difficulty::gradual::CatchGradualDifficulty,
|
difficulty::gradual::CatchGradualDifficulty,
|
||||||
performance::{gradual::CatchGradualPerformance, CatchPerformance},
|
performance::{gradual::CatchGradualPerformance, CatchPerformance},
|
||||||
score_state::CatchScoreState,
|
score_state::CatchScoreState,
|
||||||
@@ -31,6 +32,13 @@ const PLAYFIELD_WIDTH: f32 = 512.0;
|
|||||||
/// [`GameMode::Catch`]: rosu_map::section::general::GameMode::Catch
|
/// [`GameMode::Catch`]: rosu_map::section::general::GameMode::Catch
|
||||||
pub struct Catch;
|
pub struct Catch;
|
||||||
|
|
||||||
|
impl Catch {
|
||||||
|
pub fn convert(map: &mut Beatmap) {
|
||||||
|
debug_assert!(!map.is_convert && map.mode == GameMode::Osu);
|
||||||
|
convert::convert(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IGameMode for Catch {
|
impl IGameMode for Catch {
|
||||||
type DifficultyAttributes = CatchDifficultyAttributes;
|
type DifficultyAttributes = CatchDifficultyAttributes;
|
||||||
type Strains = CatchStrains;
|
type Strains = CatchStrains;
|
||||||
@@ -38,40 +46,32 @@ impl IGameMode for Catch {
|
|||||||
type GradualDifficulty = CatchGradualDifficulty;
|
type GradualDifficulty = CatchGradualDifficulty;
|
||||||
type GradualPerformance = CatchGradualPerformance;
|
type GradualPerformance = CatchGradualPerformance;
|
||||||
|
|
||||||
fn check_convert(map: &Beatmap) -> ConvertStatus {
|
|
||||||
convert::check_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_convert(map: &mut Beatmap) -> ConvertStatus {
|
|
||||||
convert::try_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn difficulty(
|
fn difficulty(
|
||||||
difficulty: &Difficulty,
|
difficulty: &Difficulty,
|
||||||
converted: &CatchBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::DifficultyAttributes {
|
) -> Result<Self::DifficultyAttributes, ConvertError> {
|
||||||
difficulty::difficulty(difficulty, converted)
|
difficulty::difficulty(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strains(difficulty: &Difficulty, converted: &CatchBeatmap<'_>) -> Self::Strains {
|
fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<Self::Strains, ConvertError> {
|
||||||
strains::strains(difficulty, converted)
|
strains::strains(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn performance(map: CatchBeatmap<'_>) -> Self::Performance<'_> {
|
fn performance(map: &Beatmap) -> Self::Performance<'_> {
|
||||||
CatchPerformance::new(map)
|
CatchPerformance::new(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradual_difficulty(
|
fn gradual_difficulty(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &CatchBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualDifficulty {
|
) -> Result<Self::GradualDifficulty, ConvertError> {
|
||||||
CatchGradualDifficulty::new(difficulty, map)
|
CatchGradualDifficulty::new(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradual_performance(
|
fn gradual_performance(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &CatchBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualPerformance {
|
) -> Result<Self::GradualPerformance, ConvertError> {
|
||||||
CatchGradualPerformance::new(difficulty, map)
|
CatchGradualPerformance::new(difficulty, map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ use rosu_map::section::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
catch::{attributes::ObjectCountBuilder, convert::CatchBeatmap},
|
catch::attributes::ObjectCountBuilder,
|
||||||
model::{
|
model::{
|
||||||
control_point::{DifficultyPoint, TimingPoint},
|
control_point::{DifficultyPoint, TimingPoint},
|
||||||
hit_object::Slider,
|
hit_object::Slider,
|
||||||
},
|
},
|
||||||
util::get_precision_adjusted_beat_len,
|
util::get_precision_adjusted_beat_len,
|
||||||
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct JuiceStream<'a> {
|
pub struct JuiceStream<'a> {
|
||||||
@@ -26,18 +27,18 @@ impl<'a> JuiceStream<'a> {
|
|||||||
effective_x: f32,
|
effective_x: f32,
|
||||||
start_time: f64,
|
start_time: f64,
|
||||||
slider: &'a Slider,
|
slider: &'a Slider,
|
||||||
converted: &CatchBeatmap<'_>,
|
map: &Beatmap,
|
||||||
count: &mut ObjectCountBuilder,
|
count: &mut ObjectCountBuilder,
|
||||||
bufs: &'a mut JuiceStreamBufs,
|
bufs: &'a mut JuiceStreamBufs,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let slider_multiplier = converted.slider_multiplier;
|
let slider_multiplier = map.slider_multiplier;
|
||||||
let slider_tick_rate = converted.slider_tick_rate;
|
let slider_tick_rate = map.slider_tick_rate;
|
||||||
|
|
||||||
let beat_len = converted
|
let beat_len = map
|
||||||
.timing_point_at(start_time)
|
.timing_point_at(start_time)
|
||||||
.map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
|
.map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
|
||||||
|
|
||||||
let slider_velocity = converted
|
let slider_velocity = map
|
||||||
.difficulty_point_at(start_time)
|
.difficulty_point_at(start_time)
|
||||||
.map_or(DifficultyPoint::DEFAULT_SLIDER_VELOCITY, |point| {
|
.map_or(DifficultyPoint::DEFAULT_SLIDER_VELOCITY, |point| {
|
||||||
point.slider_velocity
|
point.slider_velocity
|
||||||
@@ -49,7 +50,7 @@ impl<'a> JuiceStream<'a> {
|
|||||||
/ get_precision_adjusted_beat_len(slider_velocity, beat_len);
|
/ get_precision_adjusted_beat_len(slider_velocity, beat_len);
|
||||||
let scoring_dist = velocity * beat_len;
|
let scoring_dist = velocity * beat_len;
|
||||||
|
|
||||||
let tick_dist_multiplier = if converted.version < 8 {
|
let tick_dist_multiplier = if map.version < 8 {
|
||||||
slider_velocity.recip()
|
slider_velocity.recip()
|
||||||
} else {
|
} else {
|
||||||
1.0
|
1.0
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
catch::{CatchBeatmap, CatchGradualDifficulty, CatchPerformanceAttributes, CatchScoreState},
|
catch::{CatchGradualDifficulty, CatchPerformanceAttributes, CatchScoreState},
|
||||||
Difficulty,
|
model::mode::ConvertError,
|
||||||
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gradually calculate the performance attributes of an osu!catch map.
|
/// Gradually calculate the performance attributes of an osu!catch map.
|
||||||
@@ -90,10 +91,10 @@ pub struct CatchGradualPerformance {
|
|||||||
|
|
||||||
impl CatchGradualPerformance {
|
impl CatchGradualPerformance {
|
||||||
/// Create a new gradual performance calculator for osu!catch maps.
|
/// Create a new gradual performance calculator for osu!catch maps.
|
||||||
pub fn new(difficulty: Difficulty, converted: &CatchBeatmap<'_>) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
|
||||||
let difficulty = CatchGradualDifficulty::new(difficulty, converted);
|
let difficulty = CatchGradualDifficulty::new(difficulty, map)?;
|
||||||
|
|
||||||
Self { difficulty }
|
Ok(Self { difficulty })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process the next hit object and calculate the performance attributes
|
/// Process the next hit object and calculate the performance attributes
|
||||||
@@ -116,6 +117,7 @@ impl CatchGradualPerformance {
|
|||||||
///
|
///
|
||||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
||||||
/// `n=1` will process 2, and so on.
|
/// `n=1` will process 2, and so on.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn nth(&mut self, state: CatchScoreState, n: usize) -> Option<CatchPerformanceAttributes> {
|
pub fn nth(&mut self, state: CatchScoreState, n: usize) -> Option<CatchPerformanceAttributes> {
|
||||||
let performance = self
|
let performance = self
|
||||||
.difficulty
|
.difficulty
|
||||||
@@ -124,7 +126,8 @@ impl CatchGradualPerformance {
|
|||||||
.state(state)
|
.state(state)
|
||||||
.difficulty(self.difficulty.difficulty.clone())
|
.difficulty(self.difficulty.difficulty.clone())
|
||||||
.passed_objects(self.difficulty.idx as u32)
|
.passed_objects(self.difficulty.idx as u32)
|
||||||
.calculate();
|
.calculate()
|
||||||
|
.expect("no conversion required");
|
||||||
|
|
||||||
Some(performance)
|
Some(performance)
|
||||||
}
|
}
|
||||||
@@ -144,15 +147,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_and_nth() {
|
fn next_and_nth() {
|
||||||
let converted = Beatmap::from_path("./resources/2118524.osu")
|
let map = Beatmap::from_path("./resources/2118524.osu").unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted();
|
|
||||||
|
|
||||||
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
||||||
|
|
||||||
let mut gradual = CatchGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual = CatchGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_2nd = CatchGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual_2nd = CatchGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_3rd = CatchGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual_3rd = CatchGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
|
|
||||||
let mut state = CatchScoreState::default();
|
let mut state = CatchScoreState::default();
|
||||||
|
|
||||||
@@ -176,12 +177,12 @@ mod tests {
|
|||||||
assert_eq!(next_gradual, next_gradual_3rd);
|
assert_eq!(next_gradual, next_gradual_3rd);
|
||||||
}
|
}
|
||||||
|
|
||||||
let regular_calc = CatchPerformance::new(converted.as_owned())
|
let regular_calc = CatchPerformance::new(&map)
|
||||||
.difficulty(difficulty.clone())
|
.difficulty(difficulty.clone())
|
||||||
.passed_objects(i as u32)
|
.passed_objects(i as u32)
|
||||||
.state(state.clone());
|
.state(state.clone());
|
||||||
|
|
||||||
let expected = regular_calc.calculate();
|
let expected = regular_calc.calculate().unwrap();
|
||||||
|
|
||||||
assert_eq!(next_gradual, expected);
|
assert_eq!(next_gradual, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::cmp::{self, Ordering};
|
use std::cmp::{self, Ordering};
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{Difficulty, IntoModePerformance, IntoPerformance},
|
any::{Difficulty, IntoModePerformance, IntoPerformance},
|
||||||
model::mods::GameMods,
|
model::{mode::ConvertError, mods::GameMods},
|
||||||
osu::OsuPerformance,
|
osu::OsuPerformance,
|
||||||
util::map_or_attrs::MapOrAttrs,
|
util::map_or_attrs::MapOrAttrs,
|
||||||
Performance,
|
Performance,
|
||||||
@@ -37,7 +39,7 @@ impl<'map> CatchPerformance<'map> {
|
|||||||
/// The argument `map_or_attrs` must be either
|
/// The argument `map_or_attrs` must be either
|
||||||
/// - previously calculated attributes ([`CatchDifficultyAttributes`]
|
/// - previously calculated attributes ([`CatchDifficultyAttributes`]
|
||||||
/// or [`CatchPerformanceAttributes`])
|
/// or [`CatchPerformanceAttributes`])
|
||||||
/// - a beatmap ([`CatchBeatmap<'map>`])
|
/// - a [`Beatmap`] (by reference or value)
|
||||||
///
|
///
|
||||||
/// If a map is given, difficulty attributes will need to be calculated
|
/// If a map is given, difficulty attributes will need to be calculated
|
||||||
/// internally which is a costly operation. Hence, passing attributes
|
/// internally which is a costly operation. Hence, passing attributes
|
||||||
@@ -47,20 +49,19 @@ impl<'map> CatchPerformance<'map> {
|
|||||||
/// have been calculated for the same map and [`Difficulty`] settings.
|
/// have been calculated for the same map and [`Difficulty`] settings.
|
||||||
/// Otherwise, the final attributes will be incorrect.
|
/// Otherwise, the final attributes will be incorrect.
|
||||||
///
|
///
|
||||||
/// [`CatchBeatmap<'map>`]: crate::catch::CatchBeatmap
|
/// [`Beatmap`]: crate::model::beatmap::Beatmap
|
||||||
pub fn new(map_or_attrs: impl IntoModePerformance<'map, Catch>) -> Self {
|
pub fn new(map_or_attrs: impl IntoModePerformance<'map, Catch>) -> Self {
|
||||||
map_or_attrs.into_performance()
|
map_or_attrs.into_performance()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to create a new performance calculator for osu!catch maps.
|
/// Try to create a new performance calculator for osu!catch maps.
|
||||||
///
|
///
|
||||||
/// Returns `None` if `map_or_attrs` does not belong to osu!catch e.g.
|
/// Returns `None` if `map_or_attrs` does not belong to osu!catch i.e.
|
||||||
/// a [`Converted`], [`DifficultyAttributes`], or [`PerformanceAttributes`]
|
/// a [`DifficultyAttributes`] or [`PerformanceAttributes`] of a different
|
||||||
/// of a different mode.
|
/// mode.
|
||||||
///
|
///
|
||||||
/// See [`CatchPerformance::new`] for more information.
|
/// See [`CatchPerformance::new`] for more information.
|
||||||
///
|
///
|
||||||
/// [`Converted`]: crate::model::beatmap::Converted
|
|
||||||
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
||||||
/// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
|
/// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
|
||||||
pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
|
pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
|
||||||
@@ -262,10 +263,10 @@ impl<'map> CatchPerformance<'map> {
|
|||||||
|
|
||||||
/// Create the [`CatchScoreState`] that will be used for performance calculation.
|
/// Create the [`CatchScoreState`] that will be used for performance calculation.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn generate_state(&mut self) -> CatchScoreState {
|
pub fn generate_state(&mut self) -> Result<CatchScoreState, ConvertError> {
|
||||||
let attrs = match self.map_or_attrs {
|
let attrs = match self.map_or_attrs {
|
||||||
MapOrAttrs::Map(ref map) => {
|
MapOrAttrs::Map(ref map) => {
|
||||||
let attrs = self.difficulty.with_mode().calculate(map);
|
let attrs = self.difficulty.calculate_for_mode::<Catch>(map)?;
|
||||||
|
|
||||||
self.map_or_attrs.insert_attrs(attrs)
|
self.map_or_attrs.insert_attrs(attrs)
|
||||||
}
|
}
|
||||||
@@ -411,16 +412,16 @@ impl<'map> CatchPerformance<'map> {
|
|||||||
self.tiny_droplet_misses = Some(best_state.tiny_droplet_misses);
|
self.tiny_droplet_misses = Some(best_state.tiny_droplet_misses);
|
||||||
self.misses = Some(best_state.misses);
|
self.misses = Some(best_state.misses);
|
||||||
|
|
||||||
best_state
|
Ok(best_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> CatchPerformanceAttributes {
|
pub fn calculate(mut self) -> Result<CatchPerformanceAttributes, ConvertError> {
|
||||||
let state = self.generate_state();
|
let state = self.generate_state()?;
|
||||||
|
|
||||||
let attrs = match self.map_or_attrs {
|
let attrs = match self.map_or_attrs {
|
||||||
MapOrAttrs::Map(ref map) => self.difficulty.with_mode().calculate(map),
|
|
||||||
MapOrAttrs::Attrs(attrs) => attrs,
|
MapOrAttrs::Attrs(attrs) => attrs,
|
||||||
|
MapOrAttrs::Map(ref map) => self.difficulty.calculate_for_mode::<Catch>(map)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = CatchPerformanceInner {
|
let inner = CatchPerformanceInner {
|
||||||
@@ -429,7 +430,7 @@ impl<'map> CatchPerformance<'map> {
|
|||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
inner.calculate()
|
Ok(inner.calculate())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Catch>) -> Self {
|
pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Catch>) -> Self {
|
||||||
@@ -456,14 +457,12 @@ impl<'map> TryFrom<OsuPerformance<'map>> for CatchPerformance<'map> {
|
|||||||
/// if it was constructed through attributes or
|
/// if it was constructed through attributes or
|
||||||
/// [`OsuPerformance::generate_state`] was called.
|
/// [`OsuPerformance::generate_state`] was called.
|
||||||
fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
|
fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
|
||||||
let MapOrAttrs::Map(converted) = osu.map_or_attrs else {
|
let mods = osu.difficulty.get_mods();
|
||||||
return Err(osu);
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = match converted.try_convert() {
|
let map = match OsuPerformance::try_convert_map(osu.map_or_attrs, GameMode::Catch, mods) {
|
||||||
Ok(map) => map,
|
Ok(map) => map,
|
||||||
Err(map) => {
|
Err(map_or_attrs) => {
|
||||||
osu.map_or_attrs = MapOrAttrs::Map(map);
|
osu.map_or_attrs = map_or_attrs;
|
||||||
|
|
||||||
return Err(osu);
|
return Err(osu);
|
||||||
}
|
}
|
||||||
@@ -607,7 +606,7 @@ mod test {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{DifficultyAttributes, PerformanceAttributes},
|
any::{DifficultyAttributes, PerformanceAttributes},
|
||||||
osu::{Osu, OsuDifficultyAttributes, OsuPerformanceAttributes},
|
osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||||
Beatmap,
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -626,8 +625,8 @@ mod test {
|
|||||||
fn attrs() -> CatchDifficultyAttributes {
|
fn attrs() -> CatchDifficultyAttributes {
|
||||||
ATTRS
|
ATTRS
|
||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
let converted = beatmap().unchecked_into_converted::<Catch>();
|
let map = beatmap();
|
||||||
let attrs = Difficulty::new().with_mode().calculate(&converted);
|
let attrs = Difficulty::new().calculate_for_mode::<Catch>(&map).unwrap();
|
||||||
|
|
||||||
assert_eq!(N_FRUITS, attrs.n_fruits);
|
assert_eq!(N_FRUITS, attrs.n_fruits);
|
||||||
assert_eq!(N_DROPLETS, attrs.n_droplets);
|
assert_eq!(N_DROPLETS, attrs.n_droplets);
|
||||||
@@ -782,8 +781,8 @@ mod test {
|
|||||||
state = state.misses(misses);
|
state = state.misses(misses);
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = state.generate_state();
|
let first = state.generate_state().unwrap();
|
||||||
let state = state.generate_state();
|
let state = state.generate_state().unwrap();
|
||||||
assert_eq!(first, state);
|
assert_eq!(first, state);
|
||||||
|
|
||||||
let expected = brute_force_best(
|
let expected = brute_force_best(
|
||||||
@@ -807,7 +806,8 @@ mod test {
|
|||||||
.tiny_droplets(N_TINY_DROPLETS - 50)
|
.tiny_droplets(N_TINY_DROPLETS - 50)
|
||||||
.tiny_droplet_misses(20)
|
.tiny_droplet_misses(20)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = CatchScoreState {
|
let expected = CatchScoreState {
|
||||||
max_combo: N_FRUITS + N_DROPLETS - 2,
|
max_combo: N_FRUITS + N_DROPLETS - 2,
|
||||||
@@ -824,12 +824,11 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn create() {
|
fn create() {
|
||||||
let mut map = beatmap();
|
let mut map = beatmap();
|
||||||
let converted = map.unchecked_as_converted();
|
|
||||||
|
|
||||||
let _ = CatchPerformance::new(CatchDifficultyAttributes::default());
|
let _ = CatchPerformance::new(CatchDifficultyAttributes::default());
|
||||||
let _ = CatchPerformance::new(CatchPerformanceAttributes::default());
|
let _ = CatchPerformance::new(CatchPerformanceAttributes::default());
|
||||||
let _ = CatchPerformance::new(&converted);
|
let _ = CatchPerformance::new(&map);
|
||||||
let _ = CatchPerformance::new(converted.as_owned());
|
let _ = CatchPerformance::new(map.clone());
|
||||||
|
|
||||||
let _ = CatchPerformance::try_new(CatchDifficultyAttributes::default()).unwrap();
|
let _ = CatchPerformance::try_new(CatchDifficultyAttributes::default()).unwrap();
|
||||||
let _ = CatchPerformance::try_new(CatchPerformanceAttributes::default()).unwrap();
|
let _ = CatchPerformance::try_new(CatchPerformanceAttributes::default()).unwrap();
|
||||||
@@ -841,19 +840,20 @@ mod test {
|
|||||||
CatchPerformanceAttributes::default(),
|
CatchPerformanceAttributes::default(),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = CatchPerformance::try_new(&converted).unwrap();
|
let _ = CatchPerformance::try_new(&map).unwrap();
|
||||||
let _ = CatchPerformance::try_new(converted.as_owned()).unwrap();
|
let _ = CatchPerformance::try_new(map.clone()).unwrap();
|
||||||
|
|
||||||
let _ = CatchPerformance::from(CatchDifficultyAttributes::default());
|
let _ = CatchPerformance::from(CatchDifficultyAttributes::default());
|
||||||
let _ = CatchPerformance::from(CatchPerformanceAttributes::default());
|
let _ = CatchPerformance::from(CatchPerformanceAttributes::default());
|
||||||
let _ = CatchPerformance::from(&converted);
|
let _ = CatchPerformance::from(&map);
|
||||||
let _ = CatchPerformance::from(converted);
|
let _ = CatchPerformance::from(map.clone());
|
||||||
|
|
||||||
let _ = CatchDifficultyAttributes::default().performance();
|
let _ = CatchDifficultyAttributes::default().performance();
|
||||||
let _ = CatchPerformanceAttributes::default().performance();
|
let _ = CatchPerformanceAttributes::default().performance();
|
||||||
|
|
||||||
map.mode = GameMode::Osu;
|
assert!(map
|
||||||
let converted = map.unchecked_as_converted::<Osu>();
|
.convert_mut(GameMode::Osu, &GameMods::default())
|
||||||
|
.is_err());
|
||||||
|
|
||||||
assert!(CatchPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
assert!(CatchPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
||||||
assert!(CatchPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
assert!(CatchPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
||||||
@@ -865,7 +865,5 @@ mod test {
|
|||||||
OsuPerformanceAttributes::default()
|
OsuPerformanceAttributes::default()
|
||||||
))
|
))
|
||||||
.is_none());
|
.is_none());
|
||||||
assert!(CatchPerformance::try_new(&converted).is_none());
|
|
||||||
assert!(CatchPerformance::try_new(converted).is_none());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::{any::Difficulty, catch::difficulty::DifficultyValues};
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use super::convert::CatchBeatmap;
|
use crate::{
|
||||||
|
any::Difficulty, catch::difficulty::DifficultyValues, model::mode::ConvertError, Beatmap,
|
||||||
|
};
|
||||||
|
|
||||||
/// The result of calculating the strains on a osu!catch map.
|
/// The result of calculating the strains on a osu!catch map.
|
||||||
///
|
///
|
||||||
@@ -16,10 +18,11 @@ impl CatchStrains {
|
|||||||
pub const SECTION_LEN: f64 = 750.0;
|
pub const SECTION_LEN: f64 = 750.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn strains(difficulty: &Difficulty, converted: &CatchBeatmap<'_>) -> CatchStrains {
|
pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<CatchStrains, ConvertError> {
|
||||||
let DifficultyValues { movement, .. } = DifficultyValues::calculate(difficulty, converted);
|
let map = map.convert_ref(GameMode::Catch, difficulty.get_mods())?;
|
||||||
|
let DifficultyValues { movement, .. } = DifficultyValues::calculate(difficulty, &map);
|
||||||
|
|
||||||
CatchStrains {
|
Ok(CatchStrains {
|
||||||
movement: movement.get_curr_strain_peaks().into_vec(),
|
movement: movement.get_curr_strain_peaks().into_vec(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -159,10 +159,7 @@
|
|||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use self::{
|
pub use self::{
|
||||||
any::{Difficulty, GradualDifficulty, GradualPerformance, Performance},
|
any::{Difficulty, GradualDifficulty, GradualPerformance, Performance},
|
||||||
model::{
|
model::{beatmap::Beatmap, mods::GameMods},
|
||||||
beatmap::{Beatmap, Converted},
|
|
||||||
mods::GameMods,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Types for calculations of any mode.
|
/// Types for calculations of any mode.
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use rosu_map::{section::general::GameMode, util::Pos};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
beatmap::{Beatmap, Converted},
|
beatmap::Beatmap,
|
||||||
hit_object::{HitObjectKind, HoldNote, Spinner},
|
hit_object::{HitObjectKind, HoldNote, Spinner},
|
||||||
mode::ConvertStatus,
|
|
||||||
},
|
},
|
||||||
util::{limited_queue::LimitedQueue, random::Random, sort},
|
util::{limited_queue::LimitedQueue, random::Random, sort},
|
||||||
|
GameMods,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
@@ -18,38 +18,13 @@ use self::{
|
|||||||
pattern_type::PatternType,
|
pattern_type::PatternType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Mania;
|
|
||||||
|
|
||||||
mod pattern;
|
mod pattern;
|
||||||
mod pattern_generator;
|
mod pattern_generator;
|
||||||
mod pattern_type;
|
mod pattern_type;
|
||||||
|
|
||||||
/// A [`Beatmap`] for [`Mania`] calculations.
|
|
||||||
pub type ManiaBeatmap<'a> = Converted<'a, Mania>;
|
|
||||||
|
|
||||||
const MAX_NOTES_FOR_DENSITY: usize = 7;
|
const MAX_NOTES_FOR_DENSITY: usize = 7;
|
||||||
|
|
||||||
pub const fn check_convert(map: &Beatmap) -> ConvertStatus {
|
pub fn convert(map: &mut Beatmap, mods: &GameMods) {
|
||||||
match map.mode {
|
|
||||||
GameMode::Osu => ConvertStatus::Conversion,
|
|
||||||
GameMode::Mania => ConvertStatus::Noop,
|
|
||||||
GameMode::Taiko | GameMode::Catch => ConvertStatus::Incompatible,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_convert(map: &mut Beatmap) -> ConvertStatus {
|
|
||||||
match map.mode {
|
|
||||||
GameMode::Osu => {
|
|
||||||
convert(map);
|
|
||||||
|
|
||||||
ConvertStatus::Conversion
|
|
||||||
}
|
|
||||||
GameMode::Mania => ConvertStatus::Noop,
|
|
||||||
GameMode::Taiko | GameMode::Catch => ConvertStatus::Incompatible,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert(map: &mut Beatmap) {
|
|
||||||
let seed = (map.hp + map.cs).round_ties_even() as i32 * 20
|
let seed = (map.hp + map.cs).round_ties_even() as i32 * 20
|
||||||
+ (map.od * 41.2) as i32
|
+ (map.od * 41.2) as i32
|
||||||
+ map.ar.round_ties_even() as i32;
|
+ map.ar.round_ties_even() as i32;
|
||||||
@@ -226,11 +201,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn convert_mania() {
|
fn convert_mania() {
|
||||||
let converted = Beatmap::from_path("./resources/2785319.osu")
|
let map = Beatmap::from_path("./resources/2785319.osu").unwrap();
|
||||||
.unwrap()
|
let map = map.convert(GameMode::Mania, &GameMods::default()).unwrap();
|
||||||
.unchecked_into_converted::<Mania>();
|
|
||||||
|
|
||||||
let map = &converted;
|
|
||||||
assert!(map.is_convert);
|
assert!(map.is_convert);
|
||||||
|
|
||||||
assert_eq!(map.mode, GameMode::Mania);
|
assert_eq!(map.mode, GameMode::Mania);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::skills::Skill,
|
any::difficulty::skills::Skill,
|
||||||
mania::{object::ObjectParams, ManiaBeatmap},
|
mania::object::ObjectParams,
|
||||||
model::{beatmap::HitWindows, hit_object::HitObject},
|
model::{beatmap::HitWindows, hit_object::HitObject, mode::ConvertError},
|
||||||
Difficulty,
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -65,18 +67,20 @@ struct NoteState {
|
|||||||
|
|
||||||
impl ManiaGradualDifficulty {
|
impl ManiaGradualDifficulty {
|
||||||
/// Create a new difficulty attributes iterator for osu!mania maps.
|
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||||
pub fn new(difficulty: Difficulty, converted: &ManiaBeatmap<'_>) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
|
||||||
|
let map = map.convert_ref(GameMode::Mania, difficulty.get_mods())?;
|
||||||
|
|
||||||
let take = difficulty.get_passed_objects();
|
let take = difficulty.get_passed_objects();
|
||||||
let total_columns = converted.cs.round_ties_even().max(1.0);
|
let total_columns = map.cs.round_ties_even().max(1.0);
|
||||||
let clock_rate = difficulty.get_clock_rate();
|
let clock_rate = difficulty.get_clock_rate();
|
||||||
let mut params = ObjectParams::new(converted);
|
let mut params = ObjectParams::new(&map);
|
||||||
|
|
||||||
let HitWindows {
|
let HitWindows {
|
||||||
od_great: hit_window,
|
od_great: hit_window,
|
||||||
..
|
..
|
||||||
} = converted.attributes().difficulty(&difficulty).hit_windows();
|
} = map.attributes().difficulty(&difficulty).hit_windows();
|
||||||
|
|
||||||
let mania_objects = converted
|
let mania_objects = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.map(|h| ManiaObject::new(h, total_columns, &mut params))
|
.map(|h| ManiaObject::new(h, total_columns, &mut params))
|
||||||
@@ -88,13 +92,10 @@ impl ManiaGradualDifficulty {
|
|||||||
|
|
||||||
let mut note_state = NoteState::default();
|
let mut note_state = NoteState::default();
|
||||||
|
|
||||||
let objects_is_circle: Box<[_]> = converted
|
let objects_is_circle: Box<[_]> =
|
||||||
.hit_objects
|
map.hit_objects.iter().map(HitObject::is_circle).collect();
|
||||||
.iter()
|
|
||||||
.map(HitObject::is_circle)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(h) = converted.hit_objects.first() {
|
if let Some(h) = map.hit_objects.first() {
|
||||||
let hit_object = ManiaObject::new(h, total_columns, &mut params);
|
let hit_object = ManiaObject::new(h, total_columns, &mut params);
|
||||||
|
|
||||||
increment_combo_raw(
|
increment_combo_raw(
|
||||||
@@ -105,16 +106,16 @@ impl ManiaGradualDifficulty {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Ok(Self {
|
||||||
idx: 0,
|
idx: 0,
|
||||||
difficulty,
|
difficulty,
|
||||||
objects_is_circle,
|
objects_is_circle,
|
||||||
is_convert: converted.is_convert,
|
is_convert: map.is_convert,
|
||||||
strain,
|
strain,
|
||||||
diff_objects,
|
diff_objects,
|
||||||
hit_window,
|
hit_window,
|
||||||
note_state,
|
note_state,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,28 +225,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn empty() {
|
||||||
let converted = Beatmap::from_bytes(&[])
|
let map = Beatmap::from_bytes(&[]).unwrap();
|
||||||
.unwrap()
|
let mut gradual = ManiaGradualDifficulty::new(Difficulty::new(), &map).unwrap();
|
||||||
.unchecked_into_converted::<Mania>();
|
|
||||||
|
|
||||||
let mut gradual = ManiaGradualDifficulty::new(Difficulty::new(), &converted);
|
|
||||||
|
|
||||||
assert!(gradual.next().is_none());
|
assert!(gradual.next().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_and_nth() {
|
fn next_and_nth() {
|
||||||
let converted = Beatmap::from_path("./resources/1638954.osu")
|
let map = Beatmap::from_path("./resources/1638954.osu").unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted::<Mania>();
|
|
||||||
|
|
||||||
let difficulty = Difficulty::new();
|
let difficulty = Difficulty::new();
|
||||||
|
|
||||||
let mut gradual = ManiaGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_2nd = ManiaGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual_2nd = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_3rd = ManiaGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual_3rd = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
|
|
||||||
let hit_objects_len = converted.hit_objects.len();
|
let hit_objects_len = map.hit_objects.len();
|
||||||
|
|
||||||
for i in 1.. {
|
for i in 1.. {
|
||||||
let Some(next_gradual) = gradual.next() else {
|
let Some(next_gradual) = gradual.next() else {
|
||||||
@@ -268,8 +263,8 @@ mod tests {
|
|||||||
let expected = difficulty
|
let expected = difficulty
|
||||||
.clone()
|
.clone()
|
||||||
.passed_objects(i as u32)
|
.passed_objects(i as u32)
|
||||||
.with_mode()
|
.calculate_for_mode::<Mania>(&map)
|
||||||
.calculate(&converted);
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(next_gradual, expected);
|
assert_eq!(next_gradual, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-13
@@ -1,14 +1,18 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::{skills::Skill, Difficulty},
|
any::difficulty::{skills::Skill, Difficulty},
|
||||||
mania::{
|
mania::{
|
||||||
difficulty::{object::ManiaDifficultyObject, skills::strain::Strain},
|
difficulty::{object::ManiaDifficultyObject, skills::strain::Strain},
|
||||||
object::{ManiaObject, ObjectParams},
|
object::{ManiaObject, ObjectParams},
|
||||||
},
|
},
|
||||||
|
model::mode::ConvertError,
|
||||||
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{attributes::ManiaDifficultyAttributes, convert::ManiaBeatmap};
|
use super::attributes::ManiaDifficultyAttributes;
|
||||||
|
|
||||||
pub mod gradual;
|
pub mod gradual;
|
||||||
mod object;
|
mod object;
|
||||||
@@ -18,26 +22,28 @@ const DIFFICULTY_MULTIPLIER: f64 = 0.018;
|
|||||||
|
|
||||||
pub fn difficulty(
|
pub fn difficulty(
|
||||||
difficulty: &Difficulty,
|
difficulty: &Difficulty,
|
||||||
converted: &ManiaBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> ManiaDifficultyAttributes {
|
) -> Result<ManiaDifficultyAttributes, ConvertError> {
|
||||||
let n_objects = cmp::min(difficulty.get_passed_objects(), converted.hit_objects.len()) as u32;
|
let map = map.convert_ref(GameMode::Mania, difficulty.get_mods())?;
|
||||||
|
|
||||||
let values = DifficultyValues::calculate(difficulty, converted);
|
let n_objects = cmp::min(difficulty.get_passed_objects(), map.hit_objects.len()) as u32;
|
||||||
|
|
||||||
let hit_window = converted
|
let values = DifficultyValues::calculate(difficulty, &map);
|
||||||
|
|
||||||
|
let hit_window = map
|
||||||
.attributes()
|
.attributes()
|
||||||
.difficulty(difficulty)
|
.difficulty(difficulty)
|
||||||
.hit_windows()
|
.hit_windows()
|
||||||
.od_great;
|
.od_great;
|
||||||
|
|
||||||
ManiaDifficultyAttributes {
|
Ok(ManiaDifficultyAttributes {
|
||||||
stars: values.strain.difficulty_value() * DIFFICULTY_MULTIPLIER,
|
stars: values.strain.difficulty_value() * DIFFICULTY_MULTIPLIER,
|
||||||
hit_window,
|
hit_window,
|
||||||
max_combo: values.max_combo,
|
max_combo: values.max_combo,
|
||||||
n_objects,
|
n_objects,
|
||||||
n_hold_notes: values.n_hold_notes,
|
n_hold_notes: values.n_hold_notes,
|
||||||
is_convert: converted.is_convert,
|
is_convert: map.is_convert,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DifficultyValues {
|
pub struct DifficultyValues {
|
||||||
@@ -47,13 +53,13 @@ pub struct DifficultyValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DifficultyValues {
|
impl DifficultyValues {
|
||||||
pub fn calculate(difficulty: &Difficulty, converted: &ManiaBeatmap<'_>) -> Self {
|
pub fn calculate(difficulty: &Difficulty, map: &Beatmap) -> Self {
|
||||||
let take = difficulty.get_passed_objects();
|
let take = difficulty.get_passed_objects();
|
||||||
let total_columns = converted.cs.round_ties_even().max(1.0);
|
let total_columns = map.cs.round_ties_even().max(1.0);
|
||||||
let clock_rate = difficulty.get_clock_rate();
|
let clock_rate = difficulty.get_clock_rate();
|
||||||
let mut params = ObjectParams::new(converted);
|
let mut params = ObjectParams::new(map);
|
||||||
|
|
||||||
let mania_objects = converted
|
let mania_objects = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.map(|h| ManiaObject::new(h, total_columns, &mut params))
|
.map(|h| ManiaObject::new(h, total_columns, &mut params))
|
||||||
|
|||||||
+21
-21
@@ -1,14 +1,15 @@
|
|||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
beatmap::Beatmap,
|
beatmap::Beatmap,
|
||||||
mode::{ConvertStatus, IGameMode},
|
mode::{ConvertError, IGameMode},
|
||||||
},
|
},
|
||||||
Difficulty,
|
Difficulty, GameMods,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
attributes::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
|
attributes::{ManiaDifficultyAttributes, ManiaPerformanceAttributes},
|
||||||
convert::ManiaBeatmap,
|
|
||||||
difficulty::gradual::ManiaGradualDifficulty,
|
difficulty::gradual::ManiaGradualDifficulty,
|
||||||
performance::{gradual::ManiaGradualPerformance, ManiaPerformance},
|
performance::{gradual::ManiaGradualPerformance, ManiaPerformance},
|
||||||
score_state::ManiaScoreState,
|
score_state::ManiaScoreState,
|
||||||
@@ -28,6 +29,13 @@ mod strains;
|
|||||||
/// [`GameMode::Mania`]: rosu_map::section::general::GameMode::Mania
|
/// [`GameMode::Mania`]: rosu_map::section::general::GameMode::Mania
|
||||||
pub struct Mania;
|
pub struct Mania;
|
||||||
|
|
||||||
|
impl Mania {
|
||||||
|
pub(crate) fn convert(map: &mut Beatmap, mods: &GameMods) {
|
||||||
|
debug_assert!(!map.is_convert && map.mode == GameMode::Osu);
|
||||||
|
convert::convert(map, mods);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IGameMode for Mania {
|
impl IGameMode for Mania {
|
||||||
type DifficultyAttributes = ManiaDifficultyAttributes;
|
type DifficultyAttributes = ManiaDifficultyAttributes;
|
||||||
type Strains = ManiaStrains;
|
type Strains = ManiaStrains;
|
||||||
@@ -35,40 +43,32 @@ impl IGameMode for Mania {
|
|||||||
type GradualDifficulty = ManiaGradualDifficulty;
|
type GradualDifficulty = ManiaGradualDifficulty;
|
||||||
type GradualPerformance = ManiaGradualPerformance;
|
type GradualPerformance = ManiaGradualPerformance;
|
||||||
|
|
||||||
fn check_convert(map: &Beatmap) -> ConvertStatus {
|
|
||||||
convert::check_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_convert(map: &mut Beatmap) -> ConvertStatus {
|
|
||||||
convert::try_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn difficulty(
|
fn difficulty(
|
||||||
difficulty: &Difficulty,
|
difficulty: &Difficulty,
|
||||||
converted: &ManiaBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::DifficultyAttributes {
|
) -> Result<Self::DifficultyAttributes, ConvertError> {
|
||||||
difficulty::difficulty(difficulty, converted)
|
difficulty::difficulty(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strains(difficulty: &Difficulty, converted: &ManiaBeatmap<'_>) -> Self::Strains {
|
fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<Self::Strains, ConvertError> {
|
||||||
strains::strains(difficulty, converted)
|
strains::strains(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn performance(map: ManiaBeatmap<'_>) -> Self::Performance<'_> {
|
fn performance(map: &Beatmap) -> Self::Performance<'_> {
|
||||||
ManiaPerformance::new(map)
|
ManiaPerformance::new(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradual_difficulty(
|
fn gradual_difficulty(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &ManiaBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualDifficulty {
|
) -> Result<Self::GradualDifficulty, ConvertError> {
|
||||||
ManiaGradualDifficulty::new(difficulty, map)
|
ManiaGradualDifficulty::new(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradual_performance(
|
fn gradual_performance(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &ManiaBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualPerformance {
|
) -> Result<Self::GradualPerformance, ConvertError> {
|
||||||
ManiaGradualPerformance::new(difficulty, map)
|
ManiaGradualPerformance::new(difficulty, map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
use crate::{
|
use crate::{mania::ManiaGradualDifficulty, model::mode::ConvertError, Beatmap, Difficulty};
|
||||||
mania::{ManiaBeatmap, ManiaGradualDifficulty},
|
|
||||||
Difficulty,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{ManiaPerformanceAttributes, ManiaScoreState};
|
use super::{ManiaPerformanceAttributes, ManiaScoreState};
|
||||||
|
|
||||||
@@ -75,10 +72,10 @@ pub struct ManiaGradualPerformance {
|
|||||||
|
|
||||||
impl ManiaGradualPerformance {
|
impl ManiaGradualPerformance {
|
||||||
/// Create a new gradual performance calculator for osu!mania maps.
|
/// Create a new gradual performance calculator for osu!mania maps.
|
||||||
pub fn new(difficulty: Difficulty, converted: &ManiaBeatmap<'_>) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
|
||||||
let difficulty = ManiaGradualDifficulty::new(difficulty, converted);
|
let difficulty = ManiaGradualDifficulty::new(difficulty, map)?;
|
||||||
|
|
||||||
Self { difficulty }
|
Ok(Self { difficulty })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process the next hit object and calculate the performance attributes
|
/// Process the next hit object and calculate the performance attributes
|
||||||
@@ -98,6 +95,7 @@ impl ManiaGradualPerformance {
|
|||||||
///
|
///
|
||||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
||||||
/// `n=1` will process 2, and so on.
|
/// `n=1` will process 2, and so on.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
|
pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
|
||||||
let performance = self
|
let performance = self
|
||||||
.difficulty
|
.difficulty
|
||||||
@@ -106,7 +104,8 @@ impl ManiaGradualPerformance {
|
|||||||
.state(state)
|
.state(state)
|
||||||
.difficulty(self.difficulty.difficulty.clone())
|
.difficulty(self.difficulty.difficulty.clone())
|
||||||
.passed_objects(self.difficulty.idx as u32)
|
.passed_objects(self.difficulty.idx as u32)
|
||||||
.calculate();
|
.calculate()
|
||||||
|
.expect("no conversion required");
|
||||||
|
|
||||||
Some(performance)
|
Some(performance)
|
||||||
}
|
}
|
||||||
@@ -120,34 +119,29 @@ impl ManiaGradualPerformance {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{mania::ManiaPerformance, Beatmap};
|
||||||
mania::{Mania, ManiaPerformance},
|
|
||||||
Beatmap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_and_nth() {
|
fn next_and_nth() {
|
||||||
let converted = Beatmap::from_path("./resources/1638954.osu")
|
let map = Beatmap::from_path("./resources/1638954.osu").unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted::<Mania>();
|
|
||||||
|
|
||||||
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
||||||
|
|
||||||
let mut gradual = ManiaGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual = ManiaGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_2nd = ManiaGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual_2nd = ManiaGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_3rd = ManiaGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual_3rd = ManiaGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
|
|
||||||
let mut state = ManiaScoreState::default();
|
let mut state = ManiaScoreState::default();
|
||||||
|
|
||||||
let hit_objects_len = converted.hit_objects.len();
|
let hit_objects_len = map.hit_objects.len();
|
||||||
|
|
||||||
for i in 1.. {
|
for i in 1.. {
|
||||||
state.misses += 1;
|
state.misses += 1;
|
||||||
|
|
||||||
// Hold notes award two hitresults in lazer
|
// Hold notes award two hitresults in lazer
|
||||||
if let Some(h) = converted.hit_objects.get(i - 1) {
|
if let Some(h) = map.hit_objects.get(i - 1) {
|
||||||
if !h.is_circle() {
|
if !h.is_circle() {
|
||||||
state.n320 += 1;
|
state.n320 += 1;
|
||||||
}
|
}
|
||||||
@@ -170,15 +164,15 @@ mod tests {
|
|||||||
assert_eq!(next_gradual, next_gradual_3rd);
|
assert_eq!(next_gradual, next_gradual_3rd);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut regular_calc = ManiaPerformance::new(converted.as_owned())
|
let mut regular_calc = ManiaPerformance::new(&map)
|
||||||
.difficulty(difficulty.clone())
|
.difficulty(difficulty.clone())
|
||||||
.passed_objects(i as u32)
|
.passed_objects(i as u32)
|
||||||
.state(state.clone());
|
.state(state.clone());
|
||||||
|
|
||||||
let regular_state = regular_calc.generate_state();
|
let regular_state = regular_calc.generate_state().unwrap();
|
||||||
assert_eq!(state, regular_state);
|
assert_eq!(state, regular_state);
|
||||||
|
|
||||||
let expected = regular_calc.calculate();
|
let expected = regular_calc.calculate().unwrap();
|
||||||
|
|
||||||
assert_eq!(next_gradual, expected);
|
assert_eq!(next_gradual, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance},
|
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance},
|
||||||
model::mods::GameMods,
|
model::{mode::ConvertError, mods::GameMods},
|
||||||
osu::OsuPerformance,
|
osu::OsuPerformance,
|
||||||
util::map_or_attrs::MapOrAttrs,
|
util::map_or_attrs::MapOrAttrs,
|
||||||
Performance,
|
Performance,
|
||||||
@@ -38,7 +40,7 @@ impl<'map> ManiaPerformance<'map> {
|
|||||||
/// The argument `map_or_attrs` must be either
|
/// The argument `map_or_attrs` must be either
|
||||||
/// - previously calculated attributes ([`ManiaDifficultyAttributes`]
|
/// - previously calculated attributes ([`ManiaDifficultyAttributes`]
|
||||||
/// or [`ManiaPerformanceAttributes`])
|
/// or [`ManiaPerformanceAttributes`])
|
||||||
/// - a beatmap ([`ManiaBeatmap<'map>`])
|
/// - a [`Beatmap`] (by reference or value)
|
||||||
///
|
///
|
||||||
/// If a map is given, difficulty attributes will need to be calculated
|
/// If a map is given, difficulty attributes will need to be calculated
|
||||||
/// internally which is a costly operation. Hence, passing attributes
|
/// internally which is a costly operation. Hence, passing attributes
|
||||||
@@ -48,20 +50,19 @@ impl<'map> ManiaPerformance<'map> {
|
|||||||
/// have been calculated for the same map and [`Difficulty`] settings.
|
/// have been calculated for the same map and [`Difficulty`] settings.
|
||||||
/// Otherwise, the final attributes will be incorrect.
|
/// Otherwise, the final attributes will be incorrect.
|
||||||
///
|
///
|
||||||
/// [`ManiaBeatmap<'map>`]: crate::mania::ManiaBeatmap
|
/// [`Beatmap`]: crate::model::beatmap::Beatmap
|
||||||
pub fn new(map_or_attrs: impl IntoModePerformance<'map, Mania>) -> Self {
|
pub fn new(map_or_attrs: impl IntoModePerformance<'map, Mania>) -> Self {
|
||||||
map_or_attrs.into_performance()
|
map_or_attrs.into_performance()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to create a new performance calculator for osu!mania maps.
|
/// Try to create a new performance calculator for osu!mania maps.
|
||||||
///
|
///
|
||||||
/// Returns `None` if `map_or_attrs` does not belong to osu!mania e.g.
|
/// Returns `None` if `map_or_attrs` does not belong to osu!mania i.e.
|
||||||
/// a [`Converted`], [`DifficultyAttributes`], or [`PerformanceAttributes`]
|
/// a [`DifficultyAttributes`] or [`PerformanceAttributes`] of a different
|
||||||
/// of a different mode.
|
/// mode.
|
||||||
///
|
///
|
||||||
/// See [`ManiaPerformance::new`] for more information.
|
/// See [`ManiaPerformance::new`] for more information.
|
||||||
///
|
///
|
||||||
/// [`Converted`]: crate::model::beatmap::Converted
|
|
||||||
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
||||||
/// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
|
/// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
|
||||||
pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
|
pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
|
||||||
@@ -248,10 +249,10 @@ impl<'map> ManiaPerformance<'map> {
|
|||||||
|
|
||||||
/// Create the [`ManiaScoreState`] that will be used for performance calculation.
|
/// Create the [`ManiaScoreState`] that will be used for performance calculation.
|
||||||
#[allow(clippy::too_many_lines, clippy::similar_names)]
|
#[allow(clippy::too_many_lines, clippy::similar_names)]
|
||||||
pub fn generate_state(&mut self) -> ManiaScoreState {
|
pub fn generate_state(&mut self) -> Result<ManiaScoreState, ConvertError> {
|
||||||
let attrs = match self.map_or_attrs {
|
let attrs = match self.map_or_attrs {
|
||||||
MapOrAttrs::Map(ref map) => {
|
MapOrAttrs::Map(ref map) => {
|
||||||
let attrs = self.difficulty.with_mode().calculate(map);
|
let attrs = self.difficulty.calculate_for_mode::<Mania>(map)?;
|
||||||
|
|
||||||
self.map_or_attrs.insert_attrs(attrs)
|
self.map_or_attrs.insert_attrs(attrs)
|
||||||
}
|
}
|
||||||
@@ -774,23 +775,23 @@ impl<'map> ManiaPerformance<'map> {
|
|||||||
self.n50 = Some(n50);
|
self.n50 = Some(n50);
|
||||||
self.misses = Some(misses);
|
self.misses = Some(misses);
|
||||||
|
|
||||||
ManiaScoreState {
|
Ok(ManiaScoreState {
|
||||||
n320,
|
n320,
|
||||||
n300,
|
n300,
|
||||||
n200,
|
n200,
|
||||||
n100,
|
n100,
|
||||||
n50,
|
n50,
|
||||||
misses,
|
misses,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> ManiaPerformanceAttributes {
|
pub fn calculate(mut self) -> Result<ManiaPerformanceAttributes, ConvertError> {
|
||||||
let state = self.generate_state();
|
let state = self.generate_state()?;
|
||||||
|
|
||||||
let attrs = match self.map_or_attrs {
|
let attrs = match self.map_or_attrs {
|
||||||
MapOrAttrs::Map(ref map) => self.difficulty.with_mode().calculate(map),
|
|
||||||
MapOrAttrs::Attrs(attrs) => attrs,
|
MapOrAttrs::Attrs(attrs) => attrs,
|
||||||
|
MapOrAttrs::Map(ref map) => self.difficulty.calculate_for_mode::<Mania>(map)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = ManiaPerformanceInner {
|
let inner = ManiaPerformanceInner {
|
||||||
@@ -799,7 +800,7 @@ impl<'map> ManiaPerformance<'map> {
|
|||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
inner.calculate()
|
Ok(inner.calculate())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Mania>) -> Self {
|
pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Mania>) -> Self {
|
||||||
@@ -827,14 +828,12 @@ impl<'map> TryFrom<OsuPerformance<'map>> for ManiaPerformance<'map> {
|
|||||||
/// if it was constructed through attributes or
|
/// if it was constructed through attributes or
|
||||||
/// [`OsuPerformance::generate_state`] was called.
|
/// [`OsuPerformance::generate_state`] was called.
|
||||||
fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
|
fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
|
||||||
let MapOrAttrs::Map(converted) = osu.map_or_attrs else {
|
let mods = osu.difficulty.get_mods();
|
||||||
return Err(osu);
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = match converted.try_convert() {
|
let map = match OsuPerformance::try_convert_map(osu.map_or_attrs, GameMode::Mania, mods) {
|
||||||
Ok(map) => map,
|
Ok(map) => map,
|
||||||
Err(map) => {
|
Err(map_or_attrs) => {
|
||||||
osu.map_or_attrs = MapOrAttrs::Map(map);
|
osu.map_or_attrs = map_or_attrs;
|
||||||
|
|
||||||
return Err(osu);
|
return Err(osu);
|
||||||
}
|
}
|
||||||
@@ -959,7 +958,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{DifficultyAttributes, PerformanceAttributes},
|
any::{DifficultyAttributes, PerformanceAttributes},
|
||||||
osu::{Osu, OsuDifficultyAttributes, OsuPerformanceAttributes},
|
osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||||
Beatmap,
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -977,17 +976,13 @@ mod tests {
|
|||||||
fn attrs() -> ManiaDifficultyAttributes {
|
fn attrs() -> ManiaDifficultyAttributes {
|
||||||
ATTRS
|
ATTRS
|
||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
let converted = beatmap().unchecked_into_converted::<Mania>();
|
let map = beatmap();
|
||||||
let attrs = Difficulty::new().with_mode().calculate(&converted);
|
let attrs = Difficulty::new().calculate_for_mode::<Mania>(&map).unwrap();
|
||||||
|
|
||||||
assert_eq!(N_OBJECTS, converted.hit_objects.len() as u32);
|
assert_eq!(N_OBJECTS, map.hit_objects.len() as u32);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
N_HOLD_NOTES,
|
N_HOLD_NOTES,
|
||||||
converted
|
map.hit_objects.iter().filter(|h| !h.is_circle()).count() as u32
|
||||||
.hit_objects
|
|
||||||
.iter()
|
|
||||||
.filter(|h| !h.is_circle())
|
|
||||||
.count() as u32
|
|
||||||
);
|
);
|
||||||
|
|
||||||
attrs
|
attrs
|
||||||
@@ -1277,8 +1272,8 @@ mod tests {
|
|||||||
state = state.misses(misses);
|
state = state.misses(misses);
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = state.generate_state();
|
let first = state.generate_state().unwrap();
|
||||||
let state = state.generate_state();
|
let state = state.generate_state().unwrap();
|
||||||
assert_eq!(first, state);
|
assert_eq!(first, state);
|
||||||
|
|
||||||
let expected = brute_force_best(
|
let expected = brute_force_best(
|
||||||
@@ -1304,7 +1299,8 @@ mod tests {
|
|||||||
.n320(500)
|
.n320(500)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.hitresult_priority(HitResultPriority::BestCase)
|
.hitresult_priority(HitResultPriority::BestCase)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = ManiaScoreState {
|
let expected = ManiaScoreState {
|
||||||
n320: 500,
|
n320: 500,
|
||||||
@@ -1326,7 +1322,8 @@ mod tests {
|
|||||||
.n50(50)
|
.n50(50)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.hitresult_priority(HitResultPriority::WorstCase)
|
.hitresult_priority(HitResultPriority::WorstCase)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = ManiaScoreState {
|
let expected = ManiaScoreState {
|
||||||
n320: 0,
|
n320: 0,
|
||||||
@@ -1343,12 +1340,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn create() {
|
fn create() {
|
||||||
let mut map = beatmap();
|
let mut map = beatmap();
|
||||||
let converted = map.unchecked_as_converted();
|
|
||||||
|
|
||||||
let _ = ManiaPerformance::new(ManiaDifficultyAttributes::default());
|
let _ = ManiaPerformance::new(ManiaDifficultyAttributes::default());
|
||||||
let _ = ManiaPerformance::new(ManiaPerformanceAttributes::default());
|
let _ = ManiaPerformance::new(ManiaPerformanceAttributes::default());
|
||||||
let _ = ManiaPerformance::new(&converted);
|
let _ = ManiaPerformance::new(&map);
|
||||||
let _ = ManiaPerformance::new(converted.as_owned());
|
let _ = ManiaPerformance::new(map.clone());
|
||||||
|
|
||||||
let _ = ManiaPerformance::try_new(ManiaDifficultyAttributes::default()).unwrap();
|
let _ = ManiaPerformance::try_new(ManiaDifficultyAttributes::default()).unwrap();
|
||||||
let _ = ManiaPerformance::try_new(ManiaPerformanceAttributes::default()).unwrap();
|
let _ = ManiaPerformance::try_new(ManiaPerformanceAttributes::default()).unwrap();
|
||||||
@@ -1360,19 +1356,20 @@ mod tests {
|
|||||||
ManiaPerformanceAttributes::default(),
|
ManiaPerformanceAttributes::default(),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = ManiaPerformance::try_new(&converted).unwrap();
|
let _ = ManiaPerformance::try_new(&map).unwrap();
|
||||||
let _ = ManiaPerformance::try_new(converted.as_owned()).unwrap();
|
let _ = ManiaPerformance::try_new(map.clone()).unwrap();
|
||||||
|
|
||||||
let _ = ManiaPerformance::from(ManiaDifficultyAttributes::default());
|
let _ = ManiaPerformance::from(ManiaDifficultyAttributes::default());
|
||||||
let _ = ManiaPerformance::from(ManiaPerformanceAttributes::default());
|
let _ = ManiaPerformance::from(ManiaPerformanceAttributes::default());
|
||||||
let _ = ManiaPerformance::from(&converted);
|
let _ = ManiaPerformance::from(&map);
|
||||||
let _ = ManiaPerformance::from(converted);
|
let _ = ManiaPerformance::from(map.clone());
|
||||||
|
|
||||||
let _ = ManiaDifficultyAttributes::default().performance();
|
let _ = ManiaDifficultyAttributes::default().performance();
|
||||||
let _ = ManiaPerformanceAttributes::default().performance();
|
let _ = ManiaPerformanceAttributes::default().performance();
|
||||||
|
|
||||||
map.mode = GameMode::Osu;
|
assert!(map
|
||||||
let converted = map.unchecked_as_converted::<Osu>();
|
.convert_mut(GameMode::Osu, &GameMods::default())
|
||||||
|
.is_err());
|
||||||
|
|
||||||
assert!(ManiaPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
assert!(ManiaPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
||||||
assert!(ManiaPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
assert!(ManiaPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
||||||
@@ -1384,7 +1381,5 @@ mod tests {
|
|||||||
OsuPerformanceAttributes::default()
|
OsuPerformanceAttributes::default()
|
||||||
))
|
))
|
||||||
.is_none());
|
.is_none());
|
||||||
assert!(ManiaPerformance::try_new(&converted).is_none());
|
|
||||||
assert!(ManiaPerformance::try_new(converted).is_none());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::{any::Difficulty, mania::difficulty::DifficultyValues};
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use super::convert::ManiaBeatmap;
|
use crate::{
|
||||||
|
any::Difficulty, mania::difficulty::DifficultyValues, model::mode::ConvertError, Beatmap,
|
||||||
|
};
|
||||||
|
|
||||||
/// The result of calculating the strains on a osu!mania map.
|
/// The result of calculating the strains on a osu!mania map.
|
||||||
///
|
///
|
||||||
@@ -16,10 +18,11 @@ impl ManiaStrains {
|
|||||||
pub const SECTION_LEN: f64 = 400.0;
|
pub const SECTION_LEN: f64 = 400.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn strains(difficulty: &Difficulty, converted: &ManiaBeatmap<'_>) -> ManiaStrains {
|
pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<ManiaStrains, ConvertError> {
|
||||||
let values = DifficultyValues::calculate(difficulty, converted);
|
let map = map.convert_ref(GameMode::Mania, difficulty.get_mods())?;
|
||||||
|
let values = DifficultyValues::calculate(difficulty, &map);
|
||||||
|
|
||||||
ManiaStrains {
|
Ok(ManiaStrains {
|
||||||
strains: values.strain.get_curr_strain_peaks().into_vec(),
|
strains: values.strain.get_curr_strain_peaks().into_vec(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use rosu_map::section::general::GameMode;
|
|||||||
|
|
||||||
use crate::{any::difficulty::ModsDependent, model::mods::GameMods, Difficulty};
|
use crate::{any::difficulty::ModsDependent, model::mods::GameMods, Difficulty};
|
||||||
|
|
||||||
use super::{converted::Converted, Beatmap};
|
use super::Beatmap;
|
||||||
|
|
||||||
/// Summary struct for a [`Beatmap`]'s attributes.
|
/// Summary struct for a [`Beatmap`]'s attributes.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -371,12 +371,6 @@ impl From<&Beatmap> for BeatmapAttributesBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M> From<&Converted<'_, M>> for BeatmapAttributesBuilder {
|
|
||||||
fn from(converted: &Converted<'_, M>) -> Self {
|
|
||||||
Self::new().map(converted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// False positive? Value looks consumed to me...
|
// False positive? Value looks consumed to me...
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
fn difficulty_range(difficulty: f64, windows: GameModeHitWindows) -> f64 {
|
fn difficulty_range(difficulty: f64, windows: GameModeHitWindows) -> f64 {
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
use std::{
|
|
||||||
borrow::Cow,
|
|
||||||
fmt::{Debug, Formatter, Result as FmtResult},
|
|
||||||
marker::PhantomData,
|
|
||||||
ops::Deref,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
model::mode::{ConvertStatus, IGameMode},
|
|
||||||
util::generic_fmt::GenericFormatter,
|
|
||||||
Difficulty,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Beatmap;
|
|
||||||
|
|
||||||
const INCOMPATIBLE_MODES: &str = "the gamemodes were incompatible";
|
|
||||||
|
|
||||||
/// A [`Beatmap`] that is attached to a mode.
|
|
||||||
///
|
|
||||||
/// # Incompatibility
|
|
||||||
///
|
|
||||||
/// The following conversions are compatible:
|
|
||||||
/// - `Osu` → `Osu`
|
|
||||||
/// - `Taiko` → `Taiko`
|
|
||||||
/// - `Catch` → `Catch`
|
|
||||||
/// - `Mania` → `Mania`
|
|
||||||
/// - `Osu` → `Taiko`
|
|
||||||
/// - `Osu` → `Catch`
|
|
||||||
/// - `Osu` → `Mania`
|
|
||||||
///
|
|
||||||
/// All other conversions are incompatible.
|
|
||||||
pub struct Converted<'a, M> {
|
|
||||||
map: Cow<'a, Beatmap>,
|
|
||||||
mode: PhantomData<M>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, M> Converted<'a, M> {
|
|
||||||
/// Initialize a [`Converted`] beatmap by promising the given map's mode
|
|
||||||
/// matches the generic type `M`.
|
|
||||||
pub(crate) const fn new(map: Cow<'a, Beatmap>) -> Self {
|
|
||||||
Self {
|
|
||||||
map,
|
|
||||||
mode: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the internal [`Beatmap`].
|
|
||||||
pub fn into_inner(self) -> Cow<'a, Beatmap> {
|
|
||||||
self.map
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Borrow the contained [`Beatmap`] to cheaply create a new owned
|
|
||||||
/// [`Converted`].
|
|
||||||
///
|
|
||||||
/// This is the same as `.clone()` except cheap - but its lifetime might be
|
|
||||||
/// shorter.
|
|
||||||
#[must_use]
|
|
||||||
pub fn as_owned(&'a self) -> Self {
|
|
||||||
Self::new(Cow::Borrowed(self.map.as_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M: IGameMode> Converted<'_, M> {
|
|
||||||
/// Attempt to convert a [`Beatmap`] to the specified mode.
|
|
||||||
///
|
|
||||||
/// If the conversion is incompatible the [`Beatmap`] will be returned
|
|
||||||
/// unchanged as `Err`.
|
|
||||||
#[allow(clippy::result_large_err)]
|
|
||||||
pub fn try_from_owned(mut map: Beatmap) -> Result<Self, Beatmap> {
|
|
||||||
match M::try_convert(&mut map) {
|
|
||||||
ConvertStatus::Noop => Ok(Self::new(Cow::Owned(map))),
|
|
||||||
ConvertStatus::Conversion => Ok(Self::new(Cow::Owned(map))),
|
|
||||||
ConvertStatus::Incompatible => Err(map),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a [`Beatmap`] to the specified mode.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics if the conversion is incompatible.
|
|
||||||
pub fn unchecked_from_owned(map: Beatmap) -> Self {
|
|
||||||
Self::try_from_owned(map).unwrap_or_else(|_| panic!("{}", INCOMPATIBLE_MODES))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a gradual difficulty calculator for the map.
|
|
||||||
pub fn gradual_difficulty(&self, difficulty: Difficulty) -> M::GradualDifficulty {
|
|
||||||
M::gradual_difficulty(difficulty, self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a gradual performance calculator for the map.
|
|
||||||
pub fn gradual_performance(&self, difficulty: Difficulty) -> M::GradualPerformance {
|
|
||||||
M::gradual_performance(difficulty, self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, M: IGameMode> Converted<'a, M> {
|
|
||||||
/// Create a performance calculator for the map.
|
|
||||||
pub fn performance(self) -> M::Performance<'a> {
|
|
||||||
M::performance(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to convert a [`&Beatmap`] to the specified mode.
|
|
||||||
///
|
|
||||||
/// If the conversion is incompatible, `None` is returned.
|
|
||||||
///
|
|
||||||
/// [`&Beatmap`]: Beatmap
|
|
||||||
pub fn try_from_ref(map: &'a Beatmap) -> Option<Self> {
|
|
||||||
let mut map = match M::check_convert(map) {
|
|
||||||
ConvertStatus::Noop => return Some(Self::new(Cow::Borrowed(map))),
|
|
||||||
ConvertStatus::Conversion => map.to_owned(),
|
|
||||||
ConvertStatus::Incompatible => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match M::try_convert(&mut map) {
|
|
||||||
ConvertStatus::Conversion => Some(Self::new(Cow::Owned(map))),
|
|
||||||
ConvertStatus::Noop => Some(Self::new(Cow::Owned(map))),
|
|
||||||
ConvertStatus::Incompatible => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a [`&Beatmap`] to the specified mode.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics if the conversion is incompatible.
|
|
||||||
///
|
|
||||||
/// [`&Beatmap`]: Beatmap
|
|
||||||
pub fn unchecked_from_ref(map: &'a Beatmap) -> Self {
|
|
||||||
Self::try_from_ref(map).expect(INCOMPATIBLE_MODES)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to convert a [`&mut Beatmap`] to the specified mode.
|
|
||||||
///
|
|
||||||
/// If the conversion is incompatible, `None` is returned.
|
|
||||||
///
|
|
||||||
/// [`&mut Beatmap`]: Beatmap
|
|
||||||
pub fn try_from_mut(map: &'a mut Beatmap) -> Option<Self> {
|
|
||||||
match M::try_convert(map) {
|
|
||||||
ConvertStatus::Conversion => Some(Self::new(Cow::Borrowed(map))),
|
|
||||||
ConvertStatus::Noop => Some(Self::new(Cow::Borrowed(map))),
|
|
||||||
ConvertStatus::Incompatible => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a [`&mut Beatmap`] to the specified mode.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics if the conversion is incompatible.
|
|
||||||
///
|
|
||||||
/// [`&mut Beatmap`]: Beatmap
|
|
||||||
pub fn unchecked_from_mut(map: &'a mut Beatmap) -> Self {
|
|
||||||
Self::try_from_mut(map).expect(INCOMPATIBLE_MODES)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to convert a [`Converted`] from mode `M` to mode `N`.
|
|
||||||
///
|
|
||||||
/// If the conversion is incompatible the [`Converted`] will be returned
|
|
||||||
/// unchanged as `Err`.
|
|
||||||
#[allow(clippy::result_large_err)]
|
|
||||||
pub fn try_convert<N: IGameMode>(self) -> Result<Converted<'a, N>, Self> {
|
|
||||||
match self.map {
|
|
||||||
Cow::Borrowed(map) => Converted::<N>::try_from_ref(map).ok_or(self),
|
|
||||||
Cow::Owned(map) => {
|
|
||||||
Converted::<N>::try_from_owned(map).map_err(|map| Self::new(Cow::Owned(map)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a [`Converted`] from mode `M` to mode `N`.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics if the conversion is incompatible.
|
|
||||||
pub fn unchecked_convert<N: IGameMode>(self) -> Converted<'a, N> {
|
|
||||||
match self.map {
|
|
||||||
Cow::Borrowed(map) => Converted::<N>::unchecked_from_ref(map),
|
|
||||||
Cow::Owned(map) => Converted::<N>::unchecked_from_owned(map),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> Clone for Converted<'_, M> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self::new(self.map.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> Debug for Converted<'_, M> {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
||||||
f.debug_struct("Converted")
|
|
||||||
.field("map", &self.map)
|
|
||||||
.field("mode", &GenericFormatter::<M>::new())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> PartialEq for Converted<'_, M> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.map == other.map
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> Deref for Converted<'_, M> {
|
|
||||||
type Target = Beatmap;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.map.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+51
-61
@@ -1,4 +1,4 @@
|
|||||||
use std::{io, path::Path, str::FromStr};
|
use std::{borrow::Cow, io, path::Path, str::FromStr};
|
||||||
|
|
||||||
use rosu_map::{
|
use rosu_map::{
|
||||||
section::{general::GameMode, hit_objects::hit_samples::HitSoundType},
|
section::{general::GameMode, hit_objects::hit_samples::HitSoundType},
|
||||||
@@ -8,13 +8,12 @@ use rosu_map::{
|
|||||||
pub use rosu_map::section::events::BreakPeriod;
|
pub use rosu_map::section::events::BreakPeriod;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
catch::Catch, mania::Mania, osu::Osu, taiko::Taiko, Difficulty, GradualDifficulty,
|
catch::Catch, mania::Mania, taiko::Taiko, Difficulty, GameMods, GradualDifficulty,
|
||||||
GradualPerformance, Performance,
|
GradualPerformance, Performance,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, HitWindows},
|
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, HitWindows},
|
||||||
converted::Converted,
|
|
||||||
decode::{BeatmapState, ParseBeatmapError},
|
decode::{BeatmapState, ParseBeatmapError},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,12 +23,11 @@ use super::{
|
|||||||
TimingPoint,
|
TimingPoint,
|
||||||
},
|
},
|
||||||
hit_object::HitObject,
|
hit_object::HitObject,
|
||||||
mode::{ConvertStatus, IGameMode},
|
mode::ConvertError,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod attributes;
|
mod attributes;
|
||||||
mod bpm;
|
mod bpm;
|
||||||
mod converted;
|
|
||||||
mod decode;
|
mod decode;
|
||||||
|
|
||||||
/// All beatmap data that is relevant for difficulty and performance
|
/// All beatmap data that is relevant for difficulty and performance
|
||||||
@@ -122,75 +120,67 @@ impl Beatmap {
|
|||||||
self.breaks.iter().map(BreakPeriod::duration).sum()
|
self.breaks.iter().map(BreakPeriod::duration).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a [`&mut Beatmap`] to the specified mode with an argument
|
/// Attempt to convert a [`Beatmap`] to the specified mode.
|
||||||
/// instead of a generic parameter.
|
pub fn convert(mut self, mode: GameMode, mods: &GameMods) -> Result<Self, ConvertError> {
|
||||||
///
|
self.convert_mut(mode, mods)?;
|
||||||
/// [`&mut Beatmap`]: Beatmap
|
|
||||||
pub fn convert_in_place(&mut self, mode: GameMode) -> ConvertStatus {
|
Ok(self)
|
||||||
match mode {
|
|
||||||
GameMode::Osu => Osu::try_convert(self),
|
|
||||||
GameMode::Taiko => Taiko::try_convert(self),
|
|
||||||
GameMode::Catch => Catch::try_convert(self),
|
|
||||||
GameMode::Mania => Mania::try_convert(self),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to convert a [`&Beatmap`] to the specified mode.
|
/// Attempt to convert a [`&Beatmap`] to the specified mode.
|
||||||
///
|
///
|
||||||
/// If the conversion is incompatible, `None` is returned.
|
|
||||||
///
|
|
||||||
/// [`&Beatmap`]: Beatmap
|
/// [`&Beatmap`]: Beatmap
|
||||||
pub fn try_as_converted<M: IGameMode>(&self) -> Option<Converted<'_, M>> {
|
pub fn convert_ref(
|
||||||
Converted::try_from_ref(self)
|
&self,
|
||||||
}
|
mode: GameMode,
|
||||||
|
mods: &GameMods,
|
||||||
|
) -> Result<Cow<'_, Self>, ConvertError> {
|
||||||
|
if self.mode == mode {
|
||||||
|
return Ok(Cow::Borrowed(self));
|
||||||
|
} else if self.is_convert {
|
||||||
|
return Err(ConvertError::AlreadyConverted);
|
||||||
|
} else if self.mode != GameMode::Osu {
|
||||||
|
return Err(ConvertError::Convert {
|
||||||
|
from: self.mode,
|
||||||
|
to: mode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a [`&Beatmap`] to the specified mode.
|
let mut map = self.to_owned();
|
||||||
///
|
|
||||||
/// # Panics
|
match mode {
|
||||||
///
|
GameMode::Taiko => Taiko::convert(&mut map),
|
||||||
/// Panics if the conversion is incompatible.
|
GameMode::Catch => Catch::convert(&mut map),
|
||||||
///
|
GameMode::Mania => Mania::convert(&mut map, mods),
|
||||||
/// [`&Beatmap`]: Beatmap
|
GameMode::Osu => unreachable!(),
|
||||||
pub fn unchecked_as_converted<M: IGameMode>(&self) -> Converted<'_, M> {
|
};
|
||||||
Converted::unchecked_from_ref(self)
|
|
||||||
|
Ok(Cow::Owned(map))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to convert a [`&mut Beatmap`] to the specified mode.
|
/// Attempt to convert a [`&mut Beatmap`] to the specified mode.
|
||||||
///
|
///
|
||||||
/// If the conversion is incompatible, `None` is returned.
|
|
||||||
///
|
|
||||||
/// [`&mut Beatmap`]: Beatmap
|
/// [`&mut Beatmap`]: Beatmap
|
||||||
pub fn try_as_converted_mut<M: IGameMode>(&mut self) -> Option<Converted<'_, M>> {
|
pub fn convert_mut(&mut self, mode: GameMode, mods: &GameMods) -> Result<(), ConvertError> {
|
||||||
Converted::try_from_mut(self)
|
if self.mode == mode {
|
||||||
}
|
return Ok(());
|
||||||
|
} else if self.is_convert {
|
||||||
|
return Err(ConvertError::AlreadyConverted);
|
||||||
|
} else if self.mode != GameMode::Osu {
|
||||||
|
return Err(ConvertError::Convert {
|
||||||
|
from: self.mode,
|
||||||
|
to: mode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a [`&mut Beatmap`] to the specified mode.
|
match mode {
|
||||||
///
|
GameMode::Taiko => Taiko::convert(self),
|
||||||
/// # Panics
|
GameMode::Catch => Catch::convert(self),
|
||||||
///
|
GameMode::Mania => Mania::convert(self, mods),
|
||||||
/// Panics if the conversion is incompatible.
|
GameMode::Osu => unreachable!(),
|
||||||
///
|
}
|
||||||
/// [`&mut Beatmap`]: Beatmap
|
|
||||||
pub fn unchecked_as_converted_mut<M: IGameMode>(&mut self) -> Converted<'_, M> {
|
|
||||||
Converted::unchecked_from_mut(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to convert a [`Beatmap`] to the specified mode.
|
Ok(())
|
||||||
///
|
|
||||||
/// If the conversion is incompatible the [`Beatmap`] will be returned
|
|
||||||
/// unchanged as `Err`.
|
|
||||||
#[allow(clippy::result_large_err)]
|
|
||||||
pub fn try_into_converted<'a, M: IGameMode>(self) -> Result<Converted<'a, M>, Self> {
|
|
||||||
Converted::try_from_owned(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a [`Beatmap`] to the specified mode.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics if the conversion is incompatible.
|
|
||||||
pub fn unchecked_into_converted<'a, M: IGameMode>(self) -> Converted<'a, M> {
|
|
||||||
Converted::unchecked_from_owned(self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+47
-38
@@ -1,8 +1,13 @@
|
|||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
|
};
|
||||||
|
|
||||||
pub use rosu_map::section::general::GameMode;
|
pub use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::Difficulty;
|
use crate::Difficulty;
|
||||||
|
|
||||||
use super::beatmap::{Beatmap, Converted};
|
use super::beatmap::Beatmap;
|
||||||
|
|
||||||
/// A way to specify a gamemode at compile-time.
|
/// A way to specify a gamemode at compile-time.
|
||||||
///
|
///
|
||||||
@@ -29,54 +34,58 @@ pub trait IGameMode: Sized {
|
|||||||
/// The type of a gradual performance calculator.
|
/// The type of a gradual performance calculator.
|
||||||
type GradualPerformance;
|
type GradualPerformance;
|
||||||
|
|
||||||
/// Check whether the map's mode can be converted to the current type.
|
/// Perform a difficulty calculation for a [`Beatmap`] and process the
|
||||||
fn check_convert(map: &Beatmap) -> ConvertStatus;
|
/// final skill values.
|
||||||
|
fn difficulty(
|
||||||
|
difficulty: &Difficulty,
|
||||||
|
map: &Beatmap,
|
||||||
|
) -> Result<Self::DifficultyAttributes, ConvertError>;
|
||||||
|
|
||||||
/// Attempt to convert a beatmap.
|
/// Perform a difficulty calculation for a [`Beatmap`] without processing
|
||||||
///
|
/// the final skill values.
|
||||||
/// In case [`ConvertStatus::Incompatible`] is returned, the map is not
|
fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<Self::Strains, ConvertError>;
|
||||||
/// modified.
|
|
||||||
fn try_convert(map: &mut Beatmap) -> ConvertStatus;
|
|
||||||
|
|
||||||
/// Perform a difficulty calculation for a [`Converted`] beatmap and
|
/// Create a performance calculator for a [`Beatmap`].
|
||||||
/// process the final skill values.
|
fn performance(map: &Beatmap) -> Self::Performance<'_>;
|
||||||
fn difficulty(difficulty: &Difficulty, map: &Converted<'_, Self>)
|
|
||||||
-> Self::DifficultyAttributes;
|
|
||||||
|
|
||||||
/// Perform a difficulty calculation for a [`Converted`] beatmap without
|
/// Create a gradual difficulty calculator for a [`Beatmap`].
|
||||||
/// processing the final skill values.
|
|
||||||
fn strains(difficulty: &Difficulty, map: &Converted<'_, Self>) -> Self::Strains;
|
|
||||||
|
|
||||||
/// Create a performance calculator for a [`Converted`] beatmap.
|
|
||||||
fn performance(map: Converted<'_, Self>) -> Self::Performance<'_>;
|
|
||||||
|
|
||||||
/// Create a gradual difficulty calculator for a [`Converted`] beatmap.
|
|
||||||
fn gradual_difficulty(
|
fn gradual_difficulty(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &Converted<'_, Self>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualDifficulty;
|
) -> Result<Self::GradualDifficulty, ConvertError>;
|
||||||
|
|
||||||
/// Create a gradual performance calculator for a [`Converted`] beatmap.
|
/// Create a gradual performance calculator for a [`Beatmap`].
|
||||||
fn gradual_performance(
|
fn gradual_performance(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &Converted<'_, Self>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualPerformance;
|
) -> Result<Self::GradualPerformance, ConvertError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The status of a conversion through [`IGameMode::try_convert`].
|
/// Error type when failing to convert a [`Beatmap`] from one [`GameMode`] to
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
/// another.
|
||||||
pub enum ConvertStatus {
|
#[derive(Copy, Clone, Debug)]
|
||||||
/// Conversion not necessary.
|
pub enum ConvertError {
|
||||||
Noop,
|
/// Cannot convert an already converted map
|
||||||
/// Conversion possible.
|
AlreadyConverted,
|
||||||
Conversion,
|
/// Cannot convert from [`GameMode`] `from` to `to`
|
||||||
/// Conversion not possible.
|
Convert { from: GameMode, to: GameMode },
|
||||||
Incompatible,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConvertStatus {
|
impl Error for ConvertError {
|
||||||
/// Whether this [`ConvertStatus`] represents a success.
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||||
pub const fn success(self) -> bool {
|
None
|
||||||
matches!(self, Self::Noop | Self::Conversion)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ConvertError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
match self {
|
||||||
|
ConvertError::AlreadyConverted => {
|
||||||
|
f.write_str("Cannot convert an already converted map")
|
||||||
|
}
|
||||||
|
ConvertError::Convert { from, to } => {
|
||||||
|
write!(f, "Cannot convert from {from:?} to {to:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-27
@@ -1,35 +1,15 @@
|
|||||||
use rosu_map::section::{general::GameMode, hit_objects::CurveBuffers};
|
use rosu_map::section::hit_objects::CurveBuffers;
|
||||||
|
|
||||||
use crate::model::{
|
use crate::model::{beatmap::Beatmap, mods::Reflection};
|
||||||
beatmap::{Beatmap, Converted},
|
|
||||||
mode::ConvertStatus,
|
|
||||||
mods::Reflection,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
attributes::OsuDifficultyAttributes,
|
attributes::OsuDifficultyAttributes,
|
||||||
difficulty::scaling_factor::ScalingFactor,
|
difficulty::scaling_factor::ScalingFactor,
|
||||||
object::{NestedSliderObjectKind, OsuObject, OsuObjectKind},
|
object::{NestedSliderObjectKind, OsuObject, OsuObjectKind},
|
||||||
Osu,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A [`Beatmap`] for [`Osu`] calculations.
|
|
||||||
pub type OsuBeatmap<'a> = Converted<'a, Osu>;
|
|
||||||
|
|
||||||
pub fn check_convert(map: &Beatmap) -> ConvertStatus {
|
|
||||||
if map.mode == GameMode::Osu {
|
|
||||||
ConvertStatus::Noop
|
|
||||||
} else {
|
|
||||||
ConvertStatus::Incompatible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_convert(map: &mut Beatmap) -> ConvertStatus {
|
|
||||||
check_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn convert_objects(
|
pub fn convert_objects(
|
||||||
converted: &OsuBeatmap<'_>,
|
map: &Beatmap,
|
||||||
scaling_factor: &ScalingFactor,
|
scaling_factor: &ScalingFactor,
|
||||||
reflection: Reflection,
|
reflection: Reflection,
|
||||||
time_preempt: f64,
|
time_preempt: f64,
|
||||||
@@ -40,10 +20,10 @@ pub fn convert_objects(
|
|||||||
// mean=5.16 | median=4
|
// mean=5.16 | median=4
|
||||||
let mut ticks_buf = Vec::new();
|
let mut ticks_buf = Vec::new();
|
||||||
|
|
||||||
let mut osu_objects: Box<[_]> = converted
|
let mut osu_objects: Box<[_]> = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.map(|h| OsuObject::new(h, converted, &mut curve_bufs, &mut ticks_buf))
|
.map(|h| OsuObject::new(h, map, &mut curve_bufs, &mut ticks_buf))
|
||||||
.inspect(|h| {
|
.inspect(|h| {
|
||||||
if take == 0 {
|
if take == 0 {
|
||||||
return;
|
return;
|
||||||
@@ -77,9 +57,9 @@ pub fn convert_objects(
|
|||||||
.for_each(OsuObject::reflect_both_axes),
|
.for_each(OsuObject::reflect_both_axes),
|
||||||
}
|
}
|
||||||
|
|
||||||
let stack_threshold = time_preempt * f64::from(converted.stack_leniency);
|
let stack_threshold = time_preempt * f64::from(map.stack_leniency);
|
||||||
|
|
||||||
if converted.version >= 6 {
|
if map.version >= 6 {
|
||||||
stacking(&mut osu_objects, stack_threshold);
|
stacking(&mut osu_objects, stack_threshold);
|
||||||
} else {
|
} else {
|
||||||
old_stacking(&mut osu_objects, stack_threshold);
|
old_stacking(&mut osu_objects, stack_threshold);
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
use std::{cmp, mem};
|
use std::{cmp, mem};
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::skills::Skill,
|
any::difficulty::skills::Skill,
|
||||||
|
model::mode::ConvertError,
|
||||||
osu::{
|
osu::{
|
||||||
convert::convert_objects,
|
convert::convert_objects,
|
||||||
object::{OsuObject, OsuObjectKind},
|
object::{OsuObject, OsuObjectKind},
|
||||||
OsuBeatmap,
|
|
||||||
},
|
},
|
||||||
Difficulty,
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::osu_objects::OsuObjects;
|
use self::osu_objects::OsuObjects;
|
||||||
@@ -71,22 +73,23 @@ struct NotClonable;
|
|||||||
|
|
||||||
impl OsuGradualDifficulty {
|
impl OsuGradualDifficulty {
|
||||||
/// Create a new difficulty attributes iterator for osu!standard maps.
|
/// Create a new difficulty attributes iterator for osu!standard maps.
|
||||||
pub fn new(difficulty: Difficulty, converted: &OsuBeatmap<'_>) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
|
||||||
let mods = difficulty.get_mods();
|
let mods = difficulty.get_mods();
|
||||||
|
let map = map.convert_ref(GameMode::Osu, mods)?;
|
||||||
|
|
||||||
let OsuDifficultySetup {
|
let OsuDifficultySetup {
|
||||||
scaling_factor,
|
scaling_factor,
|
||||||
map_attrs,
|
map_attrs,
|
||||||
mut attrs,
|
mut attrs,
|
||||||
time_preempt,
|
time_preempt,
|
||||||
} = OsuDifficultySetup::new(&difficulty, converted);
|
} = OsuDifficultySetup::new(&difficulty, &map);
|
||||||
|
|
||||||
let osu_objects = convert_objects(
|
let osu_objects = convert_objects(
|
||||||
converted,
|
&map,
|
||||||
&scaling_factor,
|
&scaling_factor,
|
||||||
mods.reflection(),
|
mods.reflection(),
|
||||||
time_preempt,
|
time_preempt,
|
||||||
converted.hit_objects.len(),
|
map.hit_objects.len(),
|
||||||
&mut attrs,
|
&mut attrs,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -111,7 +114,7 @@ impl OsuGradualDifficulty {
|
|||||||
let skills = OsuSkills::new(mods, &scaling_factor, &map_attrs, time_preempt);
|
let skills = OsuSkills::new(mods, &scaling_factor, &map_attrs, time_preempt);
|
||||||
let diff_objects = extend_lifetime(diff_objects.into_boxed_slice());
|
let diff_objects = extend_lifetime(diff_objects.into_boxed_slice());
|
||||||
|
|
||||||
Self {
|
Ok(Self {
|
||||||
idx: 0,
|
idx: 0,
|
||||||
difficulty,
|
difficulty,
|
||||||
attrs,
|
attrs,
|
||||||
@@ -119,7 +122,7 @@ impl OsuGradualDifficulty {
|
|||||||
diff_objects,
|
diff_objects,
|
||||||
osu_objects,
|
osu_objects,
|
||||||
_not_clonable: NotClonable,
|
_not_clonable: NotClonable,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn increment_combo(h: &OsuObject, attrs: &mut OsuDifficultyAttributes) {
|
fn increment_combo(h: &OsuObject, attrs: &mut OsuDifficultyAttributes) {
|
||||||
@@ -265,28 +268,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn empty() {
|
||||||
let converted = Beatmap::from_bytes(&[])
|
let map = Beatmap::from_bytes(&[]).unwrap();
|
||||||
.unwrap()
|
let mut gradual = OsuGradualDifficulty::new(Difficulty::new(), &map).unwrap();
|
||||||
.unchecked_into_converted::<Osu>();
|
|
||||||
|
|
||||||
let mut gradual = OsuGradualDifficulty::new(Difficulty::new(), &converted);
|
|
||||||
|
|
||||||
assert!(gradual.next().is_none());
|
assert!(gradual.next().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_and_nth() {
|
fn next_and_nth() {
|
||||||
let converted = Beatmap::from_path("./resources/2785319.osu")
|
let map = Beatmap::from_path("./resources/2785319.osu").unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted::<Osu>();
|
|
||||||
|
|
||||||
let difficulty = Difficulty::new();
|
let difficulty = Difficulty::new();
|
||||||
|
|
||||||
let mut gradual = OsuGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual = OsuGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_2nd = OsuGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual_2nd = OsuGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_3rd = OsuGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual_3rd = OsuGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
|
|
||||||
let hit_objects_len = converted.hit_objects.len();
|
let hit_objects_len = map.hit_objects.len();
|
||||||
|
|
||||||
for i in 1.. {
|
for i in 1.. {
|
||||||
let Some(next_gradual) = gradual.next() else {
|
let Some(next_gradual) = gradual.next() else {
|
||||||
@@ -309,8 +306,8 @@ mod tests {
|
|||||||
let expected = difficulty
|
let expected = difficulty
|
||||||
.clone()
|
.clone()
|
||||||
.passed_objects(i as u32)
|
.passed_objects(i as u32)
|
||||||
.with_mode()
|
.calculate_for_mode::<Osu>(&map)
|
||||||
.calculate(&converted);
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(next_gradual, expected);
|
assert_eq!(next_gradual, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-11
@@ -1,5 +1,6 @@
|
|||||||
use std::{cmp, pin::Pin};
|
use std::{cmp, pin::Pin};
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
use skills::{
|
use skills::{
|
||||||
flashlight::Flashlight,
|
flashlight::Flashlight,
|
||||||
strain::{DifficultyValue, OsuStrainSkill, UsedOsuStrainSkills},
|
strain::{DifficultyValue, OsuStrainSkill, UsedOsuStrainSkills},
|
||||||
@@ -7,18 +8,19 @@ use skills::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::{skills::Skill, Difficulty},
|
any::difficulty::{skills::Skill, Difficulty},
|
||||||
model::{beatmap::BeatmapAttributes, mods::GameMods},
|
model::{beatmap::BeatmapAttributes, mode::ConvertError, mods::GameMods},
|
||||||
osu::{
|
osu::{
|
||||||
convert::convert_objects,
|
convert::convert_objects,
|
||||||
difficulty::{object::OsuDifficultyObject, scaling_factor::ScalingFactor},
|
difficulty::{object::OsuDifficultyObject, scaling_factor::ScalingFactor},
|
||||||
object::OsuObject,
|
object::OsuObject,
|
||||||
performance::PERFORMANCE_BASE_MULTIPLIER,
|
performance::PERFORMANCE_BASE_MULTIPLIER,
|
||||||
},
|
},
|
||||||
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::skills::OsuSkills;
|
use self::skills::OsuSkills;
|
||||||
|
|
||||||
use super::{attributes::OsuDifficultyAttributes, convert::OsuBeatmap};
|
use super::attributes::OsuDifficultyAttributes;
|
||||||
|
|
||||||
pub mod gradual;
|
pub mod gradual;
|
||||||
mod object;
|
mod object;
|
||||||
@@ -30,7 +32,12 @@ const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
|
|||||||
const HD_FADE_IN_DURATION_MULTIPLIER: f64 = 0.4;
|
const HD_FADE_IN_DURATION_MULTIPLIER: f64 = 0.4;
|
||||||
const HD_FADE_OUT_DURATION_MULTIPLIER: f64 = 0.3;
|
const HD_FADE_OUT_DURATION_MULTIPLIER: f64 = 0.3;
|
||||||
|
|
||||||
pub fn difficulty(difficulty: &Difficulty, converted: &OsuBeatmap<'_>) -> OsuDifficultyAttributes {
|
pub fn difficulty(
|
||||||
|
difficulty: &Difficulty,
|
||||||
|
map: &Beatmap,
|
||||||
|
) -> Result<OsuDifficultyAttributes, ConvertError> {
|
||||||
|
let map = map.convert_ref(GameMode::Osu, difficulty.get_mods())?;
|
||||||
|
|
||||||
let DifficultyValues {
|
let DifficultyValues {
|
||||||
skills:
|
skills:
|
||||||
OsuSkills {
|
OsuSkills {
|
||||||
@@ -40,7 +47,7 @@ pub fn difficulty(difficulty: &Difficulty, converted: &OsuBeatmap<'_>) -> OsuDif
|
|||||||
flashlight,
|
flashlight,
|
||||||
},
|
},
|
||||||
mut attrs,
|
mut attrs,
|
||||||
} = DifficultyValues::calculate(difficulty, converted);
|
} = DifficultyValues::calculate(difficulty, &map);
|
||||||
|
|
||||||
let aim_difficulty_value = aim.difficulty_value();
|
let aim_difficulty_value = aim.difficulty_value();
|
||||||
let aim_no_sliders_difficulty_value = aim_no_sliders.difficulty_value();
|
let aim_no_sliders_difficulty_value = aim_no_sliders.difficulty_value();
|
||||||
@@ -60,7 +67,7 @@ pub fn difficulty(difficulty: &Difficulty, converted: &OsuBeatmap<'_>) -> OsuDif
|
|||||||
flashlight_difficulty_value,
|
flashlight_difficulty_value,
|
||||||
);
|
);
|
||||||
|
|
||||||
attrs
|
Ok(attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OsuDifficultySetup {
|
pub struct OsuDifficultySetup {
|
||||||
@@ -71,9 +78,9 @@ pub struct OsuDifficultySetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OsuDifficultySetup {
|
impl OsuDifficultySetup {
|
||||||
pub fn new(difficulty: &Difficulty, converted: &OsuBeatmap) -> Self {
|
pub fn new(difficulty: &Difficulty, map: &Beatmap) -> Self {
|
||||||
let clock_rate = difficulty.get_clock_rate();
|
let clock_rate = difficulty.get_clock_rate();
|
||||||
let map_attrs = converted.attributes().difficulty(difficulty).build();
|
let map_attrs = map.attributes().difficulty(difficulty).build();
|
||||||
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
||||||
|
|
||||||
let attrs = OsuDifficultyAttributes {
|
let attrs = OsuDifficultyAttributes {
|
||||||
@@ -100,7 +107,7 @@ pub struct DifficultyValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DifficultyValues {
|
impl DifficultyValues {
|
||||||
pub fn calculate(difficulty: &Difficulty, converted: &OsuBeatmap<'_>) -> Self {
|
pub fn calculate(difficulty: &Difficulty, map: &Beatmap) -> Self {
|
||||||
let mods = difficulty.get_mods();
|
let mods = difficulty.get_mods();
|
||||||
let take = difficulty.get_passed_objects();
|
let take = difficulty.get_passed_objects();
|
||||||
|
|
||||||
@@ -109,10 +116,10 @@ impl DifficultyValues {
|
|||||||
map_attrs,
|
map_attrs,
|
||||||
mut attrs,
|
mut attrs,
|
||||||
time_preempt,
|
time_preempt,
|
||||||
} = OsuDifficultySetup::new(difficulty, converted);
|
} = OsuDifficultySetup::new(difficulty, map);
|
||||||
|
|
||||||
let mut osu_objects = convert_objects(
|
let mut osu_objects = convert_objects(
|
||||||
converted,
|
map,
|
||||||
&scaling_factor,
|
&scaling_factor,
|
||||||
mods.reflection(),
|
mods.reflection(),
|
||||||
time_preempt,
|
time_preempt,
|
||||||
@@ -134,7 +141,7 @@ impl DifficultyValues {
|
|||||||
let mut flashlight = Skill::new(&mut skills.flashlight, &diff_objects);
|
let mut flashlight = Skill::new(&mut skills.flashlight, &diff_objects);
|
||||||
|
|
||||||
// The first hit object has no difficulty object
|
// The first hit object has no difficulty object
|
||||||
let take_diff_objects = cmp::min(converted.hit_objects.len(), take).saturating_sub(1);
|
let take_diff_objects = cmp::min(map.hit_objects.len(), take).saturating_sub(1);
|
||||||
|
|
||||||
for hit_object in diff_objects.iter().take(take_diff_objects) {
|
for hit_object in diff_objects.iter().take(take_diff_objects) {
|
||||||
aim.process(hit_object);
|
aim.process(hit_object);
|
||||||
|
|||||||
+13
-19
@@ -3,14 +3,13 @@ use rosu_map::util::Pos;
|
|||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
beatmap::Beatmap,
|
beatmap::Beatmap,
|
||||||
mode::{ConvertStatus, IGameMode},
|
mode::{ConvertError, IGameMode},
|
||||||
},
|
},
|
||||||
Difficulty,
|
Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
attributes::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
attributes::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||||
convert::OsuBeatmap,
|
|
||||||
difficulty::gradual::OsuGradualDifficulty,
|
difficulty::gradual::OsuGradualDifficulty,
|
||||||
performance::{gradual::OsuGradualPerformance, OsuPerformance},
|
performance::{gradual::OsuGradualPerformance, OsuPerformance},
|
||||||
score_state::{OsuScoreOrigin, OsuScoreState},
|
score_state::{OsuScoreOrigin, OsuScoreState},
|
||||||
@@ -39,37 +38,32 @@ impl IGameMode for Osu {
|
|||||||
type GradualDifficulty = OsuGradualDifficulty;
|
type GradualDifficulty = OsuGradualDifficulty;
|
||||||
type GradualPerformance = OsuGradualPerformance;
|
type GradualPerformance = OsuGradualPerformance;
|
||||||
|
|
||||||
fn check_convert(map: &Beatmap) -> ConvertStatus {
|
|
||||||
convert::check_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_convert(map: &mut Beatmap) -> ConvertStatus {
|
|
||||||
convert::try_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn difficulty(
|
fn difficulty(
|
||||||
difficulty: &Difficulty,
|
difficulty: &Difficulty,
|
||||||
converted: &OsuBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::DifficultyAttributes {
|
) -> Result<Self::DifficultyAttributes, ConvertError> {
|
||||||
difficulty::difficulty(difficulty, converted)
|
difficulty::difficulty(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strains(difficulty: &Difficulty, converted: &OsuBeatmap<'_>) -> Self::Strains {
|
fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<Self::Strains, ConvertError> {
|
||||||
strains::strains(difficulty, converted)
|
strains::strains(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn performance(map: OsuBeatmap<'_>) -> Self::Performance<'_> {
|
fn performance(map: &Beatmap) -> Self::Performance<'_> {
|
||||||
OsuPerformance::new(map)
|
OsuPerformance::new(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradual_difficulty(difficulty: Difficulty, map: &OsuBeatmap<'_>) -> Self::GradualDifficulty {
|
fn gradual_difficulty(
|
||||||
|
difficulty: Difficulty,
|
||||||
|
map: &Beatmap,
|
||||||
|
) -> Result<Self::GradualDifficulty, ConvertError> {
|
||||||
OsuGradualDifficulty::new(difficulty, map)
|
OsuGradualDifficulty::new(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradual_performance(
|
fn gradual_performance(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &OsuBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualPerformance {
|
) -> Result<Self::GradualPerformance, ConvertError> {
|
||||||
OsuGradualPerformance::new(difficulty, map)
|
OsuGradualPerformance::new(difficulty, map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-9
@@ -14,9 +14,10 @@ use crate::{
|
|||||||
hit_object::{HitObject, HitObjectKind, HoldNote, Slider, Spinner},
|
hit_object::{HitObject, HitObjectKind, HoldNote, Slider, Spinner},
|
||||||
},
|
},
|
||||||
util::{get_precision_adjusted_beat_len, sort},
|
util::{get_precision_adjusted_beat_len, sort},
|
||||||
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{convert::OsuBeatmap, PLAYFIELD_BASE_SIZE};
|
use super::PLAYFIELD_BASE_SIZE;
|
||||||
|
|
||||||
pub struct OsuObject {
|
pub struct OsuObject {
|
||||||
pub pos: Pos,
|
pub pos: Pos,
|
||||||
@@ -34,14 +35,14 @@ impl OsuObject {
|
|||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
h: &HitObject,
|
h: &HitObject,
|
||||||
converted: &OsuBeatmap<'_>,
|
map: &Beatmap,
|
||||||
curve_bufs: &mut CurveBuffers,
|
curve_bufs: &mut CurveBuffers,
|
||||||
ticks_buf: &mut Vec<SliderEvent>,
|
ticks_buf: &mut Vec<SliderEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let kind = match h.kind {
|
let kind = match h.kind {
|
||||||
HitObjectKind::Circle => OsuObjectKind::Circle,
|
HitObjectKind::Circle => OsuObjectKind::Circle,
|
||||||
HitObjectKind::Slider(ref slider) => {
|
HitObjectKind::Slider(ref slider) => {
|
||||||
OsuObjectKind::Slider(OsuSlider::new(h, slider, converted, curve_bufs, ticks_buf))
|
OsuObjectKind::Slider(OsuSlider::new(h, slider, map, curve_bufs, ticks_buf))
|
||||||
}
|
}
|
||||||
HitObjectKind::Spinner(spinner) => OsuObjectKind::Spinner(spinner),
|
HitObjectKind::Spinner(spinner) => OsuObjectKind::Spinner(spinner),
|
||||||
HitObjectKind::Hold(HoldNote { duration }) => {
|
HitObjectKind::Hold(HoldNote { duration }) => {
|
||||||
@@ -188,19 +189,19 @@ impl OsuSlider {
|
|||||||
fn new(
|
fn new(
|
||||||
h: &HitObject,
|
h: &HitObject,
|
||||||
slider: &Slider,
|
slider: &Slider,
|
||||||
converted: &OsuBeatmap<'_>,
|
map: &Beatmap,
|
||||||
curve_bufs: &mut CurveBuffers,
|
curve_bufs: &mut CurveBuffers,
|
||||||
ticks_buf: &mut Vec<SliderEvent>,
|
ticks_buf: &mut Vec<SliderEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let start_time = h.start_time;
|
let start_time = h.start_time;
|
||||||
let slider_multiplier = converted.slider_multiplier;
|
let slider_multiplier = map.slider_multiplier;
|
||||||
let slider_tick_rate = converted.slider_tick_rate;
|
let slider_tick_rate = map.slider_tick_rate;
|
||||||
|
|
||||||
let beat_len = converted
|
let beat_len = map
|
||||||
.timing_point_at(start_time)
|
.timing_point_at(start_time)
|
||||||
.map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
|
.map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
|
||||||
|
|
||||||
let (slider_velocity, generate_ticks) = converted.difficulty_point_at(start_time).map_or(
|
let (slider_velocity, generate_ticks) = map.difficulty_point_at(start_time).map_or(
|
||||||
(
|
(
|
||||||
DifficultyPoint::DEFAULT_SLIDER_VELOCITY,
|
DifficultyPoint::DEFAULT_SLIDER_VELOCITY,
|
||||||
DifficultyPoint::DEFAULT_GENERATE_TICKS,
|
DifficultyPoint::DEFAULT_GENERATE_TICKS,
|
||||||
@@ -221,7 +222,7 @@ impl OsuSlider {
|
|||||||
let duration = end_time - start_time;
|
let duration = end_time - start_time;
|
||||||
let span_duration = duration / span_count;
|
let span_duration = duration / span_count;
|
||||||
|
|
||||||
let tick_dist_multiplier = if converted.version < 8 {
|
let tick_dist_multiplier = if map.version < 8 {
|
||||||
slider_velocity.recip()
|
slider_velocity.recip()
|
||||||
} else {
|
} else {
|
||||||
1.0
|
1.0
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
use crate::{
|
use crate::{model::mode::ConvertError, osu::OsuGradualDifficulty, Beatmap, Difficulty};
|
||||||
osu::{OsuBeatmap, OsuGradualDifficulty},
|
|
||||||
Difficulty,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{OsuPerformanceAttributes, OsuScoreState};
|
use super::{OsuPerformanceAttributes, OsuScoreState};
|
||||||
|
|
||||||
@@ -86,11 +83,11 @@ pub struct OsuGradualPerformance {
|
|||||||
|
|
||||||
impl OsuGradualPerformance {
|
impl OsuGradualPerformance {
|
||||||
/// Create a new gradual performance calculator for osu!standard maps.
|
/// Create a new gradual performance calculator for osu!standard maps.
|
||||||
pub fn new(difficulty: Difficulty, converted: &OsuBeatmap<'_>) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
|
||||||
let lazer = difficulty.get_lazer();
|
let lazer = difficulty.get_lazer();
|
||||||
let difficulty = OsuGradualDifficulty::new(difficulty, converted);
|
let difficulty = OsuGradualDifficulty::new(difficulty, map)?;
|
||||||
|
|
||||||
Self { lazer, difficulty }
|
Ok(Self { lazer, difficulty })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process the next hit object and calculate the performance attributes
|
/// Process the next hit object and calculate the performance attributes
|
||||||
@@ -110,6 +107,7 @@ impl OsuGradualPerformance {
|
|||||||
///
|
///
|
||||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
||||||
/// `n=1` will process 2, and so on.
|
/// `n=1` will process 2, and so on.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn nth(&mut self, state: OsuScoreState, n: usize) -> Option<OsuPerformanceAttributes> {
|
pub fn nth(&mut self, state: OsuScoreState, n: usize) -> Option<OsuPerformanceAttributes> {
|
||||||
let performance = self
|
let performance = self
|
||||||
.difficulty
|
.difficulty
|
||||||
@@ -119,7 +117,8 @@ impl OsuGradualPerformance {
|
|||||||
.state(state)
|
.state(state)
|
||||||
.difficulty(self.difficulty.difficulty.clone())
|
.difficulty(self.difficulty.difficulty.clone())
|
||||||
.passed_objects(self.difficulty.idx as u32)
|
.passed_objects(self.difficulty.idx as u32)
|
||||||
.calculate();
|
.calculate()
|
||||||
|
.expect("no conversion required");
|
||||||
|
|
||||||
Some(performance)
|
Some(performance)
|
||||||
}
|
}
|
||||||
@@ -133,28 +132,23 @@ impl OsuGradualPerformance {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{osu::OsuPerformance, Beatmap};
|
||||||
osu::{Osu, OsuPerformance},
|
|
||||||
Beatmap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_and_nth() {
|
fn next_and_nth() {
|
||||||
let converted = Beatmap::from_path("./resources/2785319.osu")
|
let map = Beatmap::from_path("./resources/2785319.osu").unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted::<Osu>();
|
|
||||||
|
|
||||||
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
||||||
|
|
||||||
let mut gradual = OsuGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual = OsuGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_2nd = OsuGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual_2nd = OsuGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_3rd = OsuGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual_3rd = OsuGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
|
|
||||||
let mut state = OsuScoreState::default();
|
let mut state = OsuScoreState::default();
|
||||||
|
|
||||||
let hit_objects_len = converted.hit_objects.len();
|
let hit_objects_len = map.hit_objects.len();
|
||||||
|
|
||||||
for i in 1.. {
|
for i in 1.. {
|
||||||
state.misses += 1;
|
state.misses += 1;
|
||||||
@@ -176,15 +170,15 @@ mod tests {
|
|||||||
assert_eq!(next_gradual, next_gradual_3rd);
|
assert_eq!(next_gradual, next_gradual_3rd);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut regular_calc = OsuPerformance::new(converted.as_owned())
|
let mut regular_calc = OsuPerformance::new(&map)
|
||||||
.difficulty(difficulty.clone())
|
.difficulty(difficulty.clone())
|
||||||
.passed_objects(i as u32)
|
.passed_objects(i as u32)
|
||||||
.state(state);
|
.state(state);
|
||||||
|
|
||||||
let regular_state = regular_calc.generate_state();
|
let regular_state = regular_calc.generate_state().unwrap();
|
||||||
assert_eq!(state, regular_state);
|
assert_eq!(state, regular_state);
|
||||||
|
|
||||||
let expected = regular_calc.calculate();
|
let expected = regular_calc.calculate().unwrap();
|
||||||
|
|
||||||
assert_eq!(next_gradual, expected);
|
assert_eq!(next_gradual, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-37
@@ -1,4 +1,4 @@
|
|||||||
use std::cmp;
|
use std::{borrow::Cow, cmp};
|
||||||
|
|
||||||
use rosu_map::section::general::GameMode;
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
@@ -6,9 +6,10 @@ use crate::{
|
|||||||
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance, Performance},
|
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance, Performance},
|
||||||
catch::CatchPerformance,
|
catch::CatchPerformance,
|
||||||
mania::ManiaPerformance,
|
mania::ManiaPerformance,
|
||||||
model::mods::GameMods,
|
model::{mode::ConvertError, mods::GameMods},
|
||||||
taiko::TaikoPerformance,
|
taiko::TaikoPerformance,
|
||||||
util::{float_ext::FloatExt, map_or_attrs::MapOrAttrs},
|
util::{float_ext::FloatExt, map_or_attrs::MapOrAttrs},
|
||||||
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -43,7 +44,7 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
/// The argument `map_or_attrs` must be either
|
/// The argument `map_or_attrs` must be either
|
||||||
/// - previously calculated attributes ([`OsuDifficultyAttributes`]
|
/// - previously calculated attributes ([`OsuDifficultyAttributes`]
|
||||||
/// or [`OsuPerformanceAttributes`])
|
/// or [`OsuPerformanceAttributes`])
|
||||||
/// - a beatmap ([`OsuBeatmap<'map>`])
|
/// - a [`Beatmap`] (by reference or value)
|
||||||
///
|
///
|
||||||
/// If a map is given, difficulty attributes will need to be calculated
|
/// If a map is given, difficulty attributes will need to be calculated
|
||||||
/// internally which is a costly operation. Hence, passing attributes
|
/// internally which is a costly operation. Hence, passing attributes
|
||||||
@@ -52,21 +53,18 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
/// However, when passing previously calculated attributes, make sure they
|
/// However, when passing previously calculated attributes, make sure they
|
||||||
/// have been calculated for the same map and [`Difficulty`] settings.
|
/// have been calculated for the same map and [`Difficulty`] settings.
|
||||||
/// Otherwise, the final attributes will be incorrect.
|
/// Otherwise, the final attributes will be incorrect.
|
||||||
///
|
|
||||||
/// [`OsuBeatmap<'map>`]: crate::osu::OsuBeatmap
|
|
||||||
pub fn new(map_or_attrs: impl IntoModePerformance<'map, Osu>) -> Self {
|
pub fn new(map_or_attrs: impl IntoModePerformance<'map, Osu>) -> Self {
|
||||||
map_or_attrs.into_performance()
|
map_or_attrs.into_performance()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to create a new performance calculator for osu! maps.
|
/// Try to create a new performance calculator for osu! maps.
|
||||||
///
|
///
|
||||||
/// Returns `None` if `map_or_attrs` does not belong to osu! e.g.
|
/// Returns `None` if `map_or_attrs` does not belong to osu! i.e.
|
||||||
/// a [`Converted`], [`DifficultyAttributes`], or [`PerformanceAttributes`]
|
/// a [`DifficultyAttributes`] or [`PerformanceAttributes`] of a different
|
||||||
/// of a different mode.
|
/// mode.
|
||||||
///
|
///
|
||||||
/// See [`OsuPerformance::new`] for more information.
|
/// See [`OsuPerformance::new`] for more information.
|
||||||
///
|
///
|
||||||
/// [`Converted`]: crate::model::beatmap::Converted
|
|
||||||
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
||||||
/// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
|
/// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
|
||||||
pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
|
pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
|
||||||
@@ -349,10 +347,10 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
|
|
||||||
/// Create the [`OsuScoreState`] that will be used for performance calculation.
|
/// Create the [`OsuScoreState`] that will be used for performance calculation.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn generate_state(&mut self) -> OsuScoreState {
|
pub fn generate_state(&mut self) -> Result<OsuScoreState, ConvertError> {
|
||||||
let attrs = match self.map_or_attrs {
|
let attrs = match self.map_or_attrs {
|
||||||
MapOrAttrs::Map(ref map) => {
|
MapOrAttrs::Map(ref map) => {
|
||||||
let attrs = self.difficulty.with_mode().calculate(map);
|
let attrs = self.difficulty.calculate_for_mode::<Osu>(map)?;
|
||||||
|
|
||||||
self.map_or_attrs.insert_attrs(attrs)
|
self.map_or_attrs.insert_attrs(attrs)
|
||||||
}
|
}
|
||||||
@@ -636,7 +634,7 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
self.n50 = Some(n50);
|
self.n50 = Some(n50);
|
||||||
self.misses = Some(misses);
|
self.misses = Some(misses);
|
||||||
|
|
||||||
OsuScoreState {
|
Ok(OsuScoreState {
|
||||||
max_combo,
|
max_combo,
|
||||||
large_tick_hits,
|
large_tick_hits,
|
||||||
slider_end_hits,
|
slider_end_hits,
|
||||||
@@ -644,16 +642,16 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
n100,
|
n100,
|
||||||
n50,
|
n50,
|
||||||
misses,
|
misses,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> OsuPerformanceAttributes {
|
pub fn calculate(mut self) -> Result<OsuPerformanceAttributes, ConvertError> {
|
||||||
let state = self.generate_state();
|
let state = self.generate_state()?;
|
||||||
|
|
||||||
let attrs = match self.map_or_attrs {
|
let attrs = match self.map_or_attrs {
|
||||||
MapOrAttrs::Map(ref map) => self.difficulty.with_mode().calculate(map),
|
|
||||||
MapOrAttrs::Attrs(attrs) => attrs,
|
MapOrAttrs::Attrs(attrs) => attrs,
|
||||||
|
MapOrAttrs::Map(ref map) => self.difficulty.calculate_for_mode::<Osu>(map)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mods = self.difficulty.get_mods();
|
let mods = self.difficulty.get_mods();
|
||||||
@@ -718,7 +716,7 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
using_classic_slider_acc,
|
using_classic_slider_acc,
|
||||||
};
|
};
|
||||||
|
|
||||||
inner.calculate()
|
Ok(inner.calculate())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Osu>) -> Self {
|
pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Osu>) -> Self {
|
||||||
@@ -736,6 +734,33 @@ impl<'map> OsuPerformance<'map> {
|
|||||||
hitresult_priority: HitResultPriority::DEFAULT,
|
hitresult_priority: HitResultPriority::DEFAULT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::result_large_err)]
|
||||||
|
pub(crate) fn try_convert_map(
|
||||||
|
map_or_attrs: MapOrAttrs<'map, Osu>,
|
||||||
|
mode: GameMode,
|
||||||
|
mods: &GameMods,
|
||||||
|
) -> Result<Cow<'map, Beatmap>, MapOrAttrs<'map, Osu>> {
|
||||||
|
let MapOrAttrs::Map(map) = map_or_attrs else {
|
||||||
|
return Err(map_or_attrs);
|
||||||
|
};
|
||||||
|
|
||||||
|
match map {
|
||||||
|
Cow::Borrowed(map) => match map.convert_ref(mode, mods) {
|
||||||
|
Ok(map) => Ok(map),
|
||||||
|
Err(_) => {
|
||||||
|
return Err(MapOrAttrs::Map(Cow::Borrowed(map)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Cow::Owned(mut map) => {
|
||||||
|
if map.convert_mut(mode, mods).is_err() {
|
||||||
|
return Err(MapOrAttrs::Map(Cow::Owned(map)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Cow::Owned(map))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map, T: IntoModePerformance<'map, Osu>> From<T> for OsuPerformance<'map> {
|
impl<'map, T: IntoModePerformance<'map, Osu>> From<T> for OsuPerformance<'map> {
|
||||||
@@ -1147,7 +1172,7 @@ mod test {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{DifficultyAttributes, PerformanceAttributes},
|
any::{DifficultyAttributes, PerformanceAttributes},
|
||||||
taiko::{Taiko, TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
taiko::{TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
||||||
Beatmap,
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1166,8 +1191,8 @@ mod test {
|
|||||||
fn attrs() -> OsuDifficultyAttributes {
|
fn attrs() -> OsuDifficultyAttributes {
|
||||||
ATTRS
|
ATTRS
|
||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
let converted = beatmap().unchecked_into_converted::<Osu>();
|
let map = beatmap();
|
||||||
let attrs = Difficulty::new().with_mode().calculate(&converted);
|
let attrs = Difficulty::new().calculate_for_mode::<Osu>(&map).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(attrs.n_circles, attrs.n_sliders, attrs.n_spinners),
|
(attrs.n_circles, attrs.n_sliders, attrs.n_spinners),
|
||||||
@@ -1382,8 +1407,8 @@ mod test {
|
|||||||
state = state.misses(misses);
|
state = state.misses(misses);
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = state.generate_state();
|
let first = state.generate_state().unwrap();
|
||||||
let state = state.generate_state();
|
let state = state.generate_state().unwrap();
|
||||||
assert_eq!(first, state);
|
assert_eq!(first, state);
|
||||||
|
|
||||||
let mut expected = brute_force_best(
|
let mut expected = brute_force_best(
|
||||||
@@ -1413,7 +1438,8 @@ mod test {
|
|||||||
.n100(20)
|
.n100(20)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.hitresult_priority(HitResultPriority::BestCase)
|
.hitresult_priority(HitResultPriority::BestCase)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = OsuScoreState {
|
let expected = OsuScoreState {
|
||||||
max_combo: 500,
|
max_combo: 500,
|
||||||
@@ -1437,7 +1463,8 @@ mod test {
|
|||||||
.n50(10)
|
.n50(10)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.hitresult_priority(HitResultPriority::BestCase)
|
.hitresult_priority(HitResultPriority::BestCase)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = OsuScoreState {
|
let expected = OsuScoreState {
|
||||||
max_combo: 500,
|
max_combo: 500,
|
||||||
@@ -1460,7 +1487,8 @@ mod test {
|
|||||||
.n50(10)
|
.n50(10)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.hitresult_priority(HitResultPriority::WorstCase)
|
.hitresult_priority(HitResultPriority::WorstCase)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = OsuScoreState {
|
let expected = OsuScoreState {
|
||||||
max_combo: 500,
|
max_combo: 500,
|
||||||
@@ -1485,7 +1513,8 @@ mod test {
|
|||||||
.n50(10)
|
.n50(10)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.hitresult_priority(HitResultPriority::WorstCase)
|
.hitresult_priority(HitResultPriority::WorstCase)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = OsuScoreState {
|
let expected = OsuScoreState {
|
||||||
max_combo: 500,
|
max_combo: 500,
|
||||||
@@ -1503,12 +1532,11 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn create() {
|
fn create() {
|
||||||
let mut map = beatmap();
|
let mut map = beatmap();
|
||||||
let converted = map.unchecked_as_converted();
|
|
||||||
|
|
||||||
let _ = OsuPerformance::new(OsuDifficultyAttributes::default());
|
let _ = OsuPerformance::new(OsuDifficultyAttributes::default());
|
||||||
let _ = OsuPerformance::new(OsuPerformanceAttributes::default());
|
let _ = OsuPerformance::new(OsuPerformanceAttributes::default());
|
||||||
let _ = OsuPerformance::new(&converted);
|
let _ = OsuPerformance::new(&map);
|
||||||
let _ = OsuPerformance::new(converted.as_owned());
|
let _ = OsuPerformance::new(map.clone());
|
||||||
|
|
||||||
let _ = OsuPerformance::try_new(OsuDifficultyAttributes::default()).unwrap();
|
let _ = OsuPerformance::try_new(OsuDifficultyAttributes::default()).unwrap();
|
||||||
let _ = OsuPerformance::try_new(OsuPerformanceAttributes::default()).unwrap();
|
let _ = OsuPerformance::try_new(OsuPerformanceAttributes::default()).unwrap();
|
||||||
@@ -1519,19 +1547,19 @@ mod test {
|
|||||||
OsuPerformanceAttributes::default(),
|
OsuPerformanceAttributes::default(),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = OsuPerformance::try_new(&converted).unwrap();
|
let _ = OsuPerformance::try_new(&map).unwrap();
|
||||||
let _ = OsuPerformance::try_new(converted.as_owned()).unwrap();
|
let _ = OsuPerformance::try_new(map.clone()).unwrap();
|
||||||
|
|
||||||
let _ = OsuPerformance::from(OsuDifficultyAttributes::default());
|
let _ = OsuPerformance::from(OsuDifficultyAttributes::default());
|
||||||
let _ = OsuPerformance::from(OsuPerformanceAttributes::default());
|
let _ = OsuPerformance::from(OsuPerformanceAttributes::default());
|
||||||
let _ = OsuPerformance::from(&converted);
|
let _ = OsuPerformance::from(&map);
|
||||||
let _ = OsuPerformance::from(converted);
|
let _ = OsuPerformance::from(map.clone());
|
||||||
|
|
||||||
let _ = OsuDifficultyAttributes::default().performance();
|
let _ = OsuDifficultyAttributes::default().performance();
|
||||||
let _ = OsuPerformanceAttributes::default().performance();
|
let _ = OsuPerformanceAttributes::default().performance();
|
||||||
|
|
||||||
map.mode = GameMode::Taiko;
|
map.convert_mut(GameMode::Taiko, &GameMods::default())
|
||||||
let converted = map.unchecked_as_converted::<Taiko>();
|
.unwrap();
|
||||||
|
|
||||||
assert!(OsuPerformance::try_new(TaikoDifficultyAttributes::default()).is_none());
|
assert!(OsuPerformance::try_new(TaikoDifficultyAttributes::default()).is_none());
|
||||||
assert!(OsuPerformance::try_new(TaikoPerformanceAttributes::default()).is_none());
|
assert!(OsuPerformance::try_new(TaikoPerformanceAttributes::default()).is_none());
|
||||||
@@ -1543,7 +1571,7 @@ mod test {
|
|||||||
TaikoPerformanceAttributes::default()
|
TaikoPerformanceAttributes::default()
|
||||||
))
|
))
|
||||||
.is_none());
|
.is_none());
|
||||||
assert!(OsuPerformance::try_new(&converted).is_none());
|
assert!(OsuPerformance::try_new(&map).is_none());
|
||||||
assert!(OsuPerformance::try_new(converted).is_none());
|
assert!(OsuPerformance::try_new(map).is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ impl Default for OsuScoreState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Type to pass [`OsuScoreState::accuracy`] and specify the origin of a score.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum OsuScoreOrigin {
|
pub enum OsuScoreOrigin {
|
||||||
/// For scores set on osu!stable
|
/// For scores set on osu!stable
|
||||||
|
|||||||
+10
-9
@@ -1,9 +1,8 @@
|
|||||||
use crate::Difficulty;
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use super::{
|
use crate::{model::mode::ConvertError, Beatmap, Difficulty};
|
||||||
convert::OsuBeatmap,
|
|
||||||
difficulty::{skills::OsuSkills, DifficultyValues},
|
use super::difficulty::{skills::OsuSkills, DifficultyValues};
|
||||||
};
|
|
||||||
|
|
||||||
/// The result of calculating the strains on a osu! map.
|
/// The result of calculating the strains on a osu! map.
|
||||||
///
|
///
|
||||||
@@ -25,7 +24,9 @@ impl OsuStrains {
|
|||||||
pub const SECTION_LEN: f64 = 400.0;
|
pub const SECTION_LEN: f64 = 400.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn strains(difficulty: &Difficulty, converted: &OsuBeatmap<'_>) -> OsuStrains {
|
pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<OsuStrains, ConvertError> {
|
||||||
|
let map = map.convert_ref(GameMode::Osu, difficulty.get_mods())?;
|
||||||
|
|
||||||
let DifficultyValues {
|
let DifficultyValues {
|
||||||
skills:
|
skills:
|
||||||
OsuSkills {
|
OsuSkills {
|
||||||
@@ -35,12 +36,12 @@ pub fn strains(difficulty: &Difficulty, converted: &OsuBeatmap<'_>) -> OsuStrain
|
|||||||
flashlight,
|
flashlight,
|
||||||
},
|
},
|
||||||
attrs: _,
|
attrs: _,
|
||||||
} = DifficultyValues::calculate(difficulty, converted);
|
} = DifficultyValues::calculate(difficulty, &map);
|
||||||
|
|
||||||
OsuStrains {
|
Ok(OsuStrains {
|
||||||
aim: aim.get_curr_strain_peaks().into_vec(),
|
aim: aim.get_curr_strain_peaks().into_vec(),
|
||||||
aim_no_sliders: aim_no_sliders.get_curr_strain_peaks().into_vec(),
|
aim_no_sliders: aim_no_sliders.get_curr_strain_peaks().into_vec(),
|
||||||
speed: speed.get_curr_strain_peaks().into_vec(),
|
speed: speed.get_curr_strain_peaks().into_vec(),
|
||||||
flashlight: flashlight.get_curr_strain_peaks().into_vec(),
|
flashlight: flashlight.get_curr_strain_peaks().into_vec(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-28
@@ -4,43 +4,17 @@ use rosu_map::{section::general::GameMode, util::Pos};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
beatmap::{Beatmap, Converted},
|
beatmap::Beatmap,
|
||||||
control_point::{DifficultyPoint, TimingPoint},
|
control_point::{DifficultyPoint, TimingPoint},
|
||||||
hit_object::{HitObject, HitObjectKind, HoldNote, Slider, Spinner},
|
hit_object::{HitObject, HitObjectKind, HoldNote, Slider, Spinner},
|
||||||
mode::ConvertStatus,
|
|
||||||
},
|
},
|
||||||
util::{float_ext::FloatExt, get_precision_adjusted_beat_len, sort::TandemSorter},
|
util::{float_ext::FloatExt, get_precision_adjusted_beat_len, sort::TandemSorter},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Taiko;
|
|
||||||
|
|
||||||
/// A [`Beatmap`] for [`Taiko`] calculations.
|
|
||||||
pub type TaikoBeatmap<'a> = Converted<'a, Taiko>;
|
|
||||||
|
|
||||||
const VELOCITY_MULTIPLIER: f32 = 1.4;
|
const VELOCITY_MULTIPLIER: f32 = 1.4;
|
||||||
const OSU_BASE_SCORING_DIST: f32 = 100.0;
|
const OSU_BASE_SCORING_DIST: f32 = 100.0;
|
||||||
|
|
||||||
pub const fn check_convert(map: &Beatmap) -> ConvertStatus {
|
pub fn convert(map: &mut Beatmap) {
|
||||||
match map.mode {
|
|
||||||
GameMode::Osu => ConvertStatus::Conversion,
|
|
||||||
GameMode::Taiko => ConvertStatus::Noop,
|
|
||||||
GameMode::Catch | GameMode::Mania => ConvertStatus::Incompatible,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_convert(map: &mut Beatmap) -> ConvertStatus {
|
|
||||||
match map.mode {
|
|
||||||
GameMode::Osu => {
|
|
||||||
convert(map);
|
|
||||||
|
|
||||||
ConvertStatus::Conversion
|
|
||||||
}
|
|
||||||
GameMode::Taiko => ConvertStatus::Noop,
|
|
||||||
GameMode::Catch | GameMode::Mania => ConvertStatus::Incompatible,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert(map: &mut Beatmap) {
|
|
||||||
let mut new_objects = Vec::new();
|
let mut new_objects = Vec::new();
|
||||||
let mut new_sounds = Vec::new();
|
let mut new_sounds = Vec::new();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use std::{cmp, mem, slice::Iter};
|
use std::{cmp, mem, slice::Iter};
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::skills::Skill,
|
any::difficulty::skills::Skill,
|
||||||
model::{beatmap::HitWindows, hit_object::HitObject},
|
model::{beatmap::HitWindows, hit_object::HitObject, mode::ConvertError},
|
||||||
taiko::TaikoBeatmap,
|
|
||||||
util::sync::RefCount,
|
util::sync::RefCount,
|
||||||
Difficulty,
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -69,13 +70,15 @@ enum FirstTwoCombos {
|
|||||||
|
|
||||||
impl TaikoGradualDifficulty {
|
impl TaikoGradualDifficulty {
|
||||||
/// Create a new difficulty attributes iterator for osu!taiko maps.
|
/// Create a new difficulty attributes iterator for osu!taiko maps.
|
||||||
pub fn new(difficulty: Difficulty, converted: &TaikoBeatmap<'_>) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
|
||||||
|
let map = map.convert_ref(GameMode::Taiko, difficulty.get_mods())?;
|
||||||
|
|
||||||
let take = difficulty.get_passed_objects();
|
let take = difficulty.get_passed_objects();
|
||||||
let clock_rate = difficulty.get_clock_rate();
|
let clock_rate = difficulty.get_clock_rate();
|
||||||
|
|
||||||
let first_combos = match (
|
let first_combos = match (
|
||||||
converted.hit_objects.first().map(HitObject::is_circle),
|
map.hit_objects.first().map(HitObject::is_circle),
|
||||||
converted.hit_objects.get(1).map(HitObject::is_circle),
|
map.hit_objects.get(1).map(HitObject::is_circle),
|
||||||
) {
|
) {
|
||||||
(None, _) | (Some(false), Some(false) | None) => FirstTwoCombos::None,
|
(None, _) | (Some(false), Some(false) | None) => FirstTwoCombos::None,
|
||||||
(Some(true), Some(false) | None) => FirstTwoCombos::OnlyFirst,
|
(Some(true), Some(false) | None) => FirstTwoCombos::OnlyFirst,
|
||||||
@@ -87,13 +90,13 @@ impl TaikoGradualDifficulty {
|
|||||||
od_great,
|
od_great,
|
||||||
od_ok,
|
od_ok,
|
||||||
ar: _,
|
ar: _,
|
||||||
} = converted.attributes().difficulty(&difficulty).hit_windows();
|
} = map.attributes().difficulty(&difficulty).hit_windows();
|
||||||
|
|
||||||
let mut n_diff_objects = 0;
|
let mut n_diff_objects = 0;
|
||||||
let mut max_combo = 0;
|
let mut max_combo = 0;
|
||||||
|
|
||||||
let diff_objects = DifficultyValues::create_difficulty_objects(
|
let diff_objects = DifficultyValues::create_difficulty_objects(
|
||||||
converted,
|
&map,
|
||||||
take as u32,
|
take as u32,
|
||||||
clock_rate,
|
clock_rate,
|
||||||
&mut max_combo,
|
&mut max_combo,
|
||||||
@@ -105,19 +108,15 @@ impl TaikoGradualDifficulty {
|
|||||||
let attrs = TaikoDifficultyAttributes {
|
let attrs = TaikoDifficultyAttributes {
|
||||||
great_hit_window: od_great,
|
great_hit_window: od_great,
|
||||||
ok_hit_window: od_ok.unwrap_or(0.0),
|
ok_hit_window: od_ok.unwrap_or(0.0),
|
||||||
is_convert: converted.is_convert,
|
is_convert: map.is_convert,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let total_hits = converted
|
let total_hits = map.hit_objects.iter().filter(|h| h.is_circle()).count();
|
||||||
.hit_objects
|
|
||||||
.iter()
|
|
||||||
.filter(|h| h.is_circle())
|
|
||||||
.count();
|
|
||||||
|
|
||||||
let diff_objects_iter = extend_lifetime(diff_objects.iter());
|
let diff_objects_iter = extend_lifetime(diff_objects.iter());
|
||||||
|
|
||||||
Self {
|
Ok(Self {
|
||||||
idx: 0,
|
idx: 0,
|
||||||
difficulty,
|
difficulty,
|
||||||
diff_objects,
|
diff_objects,
|
||||||
@@ -126,7 +125,7 @@ impl TaikoGradualDifficulty {
|
|||||||
attrs,
|
attrs,
|
||||||
total_hits,
|
total_hits,
|
||||||
first_combos,
|
first_combos,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,38 +265,30 @@ impl ExactSizeIterator for TaikoGradualDifficulty {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::Beatmap;
|
use crate::{taiko::Taiko, Beatmap};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn empty() {
|
||||||
let converted = Beatmap::from_bytes(&[]).unwrap().unchecked_into_converted();
|
let map = Beatmap::from_bytes(&[]).unwrap();
|
||||||
|
let mut gradual = TaikoGradualDifficulty::new(Difficulty::new(), &map).unwrap();
|
||||||
let mut gradual = TaikoGradualDifficulty::new(Difficulty::new(), &converted);
|
|
||||||
|
|
||||||
assert!(gradual.next().is_none());
|
assert!(gradual.next().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_and_nth() {
|
fn next_and_nth() {
|
||||||
let converted = Beatmap::from_path("./resources/1028484.osu")
|
let map = Beatmap::from_path("./resources/1028484.osu").unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted();
|
|
||||||
|
|
||||||
let difficulty = Difficulty::new();
|
let difficulty = Difficulty::new();
|
||||||
|
|
||||||
let mut gradual = TaikoGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual = TaikoGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_2nd = TaikoGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual_2nd = TaikoGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_3rd = TaikoGradualDifficulty::new(difficulty.clone(), &converted);
|
let mut gradual_3rd = TaikoGradualDifficulty::new(difficulty.clone(), &map).unwrap();
|
||||||
|
|
||||||
let hit_objects_len = converted.hit_objects.len();
|
let hit_objects_len = map.hit_objects.len();
|
||||||
|
|
||||||
let n_hits = converted
|
let n_hits = map.hit_objects.iter().filter(|h| h.is_circle()).count();
|
||||||
.hit_objects
|
|
||||||
.iter()
|
|
||||||
.filter(|h| h.is_circle())
|
|
||||||
.count();
|
|
||||||
|
|
||||||
for i in 1.. {
|
for i in 1.. {
|
||||||
let Some(next_gradual) = gradual.next() else {
|
let Some(next_gradual) = gradual.next() else {
|
||||||
@@ -320,8 +311,8 @@ mod tests {
|
|||||||
let expected = difficulty
|
let expected = difficulty
|
||||||
.clone()
|
.clone()
|
||||||
.passed_objects(i as u32)
|
.passed_objects(i as u32)
|
||||||
.with_mode()
|
.calculate_for_mode::<Taiko>(&map)
|
||||||
.calculate(&converted);
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(next_gradual, expected);
|
assert_eq!(next_gradual, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-11
@@ -1,8 +1,10 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::difficulty::skills::Skill,
|
any::difficulty::skills::Skill,
|
||||||
model::beatmap::HitWindows,
|
model::{beatmap::HitWindows, mode::ConvertError},
|
||||||
taiko::{
|
taiko::{
|
||||||
difficulty::{
|
difficulty::{
|
||||||
color::preprocessor::ColorDifficultyPreprocessor,
|
color::preprocessor::ColorDifficultyPreprocessor,
|
||||||
@@ -10,12 +12,12 @@ use crate::{
|
|||||||
},
|
},
|
||||||
object::TaikoObject,
|
object::TaikoObject,
|
||||||
},
|
},
|
||||||
Difficulty,
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::skills::{color::Color, rhythm::Rhythm, stamina::Stamina, TaikoSkills};
|
use self::skills::{color::Color, rhythm::Rhythm, stamina::Stamina, TaikoSkills};
|
||||||
|
|
||||||
use super::{attributes::TaikoDifficultyAttributes, convert::TaikoBeatmap};
|
use super::attributes::TaikoDifficultyAttributes;
|
||||||
|
|
||||||
mod color;
|
mod color;
|
||||||
pub mod gradual;
|
pub mod gradual;
|
||||||
@@ -30,27 +32,29 @@ const STAMINA_SKILL_MULTIPLIER: f64 = 0.375 * DIFFICULTY_MULTIPLIER;
|
|||||||
|
|
||||||
pub fn difficulty(
|
pub fn difficulty(
|
||||||
difficulty: &Difficulty,
|
difficulty: &Difficulty,
|
||||||
converted: &TaikoBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> TaikoDifficultyAttributes {
|
) -> Result<TaikoDifficultyAttributes, ConvertError> {
|
||||||
|
let map = map.convert_ref(GameMode::Taiko, difficulty.get_mods())?;
|
||||||
|
|
||||||
let HitWindows {
|
let HitWindows {
|
||||||
od_great,
|
od_great,
|
||||||
od_ok,
|
od_ok,
|
||||||
ar: _,
|
ar: _,
|
||||||
} = converted.attributes().difficulty(difficulty).hit_windows();
|
} = map.attributes().difficulty(difficulty).hit_windows();
|
||||||
|
|
||||||
let DifficultyValues { skills, max_combo } = DifficultyValues::calculate(difficulty, converted);
|
let DifficultyValues { skills, max_combo } = DifficultyValues::calculate(difficulty, &map);
|
||||||
|
|
||||||
let mut attrs = TaikoDifficultyAttributes {
|
let mut attrs = TaikoDifficultyAttributes {
|
||||||
great_hit_window: od_great,
|
great_hit_window: od_great,
|
||||||
ok_hit_window: od_ok.unwrap_or(0.0),
|
ok_hit_window: od_ok.unwrap_or(0.0),
|
||||||
max_combo,
|
max_combo,
|
||||||
is_convert: converted.is_convert,
|
is_convert: map.is_convert,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
DifficultyValues::eval(&mut attrs, skills);
|
DifficultyValues::eval(&mut attrs, skills);
|
||||||
|
|
||||||
attrs
|
Ok(attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn combined_difficulty_value(color: Color, rhythm: Rhythm, stamina: Stamina) -> f64 {
|
fn combined_difficulty_value(color: Color, rhythm: Rhythm, stamina: Stamina) -> f64 {
|
||||||
@@ -118,7 +122,7 @@ pub struct DifficultyValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DifficultyValues {
|
impl DifficultyValues {
|
||||||
pub fn calculate(difficulty: &Difficulty, converted: &TaikoBeatmap<'_>) -> Self {
|
pub fn calculate(difficulty: &Difficulty, converted: &Beatmap) -> Self {
|
||||||
let take = difficulty.get_passed_objects();
|
let take = difficulty.get_passed_objects();
|
||||||
let clock_rate = difficulty.get_clock_rate();
|
let clock_rate = difficulty.get_clock_rate();
|
||||||
|
|
||||||
@@ -190,7 +194,7 @@ impl DifficultyValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_difficulty_objects(
|
pub fn create_difficulty_objects(
|
||||||
converted: &TaikoBeatmap<'_>,
|
converted: &Beatmap,
|
||||||
take: u32,
|
take: u32,
|
||||||
clock_rate: f64,
|
clock_rate: f64,
|
||||||
max_combo: &mut u32,
|
max_combo: &mut u32,
|
||||||
|
|||||||
+20
-20
@@ -1,14 +1,15 @@
|
|||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
beatmap::Beatmap,
|
beatmap::Beatmap,
|
||||||
mode::{ConvertStatus, IGameMode},
|
mode::{ConvertError, IGameMode},
|
||||||
},
|
},
|
||||||
Difficulty,
|
Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
attributes::{TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
attributes::{TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
||||||
convert::TaikoBeatmap,
|
|
||||||
difficulty::gradual::TaikoGradualDifficulty,
|
difficulty::gradual::TaikoGradualDifficulty,
|
||||||
performance::{gradual::TaikoGradualPerformance, TaikoPerformance},
|
performance::{gradual::TaikoGradualPerformance, TaikoPerformance},
|
||||||
score_state::TaikoScoreState,
|
score_state::TaikoScoreState,
|
||||||
@@ -28,6 +29,13 @@ mod strains;
|
|||||||
/// [`GameMode::Taiko`]: rosu_map::section::general::GameMode::Taiko
|
/// [`GameMode::Taiko`]: rosu_map::section::general::GameMode::Taiko
|
||||||
pub struct Taiko;
|
pub struct Taiko;
|
||||||
|
|
||||||
|
impl Taiko {
|
||||||
|
pub fn convert(map: &mut Beatmap) {
|
||||||
|
debug_assert!(!map.is_convert && map.mode == GameMode::Osu);
|
||||||
|
convert::convert(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IGameMode for Taiko {
|
impl IGameMode for Taiko {
|
||||||
type DifficultyAttributes = TaikoDifficultyAttributes;
|
type DifficultyAttributes = TaikoDifficultyAttributes;
|
||||||
type Strains = TaikoStrains;
|
type Strains = TaikoStrains;
|
||||||
@@ -35,40 +43,32 @@ impl IGameMode for Taiko {
|
|||||||
type GradualDifficulty = TaikoGradualDifficulty;
|
type GradualDifficulty = TaikoGradualDifficulty;
|
||||||
type GradualPerformance = TaikoGradualPerformance;
|
type GradualPerformance = TaikoGradualPerformance;
|
||||||
|
|
||||||
fn check_convert(map: &Beatmap) -> ConvertStatus {
|
|
||||||
convert::check_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_convert(map: &mut Beatmap) -> ConvertStatus {
|
|
||||||
convert::try_convert(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn difficulty(
|
fn difficulty(
|
||||||
difficulty: &Difficulty,
|
difficulty: &Difficulty,
|
||||||
converted: &TaikoBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::DifficultyAttributes {
|
) -> Result<Self::DifficultyAttributes, ConvertError> {
|
||||||
difficulty::difficulty(difficulty, converted)
|
difficulty::difficulty(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strains(difficulty: &Difficulty, converted: &TaikoBeatmap<'_>) -> Self::Strains {
|
fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<Self::Strains, ConvertError> {
|
||||||
strains::strains(difficulty, converted)
|
strains::strains(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn performance(map: TaikoBeatmap<'_>) -> Self::Performance<'_> {
|
fn performance(map: &Beatmap) -> Self::Performance<'_> {
|
||||||
TaikoPerformance::new(map)
|
TaikoPerformance::new(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradual_difficulty(
|
fn gradual_difficulty(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &TaikoBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualDifficulty {
|
) -> Result<Self::GradualDifficulty, ConvertError> {
|
||||||
TaikoGradualDifficulty::new(difficulty, map)
|
TaikoGradualDifficulty::new(difficulty, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gradual_performance(
|
fn gradual_performance(
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
map: &TaikoBeatmap<'_>,
|
map: &Beatmap,
|
||||||
) -> Self::GradualPerformance {
|
) -> Result<Self::GradualPerformance, ConvertError> {
|
||||||
TaikoGradualPerformance::new(difficulty, map)
|
TaikoGradualPerformance::new(difficulty, map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
taiko::{difficulty::gradual::TaikoGradualDifficulty, TaikoBeatmap, TaikoScoreState},
|
model::mode::ConvertError,
|
||||||
Difficulty,
|
taiko::{difficulty::gradual::TaikoGradualDifficulty, TaikoScoreState},
|
||||||
|
Beatmap, Difficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::TaikoPerformanceAttributes;
|
use super::TaikoPerformanceAttributes;
|
||||||
@@ -84,10 +85,10 @@ pub struct TaikoGradualPerformance {
|
|||||||
|
|
||||||
impl TaikoGradualPerformance {
|
impl TaikoGradualPerformance {
|
||||||
/// Create a new gradual performance calculator for osu!taiko maps.
|
/// Create a new gradual performance calculator for osu!taiko maps.
|
||||||
pub fn new(difficulty: Difficulty, converted: &TaikoBeatmap<'_>) -> Self {
|
pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
|
||||||
let difficulty = TaikoGradualDifficulty::new(difficulty, converted);
|
let difficulty = TaikoGradualDifficulty::new(difficulty, map)?;
|
||||||
|
|
||||||
Self { difficulty }
|
Ok(Self { difficulty })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process the next hit object and calculate the performance attributes
|
/// Process the next hit object and calculate the performance attributes
|
||||||
@@ -107,6 +108,7 @@ impl TaikoGradualPerformance {
|
|||||||
///
|
///
|
||||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
||||||
/// `n=1` will process 2, and so on.
|
/// `n=1` will process 2, and so on.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn nth(&mut self, state: TaikoScoreState, n: usize) -> Option<TaikoPerformanceAttributes> {
|
pub fn nth(&mut self, state: TaikoScoreState, n: usize) -> Option<TaikoPerformanceAttributes> {
|
||||||
let performance = self
|
let performance = self
|
||||||
.difficulty
|
.difficulty
|
||||||
@@ -115,7 +117,8 @@ impl TaikoGradualPerformance {
|
|||||||
.state(state)
|
.state(state)
|
||||||
.difficulty(self.difficulty.difficulty.clone())
|
.difficulty(self.difficulty.difficulty.clone())
|
||||||
.passed_objects(self.difficulty.idx as u32)
|
.passed_objects(self.difficulty.idx as u32)
|
||||||
.calculate();
|
.calculate()
|
||||||
|
.expect("no conversion required");
|
||||||
|
|
||||||
Some(performance)
|
Some(performance)
|
||||||
}
|
}
|
||||||
@@ -135,25 +138,19 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_and_nth() {
|
fn next_and_nth() {
|
||||||
let converted = Beatmap::from_path("./resources/1028484.osu")
|
let map = Beatmap::from_path("./resources/1028484.osu").unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted();
|
|
||||||
|
|
||||||
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
let difficulty = Difficulty::new().mods(88); // HDHRDT
|
||||||
|
|
||||||
let mut gradual = TaikoGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual = TaikoGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_2nd = TaikoGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual_2nd = TaikoGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
let mut gradual_3rd = TaikoGradualPerformance::new(difficulty.clone(), &converted);
|
let mut gradual_3rd = TaikoGradualPerformance::new(difficulty.clone(), &map).unwrap();
|
||||||
|
|
||||||
let mut state = TaikoScoreState::default();
|
let mut state = TaikoScoreState::default();
|
||||||
|
|
||||||
let hit_objects_len = converted.hit_objects.len();
|
let hit_objects_len = map.hit_objects.len();
|
||||||
|
|
||||||
let n_hits = converted
|
let n_hits = map.hit_objects.iter().filter(|h| h.is_circle()).count();
|
||||||
.hit_objects
|
|
||||||
.iter()
|
|
||||||
.filter(|h| h.is_circle())
|
|
||||||
.count();
|
|
||||||
|
|
||||||
for i in 1.. {
|
for i in 1.. {
|
||||||
state.misses += 1;
|
state.misses += 1;
|
||||||
@@ -175,15 +172,15 @@ mod tests {
|
|||||||
assert_eq!(next_gradual, next_gradual_3rd);
|
assert_eq!(next_gradual, next_gradual_3rd);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut regular_calc = TaikoPerformance::new(converted.as_owned())
|
let mut regular_calc = TaikoPerformance::new(&map)
|
||||||
.difficulty(difficulty.clone())
|
.difficulty(difficulty.clone())
|
||||||
.passed_objects(i as u32)
|
.passed_objects(i as u32)
|
||||||
.state(state);
|
.state(state);
|
||||||
|
|
||||||
let regular_state = regular_calc.generate_state();
|
let regular_state = regular_calc.generate_state().unwrap();
|
||||||
assert_eq!(state, regular_state);
|
assert_eq!(state, regular_state);
|
||||||
|
|
||||||
let expected = regular_calc.calculate();
|
let expected = regular_calc.calculate().unwrap();
|
||||||
|
|
||||||
assert_eq!(next_gradual, expected);
|
assert_eq!(next_gradual, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance},
|
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance},
|
||||||
model::mods::GameMods,
|
model::{mode::ConvertError, mods::GameMods},
|
||||||
osu::OsuPerformance,
|
osu::OsuPerformance,
|
||||||
util::{map_or_attrs::MapOrAttrs, special_functions},
|
util::{map_or_attrs::MapOrAttrs, special_functions},
|
||||||
Performance,
|
Performance,
|
||||||
@@ -36,7 +38,7 @@ impl<'map> TaikoPerformance<'map> {
|
|||||||
/// The argument `map_or_attrs` must be either
|
/// The argument `map_or_attrs` must be either
|
||||||
/// - previously calculated attributes ([`TaikoDifficultyAttributes`]
|
/// - previously calculated attributes ([`TaikoDifficultyAttributes`]
|
||||||
/// or [`TaikoPerformanceAttributes`])
|
/// or [`TaikoPerformanceAttributes`])
|
||||||
/// - a beatmap ([`TaikoBeatmap<'map>`])
|
/// - a [`Beatmap`] (by reference or value)
|
||||||
///
|
///
|
||||||
/// If a map is given, difficulty attributes will need to be calculated
|
/// If a map is given, difficulty attributes will need to be calculated
|
||||||
/// internally which is a costly operation. Hence, passing attributes
|
/// internally which is a costly operation. Hence, passing attributes
|
||||||
@@ -46,20 +48,19 @@ impl<'map> TaikoPerformance<'map> {
|
|||||||
/// have been calculated for the same map and [`Difficulty`] settings.
|
/// have been calculated for the same map and [`Difficulty`] settings.
|
||||||
/// Otherwise, the final attributes will be incorrect.
|
/// Otherwise, the final attributes will be incorrect.
|
||||||
///
|
///
|
||||||
/// [`TaikoBeatmap<'map>`]: crate::taiko::TaikoBeatmap
|
/// [`Beatmap`]: crate::model::beatmap::Beatmap
|
||||||
pub fn new(map_or_attrs: impl IntoModePerformance<'map, Taiko>) -> Self {
|
pub fn new(map_or_attrs: impl IntoModePerformance<'map, Taiko>) -> Self {
|
||||||
map_or_attrs.into_performance()
|
map_or_attrs.into_performance()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to create a new performance calculator for osu!taiko maps.
|
/// Try to create a new performance calculator for osu!taiko maps.
|
||||||
///
|
///
|
||||||
/// Returns `None` if `map_or_attrs` does not belong to osu!taiko e.g.
|
/// Returns `None` if `map_or_attrs` does not belong to osu!taiko i.e.
|
||||||
/// a [`Converted`], [`DifficultyAttributes`], or [`PerformanceAttributes`]
|
/// a [`DifficultyAttributes`] or [`PerformanceAttributes`] of a different
|
||||||
/// of a different mode.
|
/// mode.
|
||||||
///
|
///
|
||||||
/// See [`TaikoPerformance::new`] for more information.
|
/// See [`TaikoPerformance::new`] for more information.
|
||||||
///
|
///
|
||||||
/// [`Converted`]: crate::model::beatmap::Converted
|
|
||||||
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
/// [`DifficultyAttributes`]: crate::any::DifficultyAttributes
|
||||||
/// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
|
/// [`PerformanceAttributes`]: crate::any::PerformanceAttributes
|
||||||
pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
|
pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
|
||||||
@@ -214,10 +215,10 @@ impl<'map> TaikoPerformance<'map> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create the [`TaikoScoreState`] that will be used for performance calculation.
|
/// Create the [`TaikoScoreState`] that will be used for performance calculation.
|
||||||
pub fn generate_state(&mut self) -> TaikoScoreState {
|
pub fn generate_state(&mut self) -> Result<TaikoScoreState, ConvertError> {
|
||||||
let attrs = match self.map_or_attrs {
|
let attrs = match self.map_or_attrs {
|
||||||
MapOrAttrs::Map(ref map) => {
|
MapOrAttrs::Map(ref map) => {
|
||||||
let attrs = self.difficulty.with_mode().calculate(map);
|
let attrs = self.difficulty.calculate_for_mode::<Taiko>(map)?;
|
||||||
|
|
||||||
self.map_or_attrs.insert_attrs(attrs)
|
self.map_or_attrs.insert_attrs(attrs)
|
||||||
}
|
}
|
||||||
@@ -297,21 +298,21 @@ impl<'map> TaikoPerformance<'map> {
|
|||||||
self.n100 = Some(n100);
|
self.n100 = Some(n100);
|
||||||
self.misses = Some(misses);
|
self.misses = Some(misses);
|
||||||
|
|
||||||
TaikoScoreState {
|
Ok(TaikoScoreState {
|
||||||
max_combo,
|
max_combo,
|
||||||
n300,
|
n300,
|
||||||
n100,
|
n100,
|
||||||
misses,
|
misses,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
pub fn calculate(mut self) -> Result<TaikoPerformanceAttributes, ConvertError> {
|
||||||
let state = self.generate_state();
|
let state = self.generate_state()?;
|
||||||
|
|
||||||
let attrs = match self.map_or_attrs {
|
let attrs = match self.map_or_attrs {
|
||||||
MapOrAttrs::Map(ref map) => self.difficulty.with_mode().calculate(map),
|
|
||||||
MapOrAttrs::Attrs(attrs) => attrs,
|
MapOrAttrs::Attrs(attrs) => attrs,
|
||||||
|
MapOrAttrs::Map(ref map) => self.difficulty.calculate_for_mode::<Taiko>(map)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = TaikoPerformanceInner {
|
let inner = TaikoPerformanceInner {
|
||||||
@@ -320,7 +321,7 @@ impl<'map> TaikoPerformance<'map> {
|
|||||||
attrs,
|
attrs,
|
||||||
};
|
};
|
||||||
|
|
||||||
inner.calculate()
|
Ok(inner.calculate())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Taiko>) -> Self {
|
pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Taiko>) -> Self {
|
||||||
@@ -346,14 +347,12 @@ impl<'map> TryFrom<OsuPerformance<'map>> for TaikoPerformance<'map> {
|
|||||||
/// if it was constructed through attributes or
|
/// if it was constructed through attributes or
|
||||||
/// [`OsuPerformance::generate_state`] was called.
|
/// [`OsuPerformance::generate_state`] was called.
|
||||||
fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
|
fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
|
||||||
let MapOrAttrs::Map(converted) = osu.map_or_attrs else {
|
let mods = osu.difficulty.get_mods();
|
||||||
return Err(osu);
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = match converted.try_convert() {
|
let map = match OsuPerformance::try_convert_map(osu.map_or_attrs, GameMode::Taiko, mods) {
|
||||||
Ok(map) => map,
|
Ok(map) => map,
|
||||||
Err(map) => {
|
Err(map_or_attrs) => {
|
||||||
osu.map_or_attrs = MapOrAttrs::Map(map);
|
osu.map_or_attrs = map_or_attrs;
|
||||||
|
|
||||||
return Err(osu);
|
return Err(osu);
|
||||||
}
|
}
|
||||||
@@ -594,7 +593,7 @@ mod test {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any::{DifficultyAttributes, PerformanceAttributes},
|
any::{DifficultyAttributes, PerformanceAttributes},
|
||||||
osu::{Osu, OsuDifficultyAttributes, OsuPerformanceAttributes},
|
osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||||
Beatmap,
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -611,8 +610,8 @@ mod test {
|
|||||||
fn attrs() -> TaikoDifficultyAttributes {
|
fn attrs() -> TaikoDifficultyAttributes {
|
||||||
ATTRS
|
ATTRS
|
||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
let converted = beatmap().unchecked_into_converted::<Taiko>();
|
let map = beatmap();
|
||||||
let attrs = Difficulty::new().with_mode().calculate(&converted);
|
let attrs = Difficulty::new().calculate_for_mode::<Taiko>(&map).unwrap();
|
||||||
|
|
||||||
assert_eq!(MAX_COMBO, attrs.max_combo);
|
assert_eq!(MAX_COMBO, attrs.max_combo);
|
||||||
|
|
||||||
@@ -715,8 +714,8 @@ mod test {
|
|||||||
state = state.misses(misses);
|
state = state.misses(misses);
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = state.generate_state();
|
let first = state.generate_state().unwrap();
|
||||||
let state = state.generate_state();
|
let state = state.generate_state().unwrap();
|
||||||
assert_eq!(first, state);
|
assert_eq!(first, state);
|
||||||
|
|
||||||
let mut expected = brute_force_best(
|
let mut expected = brute_force_best(
|
||||||
@@ -739,7 +738,8 @@ mod test {
|
|||||||
.n300(150)
|
.n300(150)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.hitresult_priority(HitResultPriority::BestCase)
|
.hitresult_priority(HitResultPriority::BestCase)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = TaikoScoreState {
|
let expected = TaikoScoreState {
|
||||||
max_combo: 100,
|
max_combo: 100,
|
||||||
@@ -757,7 +757,8 @@ mod test {
|
|||||||
.combo(100)
|
.combo(100)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.hitresult_priority(HitResultPriority::BestCase)
|
.hitresult_priority(HitResultPriority::BestCase)
|
||||||
.generate_state();
|
.generate_state()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let expected = TaikoScoreState {
|
let expected = TaikoScoreState {
|
||||||
max_combo: 100,
|
max_combo: 100,
|
||||||
@@ -772,12 +773,11 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn create() {
|
fn create() {
|
||||||
let mut map = beatmap();
|
let mut map = beatmap();
|
||||||
let converted = map.unchecked_as_converted();
|
|
||||||
|
|
||||||
let _ = TaikoPerformance::new(TaikoDifficultyAttributes::default());
|
let _ = TaikoPerformance::new(TaikoDifficultyAttributes::default());
|
||||||
let _ = TaikoPerformance::new(TaikoPerformanceAttributes::default());
|
let _ = TaikoPerformance::new(TaikoPerformanceAttributes::default());
|
||||||
let _ = TaikoPerformance::new(&converted);
|
let _ = TaikoPerformance::new(&map);
|
||||||
let _ = TaikoPerformance::new(converted.as_owned());
|
let _ = TaikoPerformance::new(map.clone());
|
||||||
|
|
||||||
let _ = TaikoPerformance::try_new(TaikoDifficultyAttributes::default()).unwrap();
|
let _ = TaikoPerformance::try_new(TaikoDifficultyAttributes::default()).unwrap();
|
||||||
let _ = TaikoPerformance::try_new(TaikoPerformanceAttributes::default()).unwrap();
|
let _ = TaikoPerformance::try_new(TaikoPerformanceAttributes::default()).unwrap();
|
||||||
@@ -789,19 +789,18 @@ mod test {
|
|||||||
TaikoPerformanceAttributes::default(),
|
TaikoPerformanceAttributes::default(),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = TaikoPerformance::try_new(&converted).unwrap();
|
let _ = TaikoPerformance::try_new(&map).unwrap();
|
||||||
let _ = TaikoPerformance::try_new(converted.as_owned()).unwrap();
|
let _ = TaikoPerformance::try_new(map.clone()).unwrap();
|
||||||
|
|
||||||
let _ = TaikoPerformance::from(TaikoDifficultyAttributes::default());
|
let _ = TaikoPerformance::from(TaikoDifficultyAttributes::default());
|
||||||
let _ = TaikoPerformance::from(TaikoPerformanceAttributes::default());
|
let _ = TaikoPerformance::from(TaikoPerformanceAttributes::default());
|
||||||
let _ = TaikoPerformance::from(&converted);
|
let _ = TaikoPerformance::from(&map);
|
||||||
let _ = TaikoPerformance::from(converted);
|
let _ = TaikoPerformance::from(map.clone());
|
||||||
|
|
||||||
let _ = TaikoDifficultyAttributes::default().performance();
|
let _ = TaikoDifficultyAttributes::default().performance();
|
||||||
let _ = TaikoPerformanceAttributes::default().performance();
|
let _ = TaikoPerformanceAttributes::default().performance();
|
||||||
|
|
||||||
map.mode = GameMode::Osu;
|
assert!(map.convert_mut(GameMode::Osu, &Default::default()).is_err());
|
||||||
let converted = map.unchecked_as_converted::<Osu>();
|
|
||||||
|
|
||||||
assert!(TaikoPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
assert!(TaikoPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
|
||||||
assert!(TaikoPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
assert!(TaikoPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
|
||||||
@@ -813,7 +812,5 @@ mod test {
|
|||||||
OsuPerformanceAttributes::default()
|
OsuPerformanceAttributes::default()
|
||||||
))
|
))
|
||||||
.is_none());
|
.is_none());
|
||||||
assert!(TaikoPerformance::try_new(&converted).is_none());
|
|
||||||
assert!(TaikoPerformance::try_new(converted).is_none());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{taiko::difficulty::DifficultyValues, Difficulty};
|
use rosu_map::section::general::GameMode;
|
||||||
|
|
||||||
use super::convert::TaikoBeatmap;
|
use crate::{model::mode::ConvertError, taiko::difficulty::DifficultyValues, Beatmap, Difficulty};
|
||||||
|
|
||||||
/// The result of calculating the strains on a osu!taiko map.
|
/// The result of calculating the strains on a osu!taiko map.
|
||||||
///
|
///
|
||||||
@@ -20,12 +20,13 @@ impl TaikoStrains {
|
|||||||
pub const SECTION_LEN: f64 = 400.0;
|
pub const SECTION_LEN: f64 = 400.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn strains(difficulty: &Difficulty, converted: &TaikoBeatmap<'_>) -> TaikoStrains {
|
pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<TaikoStrains, ConvertError> {
|
||||||
let values = DifficultyValues::calculate(difficulty, converted);
|
let map = map.convert_ref(GameMode::Taiko, difficulty.get_mods())?;
|
||||||
|
let values = DifficultyValues::calculate(difficulty, &map);
|
||||||
|
|
||||||
TaikoStrains {
|
Ok(TaikoStrains {
|
||||||
color: values.skills.color.get_curr_strain_peaks().into_vec(),
|
color: values.skills.color.get_curr_strain_peaks().into_vec(),
|
||||||
rhythm: values.skills.rhythm.get_curr_strain_peaks().into_vec(),
|
rhythm: values.skills.rhythm.get_curr_strain_peaks().into_vec(),
|
||||||
stamina: values.skills.stamina.get_curr_strain_peaks().into_vec(),
|
stamina: values.skills.stamina.get_curr_strain_peaks().into_vec(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
use std::{
|
|
||||||
any,
|
|
||||||
fmt::{Debug, Formatter, Result as FmtResult},
|
|
||||||
marker::PhantomData,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct GenericFormatter<T>(PhantomData<T>);
|
|
||||||
|
|
||||||
impl<T> GenericFormatter<T> {
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self(PhantomData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Debug for GenericFormatter<T> {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
||||||
fn fmt_stripped(full_type_name: &str, f: &mut Formatter<'_>) -> FmtResult {
|
|
||||||
// Strip fully qualified syntax
|
|
||||||
if let Some(position) = full_type_name.rfind("::") {
|
|
||||||
if let Some(type_name) = full_type_name.get(position + 2..) {
|
|
||||||
f.write_str(type_name)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt_stripped(any::type_name::<T>(), f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
use std::fmt::{Debug, Formatter, Result as FmtResult};
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
fmt::{Debug, Formatter, Result as FmtResult},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::model::{beatmap::Converted, mode::IGameMode};
|
use crate::{model::mode::IGameMode, Beatmap};
|
||||||
|
|
||||||
pub enum MapOrAttrs<'map, M: IGameMode> {
|
pub enum MapOrAttrs<'map, M: IGameMode> {
|
||||||
Map(Converted<'map, M>),
|
Map(Cow<'map, Beatmap>),
|
||||||
Attrs(M::DifficultyAttributes),
|
Attrs(M::DifficultyAttributes),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +72,15 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map, M: IGameMode> From<Converted<'map, M>> for MapOrAttrs<'map, M> {
|
impl<'map, M: IGameMode> From<&'map Beatmap> for MapOrAttrs<'map, M> {
|
||||||
fn from(converted: Converted<'map, M>) -> Self {
|
fn from(map: &'map Beatmap) -> Self {
|
||||||
Self::Map(converted)
|
Self::Map(Cow::Borrowed(map))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: IGameMode> From<Beatmap> for MapOrAttrs<'_, M> {
|
||||||
|
fn from(map: Beatmap) -> Self {
|
||||||
|
Self::Map(Cow::Owned(map))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod float_ext;
|
pub mod float_ext;
|
||||||
pub mod generic_fmt;
|
|
||||||
pub mod limited_queue;
|
pub mod limited_queue;
|
||||||
pub mod map_or_attrs;
|
pub mod map_or_attrs;
|
||||||
pub mod random;
|
pub mod random;
|
||||||
|
|||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
use rosu_pp::{catch::Catch, mania::Mania, model::mode::GameMode, osu::Osu, taiko::Taiko, Beatmap};
|
use rosu_pp::{model::mode::GameMode, Beatmap, GameMods};
|
||||||
|
|
||||||
use crate::common::assert_eq_float;
|
use crate::common::assert_eq_float;
|
||||||
|
|
||||||
@@ -92,23 +92,23 @@ fn mania() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn empty_osu() {
|
fn empty_osu() {
|
||||||
let map = Beatmap::from_bytes(&[]).unwrap();
|
let map = Beatmap::from_bytes(&[]).unwrap();
|
||||||
let _ = map.unchecked_as_converted::<Osu>();
|
let _ = map.convert(GameMode::Osu, &GameMods::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_taiko() {
|
fn empty_taiko() {
|
||||||
let map = Beatmap::from_bytes(&[]).unwrap();
|
let map = Beatmap::from_bytes(&[]).unwrap();
|
||||||
let _ = map.unchecked_as_converted::<Taiko>();
|
let _ = map.convert(GameMode::Taiko, &GameMods::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_catch() {
|
fn empty_catch() {
|
||||||
let map = Beatmap::from_bytes(&[]).unwrap();
|
let map = Beatmap::from_bytes(&[]).unwrap();
|
||||||
let _ = map.unchecked_as_converted::<Catch>();
|
let _ = map.convert(GameMode::Catch, &GameMods::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_mania() {
|
fn empty_mania() {
|
||||||
let map = Beatmap::from_bytes(&[]).unwrap();
|
let map = Beatmap::from_bytes(&[]).unwrap();
|
||||||
let _ = map.unchecked_as_converted::<Mania>();
|
let _ = map.convert(GameMode::Mania, &GameMods::default());
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -18,14 +18,17 @@ macro_rules! test_cases {
|
|||||||
$( $key:ident: $value:literal $( , )? )*
|
$( $key:ident: $value:literal $( , )? )*
|
||||||
} $( ; )? )*
|
} $( ; )? )*
|
||||||
} ) => {
|
} ) => {
|
||||||
let map = Beatmap::from_path(common::$path)
|
let map = Beatmap::from_path(common::$path).unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted::<$mode>();
|
|
||||||
|
|
||||||
$(
|
$(
|
||||||
let mods = 0 $( + $mods )*;
|
let mods = 0 $( + $mods )*;
|
||||||
let expected = test_cases!(@$mode { $( $key: $value, )* });
|
let expected = test_cases!(@$mode { $( $key: $value, )* });
|
||||||
let actual = Difficulty::new().mods(mods).with_mode().calculate(&map);
|
|
||||||
|
let actual = Difficulty::new()
|
||||||
|
.mods(mods)
|
||||||
|
.calculate_for_mode::<$mode>(&map)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
run(&actual, &expected, mods);
|
run(&actual, &expected, mods);
|
||||||
)*
|
)*
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,14 +18,12 @@ macro_rules! test_cases {
|
|||||||
$( $key:ident: $value:expr $( , )? )*
|
$( $key:ident: $value:expr $( , )? )*
|
||||||
} ;)*
|
} ;)*
|
||||||
} ) => {
|
} ) => {
|
||||||
let map = Beatmap::from_path(common::$path)
|
let map = Beatmap::from_path(common::$path).unwrap();
|
||||||
.unwrap()
|
|
||||||
.unchecked_into_converted();
|
|
||||||
|
|
||||||
$(
|
$(
|
||||||
let mods = 0 $( + $mods )*;
|
let mods = 0 $( + $mods )*;
|
||||||
let (calc, expected) = test_cases!(@$mode { map, $( $key: $value, )* });
|
let (calc, expected) = test_cases!(@$mode { map, $( $key: $value, )* });
|
||||||
let actual = calc.mods(mods).calculate();
|
let actual = calc.mods(mods).calculate().unwrap();
|
||||||
run(&actual, &expected, mods);
|
run(&actual, &expected, mods);
|
||||||
)*
|
)*
|
||||||
};
|
};
|
||||||
@@ -39,7 +37,7 @@ macro_rules! test_cases {
|
|||||||
effective_miss_count: $effective_miss_count:expr,
|
effective_miss_count: $effective_miss_count:expr,
|
||||||
}) => {
|
}) => {
|
||||||
(
|
(
|
||||||
OsuPerformance::from($map.as_owned()).lazer(true),
|
OsuPerformance::from(&$map).lazer(true),
|
||||||
OsuPerformanceAttributes {
|
OsuPerformanceAttributes {
|
||||||
pp: $pp,
|
pp: $pp,
|
||||||
pp_acc: $pp_acc,
|
pp_acc: $pp_acc,
|
||||||
@@ -60,7 +58,7 @@ macro_rules! test_cases {
|
|||||||
estimated_unstable_rate: $estimated_unstable_rate:expr,
|
estimated_unstable_rate: $estimated_unstable_rate:expr,
|
||||||
}) => {
|
}) => {
|
||||||
(
|
(
|
||||||
TaikoPerformance::from($map.as_owned()),
|
TaikoPerformance::from(&$map),
|
||||||
TaikoPerformanceAttributes {
|
TaikoPerformanceAttributes {
|
||||||
pp: $pp,
|
pp: $pp,
|
||||||
pp_acc: $pp_acc,
|
pp_acc: $pp_acc,
|
||||||
@@ -76,7 +74,7 @@ macro_rules! test_cases {
|
|||||||
pp: $pp:expr,
|
pp: $pp:expr,
|
||||||
}) => {
|
}) => {
|
||||||
(
|
(
|
||||||
CatchPerformance::from($map.as_owned()),
|
CatchPerformance::from(&$map),
|
||||||
CatchPerformanceAttributes {
|
CatchPerformanceAttributes {
|
||||||
pp: $pp,
|
pp: $pp,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -89,7 +87,7 @@ macro_rules! test_cases {
|
|||||||
pp_difficulty: $pp_difficulty:expr,
|
pp_difficulty: $pp_difficulty:expr,
|
||||||
}) => {
|
}) => {
|
||||||
(
|
(
|
||||||
ManiaPerformance::from($map.as_owned()),
|
ManiaPerformance::from(&$map),
|
||||||
ManiaPerformanceAttributes {
|
ManiaPerformanceAttributes {
|
||||||
pp: $pp,
|
pp: $pp,
|
||||||
pp_difficulty: $pp_difficulty,
|
pp_difficulty: $pp_difficulty,
|
||||||
|
|||||||
Reference in New Issue
Block a user