initial rewrite
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
use crate::{
|
||||
catch::{Catch, CatchDifficultyAttributes, CatchPerformanceAttributes},
|
||||
mania::{Mania, ManiaDifficultyAttributes, ManiaPerformanceAttributes},
|
||||
model::mode::IGameMode,
|
||||
osu::{Osu, OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||
taiko::{Taiko, TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
||||
};
|
||||
|
||||
use super::performance::Performance;
|
||||
|
||||
/// The result of a difficulty calculation based on the mode.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum DifficultyAttributes {
|
||||
/// osu!standard difficulty calculation result.
|
||||
Osu(OsuDifficultyAttributes),
|
||||
/// osu!taiko difficulty calculation result.
|
||||
Taiko(TaikoDifficultyAttributes),
|
||||
/// osu!catch difficulty calculation result.
|
||||
Catch(CatchDifficultyAttributes),
|
||||
/// osu!mania difficulty calculation result.
|
||||
Mania(ManiaDifficultyAttributes),
|
||||
}
|
||||
|
||||
impl DifficultyAttributes {
|
||||
/// The star value.
|
||||
pub const fn stars(&self) -> f64 {
|
||||
match self {
|
||||
Self::Osu(attrs) => attrs.stars,
|
||||
Self::Taiko(attrs) => attrs.stars,
|
||||
Self::Catch(attrs) => attrs.stars,
|
||||
Self::Mania(attrs) => attrs.stars,
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum combo of the map.
|
||||
pub const fn max_combo(&self) -> u32 {
|
||||
match self {
|
||||
Self::Osu(attrs) => attrs.max_combo,
|
||||
Self::Taiko(attrs) => attrs.max_combo,
|
||||
Self::Catch(attrs) => attrs.max_combo(),
|
||||
Self::Mania(attrs) => attrs.max_combo,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for performance calculation.
|
||||
pub fn pp<'a>(self) -> Performance<'a> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a performance calculation based on the mode.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PerformanceAttributes {
|
||||
/// osu!standard performance calculation result.
|
||||
Osu(OsuPerformanceAttributes),
|
||||
/// osu!taiko performance calculation result.
|
||||
Taiko(TaikoPerformanceAttributes),
|
||||
/// osu!catch performance calculation result.
|
||||
Catch(CatchPerformanceAttributes),
|
||||
/// osu!mania performance calculation result.
|
||||
Mania(ManiaPerformanceAttributes),
|
||||
}
|
||||
|
||||
impl PerformanceAttributes {
|
||||
/// The pp value.
|
||||
pub const fn pp(&self) -> f64 {
|
||||
match self {
|
||||
Self::Osu(attrs) => attrs.pp,
|
||||
Self::Taiko(attrs) => attrs.pp,
|
||||
Self::Catch(attrs) => attrs.pp,
|
||||
Self::Mania(attrs) => attrs.pp,
|
||||
}
|
||||
}
|
||||
|
||||
/// The star value.
|
||||
pub const fn stars(&self) -> f64 {
|
||||
match self {
|
||||
Self::Osu(attrs) => attrs.stars(),
|
||||
Self::Taiko(attrs) => attrs.stars(),
|
||||
Self::Catch(attrs) => attrs.stars(),
|
||||
Self::Mania(attrs) => attrs.stars(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Difficulty attributes that were used for the performance calculation.
|
||||
pub fn difficulty_attributes(&self) -> DifficultyAttributes {
|
||||
match self {
|
||||
Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty.clone()),
|
||||
Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty.clone()),
|
||||
Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty.clone()),
|
||||
Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum combo of the map.
|
||||
pub const fn max_combo(&self) -> u32 {
|
||||
match self {
|
||||
Self::Osu(attrs) => attrs.difficulty.max_combo,
|
||||
Self::Taiko(attrs) => attrs.difficulty.max_combo,
|
||||
Self::Catch(attrs) => attrs.difficulty.max_combo(),
|
||||
Self::Mania(attrs) => attrs.difficulty.max_combo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||
pub trait AttributeProvider {
|
||||
/// Provide the actual difficulty attributes.
|
||||
fn attributes(self) -> DifficultyAttributes;
|
||||
}
|
||||
|
||||
impl AttributeProvider for DifficultyAttributes {
|
||||
fn attributes(self) -> DifficultyAttributes {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributeProvider for PerformanceAttributes {
|
||||
fn attributes(self) -> DifficultyAttributes {
|
||||
match self {
|
||||
Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty),
|
||||
Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty),
|
||||
Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty),
|
||||
Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||
pub trait ModeAttributeProvider<M: IGameMode> {
|
||||
/// Provide the actual difficulty attributes.
|
||||
fn attributes(self) -> Option<M::DifficultyAttributes>;
|
||||
}
|
||||
|
||||
impl<M: IGameMode> ModeAttributeProvider<M> for M::DifficultyAttributes {
|
||||
fn attributes(self) -> Option<M::DifficultyAttributes> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_attr_provider {
|
||||
( $mode:ident: $difficulty:ident, $performance:ident ) => {
|
||||
impl AttributeProvider for $difficulty {
|
||||
fn attributes(self) -> DifficultyAttributes {
|
||||
DifficultyAttributes::$mode(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributeProvider for $performance {
|
||||
fn attributes(self) -> DifficultyAttributes {
|
||||
DifficultyAttributes::$mode(self.difficulty)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModeAttributeProvider<$mode> for $performance {
|
||||
fn attributes(self) -> Option<<$mode as IGameMode>::DifficultyAttributes> {
|
||||
Some(self.difficulty)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModeAttributeProvider<$mode> for DifficultyAttributes {
|
||||
fn attributes(self) -> Option<<$mode as IGameMode>::DifficultyAttributes> {
|
||||
if let Self::$mode(attrs) = self {
|
||||
Some(attrs)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModeAttributeProvider<$mode> for PerformanceAttributes {
|
||||
fn attributes(self) -> Option<<$mode as IGameMode>::DifficultyAttributes> {
|
||||
if let Self::$mode(attrs) = self {
|
||||
Some(attrs.difficulty)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_attr_provider!(Catch: CatchDifficultyAttributes, CatchPerformanceAttributes);
|
||||
impl_attr_provider!(Mania: ManiaDifficultyAttributes, ManiaPerformanceAttributes);
|
||||
impl_attr_provider!(Osu: OsuDifficultyAttributes, OsuPerformanceAttributes);
|
||||
impl_attr_provider!(Taiko: TaikoDifficultyAttributes, TaikoPerformanceAttributes);
|
||||
@@ -0,0 +1,210 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{
|
||||
catch::{Catch, CatchBeatmap},
|
||||
mania::{Mania, ManiaBeatmap},
|
||||
model::beatmap::{Beatmap, Converted},
|
||||
osu::{Osu, OsuBeatmap},
|
||||
taiko::{Taiko, TaikoBeatmap},
|
||||
};
|
||||
|
||||
pub use self::mode::ModeDifficulty;
|
||||
|
||||
use super::attributes::DifficultyAttributes;
|
||||
|
||||
mod mode;
|
||||
pub mod object;
|
||||
pub mod skills;
|
||||
|
||||
/// Difficulty calculator on maps of any mode.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Difficulty<'map> {
|
||||
map: Cow<'map, Beatmap>,
|
||||
is_convert: bool,
|
||||
inner: ModeDifficulty,
|
||||
}
|
||||
|
||||
impl<'map> Difficulty<'map> {
|
||||
/// Create a new difficulty calculator for the given beatmap.
|
||||
pub fn new(map: &'map Beatmap) -> Self {
|
||||
Self::new_with_is_convert(Cow::Borrowed(map), false)
|
||||
}
|
||||
|
||||
fn new_with_is_convert(map: Cow<'map, Beatmap>, is_convert: bool) -> Self {
|
||||
Self {
|
||||
map,
|
||||
is_convert,
|
||||
inner: ModeDifficulty::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from_mode {
|
||||
( $mode:ident ) => {
|
||||
impl<'a> From<Converted<'a, $mode>> for Difficulty<'a> {
|
||||
fn from(converted: Converted<'a, $mode>) -> Self {
|
||||
Self::new_with_is_convert(converted.map, converted.is_convert)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b: 'a> From<&'b Converted<'a, $mode>> for Difficulty<'a> {
|
||||
fn from(converted: &'b Converted<'a, $mode>) -> Self {
|
||||
Self::new_with_is_convert(
|
||||
Cow::Borrowed(converted.map.as_ref()),
|
||||
converted.is_convert,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from_mode!(Osu);
|
||||
impl_from_mode!(Taiko);
|
||||
impl_from_mode!(Catch);
|
||||
impl_from_mode!(Mania);
|
||||
|
||||
impl Difficulty<'_> {
|
||||
/// Attempt to convert the map to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible, `None` is returned.
|
||||
///
|
||||
/// If the given mode should be ignored in case it is incompatible, use
|
||||
/// [`mode_or_ignore`] instead.
|
||||
///
|
||||
/// [`mode_or_ignore`]: Self::mode_or_ignore
|
||||
pub fn try_mode(&mut self, mode: GameMode) -> Option<&mut Self> {
|
||||
let (map, is_convert) = match mode {
|
||||
GameMode::Osu => {
|
||||
let converted = OsuBeatmap::try_from_ref(self.map.as_ref())?;
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Taiko => {
|
||||
let converted = TaikoBeatmap::try_from_ref(self.map.as_ref())?;
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Catch => {
|
||||
let converted = CatchBeatmap::try_from_ref(self.map.as_ref())?;
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Mania => {
|
||||
let converted = ManiaBeatmap::try_from_ref(self.map.as_ref())?;
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(map, Cow::Owned(_)) {
|
||||
self.is_convert |= is_convert;
|
||||
let map = map.into_owned();
|
||||
self.map = Cow::Owned(map);
|
||||
}
|
||||
|
||||
Some(self)
|
||||
}
|
||||
|
||||
/// Attempt to convert the map to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible, the map won't be modified.
|
||||
///
|
||||
/// To see whether the given mode is incompatible, use [`try_mode`]
|
||||
/// instead.
|
||||
///
|
||||
/// [`try_mode`]: Self::try_mode
|
||||
pub fn mode_or_ignore(&mut self, mode: GameMode) -> &mut Self {
|
||||
let (map, is_convert) = match mode {
|
||||
GameMode::Osu => {
|
||||
let Some(converted) = OsuBeatmap::try_from_ref(self.map.as_ref()) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Taiko => {
|
||||
let Some(converted) = TaikoBeatmap::try_from_ref(self.map.as_ref()) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Catch => {
|
||||
let Some(converted) = CatchBeatmap::try_from_ref(self.map.as_ref()) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Mania => {
|
||||
let Some(converted) = ManiaBeatmap::try_from_ref(self.map.as_ref()) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(map, Cow::Owned(_)) {
|
||||
self.is_convert |= is_convert;
|
||||
let map = map.into_owned();
|
||||
self.map = Cow::Owned(map);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
pub fn mods(&mut self, mods: u32) -> &mut Self {
|
||||
self.inner.mods(mods);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
pub fn passed_objects(&mut self, passed_objects: u32) -> &mut Self {
|
||||
self.inner.passed_objects(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adjust the clock rate used in the calculation.
|
||||
///
|
||||
/// If none is specified, it will take the clock rate based on the mods
|
||||
/// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
|
||||
pub fn clock_rate(&mut self, clock_rate: f64) -> &mut Self {
|
||||
self.inner.clock_rate(clock_rate);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform the difficulty calculation.
|
||||
///
|
||||
/// The returned attributes depend on the map's mode.
|
||||
pub fn calculate(&self) -> DifficultyAttributes {
|
||||
let map = Cow::Borrowed(self.map.as_ref());
|
||||
|
||||
match self.map.mode {
|
||||
GameMode::Osu => DifficultyAttributes::Osu(
|
||||
self.inner
|
||||
.calculate(&Converted::<Osu>::new(map, self.is_convert)),
|
||||
),
|
||||
GameMode::Taiko => DifficultyAttributes::Taiko(
|
||||
self.inner
|
||||
.calculate(&Converted::<Taiko>::new(map, self.is_convert)),
|
||||
),
|
||||
GameMode::Catch => DifficultyAttributes::Catch(
|
||||
self.inner
|
||||
.calculate(&Converted::<Catch>::new(map, self.is_convert)),
|
||||
),
|
||||
GameMode::Mania => DifficultyAttributes::Mania(
|
||||
self.inner
|
||||
.calculate(&Converted::<Mania>::new(map, self.is_convert)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use crate::{
|
||||
model::{beatmap::Converted, mode::IGameMode},
|
||||
util::mods::Mods,
|
||||
};
|
||||
|
||||
/// Difficulty calculator on maps of a given mode.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ModeDifficulty {
|
||||
mods: u32,
|
||||
passed_objects: Option<u32>,
|
||||
clock_rate: Option<f64>,
|
||||
}
|
||||
|
||||
impl ModeDifficulty {
|
||||
/// Create a new difficulty calculator.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
pub fn mods(&mut self, mods: u32) -> &mut Self {
|
||||
self.mods = mods;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
pub fn passed_objects(&mut self, passed_objects: u32) -> &mut Self {
|
||||
self.passed_objects = Some(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adjust the clock rate used in the calculation.
|
||||
///
|
||||
/// If none is specified, it will take the clock rate based on the mods
|
||||
/// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
|
||||
pub fn clock_rate(&mut self, clock_rate: f64) -> &mut Self {
|
||||
self.clock_rate = Some(clock_rate);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform the difficulty calculation for a [`Converted`] beatmap and
|
||||
/// process the final skill values.
|
||||
pub fn calculate<M: IGameMode>(&self, map: &Converted<'_, M>) -> M::DifficultyAttributes {
|
||||
M::difficulty(self, map)
|
||||
}
|
||||
|
||||
/// Perform a difficulty calculation for a [`Converted`] beatmap without
|
||||
/// processing the final skill values.
|
||||
pub fn strains<M: IGameMode>(&self, map: &Converted<'_, M>) -> M::Strains {
|
||||
M::strains(self, map)
|
||||
}
|
||||
|
||||
pub(crate) const fn get_mods(&self) -> u32 {
|
||||
self.mods
|
||||
}
|
||||
|
||||
pub(crate) fn get_clock_rate(&self) -> f64 {
|
||||
self.clock_rate.unwrap_or_else(|| self.mods.clock_rate())
|
||||
}
|
||||
|
||||
pub(crate) fn get_passed_objects(&self) -> usize {
|
||||
self.passed_objects.map_or(usize::MAX, |n| n as usize)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub trait IDifficultyObject: Sized {
|
||||
fn idx(&self) -> usize;
|
||||
|
||||
fn previous<'a, D>(&self, backwards_idx: usize, diff_objects: &'a [D]) -> Option<&'a D> {
|
||||
self.idx()
|
||||
.checked_sub(backwards_idx + 1)
|
||||
.and_then(|idx| diff_objects.get(idx))
|
||||
}
|
||||
|
||||
fn next<'a, D>(&self, forwards_idx: usize, diff_objects: &'a [D]) -> Option<&'a D> {
|
||||
diff_objects.get(self.idx() + (forwards_idx + 1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
pub fn strain_decay(ms: f64, strain_decay_base: f64) -> f64 {
|
||||
strain_decay_base.powf(ms / 1000.0)
|
||||
}
|
||||
|
||||
/// Wrapper around a difficulty skill that carries a list of all difficulty
|
||||
/// objects.
|
||||
pub struct Skill<'a, S: ISkill> {
|
||||
pub inner: S,
|
||||
pub diff_objects: &'a S::DifficultyObjects<'a>,
|
||||
}
|
||||
|
||||
impl<'a, S: ISkill> Skill<'a, S> {
|
||||
pub const fn new(skill: S, diff_objects: &'a S::DifficultyObjects<'a>) -> Self {
|
||||
Self {
|
||||
inner: skill,
|
||||
diff_objects,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait required for [`Skill`].
|
||||
pub trait ISkill {
|
||||
type DifficultyObjects<'a>: ?Sized;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StrainSkill {
|
||||
pub curr_section_peak: f64,
|
||||
pub curr_section_end: f64,
|
||||
pub strain_peaks: Vec<f64>, // TODO: default capacity
|
||||
}
|
||||
|
||||
impl StrainSkill {
|
||||
pub const DECAY_WEIGHT: f64 = 0.9;
|
||||
pub const SECTION_LEN: f64 = 400.0;
|
||||
|
||||
pub fn save_curr_peak(&mut self) {
|
||||
self.strain_peaks.push(self.curr_section_peak);
|
||||
}
|
||||
|
||||
pub fn start_new_section_from(&mut self, initial_strain: f64) {
|
||||
self.curr_section_peak = initial_strain;
|
||||
}
|
||||
|
||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
||||
let mut strain_peaks = self.strain_peaks;
|
||||
strain_peaks.push(self.curr_section_peak);
|
||||
|
||||
strain_peaks
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self, decay_weight: f64) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
let mut peaks = self.get_curr_strain_peaks();
|
||||
peaks.retain(|&strain| strain > 0.0);
|
||||
peaks.sort_by(|a, b| b.total_cmp(a));
|
||||
|
||||
for strain in peaks {
|
||||
difficulty += strain * weight;
|
||||
weight *= decay_weight;
|
||||
}
|
||||
|
||||
difficulty
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StrainDecaySkill {
|
||||
pub inner: StrainSkill,
|
||||
pub curr_strain: f64,
|
||||
}
|
||||
|
||||
impl StrainDecaySkill {
|
||||
pub const DECAY_WEIGHT: f64 = StrainSkill::DECAY_WEIGHT;
|
||||
pub const SECTION_LEN: f64 = StrainSkill::SECTION_LEN;
|
||||
|
||||
pub fn save_curr_peak(&mut self) {
|
||||
self.inner.save_curr_peak();
|
||||
}
|
||||
|
||||
pub fn start_new_section_from(&mut self, initial_strain: f64) {
|
||||
self.inner.start_new_section_from(initial_strain);
|
||||
}
|
||||
|
||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
||||
self.inner.get_curr_strain_peaks()
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self, decay_weight: f64) -> f64 {
|
||||
self.inner.difficulty_value(decay_weight)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub use self::{
|
||||
attributes::{
|
||||
AttributeProvider, DifficultyAttributes, ModeAttributeProvider, PerformanceAttributes,
|
||||
},
|
||||
difficulty::{Difficulty, ModeDifficulty},
|
||||
performance::{HitResultPriority, Performance},
|
||||
score_state::ScoreState,
|
||||
};
|
||||
|
||||
mod attributes;
|
||||
pub(crate) mod difficulty;
|
||||
mod performance;
|
||||
mod score_state;
|
||||
@@ -0,0 +1,318 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{
|
||||
any::attributes::DifficultyAttributes,
|
||||
catch::{Catch, CatchPerformance},
|
||||
mania::{Mania, ManiaPerformance},
|
||||
model::beatmap::{Beatmap, Converted},
|
||||
osu::{Osu, OsuPerformance},
|
||||
taiko::{Taiko, TaikoPerformance},
|
||||
};
|
||||
|
||||
use super::{
|
||||
attributes::{AttributeProvider, PerformanceAttributes},
|
||||
score_state::ScoreState,
|
||||
};
|
||||
|
||||
/// Performance calculator on maps of any mode.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[must_use]
|
||||
pub enum Performance<'map> {
|
||||
Osu(OsuPerformance<'map>),
|
||||
Taiko(TaikoPerformance<'map>),
|
||||
Catch(CatchPerformance<'map>),
|
||||
Mania(ManiaPerformance<'map>),
|
||||
}
|
||||
|
||||
impl<'map> Performance<'map> {
|
||||
/// Create a new performance calculator for maps of any mode.
|
||||
pub fn new(map: &'map Beatmap) -> Self {
|
||||
let map = Cow::Borrowed(map);
|
||||
|
||||
match map.mode {
|
||||
GameMode::Osu => Self::Osu(OsuPerformance::new(Converted::new(map, false))),
|
||||
GameMode::Taiko => Self::Taiko(TaikoPerformance::new(Converted::new(map, false))),
|
||||
GameMode::Catch => Self::Catch(CatchPerformance::new(Converted::new(map, false))),
|
||||
GameMode::Mania => Self::Mania(ManiaPerformance::new(Converted::new(map, false))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new performance calculator through previously calculated
|
||||
/// attributes.
|
||||
///
|
||||
/// Note that the map, mods, and passed object count should be the same
|
||||
/// as when the attributes were calculated.
|
||||
pub fn from_attributes(attributes: impl AttributeProvider) -> Self {
|
||||
Self::from(attributes)
|
||||
}
|
||||
|
||||
/// Consume the performance calculator and calculate
|
||||
/// performance attributes for the given parameters.
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide the result of a previous difficulty or performance calculation.
|
||||
/// If you already calculated the attributes for the current map-mod combination,
|
||||
/// be sure to put them in here so that they don't have to be recalculated.
|
||||
pub fn attributes(self, attributes: impl AttributeProvider) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.attributes(attributes.attributes())),
|
||||
Self::Taiko(t) => Self::Taiko(t.attributes(attributes.attributes())),
|
||||
Self::Catch(f) => Self::Catch(f.attributes(attributes.attributes())),
|
||||
Self::Mania(m) => Self::Mania(m.attributes(attributes.attributes())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to convert the map to the specified mode.
|
||||
///
|
||||
/// Returns `None` if the conversion is incompatible or the internal
|
||||
/// beatmap was already replaced with difficulty attributes, i.e. if
|
||||
/// [`Performance::attributes`] or [`Performance::generate_state`] was
|
||||
/// called.
|
||||
///
|
||||
/// If the given mode should be ignored in case it is incompatible or if
|
||||
/// the internal beatmap was replaced, use [`mode_or_ignore`] instead.
|
||||
///
|
||||
/// [`mode_or_ignore`]: Self::mode_or_ignore
|
||||
pub fn try_mode(self, mode: GameMode) -> Option<Self> {
|
||||
match (self, mode) {
|
||||
(Self::Osu(o), _) => o.try_mode(mode),
|
||||
(this @ Self::Taiko(_), GameMode::Taiko)
|
||||
| (this @ Self::Catch(_), GameMode::Catch)
|
||||
| (this @ Self::Mania(_), GameMode::Mania) => Some(this),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to convert the map to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible or if the internal beatmap was
|
||||
/// already replaced with difficulty attributes, the map won't be modified.
|
||||
///
|
||||
/// To see whether the given mode is incompatible or the internal beatmap
|
||||
/// was replaced, use [`try_mode`] instead.
|
||||
///
|
||||
/// [`try_mode`]: Self::try_mode
|
||||
pub fn mode_or_ignore(self, mode: GameMode) -> Self {
|
||||
if let Self::Osu(osu) = self {
|
||||
osu.mode_or_ignore(mode)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
pub fn mods(self, mods: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.mods(mods)),
|
||||
Self::Taiko(t) => Self::Taiko(t.mods(mods)),
|
||||
Self::Catch(f) => Self::Catch(f.mods(mods)),
|
||||
Self::Mania(m) => Self::Mania(m.mods(mods)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
///
|
||||
#[cfg_attr(
|
||||
feature = "gradual",
|
||||
doc = "If you want to calculate the performance after every few objects, instead of
|
||||
using [`AnyPP`] multiple times with different `passed_objects`, you should use
|
||||
[`GradualPerformanceAttributes`](crate::GradualPerformance)."
|
||||
)]
|
||||
pub fn passed_objects(self, passed_objects: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.passed_objects(passed_objects)),
|
||||
Self::Taiko(t) => Self::Taiko(t.passed_objects(passed_objects)),
|
||||
Self::Catch(f) => Self::Catch(f.passed_objects(passed_objects)),
|
||||
Self::Mania(m) => Self::Mania(m.passed_objects(passed_objects)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust the clock rate used in the calculation.
|
||||
/// If none is specified, it will take the clock rate based on the mods
|
||||
/// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
|
||||
pub fn clock_rate(self, clock_rate: f64) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.clock_rate(clock_rate)),
|
||||
Self::Taiko(t) => Self::Taiko(t.clock_rate(clock_rate)),
|
||||
Self::Catch(f) => Self::Catch(f.clock_rate(clock_rate)),
|
||||
Self::Mania(m) => Self::Mania(m.clock_rate(clock_rate)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide parameters through a [`ScoreState`].
|
||||
pub fn state(self, state: ScoreState) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.state(state.into())),
|
||||
Self::Taiko(t) => Self::Taiko(t.state(state.into())),
|
||||
Self::Catch(f) => Self::Catch(f.state(state.into())),
|
||||
Self::Mania(m) => Self::Mania(m.state(state.into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the accuracy between `0.0` and `100.0`.
|
||||
pub fn accuracy(self, acc: f64) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.accuracy(acc)),
|
||||
Self::Taiko(t) => Self::Taiko(t.accuracy(acc)),
|
||||
Self::Catch(f) => Self::Catch(f.accuracy(acc)),
|
||||
Self::Mania(m) => Self::Mania(m.accuracy(acc)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the amount of misses of a play.
|
||||
pub fn n_misses(self, n_misses: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.n_misses(n_misses)),
|
||||
Self::Taiko(t) => Self::Taiko(t.n_misses(n_misses)),
|
||||
Self::Catch(f) => Self::Catch(f.misses(n_misses)),
|
||||
Self::Mania(m) => Self::Mania(m.n_misses(n_misses)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the max combo of the play.
|
||||
///
|
||||
/// Irrelevant for osu!mania.
|
||||
pub fn combo(self, combo: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.combo(combo)),
|
||||
Self::Taiko(t) => Self::Taiko(t.combo(combo)),
|
||||
Self::Catch(f) => Self::Catch(f.combo(combo)),
|
||||
Self::Mania(_) => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify how hitresults should be generated.
|
||||
///
|
||||
/// Defauls to [`HitResultPriority::BestCase`].
|
||||
pub fn hitresult_priority(self, priority: HitResultPriority) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.hitresult_priority(priority)),
|
||||
Self::Taiko(t) => Self::Taiko(t.hitresult_priority(priority)),
|
||||
Self::Catch(_) => self,
|
||||
Self::Mania(m) => Self::Mania(m.hitresult_priority(priority)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the amount of 300s of a play.
|
||||
pub fn n300(self, n300: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.n300(n300)),
|
||||
Self::Taiko(t) => Self::Taiko(t.n300(n300)),
|
||||
Self::Catch(f) => Self::Catch(f.fruits(n300)),
|
||||
Self::Mania(m) => Self::Mania(m.n300(n300)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the amount of 100s of a play.
|
||||
pub fn n100(self, n100: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.n100(n100)),
|
||||
Self::Taiko(t) => Self::Taiko(t.n100(n100)),
|
||||
Self::Catch(f) => Self::Catch(f.droplets(n100)),
|
||||
Self::Mania(m) => Self::Mania(m.n100(n100)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the amount of 50s of a play.
|
||||
///
|
||||
/// Irrelevant for osu!taiko.
|
||||
pub fn n50(self, n50: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.n50(n50)),
|
||||
Self::Taiko(_) => self,
|
||||
Self::Catch(f) => Self::Catch(f.tiny_droplets(n50)),
|
||||
Self::Mania(m) => Self::Mania(m.n50(n50)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the amount of katus of a play.
|
||||
///
|
||||
/// This value is only relevant for osu!catch for which it represents
|
||||
/// the amount of tiny droplet misses and osu!mania for which it.
|
||||
/// repesents the amount of n200.
|
||||
pub fn n_katu(self, n_katu: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(_) | Self::Taiko(_) => self,
|
||||
Self::Catch(f) => Self::Catch(f.tiny_droplet_misses(n_katu)),
|
||||
Self::Mania(m) => Self::Mania(m.n200(n_katu)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the amount of gekis of a play.
|
||||
///
|
||||
/// This value is only relevant for osu!mania for which it.
|
||||
/// repesents the amount of n320.
|
||||
pub fn n_geki(self, n_geki: u32) -> Self {
|
||||
match self {
|
||||
Self::Osu(_) | Self::Taiko(_) | Self::Catch(_) => self,
|
||||
Self::Mania(m) => Self::Mania(m.n320(n_geki)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the [`ScoreState`] that will be used for performance calculation.
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: AttributeProvider> From<A> for Performance<'_> {
|
||||
fn from(attrs: A) -> Self {
|
||||
fn inner(attrs: DifficultyAttributes) -> Performance<'static> {
|
||||
match attrs {
|
||||
DifficultyAttributes::Osu(attrs) => Performance::Osu(attrs.pp()),
|
||||
DifficultyAttributes::Taiko(attrs) => Performance::Taiko(attrs.pp()),
|
||||
DifficultyAttributes::Catch(attrs) => Performance::Catch(attrs.pp()),
|
||||
DifficultyAttributes::Mania(attrs) => Performance::Mania(attrs.pp()),
|
||||
}
|
||||
}
|
||||
|
||||
inner(attrs.attributes())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from_mode {
|
||||
( $mode:ident: $performance:ident ) => {
|
||||
impl<'a> From<Converted<'a, $mode>> for Performance<'a> {
|
||||
fn from(converted: Converted<'a, $mode>) -> Self {
|
||||
Self::$mode($performance::new(converted))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b: 'a> From<&'b Converted<'a, $mode>> for Performance<'a> {
|
||||
fn from(converted: &'b Converted<'a, $mode>) -> Self {
|
||||
Self::$mode($performance::new(converted.as_owned()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from_mode!(Osu: OsuPerformance);
|
||||
impl_from_mode!(Taiko: TaikoPerformance);
|
||||
impl_from_mode!(Catch: CatchPerformance);
|
||||
impl_from_mode!(Mania: ManiaPerformance);
|
||||
|
||||
/// While generating remaining hitresults, decide how they should be distributed.
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub enum HitResultPriority {
|
||||
/// Prioritize good hitresults over bad ones
|
||||
#[default]
|
||||
BestCase,
|
||||
/// Prioritize bad hitresults over good ones
|
||||
WorstCase,
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{
|
||||
catch::CatchScoreState, mania::ManiaScoreState, osu::OsuScoreState, taiko::TaikoScoreState,
|
||||
};
|
||||
|
||||
/// Aggregation for a score's current state.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ScoreState {
|
||||
/// Maximum combo that the score has had so far. **Not** the maximum
|
||||
/// possible combo of the map so far.
|
||||
///
|
||||
/// Note that for osu!catch only fruits and droplets are considered for
|
||||
/// combo.
|
||||
///
|
||||
/// Irrelevant for osu!mania.
|
||||
pub max_combo: u32,
|
||||
/// Amount of current gekis (n320 for osu!mania).
|
||||
pub n_geki: u32,
|
||||
/// Amount of current katus (tiny droplet misses for osu!catch / n200 for
|
||||
/// osu!mania).
|
||||
pub n_katu: u32,
|
||||
/// Amount of current 300s (fruits for osu!catch).
|
||||
pub n300: u32,
|
||||
/// Amount of current 100s (droplets for osu!catch).
|
||||
pub n100: u32,
|
||||
/// Amount of current 50s (tiny droplets for osu!catch).
|
||||
pub n50: u32,
|
||||
/// Amount of current misses (fruits + droplets for osu!catch).
|
||||
pub n_misses: u32,
|
||||
}
|
||||
|
||||
impl ScoreState {
|
||||
/// Create a new empty score state.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Return the total amount of hits by adding everything up based on the
|
||||
/// mode.
|
||||
pub fn total_hits(&self, mode: GameMode) -> u32 {
|
||||
let mut amount = self.n300 + self.n100 + self.n_misses;
|
||||
|
||||
if mode != GameMode::Taiko {
|
||||
amount += self.n50;
|
||||
|
||||
if mode != GameMode::Osu {
|
||||
amount += self.n_katu;
|
||||
amount += u32::from(mode != GameMode::Catch) * self.n_geki;
|
||||
}
|
||||
}
|
||||
|
||||
amount
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScoreState> for OsuScoreState {
|
||||
fn from(state: ScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n50: state.n50,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScoreState> for TaikoScoreState {
|
||||
fn from(state: ScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScoreState> for CatchScoreState {
|
||||
fn from(state: ScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n_fruits: state.n300,
|
||||
n_droplets: state.n100,
|
||||
n_tiny_droplets: state.n50,
|
||||
n_tiny_droplet_misses: state.n_katu,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScoreState> for ManiaScoreState {
|
||||
fn from(state: ScoreState) -> Self {
|
||||
Self {
|
||||
n320: state.n_geki,
|
||||
n300: state.n300,
|
||||
n200: state.n_katu,
|
||||
n100: state.n100,
|
||||
n50: state.n50,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsuScoreState> for ScoreState {
|
||||
fn from(state: OsuScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n50: state.n50,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TaikoScoreState> for ScoreState {
|
||||
fn from(state: TaikoScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n50: 0,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CatchScoreState> for ScoreState {
|
||||
fn from(state: CatchScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n_geki: 0,
|
||||
n_katu: state.n_tiny_droplet_misses,
|
||||
n300: state.n_fruits,
|
||||
n100: state.n_droplets,
|
||||
n50: state.n_tiny_droplets,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ManiaScoreState> for ScoreState {
|
||||
fn from(state: ManiaScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: 0,
|
||||
n_geki: state.n320,
|
||||
n_katu: state.n200,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n50: state.n50,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user