restructured workspace into single crate

This commit is contained in:
MaxOhn
2021-01-15 22:06:51 +01:00
parent ea7ff00c0a
commit 260939a7ec
85 changed files with 777 additions and 29675 deletions
+259
View File
@@ -0,0 +1,259 @@
mod strain;
use strain::Strain;
use crate::{Beatmap, HitObject, Mods};
const SECTION_LEN: f32 = 400.0;
const STAR_SCALING_FACTOR: f32 = 0.018;
/// Star calculation for osu!mania maps
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
if map.hit_objects.is_empty() {
return 0.0;
}
let clock_rate = mods.speed();
let section_len = SECTION_LEN * clock_rate;
let mut strain = Strain::new(map.cs as u8);
let hit_objects = map
.hit_objects
.iter()
.skip(1)
.zip(map.hit_objects.iter())
.map(|(base, prev)| DifficultyHitObject::new(base, prev, map.cs, clock_rate));
// No strain for first object
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
for h in hit_objects {
while h.base.start_time > current_section_end {
strain.save_current_peak();
strain.start_new_section_from(current_section_end);
current_section_end += section_len;
}
strain.process(&h);
}
strain.save_current_peak();
strain.difficulty_value() * STAR_SCALING_FACTOR
}
#[derive(Debug)]
pub(crate) struct DifficultyHitObject<'o> {
base: &'o HitObject,
column: usize,
delta: f32,
}
impl<'o> DifficultyHitObject<'o> {
fn new(base: &'o HitObject, prev: &'o HitObject, cs: f32, clock_rate: f32) -> Self {
let x_divisor = 512.0 / cs;
let column = (base.pos.x / x_divisor).floor() as usize;
Self {
base,
column,
delta: (base.start_time - prev.start_time) / clock_rate,
}
}
}
pub struct PpResult {
pub pp: f32,
pub stars: f32,
}
pub trait PpProvider {
fn pp(&self) -> PpCalculator;
}
impl PpProvider for Beatmap {
#[inline]
fn pp(&self) -> PpCalculator {
PpCalculator::new(self)
}
}
// TODO: Allow partial plays
pub struct PpCalculator<'m> {
map: &'m Beatmap,
stars: Option<f32>,
mods: u32,
score: f32,
}
impl<'m> PpCalculator<'m> {
#[inline]
pub fn new(map: &'m Beatmap) -> Self {
Self {
map,
stars: None,
mods: 0,
score: 1_000_000.0,
}
}
#[inline]
pub fn stars(mut self, stars: f32) -> Self {
self.stars.replace(stars);
self
}
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
self
}
#[inline]
pub fn score(mut self, score: u32) -> Self {
self.score = score as f32;
self
}
pub fn calculate(self) -> PpResult {
let stars = self.stars.unwrap_or_else(|| stars(self.map, self.mods));
let ez = self.mods.ez();
let nf = self.mods.nf();
let ht = self.mods.ht();
let score_multiplier = 0.5_f32.powi(ez as i32 + nf as i32 + ht as i32);
let scaled_score = self.score / score_multiplier;
let mut multiplier = 0.8;
if nf {
multiplier *= 0.9;
}
if ez {
multiplier *= 0.5;
}
let hit_window = {
let od = (10.0 - self.map.od).max(0.0).min(10.0);
let mut val = 34.0 + 3.0 * od;
if ez {
val *= 1.4;
} else if self.mods.hr() {
val /= 1.4;
}
val * self.mods.speed()
};
let strain_value = self.compute_strain(scaled_score, stars);
let acc_value = self.compute_accuracy_value(scaled_score, strain_value, hit_window);
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
PpResult { pp, stars }
}
fn compute_strain(&self, score: f32, stars: f32) -> f32 {
let mut strain_value = (5.0 * (stars / 0.2).max(1.0) - 4.0).powf(2.2) / 135.0;
strain_value *= 1.0 + 0.1 * (self.map.hit_objects.len() as f32 / 1500.0).min(1.0);
if score <= 500_000.0 {
strain_value = 0.0;
} else if score <= 600_000.0 {
strain_value *= (score - 500_000.0) / 100_000.0 * 0.3;
} else if score <= 700_000.0 {
strain_value *= 0.3 + (score - 600_000.0) / 100_000.0 * 0.25;
} else if score <= 800_000.0 {
strain_value *= 0.55 + (score - 700_000.0) / 100_000.0 * 0.2;
} else if score <= 900_000.0 {
strain_value *= 0.75 + (score - 800_000.0) / 100_000.0 * 0.15;
} else {
strain_value *= 0.9 + (score - 900_000.0) / 100_000.0 * 0.1;
}
strain_value
}
#[inline]
fn compute_accuracy_value(&self, score: f32, strain: f32, hit_window: f32) -> f32 {
(0.2 - (hit_window - 34.0) * 0.006667).max(0.0)
* strain
* ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
fn test_single() {
let file = match File::open("E:/Games/osu!/beatmaps/1355822.osu") {
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 stars = stars(&map, 256);
println!("Stars: {}", stars);
}
#[test]
fn test_mania() {
let margin = 0.005;
#[rustfmt::skip]
let data = vec![
(1355822, 1 << 8, 2.2710870990702627), // HT
(1355822, 0, 2.7966565927524574), // NM
(1355822, 1 << 6, 3.748525363730352), // DT
(1974394, 1 << 8, 3.8736942117487256), // HT
(1974394, 0, 4.801793001581714), // NM
(1974394, 1 << 6, 6.517894438878535), // DT
(992512, 1 << 8, 5.29507262961579), // HT
(992512, 0, 6.536292432114728), // NM
(992512, 1 << 6, 8.944195050951032), // DT
];
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
);
}
}
}
+134
View File
@@ -0,0 +1,134 @@
use super::DifficultyHitObject;
use std::cmp::Ordering;
pub(crate) struct Strain {
current_strain: f32,
current_section_peak: f32,
individual_strain: f32,
overall_strain: f32,
hold_end_times: Vec<f32>,
individual_strains: Vec<f32>,
pub strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
const INDIVISUAL_DECAY_BASE: f32 = 0.125;
const OVERALL_DECAY_BASE: f32 = 0.3;
const STRAIN_DECAY_BASE: f32 = 1.0;
const SKILL_MULTIPLIER: f32 = 1.0;
const DECAY_WEIGHT: f32 = 0.9;
impl Strain {
#[inline]
pub(crate) fn new(column_count: u8) -> Self {
Self {
current_strain: 1.0,
current_section_peak: 1.0,
individual_strain: 0.0,
overall_strain: 1.0,
hold_end_times: vec![0.0; column_count as usize],
individual_strains: vec![0.0; column_count as usize],
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) = self.prev_time {
self.current_section_peak = self.peak_strain(time - prev);
}
}
#[inline]
fn peak_strain(&self, delta_time: f32) -> f32 {
apply_decay(self.individual_strain, delta_time, INDIVISUAL_DECAY_BASE)
+ apply_decay(self.overall_strain, delta_time, OVERALL_DECAY_BASE)
}
#[inline]
fn strain_decay(&self, ms: f32) -> f32 {
STRAIN_DECAY_BASE.powf(ms / 1000.0)
}
#[inline]
pub(crate) fn process(&mut self, current: &DifficultyHitObject) {
self.current_strain *= self.strain_decay(current.delta);
self.current_strain += self.strain_value_of(&current) * SKILL_MULTIPLIER;
self.current_section_peak = self.current_strain.max(self.current_section_peak);
self.prev_time.replace(current.base.start_time);
}
fn strain_value_of(&mut self, current: &DifficultyHitObject) -> f32 {
let end_time = current.base.end_time();
let mut hold_factor = 1.0;
let mut hold_addition = 0.0;
for col in 0..self.hold_end_times.len() {
let hold_end_time = self.hold_end_times[col as usize];
if end_time > hold_end_time + 1.0 {
if hold_end_time > current.base.start_time + 1.0 {
hold_addition = 1.0;
}
} else if (end_time - hold_end_time).abs() < 1.0 {
hold_addition = 0.0;
} else if end_time < hold_end_time - 1.0 {
hold_factor = 1.25;
}
self.individual_strains[col] = apply_decay(
self.individual_strains[col],
current.delta,
INDIVISUAL_DECAY_BASE,
);
}
self.hold_end_times[current.column] = end_time;
self.individual_strains[current.column] += 2.0 * hold_factor;
self.individual_strain = self.individual_strains[current.column];
self.overall_strain = apply_decay(self.overall_strain, current.delta, OVERALL_DECAY_BASE)
+ (1.0 + hold_addition) * hold_factor;
self.individual_strain + self.overall_strain - self.current_strain
}
#[inline]
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
}
}
#[inline]
fn apply_decay(value: f32, delta_time: f32, decay_base: f32) -> f32 {
value * decay_base.powf(delta_time / 1000.0)
}