This commit is contained in:
HorizonCode 2025-05-14 13:37:59 +02:00
commit 83a25c6911
23 changed files with 2284 additions and 34 deletions

View File

@ -23,7 +23,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"
@ -34,7 +34,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",

227
src-tauri/Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -1,16 +1,32 @@
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)]
{
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");
}));
}
@ -21,3 +37,116 @@ 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;
}
};
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::<ProcessError>() {
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::<ProcessError>() {
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;
}
}
}
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));
}
}
});
}

View File

@ -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::<f32>() * 4];
p.read(
beatmap_addr + 0x2c,
size_of::<f32>() * 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(())
}

1065
src-tauri/src/structs.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,5 @@
<script lang="ts">
import background from "../../../../assets/background.gif";
const prop: {beatmapId: number} = $props();
</script>
<div
@ -11,5 +8,5 @@
<div
style="background: url(https://osu.direct/api/media/background/{prop.beatmapId})"
class="absolute top-0 left-0 w-full h-full !bg-cover -z-10 pointer-events-none blur opacity-10 rounded"
></div>
></div> -->
</div>

View File

@ -0,0 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
{href}
class={cn(badgeVariants({ variant, className }))}
{...restProps}
>
{@render children?.()}
</svelte:element>

View File

@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<Minus class="size-4" />
{:else}
<Check class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View File

@ -0,0 +1,26 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
{sideOffset}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md outline-none",
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.GroupHeadingProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
/>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...restProps}
/>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { type WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@ -0,0 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<Circle class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View File

@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...restProps}
/>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { type WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...restProps}
>
{@render children?.()}
</span>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className
)}
{...restProps}
/>

View File

@ -0,0 +1,28 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRight class="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>

View File

@ -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,
};

View File

@ -1,9 +1,9 @@
<script lang="ts">
import { onMount } from "svelte";
import ezppLogo from "../../../../assets/logo.png";
import { osudirect } from "@/api/osudirect";
import { gameSounds, playAudio } from "@/utils";
import { BeatmapDecoder } from "osu-parsers";
import { onMount } from "svelte";
type logoProps = {
beatmapId: number;
@ -18,6 +18,8 @@
let lastTimeout: number | undefined = undefined;
onMount(async () => {
gameSounds.preload();
const beatmapData = await osudirect.osu(beatmapId);
if (beatmapData) {
const decoder = new BeatmapDecoder();
@ -69,16 +71,12 @@
// Wait for audio to be ready before starting playback and syncing
audio.addEventListener("canplay", async () => {
audio.addEventListener("play", syncHeartbeat);
audio.addEventListener("ended", async() => await audio.play());
audio.addEventListener("ended", async () => await audio.play());
await audio.play();
syncHeartbeat();
});
}
});
onMount(() => {
gameSounds.preload();
});
</script>
<div

View File

@ -3,3 +3,4 @@
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true;
export const ssr = false;

View File

@ -3,21 +3,63 @@
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";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
let progress = $state(0);
let extended = $state(false);
let beatmapId = $state(3820896);
const current = WebviewWindow.getCurrent();
current.setAlwaysOnTop(true);
</script>
<div class="relative h-screen w-screen">
<Background {beatmapId} />
<div class="absolute top-2 right-2 py-7">
<Avatar.Root>
<Avatar.AvatarFallback>U</Avatar.AvatarFallback>
<Avatar.AvatarImage src="https://a.ez-pp.farm/1001"></Avatar.AvatarImage>
</Avatar.Root>
<Background />
<div class="absolute z-20 top-2 right-2 py-7">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<div class="relative">
<p
class={twMerge(
badgeVariants(),
"p-0 h-5 w-5 absolute -right-0.5 -top-0.5 z-50 !bg-pink-600 border-2 border-pink-800 text-white"
)}
>
<Heart class="h-3 w-3 m-auto p-0" />
</p>
<Avatar.Root class="border-[3px] z-40">
<Avatar.AvatarFallback>U</Avatar.AvatarFallback>
<Avatar.AvatarImage src="https://a.ez-pp.farm/1001"
></Avatar.AvatarImage>
</Avatar.Root>
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-48 max-w-48 mx-2" side="bottom">
<DropdownMenu.Group>
<DropdownMenu.GroupHeading class="truncate"
>Hello, Quetzalcoatl!</DropdownMenu.GroupHeading
>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item class="cursor-pointer">
<Settings class="mr-2 size-4" />
<span>Settings</span>
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item class="cursor-pointer">
<LogOut class="mr-2 size-4" />
<span>Log out</span>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div