added inner pp-gen crate

This commit is contained in:
MaxOhn
2021-11-03 00:53:18 +01:00
parent 62d00d3619
commit b7caf35bb4
6 changed files with 339 additions and 1 deletions
+5 -1
View File
@@ -1,3 +1,7 @@
/target
Cargo.lock
/maps
/maps
/pp-gen/.env
/pp-gen/target
/pp-gen/input/*
/pp-gen/output/*
+1
View File
@@ -1,5 +1,6 @@
## Upcoming
- added internal binary crate `pp-gen` to calculate difficulty & pp values via `PerformanceCalculator.dll`
- osu: Updated up to commit [6944151486e677bfd11f2390163aca9161defbbf](https://github.com/ppy/osu/commit/6944151486e677bfd11f2390163aca9161defbbf) (2021-10-27)
# v0.2.3
+2
View File
@@ -0,0 +1,2 @@
PERF_CALC_PATH="path/to/PerformanceCalculator.dll"
MAP_PATH="path/to/.osu/files"
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "pp-gen"
version = "0.1.0"
edition = "2021"
[dependencies]
dotenv = { version = "0.15", default-features = false }
futures = { version = "0.3", default-features = false, features = ["std"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = ["std"] }
tokio = { version = "1.0", default-features = false, features = ["fs", "io-util", "process", "rt-multi-thread"] }
+70
View File
@@ -0,0 +1,70 @@
# pp-gen
Small script to calculate difficulty and performance points related values for a given list of map ids.
### How to use
- Create 4 files in `/input/`: `osu.csv`, `taiko.csv`, `catch.csv`, and `mania.csv`. For all beatmaps that you want to calculate the values for, add the map's id into the file that corresponds to the map's mode. The ids must be seperated either by comma (",") or by whitespace (" ", new line, tab).
- Rename `.env.example` to `.env` and put proper values for both of its variables:
- `PERF_CALC_PATH` is going to be something like `C:/osu-tools/PerformanceCalculator/bin/Release/net5.0/PerformanceCalculator.dll`
- `MAP_PATH` is the path to the folder containing all relevant `.osu` files that were specified in the `.csv` files
- Since `PerformanceCalculator.dll` won't print the stars for a map simulation by default, you will need to make a tiny adjustment to a file in the osu-tools repo. In `osu-tools/PerformanceCalculator/Simulate/SimulateCommand.cs`, add the following line to the other lines that look similar:
```cs
o["Stars"] = difficultyAttributes.StarRating;
```
Don't forget to recompile osu-tools via `dotnet build -c Release` after making the change.
- Run `cargo run --release`. The program will use the `PerformanceCalculator.dll` to calculate the values and then store them in `/output/{mode}.json`.
### Output format
The calculated values for each map for multiple different mod combinations will be stored in a JSON array. The array elements will be of the following form:
- osu:
```js
{
"mode": 0,
"map_id": integer,
"aim": float,
"speed": float,
"accuracy": float,
"flashlight": float,
"od": float,
"ar": float,
"mods": "HD, HR", // comma + whitespace separated string of mod abbreviations ("None" for nomod)
"stars": float,
"pp": float,
}
```
- taiko:
```js
{
"mode": 1,
"map_id": integer,
"accuracy": float,
"strain": float,
"mods": "HD, HR", // comma + whitespace separated string of mod abbreviations ("None" for nomod)
"stars": float,
"pp": float,
}
```
- catch:
```js
{
"mode": 2,
"map_id": integer,
"mods": "HD, HR", // comma + whitespace separated string of mod abbreviations ("None" for nomod)
"stars": float,
"pp": float,
}
```
- mania:
```js
{
"mode": 3,
"map_id": integer,
"accuracy": float,
"strain": float,
"mods": "HD, HR", // comma + whitespace separated string of mod abbreviations ("None" for nomod)
"stars": float,
"pp": float,
}
```
+250
View File
@@ -0,0 +1,250 @@
use std::{env, fs::File as StdFile};
use futures::{stream::FuturesUnordered, StreamExt};
use serde::{Deserialize, Serialize};
use tokio::{fs::File, io::AsyncReadExt, process::Command, runtime::Runtime};
macro_rules! info {
($($args:tt)*) => {
println!("[INFO] {}", format_args!($($args)*))
}
}
macro_rules! error {
($($args:tt)*) => {
eprintln!("[ERROR] {}", format_args!($($args)*))
}
}
const OSU_MODS: &[&[&str]] = &[
&[""],
&["hd"],
&["hr"],
&["dt"],
&["fl"],
&["ez"],
&["ht"],
&["hd", "fl"],
&["hr", "dt"],
&["ez", "dt"],
&["ht", "ez"],
&["hd", "hr", "dt"],
];
const TAIKO_MODS: &[&[&str]] = &[
&[""],
&["hd"],
&["hr"],
&["ht"],
&["dt"],
&["hr", "dt"],
&["ez", "dt"],
];
const CATCH_MODS: &[&[&str]] = &[
&[""],
&["hd"],
&["ht"],
&["dt"],
&["ez"],
&["hd", "dt"],
&["hr", "dt"],
];
const MANIA_MODS: &[&[&str]] = &[
&[""],
&["ht"],
&["dt"],
&["ez"],
&["ez", "ht"],
&["ez", "nf", "ht"],
];
const OSU: &str = "osu";
const TAIKO: &str = "taiko";
const CATCH: &str = "catch";
const MANIA: &str = "mania";
fn main() {
dotenv::dotenv().expect("failed to read .env file");
let runtime = Runtime::new().expect("failed to create runtiem");
for mode in [OSU, TAIKO, CATCH, MANIA] {
runtime.block_on(handle_mode(mode));
}
}
async fn handle_mode(mode: &'static str) {
let perf_calc_path_ =
env::var("PERF_CALC_PATH").expect("missing `PERF_CALC_PATH` environment variable");
let perf_calc_path = perf_calc_path_.as_str();
let map_path_ = env::var("MAP_PATH").expect("missing `MAP_PATH` environment variable");
let map_path = map_path_.as_str();
let input_filename = format!("./input/{}.csv", mode);
let mut file = match File::open(&input_filename).await {
Ok(file) => file,
Err(err) => {
return error!(
"skipping file `{}` because it failed to open: {}",
input_filename, err
)
}
};
let mut csv_data = String::new();
if let Err(err) = file.read_to_string(&mut csv_data).await {
return error!(
"skipping file `{}` because it could not be read: {}",
input_filename, err
);
}
let output_filename = format!("./output/{}.json", mode);
let mut output = match StdFile::create(&output_filename) {
Ok(file) => file,
Err(err) => {
return error!(
"skipping file `{}` because its output file `{}` could not be created: {}",
input_filename, output_filename, err
)
}
};
let (mods, mode_int) = match mode {
OSU => (OSU_MODS, 0),
TAIKO => (TAIKO_MODS, 1),
CATCH => (CATCH_MODS, 2),
MANIA => (MANIA_MODS, 3),
_ => unreachable!(),
};
info!(
"Starting to calculate {} data, each map with {} different mod combinations...",
mode,
mods.len()
);
let data: Vec<Data> = csv_data
.split(|c: char| c == ',' || c.is_whitespace())
.filter(|id| !id.is_empty())
.filter_map(|id| match id.parse::<u32>() {
Ok(id) => Some(id),
Err(_) => {
error!("could not parse `{}` as u32", id);
return None;
}
})
.map(|id| mods.iter().map(move |m| (m, id)))
.flatten()
.map(|(mods, map_id)| async move {
let map_path = format!("{}/{}.osu", map_path, map_id);
let mut command = Command::new("dotnet");
command
.arg(perf_calc_path)
.arg("simulate")
.arg(mode)
.arg(map_path)
.arg("--json");
if !mods[0].is_empty() {
for &m in mods.iter() {
command.arg("-m").arg(m);
}
}
let output = match command.output().await {
Ok(output) => output,
Err(err) => {
error!(
"failed to calculate values for map {} on {:?}: {}",
map_id, mods, err
);
return None;
}
};
match serde_json::from_slice(&output.stdout) {
Ok(data) => Some(Data::new(mode_int, map_id, data)),
Err(err) => {
error!(
"failed to deserialize output for map {} on {:?}: {}\n \
>stdout: {}\n >stderr: {}",
map_id,
mods,
err,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
None
}
}
})
.collect::<FuturesUnordered<_>>()
.filter_map(|data| async { data })
.collect::<Vec<Data>>()
.await;
info!(
"Calculated data for {} map-mod pairs, storing in file `{}`...",
data.len(),
output_filename
);
match serde_json::to_writer(&mut output, &data) {
Ok(_) => info!("Finished calculating {} data", mode),
Err(err) => error!(
"failed to serialize data into file `{}`: {}",
output_filename, err
),
}
}
#[derive(Debug, Deserialize, Serialize)]
struct Data {
mode: u32,
map_id: u32,
#[serde(flatten)]
inner: GenericData,
}
impl Data {
fn new(mode: u32, map_id: u32, inner: GenericData) -> Self {
Self {
mode,
map_id,
inner,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
struct GenericData {
#[serde(default, alias = "Aim", skip_serializing_if = "Option::is_none")]
aim: Option<f32>,
#[serde(default, alias = "Speed", skip_serializing_if = "Option::is_none")]
speed: Option<f32>,
#[serde(default, alias = "Accuracy", skip_serializing_if = "Option::is_none")]
accuracy: Option<f32>,
#[serde(default, alias = "Flashlight", skip_serializing_if = "Option::is_none")]
flashlight: Option<f32>,
#[serde(default, alias = "Strain", skip_serializing_if = "Option::is_none")]
strain: Option<f32>,
#[serde(default, alias = "OD", skip_serializing_if = "Option::is_none")]
od: Option<f32>,
#[serde(default, alias = "AR", skip_serializing_if = "Option::is_none")]
ar: Option<f32>,
#[serde(alias = "Mods")]
mods: String,
#[serde(alias = "Stars")]
stars: f32,
pp: f32,
}