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"
|
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 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)]
|
||||||
mode: u32,
|
struct SimulateData {
|
||||||
map_id: u32,
|
score: Score,
|
||||||
#[serde(flatten)]
|
#[serde(alias = "performance_attributes")]
|
||||||
inner: GenericData,
|
performance: Performance,
|
||||||
|
#[serde(alias = "difficulty_attributes")]
|
||||||
|
difficulty: Difficulty,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Data {
|
#[derive(Deserialize, Serialize)]
|
||||||
fn new(mode: u32, map_id: u32, inner: GenericData) -> Self {
|
#[serde(deny_unknown_fields)]
|
||||||
Self {
|
struct Score {
|
||||||
mode,
|
#[serde(alias = "ruleset_id")]
|
||||||
map_id,
|
mode: u32,
|
||||||
inner,
|
#[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)]
|
#[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
@@ -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>,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user