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