big first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "mania"
|
||||
version = "0.1.0"
|
||||
authors = ["MaxOhn <ohn.m@hotmail.de>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies.parse]
|
||||
path = "../parse"
|
||||
@@ -0,0 +1,149 @@
|
||||
mod strain;
|
||||
|
||||
use strain::Strain;
|
||||
|
||||
use parse::{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)]
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wack_map() {
|
||||
let file = match File::open("E:/Games/osu!/beatmaps/1443309.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, 0);
|
||||
|
||||
println!("Stars: {}", stars);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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) {
|
||||
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(¤t) * 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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user