From 80a4b9f1d181b9d470cd852096a00cae78a38d75 Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Sat, 4 Jan 2025 23:20:20 +0100 Subject: [PATCH] add osu memory reader --- src-tauri/Cargo.lock | 227 ++++++- src-tauri/Cargo.toml | 5 + src-tauri/src/lib.rs | 90 +++ src-tauri/src/reading_loop.rs | 462 ++++++++++++++ src-tauri/src/structs.rs | 1065 +++++++++++++++++++++++++++++++++ 5 files changed, 1838 insertions(+), 11 deletions(-) create mode 100644 src-tauri/src/reading_loop.rs create mode 100644 src-tauri/src/structs.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9a83320..0a4ca5e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1097,12 +1097,26 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "ezpplauncher" version = "0.1.0" dependencies = [ + "eyre", + "rosu-mem", + "rosu-pp", "serde", "serde_json", + "serde_repr", "tauri", "tauri-build", "tauri-plugin-dialog", @@ -1110,6 +1124,7 @@ dependencies = [ "tauri-plugin-shell", "tauri-plugin-single-instance", "tauri-plugin-sql", + "tracy-client", ] [[package]] @@ -1415,6 +1430,19 @@ dependencies = [ "x11", ] +[[package]] +name = "generator" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +dependencies = [ + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.58.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1953,6 +1981,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.3" @@ -2235,6 +2269,19 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "mac" version = "0.1.1" @@ -2264,6 +2311,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matches" version = "0.1.10" @@ -2384,6 +2440,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.27.1" @@ -2425,6 +2493,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2493,7 +2571,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", "syn 2.0.90", @@ -2779,6 +2857,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pango" version = "0.18.3" @@ -3315,8 +3399,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -3327,9 +3420,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -3396,6 +3495,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rosu-mem" +version = "1.0.0" +source = "git+https://github.com/486c/rosu-mem.git?tag=v1.0.0#2349cb845e3149c71873bfeda7ec81afec94c8c0" +dependencies = [ + "cfg-if", + "nix 0.25.1", + "paste", + "windows 0.48.0", +] + +[[package]] +name = "rosu-pp" +version = "0.10.0" +source = "git+https://github.com/486c/rosu-pp.git?branch=main#a82dd41e5008944893b2434901d62dd5e6160d74" + [[package]] name = "rsa" version = "0.9.7" @@ -3444,6 +3559,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -3697,6 +3818,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shared_child" version = "1.0.1" @@ -4204,7 +4334,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.58.0", "windows-core 0.58.0", "windows-version", "x11-dl", @@ -4274,7 +4404,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.58.0", ] [[package]] @@ -4469,7 +4599,7 @@ dependencies = [ "tauri-utils", "thiserror 2.0.6", "url", - "windows", + "windows 0.58.0", ] [[package]] @@ -4494,7 +4624,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.58.0", "wry", ] @@ -4615,6 +4745,16 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.37" @@ -4807,6 +4947,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracy-client" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307e6b7030112fe9640fdd87988a40795549ba75c355f59485d14e6b444d2987" +dependencies = [ + "loom", + "once_cell", + "tracy-client-sys", +] + +[[package]] +name = "tracy-client-sys" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d104d610dfa9dd154535102cc9c6164ae1fa37842bc2d9e83f9ac82b0ae0882" +dependencies = [ + "cc", ] [[package]] @@ -4991,6 +5181,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -5268,7 +5464,7 @@ checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.58.0", "windows-core 0.58.0", "windows-implement", "windows-interface", @@ -5292,7 +5488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" dependencies = [ "thiserror 1.0.69", - "windows", + "windows 0.58.0", "windows-core 0.58.0", ] @@ -5351,6 +5547,15 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.58.0" @@ -5735,7 +5940,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.58.0", "windows-core 0.58.0", "windows-version", "x11-dl", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7b16285..4e399a5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,10 +18,15 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] +rosu-mem = { git = "https://github.com/486c/rosu-mem.git", tag = "v1.0.0" } +rosu-pp = { git = "https://github.com/486c/rosu-pp.git", branch = "main", features = ["gradual"] } +tracy-client = { version = "0.16.4", default-features = false } +eyre = "0.6.12" tauri = { version = "2", features = [] } tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_repr = "0.1.17" tauri-plugin-sql = "2.2.0" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8472259..1ce1294 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,8 +1,23 @@ +mod reading_loop; +mod structs; + // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ +use crate::reading_loop::process_reading_loop; +use crate::structs::{State, StaticAddresses}; +use eyre::Report; +use rosu_mem::{ + error::ProcessError, + process::{Process, ProcessTraits}, +}; +use std::sync::{Arc, Mutex}; +use std::{thread, time::Duration}; +use structs::{InnerValues, OutputValues}; use tauri::Manager; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + println!("starting ezpplauncher!"); + osu_memory_reading(); let mut builder = tauri::Builder::default().plugin(tauri_plugin_fs::init()); #[cfg(desktop)] { @@ -21,3 +36,78 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +fn osu_memory_reading() { + thread::spawn(|| { + let output_values = Arc::new(Mutex::new(OutputValues::default())); + let inner_values = InnerValues::default(); + + let mut state = State { + addresses: StaticAddresses::default(), + ivalues: inner_values, + values: output_values, + }; + + 'init_loop: loop { + println!("Searching for osu! process"); + let p = match Process::initialize("osu!.exe") { + Ok(p) => p, + Err(e) => { + println!("{:?}", Report::new(e)); + thread::sleep(Duration::from_millis(5000)); + continue 'init_loop; + } + }; + + println!("Reading static signatures..."); + match StaticAddresses::new(&p) { + Ok(v) => state.addresses = v, + Err(e) => { + match e.downcast_ref::() { + Some(&ProcessError::ProcessNotFound) => { + thread::sleep(Duration::from_millis(2000)); + continue 'init_loop + }, + #[cfg(target_os = "windows")] + Some(&ProcessError::OsError{ .. }) => { + println!("{:?}", e); + thread::sleep(Duration::from_millis(2000)); + continue 'init_loop + }, + Some(_) | None => { + println!("{:?}", e); + thread::sleep(Duration::from_millis(2000)); + continue 'init_loop + }, + } + }, + }; + + println!("osu! process found, Starting reading loop"); + + 'main_loop: loop { + if let Err(e) = process_reading_loop(&p, &mut state) { + match e.downcast_ref::() { + Some(&ProcessError::ProcessNotFound) => { + thread::sleep(Duration::from_millis(5000)); + continue 'init_loop; + }, + #[cfg(target_os = "windows")] + Some(&ProcessError::OsError{ .. }) => { + println!("{:?}", e); + thread::sleep(Duration::from_millis(5000)); + continue 'init_loop + }, + Some(_) | None => { + println!("{:?}", e); + thread::sleep(Duration::from_millis(5000)); + continue 'main_loop; + } + } + } + println!("{:?}", state.values.clone()); + thread::sleep(Duration::from_millis(2000)); + } + } + }); +} diff --git a/src-tauri/src/reading_loop.rs b/src-tauri/src/reading_loop.rs new file mode 100644 index 0000000..3f1e0ab --- /dev/null +++ b/src-tauri/src/reading_loop.rs @@ -0,0 +1,462 @@ +use std::{borrow::Cow, mem::size_of}; + +use rosu_pp::Beatmap; +use tracy_client::*; +use eyre::Result; + +use rosu_mem::process::{Process, ProcessTraits}; + +use crate::structs::{State, GameState, BeatmapStatus, OutputValues}; + + +/// Here cases when key overlay is not gonna be available for reading: +/// 1. Map is not fully loaded +/// 2. If key overlay is not enabled in settings +pub fn process_key_overlay( + p: &Process, + values: &mut OutputValues, + ruleset_addr: i32, +) -> Result<()> { + let keyoverlay_ptr = p.read_i32(ruleset_addr + 0xB0)?; + + if keyoverlay_ptr == 0 { + return Ok(()) + } + + // TODO optimize using batches? + + let keyoverlay_addr = p.read_i32( + p.read_i32(keyoverlay_ptr + 0x10)? + 0x4 + )?; + + values.keyoverlay.k1_pressed = p.read_i8( + p.read_i32(keyoverlay_addr + 0x8)? + 0x1C + )? != 0; + + values.keyoverlay.k1_count = p.read_i32( + p.read_i32(keyoverlay_addr + 0x8)? + 0x14 + )? as u32; + + values.keyoverlay.k2_pressed = p.read_i8( + p.read_i32(keyoverlay_addr + 0xC)? + 0x1C + )? != 0; + + values.keyoverlay.k2_count = p.read_i32( + p.read_i32(keyoverlay_addr + 0xC)? + 0x14 + )? as u32; + + values.keyoverlay.m1_pressed = p.read_i8( + p.read_i32(keyoverlay_addr + 0x10)? + 0x1C + )? != 0; + + values.keyoverlay.m1_count = p.read_i32( + p.read_i32(keyoverlay_addr + 0x10)? + 0x14 + )? as u32; + + values.keyoverlay.m2_pressed = p.read_i8( + p.read_i32(keyoverlay_addr + 0x14)? + 0x1C + )? != 0; + + values.keyoverlay.m2_count = p.read_i32( + p.read_i32(keyoverlay_addr + 0x14)? + 0x14 + )? as u32; + + Ok(()) +} + +pub fn process_gameplay( + p: &Process, + state: &mut State, + values: &mut OutputValues, + ruleset_addr: i32, +) -> Result<()> { + let _span = span!("Gameplay data"); + + if values.prev_playtime > values.playtime { + values.reset_gameplay(); + state.ivalues.reset(); + } + + values.prev_playtime = values.playtime; + + if ruleset_addr == 0 { + return Ok(()) + }; + + let gameplay_base = + p.read_i32(ruleset_addr + 0x68)?; + + if gameplay_base == 0 { + return Ok(()) + } + + let score_base = p.read_i32(gameplay_base + 0x38)?; + + let hp_base = p.read_i32(gameplay_base + 0x40)?; + + // Random value but seems to work pretty well + // TODO sometimes playtime is >150 but game doesn't have + // values yet unreal to debug, occurs rarely and randomly + if values.playtime > 150 { + values.gameplay.current_hp = p.read_f64(hp_base + 0x1C)?; + values.gameplay.current_hp_smooth = + p.read_f64(hp_base + 0x14)?; + } + + let hit_errors_base = p.read_i32(score_base + 0x38)?; + + p.read_i32_array( + hit_errors_base, + &mut values.gameplay.hit_errors + )?; + + values.gameplay.unstable_rate = + values.gameplay.calculate_unstable_rate(); + + // TODO batch + values.gameplay.mode = p.read_i32(score_base + 0x64)?; + + // TODO batch + values.gameplay.hit_300 = p.read_i16(score_base + 0x8a)?; + values.gameplay.hit_100 = p.read_i16(score_base + 0x88)?; + values.gameplay.hit_50 = p.read_i16(score_base + 0x8c)?; + + values.gameplay.username = p.read_string(score_base + 0x28)?; + + // TODO batch + values.gameplay.hit_geki = p.read_i16(score_base + 0x8e)?; + values.gameplay.hit_katu = p.read_i16(score_base + 0x90)?; + values.gameplay.hit_miss = p.read_i16(score_base + 0x92)?; + + let passed_objects = values.gameplay.passed_objects()?; + + values.gameplay.passed_objects = passed_objects; + + values.gameplay.update_accuracy(); + + values.gameplay.score = p.read_i32(score_base + 0x78)?; + + values.gameplay.combo = p.read_i16(score_base + 0x94)?; + values.gameplay.max_combo = p.read_i16(score_base + 0x68)?; + + if values.gameplay.combo < values.prev_combo + && values.gameplay.hit_miss == values.prev_hit_miss { + values.gameplay.slider_breaks += 1; + } + + values.prev_hit_miss = values.gameplay.hit_miss; + + let mods_xor_base = p.read_i32(score_base + 0x1C)?; + + let mods_raw = p.read_u64(mods_xor_base + 0x8)?; + + let mods_xor1 = mods_raw & 0xFFFFFFFF; + let mods_xor2 = mods_raw >> 32; + + // Read key overlay + process_key_overlay( + p, + values, + ruleset_addr + )?; + + values.gameplay.mods = (mods_xor1 ^ mods_xor2) as u32; + values.update_readable_mods(); + + // Calculate pp + values.update_current_pp(&mut state.ivalues); + values.update_fc_pp(&mut state.ivalues); + + values.prev_passed_objects = passed_objects; + values.prev_combo = values.gameplay.combo; + + values.gameplay.grade = values.gameplay.get_current_grade(); + values.update_current_bpm(); + values.update_kiai(); + + Ok(()) +} + +pub fn process_reading_loop( + p: &Process, + state: &mut State +) -> Result<()> { + let _span = span!("reading loop"); + + let values = state.values.clone(); + let mut values = values.lock().unwrap(); + + let menu_mods_ptr = p.read_i32( + state.addresses.menu_mods + 0x9 + )?; + + let menu_mods = p.read_u32(menu_mods_ptr)?; + values.menu_mods = menu_mods; + + let playtime_ptr = p.read_i32(state.addresses.playtime + 0x5)?; + values.playtime = p.read_i32(playtime_ptr)?; + + let beatmap_ptr = p.read_i32(state.addresses.base - 0xC)?; + let beatmap_addr = p.read_i32(beatmap_ptr)?; + + let status_ptr = p.read_i32(state.addresses.status - 0x4)?; + + let skin_ptr = p.read_i32(state.addresses.skin + 0x4)?; + let skin_data = p.read_i32(skin_ptr)?; + values.skin = p.read_string(skin_data + 0x44)?; + + values.state = GameState::from( + p.read_u32(status_ptr)? + ); + + // Handle leaving `Playing` state + if values.prev_state == GameState::Playing + && values.state != GameState::Playing { + values.reset_gameplay(); + state.ivalues.reset(); + values.update_stars_and_ss_pp(); + } + + if beatmap_addr == 0 { + return Ok(()) + } + + if values.state != GameState::MultiplayerLobby { + let mut beatmap_stats_buff = [0u8; size_of::() * 4]; + + p.read( + beatmap_addr + 0x2c, + size_of::() * 4, + &mut beatmap_stats_buff + )?; + + // Safety: unwrap here because buff is already initialized + // and filled with zeros, the worst case scenario is + // ar, cs, od, hp going to be zero's + values.beatmap.ar = f32::from_le_bytes( + beatmap_stats_buff[0..4].try_into().unwrap() + ); + + values.beatmap.cs = f32::from_le_bytes( + beatmap_stats_buff[4..8].try_into().unwrap() + ); + + values.beatmap.hp = f32::from_le_bytes( + beatmap_stats_buff[8..12].try_into().unwrap() + ); + + values.beatmap.od = f32::from_le_bytes( + beatmap_stats_buff[12..].try_into().unwrap() + ); + + let plays_addr = p.read_i32(state.addresses.base - 0x33)? + 0xC; + values.plays = p.read_i32(plays_addr)?; + + values.beatmap.artist = p.read_string(beatmap_addr + 0x18)?; + values.beatmap.title = p.read_string(beatmap_addr + 0x24)?; + values.beatmap.creator = p.read_string(beatmap_addr + 0x7C)?; + values.beatmap.difficulty = p.read_string(beatmap_addr + 0xAC)?; + values.beatmap.map_id = p.read_i32(beatmap_addr + 0xC8)?; // TODO batch + values.beatmap.mapset_id = p.read_i32(beatmap_addr + 0xCC)?; // TODO batch + } + + values.beatmap.beatmap_status = BeatmapStatus::from( + p.read_i16(beatmap_addr + 0x12C)? + ); + + let mut new_map = false; + + // All time values that available everywhere + values.chat_enabled = p.read_i8( + state.addresses.chat_checker - 0x20 + )? != 0; + + // Skin folder + let skin_data_ptr = p.read_i32( + p.read_i32(state.addresses.skin + 4)? + )?; + + values.skin_folder = p.read_string( + skin_data_ptr + 68 + )?; + + if values.state != GameState::PreSongSelect + && values.state != GameState::MultiplayerLobby + && values.state != GameState::MultiplayerResultScreen { + let menu_mode_addr = p.read_i32(state.addresses.base - 0x33)?; + + let beatmap_file = p.read_string(beatmap_addr + 0x90)?; + let beatmap_folder = p.read_string(beatmap_addr + 0x78)?; + let audio_file = p.read_string(beatmap_addr + 0x64)?; + values.menu_mode = p.read_i32(menu_mode_addr)?; + + values.beatmap.paths.beatmap_full_path + = values.osu_path.join("Songs/"); + + values.beatmap.paths.beatmap_full_path.push(&beatmap_folder); + values.beatmap.paths.beatmap_full_path.push(&beatmap_file); + + values.beatmap.md5 = + p.read_string(beatmap_addr + 0x6C)?; + + // Check if beatmap changed + if (beatmap_folder != values.beatmap.paths.beatmap_folder + || beatmap_file != values.beatmap.paths.beatmap_file + || values.prev_menu_mode != values.menu_mode) + && values.beatmap.paths.beatmap_full_path.exists() { + let current_beatmap = match Beatmap::from_path( + &values.beatmap.paths.beatmap_full_path + ) { + Ok(beatmap) => { + new_map = true; + + values.beatmap.paths.background_file.clone_from( + &beatmap.background.filename + ); + + if let Some(hobj) = beatmap.hit_objects.last() { + values.beatmap.last_obj_time = hobj.start_time; + } + + if let Some(hobj) = beatmap.hit_objects.first() { + values.beatmap.first_obj_time = hobj.start_time; + } + + values.beatmap.bpm = beatmap.bpm(); + + Some(beatmap) + }, + Err(_) => { + println!("Failed to parse beatmap"); + None + }, + }; + + values.current_beatmap = current_beatmap; + + values.beatmap.paths.beatmap_folder = beatmap_folder; + values.beatmap.paths.beatmap_file = beatmap_file; + values.beatmap.paths.audio_file = audio_file; + + values.update_min_max_bpm(); + values.update_full_paths(); + values.adjust_bpm(); + } + } + + // store the converted map so it's not converted + // everytime it's used for pp calc + if new_map { + if let Some(map) = &values.current_beatmap { + if let Cow::Owned(converted) = map + .convert_mode(values.menu_gamemode()) + { + values.current_beatmap = Some(converted); + } + } + + values.update_stars_and_ss_pp(); + values.update_current_pp(&mut state.ivalues); + } + + let ruleset_addr = p.read_i32( + p.read_i32(state.addresses.rulesets - 0xb)? + 0x4 + )?; + + let audio_time_ptr = p.read_i32(state.addresses.audio_time_base + 0x9)?; + values.precise_audio_time = p.read_i32(audio_time_ptr)?; + + // If this happened there is zero sense to continue + // reading because all the values depends on this + // address + if ruleset_addr == 0 { + return Ok(()) + } + + // Process result screen + // TODO handle situations when result screen is not ready + if values.state == GameState::ResultScreen { + let result_base = p.read_i32(ruleset_addr+ 0x38)?; + + values.result_screen.username = p.read_string(result_base + 0x28)?; + + let mods_xor_base = p.read_i32(result_base + 0x1C)?; + + // TODO batch + let mods_xor1 = p.read_i32(mods_xor_base + 0xC)?; + let mods_xor2 = p.read_i32(mods_xor_base + 0x8)?; + + values.result_screen.mods = (mods_xor1 ^ mods_xor2) as u32; + values.result_screen.mode = p.read_i32(result_base + 0x64)? as u8; + values.result_screen.score = p.read_i32(result_base + 0x78)?; + + // TODO batch + values.result_screen.hit_300 = p.read_i16(result_base + 0x8A)?; + values.result_screen.hit_100 = p.read_i16(result_base + 0x88)?; + values.result_screen.hit_50 = p.read_i16(result_base + 0x8C)?; + values.result_screen.hit_geki = p.read_i16(result_base + 0x8E)?; + values.result_screen.hit_katu = p.read_i16(result_base + 0x90)?; + + values.result_screen.update_accuracy(); + } + + // Process gameplay + if values.state == GameState::Playing { + let res = process_gameplay( + p, + state, + &mut values, + ruleset_addr + ); + + if let Err(e) = res { + println!("{:?}", e); + println!("Skipped gameplay reading, probably it's not ready yet"); + } + } + + // Handling entering `ResultScreen` state + if values.prev_state != GameState::ResultScreen + && values.state == GameState::ResultScreen { + if values.prev_state != GameState::Playing { + values.update_current_pp(&mut state.ivalues); + } + + values.update_stars_and_ss_pp(); + } + + // Handling entering `SongSelect` state + if values.prev_state != GameState::SongSelect + && values.state == GameState::SongSelect { + // Reseting pp's from result screen + if values.prev_state == GameState::ResultScreen { + values.current_pp = 0.0; + } + + values.update_current_pp(&mut state.ivalues); + values.update_stars_and_ss_pp(); + values.adjust_bpm(); + } + + // Update stars when entering `Playing` state + if values.prev_state != GameState::Playing + && values.state == GameState::Playing { + values.reset_gameplay(); + values.update_stars_and_ss_pp(); + values.adjust_bpm(); + } + + // Handle mods changes inside `SongSelect` state + if values.state == GameState::SongSelect + && values.prev_menu_mods != values.menu_mods { + values.update_stars_and_ss_pp(); + values.update_current_pp(&mut state.ivalues); + values.adjust_bpm(); + } + + values.prev_menu_mode = values.menu_mode; + values.prev_menu_mods = menu_mods; + values.prev_state = values.state; + + Ok(()) +} \ No newline at end of file diff --git a/src-tauri/src/structs.rs b/src-tauri/src/structs.rs new file mode 100644 index 0000000..150037d --- /dev/null +++ b/src-tauri/src/structs.rs @@ -0,0 +1,1065 @@ +use std::{ + num::TryFromIntError, + path::PathBuf, + str::FromStr, + sync::{Arc, Mutex} +}; + +use rosu_mem::{ + process::{Process, ProcessTraits}, + signature::Signature +}; + +use rosu_pp::{Beatmap, BeatmapExt, GameMode, + PerformanceAttributes, GradualPerformance, + beatmap::EffectPoint, ScoreState, AnyPP +}; +use eyre::Result; +use serde::Serialize; +use serde_repr::Serialize_repr; + +pub type Arm = Arc>; + +macro_rules! calculate_accuracy { + ($self: expr) => {{ + match $self.gamemode() { + GameMode::Osu => + ($self.hit_300 as f64 * 6. + + $self.hit_100 as f64 * 2. + + $self.hit_50 as f64) + / + (($self.hit_300 + + $self.hit_100 + + $self.hit_50 + + $self.hit_miss) as f64 * 6. + ), + GameMode::Taiko => + ($self.hit_300 as f64 * 2. + $self.hit_100 as f64) + / + (($self.hit_300 + + $self.hit_100 + + $self.hit_50 + + $self.hit_miss) as f64 * 2.), + GameMode::Catch => + ($self.hit_300 + $self.hit_100 + $self.hit_50) as f64 + / + ($self.hit_300 + $self.hit_100 + $self.hit_50 + + $self.hit_katu + $self.hit_miss) as f64, + GameMode::Mania => + (($self.hit_geki + $self.hit_300) as f64 + * 6. + $self.hit_katu as f64 + * 4. + $self.hit_100 as f64 + * 2. + $self.hit_50 as f64) + / + (($self.hit_geki + + $self.hit_300 + + $self.hit_katu + + $self.hit_100 + + $self.hit_50 + + $self.hit_miss) as f64 * 6. + ) + } + }} +} + +//TODO use bitflags & enum & bitflags iterator for converting to string? +const MODS: [(u32, &str); 31] = [ + (1 << 0, "NF"), + (1 << 1, "EZ"), + (1 << 2, "TD"), + (1 << 3, "HD"), + (1 << 4, "HR"), + (1 << 5, "SD"), + (1 << 6, "DT"), + (1 << 7, "RX"), + (1 << 8, "HT"), + (1 << 9, "NC"), + (1 << 10, "FL"), + (1 << 11, "AU"), + (1 << 12, "SO"), + (1 << 13, "AP"), + (1 << 14, "PF"), + (1 << 15, "K4"), + (1 << 16, "K5"), + (1 << 17, "K6"), + (1 << 18, "K7"), + (1 << 19, "K8"), + (1 << 20, "FI"), + (1 << 21, "RN"), + (1 << 22, "CN"), + (1 << 23, "TP"), + (1 << 24, "K9"), + (1 << 25, "Coop"), + (1 << 26, "K1"), + (1 << 27, "K3"), + (1 << 28, "K2"), + (1 << 29, "V2"), + (1 << 30, "MR"), +]; + +#[derive(Serialize_repr, Debug, Default, PartialEq, Eq, Clone, Copy)] +#[repr(u32)] +pub enum GameState { + PreSongSelect = 0, + Playing = 2, + SongSelect = 5, + EditorSongSelect = 4, + ResultScreen = 7, + MultiplayerLobbySelect = 11, + MultiplayerLobby = 12, + MultiplayerResultScreen = 14, + + #[default] + Unknown, +} + +impl From for GameState { + fn from(value: u32) -> Self { + match value { + 0 => Self::PreSongSelect, + 2 => Self::Playing, + 4 => Self::EditorSongSelect, + 5 => Self::SongSelect, + 7 => Self::ResultScreen, + 11 => Self::MultiplayerLobbySelect, + 12 => Self::MultiplayerLobby, + 14 => Self::MultiplayerResultScreen, + _ => Self::Unknown, + } + } +} + +#[derive(Serialize_repr, Debug, Default, PartialEq, Eq, Copy, Clone)] +#[repr(i16)] +pub enum BeatmapStatus { + #[default] + Unknown = 0, + Unsubmitted = 1, + Unranked = 2, + Unused = 3, + Ranked = 4, + Approved = 5, + Qualified = 6, + Loved = 7, +} + +impl From for BeatmapStatus { + fn from(value: i16) -> Self { + match value { + 1 => Self::Unsubmitted, + 2 => Self::Unranked, + 3 => Self::Unused, + 4 => Self::Ranked, + 5 => Self::Approved, + 6 => Self::Qualified, + 7 => Self::Loved, + _ => Self::Unknown, + } + } +} + +#[derive(Default)] +pub struct StaticAddresses { + pub base: i32, + pub status: i32, + pub menu_mods: i32, + pub rulesets: i32, + pub playtime: i32, + pub skin: i32, + pub chat_checker: i32, + pub audio_time_base: i32, +} + +impl StaticAddresses { + pub fn new(p: &Process) -> Result { + let _span = tracy_client::span!("static addresses"); + + let base_sign = Signature::from_str("F8 01 74 04 83 65")?; + let status_sign = Signature::from_str("48 83 F8 04 73 1E")?; + let menu_mods_sign = Signature::from_str( + "C8 FF ?? ?? ?? ?? ?? 81 0D ?? ?? ?? ?? 00 08 00 00" + )?; + + let rulesets_sign = Signature::from_str( + "7D 15 A1 ?? ?? ?? ?? 85 C0" + )?; + + let playtime_sign = Signature::from_str( + "5E 5F 5D C3 A1 ?? ?? ?? ?? 89 ?? 04" + )?; + + let skin_sign = Signature::from_str("75 21 8B 1D")?; + + let chat_checker = Signature::from_str("0A D7 23 3C 00 00 ?? 01")?; + + let audio_time_base = Signature::from_str("DB 5C 24 34 8B 44 24 34")?; + + Ok(Self { + base: p.read_signature(&base_sign)?, + status: p.read_signature(&status_sign)?, + menu_mods: p.read_signature(&menu_mods_sign)?, + rulesets: p.read_signature(&rulesets_sign)?, + playtime: p.read_signature(&playtime_sign)?, + skin: p.read_signature(&skin_sign)?, + chat_checker: p.read_signature(&chat_checker)?, + audio_time_base: p.read_signature(&audio_time_base)?, + }) + } +} + + +pub struct State { + pub addresses: StaticAddresses, + pub values: Arm, + pub ivalues: InnerValues, +} + +// Inner values that used only inside +// reading loop and shouldn't be +// shared between any threads +#[derive(Default)] +pub struct InnerValues { + pub gradual_performance_current: + Option>, + + pub current_beatmap_perf: Option, +} + +impl InnerValues { + pub fn reset(&mut self) { + self.current_beatmap_perf = None; + self.gradual_performance_current = None; + } +} + +#[derive(Debug, Default, Serialize)] +pub struct KeyOverlayValues { + pub k1_pressed: bool, + pub k1_count: u32, + pub k2_pressed: bool, + pub k2_count: u32, + pub m1_pressed: bool, + pub m1_count: u32, + pub m2_pressed: bool, + pub m2_count: u32, +} + +impl KeyOverlayValues { + pub fn reset(&mut self) { + self.k1_pressed = false; + self.k1_count = 0; + self.k2_pressed = false; + self.k2_count = 0; + self.m1_pressed = false; + self.m1_count = 0; + self.m2_pressed = false; + self.m2_count = 0; + } +} + +#[derive(Debug, Default, Serialize)] +pub struct ResultScreenValues { + pub username: String, + pub mods: u32, + pub mode: u8, + pub max_combo: i16, + pub score: i32, + pub hit_300: i16, + pub hit_100: i16, + pub hit_50: i16, + pub hit_geki: i16, + pub hit_katu: i16, + pub hit_miss: i16, + pub accuracy: f64, +} + +impl ResultScreenValues { + pub fn gamemode(&self) -> GameMode { + GameMode::from(self.mode) + } + + pub fn update_accuracy(&mut self) { + let _span = tracy_client::span!("result_screen: calculate accuracy"); + + self.accuracy = calculate_accuracy!(self); + } +} + +#[derive(Debug, Default, Serialize)] +pub struct BeatmapPathValues { + /// Absolute beatmap file path + /// Example: `/path/to/osu/Songs/124321 Artist - Title/my_map.osu` + pub beatmap_full_path: PathBuf, + + /// Relative to osu! folder beatmap folder path + /// Example: `124321 Artist - Title` + pub beatmap_folder: String, + + /// Relative to beatmap folder background file path + /// Example: `my_map.osu` + pub beatmap_file: String, + + /// Relative to beatmap folder background file path + /// Example: `background.jpg` + pub background_file: String, + + /// Absolute background file path + /// Example: `/path/to/osu/Songs/beatmap/background.jpg` + pub background_path_full: PathBuf, + + /// Relative to beatmap folder audio file path + /// Example: `` + pub audio_file: String, +} + +#[derive(Debug, Default, Serialize)] +pub struct BeatmapValues { + pub artist: String, + pub title: String, + pub creator: String, + pub difficulty: String, + + /// ID of particular difficulty inside mapset + pub map_id: i32, + + /// ID of whole mapset + pub mapset_id: i32, + + /// MD5 hash of the beatmap + pub md5: String, + + pub ar: f32, + pub cs: f32, + pub hp: f32, + pub od: f32, + + /// Beatmap Status aka Ranked, Pending, Loved, etc + pub beatmap_status: BeatmapStatus, + + /// Time in milliseconds of last object of beatmap + pub last_obj_time: f64, + + /// Time in milliseconds of first object of beatmap + pub first_obj_time: f64, + + /// BPM of currently selected beatmap + pub bpm: f64, + + /// Max BPM of currently selected beatmap + pub max_bpm: f64, + + /// Min BPM of currently selected beatmap + pub min_bpm: f64, + + /// Paths of files used by beatmap + /// .osu file, background file, etc + pub paths: BeatmapPathValues, +} + +#[derive(Debug, Default, Serialize)] +pub struct GameplayValues { + #[serde(skip)] + pub hit_errors: Vec, + + pub mods: u32, + + pub username: String, + pub score: i32, + pub hit_300: i16, + pub hit_100: i16, + pub hit_50: i16, + pub hit_geki: i16, + pub hit_katu: i16, + pub hit_miss: i16, + pub accuracy: f64, + pub combo: i16, + pub max_combo: i16, + pub mode: i32, + pub slider_breaks: i16, + pub unstable_rate: f64, + + pub passed_objects: usize, + + #[serde(default = "SS")] + pub grade: &'static str, + pub current_hp: f64, + pub current_hp_smooth: f64, +} + +impl GameplayValues { + #[inline] + pub fn gamemode(&self) -> GameMode { + let _span = tracy_client::span!("gamplay gamemode"); + GameMode::from(self.mode as u8) + } + + pub fn passed_objects(&self) -> Result { + let _span = tracy_client::span!("passed objects"); + + let value = match self.gamemode() { + GameMode::Osu => + self.hit_300 + self.hit_100 + + self.hit_50 + self.hit_miss, + GameMode::Taiko => + self.hit_300 + self.hit_100 + self.hit_miss, + GameMode::Catch => + self.hit_300 + self.hit_100 + + self.hit_50 + self.hit_miss + + self.hit_katu, + GameMode::Mania => + self.hit_300 + self.hit_100 + + self.hit_50 + self.hit_miss + + self.hit_katu + self.hit_geki, + }; + + usize::try_from(value) + } + + + pub fn get_current_grade(&self) -> &'static str { + let _span = tracy_client::span!("calculate current grade"); + let total_hits = self.passed_objects as f64; + let base_grade = match self.gamemode() { + GameMode::Osu => { + let ratio300 = self.hit_300 as f64 / total_hits; + let ratio50 = self.hit_50 as f64 / total_hits; + if self.accuracy == 1. { + "SS" + } else if ratio300 > 0.9 + && self.hit_miss == 0 + && ratio50 <= 0.1 { + "S" + } else if ratio300 > 0.8 + && self.hit_miss == 0 || ratio300 > 0.9 { + "A" + } else if ratio300 > 0.7 + && self.hit_miss == 0 + || ratio300 > 0.8 { + "B" + } else if ratio300 > 0.6 { + "C" + } else { + "D" + } + }, + GameMode::Taiko => { + let ratio300 = self.hit_300 as f64 / total_hits; + if self.accuracy == 1. { + "SS" + } else if ratio300 > 0.9 && self.hit_miss == 0 { + "S" + } else if ratio300 > 0.8 + && self.hit_miss == 0 + || ratio300 > 0.9 { + "A" + } else if ratio300 > 0.7 + && self.hit_miss == 0 + || ratio300 > 0.8 { + "B" + } else if ratio300 > 0.6 { + "C" + } else { + "D" + } + }, + GameMode::Catch => { + if self.accuracy == 1. { + "SS" + } else if self.accuracy > 0.98 { + "S" + } else if self.accuracy > 0.94 { + "A" + } else if self.accuracy > 0.90 { + "B" + } else if self.accuracy > 0.85 { + "C" + } else { + "D" + } + }, + GameMode::Mania => { + if self.accuracy == 1. { + "SS" + } else if self.accuracy > 0.95 { + "S" + } else if self.accuracy > 0.9 { + "A" + } else if self.accuracy > 0.8 { + "B" + } else if self.accuracy > 0.7 { + "C" + } else { + "D" + } + } + }; + + // Hidden | Flashlight | Fade In + match (base_grade, self.mods & (8 | 1024 | 1048576)) { + ("SS", conj) if conj > 0 => "SSH", + ("S", conj) if conj > 0 => "SH", + _ => base_grade + } + } + + pub fn update_accuracy(&mut self) { + let _span = tracy_client::span!("calculate accuracy"); + + let acc: f64 = 'blk: { + if self.passed_objects == 0 { + break 'blk 1.; + } + + calculate_accuracy!(self) + }; + + self.accuracy = acc; + } + + pub fn calculate_unstable_rate(&self) -> f64 { + let _span = tracy_client::span!("calculate ur"); + + if self.hit_errors.is_empty() { + return 0.0 + }; + + let hit_errors_len = self.hit_errors.len() as i32; + + let total: &i32 = &self.hit_errors.iter().sum(); + let average = total / hit_errors_len; + + let mut variance = 0; + for hit in &self.hit_errors { + variance += i32::pow(*hit - average, 2) + } + + variance /= hit_errors_len; + + f64::sqrt(variance as f64) * 10.0 + } +} + +#[derive(Debug, Default, Serialize)] +pub struct OutputValues { + /// Absolute path to the osu! folder + /// Example: `/path/to/osu` + /// Used internally + #[serde(skip)] + pub osu_path: PathBuf, + + #[serde(skip)] + pub current_beatmap: Option, + + #[serde(skip)] + pub prev_combo: i16, + #[serde(skip)] + pub prev_hit_miss: i16, + #[serde(skip)] + pub prev_playtime: i32, + #[serde(skip)] + pub prev_passed_objects: usize, + #[serde(skip)] + pub prev_state: GameState, + #[serde(skip)] + pub prev_menu_mods: u32, + #[serde(skip)] + pub prev_menu_mode: i32, + #[serde(skip)] + pub delta_sum: usize, + + /// Name of the current skin + pub skin: String, + + /// Skin folder relative to the osu! folder + pub skin_folder: String, + + /// Is chat enabled (F9/F8) + pub chat_enabled: bool, + + /// Playtime in milliseconds + /// `Playing` => represents your progress into current beatmap + /// `SongSelect` => represents progress of mp3 + /// Note: can be negative + pub playtime: i32, + + /// Current gamemode on `SongSelect` state + pub menu_mode: i32, + + /// Current state of the game + pub state: GameState, + + /// Stars of current beatmap without any mods + pub stars: f64, + + /// Stars of current beatmap taking in account state and mods + /// `Playing` => using gameplay mods + /// `SongSelect` => using menu_mods + /// `ResultScreen` => using result_screen mods + pub stars_mods: f64, + + /// Stars calculated during gameplay and based on + /// current gameplay mods and passed objects + /// calculated gradually + pub current_stars: f64, + + /// Result Screen info + pub result_screen: ResultScreenValues, + + /// Gameplay info + pub gameplay: GameplayValues, + + /// Beatmap info + pub beatmap: BeatmapValues, + + // KeyOverlay infi + pub keyoverlay: KeyOverlayValues, + + /// BPM calculated during gameplay + /// based on your progress into the beatmap and gameplay mods + pub current_bpm: f64, + + /// Is kiai is active now + /// based on your progress into the beatmap + pub kiai_now: bool, + + /// Current PP based on your state + /// + /// `Playing` => based on your progress into the beatmap + /// and gameplay mods + /// `SongSelect` => ss_pp for current map using menu_mods + /// `ResultScreen` => pp calculated for score on the screen + /// (values are taken from result_screen) + pub current_pp: f64, + + /// Fullcombo PP during gameplay + /// based on your progress into the beatmap and gameplay mods + /// basically just removes misses + pub fc_pp: f64, + + /// SS PP's + /// based on your progress into the beatmap and mods + /// `Playing` => using gameplay mods + /// `SongSelect` => using menu_mods + /// `ResultScreen` => using result_screen mods + pub ss_pp: f64, + + /// Mods on `SongSelect` state + pub menu_mods: u32, + + /// String representation of current selected mods + /// `Playing` => using gameplay mods + /// `SongSelect` => using menu_mods + /// `ResultScreen` => using result_screen mods + pub mods_str: Vec<&'static str>, + + pub plays: i32, + + /// Position of current playing audio in milliseconds + /// (to be honest it have nothing to do with precision) + pub precise_audio_time: i32, +} + +impl OutputValues { + // Reseting values should happen from `OutputValues` functions + // Separating it in individual functions gonna decrease readability + // a lot + pub fn reset_gameplay(&mut self) { + let _span = tracy_client::span!("reset gameplay!"); + + self.keyoverlay.reset(); + + self.prev_combo = 0; + self.prev_hit_miss = 0; + self.prev_playtime = 0; + + self.mods_str.clear(); + + self.current_pp = 0.0; + self.fc_pp = 0.0; + self.ss_pp = 0.0; + + self.current_bpm = 0.0; + self.prev_passed_objects = 0; + self.delta_sum = 0; + self.kiai_now = false; + + self.gameplay.slider_breaks = 0; + self.gameplay.score = 0; + self.gameplay.hit_300 = 0; + self.gameplay.hit_100 = 0; + self.gameplay.hit_50 = 0; + self.gameplay.hit_geki = 0; + self.gameplay.passed_objects = 0; + self.gameplay.hit_katu = 0; + self.gameplay.hit_miss = 0; + self.gameplay.combo = 0; + self.gameplay.max_combo = 0; + self.gameplay.mode = 0; + self.gameplay.slider_breaks = 0; + self.gameplay.current_hp = 0.0; + self.gameplay.current_hp_smooth = 0.0; + + self.gameplay.unstable_rate = 0.0; + } + + #[inline] + pub fn menu_gamemode(&self) -> GameMode { + let _span = tracy_client::span!("menu gamemody"); + GameMode::from(self.menu_mode as u8) + } + + pub fn update_min_max_bpm(&mut self) { + if let Some(beatmap) = &self.current_beatmap { + // Maybe this is not very idiomatic approach + // but atleast we dont need to iterate twice + // to calculate min and max values + let mut max_bpm = f64::MIN; + let mut min_bpm = f64::MAX; + + for timing_point in beatmap.timing_points.iter() { + let bpm = 60000.0 / timing_point.beat_len; + + if bpm > max_bpm { max_bpm = bpm }; + if bpm < min_bpm { min_bpm = bpm }; + } + + self.beatmap.max_bpm = max_bpm; + self.beatmap.min_bpm = min_bpm; + } + + } + + pub fn update_current_bpm(&mut self) { + let _span = tracy_client::span!("get current bpm"); + + let bpm = if let Some(beatmap) = &self.current_beatmap { + 60000.0 / beatmap + .timing_point_at(self.playtime as f64) + .beat_len + } else { + self.current_bpm + }; + + self.current_bpm = bpm; + } + + pub fn update_kiai(&mut self) { + let _span = tracy_client::span!("get_kiai"); + + self.kiai_now = if let Some(beatmap) = &self.current_beatmap { + // TODO: get rid of extra allocation? + let kiai_data: Option = beatmap + .effect_point_at(self.playtime as f64); + if let Some(kiai) = kiai_data { + kiai.kiai + } else { + self.kiai_now + } + } else { + self.kiai_now + } + } + + /// Depends on `GameplayValues` and `ResultScreenValues` + pub fn update_current_pp(&mut self, ivalues: &mut InnerValues) { + // TODO refactor this function in near future + // maybe even split pp into struct aka `GameplayValues` -> pp + // etc + let _span = tracy_client::span!("get_current_pp"); + + if self.state == GameState::ResultScreen { + if let Some(beatmap) = &self.current_beatmap { + let attr = beatmap + .pp() + .mode(self.result_screen.gamemode()) + .mods(self.result_screen.mods) + .n300(self.result_screen.hit_300 as usize) + .n100(self.result_screen.hit_100 as usize) + .n50(self.result_screen.hit_50 as usize) + .n_geki(self.result_screen.hit_geki as usize) + .n_katu(self.result_screen.hit_katu as usize) + .n_misses(self.result_screen.hit_miss as usize) + .calculate(); + + self.current_pp = attr.pp(); + } + + return; + } + + // TODO yep it definitely should be refactored + if self.state == GameState::SongSelect { + self.current_pp = self.ss_pp; + } + + if let Some(beatmap) = &self.current_beatmap { + let score_state = ScoreState { + max_combo: self.gameplay.max_combo as usize, + n_geki: self.gameplay.hit_geki as usize, + n_katu: self.gameplay.hit_katu as usize, + n300: self.gameplay.hit_300 as usize, + n100: self.gameplay.hit_100 as usize, + n50: self.gameplay.hit_50 as usize, + n_misses: self.gameplay.hit_miss as usize, + }; + + let passed_objects = self.gameplay.passed_objects; + let prev_passed_objects = self.prev_passed_objects; + let delta = passed_objects - prev_passed_objects; + + let gradual = ivalues + .gradual_performance_current + .get_or_insert_with(|| { + // TODO: required until we rework the struct + let static_beatmap = unsafe { + extend_lifetime(beatmap) + }; + GradualPerformance::new( + static_beatmap, + self.gameplay.mods + ) + }); + + // delta can't be 0 as processing 0 actually processes 1 object + if (delta > 0) && (self.delta_sum <= prev_passed_objects) { + self.delta_sum += delta; + let attributes_option = gradual.nth(score_state, delta - 1); + match attributes_option { + Some(attributes) => { + self.current_pp = attributes.pp(); + self.current_stars = attributes.stars(); + } + None => { println!("Failed to calculate current pp/sr") } + } + + } + } + } + + /// Depends on `GameplayValues` + pub fn update_fc_pp(&mut self, ivalues: &mut InnerValues) { + let _span = tracy_client::span!("get_fc_pp"); + if let Some(beatmap) = &self.current_beatmap { + if ivalues.current_beatmap_perf.is_some() { + if let Some(attributes) = + ivalues.current_beatmap_perf.clone() { + let fc_pp = AnyPP::new(beatmap) + .attributes(attributes.clone()) + .mode(self.gameplay.gamemode()) + .mods(self.gameplay.mods) + .n300(self.gameplay.hit_300 as usize) + .n100(self.gameplay.hit_100 as usize) + .n50(self.gameplay.hit_50 as usize) + .n_geki(self.gameplay.hit_geki as usize) + .n_katu(self.gameplay.hit_katu as usize) + .n_misses(0) + .calculate(); + self.fc_pp = fc_pp.pp(); + } + else { + self.fc_pp = 0.0 + } + } else { + let attr = AnyPP::new(beatmap) + .mods(self.gameplay.mods) + .mode(self.gameplay.gamemode()) + .calculate(); + + let ss_pp = attr.pp(); + self.ss_pp = ss_pp; + + ivalues.current_beatmap_perf = Some(attr); + + self.fc_pp = ss_pp; + } + } else { + self.fc_pp = 0.0 + } + } + + /// Adjust bpm based on current state and mods + /// `Playing` => using gameplay mods + /// `SongSelect` => using menu_mods + /// + /// Depends on `GameplayValues` + pub fn adjust_bpm(&mut self) { + let _span = tracy_client::span!("adjust bpm"); + match self.state { + GameState::Playing => { + if self.gameplay.mods & 64 > 0 { + self.gameplay.unstable_rate /= 1.5; + self.current_bpm *= 1.5; + self.beatmap.bpm *= 1.5; + self.beatmap.max_bpm *= 1.5; + self.beatmap.min_bpm *= 1.5; + } + else if self.gameplay.mods & 256 > 0 { + self.gameplay.unstable_rate *= 0.75; + self.current_bpm *= 0.75; + self.beatmap.bpm *= 0.75; + self.beatmap.max_bpm *= 0.75; + self.beatmap.min_bpm *= 0.75; + } else { + self.update_min_max_bpm(); + + if let Some(beatmap) = &self.current_beatmap { + self.beatmap.bpm = beatmap.bpm(); + } + } + }, + GameState::SongSelect => { + if self.menu_mods & 64 > 0 { + self.beatmap.bpm *= 1.5; + self.beatmap.max_bpm *= 1.5; + self.beatmap.min_bpm *= 1.5; + } + else if self.menu_mods & 256 > 0 { + self.beatmap.bpm *= 0.75; + self.beatmap.max_bpm *= 0.75; + self.beatmap.min_bpm *= 0.75; + } else { + self.update_min_max_bpm(); + + if let Some(beatmap) = &self.current_beatmap { + self.beatmap.bpm = beatmap.bpm(); + } + } + }, + _ => () + } + } + + /// Returns mods depending on current game state + pub fn get_current_mods(&self) -> u32 { + match self.state { + GameState::Playing => self.gameplay.mods, + GameState::SongSelect => self.menu_mods, + GameState::ResultScreen => self.result_screen.mods, + _ => self.menu_mods + } + } + + pub fn update_stars_and_ss_pp(&mut self) { + let _span = tracy_client::span!("update stars and ss_pp"); + + if let Some(beatmap) = &self.current_beatmap { + let mods = { + match self.state { + GameState::Playing => self.gameplay.mods, + GameState::SongSelect => self.menu_mods, + GameState::ResultScreen => self.result_screen.mods, + _ => self.menu_mods + } + }; + + let mode = { + match self.state { + GameState::Playing => self.gameplay.gamemode(), + GameState::SongSelect => self.menu_gamemode(), + GameState::ResultScreen => self.result_screen.gamemode(), + _ => self.menu_gamemode() + } + }; + + self.stars = beatmap + .stars() + .mode(mode) // Catch convertions is + .calculate() // broken so converting + .stars(); // manually, read #57 & #55 + + let attr = beatmap + .pp() + .mode(mode) // ^ + .mods(mods) + .calculate(); + + self.stars_mods = attr.stars(); + self.ss_pp = attr.pp(); + } + } + + pub fn update_readable_mods(&mut self) { + let _span = tracy_client::span!("get_readable_mods"); + + let mods_values = match self.state { + GameState::Playing => self.gameplay.mods, + GameState::SongSelect => self.menu_mods, + GameState::ResultScreen=> self.result_screen.mods, + _ => self.menu_mods, + }; + + self.mods_str.clear(); + + MODS.iter() + .for_each(|(idx, name)| { + if let Some(m) = (mods_values & idx > 0).then_some(*name) { + self.mods_str.push(m); + } + }); + + if self.mods_str.contains(&"NC") { + self.mods_str.retain(|x| x != &"DT"); + } + + if self.mods_str.contains(&"PF") { + self.mods_str.retain(|x| x != &"SD"); + } + } + + /// Depends on `BeatmapValues` and `BeatmapPathValues` + pub fn update_full_paths(&mut self) { + let _span = tracy_client::span!("update_full_paths"); + + // beatmap_full_path is expection because + // it depends on previous state + + self.beatmap.paths.background_path_full + = self.osu_path.join("Songs/"); + + self.beatmap.paths.background_path_full + .push(&self.beatmap.paths.beatmap_folder); + + self.beatmap.paths.background_path_full + .push(&self.beatmap.paths.background_file); + } +} + +unsafe fn extend_lifetime(value: &T) -> &'static T { + std::mem::transmute(value) +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_mod_conversion() { + let mut values = OutputValues { + state: GameState::Playing, + gameplay: GameplayValues { + mods: 88, + ..Default::default() + }, + ..Default::default() + }; + + values.update_readable_mods(); + assert_eq!( + vec!["HD", "HR", "DT"], + values.mods_str + ); + + values.gameplay.mods = 584; + values.update_readable_mods(); + assert_eq!( + vec!["HD", "NC"], + values.mods_str + ); + + values.gameplay.mods = 1107561552; + values.update_readable_mods(); + assert_eq!( + vec!["HR","DT","FL","AU","K7","Coop","MR"], + values.mods_str + ); + } +} \ No newline at end of file