fruits: optimized and tested

This commit is contained in:
MaxOhn
2021-01-16 20:57:12 +01:00
parent b404381cd3
commit aa3cc11d6e
11 changed files with 273 additions and 101 deletions
+74
View File
@@ -0,0 +1,74 @@
use crate::{Beatmap, DifficultyPoint, TimingPoint};
use std::slice::Iter;
macro_rules! next_tuple {
($iter:expr, ($first:ident, $second:ident)) => {
$iter.next().map(|e| (e.$first, e.$second))
};
}
pub(crate) struct ControlPointIter<'p> {
timing_points: Iter<'p, TimingPoint>,
difficulty_points: Iter<'p, DifficultyPoint>,
next_timing: Option<(f32, f32)>,
next_difficulty: Option<(f32, f32)>,
}
impl<'p> ControlPointIter<'p> {
#[inline]
pub(crate) fn new(map: &'p Beatmap) -> Self {
let mut timing_points = map.timing_points.iter();
let mut difficulty_points = map.difficulty_points.iter();
Self {
next_timing: next_tuple!(timing_points, (time, beat_len)),
next_difficulty: next_tuple!(difficulty_points, (time, speed_multiplier)),
timing_points,
difficulty_points,
}
}
}
pub(crate) enum ControlPoint {
Timing { time: f32, beat_len: f32 },
Difficulty { time: f32, speed_mult: f32 },
}
impl ControlPoint {
#[inline]
pub(crate) fn time(&self) -> f32 {
match self {
Self::Timing { time, .. } => *time,
Self::Difficulty { time, .. } => *time,
}
}
}
impl<'p> Iterator for ControlPointIter<'p> {
type Item = ControlPoint;
fn next(&mut self) -> Option<Self::Item> {
match (self.next_timing, self.next_difficulty) {
(Some((time, beat_len)), Some((d, _))) if time <= d => {
self.next_timing = next_tuple!(self.timing_points, (time, beat_len));
Some(ControlPoint::Timing { time, beat_len })
}
(_, Some((time, speed_mult))) => {
self.next_difficulty =
next_tuple!(self.difficulty_points, (time, speed_multiplier));
Some(ControlPoint::Difficulty { time, speed_mult })
}
(Some((time, beat_len)), None) => {
self.next_timing = next_tuple!(self.timing_points, (time, beat_len));
Some(ControlPoint::Timing { time, beat_len })
}
(None, None) => None,
}
}
}
+117 -66
View File
@@ -1,16 +1,18 @@
mod catch_object;
mod control_point_iter;
mod difficulty_object;
mod movement;
mod pp;
mod slider_state;
use catch_object::CatchObject;
use difficulty_object::DifficultyObject;
use movement::Movement;
pub use pp::*;
use slider_state::SliderState;
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType};
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType, Pos2};
use std::cmp::Ordering;
use std::convert::identity;
const SECTION_LENGTH: f32 = 750.0;
@@ -19,11 +21,7 @@ const STAR_SCALING_FACTOR: f32 = 0.153;
const ALLOWED_CATCH_RANGE: f32 = 0.8;
const CATCHER_SIZE: f32 = 106.75;
macro_rules! binary_search {
($slice:expr, $target:expr) => {
$slice.binary_search_by(|p| p.time.partial_cmp(&$target).unwrap_or(Ordering::Equal))
};
}
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
/// Star calculation for osu!ctb maps
// Slider parsing based on https://github.com/osufx/catch-the-pp
@@ -35,9 +33,11 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
let attributes = map.attributes().mods(mods);
let with_hr = mods.hr();
let mut ticks = Vec::new(); // using the same buffer for all sliders
let mut slider_state = SliderState::new(map);
let mut fruits = 0;
let mut droplets = 0;
let mut tiny_droplets = 0;
// BUG: Incorrect object order on 2B maps that have fruits within sliders
let mut hit_objects = map
@@ -66,48 +66,21 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
*last_time = h.start_time;
let (beat_len, timing_time) = {
match binary_search!(map.timing_points, h.start_time) {
Ok(idx) => {
let point = &map.timing_points[idx];
(point.beat_len, point.time)
}
Err(0) => (1000.0, 0.0),
Err(idx) => {
let point = &map.timing_points[idx - 1];
(point.beat_len, point.time)
}
}
};
let (speed_multiplier, diff_time) = {
match binary_search!(map.difficulty_points, h.start_time) {
Ok(idx) => {
let point = &map.difficulty_points[idx];
(point.speed_multiplier, point.time)
}
Err(0) => (1.0, 0.0),
Err(idx) => {
let point = &map.difficulty_points[idx - 1];
(point.speed_multiplier, point.time)
}
}
};
// Responsible for timing point values
slider_state.update(h.start_time);
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
if map.version >= 8 {
tick_distance /= (100.0 / speed_multiplier).max(10.0).min(1000.0) / 100.0;
tick_distance /=
(100.0 / slider_state.speed_mult).max(10.0).min(1000.0) / 100.0;
}
let spm = if timing_time > diff_time {
1.0
} else {
speed_multiplier
};
let duration = *repeats as f32 * beat_len * *pixel_len / (map.sv * spm) / 100.0;
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
/ (map.sv * slider_state.speed_mult)
/ 100.0;
// Ensure path type validity
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
PathType::Bezier
} else if curve_points.len() == 2 {
@@ -116,6 +89,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
*path_type
};
// Build the curve w.r.t. the curve points
let curve = match path_type {
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
PathType::Bezier => Curve::bezier(curve_points),
@@ -129,42 +103,49 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
let target = *pixel_len - tick_distance / 8.0;
ticks.reserve((target / tick_distance) as usize);
while current_distance < target {
let pos = curve.point_at_distance(current_distance);
// Tick of the first span
if current_distance < target {
for tick_idx in 1.. {
let pos = curve.point_at_distance(current_distance);
let time = h.start_time + time_add * tick_idx as f32;
ticks.push((pos, time));
current_distance += tick_distance;
ticks.push((pos, h.start_time + time_add * (ticks.len() + 1) as f32));
current_distance += tick_distance;
if current_distance >= target {
break;
}
}
}
tiny_droplets +=
tiny_droplet_count(h.start_time, time_add, duration, *repeats, &ticks);
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
slider_objects.push((h.pos, h.start_time));
// Other spans
if *repeats <= 1 {
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
} else {
slider_objects.append(&mut ticks.clone());
for repeat_id in 1..*repeats - 1 {
for repeat_id in 1..*repeats {
let dist = (repeat_id % 2) as f32 * *pixel_len;
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
let pos = curve.point_at_distance(dist);
// Reverse tick / last legacy tick
// Reverse tick
slider_objects.push((pos, h.start_time + time_offset));
ticks.reverse();
slider_objects.extend_from_slice(&ticks); // tick time doesn't need to be adjusted for some reason
// Actual ticks
if repeat_id & 1 == 1 {
slider_objects.extend(ticks.iter().copied().rev());
} else {
slider_objects.extend(ticks.iter().copied());
}
}
// Handling last span separatly so that `ticks` vector isn't cloned again
let dist = ((*repeats - 1) % 2) as f32 * *pixel_len;
let time_offset = (duration / *repeats as f32) * (*repeats - 1) as f32;
let pos = curve.point_at_distance(dist);
slider_objects.push((pos, h.start_time + time_offset));
ticks.reverse();
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
ticks.clear();
}
// Slider tail
@@ -280,12 +261,85 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
DifficultyAttributes {
stars,
ar: attributes.ar,
n_fruits: fruits,
n_droplets: droplets,
n_tiny_droplets: tiny_droplets,
max_combo: fruits + droplets,
}
}
fn tiny_droplet_count(
start_time: f32,
time_between_ticks: f32,
duration: f32,
spans: usize,
ticks: &[(Pos2, f32)],
) -> usize {
// tiny droplets preceeding a _tick_
let per_tick = if !ticks.is_empty() && time_between_ticks > 80.0 {
let time_between_tiny = shrink_down(time_between_ticks);
// add a little for floating point inaccuracies
let start = time_between_tiny + 0.001;
count_iterations(start, time_between_tiny, time_between_ticks)
} else {
0
};
// tiny droplets preceeding a _reverse_
let last = ticks.last().map_or(start_time, |(_, last)| *last);
let repeat_time = start_time + duration / spans as f32;
let since_last_tick = repeat_time - last;
let span_last_section = if since_last_tick > 80.0 {
let time_between_tiny = shrink_down(since_last_tick);
count_iterations(time_between_tiny, time_between_tiny, since_last_tick)
} else {
0
};
// tiny droplets preceeding the slider tail
// necessary to handle distinctly because of the legacy last tick
let last = ticks.last().map_or(start_time, |(_, last)| *last);
let end_time = start_time + duration / spans as f32 - LEGACY_LAST_TICK_OFFSET;
let since_last_tick = end_time - last;
let last_section = if since_last_tick > 80.0 {
let time_between_tiny = shrink_down(since_last_tick);
count_iterations(time_between_tiny, time_between_tiny, since_last_tick)
} else {
0
};
// Combine tiny droplets counts
per_tick * ticks.len() * spans + span_last_section * (spans.saturating_sub(1)) + last_section
}
#[inline]
fn shrink_down(mut val: f32) -> f32 {
while val > 100.0 {
val /= 2.0;
}
val
}
#[inline]
fn count_iterations(mut start: f32, step: f32, end: f32) -> usize {
let mut count = 0;
while start < end {
count += 1;
start += step;
}
count
}
#[inline]
pub(crate) fn calculate_catch_width(cs: f32) -> f32 {
let scale = 1.0 - 0.7 * (cs - 5.0) / 5.0;
@@ -323,8 +377,10 @@ impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
pub struct DifficultyAttributes {
pub stars: f32,
pub max_combo: usize,
pub ar: f32,
pub n_fruits: usize,
pub n_droplets: usize,
pub n_tiny_droplets: usize,
}
#[cfg(test)]
@@ -335,22 +391,17 @@ mod tests {
#[test]
#[ignore]
fn fruits_single() {
let map_id = 1972149;
let file = match File::open(format!("E:/Games/osu!/beatmaps/{}.osu", map_id)) {
let file = match File::open("E:/Games/osu!/beatmaps/2206596.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
// let file = match File::open(format!("E:/Games/osu!/beatmaps/{}.osu", map_id)) {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let result = PpCalculator::new(&map).mods(256).calculate();
let result = PpCalculator::new(&map).mods(64).calculate();
println!("Stars: {}", result.stars);
println!("PP: {}", result.pp);
+4 -4
View File
@@ -124,7 +124,7 @@ impl<'m> PpCalculator<'m> {
.saturating_sub(self.n_misses.saturating_sub(n_droplets))
});
let max_tiny_droplets = 0; // TODO
let max_tiny_droplets = attributes.n_tiny_droplets;
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
((acc * (attributes.max_combo + max_tiny_droplets) as f32).round() as usize)
@@ -151,7 +151,7 @@ impl<'m> PpCalculator<'m> {
let stars = attributes.stars;
// Relying heavily on aim
let mut pp = (5.0 * ((stars / 0.0049).max(1.0)) - 4.0).powi(2) / 100_000.0;
let mut pp = (5.0 * (stars / 0.0049).max(1.0) - 4.0).powi(2) / 100_000.0;
let mut combo_hits = self.combo_hits();
if combo_hits == 0 {
@@ -175,7 +175,7 @@ impl<'m> PpCalculator<'m> {
}
// AR scaling
let ar = self.map.ar;
let ar = attributes.ar;
let mut ar_factor = 1.0;
if ar > 9.0 {
ar_factor += 0.1 * (ar - 9.0) + (ar > 10.0) as u8 as f32 * 0.1 * (ar - 10.0);
@@ -231,7 +231,7 @@ impl<'m> PpCalculator<'m> {
let total_hits = self.total_hits();
if total_hits == 0 {
0.0
1.0
} else {
(self.successful_hits() as f32 / total_hits as f32)
.max(0.0)
+44
View File
@@ -0,0 +1,44 @@
use super::control_point_iter::{ControlPoint, ControlPointIter};
use crate::Beatmap;
pub(crate) struct SliderState<'p> {
control_points: ControlPointIter<'p>,
next: Option<ControlPoint>,
pub(crate) beat_len: f32,
pub(crate) speed_mult: f32,
}
impl<'p> SliderState<'p> {
#[inline]
pub(crate) fn new(map: &'p Beatmap) -> Self {
let mut control_points = ControlPointIter::new(map);
let (beat_len, speed_mult) = match control_points.next() {
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
Some(ControlPoint::Difficulty { speed_mult, .. }) => (1000.0, speed_mult),
None => (1000.0, 1.0),
};
Self {
next: control_points.next(),
control_points,
beat_len,
speed_mult,
}
}
#[inline]
pub(crate) fn update(&mut self, time: f32) {
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
match next {
ControlPoint::Timing { beat_len, .. } => {
self.beat_len = *beat_len;
self.speed_mult = 1.0;
}
ControlPoint::Difficulty { speed_mult, .. } => self.speed_mult = *speed_mult,
}
self.next = self.control_points.next();
}
}
}
+2
View File
@@ -6,7 +6,9 @@ pub mod taiko;
mod curve;
mod math_util;
mod mods;
pub use mods::Mods;
pub use parse::*;
#[inline]
View File
+2 -1
View File
@@ -49,7 +49,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
scaling_factor *= 1.0 + small_circle_bonus;
}
let mut slider_state = SliderState::new(&map);
let mut slider_state = SliderState::new(map);
let mut ticks_buf = Vec::new();
let mut hit_objects = map.hit_objects.iter().map(|h| {
@@ -147,6 +147,7 @@ mod tests {
use std::fs::File;
#[test]
#[ignore]
fn no_leniency_single_stars() {
let file = match File::open("./test/70090.osu") {
Ok(file) => file,
+3 -1
View File
@@ -45,7 +45,9 @@ impl OsuObject {
let mut end_pos = h.pos;
let mut travel_dist = 0.0;
slider_state.update(h.start_time); // Responsible for timing point values
// Responsible for timing point values
slider_state.update(h.start_time);
let approx_follow_circle_radius = radius * 3.0;
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::Mods;
use crate::Mods;
#[derive(Clone, Debug)]
pub struct BeatmapAttributes {
+1 -3
View File
@@ -3,7 +3,6 @@ mod control_point;
mod error;
mod hitobject;
mod hitsound;
mod mods;
mod pos2;
mod sort;
@@ -12,7 +11,6 @@ pub use control_point::{DifficultyPoint, TimingPoint};
pub use error::{ParseError, ParseResult};
pub use hitobject::{HitObject, HitObjectKind};
pub use hitsound::HitSound;
pub use mods::Mods;
pub use pos2::Pos2;
use sort::sort;
@@ -407,7 +405,7 @@ mod tests {
use std::fs::File;
#[test]
fn parsing_works() {
fn parsing() {
let file = match File::open("E:/Games/osu!/beatmaps/2223745.osu") {
Ok(file) => file,
Err(why) => panic!("Could not read file: {}", why),
+25 -25
View File
@@ -13,7 +13,7 @@ struct MapResult {
#[test]
fn fruits() {
let star_margin = 0.005;
let pp_margin = 0.005;
let pp_margin = 0.015;
for result in RESULTS {
let MapResult {
@@ -64,147 +64,147 @@ const RESULTS: &[MapResult] = &[
map_id: 1977380,
mods: 256,
stars: 2.0564713386286573,
pp: 43.49758286973066,
pp: 45.1590377326849,
},
MapResult {
map_id: 1977380,
mods: 0,
stars: 2.5695489769068742,
pp: 71.54271564752817,
pp: 67.73537806280385,
},
MapResult {
map_id: 1977380,
mods: 8,
stars: 2.5695489769068742,
pp: 78.29197525261374,
pp: 81.28245367536461,
},
MapResult {
map_id: 1977380,
mods: 64,
stars: 3.589887228221038,
pp: 135.95326950246636,
pp: 141.14620680718699,
},
MapResult {
map_id: 1977380,
mods: 16,
stars: 3.1515873669521928,
pp: 108.02360048697571,
pp: 112.14972254944568,
},
MapResult {
map_id: 1977380,
mods: 2,
stars: 3.0035260129778396,
pp: 98.10009237095251,
pp: 101.84717128368449,
},
// -----
MapResult {
map_id: 1974968,
mods: 256,
stars: 1.9544305373156605,
pp: 40.46051204584743,
pp: 42.91338693937752,
},
MapResult {
map_id: 1974968,
mods: 0,
stars: 2.521701539665241,
pp: 64.28153872477789,
pp: 68.1785376623302,
},
MapResult {
map_id: 1974968,
mods: 8,
stars: 2.521701539665241,
pp: 81.9589618740918,
pp: 86.927635519471,
},
MapResult {
map_id: 1974968,
mods: 64,
stars: 3.650649037957456,
pp: 131.5628579590708,
pp: 139.53871429136828,
},
MapResult {
map_id: 1974968,
mods: 16,
stars: 3.566302788963401,
pp: 135.59111737415918,
pp: 143.81118258776544,
},
MapResult {
map_id: 1974968,
mods: 2,
stars: 2.2029392066882654,
pp: 53.2211645832911,
pp: 56.44764027057014,
},
// -----
MapResult {
map_id: 2420076,
mods: 256,
stars: 4.791039358886245,
pp: 226.85533170425614,
pp: 258.46694642171224,
},
MapResult {
map_id: 2420076,
mods: 0,
stars: 6.223136555625056,
pp: 413.51912544400295,
pp: 471.1417837859138,
},
MapResult {
map_id: 2420076,
mods: 8,
stars: 6.223136555625056,
pp: 440.3978626824246,
pp: 501.7659929922609,
},
MapResult {
map_id: 2420076,
mods: 64,
stars: 8.908315960310958,
pp: 999.4280253427237,
pp: 1138.695343583009,
},
MapResult {
map_id: 2420076,
mods: 16,
stars: 6.54788067620051,
pp: 466.3097817709075,
pp: 531.2886608194283,
},
MapResult {
map_id: 2420076,
mods: 2,
stars: 6.067971540209479,
pp: 392.2324532647843,
pp: 446.888877247154,
},
// -----
MapResult {
map_id: 2206596,
mods: 256,
stars: 4.767182611189798,
pp: 227.40643918013868,
pp: 300.15942914986067,
},
MapResult {
map_id: 2206596,
mods: 0,
stars: 6.157660207091584,
pp: 402.3258172661857,
pp: 531.0398776668264,
},
MapResult {
map_id: 2206596,
mods: 8,
stars: 6.157660207091584,
pp: 434.5118711368466,
pp: 573.5230526869998,
},
MapResult {
map_id: 2206596,
mods: 64,
stars: 8.93391286552717,
pp: 996.4288537655079,
pp: 1315.2112887084272,
},
MapResult {
map_id: 2206596,
mods: 16,
stars: 6.8639096665110735,
pp: 518.8398368985938,
pp: 684.8296373011866,
},
MapResult {
map_id: 2206596,
mods: 2,
stars: 5.60279198088948,
pp: 339.327091261929,
pp: 447.8862884246722,
},
];