Compare commits
2 Commits
d751ff4505
...
f7d23174a5
Author | SHA1 | Date | |
---|---|---|---|
f7d23174a5 | |||
80a4b9f1d1 |
@ -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
227
src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
462
src-tauri/src/reading_loop.rs
Normal file
462
src-tauri/src/reading_loop.rs
Normal 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
1065
src-tauri/src/structs.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
50
src/lib/components/ui/badge/badge.svelte
Normal file
50
src/lib/components/ui/badge/badge.svelte
Normal 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>
|
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
@ -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>
|
@ -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>
|
@ -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}
|
||||
/>
|
@ -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}
|
||||
/>
|
@ -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>
|
@ -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>
|
@ -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}
|
||||
/>
|
@ -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>
|
@ -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}
|
||||
/>
|
@ -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>
|
50
src/lib/components/ui/dropdown-menu/index.ts
Normal file
50
src/lib/components/ui/dropdown-menu/index.ts
Normal 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,
|
||||
};
|
@ -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();
|
||||
}}
|
||||
|
@ -3,3 +3,4 @@
|
||||
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
|
||||
export const prerender = true;
|
||||
export const ssr = false;
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user