Compare commits

...

2 Commits

Author SHA1 Message Date
f7d23174a5 fix menu 2025-01-04 23:20:42 +01:00
80a4b9f1d1 add osu memory reader 2025-01-04 23:20:20 +01:00
24 changed files with 2240 additions and 76 deletions

BIN
bun.lockb

Binary file not shown.

View File

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

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,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::<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;
}
}
}
println!("{:?}", state.values.clone());
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

@ -5,8 +5,13 @@
<div
class="absolute w-screen h-screen overflow-hidden pointer-events-none rounded"
>
<div
<img
src={background}
alt="background"
class="absolute top-0 left-0 w-full h-full !bg-cover -z-10 pointer-events-none blur opacity-10 rounded"
/>
<!-- <div
style="background: url(https://osu.direct/api/media/background/2226722)"
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,6 @@
<script lang="ts">
import { onMount } from "svelte";
import ezppLogo from "../../../../assets/logo.png";
import { osudirect } from "@/api/osudirect";
import { playAudio } from "@/utils";
import { BeatmapDecoder } from "osu-parsers";
type logoProps = {
extended: boolean;
@ -13,57 +10,7 @@
let { extended, onclick }: logoProps = $props();
let hovered = $state(false);
let bpm = $state(150); // 1000 * 60 / bpm
let lastTimeout: number | undefined = undefined;
onMount(async () => {
const beatmapId = 2226722;
const beatmapData = await osudirect.osu(beatmapId);
if (beatmapData) {
const decoder = new BeatmapDecoder();
const beatmap = decoder.decodeFromString(beatmapData);
console.log(beatmap);
const audio = new Audio(
`https://osu.direct/api/media/audio/${beatmapId}`
);
audio.volume = 0.3;
// Function to play the heartbeat sound
const playHeartbeat = () => {
playAudio("/audio/menuHeartbeat.mp3", hovered ? 1 : 0.3);
};
// Function to synchronize the heartbeat with the song
const syncHeartbeat = () => {
const currentTime = audio.currentTime * 1000; // Convert to milliseconds
const timingPoint = beatmap.controlPoints.timingPointAt(currentTime);
if (timingPoint && bpm !== timingPoint.bpm) {
bpm = timingPoint.bpm;
if (lastTimeout) {
clearTimeout(lastTimeout);
}
const interval = (1000 * 60) / bpm; // Interval in milliseconds
const nextBeat = interval - (currentTime % interval); // Time to the next beat
setTimeout(() => {
playHeartbeat();
lastTimeout = setInterval(playHeartbeat, interval);
}, nextBeat);
}
// Continue syncing
requestAnimationFrame(syncHeartbeat);
};
// Start playback and syncing
audio.addEventListener("play", () => {
syncHeartbeat();
});
await audio.play();
}
});
const bpm = $state(130);
</script>
<div
@ -83,9 +30,9 @@
onmouseleave={() => (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();
}}

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,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);
</script>
<div class="relative h-screen w-screen">
<Background />
<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>
<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