feat!: overhaul convert api

This commit is contained in:
MaxOhn
2024-11-11 02:51:43 +01:00
parent 9f45d9594d
commit b3d0475d47
49 changed files with 837 additions and 1298 deletions
-114
View File
@@ -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
}
}
+21 -35
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+21 -35
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+21 -20
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+8 -7
View File
@@ -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
+15 -14
View File
@@ -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);
}
+35 -37
View File
@@ -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());
}
}
+9 -6
View File
@@ -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
View File
@@ -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.
+5 -33
View File
@@ -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);
+27 -32
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+17 -23
View File
@@ -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);
}
+40 -45
View File
@@ -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());
}
}
+9 -6
View File
@@ -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(),
}
})
}
+1 -7
View File
@@ -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 {
-211
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+20 -23
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+16 -22
View File
@@ -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
View File
@@ -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());
}
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+26 -35
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+18 -21
View File
@@ -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);
}
+36 -39
View File
@@ -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());
}
}
+7 -6
View File
@@ -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(),
}
})
}
-30
View File
@@ -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)
}
}
+15 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
)*
};
+6 -8
View File
@@ -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,