updated pp-gen & pp-plot w.r.t. osu-tools output

This commit is contained in:
MaxOhn
2022-01-08 18:43:37 +01:00
parent 7c8fa159d3
commit 7b9bd0b1f4
3 changed files with 260 additions and 88 deletions
+2
View File
@@ -1,2 +1,4 @@
PERF_CALC_PATH="path/to/PerformanceCalculator.dll" PERF_CALC_PATH="path/to/PerformanceCalculator.dll"
MAP_PATH="path/to/.osu/files" MAP_PATH="path/to/.osu/files"
MAP_TAKE=integer, how many maps to consider
MAP_SKIP=integer, how many maps to skip inbetween
+166 -38
View File
@@ -12,7 +12,10 @@ use std::{
use futures::{future, stream::FuturesUnordered, Stream, StreamExt, TryStreamExt}; use futures::{future, stream::FuturesUnordered, Stream, StreamExt, TryStreamExt};
use pbr::ProgressBar; use pbr::ProgressBar;
use rosu_pp::{Beatmap, GameMode}; use rosu_pp::{Beatmap, GameMode};
use serde::{Deserialize, Serialize}; use serde::{
de::{SeqAccess, Visitor},
Deserialize, Deserializer, Serialize,
};
use tokio::{ use tokio::{
fs::{self, DirEntry, File, ReadDir}, fs::{self, DirEntry, File, ReadDir},
process::Command, process::Command,
@@ -86,7 +89,16 @@ async fn async_main() {
let mut output = StdFile::create("./output.json").expect("failed to create output file"); let mut output = StdFile::create("./output.json").expect("failed to create output file");
let take = 250; let take = env::var("MAP_TAKE")
.expect("missing `MAP_TAKE` environment variable")
.parse()
.expect("`MAP_TAKE` must be an integer");
let mut skip: u32 = env::var("MAP_SKIP")
.expect("missing `MAP_SKIP` environment variable")
.parse()
.expect("`MAP_SKIP` must be an integer");
skip += 1;
let pbr = Mutex::new(ProgressBar::new(take)); let pbr = Mutex::new(ProgressBar::new(take));
println!("[INFO] Calculating..."); println!("[INFO] Calculating...");
@@ -98,9 +110,9 @@ async fn async_main() {
.scan(0, |idx, entry| { .scan(0, |idx, entry| {
*idx += 1; *idx += 1;
future::ready(Some((*idx % 76 == 0).then(|| entry))) future::ready(Some((*idx % skip == 0).then(|| entry)))
}) })
.filter_map(|opt| future::ready(opt)) .filter_map(future::ready)
.take(take as usize) .take(take as usize)
.map(|dir_entry| async { .map(|dir_entry| async {
let dir_entry = dir_entry?; let dir_entry = dir_entry?;
@@ -129,7 +141,7 @@ async fn async_main() {
}) })
.collect::<FuturesUnordered<_>>() .collect::<FuturesUnordered<_>>()
.await .await
.try_collect::<Vec<Vec<Data>>>() .try_collect::<Vec<Vec<SimulateData>>>()
.await; .await;
pbr.lock().unwrap().finish_println(&format!( pbr.lock().unwrap().finish_println(&format!(
@@ -139,19 +151,19 @@ async fn async_main() {
let mut mode_counts = [0; 4]; let mut mode_counts = [0; 4];
let data: Vec<Data> = match result { let data: Vec<SimulateData> = match result {
Ok(data) => data Ok(data) => data
.into_iter() .into_iter()
.map(|d| { .map(|d| {
if let Some(data) = d.get(0) { if let Some(data) = d.get(0) {
mode_counts[data.mode as usize] += 1; mode_counts[data.score.mode as usize] += 1;
} }
d.into_iter() d.into_iter()
}) })
.flatten() .flatten()
.collect(), .collect(),
Err(err) => return print_err(err.into()), Err(err) => return print_err(err),
}; };
println!( println!(
@@ -189,7 +201,7 @@ async fn handle_map(
map_id: u32, map_id: u32,
path: PathBuf, path: PathBuf,
perf_calc_path: &str, perf_calc_path: &str,
) -> Result<Vec<Data>, Error> { ) -> Result<Vec<SimulateData>, Error> {
let (mods, mode_str) = match mode { let (mods, mode_str) = match mode {
GameMode::STD => (OSU_MODS, OSU), GameMode::STD => (OSU_MODS, OSU),
GameMode::TKO => (TAIKO_MODS, TAIKO), GameMode::TKO => (TAIKO_MODS, TAIKO),
@@ -217,7 +229,7 @@ async fn handle_map(
let output = command.output().await?; let output = command.output().await?;
let data = match serde_json::from_slice(&output.stdout) { let mut data: SimulateData = match serde_json::from_slice(&output.stdout) {
Ok(data) => data, Ok(data) => data,
Err(_) => { Err(_) => {
let content = String::from_utf8_lossy(&output.stderr); let content = String::from_utf8_lossy(&output.stderr);
@@ -230,7 +242,7 @@ async fn handle_map(
} }
}; };
let data = Data::new(mode as u32, map_id, data); data.score.map_id = map_id;
result.push(data); result.push(data);
} }
@@ -288,47 +300,163 @@ impl From<serde_json::Error> for Error {
} }
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct Data { #[serde(deny_unknown_fields)]
struct SimulateData {
score: Score,
#[serde(alias = "performance_attributes")]
performance: Performance,
#[serde(alias = "difficulty_attributes")]
difficulty: Difficulty,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Score {
#[serde(alias = "ruleset_id")]
mode: u32, mode: u32,
#[serde(alias = "beatmap_id")]
map_id: u32, map_id: u32,
#[serde(flatten)] #[serde(alias = "beatmap", skip_serializing)]
inner: GenericData, _map: String,
#[serde(deserialize_with = "deserialize_mods")]
mods: Vec<String>,
total_score: u32,
#[serde(alias = "accuracy")]
acc: f64,
combo: u32,
#[serde(alias = "statistics")]
stats: Statistics,
} }
impl Data { fn deserialize_mods<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
fn new(mode: u32, map_id: u32, inner: GenericData) -> Self { d.deserialize_seq(ModVisitor)
Self { }
mode,
map_id, struct ModVisitor;
inner,
impl<'de> Visitor<'de> for ModVisitor {
type Value = Vec<String>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a sequence of mods")
} }
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut mods = Vec::with_capacity(seq.size_hint().unwrap_or(0));
#[derive(Deserialize)]
struct Mod {
acronym: String,
}
while let Some(elem) = seq.next_element::<Mod>()? {
mods.push(elem.acronym);
}
Ok(mods)
} }
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct GenericData { #[serde(deny_unknown_fields)]
#[serde(default, alias = "Aim", skip_serializing_if = "Option::is_none")] struct Statistics {
#[serde(alias = "Perfect", default)]
perfect: usize,
#[serde(alias = "Great")]
great: usize,
#[serde(alias = "Good", alias = "SmallTickMiss", default)]
good: usize,
#[serde(alias = "Ok", alias = "LargeTickHit")]
ok: usize,
#[serde(alias = "Meh", alias = "SmallTickHit")]
meh: usize,
#[serde(alias = "Miss")]
miss: usize,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Performance {
#[serde(default, skip_serializing_if = "Option::is_none")]
aim: Option<f64>, aim: Option<f64>,
#[serde(default, alias = "Speed", skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
speed: Option<f64>, speed: Option<f64>,
#[serde(default, alias = "Accuracy", skip_serializing_if = "Option::is_none")] #[serde(alias = "accuracy", default, skip_serializing_if = "Option::is_none")]
accuracy: Option<f64>, acc: Option<f64>,
#[serde(default, alias = "Flashlight", skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
flashlight: Option<f64>, flashlight: Option<f64>,
#[serde(default, alias = "Strain", skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
strain: Option<f64>, effective_miss_count: Option<f64>,
#[serde(default, alias = "OD", skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
od: Option<f64>, scaled_score: Option<f64>,
#[serde(default, alias = "AR", skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
ar: Option<f64>, difficulty: Option<f64>,
#[serde(alias = "Mods")]
mods: String,
#[serde(alias = "Stars")]
stars: f64,
pp: f64, pp: f64,
} }
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Difficulty {
#[serde(alias = "star_rating")]
stars: f64,
max_combo: u32,
#[serde(
alias = "aim_difficulty",
default,
skip_serializing_if = "Option::is_none"
)]
aim: Option<f64>,
#[serde(
alias = "speed_difficulty",
default,
skip_serializing_if = "Option::is_none"
)]
speed: Option<f64>,
#[serde(
alias = "flashlight_difficulty",
default,
skip_serializing_if = "Option::is_none"
)]
flashlight: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
slider_factor: Option<f64>,
#[serde(
alias = "stamina_difficulty",
default,
skip_serializing_if = "Option::is_none"
)]
stamina: Option<f64>,
#[serde(
alias = "rhythm_difficulty",
default,
skip_serializing_if = "Option::is_none"
)]
rhythm: Option<f64>,
#[serde(
alias = "colour_difficulty",
default,
skip_serializing_if = "Option::is_none"
)]
colour: Option<f64>,
#[serde(
alias = "approach_rate",
default,
skip_serializing_if = "Option::is_none"
)]
ar: Option<f64>,
#[serde(
alias = "overall_difficulty",
default,
skip_serializing_if = "Option::is_none"
)]
od: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
great_hit_window: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
score_multiplier: Option<f64>,
}
struct ReadDirStream { struct ReadDirStream {
inner: ReadDir, inner: ReadDir,
} }
+89 -47
View File
@@ -28,7 +28,8 @@ async fn async_main() {
println!("Deserializing data from output.json..."); println!("Deserializing data from output.json...");
let file = StdFile::open("../pp-gen/output.json").expect("failed to open `output.json` file"); let file = StdFile::open("../pp-gen/output.json").expect("failed to open `output.json` file");
let data: Vec<Data> = serde_json::from_reader(file).expect("failed to deserialize data"); let data: Vec<SimulateData> =
serde_json::from_reader(file).expect("failed to deserialize data");
println!( println!(
"Calculating values for {} map-mod combinations...", "Calculating values for {} map-mod combinations...",
@@ -39,10 +40,10 @@ async fn async_main() {
let result = data let result = data
.into_iter() .into_iter()
.map(|data| async move { .map(|data| async move {
let path = format!("{}/{}.osu", map_path, data.map_id); let path = format!("{}/{}.osu", map_path, data.score.map_id);
let file = File::open(path).await?; let file = File::open(path).await?;
let map = Beatmap::parse(file).await?; let map = Beatmap::parse(file).await?;
let mods = parse_mods(&data.inner.mods); let mods = parse_mods(&data.score.mods);
let attrs = map.max_pp(mods); let attrs = map.max_pp(mods);
Ok::<_, Error>((data, attrs, mods)) Ok::<_, Error>((data, attrs, mods))
@@ -67,7 +68,7 @@ async fn async_main() {
]; ];
for (data, attributes, mods) in tuples { for (data, attributes, mods) in tuples {
evaluators[data.mode as usize].process(data, attributes, mods); evaluators[data.score.mode as usize].process(data, attributes, mods);
} }
for evaluator in evaluators { for evaluator in evaluators {
@@ -78,8 +79,6 @@ async fn async_main() {
print_err(err); print_err(err);
} }
} }
println!("Done");
} }
/// Mode specific evaluator containing differences /// Mode specific evaluator containing differences
@@ -109,61 +108,62 @@ impl Evaluator {
/// For all mode-specific data points, calculate the /// For all mode-specific data points, calculate the
/// differences of `data`'s value and `attrs`' value /// differences of `data`'s value and `attrs`' value
fn process(&mut self, data: Data, attrs: PerformanceAttributes, mods: u32) { fn process(&mut self, data: SimulateData, attrs: PerformanceAttributes, mods: u32) {
self.count += 1; self.count += 1;
self.stars.push(difference(data.inner.stars, attrs.stars())); self.stars
self.pp.push(difference(data.inner.pp, attrs.pp())); .push(difference(data.difficulty.stars, attrs.stars()));
self.pp.push(difference(data.performance.pp, attrs.pp()));
match attrs { match attrs {
PerformanceAttributes::Fruits(_) => {} PerformanceAttributes::Fruits(_) => {}
PerformanceAttributes::Mania(attrs) => { PerformanceAttributes::Mania(attrs) => {
if let Some(acc) = data.inner.accuracy { if let Some(acc) = data.performance.acc {
let values = self.accuracy.get_or_insert_with(Vec::new); let values = self.accuracy.get_or_insert_with(Vec::new);
let entry = difference(acc, attrs.pp_acc); let entry = difference(acc, attrs.pp_acc);
values.push(entry); values.push(entry);
} }
if let Some(strain) = data.inner.strain { if let Some(strain) = data.performance.difficulty {
let values = self.strain.get_or_insert_with(Vec::new); let values = self.strain.get_or_insert_with(Vec::new);
let entry = difference(strain, attrs.pp_strain); let entry = difference(strain, attrs.pp_strain);
values.push(entry); values.push(entry);
} }
} }
PerformanceAttributes::Osu(attrs) => { PerformanceAttributes::Osu(attrs) => {
if let Some(acc) = data.inner.accuracy { if let Some(acc) = data.performance.acc {
let values = self.accuracy.get_or_insert_with(Vec::new); let values = self.accuracy.get_or_insert_with(Vec::new);
let entry = difference(acc, attrs.pp_acc); let entry = difference(acc, attrs.pp_acc);
values.push(entry); values.push(entry);
} }
if let Some(aim) = data.inner.aim { if let Some(aim) = data.performance.aim {
let values = self.aim.get_or_insert_with(Vec::new); let values = self.aim.get_or_insert_with(Vec::new);
let entry = difference(aim, attrs.pp_aim); let entry = difference(aim, attrs.pp_aim);
values.push(entry); values.push(entry);
} }
if mods & 1024 > 0 { if mods & 1024 > 0 {
if let Some(flashlight) = data.inner.flashlight { if let Some(flashlight) = data.performance.flashlight {
let values = self.flashlight.get_or_insert_with(Vec::new); let values = self.flashlight.get_or_insert_with(Vec::new);
let entry = difference(flashlight, attrs.pp_flashlight); let entry = difference(flashlight, attrs.pp_flashlight);
values.push(entry); values.push(entry);
} }
} }
if let Some(speed) = data.inner.speed { if let Some(speed) = data.performance.speed {
let values = self.speed.get_or_insert_with(Vec::new); let values = self.speed.get_or_insert_with(Vec::new);
let entry = difference(speed, attrs.pp_speed); let entry = difference(speed, attrs.pp_speed);
values.push(entry); values.push(entry);
} }
} }
PerformanceAttributes::Taiko(attrs) => { PerformanceAttributes::Taiko(attrs) => {
if let Some(acc) = data.inner.accuracy { if let Some(acc) = data.performance.acc {
let values = self.accuracy.get_or_insert_with(Vec::new); let values = self.accuracy.get_or_insert_with(Vec::new);
let entry = difference(acc, attrs.pp_acc); let entry = difference(acc, attrs.pp_acc);
values.push(entry); values.push(entry);
} }
if let Some(strain) = data.inner.strain { if let Some(strain) = data.performance.difficulty {
let values = self.strain.get_or_insert_with(Vec::new); let values = self.strain.get_or_insert_with(Vec::new);
let entry = difference(strain, attrs.pp_strain); let entry = difference(strain, attrs.pp_strain);
values.push(entry); values.push(entry);
@@ -220,7 +220,6 @@ impl Evaluator {
} }
root.present()?; root.present()?;
println!("Finished plotting {}", mode);
Ok(()) Ok(())
} }
@@ -295,8 +294,6 @@ impl Evaluator {
vec.reverse(); vec.reverse();
println!("---");
vec vec
} }
} }
@@ -350,12 +347,11 @@ fn difference(actual: f64, calculated: f64) -> f64 {
(actual - calculated).abs() (actual - calculated).abs()
} }
fn parse_mods(mods_str: &str) -> u32 { fn parse_mods(mods_list: &[String]) -> u32 {
let mut mods = 0; let mut mods = 0;
for m in mods_str.split(", ") { for m in mods_list {
match m { match m.as_str() {
"None" => {}
"NF" => mods += 1, "NF" => mods += 1,
"EZ" => mods += 2, "EZ" => mods += 2,
"TD" => mods += 4, "TD" => mods += 4,
@@ -382,33 +378,79 @@ fn print_err(err: Error) {
} }
} }
#[derive(Debug, Deserialize)] #[derive(Deserialize)]
struct Data { struct SimulateData {
mode: u32, score: Score,
map_id: u32, performance: Performance,
#[serde(flatten)] difficulty: Difficulty,
inner: GenericData,
} }
#[derive(Debug, Deserialize)] #[derive(Deserialize)]
struct GenericData { struct Score {
#[serde(default, alias = "Aim")] mode: u32,
map_id: u32,
mods: Vec<String>,
total_score: u32,
acc: f64,
combo: u32,
stats: Statistics,
}
#[derive(Deserialize)]
struct Statistics {
#[serde(default)]
perfect: usize,
great: usize,
#[serde(default)]
good: usize,
ok: usize,
meh: usize,
miss: usize,
}
#[derive(Deserialize)]
struct Performance {
#[serde(default)]
aim: Option<f64>, aim: Option<f64>,
#[serde(default, alias = "Speed")] #[serde(default)]
speed: Option<f64>, speed: Option<f64>,
#[serde(default, alias = "Accuracy")] #[serde(default)]
accuracy: Option<f64>, acc: Option<f64>,
#[serde(default, alias = "Flashlight")] #[serde(default)]
flashlight: Option<f64>, flashlight: Option<f64>,
#[serde(default, alias = "Strain")] #[serde(default)]
strain: Option<f64>, effective_miss_count: Option<f64>,
#[serde(default, alias = "OD")] #[serde(default)]
od: Option<f64>, scaled_score: Option<f64>,
#[serde(default, alias = "AR")] #[serde(default)]
ar: Option<f64>, difficulty: Option<f64>,
#[serde(alias = "Mods")]
mods: String,
#[serde(alias = "Stars")]
stars: f64,
pp: f64, pp: f64,
} }
#[derive(Deserialize)]
struct Difficulty {
stars: f64,
max_combo: u32,
#[serde(default)]
aim: Option<f64>,
#[serde(default)]
speed: Option<f64>,
#[serde(default)]
flashlight: Option<f64>,
#[serde(default)]
slider_factor: Option<f64>,
#[serde(default)]
stamina: Option<f64>,
#[serde(default)]
rhythm: Option<f64>,
#[serde(default)]
colour: Option<f64>,
#[serde(default)]
ar: Option<f64>,
#[serde(default)]
od: Option<f64>,
#[serde(default)]
great_hit_window: Option<f64>,
#[serde(default)]
score_multiplier: Option<f64>,
}