expose BeatmapExt methods for mode-specific objects (#20)

* expose osu hitobjects

* added pub method to create osu objects

* expose mania objects

* added some derived traits to public types

* expose taiko objects

* expose catch objects

* add documentation

* fix documentation
This commit is contained in:
MaxOhn
2023-01-28 04:33:05 +01:00
committed by GitHub
parent cdb0bbb8f4
commit fb36edd132
11 changed files with 294 additions and 112 deletions
+116 -3
View File
@@ -1,7 +1,16 @@
use crate::{
catch::{
calculate_catch_width, CatchDifficultyAttributes, CatchObject, FruitOrJuice, FruitParams,
ALLOWED_CATCH_RANGE,
},
curve::CurveBuffers,
mania::{ManiaObject, ObjectParameters},
osu::{OsuDifficultyAttributes, OsuObject, ScalingFactor},
taiko::{IntoTaikoObjectIter, TaikoObject},
util::FloatExt,
AnyPP, AnyStars, Beatmap, CatchPP, CatchStars, GameMode, GradualDifficultyAttributes,
GradualPerformanceAttributes, ManiaPP, ManiaStars, OsuPP, OsuStars, PerformanceAttributes,
Strains, TaikoPP, TaikoStars,
GradualPerformanceAttributes, ManiaPP, ManiaStars, Mods, OsuPP, OsuStars,
PerformanceAttributes, Strains, TaikoPP, TaikoStars,
};
/// Provides some additional methods on [`Beatmap`].
@@ -26,7 +35,7 @@ pub trait BeatmapExt {
/// Suitable to plot the difficulty of a map over time.
fn strains(&self, mods: u32) -> Strains;
/// Return an iterator that gives you the [`DifficultyAttributes`] after each hit object.
/// Return an iterator that gives you the [`DifficultyAttributes`](crate::DifficultyAttributes) after each hit object.
///
/// Suitable to efficiently get the map's star rating after multiple different locations.
fn gradual_difficulty(&self, mods: u32) -> GradualDifficultyAttributes<'_>;
@@ -36,6 +45,29 @@ pub trait BeatmapExt {
/// Suitable to efficiently get a score's performance after multiple different locations,
/// i.e. live update a score's pp.
fn gradual_performance(&self, mods: u32) -> GradualPerformanceAttributes<'_>;
/// Process each [`HitObject`](crate::parse::HitObject) into a an osu!-specific [`OsuObject`],
/// just like the difficulty calculation does.
fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject>;
/// Process each [`HitObject`](crate::parse::HitObject) into a an osu!taiko-specific [`TaikoObject`],
/// just like the difficulty calculation does.
///
/// Clockrate is *not* considered.
fn taiko_hitobjects(&self) -> Vec<TaikoObject>;
/// Process each [`HitObject`](crate::parse::HitObject) into a an osu!ctb-specific [`CatchObject`],
/// just like the difficulty calculation does.
///
/// A [`CatchObject`] is either a fruit or a droplet which means
/// tiny droplets and bananas are not included.
fn catch_hitobjects(&self, mods: u32) -> Vec<CatchObject>;
/// Process each [`HitObject`](crate::parse::HitObject) into a an osu!mania-specific [`ManiaObject`],
/// just like the difficulty calculation does.
///
/// Clockrate is *not* considered.
fn mania_hitobjects(&self) -> Vec<ManiaObject>;
}
impl BeatmapExt for Beatmap {
@@ -89,4 +121,85 @@ impl BeatmapExt for Beatmap {
fn gradual_performance(&self, mods: u32) -> GradualPerformanceAttributes<'_> {
GradualPerformanceAttributes::new(self, mods)
}
fn osu_hitobjects(&self, mods: u32) -> Vec<OsuObject> {
let attrs = self.attributes().mods(mods).build();
let scaling_factor = ScalingFactor::new(attrs.cs);
let hr = mods.hr();
let time_preempt = (attrs.hit_windows.ar * attrs.clock_rate) as f32 as f64;
let mut attrs = OsuDifficultyAttributes::default();
crate::osu::create_osu_objects(
self,
&mut attrs,
&scaling_factor,
usize::MAX,
hr,
time_preempt,
)
}
fn taiko_hitobjects(&self) -> Vec<TaikoObject> {
let map = self.convert_mode(GameMode::Taiko);
map.taiko_objects()
.map(|(h, start_time)| TaikoObject {
start_time,
is_hit: h.is_hit,
is_rim: h.is_rim,
})
.collect()
}
fn catch_hitobjects(&self, mods: u32) -> Vec<CatchObject> {
let attrs = self.attributes().mods(mods).build();
let mut params = FruitParams {
attributes: CatchDifficultyAttributes::default(),
curve_bufs: CurveBuffers::default(),
last_pos: None,
last_time: 0.0,
map: self,
ticks: Vec::new(),
with_hr: mods.hr(),
};
let mut hit_objects: Vec<_> = self
.hit_objects
.iter()
.filter_map(|h| FruitOrJuice::new(h, &mut params))
.flatten()
.collect();
let half_catcher_width =
(calculate_catch_width(attrs.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
let mut last_direction = 0;
let mut last_excess = half_catcher_width;
for i in 1..hit_objects.len() {
// SAFETY: The indices are guaranteed the be included based on the loop condition
let window = unsafe { hit_objects.get_unchecked_mut(i - 1..=i) };
let [curr, next] = window else { unreachable!() };
curr.init_hyper_dash(
half_catcher_width,
&*next,
&mut last_direction,
&mut last_excess,
);
}
hit_objects
}
fn mania_hitobjects(&self) -> Vec<ManiaObject> {
let map = self.convert_mode(GameMode::Mania);
let total_columns = map.cs.round_even().max(1.0);
let mut params = ObjectParameters::new(map.as_ref());
self.hit_objects
.iter()
.map(|h| ManiaObject::new(h, total_columns, &mut params))
.collect()
}
}
+10 -6
View File
@@ -5,13 +5,17 @@ use super::fruit_or_juice::FruitParams;
const PLAYFIELD_WIDTH: f32 = 512.0;
const BASE_SPEED: f64 = 1.0;
#[derive(Clone, Debug)]
/// A [`HitObject`](crate::parse::HitObject) that was processed for the osu!ctb gamemode.
#[derive(Clone, Debug, PartialEq)]
pub struct CatchObject {
pub(crate) pos: f32,
pub(crate) time: f64,
pub(crate) hyper_dash: bool,
pub(crate) hyper_dist: f32,
/// The X position of the object.
pub pos: f32,
/// The time of the object.
pub time: f64,
/// Whether the object is a hyper dash.
pub hyper_dash: bool,
/// The hyper distance to the next object
pub hyper_dist: f32,
}
impl CatchObject {
+6 -7
View File
@@ -6,20 +6,19 @@ mod gradual_performance;
mod movement;
mod pp;
use catch_object::CatchObject;
use difficulty_object::DifficultyObject;
use fruit_or_juice::FruitOrJuice;
pub use gradual_difficulty::*;
pub use gradual_performance::*;
use movement::Movement;
pub use pp::*;
use crate::{catch::fruit_or_juice::FruitParams, curve::CurveBuffers, Beatmap, Mods, OsuStars};
pub use self::{catch_object::CatchObject, gradual_difficulty::*, gradual_performance::*, pp::*};
pub(crate) use self::fruit_or_juice::{FruitOrJuice, FruitParams};
use crate::{curve::CurveBuffers, Beatmap, Mods, OsuStars};
const SECTION_LENGTH: f64 = 750.0;
const STAR_SCALING_FACTOR: f64 = 0.153;
const ALLOWED_CATCH_RANGE: f32 = 0.8;
pub(crate) const ALLOWED_CATCH_RANGE: f32 = 0.8;
const CATCHER_SIZE: f32 = 106.75;
/// Difficulty calculator on osu!catch maps.
+9 -4
View File
@@ -22,10 +22,15 @@ impl<'a> ObjectParameters<'a> {
}
}
pub(crate) struct ManiaObject {
pub(crate) start_time: f64,
pub(crate) end_time: f64,
pub(crate) column: usize,
/// A [`HitObject`] that was processed for the osu!mania gamemode.
#[derive(Clone, Debug, PartialEq)]
pub struct ManiaObject {
/// Start time of the object.
pub start_time: f64,
/// Endtime of the object.
pub end_time: f64,
/// Column of the object.
pub column: usize,
}
impl ManiaObject {
+2 -3
View File
@@ -9,13 +9,12 @@ use std::borrow::Cow;
use crate::{beatmap::BeatmapHitWindows, util::FloatExt, Beatmap, GameMode, Mods, OsuStars};
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
pub use self::{gradual_difficulty::*, gradual_performance::*, mania_object::ManiaObject, pp::*};
pub(crate) use self::mania_object::ManiaObject;
pub(crate) use self::mania_object::ObjectParameters;
use self::{
difficulty_object::ManiaDifficultyObject,
mania_object::ObjectParameters,
skills::{Skill, Strain},
};
+2 -2
View File
@@ -213,7 +213,7 @@ impl Distances {
let mut lazy_travel_dist: f32 = 0.0;
for (curr_movement_obj, i) in slider.nested_iter().zip(1..) {
for (curr_movement_obj, i) in slider.nested_objects.iter().zip(1..) {
let mut curr_movement =
(curr_movement_obj.pos + hit_object.stack_offset) - curr_cursor_pos;
let mut curr_movement_len = scaling_factor * curr_movement.length() as f64;
@@ -221,7 +221,7 @@ impl Distances {
// * Amount of movement required so that the cursor position needs to be updated.
let mut required_movement = Self::ASSUMED_SLIDER_RADIUS as f64;
if i == slider.nested_len() {
if i == slider.nested_objects.len() {
// * The end of a slider has special aim rules due
// * to the relaxed time constraint on position.
// * There is both a lazy end position as well as the actual end slider position.
+1 -1
View File
@@ -202,7 +202,7 @@ impl OsuGradualDifficultyAttributes {
OsuObjectKind::Circle => attrs.n_circles += 1,
OsuObjectKind::Slider(slider) => {
attrs.n_sliders += 1;
attrs.max_combo += slider.nested_len();
attrs.max_combo += slider.nested_objects.len();
}
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
}
+46 -33
View File
@@ -10,12 +10,12 @@ use crate::{curve::CurveBuffers, parse::Pos2, AnyStars, Beatmap, GameMode, Mods}
use self::{
difficulty_object::{Distances, OsuDifficultyObject},
osu_object::{ObjectParameters, OsuObject},
scaling_factor::ScalingFactor,
skills::{Skill, Skills},
};
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
pub use self::{gradual_difficulty::*, gradual_performance::*, osu_object::*, pp::*};
pub(crate) use self::scaling_factor::ScalingFactor;
const SECTION_LEN: f64 = 400.0;
const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
@@ -269,33 +269,9 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
..Default::default()
};
let mut params = ObjectParameters {
map,
attrs: &mut attrs,
ticks: Vec::new(),
curve_bufs: CurveBuffers::default(),
};
let mut hit_objects: Vec<_> = map
.hit_objects
.iter()
.take(take)
.map(|h| OsuObject::new(h, &mut params))
.collect();
let stack_threshold = time_preempt * map.stack_leniency as f64;
if map.version >= 6 {
stacking(&mut hit_objects, stack_threshold);
} else {
old_stacking(&mut hit_objects, stack_threshold);
}
let mut hit_objects = hit_objects.iter_mut().map(|h| {
h.post_process(hr, &scaling_factor);
h
});
let mut hit_objects =
create_osu_objects(map, &mut attrs, &scaling_factor, take, hr, time_preempt);
let mut hit_objects_iter = hit_objects.iter_mut();
let mut skills = Skills::new(
mods,
@@ -305,7 +281,7 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
hit_window,
);
let last = match hit_objects.next() {
let last = match hit_objects_iter.next() {
Some(prev) => prev,
None => return (skills, attrs),
};
@@ -316,9 +292,9 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
Distances::compute_slider_cursor_pos(last, &scaling_factor);
let mut last = &*last;
let mut diff_objects = Vec::with_capacity(hit_objects.len());
let mut diff_objects = Vec::with_capacity(hit_objects_iter.len());
for (i, curr) in hit_objects.enumerate() {
for (i, curr) in hit_objects_iter.enumerate() {
let delta_time = (curr.start_time - last.start_time) / clock_rate;
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
@@ -347,6 +323,43 @@ fn calculate_skills(params: OsuStars<'_>) -> (Skills, OsuDifficultyAttributes) {
(skills, attrs)
}
pub(crate) fn create_osu_objects(
map: &Beatmap,
attrs: &mut OsuDifficultyAttributes,
scaling_factor: &ScalingFactor,
take: usize,
hr: bool,
time_preempt: f64,
) -> Vec<OsuObject> {
let mut params = ObjectParameters {
map,
attrs,
ticks: Vec::new(),
curve_bufs: CurveBuffers::default(),
};
let mut hit_objects: Vec<_> = map
.hit_objects
.iter()
.take(take)
.map(|h| OsuObject::new(h, &mut params))
.collect();
let stack_threshold = time_preempt * map.stack_leniency as f64;
if map.version >= 6 {
stacking(&mut hit_objects, stack_threshold);
} else {
old_stacking(&mut hit_objects, stack_threshold);
}
hit_objects
.iter_mut()
.for_each(|h| h.post_process(hr, scaling_factor));
hit_objects
}
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
let mut extended_start_idx = 0;
+85 -51
View File
@@ -1,5 +1,3 @@
use std::slice::Iter;
use super::{scaling_factor::ScalingFactor, OsuDifficultyAttributes, PLAYFIELD_BASE_SIZE};
use crate::{
@@ -11,56 +9,75 @@ use crate::{
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
const BASE_SCORING_DISTANCE: f64 = 100.0;
#[derive(Clone, Debug)]
pub(crate) struct OsuObject {
pos: Pos2,
pub(crate) start_time: f64,
pub(crate) stack_offset: Pos2,
pub(crate) stack_height: f32,
pub(crate) kind: OsuObjectKind,
/// A [`HitObject`] that was processed for the osu! gamemode.
#[derive(Clone, Debug, PartialEq)]
pub struct OsuObject {
/// Position of the object.
pub pos: Pos2,
/// Start time of the object.
pub start_time: f64,
/// The positional offset due to stacking.
pub stack_offset: Pos2,
/// The height of the stacking offset.
pub stack_height: f32,
/// Type of the object.
pub kind: OsuObjectKind,
}
#[derive(Clone, Debug)]
pub(crate) enum OsuObjectKind {
/// The type of an [`OsuObject`].
#[derive(Clone, Debug, PartialEq)]
pub enum OsuObjectKind {
/// A hitcircle object.
Circle,
/// A slider object.
Slider(OsuSlider),
Spinner { end_time: f64 },
/// A spinner object.
Spinner {
/// The endtime of the spinner.
end_time: f64,
},
}
#[derive(Clone, Debug)]
pub(crate) struct OsuSlider {
pub(crate) end_time: f64,
pub(crate) lazy_end_pos: Pos2,
nested_objects: Vec<NestedObject>,
/// A [`HitObject`] that was processed as a slider for the osu! gamemode.
#[derive(Clone, Debug, PartialEq)]
pub struct OsuSlider {
/// The endtime of the slider.
pub end_time: f64,
/// The lazy end position of the cursor.
pub lazy_end_pos: Pos2,
/// All nested objects of the slider except for the slider head.
pub nested_objects: Vec<NestedObject>,
}
impl OsuSlider {
pub(crate) fn nested_len(&self) -> usize {
self.nested_objects.len()
}
pub(crate) fn nested_iter(&self) -> Iter<'_, NestedObject> {
self.nested_objects.iter()
}
pub(crate) fn repeat_count(&self) -> usize {
/// The amount of repeat points.
pub fn repeat_count(&self) -> usize {
self.nested_objects.iter().fold(0, |count, nested| {
count + matches!(nested.kind, NestedObjectKind::Repeat) as usize
})
}
pub(crate) fn end_pos(&self) -> Option<Pos2> {
/// The position of the slider tail.
///
/// This is usually but not necessarily the position of the last nested object.
pub fn end_pos(&self) -> Option<Pos2> {
self.tail().map(|tail| tail.pos)
}
pub(crate) fn tail(&self) -> Option<&NestedObject> {
/// A shared reference to the slider tail.
///
/// This is usually but not necessarily the last nested object.
pub fn tail(&self) -> Option<&NestedObject> {
self.nested_objects
.iter()
.rev()
.find(|nested| matches!(nested.kind, NestedObjectKind::Tail))
}
pub(crate) fn tail_mut(&mut self) -> Option<(usize, &mut NestedObject)> {
/// An exclusive reference to the slider tail and its index in the nested object list.
///
/// This is usually but not necessarily the last nested object.
pub fn tail_mut(&mut self) -> Option<(usize, &mut NestedObject)> {
self.nested_objects
.iter_mut()
.enumerate()
@@ -69,18 +86,27 @@ impl OsuSlider {
}
}
#[derive(Clone, Debug)]
pub(crate) struct NestedObject {
/// Note: `pos` does not include stacking!
pub(crate) pos: Pos2,
pub(crate) start_time: f64,
pub(crate) kind: NestedObjectKind,
/// A nested object within a slider.
#[derive(Clone, Debug, PartialEq)]
pub struct NestedObject {
/// Position of the object.
///
/// Note: `pos` does not include stacking.
pub pos: Pos2,
/// Start time of the object.
pub start_time: f64,
/// Type of the object.
pub kind: NestedObjectKind,
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum NestedObjectKind {
/// Type of a [`NestedObject`].
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum NestedObjectKind {
/// A repeat point.
Repeat,
/// The logical slider tail i.e. the legacy last tick.
Tail,
/// A regular slider tick.
Tick,
}
@@ -318,7 +344,8 @@ impl OsuObject {
}
}
pub(crate) fn end_time(&self) -> f64 {
/// Endtime of the object.
pub fn end_time(&self) -> f64 {
match &self.kind {
OsuObjectKind::Circle => self.start_time,
OsuObjectKind::Slider(slider) => slider.end_time,
@@ -326,7 +353,8 @@ impl OsuObject {
}
}
pub(crate) fn end_pos(&self) -> Pos2 {
/// End position of the object.
pub fn end_pos(&self) -> Pos2 {
match &self.kind {
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
OsuObjectKind::Slider(slider) => slider.end_pos().unwrap_or(self.pos),
@@ -356,7 +384,8 @@ impl OsuObject {
.map_or(self.pos, |end_pos| self.pos + end_pos)
} else {
slider
.nested_iter()
.nested_objects
.iter()
.find(|nested| matches!(nested.kind, NestedObjectKind::Repeat))
.map_or(self.pos, |repeat| repeat.pos)
}
@@ -364,26 +393,31 @@ impl OsuObject {
}
}
pub(crate) const fn pos(&self) -> Pos2 {
/// Position of the object without stacking.
pub const fn pos(&self) -> Pos2 {
self.pos
}
pub(crate) fn stacked_pos(&self) -> Pos2 {
/// Stacked position of the object.
pub fn stacked_pos(&self) -> Pos2 {
self.pos + self.stack_offset
}
pub(crate) fn stacked_end_pos(&self) -> Pos2 {
/// Stacked end position of the object.
pub fn stacked_end_pos(&self) -> Pos2 {
self.end_pos() + self.stack_offset
}
pub(crate) fn lazy_end_pos(&self) -> Pos2 {
/// Lazy end position of the object. Stacking is included.
pub fn lazy_end_pos(&self) -> Pos2 {
match &self.kind {
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.stacked_pos(),
OsuObjectKind::Slider(slider) => slider.lazy_end_pos,
}
}
pub(crate) fn lazy_travel_time(&self) -> f64 {
/// Lazy travel time of the object.
pub fn lazy_travel_time(&self) -> f64 {
match &self.kind {
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => 0.0,
OsuObjectKind::Slider(slider) => slider
@@ -393,18 +427,18 @@ impl OsuObject {
}
}
#[inline]
pub(crate) fn is_circle(&self) -> bool {
/// Whether the object is a hitcircle.
pub const fn is_circle(&self) -> bool {
matches!(self.kind, OsuObjectKind::Circle)
}
#[inline]
pub(crate) fn is_slider(&self) -> bool {
/// Whether the object is a slider.
pub const fn is_slider(&self) -> bool {
matches!(self.kind, OsuObjectKind::Slider { .. })
}
#[inline]
pub(crate) fn is_spinner(&self) -> bool {
/// Whether the object is a spinner.
pub const fn is_spinner(&self) -> bool {
matches!(self.kind, OsuObjectKind::Spinner { .. })
}
+6 -2
View File
@@ -9,7 +9,12 @@ mod taiko_object;
use std::{borrow::Cow, cell::RefCell, rc::Rc};
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
pub use self::{
gradual_difficulty::*, gradual_performance::*, pp::*,
taiko_object::TaikoObjectPub as TaikoObject,
};
pub(crate) use self::taiko_object::IntoTaikoObjectIter;
use crate::{beatmap::BeatmapHitWindows, Beatmap, GameMode, Mods, OsuStars};
@@ -17,7 +22,6 @@ use self::{
colours::ColourDifficultyPreprocessor,
difficulty_object::{MonoIndex, ObjectLists, TaikoDifficultyObject},
skills::{Peaks, PeaksDifficultyValues, PeaksRaw, Skill},
taiko_object::IntoTaikoObjectIter,
};
const SECTION_LEN: usize = 400;
+11
View File
@@ -62,3 +62,14 @@ impl ExactSizeIterator for TaikoObjectIter<'_> {
self.hit_objects.len()
}
}
/// A [`HitObject`] that was processed for the osu!taiko gamemode.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct TaikoObjectPub {
/// Start time of the object.
pub start_time: f64,
/// Whether the object is a circle i.e. this is false if the object is a drum roll or a swell.
pub is_hit: bool,
/// Whether it's a rim or center hit.
pub is_rim: bool,
}