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
+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)
}
}