feat!: overhaul convert api
This commit is contained in:
@@ -2,7 +2,7 @@ use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{any::difficulty::ModsDependent, model::mods::GameMods, Difficulty};
|
||||
|
||||
use super::{converted::Converted, Beatmap};
|
||||
use super::Beatmap;
|
||||
|
||||
/// Summary struct for a [`Beatmap`]'s attributes.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -371,12 +371,6 @@ impl From<&Beatmap> for BeatmapAttributesBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> From<&Converted<'_, M>> for BeatmapAttributesBuilder {
|
||||
fn from(converted: &Converted<'_, M>) -> Self {
|
||||
Self::new().map(converted)
|
||||
}
|
||||
}
|
||||
|
||||
// False positive? Value looks consumed to me...
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn difficulty_range(difficulty: f64, windows: GameModeHitWindows) -> f64 {
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{Debug, Formatter, Result as FmtResult},
|
||||
marker::PhantomData,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
model::mode::{ConvertStatus, IGameMode},
|
||||
util::generic_fmt::GenericFormatter,
|
||||
Difficulty,
|
||||
};
|
||||
|
||||
use super::Beatmap;
|
||||
|
||||
const INCOMPATIBLE_MODES: &str = "the gamemodes were incompatible";
|
||||
|
||||
/// A [`Beatmap`] that is attached to a mode.
|
||||
///
|
||||
/// # Incompatibility
|
||||
///
|
||||
/// The following conversions are compatible:
|
||||
/// - `Osu` → `Osu`
|
||||
/// - `Taiko` → `Taiko`
|
||||
/// - `Catch` → `Catch`
|
||||
/// - `Mania` → `Mania`
|
||||
/// - `Osu` → `Taiko`
|
||||
/// - `Osu` → `Catch`
|
||||
/// - `Osu` → `Mania`
|
||||
///
|
||||
/// All other conversions are incompatible.
|
||||
pub struct Converted<'a, M> {
|
||||
map: Cow<'a, Beatmap>,
|
||||
mode: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<'a, M> Converted<'a, M> {
|
||||
/// Initialize a [`Converted`] beatmap by promising the given map's mode
|
||||
/// matches the generic type `M`.
|
||||
pub(crate) const fn new(map: Cow<'a, Beatmap>) -> Self {
|
||||
Self {
|
||||
map,
|
||||
mode: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the internal [`Beatmap`].
|
||||
pub fn into_inner(self) -> Cow<'a, Beatmap> {
|
||||
self.map
|
||||
}
|
||||
|
||||
/// Borrow the contained [`Beatmap`] to cheaply create a new owned
|
||||
/// [`Converted`].
|
||||
///
|
||||
/// This is the same as `.clone()` except cheap - but its lifetime might be
|
||||
/// shorter.
|
||||
#[must_use]
|
||||
pub fn as_owned(&'a self) -> Self {
|
||||
Self::new(Cow::Borrowed(self.map.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: IGameMode> Converted<'_, M> {
|
||||
/// Attempt to convert a [`Beatmap`] to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible the [`Beatmap`] will be returned
|
||||
/// unchanged as `Err`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn try_from_owned(mut map: Beatmap) -> Result<Self, Beatmap> {
|
||||
match M::try_convert(&mut map) {
|
||||
ConvertStatus::Noop => Ok(Self::new(Cow::Owned(map))),
|
||||
ConvertStatus::Conversion => Ok(Self::new(Cow::Owned(map))),
|
||||
ConvertStatus::Incompatible => Err(map),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`Beatmap`] to the specified mode.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the conversion is incompatible.
|
||||
pub fn unchecked_from_owned(map: Beatmap) -> Self {
|
||||
Self::try_from_owned(map).unwrap_or_else(|_| panic!("{}", INCOMPATIBLE_MODES))
|
||||
}
|
||||
|
||||
/// Create a gradual difficulty calculator for the map.
|
||||
pub fn gradual_difficulty(&self, difficulty: Difficulty) -> M::GradualDifficulty {
|
||||
M::gradual_difficulty(difficulty, self)
|
||||
}
|
||||
|
||||
/// Create a gradual performance calculator for the map.
|
||||
pub fn gradual_performance(&self, difficulty: Difficulty) -> M::GradualPerformance {
|
||||
M::gradual_performance(difficulty, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, M: IGameMode> Converted<'a, M> {
|
||||
/// Create a performance calculator for the map.
|
||||
pub fn performance(self) -> M::Performance<'a> {
|
||||
M::performance(self)
|
||||
}
|
||||
|
||||
/// Attempt to convert a [`&Beatmap`] to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible, `None` is returned.
|
||||
///
|
||||
/// [`&Beatmap`]: Beatmap
|
||||
pub fn try_from_ref(map: &'a Beatmap) -> Option<Self> {
|
||||
let mut map = match M::check_convert(map) {
|
||||
ConvertStatus::Noop => return Some(Self::new(Cow::Borrowed(map))),
|
||||
ConvertStatus::Conversion => map.to_owned(),
|
||||
ConvertStatus::Incompatible => return None,
|
||||
};
|
||||
|
||||
match M::try_convert(&mut map) {
|
||||
ConvertStatus::Conversion => Some(Self::new(Cow::Owned(map))),
|
||||
ConvertStatus::Noop => Some(Self::new(Cow::Owned(map))),
|
||||
ConvertStatus::Incompatible => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`&Beatmap`] to the specified mode.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the conversion is incompatible.
|
||||
///
|
||||
/// [`&Beatmap`]: Beatmap
|
||||
pub fn unchecked_from_ref(map: &'a Beatmap) -> Self {
|
||||
Self::try_from_ref(map).expect(INCOMPATIBLE_MODES)
|
||||
}
|
||||
|
||||
/// Attempt to convert a [`&mut Beatmap`] to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible, `None` is returned.
|
||||
///
|
||||
/// [`&mut Beatmap`]: Beatmap
|
||||
pub fn try_from_mut(map: &'a mut Beatmap) -> Option<Self> {
|
||||
match M::try_convert(map) {
|
||||
ConvertStatus::Conversion => Some(Self::new(Cow::Borrowed(map))),
|
||||
ConvertStatus::Noop => Some(Self::new(Cow::Borrowed(map))),
|
||||
ConvertStatus::Incompatible => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`&mut Beatmap`] to the specified mode.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the conversion is incompatible.
|
||||
///
|
||||
/// [`&mut Beatmap`]: Beatmap
|
||||
pub fn unchecked_from_mut(map: &'a mut Beatmap) -> Self {
|
||||
Self::try_from_mut(map).expect(INCOMPATIBLE_MODES)
|
||||
}
|
||||
|
||||
/// Attempt to convert a [`Converted`] from mode `M` to mode `N`.
|
||||
///
|
||||
/// If the conversion is incompatible the [`Converted`] will be returned
|
||||
/// unchanged as `Err`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn try_convert<N: IGameMode>(self) -> Result<Converted<'a, N>, Self> {
|
||||
match self.map {
|
||||
Cow::Borrowed(map) => Converted::<N>::try_from_ref(map).ok_or(self),
|
||||
Cow::Owned(map) => {
|
||||
Converted::<N>::try_from_owned(map).map_err(|map| Self::new(Cow::Owned(map)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`Converted`] from mode `M` to mode `N`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the conversion is incompatible.
|
||||
pub fn unchecked_convert<N: IGameMode>(self) -> Converted<'a, N> {
|
||||
match self.map {
|
||||
Cow::Borrowed(map) => Converted::<N>::unchecked_from_ref(map),
|
||||
Cow::Owned(map) => Converted::<N>::unchecked_from_owned(map),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Clone for Converted<'_, M> {
|
||||
fn clone(&self) -> Self {
|
||||
Self::new(self.map.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Debug for Converted<'_, M> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.debug_struct("Converted")
|
||||
.field("map", &self.map)
|
||||
.field("mode", &GenericFormatter::<M>::new())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> PartialEq for Converted<'_, M> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.map == other.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Deref for Converted<'_, M> {
|
||||
type Target = Beatmap;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.map.as_ref()
|
||||
}
|
||||
}
|
||||
+51
-61
@@ -1,4 +1,4 @@
|
||||
use std::{io, path::Path, str::FromStr};
|
||||
use std::{borrow::Cow, io, path::Path, str::FromStr};
|
||||
|
||||
use rosu_map::{
|
||||
section::{general::GameMode, hit_objects::hit_samples::HitSoundType},
|
||||
@@ -8,13 +8,12 @@ use rosu_map::{
|
||||
pub use rosu_map::section::events::BreakPeriod;
|
||||
|
||||
use crate::{
|
||||
catch::Catch, mania::Mania, osu::Osu, taiko::Taiko, Difficulty, GradualDifficulty,
|
||||
catch::Catch, mania::Mania, taiko::Taiko, Difficulty, GameMods, GradualDifficulty,
|
||||
GradualPerformance, Performance,
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, HitWindows},
|
||||
converted::Converted,
|
||||
decode::{BeatmapState, ParseBeatmapError},
|
||||
};
|
||||
|
||||
@@ -24,12 +23,11 @@ use super::{
|
||||
TimingPoint,
|
||||
},
|
||||
hit_object::HitObject,
|
||||
mode::{ConvertStatus, IGameMode},
|
||||
mode::ConvertError,
|
||||
};
|
||||
|
||||
mod attributes;
|
||||
mod bpm;
|
||||
mod converted;
|
||||
mod decode;
|
||||
|
||||
/// All beatmap data that is relevant for difficulty and performance
|
||||
@@ -122,75 +120,67 @@ impl Beatmap {
|
||||
self.breaks.iter().map(BreakPeriod::duration).sum()
|
||||
}
|
||||
|
||||
/// Convert a [`&mut Beatmap`] to the specified mode with an argument
|
||||
/// instead of a generic parameter.
|
||||
///
|
||||
/// [`&mut Beatmap`]: Beatmap
|
||||
pub fn convert_in_place(&mut self, mode: GameMode) -> ConvertStatus {
|
||||
match mode {
|
||||
GameMode::Osu => Osu::try_convert(self),
|
||||
GameMode::Taiko => Taiko::try_convert(self),
|
||||
GameMode::Catch => Catch::try_convert(self),
|
||||
GameMode::Mania => Mania::try_convert(self),
|
||||
}
|
||||
/// Attempt to convert a [`Beatmap`] to the specified mode.
|
||||
pub fn convert(mut self, mode: GameMode, mods: &GameMods) -> Result<Self, ConvertError> {
|
||||
self.convert_mut(mode, mods)?;
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Attempt to convert a [`&Beatmap`] to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible, `None` is returned.
|
||||
///
|
||||
/// [`&Beatmap`]: Beatmap
|
||||
pub fn try_as_converted<M: IGameMode>(&self) -> Option<Converted<'_, M>> {
|
||||
Converted::try_from_ref(self)
|
||||
}
|
||||
pub fn convert_ref(
|
||||
&self,
|
||||
mode: GameMode,
|
||||
mods: &GameMods,
|
||||
) -> Result<Cow<'_, Self>, ConvertError> {
|
||||
if self.mode == mode {
|
||||
return Ok(Cow::Borrowed(self));
|
||||
} else if self.is_convert {
|
||||
return Err(ConvertError::AlreadyConverted);
|
||||
} else if self.mode != GameMode::Osu {
|
||||
return Err(ConvertError::Convert {
|
||||
from: self.mode,
|
||||
to: mode,
|
||||
});
|
||||
}
|
||||
|
||||
/// Convert a [`&Beatmap`] to the specified mode.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the conversion is incompatible.
|
||||
///
|
||||
/// [`&Beatmap`]: Beatmap
|
||||
pub fn unchecked_as_converted<M: IGameMode>(&self) -> Converted<'_, M> {
|
||||
Converted::unchecked_from_ref(self)
|
||||
let mut map = self.to_owned();
|
||||
|
||||
match mode {
|
||||
GameMode::Taiko => Taiko::convert(&mut map),
|
||||
GameMode::Catch => Catch::convert(&mut map),
|
||||
GameMode::Mania => Mania::convert(&mut map, mods),
|
||||
GameMode::Osu => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(Cow::Owned(map))
|
||||
}
|
||||
|
||||
/// Attempt to convert a [`&mut Beatmap`] to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible, `None` is returned.
|
||||
///
|
||||
/// [`&mut Beatmap`]: Beatmap
|
||||
pub fn try_as_converted_mut<M: IGameMode>(&mut self) -> Option<Converted<'_, M>> {
|
||||
Converted::try_from_mut(self)
|
||||
}
|
||||
pub fn convert_mut(&mut self, mode: GameMode, mods: &GameMods) -> Result<(), ConvertError> {
|
||||
if self.mode == mode {
|
||||
return Ok(());
|
||||
} else if self.is_convert {
|
||||
return Err(ConvertError::AlreadyConverted);
|
||||
} else if self.mode != GameMode::Osu {
|
||||
return Err(ConvertError::Convert {
|
||||
from: self.mode,
|
||||
to: mode,
|
||||
});
|
||||
}
|
||||
|
||||
/// Convert a [`&mut Beatmap`] to the specified mode.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the conversion is incompatible.
|
||||
///
|
||||
/// [`&mut Beatmap`]: Beatmap
|
||||
pub fn unchecked_as_converted_mut<M: IGameMode>(&mut self) -> Converted<'_, M> {
|
||||
Converted::unchecked_from_mut(self)
|
||||
}
|
||||
match mode {
|
||||
GameMode::Taiko => Taiko::convert(self),
|
||||
GameMode::Catch => Catch::convert(self),
|
||||
GameMode::Mania => Mania::convert(self, mods),
|
||||
GameMode::Osu => unreachable!(),
|
||||
}
|
||||
|
||||
/// Attempt to convert a [`Beatmap`] to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible the [`Beatmap`] will be returned
|
||||
/// unchanged as `Err`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn try_into_converted<'a, M: IGameMode>(self) -> Result<Converted<'a, M>, Self> {
|
||||
Converted::try_from_owned(self)
|
||||
}
|
||||
|
||||
/// Convert a [`Beatmap`] to the specified mode.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the conversion is incompatible.
|
||||
pub fn unchecked_into_converted<'a, M: IGameMode>(self) -> Converted<'a, M> {
|
||||
Converted::unchecked_from_owned(self)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+47
-38
@@ -1,8 +1,13 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
};
|
||||
|
||||
pub use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::Difficulty;
|
||||
|
||||
use super::beatmap::{Beatmap, Converted};
|
||||
use super::beatmap::Beatmap;
|
||||
|
||||
/// A way to specify a gamemode at compile-time.
|
||||
///
|
||||
@@ -29,54 +34,58 @@ pub trait IGameMode: Sized {
|
||||
/// The type of a gradual performance calculator.
|
||||
type GradualPerformance;
|
||||
|
||||
/// Check whether the map's mode can be converted to the current type.
|
||||
fn check_convert(map: &Beatmap) -> ConvertStatus;
|
||||
/// Perform a difficulty calculation for a [`Beatmap`] and process the
|
||||
/// final skill values.
|
||||
fn difficulty(
|
||||
difficulty: &Difficulty,
|
||||
map: &Beatmap,
|
||||
) -> Result<Self::DifficultyAttributes, ConvertError>;
|
||||
|
||||
/// Attempt to convert a beatmap.
|
||||
///
|
||||
/// In case [`ConvertStatus::Incompatible`] is returned, the map is not
|
||||
/// modified.
|
||||
fn try_convert(map: &mut Beatmap) -> ConvertStatus;
|
||||
/// Perform a difficulty calculation for a [`Beatmap`] without processing
|
||||
/// the final skill values.
|
||||
fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<Self::Strains, ConvertError>;
|
||||
|
||||
/// Perform a difficulty calculation for a [`Converted`] beatmap and
|
||||
/// process the final skill values.
|
||||
fn difficulty(difficulty: &Difficulty, map: &Converted<'_, Self>)
|
||||
-> Self::DifficultyAttributes;
|
||||
/// Create a performance calculator for a [`Beatmap`].
|
||||
fn performance(map: &Beatmap) -> Self::Performance<'_>;
|
||||
|
||||
/// Perform a difficulty calculation for a [`Converted`] beatmap without
|
||||
/// processing the final skill values.
|
||||
fn strains(difficulty: &Difficulty, map: &Converted<'_, Self>) -> Self::Strains;
|
||||
|
||||
/// Create a performance calculator for a [`Converted`] beatmap.
|
||||
fn performance(map: Converted<'_, Self>) -> Self::Performance<'_>;
|
||||
|
||||
/// Create a gradual difficulty calculator for a [`Converted`] beatmap.
|
||||
/// Create a gradual difficulty calculator for a [`Beatmap`].
|
||||
fn gradual_difficulty(
|
||||
difficulty: Difficulty,
|
||||
map: &Converted<'_, Self>,
|
||||
) -> Self::GradualDifficulty;
|
||||
map: &Beatmap,
|
||||
) -> Result<Self::GradualDifficulty, ConvertError>;
|
||||
|
||||
/// Create a gradual performance calculator for a [`Converted`] beatmap.
|
||||
/// Create a gradual performance calculator for a [`Beatmap`].
|
||||
fn gradual_performance(
|
||||
difficulty: Difficulty,
|
||||
map: &Converted<'_, Self>,
|
||||
) -> Self::GradualPerformance;
|
||||
map: &Beatmap,
|
||||
) -> Result<Self::GradualPerformance, ConvertError>;
|
||||
}
|
||||
|
||||
/// The status of a conversion through [`IGameMode::try_convert`].
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ConvertStatus {
|
||||
/// Conversion not necessary.
|
||||
Noop,
|
||||
/// Conversion possible.
|
||||
Conversion,
|
||||
/// Conversion not possible.
|
||||
Incompatible,
|
||||
/// Error type when failing to convert a [`Beatmap`] from one [`GameMode`] to
|
||||
/// another.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ConvertError {
|
||||
/// Cannot convert an already converted map
|
||||
AlreadyConverted,
|
||||
/// Cannot convert from [`GameMode`] `from` to `to`
|
||||
Convert { from: GameMode, to: GameMode },
|
||||
}
|
||||
|
||||
impl ConvertStatus {
|
||||
/// Whether this [`ConvertStatus`] represents a success.
|
||||
pub const fn success(self) -> bool {
|
||||
matches!(self, Self::Noop | Self::Conversion)
|
||||
impl Error for ConvertError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ConvertError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
ConvertError::AlreadyConverted => {
|
||||
f.write_str("Cannot convert an already converted map")
|
||||
}
|
||||
ConvertError::Convert { from, to } => {
|
||||
write!(f, "Cannot convert from {from:?} to {to:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user