big first commit
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
use parse::Pos2;
|
||||
|
||||
const PLAYFIELD_WIDHT: f32 = 512.0;
|
||||
const BASE_SPEED: f32 = 1.0;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CatchObject {
|
||||
pub(crate) pos: f32,
|
||||
pub(crate) time: f32,
|
||||
|
||||
pub(crate) hyper_dash: bool,
|
||||
pub(crate) hyper_dist: f32,
|
||||
}
|
||||
|
||||
impl CatchObject {
|
||||
#[inline]
|
||||
pub(crate) fn new((pos, time): (Pos2, f32)) -> Self {
|
||||
Self {
|
||||
pos: pos.x,
|
||||
time,
|
||||
hyper_dash: false,
|
||||
hyper_dist: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_hr(mut self, last_pos: &mut Option<f32>, last_time: &mut f32) -> Self {
|
||||
let mut offset_pos = self.pos;
|
||||
let time_diff = self.time - *last_time;
|
||||
|
||||
if let Some(last_pos_ref) = last_pos.filter(|_| time_diff <= 1000.0) {
|
||||
let pos_diff = offset_pos - last_pos_ref;
|
||||
|
||||
if pos_diff.abs() > f32::EPSILON {
|
||||
if pos_diff.abs() < (time_diff / 3.0).floor() {
|
||||
if pos_diff > 0.0 {
|
||||
if offset_pos + pos_diff < PLAYFIELD_WIDHT {
|
||||
offset_pos += pos_diff;
|
||||
}
|
||||
} else if offset_pos + pos_diff > 0.0 {
|
||||
offset_pos += pos_diff;
|
||||
}
|
||||
}
|
||||
|
||||
last_pos.replace(offset_pos);
|
||||
*last_time = self.time;
|
||||
}
|
||||
|
||||
self.pos = offset_pos;
|
||||
} else {
|
||||
last_pos.replace(offset_pos);
|
||||
*last_time = self.time;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn init_hyper_dash(
|
||||
&mut self,
|
||||
half_catcher_width: f32,
|
||||
next: &CatchObject,
|
||||
last_direction: &mut i8,
|
||||
last_excess: &mut f32,
|
||||
) {
|
||||
let next_x = next.pos;
|
||||
let curr_x = self.pos;
|
||||
|
||||
let this_direction = (next_x > curr_x) as i8 * 2 - 1;
|
||||
let time_to_next = next.time - self.time - 1000.0 / 60.0 / 4.0;
|
||||
|
||||
let sub = if *last_direction == this_direction {
|
||||
*last_excess
|
||||
} else {
|
||||
half_catcher_width
|
||||
};
|
||||
|
||||
let dist_to_next = (next_x - curr_x).abs() - sub;
|
||||
let hyper_dist = time_to_next * BASE_SPEED - dist_to_next;
|
||||
|
||||
if hyper_dist < 0.0 {
|
||||
self.hyper_dash = true;
|
||||
*last_excess = half_catcher_width;
|
||||
} else {
|
||||
self.hyper_dist = hyper_dist;
|
||||
*last_excess = hyper_dist.max(0.0).min(half_catcher_width);
|
||||
}
|
||||
|
||||
*last_direction = this_direction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use super::math_util;
|
||||
|
||||
use parse::Pos2;
|
||||
|
||||
const SLIDER_QUALITY: f32 = 50.0;
|
||||
|
||||
pub(crate) enum Points {
|
||||
Single(Pos2),
|
||||
Multi(Vec<Pos2>),
|
||||
}
|
||||
|
||||
pub(crate) enum Curve {
|
||||
Linear {
|
||||
a: Pos2,
|
||||
b: Pos2,
|
||||
},
|
||||
Bezier(Points),
|
||||
Catmull(Points),
|
||||
Perfect {
|
||||
origin: Pos2,
|
||||
cx: f32,
|
||||
cy: f32,
|
||||
radius: f32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
pub(crate) fn linear(a: Pos2, b: Pos2) -> Self {
|
||||
Self::Linear { a, b }
|
||||
}
|
||||
|
||||
pub(crate) fn bezier(points: &[Pos2]) -> Self {
|
||||
if points.len() == 1 {
|
||||
return Self::Bezier(Points::Single(points[0]));
|
||||
}
|
||||
|
||||
let mut start = 0;
|
||||
let mut end = 0;
|
||||
let mut result = Vec::with_capacity(4);
|
||||
|
||||
for i in 0..points.len() - 1 {
|
||||
if end - start > 1 && points[i] == points[end - 1] {
|
||||
Self::_bezier(&mut result, &points[start..end]);
|
||||
start = end;
|
||||
}
|
||||
|
||||
end += 1;
|
||||
}
|
||||
|
||||
Self::_bezier(&mut result, &points[start..end + 1]);
|
||||
|
||||
Self::Bezier(Points::Multi(result))
|
||||
}
|
||||
|
||||
fn _bezier(result: &mut Vec<Pos2>, points: &[Pos2]) {
|
||||
let step = 0.25 / SLIDER_QUALITY / points.len() as f32;
|
||||
let mut i = 0.0;
|
||||
let n = points.len() as i32 - 1;
|
||||
|
||||
while i < 1.0 + step {
|
||||
let point = (0..=n).fold(Pos2 { x: 0.0, y: 0.0 }, |point, p| {
|
||||
let factor = math_util::cpn(p, n) * (1.0 - i).powi(n - p) * i.powi(p);
|
||||
|
||||
point + points[p as usize] * factor
|
||||
});
|
||||
|
||||
result.push(point);
|
||||
i += step;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn catmull(points: &[Pos2]) -> Self {
|
||||
if points.len() == 1 {
|
||||
return Self::Catmull(Points::Single(points[0]));
|
||||
}
|
||||
|
||||
let order = points.len();
|
||||
let step = 2.5 / SLIDER_QUALITY;
|
||||
let target = step + 1.0;
|
||||
|
||||
let mut resulting_points = Vec::with_capacity(4);
|
||||
|
||||
for x in 0..order - 1 {
|
||||
let mut t = 0.0;
|
||||
|
||||
while t < target {
|
||||
let v1 = if x >= 1 { points[x - 1] } else { points[x] };
|
||||
let v2 = points[x];
|
||||
|
||||
let v3 = if x + 1 < order {
|
||||
points[x + 1]
|
||||
} else {
|
||||
v2.add_scaled(v2.add_scaled(v1, -1.0), 1.0)
|
||||
};
|
||||
|
||||
let v4 = if x + 2 < order {
|
||||
points[x + 2]
|
||||
} else {
|
||||
v3.add_scaled(v3.add_scaled(v2, -1.0), 1.0)
|
||||
};
|
||||
|
||||
let point = Self::catmull_point(v1, v2, v3, v4, t);
|
||||
resulting_points.push(point);
|
||||
t += step;
|
||||
}
|
||||
}
|
||||
|
||||
Self::Catmull(Points::Multi(resulting_points))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn catmull_point(p0: Pos2, p1: Pos2, p2: Pos2, p3: Pos2, len: f32) -> Pos2 {
|
||||
Pos2 {
|
||||
x: math_util::catmull(p0.x, p1.x, p2.x, p3.x, len),
|
||||
y: math_util::catmull(p0.y, p1.y, p2.y, p3.y, len),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn perfect(points: &[Pos2]) -> Self {
|
||||
let (cx, cy, mut radius) = math_util::get_circum_circle(&points);
|
||||
radius *= ((!math_util::is_left(&points)) as i8 * 2 - 1) as f32;
|
||||
|
||||
Self::Perfect {
|
||||
origin: points[0],
|
||||
cx,
|
||||
cy,
|
||||
radius,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn point_at_distance(&self, len: f32) -> Pos2 {
|
||||
let points = match self {
|
||||
Self::Bezier(points) => points,
|
||||
Self::Catmull(points) => points,
|
||||
Self::Linear { a, b } => return math_util::point_on_line(*a, *b, len),
|
||||
Self::Perfect {
|
||||
origin,
|
||||
cx,
|
||||
cy,
|
||||
radius,
|
||||
} => return math_util::rotate(*cx, *cy, *origin, len / *radius),
|
||||
};
|
||||
|
||||
match points {
|
||||
Points::Single(point) => *point,
|
||||
Points::Multi(points) => math_util::point_at_distance(points, len),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use super::CatchObject;
|
||||
|
||||
const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0;
|
||||
|
||||
pub(crate) struct DifficultyObject<'o> {
|
||||
pub(crate) base: &'o CatchObject,
|
||||
pub(crate) last: &'o CatchObject,
|
||||
|
||||
pub(crate) delta: f32,
|
||||
|
||||
pub(crate) normalized_pos: f32,
|
||||
pub(crate) last_normalized_pos: f32,
|
||||
|
||||
pub(crate) strain_time: f32,
|
||||
pub(crate) clock_rate: f32,
|
||||
}
|
||||
|
||||
impl<'o> DifficultyObject<'o> {
|
||||
#[inline]
|
||||
pub(crate) fn new(
|
||||
base: &'o CatchObject,
|
||||
last: &'o CatchObject,
|
||||
half_catcher_width: f32,
|
||||
clock_rate: f32,
|
||||
) -> Self {
|
||||
let delta = (base.time - last.time) / clock_rate;
|
||||
let strain_time = delta.max(40.0);
|
||||
|
||||
let scaling_factor = NORMALIZED_HITOBJECT_RADIUS / half_catcher_width;
|
||||
let normalized_pos = base.pos * scaling_factor;
|
||||
let last_normalized_pos = last.pos * scaling_factor;
|
||||
|
||||
Self {
|
||||
base,
|
||||
last,
|
||||
delta,
|
||||
normalized_pos,
|
||||
last_normalized_pos,
|
||||
strain_time,
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
mod catch_object;
|
||||
mod curve;
|
||||
mod difficulty_object;
|
||||
mod math_util;
|
||||
mod movement;
|
||||
|
||||
use catch_object::CatchObject;
|
||||
use curve::Curve;
|
||||
use difficulty_object::DifficultyObject;
|
||||
use movement::Movement;
|
||||
|
||||
use parse::{Beatmap, HitObjectKind, Mods, PathType};
|
||||
use std::cmp::Ordering;
|
||||
use std::convert::identity;
|
||||
|
||||
const SECTION_LENGTH: f32 = 750.0;
|
||||
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))
|
||||
};
|
||||
}
|
||||
|
||||
/// Star calculation for osu!ctb maps
|
||||
// Slider parsing based on https://github.com/osufx/catch-the-pp
|
||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
||||
if map.hit_objects.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let attributes = map.attributes().mods(mods);
|
||||
let with_hr = mods.hr();
|
||||
let mut ticks = Vec::new(); // using the same buffer for all sliders
|
||||
|
||||
// BUG: Incorrect object order on 2B maps that have fruits within sliders
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.scan((None, 0.0), |(last_pos, last_time), h| match &h.kind {
|
||||
HitObjectKind::Circle => {
|
||||
let mut h = CatchObject::new((h.pos, h.start_time));
|
||||
|
||||
if with_hr {
|
||||
h = h.with_hr(last_pos, last_time);
|
||||
}
|
||||
|
||||
Some(Some(FruitOrJuice::Fruit(Some(h))))
|
||||
}
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
repeats,
|
||||
curve_points,
|
||||
path_type,
|
||||
} => {
|
||||
// HR business
|
||||
last_pos
|
||||
.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)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
|
||||
PathType::Bezier
|
||||
} else if curve_points.len() == 2 {
|
||||
PathType::Linear
|
||||
} else {
|
||||
*path_type
|
||||
};
|
||||
|
||||
let curve = match path_type {
|
||||
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
|
||||
PathType::Bezier => Curve::bezier(curve_points),
|
||||
PathType::Catmull => Curve::catmull(curve_points),
|
||||
PathType::PerfectCurve => Curve::perfect(curve_points),
|
||||
};
|
||||
|
||||
let mut current_distance = tick_distance;
|
||||
let time_add = duration * (tick_distance / (*pixel_len * *repeats as f32));
|
||||
|
||||
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);
|
||||
|
||||
ticks.push((pos, h.start_time + time_add * (ticks.len() + 1) as f32));
|
||||
current_distance += tick_distance;
|
||||
}
|
||||
|
||||
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
|
||||
slider_objects.push((h.pos, h.start_time));
|
||||
|
||||
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 {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
||||
let pos = curve.point_at_distance(dist_end);
|
||||
slider_objects.push((pos, h.start_time + duration));
|
||||
|
||||
let iter = slider_objects.into_iter().map(CatchObject::new);
|
||||
|
||||
Some(Some(FruitOrJuice::Juice(iter)))
|
||||
}
|
||||
HitObjectKind::Spinner { .. } | HitObjectKind::Hold { .. } => Some(None),
|
||||
})
|
||||
.filter_map(identity)
|
||||
.flatten();
|
||||
|
||||
// Hyper dash business
|
||||
let half_catcher_width = calculate_catch_width(attributes.cs) / 2.0 / ALLOWED_CATCH_RANGE;
|
||||
let mut last_direction = 0;
|
||||
let mut last_excess = half_catcher_width;
|
||||
|
||||
// Strain business
|
||||
let mut movement = Movement::new(attributes.cs);
|
||||
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
||||
let mut current_section_end =
|
||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
let mut curr = hit_objects.next().unwrap();
|
||||
|
||||
prev.init_hyper_dash(
|
||||
half_catcher_width,
|
||||
&curr,
|
||||
&mut last_direction,
|
||||
&mut last_excess,
|
||||
);
|
||||
|
||||
for next in hit_objects {
|
||||
curr.init_hyper_dash(
|
||||
half_catcher_width,
|
||||
&next,
|
||||
&mut last_direction,
|
||||
&mut last_excess,
|
||||
);
|
||||
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
movement.half_catcher_width,
|
||||
attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
movement.save_current_peak();
|
||||
movement.start_new_section_from(current_section_end);
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
movement.process(&h);
|
||||
|
||||
prev = curr;
|
||||
curr = next;
|
||||
}
|
||||
|
||||
// Same as in loop but without init_hyper_dash because `curr` is the last element
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
movement.half_catcher_width,
|
||||
attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
movement.save_current_peak();
|
||||
movement.start_new_section_from(current_section_end);
|
||||
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
movement.process(&h);
|
||||
movement.save_current_peak();
|
||||
|
||||
movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn calculate_catch_width(cs: f32) -> f32 {
|
||||
let scale = 1.0 - 0.7 * (cs - 5.0) / 5.0;
|
||||
|
||||
CATCHER_SIZE * scale.abs() * ALLOWED_CATCH_RANGE
|
||||
}
|
||||
|
||||
enum FruitOrJuice<I> {
|
||||
Fruit(Option<CatchObject>),
|
||||
Juice(I),
|
||||
}
|
||||
|
||||
impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
|
||||
type Item = CatchObject;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
Self::Fruit(fruit) => fruit.take(),
|
||||
Self::Juice(slider) => slider.next(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self {
|
||||
Self::Fruit(Some(_)) => (1, Some(1)),
|
||||
Self::Fruit(None) => (0, Some(0)),
|
||||
Self::Juice(slider) => slider.size_hint(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs::File;
|
||||
|
||||
#[test]
|
||||
fn test_single() {
|
||||
let map_id = 1972149;
|
||||
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 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 mods = 0;
|
||||
let stars = stars(&map, mods);
|
||||
|
||||
println!("Stars: {} [map={} | mods={}]", stars, map_id, mods);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fruits() {
|
||||
let margin = 0.005;
|
||||
|
||||
#[rustfmt::skip]
|
||||
let data = vec![
|
||||
(1977380, 1 << 8, 2.0564713386286573),// HT
|
||||
(1977380, 0, 2.5695489769068742), // NM
|
||||
(1977380, 1 << 6, 3.589887228221038), // DT
|
||||
(1977380, 1 << 4, 3.1515873669521928),// HR
|
||||
(1977380, 1 << 1, 3.0035260129778396),// EZ
|
||||
|
||||
(1974968, 1 << 8, 1.9544305373156605),// HT
|
||||
(1974968, 0, 2.521701539665241), // NM
|
||||
(1974968, 1 << 6, 3.650649037957456), // DT
|
||||
(1974968, 1 << 4, 3.566302788963401), // HR
|
||||
(1974968, 1 << 1, 2.2029392066882654),// EZ
|
||||
|
||||
(2420076, 1 << 8, 4.791039358886245), // HT
|
||||
(2420076, 0, 6.223136555625056), // NM
|
||||
(2420076, 1 << 6, 8.908315960310958), // DT
|
||||
(2420076, 1 << 4, 6.54788067620051), // HR
|
||||
(2420076, 1 << 1, 6.067971540209479), // EZ
|
||||
|
||||
(2206596, 1 << 8, 4.767182611189798), // HT
|
||||
(2206596, 0, 6.157660207091584), // NM
|
||||
(2206596, 1 << 6, 8.93391286552717), // DT
|
||||
(2206596, 1 << 4, 6.8639096665110735),// HR
|
||||
(2206596, 1 << 1, 5.60279198088948), // EZ
|
||||
|
||||
// Super long juice stream towards end
|
||||
// (1972149, 1 << 8, 4.671425766413811), // HT
|
||||
// (1972149, 0, 6.043742871084152), // NM
|
||||
// (1972149, 1 << 6, 8.469259368304225), // DT
|
||||
// (1972149, 1 << 4, 6.81222485322862), // HR
|
||||
// (1972149, 1 << 1, 5.289343020686747), // EZ
|
||||
|
||||
// Convert slider fiesta
|
||||
// (1657535, 1 << 8, 3.862453635711741), // HT
|
||||
// (1657535, 0, 4.792543335869686), // NM
|
||||
// (1657535, 1 << 6, 6.655478646330863), // DT
|
||||
// (1657535, 1 << 4, 5.259728567781568), // HR
|
||||
// (1657535, 1 << 1, 4.127535166776765), // EZ
|
||||
];
|
||||
|
||||
for (map_id, mods, expected_stars) in data {
|
||||
let file = match File::open(format!("./test/{}.osu", map_id)) {
|
||||
Ok(file) => file,
|
||||
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
|
||||
};
|
||||
|
||||
let map = match Beatmap::parse(file) {
|
||||
Ok(map) => map,
|
||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
||||
};
|
||||
|
||||
let stars = stars(&map, mods);
|
||||
|
||||
assert!(
|
||||
(stars - expected_stars).abs() < margin,
|
||||
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
|
||||
stars,
|
||||
expected_stars,
|
||||
(stars - expected_stars).abs(),
|
||||
map_id,
|
||||
mods
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
use parse::Pos2;
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn cpn(mut p: i32, n: i32) -> f32 {
|
||||
if p < 0 || p > n {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
p = p.min(n - p);
|
||||
let mut out = 1.0;
|
||||
|
||||
for i in 1..=p {
|
||||
out *= (n - p + i) as f32 / i as f32;
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
|
||||
0.5 * ((2.0 * p1)
|
||||
+ (-p0 + p2) * t
|
||||
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t.powi(2)
|
||||
+ (-p0 + 3.0 * p1 - 3.0 * p2 + p3 * t.powi(3)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn point_on_line(p1: Pos2, p2: Pos2, len: f32) -> Pos2 {
|
||||
let mut full_len = ((p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sqrt();
|
||||
let n = full_len - len;
|
||||
|
||||
if full_len.abs() < f32::EPSILON {
|
||||
full_len = 1.0;
|
||||
}
|
||||
|
||||
(p1 * n + p2 * len) / full_len
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn angle_from_points(p0: Pos2, p1: Pos2) -> f32 {
|
||||
(p1.y - p0.y).atan2(p1.x - p0.x)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn distance_from_points(arr: &[Pos2]) -> f32 {
|
||||
arr.iter()
|
||||
.skip(1)
|
||||
.zip(arr.iter())
|
||||
.map(|(curr, prev)| curr.distance(prev))
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn cart_from_pol(r: f32, t: f32) -> Pos2 {
|
||||
Pos2 {
|
||||
x: r * t.cos(),
|
||||
y: r * t.sin(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn point_at_distance(array: &[Pos2], distance: f32) -> Pos2 {
|
||||
if array.len() < 2 {
|
||||
return Pos2 { x: 0.0, y: 0.0 };
|
||||
} else if distance.abs() < f32::EPSILON {
|
||||
return array[0];
|
||||
} else if distance_from_points(array) <= distance {
|
||||
return array[array.len() - 1];
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
let mut current_distance = 0.0;
|
||||
let mut new_distance = 0.0;
|
||||
|
||||
while i < array.len() - 2 {
|
||||
new_distance = (array[i] - array[i + 1]).length();
|
||||
current_distance += new_distance;
|
||||
|
||||
if distance <= current_distance {
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
current_distance -= new_distance;
|
||||
|
||||
if (distance - current_distance).abs() <= f32::EPSILON {
|
||||
array[i]
|
||||
} else {
|
||||
let angle = angle_from_points(array[i], array[i + 1]);
|
||||
let cart = cart_from_pol(distance - current_distance, angle);
|
||||
|
||||
if array[i].x > array[i + 1].x {
|
||||
array[i] - cart
|
||||
} else {
|
||||
array[i] + cart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_circum_circle(p: &[Pos2]) -> (f32, f32, f32) {
|
||||
let d = 2.0
|
||||
* (p[0].x * (p[1].y - p[2].y) + p[1].x * (p[2].y - p[0].y) + p[2].x * (p[0].y - p[1].y));
|
||||
|
||||
let p0 = p[0].x * p[0].x + p[0].y * p[0].y;
|
||||
let p1 = p[1].x * p[1].x + p[1].y * p[1].y;
|
||||
let p2 = p[2].x * p[2].x + p[2].y * p[2].y;
|
||||
|
||||
let ux = (p0 * (p[1].y - p[2].y) + p1 * (p[2].y - p[0].y) + p2 * (p[0].y - p[1].y)) / d;
|
||||
let uy = (p0 * (p[2].x - p[1].x) + p1 * (p[0].x - p[2].x) + p2 * (p[1].x - p[0].x)) / d;
|
||||
|
||||
let px = ux - p[0].x;
|
||||
let py = uy - p[0].y;
|
||||
let r = (px * px + py * py).sqrt();
|
||||
|
||||
(ux, uy, r)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_left(p: &[Pos2]) -> bool {
|
||||
((p[1].x - p[0].x) * (p[2].y - p[0].y) - (p[1].y - p[0].y) * (p[2].x - p[0].x)) < 0.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn rotate(cx: f32, cy: f32, p: Pos2, radians: f32) -> Pos2 {
|
||||
let cos = radians.cos();
|
||||
let sin = radians.sin();
|
||||
|
||||
Pos2 {
|
||||
x: (cos * (p.x - cx)) - (sin * (p.y - cy)) + cx,
|
||||
y: (sin * (p.x - cx)) + (cos * (p.y - cy)) + cy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use super::DifficultyObject;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0;
|
||||
const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0;
|
||||
const POSITION_EPSILON: f32 = NORMALIZED_HITOBJECT_RADIUS - ABSOLUTE_PLAYER_POSITIONING_ERROR;
|
||||
const DIRECTION_CHANGE_BONUS: f32 = 21.0;
|
||||
const SKILL_MULTIPLIER: f32 = 900.0;
|
||||
const STRAIN_DECAY_BASE: f32 = 0.2;
|
||||
const DECAY_WEIGHT: f32 = 0.94;
|
||||
|
||||
pub(crate) struct Movement {
|
||||
pub(crate) half_catcher_width: f32,
|
||||
|
||||
last_player_position: Option<f32>,
|
||||
last_distance_moved: f32,
|
||||
last_strain_time: f32,
|
||||
|
||||
current_strain: f32,
|
||||
current_section_peak: f32,
|
||||
|
||||
strain_peaks: Vec<f32>,
|
||||
prev_time: Option<f32>,
|
||||
}
|
||||
|
||||
impl Movement {
|
||||
#[inline]
|
||||
pub(crate) fn new(cs: f32) -> Self {
|
||||
let mut half_catcher_width = super::calculate_catch_width(cs) * 0.5;
|
||||
half_catcher_width *= 1.0 - ((cs - 5.5).max(0.0) * 0.0625);
|
||||
|
||||
Self {
|
||||
half_catcher_width,
|
||||
|
||||
last_player_position: None,
|
||||
last_distance_moved: 0.0,
|
||||
last_strain_time: 0.0,
|
||||
|
||||
current_strain: 1.0,
|
||||
current_section_peak: 1.0,
|
||||
|
||||
strain_peaks: Vec::with_capacity(128),
|
||||
prev_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn save_current_peak(&mut self) {
|
||||
// TODO: Remove branching
|
||||
if self.prev_time.is_some() {
|
||||
self.strain_peaks.push(self.current_section_peak);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
if let Some(prev_time) = self.prev_time {
|
||||
self.current_section_peak = self.peak_strain(time - prev_time);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process(&mut self, current: &DifficultyObject) {
|
||||
self.current_strain *= strain_decay(current.delta);
|
||||
self.current_strain += self.strain_value_of(¤t) * SKILL_MULTIPLIER;
|
||||
self.current_section_peak = self.current_strain.max(self.current_section_peak);
|
||||
self.prev_time.replace(current.base.time);
|
||||
}
|
||||
|
||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
for &strain in self.strain_peaks.iter() {
|
||||
difficulty += strain * weight;
|
||||
weight *= DECAY_WEIGHT;
|
||||
}
|
||||
|
||||
difficulty
|
||||
}
|
||||
|
||||
fn strain_value_of(&mut self, current: &DifficultyObject) -> f32 {
|
||||
let last_player_pos = self
|
||||
.last_player_position
|
||||
.unwrap_or(current.last_normalized_pos);
|
||||
|
||||
let mut pos = last_player_pos
|
||||
.max(current.normalized_pos - POSITION_EPSILON)
|
||||
.min(current.normalized_pos + POSITION_EPSILON);
|
||||
|
||||
let dist_moved = pos - last_player_pos;
|
||||
let weighted_strain_time = current.strain_time + 13.0 + (3.0 / current.clock_rate);
|
||||
|
||||
let mut dist_addition = dist_moved.abs().powf(1.3) / 510.0;
|
||||
|
||||
if dist_moved.abs() > 0.1 {
|
||||
if self.last_distance_moved.abs() > 0.1
|
||||
&& dist_moved.signum() != self.last_distance_moved.signum()
|
||||
{
|
||||
let bonus_factor = dist_moved.abs().min(50.0) / 50.0;
|
||||
let anti_flow_factor = (self.last_distance_moved.abs().min(70.0) / 70.0).max(0.38);
|
||||
|
||||
dist_addition += DIRECTION_CHANGE_BONUS / (self.last_strain_time + 16.0).sqrt()
|
||||
* bonus_factor
|
||||
* anti_flow_factor
|
||||
* (1.0 - (weighted_strain_time / 1000.0).powi(3)).max(0.0);
|
||||
}
|
||||
|
||||
dist_addition += 12.5 * dist_moved.abs().min(NORMALIZED_HITOBJECT_RADIUS * 2.0)
|
||||
/ (NORMALIZED_HITOBJECT_RADIUS * 6.0)
|
||||
/ weighted_strain_time.sqrt();
|
||||
}
|
||||
|
||||
let mut edge_dash_bonus = 0.0;
|
||||
|
||||
if current.last.hyper_dist <= 20.0 {
|
||||
if !current.last.hyper_dash {
|
||||
edge_dash_bonus += 5.7;
|
||||
} else {
|
||||
pos = current.normalized_pos;
|
||||
}
|
||||
|
||||
dist_addition *= 1.0
|
||||
+ edge_dash_bonus
|
||||
* ((20.0 - current.last.hyper_dist) / 20.0)
|
||||
* ((current.strain_time * current.clock_rate).min(265.0) / 265.0).powf(1.5);
|
||||
}
|
||||
|
||||
self.last_player_position.replace(pos);
|
||||
self.last_distance_moved = dist_moved;
|
||||
self.last_strain_time = current.strain_time;
|
||||
|
||||
dist_addition / weighted_strain_time
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
||||
self.current_strain * strain_decay(delta_time)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_decay(ms: f32) -> f32 {
|
||||
STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
||||
}
|
||||
Reference in New Issue
Block a user