removed pp-gen

This commit is contained in:
MaxOhn
2022-10-21 12:59:14 +02:00
parent 5a129437eb
commit df93d5a39c
4 changed files with 0 additions and 554 deletions
-4
View File
@@ -1,4 +0,0 @@
PERF_CALC_PATH="path/to/PerformanceCalculator.dll"
MAP_PATH="path/to/.osu/files"
MAP_TAKE=integer, how many maps to consider
MAP_SKIP=integer, how many maps to skip inbetween
-13
View File
@@ -1,13 +0,0 @@
[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"] }
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"] }
-61
View File
@@ -1,61 +0,0 @@
# pp-gen
Small script to calculate difficulty and performance points related values for a given folder containing `.osu` files.
### How to use
- 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 a bunch of `{map_id}.osu` files
- `MAP_TAKE` is the amount of maps for which values will be calculated
- `MAP_SKIP` is the amount of maps that will be skipped in the `MAP_PATH` folder after each calculation
- Run `cargo run --release`. The program will use the `PerformanceCalculator.dll` to calculate the values and then store them in `output.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:
```js
{
"score": {
"mode": integer,
"map_id": integer,
"mods": [string],
"total_score": integer,
"acc": double,
"combo": integer,
"stats": {
"perfect": integer?,
"great": integer,
"good": integer?,
"ok": integer,
"meh": integer,
"miss": integer,
}
},
"performance": {
"aim": double?,
"speed": double?,
"acc": double?,
"flashlight": double?,
"effective_miss_count": double?,
"scaled_score": double?,
"difficulty": double?,
"pp": double,
},
"difficulty": {
"stars": double,
"max_combo": integer,
"aim": double?,
"speed": double?,
"flashlight": double?,
"slider_factor": double?,
"stamina": double?,
"rhythm": double?,
"colour": double?,
"ar": double?,
"od": double?,
"great_hit_window": double?,
"score_multiplier": double?,
}
}
```
-476
View File
@@ -1,476 +0,0 @@
use std::{
env, fmt,
fs::File as StdFile,
io,
path::PathBuf,
pin::Pin,
sync::Mutex,
task::{Context, Poll},
time::Instant,
};
use futures::{future, stream::FuturesUnordered, Stream, StreamExt, TryStreamExt};
use pbr::ProgressBar;
use rosu_pp::{Beatmap, GameMode};
use serde::{
de::{SeqAccess, Visitor},
Deserialize, Deserializer, Serialize,
};
use tokio::{
fs::{self, DirEntry, File, ReadDir},
process::Command,
runtime::Runtime,
};
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");
Runtime::new()
.expect("failed to create runtime")
.block_on(async_main());
}
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 mut output = StdFile::create("./output.json").expect("failed to create output file");
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...");
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;
future::ready(Some((*idx % skip == 0).then(|| entry)))
})
.filter_map(future::ready)
.take(take as usize)
.map(|dir_entry| async {
let dir_entry = dir_entry?;
let file_name = dir_entry.file_name();
let file_name_lossy = file_name.to_string_lossy();
let file_path = dir_entry.path();
let map_id_str = file_name_lossy.split('.').next();
let map_id = match map_id_str.map(str::parse) {
Some(Ok(id)) => id,
Some(Err(_)) => return Err(Error::ParseId(map_id_str.unwrap().to_owned())),
None => return Err(Error::EmptyFileName),
};
let file = File::open(&file_path).await?;
let mode = Beatmap::parse(file).await?.mode;
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<_>>()
.await
.try_collect::<Vec<Vec<SimulateData>>>()
.await;
pbr.lock().unwrap().finish_println(&format!(
"[INFO] [{:?}] Finished calculating, now flattening...\n",
start.elapsed(),
));
let mut mode_counts = [0; 4];
let data: Vec<SimulateData> = match result {
Ok(data) => data
.into_iter()
.map(|d| {
if let Some(data) = d.get(0) {
mode_counts[data.score.mode as usize] += 1;
}
d.into_iter()
})
.flatten()
.collect(),
Err(err) => return print_err(err),
};
println!(
"[INFO] [{:?}] Flattened {} calculations, now writing to file...",
start.elapsed(),
data.len()
);
match serde_json::to_writer(&mut output, &data) {
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<SimulateData>, 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 mut data: SimulateData = 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;
}
};
data.score.map_id = map_id;
result.push(data);
}
Ok(result)
}
#[derive(Debug)]
enum Error {
EmptyFileName,
Io(std::io::Error),
ParseId(String),
ParseMap(rosu_pp::ParseError),
Serde(serde_json::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyFileName => f.write_str("empty file name"),
Self::Io(_) => f.write_str("io error"),
Self::ParseId(s) => write!(f, "failed to parse map id from `{}`", s),
Self::ParseMap(_) => f.write_str("failed to parse map"),
Self::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)
}
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct SimulateData {
score: Score,
#[serde(alias = "performance_attributes")]
performance: Performance,
#[serde(alias = "difficulty_attributes")]
difficulty: Difficulty,
}
#[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(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, skip_serializing_if = "Option::is_none")]
speed: Option<f64>,
#[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, 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,
}
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)
}
}