initial rewrite

This commit is contained in:
MaxOhn
2024-02-09 17:54:14 +01:00
parent ecf1b2c7f4
commit a8bf9601b6
190 changed files with 10892 additions and 18142 deletions
+186
View File
@@ -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);
+210
View File
@@ -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)),
),
}
}
}
+69
View File
@@ -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)
}
}
+13
View File
@@ -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))
}
}
+94
View File
@@ -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)
}
}
+13
View File
@@ -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;
+318
View File
@@ -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,
}
+160
View File
@@ -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,
}
}
}