From 80a4b9f1d181b9d470cd852096a00cae78a38d75 Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Sat, 4 Jan 2025 23:20:20 +0100 Subject: [PATCH 1/3] 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 From f7d23174a5d8047e0195cdc15049ec11174d16ea Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Sat, 4 Jan 2025 23:20:42 +0100 Subject: [PATCH 2/3] fix menu --- bun.lockb | Bin 107187 -> 107173 bytes package.json | 4 +- .../ui/background/background.svelte | 9 ++- src/lib/components/ui/badge/badge.svelte | 50 +++++++++++++++ src/lib/components/ui/badge/index.ts | 2 + .../dropdown-menu-checkbox-item.svelte | 40 ++++++++++++ .../dropdown-menu-content.svelte | 26 ++++++++ .../dropdown-menu-group-heading.svelte | 19 ++++++ .../dropdown-menu/dropdown-menu-item.svelte | 23 +++++++ .../dropdown-menu/dropdown-menu-label.svelte | 23 +++++++ .../dropdown-menu-radio-item.svelte | 30 +++++++++ .../dropdown-menu-separator.svelte | 16 +++++ .../dropdown-menu-shortcut.svelte | 20 ++++++ .../dropdown-menu-sub-content.svelte | 19 ++++++ .../dropdown-menu-sub-trigger.svelte | 28 +++++++++ src/lib/components/ui/dropdown-menu/index.ts | 50 +++++++++++++++ src/lib/components/ui/logo/logo.svelte | 59 +----------------- src/routes/+layout.ts | 1 + src/routes/+page.svelte | 48 ++++++++++++-- 19 files changed, 402 insertions(+), 65 deletions(-) create mode 100644 src/lib/components/ui/badge/badge.svelte create mode 100644 src/lib/components/ui/badge/index.ts create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte create mode 100644 src/lib/components/ui/dropdown-menu/index.ts diff --git a/bun.lockb b/bun.lockb index 8be036112e213246edd95a163f6f7cf8d6a3e9eb..401cccf3a098a35a7c1cca21f343b3246e119193 100644 GIT binary patch delta 9429 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 U6{}ec6Gc;)x&8TO_G`eu09g9mg8%>k diff --git a/package.json b/package.json index be7ec56..4a9c08d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@tauri-apps/plugin-shell": "^2", "@tauri-apps/plugin-sql": "^2.2.0", "ky": "^1.7.3", - "lucide-svelte": "^0.468.0", + "lucide-svelte": "^0.469.0", "osu-classes": "^3.1.0", "osu-parsers": "^4.1.7", "radix-icons-svelte": "^1.2.1" @@ -33,7 +33,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "@tauri-apps/cli": "^2", "autoprefixer": "^10.4.20", - "bits-ui": "^1.0.0-next.70", + "bits-ui": "^1.0.0-next.74", "clsx": "^2.1.1", "sass-embedded": "^1.82.0", "svelte": "^5.0.0", diff --git a/src/lib/components/ui/background/background.svelte b/src/lib/components/ui/background/background.svelte index 660cad9..077076d 100644 --- a/src/lib/components/ui/background/background.svelte +++ b/src/lib/components/ui/background/background.svelte @@ -5,8 +5,13 @@
-
+
diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..9c7457a --- /dev/null +++ b/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/src/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..20e3777 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,40 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..fdbaa47 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..84d5cca --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..70a5236 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..9837d5a --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..0f219da --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,30 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..32fac4b --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..053e2a2 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..0bb6eea --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..c2dcd32 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,28 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..40c4502 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,50 @@ +import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; + +const Sub = DropdownMenuPrimitive.Sub; +const Root = DropdownMenuPrimitive.Root; +const Trigger = DropdownMenuPrimitive.Trigger; +const Group = DropdownMenuPrimitive.Group; +const RadioGroup = DropdownMenuPrimitive.RadioGroup; + +export { + CheckboxItem, + Content, + Root as DropdownMenu, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Group as DropdownMenuGroup, + GroupHeading as DropdownMenuGroupHeading, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/src/lib/components/ui/logo/logo.svelte b/src/lib/components/ui/logo/logo.svelte index d53e3db..d6031f9 100644 --- a/src/lib/components/ui/logo/logo.svelte +++ b/src/lib/components/ui/logo/logo.svelte @@ -1,9 +1,6 @@
(hovered = false)} onclick={() => { if (extended) { - playAudio("/audio/menuBack.wav", 1); + playAudio("/audio/menuBack.wav", 0.35); } else { - playAudio("/audio/menuHit.wav", 1); + playAudio("/audio/menuHit.wav", 0.35); } onclick(); }} diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index f4fb689..958328b 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -3,3 +3,4 @@ // See: https://v2.tauri.app/start/frontend/sveltekit/ for more info export const prerender = true; export const ssr = false; + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d0ae83c..f95d8ed 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,18 +3,56 @@ import Button from "@/components/ui/button/button.svelte"; import Logo from "@/components/ui/logo/logo.svelte"; import * as Avatar from "@/components/ui/avatar"; + import * as DropdownMenu from "@/components/ui/dropdown-menu"; import Progressbar from "@/components/ui/progressbar/progressbar.svelte"; + import Settings from "lucide-svelte/icons/settings"; + import LogOut from "lucide-svelte/icons/log-out"; + import Heart from "lucide-svelte/icons/heart"; + import { badgeVariants } from "@/components/ui/badge"; + import { twMerge } from "tailwind-merge"; let progress = $state(0); let extended = $state(false);
-
- - U - - +
+ + +
+

+ +

+ + U + + +
+
+ + + Hello, Quetzalcoatl! + + + + + Settings + + + + + + Log out + + + +
Date: Tue, 21 Jan 2025 21:15:40 +0100 Subject: [PATCH 3/3] fix osu detection --- src-tauri/src/lib.rs | 91 +++++++++++++++++++++++++++++------------ src/routes/+page.svelte | 4 ++ 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1ce1294..84b8437 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,10 +22,11 @@ pub fn run() { #[cfg(desktop)] { builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { - let _ = app - .get_webview_window("main") - .expect("no main window") - .set_focus(); + let app_window = app.get_webview_window("main").expect("no main window"); + app_window + .set_always_on_top(true) + .expect("failed to set always on top"); + app_window.set_focus().expect("failed to focus"); })); } @@ -59,26 +60,60 @@ fn osu_memory_reading() { } }; + let mut values = state.values.lock().unwrap(); + + println!("Using auto-detected osu! folder path"); + if let Some(ref dir) = p.executable_dir { + values.osu_path.clone_from(dir); + } else { + println!( + "{:?}", + Report::msg( + "Can't auto-detect osu! folder path \ + nor any was provided through command \ + line argument", + ) + ); + continue 'init_loop; + } + + if !values.osu_path.exists() { + println!( + "Provided osu path doesn't exists!\n Path: {}", + &values.osu_path.to_str().unwrap() + ); + + println!( + "{:?}", + Report::msg( + "Can't auto-detect osu! folder path \ + nor any was provided through command \ + line argument", + ) + ); + continue 'init_loop; + }; + + drop(values); + 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 - }, + 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; } }, }; @@ -91,13 +126,13 @@ fn osu_memory_reading() { Some(&ProcessError::ProcessNotFound) => { thread::sleep(Duration::from_millis(5000)); continue 'init_loop; - }, + } #[cfg(target_os = "windows")] - Some(&ProcessError::OsError{ .. }) => { + Some(&ProcessError::OsError { .. }) => { println!("{:?}", e); thread::sleep(Duration::from_millis(5000)); - continue 'init_loop - }, + continue 'init_loop; + } Some(_) | None => { println!("{:?}", e); thread::sleep(Duration::from_millis(5000)); @@ -105,7 +140,11 @@ fn osu_memory_reading() { } } } - println!("{:?}", state.values.clone()); + let cloned_values = state.values.clone(); + let values_lock = cloned_values.lock().unwrap(); + let values = &*values_lock; + + println!("{:?}", values.current_stars); thread::sleep(Duration::from_millis(2000)); } } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f95d8ed..9a1cc94 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -10,8 +10,12 @@ import Heart from "lucide-svelte/icons/heart"; import { badgeVariants } from "@/components/ui/badge"; import { twMerge } from "tailwind-merge"; + import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; let progress = $state(0); let extended = $state(false); + + const current = WebviewWindow.getCurrent(); + current.setAlwaysOnTop(true);