updated pp-gen & pp-plot w.r.t. osu-tools output
This commit is contained in:
+3
-1
@@ -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
@@ -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
@@ -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>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user