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
+3 -1
View File
@@ -1,2 +1,4 @@
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
+168 -40
View File
@@ -12,7 +12,10 @@ use std::{
use futures::{future, stream::FuturesUnordered, Stream, StreamExt, TryStreamExt};
use pbr::ProgressBar;
use rosu_pp::{Beatmap, GameMode};
use serde::{Deserialize, Serialize};
use serde::{
de::{SeqAccess, Visitor},
Deserialize, Deserializer, Serialize,
};
use tokio::{
fs::{self, DirEntry, File, ReadDir},
process::Command,
@@ -86,7 +89,16 @@ async fn async_main() {
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));
println!("[INFO] Calculating...");
@@ -98,9 +110,9 @@ async fn async_main() {
.scan(0, |idx, entry| {
*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)
.map(|dir_entry| async {
let dir_entry = dir_entry?;
@@ -129,7 +141,7 @@ async fn async_main() {
})
.collect::<FuturesUnordered<_>>()
.await
.try_collect::<Vec<Vec<Data>>>()
.try_collect::<Vec<Vec<SimulateData>>>()
.await;
pbr.lock().unwrap().finish_println(&format!(
@@ -139,19 +151,19 @@ async fn async_main() {
let mut mode_counts = [0; 4];
let data: Vec<Data> = match result {
let data: Vec<SimulateData> = match result {
Ok(data) => data
.into_iter()
.map(|d| {
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()
})
.flatten()
.collect(),
Err(err) => return print_err(err.into()),
Err(err) => return print_err(err),
};
println!(
@@ -189,7 +201,7 @@ async fn handle_map(
map_id: u32,
path: PathBuf,
perf_calc_path: &str,
) -> Result<Vec<Data>, Error> {
) -> Result<Vec<SimulateData>, Error> {
let (mods, mode_str) = match mode {
GameMode::STD => (OSU_MODS, OSU),
GameMode::TKO => (TAIKO_MODS, TAIKO),
@@ -217,7 +229,7 @@ async fn handle_map(
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,
Err(_) => {
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);
}
@@ -288,47 +300,163 @@ impl From<serde_json::Error> for Error {
}
}
#[derive(Debug, Deserialize, Serialize)]
struct Data {
mode: u32,
map_id: u32,
#[serde(flatten)]
inner: GenericData,
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct SimulateData {
score: Score,
#[serde(alias = "performance_attributes")]
performance: Performance,
#[serde(alias = "difficulty_attributes")]
difficulty: Difficulty,
}
impl Data {
fn new(mode: u32, map_id: u32, inner: GenericData) -> Self {
Self {
mode,
map_id,
inner,
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Score {
#[serde(alias = "ruleset_id")]
mode: u32,
#[serde(alias = "beatmap_id")]
map_id: u32,
#[serde(alias = "beatmap", skip_serializing)]
_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,
}
fn deserialize_mods<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
d.deserialize_seq(ModVisitor)
}
struct ModVisitor;
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)]
struct GenericData {
#[serde(default, alias = "Aim", skip_serializing_if = "Option::is_none")]
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
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>,
#[serde(default, alias = "Speed", skip_serializing_if = "Option::is_none")]
#[serde(default, skip_serializing_if = "Option::is_none")]
speed: Option<f64>,
#[serde(default, alias = "Accuracy", skip_serializing_if = "Option::is_none")]
accuracy: Option<f64>,
#[serde(default, alias = "Flashlight", skip_serializing_if = "Option::is_none")]
#[serde(alias = "accuracy", default, skip_serializing_if = "Option::is_none")]
acc: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
flashlight: Option<f64>,
#[serde(default, alias = "Strain", skip_serializing_if = "Option::is_none")]
strain: Option<f64>,
#[serde(default, alias = "OD", skip_serializing_if = "Option::is_none")]
od: Option<f64>,
#[serde(default, alias = "AR", skip_serializing_if = "Option::is_none")]
ar: Option<f64>,
#[serde(alias = "Mods")]
mods: String,
#[serde(alias = "Stars")]
stars: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
effective_miss_count: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
scaled_score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
difficulty: Option<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 {
inner: ReadDir,
}
+89 -47
View File
@@ -28,7 +28,8 @@ async fn async_main() {
println!("Deserializing data from output.json...");
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!(
"Calculating values for {} map-mod combinations...",
@@ -39,10 +40,10 @@ async fn async_main() {
let result = data
.into_iter()
.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 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);
Ok::<_, Error>((data, attrs, mods))
@@ -67,7 +68,7 @@ async fn async_main() {
];
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 {
@@ -78,8 +79,6 @@ async fn async_main() {
print_err(err);
}
}
println!("Done");
}
/// Mode specific evaluator containing differences
@@ -109,61 +108,62 @@ impl Evaluator {
/// For all mode-specific data points, calculate the
/// 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.stars.push(difference(data.inner.stars, attrs.stars()));
self.pp.push(difference(data.inner.pp, attrs.pp()));
self.stars
.push(difference(data.difficulty.stars, attrs.stars()));
self.pp.push(difference(data.performance.pp, attrs.pp()));
match attrs {
PerformanceAttributes::Fruits(_) => {}
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 entry = difference(acc, attrs.pp_acc);
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 entry = difference(strain, attrs.pp_strain);
values.push(entry);
}
}
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 entry = difference(acc, attrs.pp_acc);
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 entry = difference(aim, attrs.pp_aim);
values.push(entry);
}
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 entry = difference(flashlight, attrs.pp_flashlight);
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 entry = difference(speed, attrs.pp_speed);
values.push(entry);
}
}
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 entry = difference(acc, attrs.pp_acc);
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 entry = difference(strain, attrs.pp_strain);
values.push(entry);
@@ -220,7 +220,6 @@ impl Evaluator {
}
root.present()?;
println!("Finished plotting {}", mode);
Ok(())
}
@@ -295,8 +294,6 @@ impl Evaluator {
vec.reverse();
println!("---");
vec
}
}
@@ -350,12 +347,11 @@ fn difference(actual: f64, calculated: f64) -> f64 {
(actual - calculated).abs()
}
fn parse_mods(mods_str: &str) -> u32 {
fn parse_mods(mods_list: &[String]) -> u32 {
let mut mods = 0;
for m in mods_str.split(", ") {
match m {
"None" => {}
for m in mods_list {
match m.as_str() {
"NF" => mods += 1,
"EZ" => mods += 2,
"TD" => mods += 4,
@@ -382,33 +378,79 @@ fn print_err(err: Error) {
}
}
#[derive(Debug, Deserialize)]
struct Data {
mode: u32,
map_id: u32,
#[serde(flatten)]
inner: GenericData,
#[derive(Deserialize)]
struct SimulateData {
score: Score,
performance: Performance,
difficulty: Difficulty,
}
#[derive(Debug, Deserialize)]
struct GenericData {
#[serde(default, alias = "Aim")]
#[derive(Deserialize)]
struct Score {
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>,
#[serde(default, alias = "Speed")]
#[serde(default)]
speed: Option<f64>,
#[serde(default, alias = "Accuracy")]
accuracy: Option<f64>,
#[serde(default, alias = "Flashlight")]
#[serde(default)]
acc: Option<f64>,
#[serde(default)]
flashlight: Option<f64>,
#[serde(default, alias = "Strain")]
strain: Option<f64>,
#[serde(default, alias = "OD")]
od: Option<f64>,
#[serde(default, alias = "AR")]
ar: Option<f64>,
#[serde(alias = "Mods")]
mods: String,
#[serde(alias = "Stars")]
stars: f64,
#[serde(default)]
effective_miss_count: Option<f64>,
#[serde(default)]
scaled_score: Option<f64>,
#[serde(default)]
difficulty: Option<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>,
}