adjusted in- and output of pp-gen

This commit is contained in:
MaxOhn
2021-11-12 18:38:36 +01:00
parent 09943c9ea2
commit 26e11843c6
4 changed files with 252 additions and 151 deletions
+1 -2
View File
@@ -4,5 +4,4 @@ Cargo.lock
output
/pp-gen/.env
/pp-gen/target
/pp-gen/input/*
/pp-gen/output/*
/pp-gen/output.json
+2
View File
@@ -6,6 +6,8 @@ edition = "2021"
[dependencies]
dotenv = { version = "0.15", default-features = false }
futures = { version = "0.3", default-features = false, features = ["std"] }
pbr = { version = "1.0" }
rosu-pp = { path = "..", features = ["async_tokio"] }
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"] }
+20 -21
View File
@@ -4,16 +4,15 @@ Small script to calculate difficulty and performance points related values for a
### 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
- `MAP_PATH` is the path to the folder containing a bunch of `{map_id}.osu` 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`.
- Run `cargo run --release`. The program will use the `PerformanceCalculator.dll` to calculate the values and then store them in `output.json`.
### Output format
@@ -23,15 +22,15 @@ The calculated values for each map for multiple different mod combinations will
{
"mode": 0,
"map_id": integer,
"aim": float,
"speed": float,
"accuracy": float,
"flashlight": float,
"od": float,
"ar": float,
"aim": double,
"speed": double,
"accuracy": double,
"flashlight": double,
"od": double,
"ar": double,
"mods": "HD, HR", // comma + whitespace separated string of mod abbreviations ("None" for nomod)
"stars": float,
"pp": float,
"stars": double,
"pp": double,
}
```
- taiko:
@@ -39,11 +38,11 @@ The calculated values for each map for multiple different mod combinations will
{
"mode": 1,
"map_id": integer,
"accuracy": float,
"strain": float,
"accuracy": double,
"strain": double,
"mods": "HD, HR", // comma + whitespace separated string of mod abbreviations ("None" for nomod)
"stars": float,
"pp": float,
"stars": double,
"pp": double,
}
```
- catch:
@@ -52,8 +51,8 @@ The calculated values for each map for multiple different mod combinations will
"mode": 2,
"map_id": integer,
"mods": "HD, HR", // comma + whitespace separated string of mod abbreviations ("None" for nomod)
"stars": float,
"pp": float,
"stars": double,
"pp": double,
}
```
- mania:
@@ -61,10 +60,10 @@ The calculated values for each map for multiple different mod combinations will
{
"mode": 3,
"map_id": integer,
"accuracy": float,
"strain": float,
"accuracy": double,
"strain": double,
"mods": "HD, HR", // comma + whitespace separated string of mod abbreviations ("None" for nomod)
"stars": float,
"pp": float,
"stars": double,
"pp": double,
}
```
+229 -128
View File
@@ -1,20 +1,23 @@
use std::{env, fs::File as StdFile};
use std::{
env, fmt,
fs::File as StdFile,
io,
path::PathBuf,
pin::Pin,
sync::Mutex,
task::{Context, Poll},
time::Instant,
};
use futures::{stream::FuturesUnordered, StreamExt};
use futures::{future, stream::FuturesUnordered, Stream, StreamExt, TryStreamExt};
use pbr::ProgressBar;
use rosu_pp::{Beatmap, GameMode};
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)*))
}
}
use tokio::{
fs::{self, DirEntry, File, ReadDir},
process::Command,
runtime::Runtime,
};
const OSU_MODS: &[&[&str]] = &[
&[""],
@@ -67,144 +70,224 @@ 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));
}
Runtime::new()
.expect("failed to create runtiem")
.block_on(async_main());
}
async fn handle_mode(mode: &'static str) {
async fn async_main() {
let start = Instant::now();
let map_path = env::var("MAP_PATH").expect("missing `MAP_PATH` environment variable");
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 mut output = StdFile::create("./output_.json").expect("failed to create output file");
let input_filename = format!("./input/{}.csv", mode);
let take = 50;
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 pbr = Mutex::new(ProgressBar::new(take));
println!("[INFO] Calculating...");
let mut csv_data = String::new();
let result = fs::read_dir(map_path)
.await
.map(ReadDirStream::new)
.expect("failed to open directory at `MAP_PATH`")
.scan(0, |idx, entry| {
*idx += 1;
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;
}
future::ready(Some((*idx % 75 == 0).then(|| entry)))
})
.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");
.filter_map(|opt| future::ready(opt))
.take(take as usize)
.map(|dir_entry| async {
let dir_entry = dir_entry?;
let file_name = dir_entry.file_name();
let file_path = dir_entry.path();
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;
}
let map_id = match file_name
.to_string_lossy()
.split('.')
.next()
.map(str::parse)
.transpose()
.map_err(|_| Error::ParseId)?
{
Some(id) => id,
None => return Err(Error::EmptyFileName),
};
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),
);
let file = File::open(&file_path).await?;
let mode = Beatmap::parse(file).await?.mode;
None
}
let result = handle_map(mode, map_id, file_path, perf_calc_path).await;
if let Ok(mut progress) = pbr.lock() {
progress.inc();
}
result
})
.collect::<FuturesUnordered<_>>()
.filter_map(|data| async { data })
.collect::<Vec<Data>>()
.await
.try_collect::<Vec<Vec<Data>>>()
.await;
info!(
"Calculated data for {} map-mod pairs, storing in file `{}`...",
data.len(),
output_filename
pbr.lock().unwrap().finish_println(&format!(
"[INFO] [{:?}] Finished calculating, now flattening...\n",
start.elapsed(),
));
let mut mode_counts = [0; 4];
let data: Vec<Data> = match result {
Ok(data) => data
.into_iter()
.map(|d| {
if let Some(data) = d.get(0) {
mode_counts[data.mode as usize] += 1;
}
d.into_iter()
})
.flatten()
.collect(),
Err(err) => return print_err(err.into()),
};
println!(
"[INFO] [{:?}] Flattened {} calculations, now writing to file...",
start.elapsed(),
data.len()
);
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
Ok(_) => println!(
"[INFO] [{:?}] Finished writing data into `output.json`\n\
Maps:\n - osu: {}\n - taiko: {}\n - catch: {}\n - mania: {}",
start.elapsed(),
mode_counts[0],
mode_counts[1],
mode_counts[2],
mode_counts[3]
),
Err(err) => print_err(err.into()),
}
}
fn print_err(err: Error) {
let mut e: &dyn std::error::Error = &err;
eprintln!("[ERROR] {}", err);
while let Some(src) = e.source() {
eprintln!("[ERROR] - caused by: {}", src);
e = src;
}
}
async fn handle_map(
mode: GameMode,
map_id: u32,
path: PathBuf,
perf_calc_path: &str,
) -> Result<Vec<Data>, Error> {
let (mods, mode_str) = match mode {
GameMode::STD => (OSU_MODS, OSU),
GameMode::TKO => (TAIKO_MODS, TAIKO),
GameMode::CTB => (CATCH_MODS, CATCH),
GameMode::MNA => (MANIA_MODS, MANIA),
};
let mut result = Vec::with_capacity(mods.len());
for mods_ in mods {
let mut command = Command::new("dotnet");
command
.arg(perf_calc_path)
.arg("simulate")
.arg(mode_str)
.arg(&path)
.arg("--json");
if !mods_[0].is_empty() {
for &m in mods_.iter() {
command.arg("-m").arg(m);
}
}
let output = command.output().await?;
let data = match serde_json::from_slice(&output.stdout) {
Ok(data) => data,
Err(_) => {
let content = String::from_utf8_lossy(&output.stderr);
eprintln!(
"[ERROR] mods={:?} | mode={:?} | map={}\n{}",
mods_, mode, map_id, content
);
continue;
}
};
let data = Data::new(mode as u32, map_id, data);
result.push(data);
}
Ok(result)
}
#[derive(Debug)]
enum Error {
EmptyFileName,
Io(std::io::Error),
ParseId,
ParseMap(rosu_pp::ParseError),
Serde(serde_json::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::EmptyFileName => f.write_str("empty file name"),
Error::Io(_) => f.write_str("io error"),
Error::ParseId => f.write_str("failed to parse map id"),
Error::ParseMap(_) => f.write_str("failed to parse map"),
Error::Serde(_) => f.write_str("failed to deserialize"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::EmptyFileName => None,
Self::Io(src) => Some(src),
Self::ParseId => None,
Self::ParseMap(src) => Some(src),
Self::Serde(src) => Some(src),
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<rosu_pp::ParseError> for Error {
fn from(e: rosu_pp::ParseError) -> Self {
Self::ParseMap(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Self::Serde(e)
}
}
@@ -248,3 +331,21 @@ struct GenericData {
stars: f64,
pp: f64,
}
struct ReadDirStream {
inner: ReadDir,
}
impl ReadDirStream {
fn new(read_dir: ReadDir) -> Self {
Self { inner: read_dir }
}
}
impl Stream for ReadDirStream {
type Item = io::Result<DirEntry>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.inner.poll_next_entry(cx).map(Result::transpose)
}
}