From dd0b54b81346fa2185091d3a2c455cd9ba021240 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Thu, 11 Dec 2025 19:35:21 +0100 Subject: [PATCH 01/16] Working minecraft renderer --- Cargo.lock | 5 +- client/Cargo.toml | 5 +- client/build.rs | 13 +- client/src/hook.rs | 331 +++++++++++++++++++++++++++++++++++++-------- client/src/lib.rs | 10 +- 5 files changed, 304 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 87c39f4..b58fe74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,6 +669,7 @@ dependencies = [ "cfg-if", "eframe", "egui", + "gl_generator", "ilhook", "jni", "libc", @@ -1162,7 +1163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2725,7 +2726,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/client/Cargo.toml b/client/Cargo.toml index 44b9ef2..c2f6e1e 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -20,4 +20,7 @@ anyhow = "1.0" libc = "0.2.178" libloading = "0.9.0" cfg-if = "1.0.4" -ilhook = "2.3.0" \ No newline at end of file +ilhook = "2.3.0" + +[build-dependencies] +gl_generator = "0.14" \ No newline at end of file diff --git a/client/build.rs b/client/build.rs index 8f665de..5d10603 100644 --- a/client/build.rs +++ b/client/build.rs @@ -3,6 +3,11 @@ // It finds the `jvm.lib` import library that is required to link JNI functions. // On Linux, this is unnecessary because the linker can directly use libjvm.so. +use std::env; +use std::fs::File; +use std::path::Path; +use gl_generator::{Api, Fallbacks, GlobalGenerator, Profile, Registry}; + #[cfg(windows)] fn main() { use std::path::PathBuf; @@ -70,7 +75,13 @@ fn main() { #[cfg(not(windows))] fn main() { - // On non-Windows systems this build script does nothing. + let dest = env::var("OUT_DIR").unwrap(); + let mut file = File::create(&Path::new(&dest).join("bindings.rs")).unwrap(); + + // Qui chiediamo il profilo "Compatibility" che include glBegin, glEnd, etc. + Registry::new(Api::Gl, (2, 1), Profile::Compatibility, Fallbacks::All, []) + .write_bindings(GlobalGenerator, &mut file) + .unwrap(); } #[cfg(windows)] diff --git a/client/src/hook.rs b/client/src/hook.rs index 7e83ac3..2215571 100644 --- a/client/src/hook.rs +++ b/client/src/hook.rs @@ -1,32 +1,195 @@ +use std::fs::File; +use std::io::{BufRead, BufReader}; use crate::client::DarkClient; use crate::mapping::client::minecraft::Minecraft; use cfg_if::cfg_if; -use log::info; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::Once; +use log::{info, error}; +use std::sync::atomic::{AtomicI32, AtomicBool, Ordering}; +use std::sync::{Mutex, Once, OnceLock}; +use std::time::Instant; +use ilhook::x64::HookPoint; +use libc::{RTLD_GLOBAL, RTLD_LAZY}; +// Importa il crate gl per disegnare +use crate::{gl, RUNNING}; static LAST_TICK: AtomicI32 = AtomicI32::new(0); -static INIT: Once = Once::new(); +static GL_LOADED: AtomicBool = AtomicBool::new(false); + +// Creiamo un wrapper per aggirare il blocco del compilatore +pub struct HookHandle(HookPoint); + +// DICIAMO A RUST: "Fidati, posso spostare questo oggetto tra thread" +unsafe impl Send for HookHandle {} +unsafe impl Sync for HookHandle {} + +// Global storage per l'hook attivo. +// Usiamo un Mutex per poterlo modificare (rimuovere) a runtime. +// Nota: Il tipo esatto dipende da cosa restituisce hooker.hook(). +// Solitamente è un oggetto che implementa Drop o ha un metodo unhook. +// Per ilhook-rs, l'oggetto `Hook` gestisce l'unhooking quando viene droppato. +// Aggiorniamo lo storage globale per usare questo tipo specifico, non dyn Any +static GLOBAL_HOOK: OnceLock>> = OnceLock::new(); + +fn get_global_hook() -> &'static Mutex> { + GLOBAL_HOOK.get_or_init(|| Mutex::new(None)) +} cfg_if! { if #[cfg(target_os = "linux")] { - use std::ffi::CString; + use std::ffi::{CString, CStr}; use ilhook::x64::{Hooker, Registers, CallbackOption, HookFlags, HookType}; + use libc::{c_void, c_char}; + + // Helper per caricare le funzioni OpenGL su Linux + fn get_proc_address(addr: &str) -> *const c_void { + unsafe { + let s = CString::new(addr).unwrap(); + // Prova prima con glXGetProcAddress se disponibile, altrimenti dlsym + // Qui usiamo un approccio semplificato assumendo che libGL sia caricata + let lib = libc::dlopen(CString::new("libGL.so.1").unwrap().as_ptr(), libc::RTLD_LAZY); + if !lib.is_null() { + libc::dlsym(lib, s.as_ptr()) + } else { + std::ptr::null() + } + } + } - // JmpBackRoutine signature: unsafe extern "win64" fn(*mut Registers, usize) - // Note: "win64" ABI is available on x86_64 Linux in Rust. unsafe extern "win64" fn my_swap_buffers_hook(_regs: *mut Registers, _user_data: usize) { - check_tick(); + on_frame(); } + } else if #[cfg(target_os = "windows")] { use ilhook::x64::{Hooker, Registers, CallbackOption, HookFlags, HookType}; + use libloading::os::windows::{Library, Symbol}; + use std::ffi::CString; + + // Helper per caricare le funzioni OpenGL su Windows + fn get_proc_address(addr: &str) -> *const std::ffi::c_void { + unsafe { + // Metodo robusto: prova wglGetProcAddress, poi GetProcAddress + let c_str = CString::new(addr).unwrap(); + + // Nota: In un contesto reale, dovremmo aver caricato opengl32.dll staticamente o lazy + // Qui facciamo un tentativo "sporco" ma funzionale per l'injection + let lib = libloading::Library::new("opengl32.dll"); + if let Ok(l) = lib { + // Prima prova wglGetProcAddress (per estensioni moderne) + let wgl_get: Result *const std::ffi::c_void>, _> = l.get(b"wglGetProcAddress"); + if let Ok(wgl) = wgl_get { + let ptr = wgl(c_str.as_ptr()); + if !ptr.is_null() { + return ptr; + } + } + // Fallback a GetProcAddress (per funzioni base GL 1.1) + let func: Result, _> = l.get(c_str.as_bytes()); + if let Ok(f) = func { + return *f as *const std::ffi::c_void; + } + } + std::ptr::null() + } + } unsafe extern "win64" fn my_swap_buffers_hook(_regs: *mut Registers, _user_data: usize) { - check_tick(); + on_frame(); } } } +const CONTEXT_PROFILE_MASK: u32 = 0x9126; +const CONTEXT_CORE_PROFILE_BIT: u32 = 0x00000001; + +// Funzione chiamata ogni frame grafico +unsafe fn on_frame() { + // === PANIC CHECK (La modifica importante) === + // Se cleanup_client() è stato chiamato, RUNNING diventa false. + // Se è false, usciamo IMMEDIATAMENTE. Non tickiamo, non disegniamo. + if !RUNNING.load(Ordering::SeqCst) { + return; + } + + // Inizializza GL pointers... + if !GL_LOADED.load(Ordering::Relaxed) { + gl::load_with(|s| get_proc_address(s)); + GL_LOADED.store(true, Ordering::Relaxed); + } + + check_tick(); + render_overlay(); +} + +// === LOGICA DI RENDERING (Fix per i glitch grafici) === +unsafe fn render_overlay() { + // === 1. DATI FINESTRA === + // Otteniamo le dimensioni reali della finestra (funziona anche se ridimensioni) + let mut viewport = [0; 4]; // [x, y, width, height] + gl::GetIntegerv(gl::VIEWPORT, viewport.as_mut_ptr()); + + let screen_w = viewport[2]; + let screen_h = viewport[3]; + + // === 2. BACKUP STATO === + let is_scissor_on = gl::IsEnabled(gl::SCISSOR_TEST) == gl::TRUE; + let mut old_scissor_box = [0; 4]; + gl::GetIntegerv(gl::SCISSOR_BOX, old_scissor_box.as_mut_ptr()); + let mut old_clear_color = [0.0; 4]; + gl::GetFloatv(gl::COLOR_CLEAR_VALUE, old_clear_color.as_mut_ptr()); + + // Abilita il taglio + gl::Enable(gl::SCISSOR_TEST); + + // === 3. DISEGNO PIXEL ART (Helper Closure) === + // Definiamo un colore Giallo + gl::ClearColor(1.0, 1.0, 0.0, 1.0); + + // Funzione interna per disegnare un blocco + // x, y sono relativi all'angolo IN ALTO A SINISTRA + let draw_block = |x: i32, y: i32, w: i32, h: i32| { + // OpenGL ha (0,0) in BASSO a sinistra. Dobbiamo invertire la Y. + // Y_gl = AltezzaSchermo - Y_alto - AltezzaBlocco + let gl_y = screen_h - y - h; + + gl::Scissor(x, gl_y, w, h); + gl::Clear(gl::COLOR_BUFFER_BIT); + }; + + // --- DISEGNO LA "D" (Di Dark) --- + // Posizione: 10px dal bordo sinistro, 10px dal bordo alto + let start_x = 10; + let start_y = 10; + let thickness = 5; + let size = 30; // Altezza lettera + + // Asta Verticale + draw_block(start_x, start_y, thickness, size); + // Trattino Alto + draw_block(start_x, start_y, size - 10, thickness); + // Trattino Basso + draw_block(start_x, start_y + size - thickness, size - 10, thickness); + // Asta Destra (chiusura D) + draw_block(start_x + size - 10, start_y + thickness, thickness, size - (thickness * 2)); + + // --- DISEGNO LA "C" (Di Client) --- + // Spostiamoci a destra + let start_x = 50; + + // Asta Verticale + draw_block(start_x, start_y, thickness, size); + // Trattino Alto + draw_block(start_x, start_y, size - 5, thickness); + // Trattino Basso + draw_block(start_x, start_y + size - thickness, size - 5, thickness); + + // === 4. RIPRISTINO STATO === + gl::ClearColor(old_clear_color[0], old_clear_color[1], old_clear_color[2], old_clear_color[3]); + gl::Scissor(old_scissor_box[0], old_scissor_box[1], old_scissor_box[2], old_scissor_box[3]); + if !is_scissor_on { + gl::Disable(gl::SCISSOR_TEST); + } +} + fn check_tick() { let client = DarkClient::instance(); // Try to get env without attaching if possible, or attach as daemon. @@ -37,6 +200,11 @@ fn check_tick() { let minecraft = Minecraft::instance(); + // Controllo errori per evitare crash + if minecraft.player.entity.is_null() { + return; + } + let tick_count = match minecraft.player.entity.get_tick_count() { Ok(t) => t, Err(_) => return, @@ -50,68 +218,121 @@ fn check_tick() { } } +fn find_library_path(partial_name: &str) -> Option { + if let Ok(file) = File::open("/proc/self/maps") { + let reader = BufReader::new(file); + for line in reader.lines() { + if let Ok(l) = line { + // Cerchiamo una riga che contenga il nome (es. "liblwjgl_opengl.so") + if l.contains(partial_name) && l.contains(".so") { + // Il formato è: indirizzo permessi offset dev inode PERCORSO + // Prendiamo l'ultima parte della stringa + if let Some(path) = l.split_whitespace().last() { + return Some(path.to_string()); + } + } + } + } + } + None +} + pub fn install_hooks() -> anyhow::Result<()> { cfg_if! { if #[cfg(target_os = "linux")] { unsafe { - let lib_name = CString::new("libGL.so.1")?; - let symbol_name = CString::new("glXSwapBuffers")?; + let mut targets = Vec::new(); - let lib = libc::dlopen(lib_name.as_ptr(), libc::RTLD_LAZY); - if lib.is_null() { - return Err(anyhow::anyhow!("Failed to load libGL.so.1")); + if let Some(path) = find_library_path("libglfw.so") { + info!("Trovata libreria GLFW: {}", path); + targets.push((path, "glfwSwapBuffers")); + } + else if let Some(path) = find_library_path("liblwjgl.so") { + info!("Trovata libreria LWJGL (Legacy): {}", path); + targets.push((path, "glXSwapBuffers")); + } + else { + info!("Nessuna libreria specifica trovata, provo libGL di sistema..."); + targets.push(("libGL.so.1".to_string(), "glXSwapBuffers")); } - let target_addr = libc::dlsym(lib, symbol_name.as_ptr()) as usize; + let mut hooked_count = 0; - if target_addr == 0 { - return Err(anyhow::anyhow!("Failed to find glXSwapBuffers")); - } + for (lib_path, func_name) in targets { + let c_lib_path = CString::new(lib_path.clone())?; + let c_func_name = CString::new(func_name)?; + + let lib = libc::dlopen(c_lib_path.as_ptr(), libc::RTLD_LAZY); + + if !lib.is_null() { + let target_addr = libc::dlsym(lib, c_func_name.as_ptr()) as usize; + + if target_addr != 0 { + info!("Trovato {} in {} a 0x{:x}", func_name, lib_path, target_addr); + + let hooker = Hooker::new( + target_addr, + HookType::JmpBack(my_swap_buffers_hook), + CallbackOption::None, + 0, + HookFlags::empty() + ); - // ilhook usage with JmpBack - let hooker = Hooker::new( - target_addr, - HookType::JmpBack(my_swap_buffers_hook), - CallbackOption::None, - 0, // user_data - HookFlags::empty() - ); + match hooker.hook() { + Ok(hook) => { + // Recuperiamo il lock globale + let mut guard = get_global_hook().lock().unwrap(); - let hook = hooker.hook(); - let hook = hook?; + // Se c'era un hook vecchio, lo sovrascriviamo (triggerando l'unhook automatico del vecchio) + if guard.is_some() { + info!("Rilevato vecchio hook, rimozione e sostituzione..."); + } - // We don't need trampoline for JmpBack hook as it jumps back automatically. + // === QUI LA MAGIA === + // Avvolgiamo l'hook nel nostro wrapper "Thread Safe" + *guard = Some(HookHandle(hook)); - // We need to keep the hook alive? - Box::leak(Box::new(hook)); + hooked_count += 1; + info!(">>> HOOK ATTIVO E SALVATO SU: {} <<<", lib_path); - info!("glXSwapBuffers hooked with ilhook (JmpBack)!"); + // Usciamo dal loop, ne basta uno attivo + break; + }, + Err(e) => { + info!("Errore installazione hook su {}: {:?}", lib_path, e); + } + } + } + } + } + + if hooked_count == 0 { + // Qui potremmo ritornare errore, MA se stiamo re-iniettando e qualcosa è andato storto + // col flag statico, potremmo voler "fingere" che vada tutto bene. + // Tuttavia, per ora lasciamo l'errore se count è 0. + return Err(anyhow::anyhow!("Fallito l'hook su tutte le librerie candidate!")); + } } } else if #[cfg(target_os = "windows")] { - unsafe { - let lib = libloading::Library::new("opengl32.dll")?; - let func: libloading::Symbol = lib.get(b"wglSwapBuffers")?; - let target_addr = *func as usize; - - // ilhook usage with JmpBack - let hooker = Hooker::new( - target_addr, - HookType::JmpBack(my_swap_buffers_hook), - CallbackOption::None, - 0, // user_data - HookFlags::empty() - ); - - let hook = hooker.hook(); - let hook = hook?; - - Box::leak(Box::new(hook)); - // Leak library to keep it loaded - Box::leak(Box::new(lib)); - - info!("wglSwapBuffers hooked with ilhook (JmpBack)!"); - } + // ... Codice Windows (non cambia molto, ma aggiungi il check HOOKS_INSTALLED se vuoi) ... + // Per brevità ometto, ma il concetto è identico. } } Ok(()) } + +pub fn uninstall_hooks() { + // Prendiamo il lock + let mut guard = get_global_hook().lock().unwrap(); + + if guard.is_some() { + info!("Rimozione hook fisico in corso..."); + // Impostando a None, il Wrapper viene distrutto. + // Il Wrapper distrugge l'HookPoint interno. + // L'HookPoint interno ripristina i byte originali della memoria. + *guard = None; + info!("Hook rimosso correttamente. Memoria pulita."); + } else { + info!("Nessun hook attivo da rimuovere."); + } +} \ No newline at end of file diff --git a/client/src/lib.rs b/client/src/lib.rs index a77eb57..28dbceb 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -7,6 +7,10 @@ mod hook; mod mapping; mod module; +pub mod gl { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + use crate::client::keyboard::{start_keyboard_handler, stop_keyboard_handler}; use crate::client::DarkClient; use crate::gui::start_gui; @@ -21,11 +25,12 @@ use std::fs::File; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, OnceLock}; use std::thread; +use crate::hook::uninstall_hooks; static GUI_THREAD: OnceLock>>> = OnceLock::new(); // Flag to control if the client is running -static RUNNING: AtomicBool = AtomicBool::new(false); +pub static RUNNING: AtomicBool = AtomicBool::new(false); fn gui_thread() -> &'static Mutex>> { GUI_THREAD.get_or_init(|| Mutex::new(None)) @@ -86,6 +91,9 @@ pub extern "C" fn cleanup_client() { // Set the execution flag to false RUNNING.store(false, Ordering::SeqCst); + // RIMUOVI FISICAMENTE L'HOOK + uninstall_hooks(); + // Stop the keyboard handler stop_keyboard_handler(); From 20568d29cd0eff795d09b9a6a5d8d1d95d93b03c Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Fri, 20 Feb 2026 12:43:30 +0100 Subject: [PATCH 02/16] Render a GUI in minecraft --- Cargo.lock | 1 + client/Cargo.toml | 1 + client/build.rs | 6 +- client/src/graphic/font.rs | 180 +++++++++++ client/src/graphic/hook.rs | 332 ++++++++++++++++++++ client/src/graphic/input.rs | 504 +++++++++++++++++++++++++++++++ client/src/graphic/mod.rs | 6 + client/src/graphic/render.rs | 309 +++++++++++++++++++ client/src/graphic/ui.rs | 207 +++++++++++++ client/src/graphic/ui_manager.rs | 153 ++++++++++ client/src/gui.rs | 7 + client/src/hook.rs | 338 --------------------- client/src/lib.rs | 17 +- 13 files changed, 1717 insertions(+), 344 deletions(-) create mode 100644 client/src/graphic/font.rs create mode 100644 client/src/graphic/hook.rs create mode 100644 client/src/graphic/input.rs create mode 100644 client/src/graphic/mod.rs create mode 100644 client/src/graphic/render.rs create mode 100644 client/src/graphic/ui.rs create mode 100644 client/src/graphic/ui_manager.rs delete mode 100644 client/src/hook.rs diff --git a/Cargo.lock b/Cargo.lock index b58fe74..5144826 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -672,6 +672,7 @@ dependencies = [ "gl_generator", "ilhook", "jni", + "lazy_static", "libc", "libloading 0.9.0", "log", diff --git a/client/Cargo.toml b/client/Cargo.toml index c2f6e1e..77ca2ab 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -21,6 +21,7 @@ libc = "0.2.178" libloading = "0.9.0" cfg-if = "1.0.4" ilhook = "2.3.0" +lazy_static = "1.4.0" [build-dependencies] gl_generator = "0.14" \ No newline at end of file diff --git a/client/build.rs b/client/build.rs index 5d10603..5752110 100644 --- a/client/build.rs +++ b/client/build.rs @@ -3,10 +3,10 @@ // It finds the `jvm.lib` import library that is required to link JNI functions. // On Linux, this is unnecessary because the linker can directly use libjvm.so. +use gl_generator::{Api, Fallbacks, GlobalGenerator, Profile, Registry}; use std::env; use std::fs::File; use std::path::Path; -use gl_generator::{Api, Fallbacks, GlobalGenerator, Profile, Registry}; #[cfg(windows)] fn main() { @@ -78,8 +78,8 @@ fn main() { let dest = env::var("OUT_DIR").unwrap(); let mut file = File::create(&Path::new(&dest).join("bindings.rs")).unwrap(); - // Qui chiediamo il profilo "Compatibility" che include glBegin, glEnd, etc. - Registry::new(Api::Gl, (2, 1), Profile::Compatibility, Fallbacks::All, []) + // Ask for OpenGL 3.3 Compatibility so we get VAOs (GenVertexArrays) and modern shader API + Registry::new(Api::Gl, (3, 3), Profile::Compatibility, Fallbacks::All, []) .write_bindings(GlobalGenerator, &mut file) .unwrap(); } diff --git a/client/src/graphic/font.rs b/client/src/graphic/font.rs new file mode 100644 index 0000000..2107f27 --- /dev/null +++ b/client/src/graphic/font.rs @@ -0,0 +1,180 @@ +use crate::graphic::render::Renderer; + +/// A simple 5x7 bitmap font for ASCII characters 32 to 127. +/// Each byte represents a column of the character (5 columns per character). +/// 1 means pixel is on, 0 means pixel is off. +static FONT_5X7: [[u8; 5]; 95] = [ + [0x00, 0x00, 0x00, 0x00, 0x00], // 32 Space + [0x00, 0x00, 0x5f, 0x00, 0x00], // 33 ! + [0x00, 0x07, 0x00, 0x07, 0x00], // 34 " + [0x14, 0x7f, 0x14, 0x7f, 0x14], // 35 # + [0x24, 0x2a, 0x7f, 0x2a, 0x12], // 36 $ + [0x23, 0x13, 0x08, 0x64, 0x62], // 37 % + [0x36, 0x49, 0x55, 0x22, 0x50], // 38 & + [0x00, 0x05, 0x03, 0x00, 0x00], // 39 ' + [0x00, 0x1c, 0x22, 0x41, 0x00], // 40 ( + [0x00, 0x41, 0x22, 0x1c, 0x00], // 41 ) + [0x14, 0x08, 0x3e, 0x08, 0x14], // 42 * + [0x08, 0x08, 0x3e, 0x08, 0x08], // 43 + + [0x00, 0x50, 0x30, 0x00, 0x00], // 44 , + [0x08, 0x08, 0x08, 0x08, 0x08], // 45 - + [0x00, 0x60, 0x60, 0x00, 0x00], // 46 . + [0x20, 0x10, 0x08, 0x04, 0x02], // 47 / + [0x3e, 0x51, 0x49, 0x45, 0x3e], // 48 0 + [0x00, 0x42, 0x7f, 0x40, 0x00], // 49 1 + [0x42, 0x61, 0x51, 0x49, 0x46], // 50 2 + [0x21, 0x41, 0x45, 0x4b, 0x31], // 51 3 + [0x18, 0x14, 0x12, 0x7f, 0x10], // 52 4 + [0x27, 0x45, 0x45, 0x45, 0x39], // 53 5 + [0x3c, 0x4a, 0x49, 0x49, 0x30], // 54 6 + [0x01, 0x71, 0x09, 0x05, 0x03], // 55 7 + [0x36, 0x49, 0x49, 0x49, 0x36], // 56 8 + [0x06, 0x49, 0x49, 0x29, 0x1e], // 57 9 + [0x00, 0x36, 0x36, 0x00, 0x00], // 58 : + [0x00, 0x56, 0x36, 0x00, 0x00], // 59 ; + [0x08, 0x14, 0x22, 0x41, 0x00], // 60 < + [0x14, 0x14, 0x14, 0x14, 0x14], // 61 = + [0x00, 0x41, 0x22, 0x14, 0x08], // 62 > + [0x02, 0x01, 0x51, 0x09, 0x06], // 63 ? + [0x32, 0x49, 0x79, 0x41, 0x3e], // 64 @ + [0x7e, 0x11, 0x11, 0x11, 0x7e], // 65 A + [0x7f, 0x49, 0x49, 0x49, 0x36], // 66 B + [0x3e, 0x41, 0x41, 0x41, 0x22], // 67 C + [0x7f, 0x41, 0x41, 0x22, 0x1c], // 68 D + [0x7f, 0x49, 0x49, 0x49, 0x41], // 69 E + [0x7f, 0x09, 0x09, 0x09, 0x01], // 70 F + [0x3e, 0x41, 0x49, 0x49, 0x7a], // 71 G + [0x7f, 0x08, 0x08, 0x08, 0x7f], // 72 H + [0x00, 0x41, 0x7f, 0x41, 0x00], // 73 I + [0x20, 0x40, 0x41, 0x3f, 0x01], // 74 J + [0x7f, 0x08, 0x14, 0x22, 0x41], // 75 K + [0x7f, 0x40, 0x40, 0x40, 0x40], // 76 L + [0x7f, 0x02, 0x0c, 0x02, 0x7f], // 77 M + [0x7f, 0x04, 0x08, 0x10, 0x7f], // 78 N + [0x3e, 0x41, 0x41, 0x41, 0x3e], // 79 O + [0x7f, 0x09, 0x09, 0x09, 0x06], // 80 P + [0x3e, 0x41, 0x51, 0x21, 0x5e], // 81 Q + [0x7f, 0x09, 0x19, 0x29, 0x46], // 82 R + [0x46, 0x49, 0x49, 0x49, 0x31], // 83 S + [0x01, 0x01, 0x7f, 0x01, 0x01], // 84 T + [0x3f, 0x40, 0x40, 0x40, 0x3f], // 85 U + [0x1f, 0x20, 0x40, 0x20, 0x1f], // 86 V + [0x3f, 0x40, 0x38, 0x40, 0x3f], // 87 W + [0x63, 0x14, 0x08, 0x14, 0x63], // 88 X + [0x07, 0x08, 0x70, 0x08, 0x07], // 89 Y + [0x61, 0x51, 0x49, 0x45, 0x43], // 90 Z + [0x00, 0x7f, 0x41, 0x41, 0x00], // 91 [ + [0x02, 0x04, 0x08, 0x10, 0x20], // 92 \ + [0x00, 0x41, 0x41, 0x7f, 0x00], // 93 ] + [0x04, 0x02, 0x01, 0x02, 0x04], // 94 ^ + [0x40, 0x40, 0x40, 0x40, 0x40], // 95 _ + [0x00, 0x01, 0x02, 0x04, 0x00], // 96 ` + [0x20, 0x54, 0x54, 0x54, 0x78], // 97 a + [0x7f, 0x48, 0x44, 0x44, 0x38], // 98 b + [0x38, 0x44, 0x44, 0x44, 0x20], // 99 c + [0x38, 0x44, 0x44, 0x48, 0x7f], // 100 d + [0x38, 0x54, 0x54, 0x54, 0x18], // 101 e + [0x08, 0x7e, 0x09, 0x01, 0x02], // 102 f + [0x18, 0xa4, 0xa4, 0xa4, 0x7c], // 103 g + [0x7f, 0x08, 0x04, 0x04, 0x78], // 104 h + [0x00, 0x44, 0x7d, 0x40, 0x00], // 105 i + [0x40, 0x80, 0x84, 0x7d, 0x00], // 106 j + [0x7f, 0x10, 0x28, 0x44, 0x00], // 107 k + [0x00, 0x41, 0x7f, 0x40, 0x00], // 108 l + [0x7c, 0x04, 0x18, 0x04, 0x78], // 109 m + [0x7c, 0x08, 0x04, 0x04, 0x78], // 110 n + [0x38, 0x44, 0x44, 0x44, 0x38], // 111 o + [0xfe, 0x14, 0x14, 0x14, 0x08], // 112 p + [0x08, 0x14, 0x14, 0x18, 0xfe], // 113 q + [0x7c, 0x08, 0x04, 0x04, 0x08], // 114 r + [0x48, 0x54, 0x54, 0x54, 0x20], // 115 s + [0x04, 0x3f, 0x44, 0x40, 0x20], // 116 t + [0x3c, 0x40, 0x40, 0x20, 0x7c], // 117 u + [0x1c, 0x20, 0x40, 0x20, 0x1c], // 118 v + [0x3c, 0x40, 0x30, 0x40, 0x3c], // 119 w + [0x44, 0x28, 0x10, 0x28, 0x44], // 120 x + [0x1c, 0xa0, 0xa0, 0xa0, 0x7c], // 121 y + [0x44, 0x64, 0x54, 0x4c, 0x44], // 122 z + [0x00, 0x08, 0x36, 0x41, 0x00], // 123 { + [0x00, 0x00, 0x7f, 0x00, 0x00], // 124 | + [0x00, 0x41, 0x36, 0x08, 0x00], // 125 } + [0x02, 0x01, 0x02, 0x04, 0x02], // 126 ~ +]; + +/// Draws a text string at (x, y) with the given scale and color. +/// It uses a horizontal grouping algorithm to drastically reduce OpenGL clear calls. +pub unsafe fn draw_text( + renderer: &mut Renderer, + text: &str, + start_x: i32, + start_y: i32, + r: f32, + g: f32, + b: f32, + a: f32, + scale: i32, +) { + renderer.set_color(r, g, b, a); + + let char_width = 5 * scale; + let char_space = scale; + + let mut current_x = start_x; + + for ch in text.chars() { + let ch_num = ch as usize; + if ch_num >= 32 && ch_num <= 126 { + let glyph = &FONT_5X7[ch_num - 32]; + + // Render each row grouping contiguous pixels horizontally + for row in 0..8 { + let mut col = 0; + while col < 5 { + // Check if current pixel is on + if (glyph[col] & (1 << row)) != 0 { + // Find how many contiguous pixels are on in this row + let mut width = 1; + while col + width < 5 && (glyph[col + width] & (1 << row)) != 0 { + width += 1; + } + + // Dispatch a unified block for grouped pixels + renderer.draw_rect( + current_x + (col as i32 * scale), + start_y + (row as i32 * scale), + (width as i32) * scale, + scale, + ); + + // Skip the pixels we just grouped + col += width; + } else { + col += 1; + } + } + } + } + current_x += char_width + char_space; + } +} + +/// Helper function to calculate the total width of a string in pixels at a given scale. +pub fn get_text_width(text: &str, scale: i32) -> i32 { + let char_width = 5 * scale; + let char_space = scale; + + let valid_chars = text + .chars() + .filter(|ch| { + let n = *ch as usize; + n >= 32 && n <= 126 + }) + .count() as i32; + + if valid_chars == 0 { + return 0; + } + + // Include space between characters, but not after the last one + (valid_chars * char_width) + ((valid_chars - 1) * char_space) +} diff --git a/client/src/graphic/hook.rs b/client/src/graphic/hook.rs new file mode 100644 index 0000000..424a32b --- /dev/null +++ b/client/src/graphic/hook.rs @@ -0,0 +1,332 @@ +use crate::client::DarkClient; +use crate::mapping::client::minecraft::Minecraft; +use cfg_if::cfg_if; +use ilhook::x64::HookPoint; +use log::info; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::{Mutex, OnceLock}; + +use crate::{gl, RUNNING}; + +static LAST_TICK: AtomicI32 = AtomicI32::new(0); +static GL_LOADED: AtomicBool = AtomicBool::new(false); + +// We create a wrapper to bypass the compiler's safety checks +pub struct HookHandle(HookPoint); + +unsafe impl Send for HookHandle {} +unsafe impl Sync for HookHandle {} + +// Global storage for the active hook. +// We use a Mutex to be able to modify (remove) it at runtime. +// Note: The exact type depends on what hooker.hook() returns. +// For ilhook-rs, the `Hook` object handles unhooking when it is dropped. +// We update the global storage to use this specific type, not dyn Any +static GLOBAL_HOOK: OnceLock>> = OnceLock::new(); + +fn get_global_hook() -> &'static Mutex> { + GLOBAL_HOOK.get_or_init(|| Mutex::new(None)) +} + +cfg_if! { + if #[cfg(target_os = "linux")] { + use std::ffi::CString; + use ilhook::x64::{Hooker, Registers, CallbackOption, HookFlags, HookType}; + use libc::c_void; + + // Helper to load OpenGL functions on Linux + fn get_proc_address(addr: &str) -> *const c_void { + unsafe { + let s = CString::new(addr).unwrap(); + // Try first with glXGetProcAddress if available, otherwise dlsym + // Here we use a simplified approach assuming libGL is loaded + let lib = libc::dlopen(CString::new("libGL.so.1").unwrap().as_ptr(), libc::RTLD_LAZY); + if !lib.is_null() { + libc::dlsym(lib, s.as_ptr()) + } else { + std::ptr::null() + } + } + } + + unsafe extern "win64" fn my_swap_buffers_hook(_regs: *mut Registers, _user_data: usize) { + on_frame(); + } + + } else if #[cfg(target_os = "windows")] { + use ilhook::x64::{Hooker, Registers, CallbackOption, HookFlags, HookType}; + use libloading::os::windows::{Library, Symbol}; + use std::ffi::CString; + + // Helper to load OpenGL functions on Windows + // In a real context, we should have loaded opengl32.dll statically or lazy + // Here we make a "dirty" but functional attempt for injection + fn get_proc_address(addr: &str) -> *const std::ffi::c_void { + unsafe { + // Robust method: try wglGetProcAddress, then GetProcAddress + let c_str = CString::new(addr).unwrap(); + + let lib = libloading::Library::new("opengl32.dll"); + if let Ok(l) = lib { + // First try wglGetProcAddress (for modern extensions) + let wgl_get: Result *const std::ffi::c_void>, _> = l.get(b"wglGetProcAddress"); + if let Ok(wgl) = wgl_get { + let ptr = wgl(c_str.as_ptr()); + if !ptr.is_null() { + return ptr; + } + } + // Fallback to GetProcAddress (for base GL 1.1 functions) + let func: Result, _> = l.get(c_str.as_bytes()); + if let Ok(f) = func { + return *f as *const std::ffi::c_void; + } + } + std::ptr::null() + } + } + + unsafe extern "win64" fn my_swap_buffers_hook(_regs: *mut Registers, _user_data: usize) { + on_frame(); + } + } +} + +const CONTEXT_PROFILE_MASK: u32 = 0x9126; +const CONTEXT_CORE_PROFILE_BIT: u32 = 0x00000001; + +// Function called every graphical frame +unsafe fn on_frame() { + // === PANIC CHECK === + // If cleanup_client() has been called, RUNNING becomes false. + // If it is false, we exit IMMEDIATELY. We don't tick, we don't draw. + if !RUNNING.load(Ordering::SeqCst) { + return; + } + + // Initialize GL pointers... + if !GL_LOADED.load(Ordering::Relaxed) { + gl::load_with(|s| get_proc_address(s)); + GL_LOADED.store(true, Ordering::Relaxed); + } + + check_tick(); + render_overlay(); +} + +use crate::graphic::render::Renderer; + +// === RENDERING LOGIC === +unsafe fn render_overlay() { + // Skip if we are rendering on the Egui window + if crate::gui::IS_EGUI_THREAD.with(|f| f.get()) { + return; + } + + // Initialize inputs when we are on the valid OpenGL thread context + crate::graphic::input::init(); + + // Initialize the renderer, which backs up OpenGL state + let mut renderer = Renderer::new(); + + // Call our new custom OpenGL UI system + crate::graphic::ui::render_gui(&mut renderer); + + // the state is restored automatically when `renderer` goes out of scope and drops +} + +fn check_tick() { + let client = DarkClient::instance(); + // Try to get env without attaching if possible, or attach as daemon. + let _env = match client.jvm.attach_current_thread_as_daemon() { + Ok(env) => env, + Err(_) => return, + }; + + let minecraft = Minecraft::instance(); + + // Error check to avoid crashes + if minecraft.player.entity.is_null() { + return; + } + + let tick_count = match minecraft.player.entity.get_tick_count() { + Ok(t) => t, + Err(_) => return, + }; + + let last_tick = LAST_TICK.load(Ordering::Relaxed); + + if tick_count > last_tick { + LAST_TICK.store(tick_count, Ordering::Relaxed); + client.tick(); + } +} + +pub fn find_library_path(partial_name: &str) -> Option { + if let Ok(file) = File::open("/proc/self/maps") { + let reader = BufReader::new(file); + for line in reader.lines() { + if let Ok(l) = line { + // Look for a line containing the name (e.g. "liblwjgl_opengl.so") + if l.contains(partial_name) && l.contains(".so") { + // The format is: address perms offset dev inode PATH + // We take the last part of the string + if let Some(path) = l.split_whitespace().last() { + return Some(path.to_string()); + } + } + } + } + } + None +} + +pub fn install_hooks() -> anyhow::Result<()> { + #[cfg(target_os = "linux")] + unsafe { + let mut targets = Vec::new(); + + if let Some(path) = find_library_path("libglfw.so") { + info!("Found GLFW library: {}", path); + targets.push((path, "glfwSwapBuffers")); + } else if let Some(path) = find_library_path("liblwjgl.so") { + info!("Found LWJGL library (Legacy): {}", path); + targets.push((path, "glXSwapBuffers")); + } else { + info!("No specific library found, trying system libGL..."); + targets.push(("libGL.so.1".to_string(), "glXSwapBuffers")); + } + + let mut hooked_count = 0; + + for (lib_path, func_name) in targets { + let c_lib_path = CString::new(lib_path.clone())?; + let c_func_name = CString::new(func_name)?; + + let lib = libc::dlopen(c_lib_path.as_ptr(), libc::RTLD_LAZY); + + if !lib.is_null() { + let target_addr = libc::dlsym(lib, c_func_name.as_ptr()) as usize; + + if target_addr != 0 { + info!("Found {} in {} at 0x{:x}", func_name, lib_path, target_addr); + + let hooker = Hooker::new( + target_addr, + HookType::JmpBack(my_swap_buffers_hook), + CallbackOption::None, + 0, + HookFlags::empty(), + ); + + match hooker.hook() { + Ok(hook) => { + // Get the global lock + let mut guard = get_global_hook().lock().unwrap(); + + // If there was an old hook, we overwrite it (triggering automatic unhook) + if guard.is_some() { + info!("Detected old hook, removing and replacing..."); + } + + // Wrap the hook in our "Thread Safe" wrapper + *guard = Some(HookHandle(hook)); + + hooked_count += 1; + info!(">>> HOOK ACTIVE AND SAVED ON: {} <<<", lib_path); + + // Exit the loop, one active hook is enough + break; + } + Err(e) => { + info!("Error installing hook on {}: {:?}", lib_path, e); + } + } + } + } + } + + if hooked_count == 0 { + // We could return an error here, BUT if we are re-injecting and something went wrong + // with the static flag, we might want to "pretend" everything is fine. + // However, for now we leave the error if count is 0. + return Err(anyhow::anyhow!("Failed to hook any candidate libraries!")); + } + } + + #[cfg(target_os = "windows")] + unsafe { + use libloading::Library; + let lib_name = "opengl32.dll"; + let func_name = "wglSwapBuffers"; + + let lib = match Library::new(lib_name) { + Ok(l) => l, + Err(e) => { + info!("Error loading opengl32: {}", e); + return Err(anyhow::anyhow!("Failed to hook opengl32")); + } + }; + + let target_addr: libloading::Symbol = + match lib.get(func_name.as_bytes()) { + Ok(s) => s, + Err(e) => { + info!("wglSwapBuffers missing: {}", e); + return Err(anyhow::anyhow!("Failed to hook wglSwapBuffers")); + } + }; + + let target_addr_val = *target_addr as *const () as usize; + info!( + "Found {} in {} at 0x{:x}", + func_name, lib_name, target_addr_val + ); + + let hooker = Hooker::new( + target_addr_val, + HookType::JmpBack(my_swap_buffers_hook), + CallbackOption::None, + 0, + HookFlags::empty(), + ); + + match hooker.hook() { + Ok(hook) => { + let mut guard = get_global_hook().lock().unwrap(); + if guard.is_some() { + info!("Detected old hook, removing..."); + } + *guard = Some(HookHandle(hook)); + info!(">>> HOOK ACTIVE AND SAVED ON: {} <<<", lib_name); + } + Err(e) => { + info!("Hook error on {}: {:?}", lib_name, e); + return Err(anyhow::anyhow!("Failed to hook wglSwapBuffers!")); + } + } + + // Keep the library handle alive + std::mem::forget(lib); + } + Ok(()) +} + +pub fn uninstall_hooks() { + // Take the lock + let mut guard = get_global_hook().lock().unwrap(); + + if guard.is_some() { + info!("Physical hook removal in progress..."); + // By setting to None, the Wrapper is destroyed. + // The Wrapper destroys the internal HookPoint. + // The internal HookPoint restores the original memory bytes. + *guard = None; + info!("Hook removed successfully. Memory cleaned."); + } else { + info!("No active hook to remove."); + } +} diff --git a/client/src/graphic/input.rs b/client/src/graphic/input.rs new file mode 100644 index 0000000..927af97 --- /dev/null +++ b/client/src/graphic/input.rs @@ -0,0 +1,504 @@ +use libc::c_void; +use log::{error, info}; +use std::ffi::CString; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex; + +// Global Input State +pub static GUI_OPEN: AtomicBool = AtomicBool::new(true); + +lazy_static::lazy_static! { + pub static ref MOUSE_STATE: Mutex = Mutex::new(MouseState::default()); +} + +#[derive(Default, Clone, Copy)] +pub struct MouseState { + pub x: f64, + pub y: f64, + pub left_down: bool, + pub right_down: bool, +} + +#[cfg(target_os = "linux")] +mod linux_input { + use super::*; + use std::sync::Once; + + // Original callbacks and GLFW state + static mut ORIGINAL_MOUSE_BTN: *mut c_void = std::ptr::null_mut(); + static mut ORIGINAL_CURSOR_POS: *mut c_void = std::ptr::null_mut(); + static mut ORIGINAL_KEY_CB: *mut c_void = std::ptr::null_mut(); + + static mut GLFW_WINDOW: *mut c_void = std::ptr::null_mut(); + static mut GLFW_SET_INPUT_MODE: Option = None; + + // To prevent massive camera spins when un-grabbing + static mut C_LOCK_X: f64 = 0.0; + static mut C_LOCK_Y: f64 = 0.0; + + // Type definitions for GLFW callbacks + type GlfwMouseButtonFun = extern "C" fn(*mut c_void, i32, i32, i32); + type GlfwCursorPosFun = extern "C" fn(*mut c_void, f64, f64); + type GlfwKeyFun = extern "C" fn(*mut c_void, i32, i32, i32, i32); + + extern "C" fn my_mouse_button_callback( + window: *mut c_void, + button: i32, + action: i32, + mods: i32, + ) { + if action == 1 { + // GLFW_PRESS + if button == 0 { + // GLFW_MOUSE_BUTTON_LEFT + if let Ok(mut state) = MOUSE_STATE.lock() { + state.left_down = true; + } + } else if button == 1 { + // GLFW_MOUSE_BUTTON_RIGHT + if let Ok(mut state) = MOUSE_STATE.lock() { + state.right_down = true; + } + } + } else if action == 0 { + // GLFW_RELEASE + if button == 0 { + if let Ok(mut state) = MOUSE_STATE.lock() { + state.left_down = false; + } + } else if button == 1 { + if let Ok(mut state) = MOUSE_STATE.lock() { + state.right_down = false; + } + } + } + + // If GUI is open, consume the event (don't pass to Minecraft) + if GUI_OPEN.load(Ordering::Relaxed) { + return; + } + + // Pass to original + unsafe { + if !ORIGINAL_MOUSE_BTN.is_null() { + let orig: GlfwMouseButtonFun = std::mem::transmute(ORIGINAL_MOUSE_BTN); + orig(window, button, action, mods); + } + } + } + + extern "C" fn my_cursor_pos_callback(window: *mut c_void, xpos: f64, ypos: f64) { + if let Ok(mut state) = MOUSE_STATE.lock() { + state.x = xpos; + state.y = ypos; + } + + // If GUI is open, we freeze the position sent to Minecraft to the last known position + let mut send_x = xpos; + let mut send_y = ypos; + + if GUI_OPEN.load(Ordering::Relaxed) { + unsafe { + send_x = C_LOCK_X; + send_y = C_LOCK_Y; + } + } else { + unsafe { + C_LOCK_X = xpos; + C_LOCK_Y = ypos; + } + } + + unsafe { + if !ORIGINAL_CURSOR_POS.is_null() { + let orig: GlfwCursorPosFun = std::mem::transmute(ORIGINAL_CURSOR_POS); + orig(window, send_x, send_y); + } + } + } + + extern "C" fn my_key_callback( + window: *mut c_void, + key: i32, + scancode: i32, + action: i32, + mods: i32, + ) { + if key == 344 /* Right Shift */ && action == 1 { + // Toggle GUI + let current = GUI_OPEN.load(Ordering::Relaxed); + let next = !current; + GUI_OPEN.store(next, Ordering::Relaxed); + + // Toggle mouse visibility + unsafe { + if let Some(set_mode) = GLFW_SET_INPUT_MODE { + if !GLFW_WINDOW.is_null() { + if next { + // GUI Open -> Normal Pointer + set_mode(GLFW_WINDOW, 0x00033001, 0x00034001); + } else { + // GUI Closed -> Disabled / Captured Pointer + set_mode(GLFW_WINDOW, 0x00033001, 0x00034003); + } + } + } + } + } + + if GUI_OPEN.load(Ordering::Relaxed) { + return; + } + + unsafe { + if !ORIGINAL_KEY_CB.is_null() { + let orig: GlfwKeyFun = std::mem::transmute(ORIGINAL_KEY_CB); + orig(window, key, scancode, action, mods); + } + } + } + + pub fn init_glfw_hooks() { + static HOOKED_ONCE: Once = Once::new(); + HOOKED_ONCE.call_once(|| { + unsafe { + let path = crate::graphic::hook::find_library_path("libglfw.so") + .unwrap_or_else(|| "libglfw.so".to_string()); + let libglfw = libc::dlopen(CString::new(path).unwrap().as_ptr(), libc::RTLD_LAZY); + if libglfw.is_null() { + error!("Could not open libglfw.so to hook inputs."); + return; + } + + // Get function pointers + let get_current_context: extern "C" fn() -> *mut c_void = + std::mem::transmute(libc::dlsym( + libglfw, + CString::new("glfwGetCurrentContext").unwrap().as_ptr(), + )); + let set_mouse_button: extern "C" fn( + *mut c_void, + GlfwMouseButtonFun, + ) -> *mut c_void = std::mem::transmute(libc::dlsym( + libglfw, + CString::new("glfwSetMouseButtonCallback").unwrap().as_ptr(), + )); + let set_cursor_pos: extern "C" fn(*mut c_void, GlfwCursorPosFun) -> *mut c_void = + std::mem::transmute(libc::dlsym( + libglfw, + CString::new("glfwSetCursorPosCallback").unwrap().as_ptr(), + )); + let set_key_cb: extern "C" fn(*mut c_void, GlfwKeyFun) -> *mut c_void = + std::mem::transmute(libc::dlsym( + libglfw, + CString::new("glfwSetKeyCallback").unwrap().as_ptr(), + )); + let set_input_mode: extern "C" fn(*mut c_void, i32, i32) = std::mem::transmute( + libc::dlsym(libglfw, CString::new("glfwSetInputMode").unwrap().as_ptr()), + ); + + let window = get_current_context(); + if window.is_null() { + error!("No glfw window context found yet."); + return; + } + + info!("Successfully got GLFW window. Placing callback overrides..."); + + // Swap callbacks and store original + ORIGINAL_MOUSE_BTN = set_mouse_button(window, my_mouse_button_callback); + ORIGINAL_CURSOR_POS = set_cursor_pos(window, my_cursor_pos_callback); + ORIGINAL_KEY_CB = set_key_cb(window, my_key_callback); + + // Store globally so we can toggle grab state later + GLFW_WINDOW = window; + GLFW_SET_INPUT_MODE = Some(set_input_mode); + + // Assuming GUI is open by default: ungrab mouse right away + if GUI_OPEN.load(Ordering::Relaxed) { + set_input_mode(window, 0x00033001, 0x00034001); + } + } + }); + } + + pub fn cleanup_glfw_hooks() { + unsafe { + if !GLFW_WINDOW.is_null() && !ORIGINAL_CURSOR_POS.is_null() { + // We need to re-find the functions since we don't store them, + // but we can just use dlsym again. + if let Some(path) = crate::graphic::hook::find_library_path("libglfw.so") { + let libglfw = + libc::dlopen(CString::new(path).unwrap().as_ptr(), libc::RTLD_LAZY); + if !libglfw.is_null() { + let set_mouse_button: extern "C" fn(*mut c_void, *mut c_void) = + std::mem::transmute(libc::dlsym( + libglfw, + CString::new("glfwSetMouseButtonCallback").unwrap().as_ptr(), + )); + let set_cursor_pos: extern "C" fn(*mut c_void, *mut c_void) = + std::mem::transmute(libc::dlsym( + libglfw, + CString::new("glfwSetCursorPosCallback").unwrap().as_ptr(), + )); + let set_key_cb: extern "C" fn(*mut c_void, *mut c_void) = + std::mem::transmute(libc::dlsym( + libglfw, + CString::new("glfwSetKeyCallback").unwrap().as_ptr(), + )); + + // Restore original callbacks + set_mouse_button(GLFW_WINDOW, ORIGINAL_MOUSE_BTN); + set_cursor_pos(GLFW_WINDOW, ORIGINAL_CURSOR_POS); + set_key_cb(GLFW_WINDOW, ORIGINAL_KEY_CB); + + // Ensure mouse is ungrabbed (normal) if we panic'd while GUI was open + if let Some(set_mode) = GLFW_SET_INPUT_MODE { + set_mode(GLFW_WINDOW, 0x00033001, 0x00034003); // 0x00034003 is GLFW_CURSOR_DISABLED (Minecraft default) + } + } + } + } + } + } +} + +#[cfg(target_os = "windows")] +mod windows_input { + use super::*; + use libloading::Library; + use std::ffi::c_void; + use std::sync::Once; + + static mut ORIGINAL_MOUSE_BTN: *mut c_void = std::ptr::null_mut(); + static mut ORIGINAL_CURSOR_POS: *mut c_void = std::ptr::null_mut(); + static mut ORIGINAL_KEY_CB: *mut c_void = std::ptr::null_mut(); + + static mut GLFW_WINDOW: *mut c_void = std::ptr::null_mut(); + static mut GLFW_SET_INPUT_MODE: Option = None; + + static mut C_LOCK_X: f64 = 0.0; + static mut C_LOCK_Y: f64 = 0.0; + + type GlfwMouseButtonFun = extern "C" fn(*mut c_void, i32, i32, i32); + type GlfwCursorPosFun = extern "C" fn(*mut c_void, f64, f64); + type GlfwKeyFun = extern "C" fn(*mut c_void, i32, i32, i32, i32); + + extern "C" fn my_mouse_button_callback( + window: *mut c_void, + button: i32, + action: i32, + mods: i32, + ) { + if action == 1 { + if button == 0 { + if let Ok(mut state) = MOUSE_STATE.lock() { + state.left_down = true; + } + } else if button == 1 { + if let Ok(mut state) = MOUSE_STATE.lock() { + state.right_down = true; + } + } + } else if action == 0 { + if button == 0 { + if let Ok(mut state) = MOUSE_STATE.lock() { + state.left_down = false; + } + } else if button == 1 { + if let Ok(mut state) = MOUSE_STATE.lock() { + state.right_down = false; + } + } + } + if GUI_OPEN.load(Ordering::Relaxed) { + return; + } + unsafe { + if !ORIGINAL_MOUSE_BTN.is_null() { + let orig: GlfwMouseButtonFun = std::mem::transmute(ORIGINAL_MOUSE_BTN); + orig(window, button, action, mods); + } + } + } + + extern "C" fn my_cursor_pos_callback(window: *mut c_void, xpos: f64, ypos: f64) { + if let Ok(mut state) = MOUSE_STATE.lock() { + state.x = xpos; + state.y = ypos; + } + let mut send_x = xpos; + let mut send_y = ypos; + + if GUI_OPEN.load(Ordering::Relaxed) { + unsafe { + send_x = C_LOCK_X; + send_y = C_LOCK_Y; + } + } else { + unsafe { + C_LOCK_X = xpos; + C_LOCK_Y = ypos; + } + } + + unsafe { + if !ORIGINAL_CURSOR_POS.is_null() { + let orig: GlfwCursorPosFun = std::mem::transmute(ORIGINAL_CURSOR_POS); + orig(window, send_x, send_y); + } + } + } + + extern "C" fn my_key_callback( + window: *mut c_void, + key: i32, + scancode: i32, + action: i32, + mods: i32, + ) { + if key == 344 && action == 1 { + let next = !GUI_OPEN.load(Ordering::Relaxed); + GUI_OPEN.store(next, Ordering::Relaxed); + unsafe { + if let Some(set_mode) = GLFW_SET_INPUT_MODE { + if !GLFW_WINDOW.is_null() { + if next { + set_mode(GLFW_WINDOW, 0x00033001, 0x00034001); + } else { + set_mode(GLFW_WINDOW, 0x00033001, 0x00034003); + } + } + } + } + } + if GUI_OPEN.load(Ordering::Relaxed) { + return; + } + unsafe { + if !ORIGINAL_KEY_CB.is_null() { + let orig: GlfwKeyFun = std::mem::transmute(ORIGINAL_KEY_CB); + orig(window, key, scancode, action, mods); + } + } + } + + static mut GLFW_LIB: Option = None; + + pub fn init_glfw_hooks() { + static HOOKED_ONCE: Once = Once::new(); + HOOKED_ONCE.call_once(|| unsafe { + let lib_result = Library::new("glfw.dll") + .or_else(|_| Library::new("glfw3.dll")) + .or_else(|_| Library::new("glfw64.dll")); + + let libglfw = match lib_result { + Ok(l) => l, + Err(_) => { + error!("Could not load glfw.dll on Windows."); + return; + } + }; + + let get_current_context: libloading::Symbol *mut c_void> = + match libglfw.get(b"glfwGetCurrentContext") { + Ok(sym) => sym, + Err(_) => return, + }; + let set_mouse_button: libloading::Symbol< + extern "C" fn(*mut c_void, GlfwMouseButtonFun) -> *mut c_void, + > = match libglfw.get(b"glfwSetMouseButtonCallback") { + Ok(sym) => sym, + Err(_) => return, + }; + let set_cursor_pos: libloading::Symbol< + extern "C" fn(*mut c_void, GlfwCursorPosFun) -> *mut c_void, + > = match libglfw.get(b"glfwSetCursorPosCallback") { + Ok(sym) => sym, + Err(_) => return, + }; + let set_key_cb: libloading::Symbol< + extern "C" fn(*mut c_void, GlfwKeyFun) -> *mut c_void, + > = match libglfw.get(b"glfwSetKeyCallback") { + Ok(sym) => sym, + Err(_) => return, + }; + let set_input_mode: libloading::Symbol = + match libglfw.get(b"glfwSetInputMode") { + Ok(sym) => sym, + Err(_) => return, + }; + + let window = get_current_context(); + if window.is_null() { + return; + } + + info!("Successfully got Windows GLFW window. Modifying Hooks..."); + + ORIGINAL_MOUSE_BTN = set_mouse_button(window, my_mouse_button_callback); + ORIGINAL_CURSOR_POS = set_cursor_pos(window, my_cursor_pos_callback); + ORIGINAL_KEY_CB = set_key_cb(window, my_key_callback); + + GLFW_WINDOW = window; + GLFW_SET_INPUT_MODE = Some(*set_input_mode); + + if GUI_OPEN.load(Ordering::Relaxed) { + (*set_input_mode)(window, 0x00033001, 0x00034001); + } + + GLFW_LIB = Some(libglfw); + }); + } + + pub fn cleanup_glfw_hooks() { + unsafe { + if !GLFW_WINDOW.is_null() && !ORIGINAL_CURSOR_POS.is_null() { + if let Some(libglfw) = &GLFW_LIB { + if let Ok(set_mouse_button) = libglfw + .get::( + b"glfwSetMouseButtonCallback", + ) + { + set_mouse_button(GLFW_WINDOW, ORIGINAL_MOUSE_BTN); + } + if let Ok(set_cursor_pos) = libglfw + .get::(b"glfwSetCursorPosCallback") + { + set_cursor_pos(GLFW_WINDOW, ORIGINAL_CURSOR_POS); + } + if let Ok(set_key_cb) = libglfw + .get::(b"glfwSetKeyCallback") + { + set_key_cb(GLFW_WINDOW, ORIGINAL_KEY_CB); + } + if let Some(set_mode) = GLFW_SET_INPUT_MODE { + set_mode(GLFW_WINDOW, 0x00033001, 0x00034003); + } + } + } + } + } +} + +pub fn init() { + #[cfg(target_os = "linux")] + { + linux_input::init_glfw_hooks(); + } + #[cfg(target_os = "windows")] + { + windows_input::init_glfw_hooks(); + } +} + +pub fn cleanup() { + #[cfg(target_os = "linux")] + { + linux_input::cleanup_glfw_hooks(); + } + #[cfg(target_os = "windows")] + { + windows_input::cleanup_glfw_hooks(); + } +} diff --git a/client/src/graphic/mod.rs b/client/src/graphic/mod.rs new file mode 100644 index 0000000..87739bb --- /dev/null +++ b/client/src/graphic/mod.rs @@ -0,0 +1,6 @@ +pub mod font; +pub mod hook; +pub mod input; +pub mod render; +pub mod ui; +pub mod ui_manager; diff --git a/client/src/graphic/render.rs b/client/src/graphic/render.rs new file mode 100644 index 0000000..6b08f07 --- /dev/null +++ b/client/src/graphic/render.rs @@ -0,0 +1,309 @@ +use crate::gl; +use std::ffi::CString; + +static mut SHADER_PROGRAM: u32 = 0; +static mut VAO: u32 = 0; +static mut VBO: u32 = 0; +static mut INITIALIZED: bool = false; + +const VERTEX_SHADER: &str = r#" +#version 150 core +in vec2 position; +uniform vec4 color; +out vec4 fragColor; +void main() { + gl_Position = vec4(position, 0.0, 1.0); + fragColor = color; +} +"#; + +const FRAGMENT_SHADER: &str = r#" +#version 150 core +in vec4 fragColor; +out vec4 outColor; +void main() { + outColor = fragColor; +} +"#; + +unsafe fn compile_shader(type_: u32, source: &str) -> u32 { + let shader = gl::CreateShader(type_); + let c_str = CString::new(source.as_bytes()).unwrap(); + gl::ShaderSource(shader, 1, &c_str.as_ptr(), std::ptr::null()); + gl::CompileShader(shader); + + let mut success = 0; + gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut success); + if success == 0 { + let mut len = 0; + gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut len); + let mut buffer = vec![0u8; len as usize]; + gl::GetShaderInfoLog( + shader, + len, + std::ptr::null_mut(), + buffer.as_mut_ptr() as *mut i8, + ); + log::error!("Shader compile error: {}", String::from_utf8_lossy(&buffer)); + } + shader +} + +unsafe fn setup_opengl() { + let vs = compile_shader(gl::VERTEX_SHADER, VERTEX_SHADER); + let fs = compile_shader(gl::FRAGMENT_SHADER, FRAGMENT_SHADER); + + SHADER_PROGRAM = gl::CreateProgram(); + gl::AttachShader(SHADER_PROGRAM, vs); + gl::AttachShader(SHADER_PROGRAM, fs); + gl::LinkProgram(SHADER_PROGRAM); + + let mut success = 0; + gl::GetProgramiv(SHADER_PROGRAM, gl::LINK_STATUS, &mut success); + if success == 0 { + let mut len = 0; + gl::GetProgramiv(SHADER_PROGRAM, gl::INFO_LOG_LENGTH, &mut len); + let mut buffer = vec![0u8; len as usize]; + gl::GetProgramInfoLog( + SHADER_PROGRAM, + len, + std::ptr::null_mut(), + buffer.as_mut_ptr() as *mut i8, + ); + log::error!("Program link error: {}", String::from_utf8_lossy(&buffer)); + } + + gl::GenVertexArrays(1, std::ptr::addr_of_mut!(VAO)); + gl::GenBuffers(1, std::ptr::addr_of_mut!(VBO)); + + gl::BindVertexArray(VAO); + gl::BindBuffer(gl::ARRAY_BUFFER, VBO); + + // Pre-allocate buffer for 4 vertices (2 floats each) + gl::BufferData( + gl::ARRAY_BUFFER, + (4 * 2 * std::mem::size_of::()) as isize, + std::ptr::null(), + gl::DYNAMIC_DRAW, + ); + + let pos_loc = gl::GetAttribLocation(SHADER_PROGRAM, CString::new("position").unwrap().as_ptr()); + if pos_loc >= 0 { + gl::VertexAttribPointer(pos_loc as u32, 2, gl::FLOAT, gl::FALSE, 8, std::ptr::null()); + gl::EnableVertexAttribArray(pos_loc as u32); + } + + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + gl::BindVertexArray(0); + + INITIALIZED = true; +} + +/// A state-restoring OpenGL renderer that supports 2D drawing with alpha blending natively. +pub struct Renderer { + pub screen_width: i32, + pub screen_height: i32, + old_blend_src_rgb: i32, + old_blend_dst_rgb: i32, + old_blend_src_alpha: i32, + old_blend_dst_alpha: i32, + is_blend_on: bool, + old_program: i32, + old_vao: i32, + old_vbo: i32, + old_depth_test: bool, + old_cull_face: bool, + current_color: (f32, f32, f32, f32), +} + +impl Renderer { + pub unsafe fn new() -> Self { + if !INITIALIZED { + setup_opengl(); + } + + let mut viewport = [0; 4]; + gl::GetIntegerv(gl::VIEWPORT, viewport.as_mut_ptr()); + let screen_width = viewport[2]; + let screen_height = viewport[3]; + + // Backup states + let is_blend_on = gl::IsEnabled(gl::BLEND) == gl::TRUE; + let mut old_blend_src_rgb = 0; + let mut old_blend_dst_rgb = 0; + let mut old_blend_src_alpha = 0; + let mut old_blend_dst_alpha = 0; + gl::GetIntegerv(gl::BLEND_SRC_RGB, &mut old_blend_src_rgb); + gl::GetIntegerv(gl::BLEND_DST_RGB, &mut old_blend_dst_rgb); + gl::GetIntegerv(gl::BLEND_SRC_ALPHA, &mut old_blend_src_alpha); + gl::GetIntegerv(gl::BLEND_DST_ALPHA, &mut old_blend_dst_alpha); + + let mut old_program = 0; + gl::GetIntegerv(gl::CURRENT_PROGRAM, &mut old_program); + let mut old_vao = 0; + gl::GetIntegerv(gl::VERTEX_ARRAY_BINDING, &mut old_vao); + let mut old_vbo = 0; + gl::GetIntegerv(gl::ARRAY_BUFFER_BINDING, &mut old_vbo); + + let old_depth_test = gl::IsEnabled(gl::DEPTH_TEST) == gl::TRUE; + let old_cull_face = gl::IsEnabled(gl::CULL_FACE) == gl::TRUE; + + // Apply our states + gl::Enable(gl::BLEND); + gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); + gl::Disable(gl::DEPTH_TEST); + gl::Disable(gl::CULL_FACE); + + gl::UseProgram(SHADER_PROGRAM); + gl::BindVertexArray(VAO); + // Ensure VBO is bound for drawing + gl::BindBuffer(gl::ARRAY_BUFFER, VBO); + + Self { + screen_width, + screen_height, + is_blend_on, + old_blend_src_rgb, + old_blend_dst_rgb, + old_blend_src_alpha, + old_blend_dst_alpha, + old_program, + old_vao, + old_vbo, + old_depth_test, + old_cull_face, + current_color: (1.0, 1.0, 1.0, 1.0), + } + } + + pub unsafe fn set_color(&mut self, r: f32, g: f32, b: f32, a: f32) { + self.current_color = (r, g, b, a); + } + + pub unsafe fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32) { + let sc_w = self.screen_width as f32; + let sc_h = self.screen_height as f32; + + let ndc_x1 = (x as f32 / sc_w) * 2.0 - 1.0; + let ndc_y1 = 1.0 - (y as f32 / sc_h) * 2.0; + let ndc_x2 = ((x + w) as f32 / sc_w) * 2.0 - 1.0; + let ndc_y2 = 1.0 - ((y + h) as f32 / sc_h) * 2.0; + + let vertices: [f32; 8] = [ + ndc_x1, ndc_y1, // Top-left + ndc_x1, ndc_y2, // Bottom-left + ndc_x2, ndc_y1, // Top-right + ndc_x2, ndc_y2, // Bottom-right + ]; + + // Send new vertices to VBO + gl::BufferSubData( + gl::ARRAY_BUFFER, + 0, + (vertices.len() * std::mem::size_of::()) as isize, + vertices.as_ptr() as *const _, + ); + + // Upload uniform color + let color_loc = + gl::GetUniformLocation(SHADER_PROGRAM, CString::new("color").unwrap().as_ptr()); + if color_loc >= 0 { + gl::Uniform4f( + color_loc, + self.current_color.0, + self.current_color.1, + self.current_color.2, + self.current_color.3, + ); + } + + // Draw as Triangle Strip (4 vertices make 2 triangles / 1 quad) + gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4); + } + + pub unsafe fn draw_quad( + &mut self, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + x3: f32, + y3: f32, + x4: f32, + y4: f32, + ) { + let sc_w = self.screen_width as f32; + let sc_h = self.screen_height as f32; + + let ndc_x1 = (x1 / sc_w) * 2.0 - 1.0; + let ndc_y1 = 1.0 - (y1 / sc_h) * 2.0; + + let ndc_x2 = (x2 / sc_w) * 2.0 - 1.0; + let ndc_y2 = 1.0 - (y2 / sc_h) * 2.0; + + let ndc_x3 = (x3 / sc_w) * 2.0 - 1.0; + let ndc_y3 = 1.0 - (y3 / sc_h) * 2.0; + + let ndc_x4 = (x4 / sc_w) * 2.0 - 1.0; + let ndc_y4 = 1.0 - (y4 / sc_h) * 2.0; + + let vertices: [f32; 8] = [ + ndc_x1, ndc_y1, // Top-left + ndc_x3, ndc_y3, // Bottom-left + ndc_x2, ndc_y2, // Top-right + ndc_x4, ndc_y4, // Bottom-right + ]; + + // Send new vertices to VBO + gl::BufferSubData( + gl::ARRAY_BUFFER, + 0, + (vertices.len() * std::mem::size_of::()) as isize, + vertices.as_ptr() as *const _, + ); + + // Upload uniform color + let color_loc = + gl::GetUniformLocation(SHADER_PROGRAM, CString::new("color").unwrap().as_ptr()); + if color_loc >= 0 { + gl::Uniform4f( + color_loc, + self.current_color.0, + self.current_color.1, + self.current_color.2, + self.current_color.3, + ); + } + + // Draw as Triangle Strip + gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4); + } +} + +impl Drop for Renderer { + fn drop(&mut self) { + unsafe { + if self.old_depth_test { + gl::Enable(gl::DEPTH_TEST); + } + if self.old_cull_face { + gl::Enable(gl::CULL_FACE); + } + + if !self.is_blend_on { + gl::Disable(gl::BLEND); + } else { + gl::BlendFuncSeparate( + self.old_blend_src_rgb as u32, + self.old_blend_dst_rgb as u32, + self.old_blend_src_alpha as u32, + self.old_blend_dst_alpha as u32, + ); + } + + gl::BindBuffer(gl::ARRAY_BUFFER, self.old_vbo as u32); + gl::BindVertexArray(self.old_vao as u32); + gl::UseProgram(self.old_program as u32); + } + } +} diff --git a/client/src/graphic/ui.rs b/client/src/graphic/ui.rs new file mode 100644 index 0000000..1a5d40a --- /dev/null +++ b/client/src/graphic/ui.rs @@ -0,0 +1,207 @@ +use crate::graphic::font::{draw_text, get_text_width}; +use crate::graphic::render::Renderer; +use crate::graphic::ui_manager::UI_MANAGER; + +/// Defines the colors and sizes for the custom DarkClient GUI. +pub struct Theme { + pub screen_bg: (f32, f32, f32, f32), + pub window_bg: (f32, f32, f32, f32), + pub title_bg: (f32, f32, f32, f32), + pub border: (f32, f32, f32, f32), + pub text_primary: (f32, f32, f32, f32), + pub text_accent: (f32, f32, f32, f32), + pub module_bg: (f32, f32, f32, f32), + pub module_bg_hover: (f32, f32, f32, f32), +} + +impl Default for Theme { + fn default() -> Self { + Self { + // Full screen dim overlay + screen_bg: (0.0, 0.0, 0.0, 0.5), + // Window background + window_bg: (0.1, 0.1, 0.1, 0.95), + // Title bar + title_bg: (0.05, 0.05, 0.05, 1.0), + // Accent border + border: (0.5, 0.0, 1.0, 1.0), + // Texts + text_primary: (1.0, 1.0, 1.0, 1.0), + text_accent: (1.0, 1.0, 0.0, 1.0), + // Module rects + module_bg: (0.15, 0.15, 0.15, 1.0), + module_bg_hover: (0.25, 0.25, 0.25, 1.0), + } + } +} + +/// Represents the main DarkClient GUI window and renders it. +pub fn render_gui(renderer: &mut Renderer) { + let mut ui = match UI_MANAGER.lock() { + Ok(guard) => guard, + Err(_) => return, + }; + + let screen_w = renderer.screen_width; + let screen_h = renderer.screen_height; + + let base_scale = (screen_h as f32 / 720.0).max(1.0).floor() as i32; + let scale_f = base_scale as f32; + + ui.update(scale_f); + + if !ui.is_visible { + return; + } + + let alpha = ui.background_alpha; + let theme = Theme::default(); + + unsafe { + // 1. Draw Full Screen Transparent Overlay + renderer.set_color( + theme.screen_bg.0, + theme.screen_bg.1, + theme.screen_bg.2, + alpha, + ); + renderer.draw_rect(0, 0, screen_w, screen_h); + + // Render each window + for window in &ui.windows { + // Rigid Title Position + let wx = (window.x * scale_f) as f32; + let wy = (window.y * scale_f) as f32; + let ww = (window.width * scale_f) as f32; + let wh = (window.height * scale_f) as f32; + let title_h = 20.0 * scale_f; + + // Veil trailing offset + let dx = (window.render_x - window.x) * scale_f; + let dy = (window.render_y - window.y) * scale_f; + + // --- Draw Title Bar --- + renderer.set_color(theme.border.0, theme.border.1, theme.border.2, alpha); + renderer.draw_rect( + wx as i32 - 1, + wy as i32 - 1, + ww as i32 + 2, + title_h as i32 + 2, + ); + + renderer.set_color( + theme.title_bg.0, + theme.title_bg.1, + theme.title_bg.2, + theme.title_bg.3 * alpha, + ); + renderer.draw_rect(wx as i32, wy as i32, ww as i32, title_h as i32); + + let text_scale = base_scale; + let t_width = get_text_width(&window.title, text_scale); + let text_x = wx as i32 + (ww as i32 - t_width) / 2; + let text_y = wy as i32 + (title_h as i32 - (7 * text_scale)) / 2; + + draw_text( + renderer, + &window.title, + text_x, + text_y, + theme.text_accent.0, + theme.text_accent.1, + theme.text_accent.2, + alpha, + text_scale, + ); + + // --- Draw Body (Veil) --- + let body_top_y = wy + title_h; + let body_h = wh - title_h; + + let tl_x = wx; + let tl_y = body_top_y; + let tr_x = wx + ww; + let tr_y = body_top_y; + + let bl_x = wx + dx; + let bl_y = body_top_y + body_h + dy; + let br_x = wx + ww + dx; + let br_y = body_top_y + body_h + dy; + + // Body Border (drawn slightly larger behind) + renderer.set_color(theme.border.0, theme.border.1, theme.border.2, alpha); + renderer.draw_quad( + tl_x - 1.0, + tl_y, + tr_x + 1.0, + tr_y, + bl_x - 1.0, + bl_y + 1.0, + br_x + 1.0, + br_y + 1.0, + ); + + // Body Background + renderer.set_color( + theme.window_bg.0, + theme.window_bg.1, + theme.window_bg.2, + theme.window_bg.3 * alpha, + ); + renderer.draw_quad(tl_x, tl_y, tr_x, tr_y, bl_x, bl_y, br_x, br_y); + + // Draw Modules inside Veil + let mut mod_y = body_top_y + (5.0 * scale_f); + for module_name in &window.modules { + let mod_h = 16.0 * scale_f; + let mod_w = ww - (10.0 * scale_f); + let mod_x = wx + (5.0 * scale_f); + + // Interpolate quad stretch + let t_top = ((mod_y - body_top_y) / body_h).clamp(0.0, 1.0); + let t_bot = ((mod_y + mod_h - body_top_y) / body_h).clamp(0.0, 1.0); + + let mtl_x = mod_x + dx * t_top; + let mtl_y = mod_y + dy * t_top; + let mtr_x = mod_x + mod_w + dx * t_top; + let mtr_y = mod_y + dy * t_top; + + let mbl_x = mod_x + dx * t_bot; + let mbl_y = mod_y + mod_h + dy * t_bot; + let mbr_x = mod_x + mod_w + dx * t_bot; + let mbr_y = mod_y + mod_h + dy * t_bot; + + renderer.set_color( + theme.module_bg.0, + theme.module_bg.1, + theme.module_bg.2, + theme.module_bg.3 * alpha, + ); + renderer.draw_quad(mtl_x, mtl_y, mtr_x, mtr_y, mbl_x, mbl_y, mbr_x, mbr_y); + + // Module text + let text_t = ((mod_y + mod_h / 2.0 - body_top_y) / body_h).clamp(0.0, 1.0); + let t_dx = dx * text_t; + let t_dy = dy * text_t; + + let mod_t_width = get_text_width(module_name, text_scale); + let mod_t_x = mod_x + t_dx + (mod_w - mod_t_width as f32) / 2.0; + let mod_t_y = mod_y + t_dy + (mod_h - (7.0 * scale_f)) / 2.0; + + draw_text( + renderer, + module_name, + mod_t_x as i32, + mod_t_y as i32, + theme.text_primary.0, + theme.text_primary.1, + theme.text_primary.2, + alpha, + text_scale, + ); + + mod_y += mod_h + (2.0 * scale_f); + } + } + } +} diff --git a/client/src/graphic/ui_manager.rs b/client/src/graphic/ui_manager.rs new file mode 100644 index 0000000..3216b18 --- /dev/null +++ b/client/src/graphic/ui_manager.rs @@ -0,0 +1,153 @@ +use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; +use lazy_static::lazy_static; +use std::sync::Mutex; + +lazy_static! { + pub static ref UI_MANAGER: Mutex = Mutex::new(UiManager::new()); +} + +pub struct WindowState { + pub title: String, + pub x: f32, + pub y: f32, + pub render_x: f32, + pub render_y: f32, + pub vel_x: f32, + pub vel_y: f32, + pub width: f32, + pub height: f32, + pub is_dragging: bool, + pub drag_offset_x: f32, + pub drag_offset_y: f32, + pub modules: Vec, +} + +pub struct UiManager { + pub windows: Vec, + pub background_alpha: f32, + pub is_visible: bool, +} + +impl UiManager { + pub fn new() -> Self { + Self { + background_alpha: 0.0, + is_visible: false, + windows: vec![ + WindowState { + title: "Combat".to_string(), + x: 50.0, + y: 50.0, + render_x: 50.0, + render_y: 50.0, + vel_x: 0.0, + vel_y: 0.0, + width: 120.0, + height: 200.0, + is_dragging: false, + drag_offset_x: 0.0, + drag_offset_y: 0.0, + modules: vec!["MobAura".to_string(), "Criticals".to_string()], + }, + WindowState { + title: "Movement".to_string(), + x: 200.0, + y: 50.0, + render_x: 200.0, + render_y: 50.0, + vel_x: 0.0, + vel_y: 0.0, + width: 120.0, + height: 200.0, + is_dragging: false, + drag_offset_x: 0.0, + drag_offset_y: 0.0, + modules: vec!["Sprint".to_string(), "Fly".to_string()], + }, + WindowState { + title: "Render".to_string(), + x: 350.0, + y: 50.0, + render_x: 350.0, + render_y: 50.0, + vel_x: 0.0, + vel_y: 0.0, + width: 120.0, + height: 200.0, + is_dragging: false, + drag_offset_x: 0.0, + drag_offset_y: 0.0, + modules: vec!["ESP".to_string(), "FullBright".to_string()], + }, + ], + } + } + + pub fn update(&mut self, scale_f: f32) { + // Handle visibility and animations + let target_visible = GUI_OPEN.load(std::sync::atomic::Ordering::Relaxed); + + if target_visible { + self.is_visible = true; + if self.background_alpha < 0.6 { + self.background_alpha += 0.05; // Fade in + } + } else { + if self.background_alpha > 0.0 { + self.background_alpha -= 0.05; // Fade out + } else { + self.is_visible = false; + } + } + + if !self.is_visible { + return; + } + + // Handle Mouse state + if let Ok(mouse) = MOUSE_STATE.lock() { + let mx = mouse.x as f32; + let my = mouse.y as f32; + + for window in &mut self.windows { + // Spring Physics + let stiffness = 0.25; // How strongly it pulls towards target + let damping = 0.65; // Defines jelly bounce vs snap (lower = more bouncy) + + let fx = (window.x - window.render_x) * stiffness; + let fy = (window.y - window.render_y) * stiffness; + + window.vel_x = (window.vel_x + fx) * damping; + window.vel_y = (window.vel_y + fy) * damping; + + window.render_x += window.vel_x; + window.render_y += window.vel_y; + + let scaled_x = window.render_x * scale_f; + let scaled_y = window.render_y * scale_f; + let scaled_w = window.width * scale_f; + let title_height = 20.0 * scale_f; + + let is_hovering_title = mx >= scaled_x + && mx <= scaled_x + scaled_w + && my >= scaled_y + && my <= scaled_y + title_height; + + if mouse.left_down { + if is_hovering_title && !window.is_dragging { + window.is_dragging = true; + window.drag_offset_x = (mx - scaled_x) / scale_f; + window.drag_offset_y = (my - scaled_y) / scale_f; + } + + if window.is_dragging { + window.x = (mx / scale_f) - window.drag_offset_x; + window.y = (my / scale_f) - window.drag_offset_y; + } + } else { + window.is_dragging = false; + } + } + } + } +} diff --git a/client/src/gui.rs b/client/src/gui.rs index 052b5bb..da3cabc 100644 --- a/client/src/gui.rs +++ b/client/src/gui.rs @@ -3,10 +3,15 @@ use crate::module::{ModuleCategory, ModuleSetting}; use crate::{cleanup_client, RUNNING}; use eframe::Frame; use egui::{Context, ScrollArea, Ui}; +use std::cell::Cell; use std::sync::atomic::Ordering::Relaxed; #[cfg(target_os = "linux")] use winit::platform::x11::EventLoopBuilderExtX11; +thread_local! { + pub static IS_EGUI_THREAD: Cell = Cell::new(false); +} + pub fn call_panic() { let client = DarkClient::instance(); client.modules.read().unwrap().values().for_each(|module| { @@ -68,6 +73,8 @@ impl Default for GUI { impl eframe::App for GUI { fn update(&mut self, ctx: &Context, _frame: &mut Frame) { + crate::gui::IS_EGUI_THREAD.with(|f| f.set(true)); + ctx.request_repaint(); if !RUNNING.load(Relaxed) { diff --git a/client/src/hook.rs b/client/src/hook.rs deleted file mode 100644 index 2215571..0000000 --- a/client/src/hook.rs +++ /dev/null @@ -1,338 +0,0 @@ -use std::fs::File; -use std::io::{BufRead, BufReader}; -use crate::client::DarkClient; -use crate::mapping::client::minecraft::Minecraft; -use cfg_if::cfg_if; -use log::{info, error}; -use std::sync::atomic::{AtomicI32, AtomicBool, Ordering}; -use std::sync::{Mutex, Once, OnceLock}; -use std::time::Instant; -use ilhook::x64::HookPoint; -use libc::{RTLD_GLOBAL, RTLD_LAZY}; -// Importa il crate gl per disegnare -use crate::{gl, RUNNING}; - -static LAST_TICK: AtomicI32 = AtomicI32::new(0); -static GL_LOADED: AtomicBool = AtomicBool::new(false); - -// Creiamo un wrapper per aggirare il blocco del compilatore -pub struct HookHandle(HookPoint); - -// DICIAMO A RUST: "Fidati, posso spostare questo oggetto tra thread" -unsafe impl Send for HookHandle {} -unsafe impl Sync for HookHandle {} - -// Global storage per l'hook attivo. -// Usiamo un Mutex per poterlo modificare (rimuovere) a runtime. -// Nota: Il tipo esatto dipende da cosa restituisce hooker.hook(). -// Solitamente è un oggetto che implementa Drop o ha un metodo unhook. -// Per ilhook-rs, l'oggetto `Hook` gestisce l'unhooking quando viene droppato. -// Aggiorniamo lo storage globale per usare questo tipo specifico, non dyn Any -static GLOBAL_HOOK: OnceLock>> = OnceLock::new(); - -fn get_global_hook() -> &'static Mutex> { - GLOBAL_HOOK.get_or_init(|| Mutex::new(None)) -} - -cfg_if! { - if #[cfg(target_os = "linux")] { - use std::ffi::{CString, CStr}; - use ilhook::x64::{Hooker, Registers, CallbackOption, HookFlags, HookType}; - use libc::{c_void, c_char}; - - // Helper per caricare le funzioni OpenGL su Linux - fn get_proc_address(addr: &str) -> *const c_void { - unsafe { - let s = CString::new(addr).unwrap(); - // Prova prima con glXGetProcAddress se disponibile, altrimenti dlsym - // Qui usiamo un approccio semplificato assumendo che libGL sia caricata - let lib = libc::dlopen(CString::new("libGL.so.1").unwrap().as_ptr(), libc::RTLD_LAZY); - if !lib.is_null() { - libc::dlsym(lib, s.as_ptr()) - } else { - std::ptr::null() - } - } - } - - unsafe extern "win64" fn my_swap_buffers_hook(_regs: *mut Registers, _user_data: usize) { - on_frame(); - } - - } else if #[cfg(target_os = "windows")] { - use ilhook::x64::{Hooker, Registers, CallbackOption, HookFlags, HookType}; - use libloading::os::windows::{Library, Symbol}; - use std::ffi::CString; - - // Helper per caricare le funzioni OpenGL su Windows - fn get_proc_address(addr: &str) -> *const std::ffi::c_void { - unsafe { - // Metodo robusto: prova wglGetProcAddress, poi GetProcAddress - let c_str = CString::new(addr).unwrap(); - - // Nota: In un contesto reale, dovremmo aver caricato opengl32.dll staticamente o lazy - // Qui facciamo un tentativo "sporco" ma funzionale per l'injection - let lib = libloading::Library::new("opengl32.dll"); - if let Ok(l) = lib { - // Prima prova wglGetProcAddress (per estensioni moderne) - let wgl_get: Result *const std::ffi::c_void>, _> = l.get(b"wglGetProcAddress"); - if let Ok(wgl) = wgl_get { - let ptr = wgl(c_str.as_ptr()); - if !ptr.is_null() { - return ptr; - } - } - // Fallback a GetProcAddress (per funzioni base GL 1.1) - let func: Result, _> = l.get(c_str.as_bytes()); - if let Ok(f) = func { - return *f as *const std::ffi::c_void; - } - } - std::ptr::null() - } - } - - unsafe extern "win64" fn my_swap_buffers_hook(_regs: *mut Registers, _user_data: usize) { - on_frame(); - } - } -} - -const CONTEXT_PROFILE_MASK: u32 = 0x9126; -const CONTEXT_CORE_PROFILE_BIT: u32 = 0x00000001; - -// Funzione chiamata ogni frame grafico -unsafe fn on_frame() { - // === PANIC CHECK (La modifica importante) === - // Se cleanup_client() è stato chiamato, RUNNING diventa false. - // Se è false, usciamo IMMEDIATAMENTE. Non tickiamo, non disegniamo. - if !RUNNING.load(Ordering::SeqCst) { - return; - } - - // Inizializza GL pointers... - if !GL_LOADED.load(Ordering::Relaxed) { - gl::load_with(|s| get_proc_address(s)); - GL_LOADED.store(true, Ordering::Relaxed); - } - - check_tick(); - render_overlay(); -} - -// === LOGICA DI RENDERING (Fix per i glitch grafici) === -unsafe fn render_overlay() { - // === 1. DATI FINESTRA === - // Otteniamo le dimensioni reali della finestra (funziona anche se ridimensioni) - let mut viewport = [0; 4]; // [x, y, width, height] - gl::GetIntegerv(gl::VIEWPORT, viewport.as_mut_ptr()); - - let screen_w = viewport[2]; - let screen_h = viewport[3]; - - // === 2. BACKUP STATO === - let is_scissor_on = gl::IsEnabled(gl::SCISSOR_TEST) == gl::TRUE; - let mut old_scissor_box = [0; 4]; - gl::GetIntegerv(gl::SCISSOR_BOX, old_scissor_box.as_mut_ptr()); - let mut old_clear_color = [0.0; 4]; - gl::GetFloatv(gl::COLOR_CLEAR_VALUE, old_clear_color.as_mut_ptr()); - - // Abilita il taglio - gl::Enable(gl::SCISSOR_TEST); - - // === 3. DISEGNO PIXEL ART (Helper Closure) === - // Definiamo un colore Giallo - gl::ClearColor(1.0, 1.0, 0.0, 1.0); - - // Funzione interna per disegnare un blocco - // x, y sono relativi all'angolo IN ALTO A SINISTRA - let draw_block = |x: i32, y: i32, w: i32, h: i32| { - // OpenGL ha (0,0) in BASSO a sinistra. Dobbiamo invertire la Y. - // Y_gl = AltezzaSchermo - Y_alto - AltezzaBlocco - let gl_y = screen_h - y - h; - - gl::Scissor(x, gl_y, w, h); - gl::Clear(gl::COLOR_BUFFER_BIT); - }; - - // --- DISEGNO LA "D" (Di Dark) --- - // Posizione: 10px dal bordo sinistro, 10px dal bordo alto - let start_x = 10; - let start_y = 10; - let thickness = 5; - let size = 30; // Altezza lettera - - // Asta Verticale - draw_block(start_x, start_y, thickness, size); - // Trattino Alto - draw_block(start_x, start_y, size - 10, thickness); - // Trattino Basso - draw_block(start_x, start_y + size - thickness, size - 10, thickness); - // Asta Destra (chiusura D) - draw_block(start_x + size - 10, start_y + thickness, thickness, size - (thickness * 2)); - - // --- DISEGNO LA "C" (Di Client) --- - // Spostiamoci a destra - let start_x = 50; - - // Asta Verticale - draw_block(start_x, start_y, thickness, size); - // Trattino Alto - draw_block(start_x, start_y, size - 5, thickness); - // Trattino Basso - draw_block(start_x, start_y + size - thickness, size - 5, thickness); - - // === 4. RIPRISTINO STATO === - gl::ClearColor(old_clear_color[0], old_clear_color[1], old_clear_color[2], old_clear_color[3]); - gl::Scissor(old_scissor_box[0], old_scissor_box[1], old_scissor_box[2], old_scissor_box[3]); - if !is_scissor_on { - gl::Disable(gl::SCISSOR_TEST); - } -} - -fn check_tick() { - let client = DarkClient::instance(); - // Try to get env without attaching if possible, or attach as daemon. - let _env = match client.jvm.attach_current_thread_as_daemon() { - Ok(env) => env, - Err(_) => return, - }; - - let minecraft = Minecraft::instance(); - - // Controllo errori per evitare crash - if minecraft.player.entity.is_null() { - return; - } - - let tick_count = match minecraft.player.entity.get_tick_count() { - Ok(t) => t, - Err(_) => return, - }; - - let last_tick = LAST_TICK.load(Ordering::Relaxed); - - if tick_count > last_tick { - LAST_TICK.store(tick_count, Ordering::Relaxed); - client.tick(); - } -} - -fn find_library_path(partial_name: &str) -> Option { - if let Ok(file) = File::open("/proc/self/maps") { - let reader = BufReader::new(file); - for line in reader.lines() { - if let Ok(l) = line { - // Cerchiamo una riga che contenga il nome (es. "liblwjgl_opengl.so") - if l.contains(partial_name) && l.contains(".so") { - // Il formato è: indirizzo permessi offset dev inode PERCORSO - // Prendiamo l'ultima parte della stringa - if let Some(path) = l.split_whitespace().last() { - return Some(path.to_string()); - } - } - } - } - } - None -} - -pub fn install_hooks() -> anyhow::Result<()> { - cfg_if! { - if #[cfg(target_os = "linux")] { - unsafe { - let mut targets = Vec::new(); - - if let Some(path) = find_library_path("libglfw.so") { - info!("Trovata libreria GLFW: {}", path); - targets.push((path, "glfwSwapBuffers")); - } - else if let Some(path) = find_library_path("liblwjgl.so") { - info!("Trovata libreria LWJGL (Legacy): {}", path); - targets.push((path, "glXSwapBuffers")); - } - else { - info!("Nessuna libreria specifica trovata, provo libGL di sistema..."); - targets.push(("libGL.so.1".to_string(), "glXSwapBuffers")); - } - - let mut hooked_count = 0; - - for (lib_path, func_name) in targets { - let c_lib_path = CString::new(lib_path.clone())?; - let c_func_name = CString::new(func_name)?; - - let lib = libc::dlopen(c_lib_path.as_ptr(), libc::RTLD_LAZY); - - if !lib.is_null() { - let target_addr = libc::dlsym(lib, c_func_name.as_ptr()) as usize; - - if target_addr != 0 { - info!("Trovato {} in {} a 0x{:x}", func_name, lib_path, target_addr); - - let hooker = Hooker::new( - target_addr, - HookType::JmpBack(my_swap_buffers_hook), - CallbackOption::None, - 0, - HookFlags::empty() - ); - - match hooker.hook() { - Ok(hook) => { - // Recuperiamo il lock globale - let mut guard = get_global_hook().lock().unwrap(); - - // Se c'era un hook vecchio, lo sovrascriviamo (triggerando l'unhook automatico del vecchio) - if guard.is_some() { - info!("Rilevato vecchio hook, rimozione e sostituzione..."); - } - - // === QUI LA MAGIA === - // Avvolgiamo l'hook nel nostro wrapper "Thread Safe" - *guard = Some(HookHandle(hook)); - - hooked_count += 1; - info!(">>> HOOK ATTIVO E SALVATO SU: {} <<<", lib_path); - - // Usciamo dal loop, ne basta uno attivo - break; - }, - Err(e) => { - info!("Errore installazione hook su {}: {:?}", lib_path, e); - } - } - } - } - } - - if hooked_count == 0 { - // Qui potremmo ritornare errore, MA se stiamo re-iniettando e qualcosa è andato storto - // col flag statico, potremmo voler "fingere" che vada tutto bene. - // Tuttavia, per ora lasciamo l'errore se count è 0. - return Err(anyhow::anyhow!("Fallito l'hook su tutte le librerie candidate!")); - } - } - } else if #[cfg(target_os = "windows")] { - // ... Codice Windows (non cambia molto, ma aggiungi il check HOOKS_INSTALLED se vuoi) ... - // Per brevità ometto, ma il concetto è identico. - } - } - Ok(()) -} - -pub fn uninstall_hooks() { - // Prendiamo il lock - let mut guard = get_global_hook().lock().unwrap(); - - if guard.is_some() { - info!("Rimozione hook fisico in corso..."); - // Impostando a None, il Wrapper viene distrutto. - // Il Wrapper distrugge l'HookPoint interno. - // L'HookPoint interno ripristina i byte originali della memoria. - *guard = None; - info!("Hook rimosso correttamente. Memoria pulita."); - } else { - info!("Nessun hook attivo da rimuovere."); - } -} \ No newline at end of file diff --git a/client/src/lib.rs b/client/src/lib.rs index 28dbceb..99437b3 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2,8 +2,8 @@ extern crate jni; mod client; +mod graphic; mod gui; -mod hook; mod mapping; mod module; @@ -13,6 +13,7 @@ pub mod gl { use crate::client::keyboard::{start_keyboard_handler, stop_keyboard_handler}; use crate::client::DarkClient; +use crate::graphic::hook::{install_hooks, uninstall_hooks}; use crate::gui::start_gui; use crate::mapping::client::minecraft::Minecraft; use crate::module::combat::mobaura::MobAuraModule; @@ -25,7 +26,6 @@ use std::fs::File; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, OnceLock}; use std::thread; -use crate::hook::uninstall_hooks; static GUI_THREAD: OnceLock>>> = OnceLock::new(); @@ -54,6 +54,14 @@ pub extern "C" fn initialize_client() { Err(e) => eprintln!("Error during logger initialization: {:?}", e), } + // Set up a custom panic hook to guarantee we release mouse/keyboard hooks + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + error!("DarkClient Panicked! Attempting to unhook inputs..."); + cleanup_client(); + default_hook(panic_info); + })); + thread::spawn(|| { info!("Starting DarkClient..."); let minecraft = Minecraft::instance(); @@ -63,7 +71,7 @@ pub extern "C" fn initialize_client() { start_keyboard_handler(); // Install hooks - if let Err(e) = hook::install_hooks() { + if let Err(e) = install_hooks() { error!("Failed to install hooks: {}", e); } @@ -97,6 +105,9 @@ pub extern "C" fn cleanup_client() { // Stop the keyboard handler stop_keyboard_handler(); + // Unlock GLFW Input / Restore callbacks if GUI was open + crate::graphic::input::cleanup(); + let gui_handle = { let mut gui_lock = gui_thread().lock().unwrap(); gui_lock.take() From 7ee9cc30c76d857d32d789ce3bc610c57fdbc086 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Fri, 20 Feb 2026 15:16:11 +0100 Subject: [PATCH 03/16] Implement cheat list on UI --- agent_loader/src/lib.rs | 10 +- client/src/graphic/input.rs | 20 +- client/src/graphic/render.rs | 18 ++ client/src/graphic/ui.rs | 431 +++++++++++++++++++++++++++++-- client/src/graphic/ui_manager.rs | 153 ++++++++++- 5 files changed, 598 insertions(+), 34 deletions(-) diff --git a/agent_loader/src/lib.rs b/agent_loader/src/lib.rs index 031e6f7..106afb1 100644 --- a/agent_loader/src/lib.rs +++ b/agent_loader/src/lib.rs @@ -84,8 +84,14 @@ fn setup_signal_handlers() { } unsafe { - libc::signal(libc::SIGTERM, handle_signal as libc::sighandler_t); - libc::signal(libc::SIGINT, handle_signal as libc::sighandler_t); + libc::signal( + libc::SIGTERM, + handle_signal as *const () as libc::sighandler_t, + ); + libc::signal( + libc::SIGINT, + handle_signal as *const () as libc::sighandler_t, + ); } info!("Signal handlers installed"); diff --git a/client/src/graphic/input.rs b/client/src/graphic/input.rs index 927af97..f914d50 100644 --- a/client/src/graphic/input.rs +++ b/client/src/graphic/input.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; // Global Input State -pub static GUI_OPEN: AtomicBool = AtomicBool::new(true); +pub static GUI_OPEN: AtomicBool = AtomicBool::new(false); lazy_static::lazy_static! { pub static ref MOUSE_STATE: Mutex = Mutex::new(MouseState::default()); @@ -17,6 +17,8 @@ pub struct MouseState { pub y: f64, pub left_down: bool, pub right_down: bool, + pub left_clicked: bool, + pub right_clicked: bool, } #[cfg(target_os = "linux")] @@ -31,6 +33,7 @@ mod linux_input { static mut GLFW_WINDOW: *mut c_void = std::ptr::null_mut(); static mut GLFW_SET_INPUT_MODE: Option = None; + static mut GLFW_SET_CURSOR_POS: Option = None; // To prevent massive camera spins when un-grabbing static mut C_LOCK_X: f64 = 0.0; @@ -53,11 +56,13 @@ mod linux_input { // GLFW_MOUSE_BUTTON_LEFT if let Ok(mut state) = MOUSE_STATE.lock() { state.left_down = true; + state.left_clicked = true; } } else if button == 1 { // GLFW_MOUSE_BUTTON_RIGHT if let Ok(mut state) = MOUSE_STATE.lock() { state.right_down = true; + state.right_clicked = true; } } } else if action == 0 { @@ -139,6 +144,9 @@ mod linux_input { set_mode(GLFW_WINDOW, 0x00033001, 0x00034001); } else { // GUI Closed -> Disabled / Captured Pointer + if let Some(set_cursor_pos) = GLFW_SET_CURSOR_POS { + set_cursor_pos(GLFW_WINDOW, C_LOCK_X, C_LOCK_Y); + } set_mode(GLFW_WINDOW, 0x00033001, 0x00034003); } } @@ -196,6 +204,9 @@ mod linux_input { let set_input_mode: extern "C" fn(*mut c_void, i32, i32) = std::mem::transmute( libc::dlsym(libglfw, CString::new("glfwSetInputMode").unwrap().as_ptr()), ); + let set_cursor_pos_func: extern "C" fn(*mut c_void, f64, f64) = std::mem::transmute( + libc::dlsym(libglfw, CString::new("glfwSetCursorPos").unwrap().as_ptr()), + ); let window = get_current_context(); if window.is_null() { @@ -213,6 +224,7 @@ mod linux_input { // Store globally so we can toggle grab state later GLFW_WINDOW = window; GLFW_SET_INPUT_MODE = Some(set_input_mode); + GLFW_SET_CURSOR_POS = Some(set_cursor_pos_func); // Assuming GUI is open by default: ungrab mouse right away if GUI_OPEN.load(Ordering::Relaxed) { @@ -276,6 +288,7 @@ mod windows_input { static mut GLFW_WINDOW: *mut c_void = std::ptr::null_mut(); static mut GLFW_SET_INPUT_MODE: Option = None; + static mut GLFW_SET_CURSOR_POS: Option = None; static mut C_LOCK_X: f64 = 0.0; static mut C_LOCK_Y: f64 = 0.0; @@ -294,10 +307,12 @@ mod windows_input { if button == 0 { if let Ok(mut state) = MOUSE_STATE.lock() { state.left_down = true; + state.left_clicked = true; } } else if button == 1 { if let Ok(mut state) = MOUSE_STATE.lock() { state.right_down = true; + state.right_clicked = true; } } } else if action == 0 { @@ -366,6 +381,9 @@ mod windows_input { if next { set_mode(GLFW_WINDOW, 0x00033001, 0x00034001); } else { + if let Some(set_cursor_pos) = GLFW_SET_CURSOR_POS { + set_cursor_pos(GLFW_WINDOW, C_LOCK_X, C_LOCK_Y); + } set_mode(GLFW_WINDOW, 0x00033001, 0x00034003); } } diff --git a/client/src/graphic/render.rs b/client/src/graphic/render.rs index 6b08f07..585f19b 100644 --- a/client/src/graphic/render.rs +++ b/client/src/graphic/render.rs @@ -113,6 +113,7 @@ pub struct Renderer { old_vbo: i32, old_depth_test: bool, old_cull_face: bool, + old_scissor_test: bool, current_color: (f32, f32, f32, f32), } @@ -147,6 +148,7 @@ impl Renderer { let old_depth_test = gl::IsEnabled(gl::DEPTH_TEST) == gl::TRUE; let old_cull_face = gl::IsEnabled(gl::CULL_FACE) == gl::TRUE; + let old_scissor_test = gl::IsEnabled(gl::SCISSOR_TEST) == gl::TRUE; // Apply our states gl::Enable(gl::BLEND); @@ -172,6 +174,7 @@ impl Renderer { old_vbo, old_depth_test, old_cull_face, + old_scissor_test, current_color: (1.0, 1.0, 1.0, 1.0), } } @@ -221,6 +224,16 @@ impl Renderer { gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4); } + pub unsafe fn enable_scissor(&self, x: i32, y: i32, w: i32, h: i32) { + gl::Enable(gl::SCISSOR_TEST); + let bottom_y = self.screen_height - (y + h); + gl::Scissor(x, bottom_y, w, h); + } + + pub unsafe fn disable_scissor(&self) { + gl::Disable(gl::SCISSOR_TEST); + } + pub unsafe fn draw_quad( &mut self, x1: f32, @@ -289,6 +302,11 @@ impl Drop for Renderer { if self.old_cull_face { gl::Enable(gl::CULL_FACE); } + if !self.old_scissor_test { + gl::Disable(gl::SCISSOR_TEST); + } else { + gl::Enable(gl::SCISSOR_TEST); + } if !self.is_blend_on { gl::Disable(gl::BLEND); diff --git a/client/src/graphic/ui.rs b/client/src/graphic/ui.rs index 1a5d40a..bdce53e 100644 --- a/client/src/graphic/ui.rs +++ b/client/src/graphic/ui.rs @@ -1,4 +1,6 @@ +use crate::client::DarkClient; use crate::graphic::font::{draw_text, get_text_width}; +use crate::graphic::input::MOUSE_STATE; use crate::graphic::render::Renderer; use crate::graphic::ui_manager::UI_MANAGER; @@ -35,6 +37,96 @@ impl Default for Theme { } } +pub enum HudColor { + Yellow, + Green, + Red, + Blue, + White, + Purple, + Cyan, +} + +impl HudColor { + pub fn to_rgba(&self) -> (f32, f32, f32, f32) { + match self { + HudColor::Yellow => (1.0, 1.0, 0.0, 1.0), + HudColor::Green => (0.2, 0.8, 0.2, 1.0), + HudColor::Red => (0.9, 0.2, 0.2, 1.0), + HudColor::Blue => (0.2, 0.4, 1.0, 1.0), + HudColor::White => (1.0, 1.0, 1.0, 1.0), + HudColor::Purple => (0.6, 0.2, 0.8, 1.0), + HudColor::Cyan => (0.2, 0.8, 0.9, 1.0), + } + } +} + +unsafe fn draw_hud(renderer: &mut Renderer, scale_f: f32) { + let watermark = "DarkClient"; + let w_color = HudColor::Yellow.to_rgba(); + + let mut hud_y = 5.0 * scale_f; + let hud_x = 5.0 * scale_f; + let text_scale = (1.2 * scale_f) as i32; + + draw_text( + renderer, + watermark, + hud_x as i32, + hud_y as i32, + w_color.0, + w_color.1, + w_color.2, + w_color.3, + text_scale, + ); + + hud_y += 18.0 * scale_f; + + if let Ok(modules_map) = DarkClient::instance().modules.read() { + let mut active_mods: Vec = modules_map + .values() + .filter_map(|m| { + let lock = m.lock().unwrap(); + if lock.get_module_data().enabled { + Some(lock.get_module_data().name.clone()) + } else { + None + } + }) + .collect(); + + // Sort by length (longest first) + active_mods.sort_by(|a, b| b.len().cmp(&a.len())); + + let arraylist_colors = [ + HudColor::Purple, + HudColor::Cyan, + HudColor::Green, + HudColor::Red, + HudColor::Yellow, + HudColor::Blue, + HudColor::White, + ]; + + for (i, mod_name) in active_mods.iter().enumerate() { + let color = arraylist_colors[i % arraylist_colors.len()].to_rgba(); + draw_text( + renderer, + mod_name, + hud_x as i32, + hud_y as i32, + color.0, + color.1, + color.2, + color.3, + scale_f as i32, + ); + hud_y += 14.0 * scale_f; + } + } +} + /// Represents the main DarkClient GUI window and renders it. pub fn render_gui(renderer: &mut Renderer) { let mut ui = match UI_MANAGER.lock() { @@ -50,6 +142,10 @@ pub fn render_gui(renderer: &mut Renderer) { ui.update(scale_f); + unsafe { + draw_hud(renderer, scale_f); + } + if !ui.is_visible { return; } @@ -67,8 +163,21 @@ pub fn render_gui(renderer: &mut Renderer) { ); renderer.draw_rect(0, 0, screen_w, screen_h); + // Retrieve mouse state for bounds interactions + let (mx, my, mut left_clicked, mut right_clicked) = { + if let Ok(mut mouse) = MOUSE_STATE.lock() { + let l = mouse.left_clicked; + let r = mouse.right_clicked; + mouse.left_clicked = false; + mouse.right_clicked = false; + (mouse.x as f32, mouse.y as f32, l, r) + } else { + (0.0, 0.0, false, false) + } + }; + // Render each window - for window in &ui.windows { + for window in &mut ui.windows { // Rigid Title Position let wx = (window.x * scale_f) as f32; let wy = (window.y * scale_f) as f32; @@ -77,8 +186,13 @@ pub fn render_gui(renderer: &mut Renderer) { let title_h = 20.0 * scale_f; // Veil trailing offset - let dx = (window.render_x - window.x) * scale_f; - let dy = (window.render_y - window.y) * scale_f; + let mut dx = (window.render_x - window.x) * scale_f; + let mut dy = (window.render_y - window.y) * scale_f; + + // Cap the stretch visual effect + let max_stretch = 30.0 * scale_f; + dx = dx.clamp(-max_stretch, max_stretch); + dy = dy.clamp(-max_stretch, max_stretch); // --- Draw Title Bar --- renderer.set_color(theme.border.0, theme.border.1, theme.border.2, alpha); @@ -152,14 +266,76 @@ pub fn render_gui(renderer: &mut Renderer) { // Draw Modules inside Veil let mut mod_y = body_top_y + (5.0 * scale_f); - for module_name in &window.modules { - let mod_h = 16.0 * scale_f; + for module_state in &mut window.modules { + let module_name = &module_state.name; + let mod_act_h = 16.0 * scale_f; // base height let mod_w = ww - (10.0 * scale_f); let mod_x = wx + (5.0 * scale_f); + let box_side = mod_act_h; + let box_x = mod_x + mod_w - box_side; + let box_y = mod_y; + + // Check interactions + let is_hovering = + mx >= mod_x && mx <= mod_x + mod_w && my >= mod_y && my <= mod_y + mod_act_h; + + let is_box_hovering = + mx >= box_x && mx <= box_x + box_side && my >= box_y && my <= box_y + box_side; + + let mut is_enabled = false; + if let Some(m) = DarkClient::instance() + .modules + .read() + .unwrap() + .get(module_name) + { + let mut lock = m.lock().unwrap(); + is_enabled = lock.get_module_data().enabled; + + if is_hovering { + if left_clicked { + left_clicked = false; + + if is_box_hovering { + module_state.is_expanded = !module_state.is_expanded; + } else { + lock.get_module_data_mut().set_enabled(!is_enabled); + if !is_enabled { + let _ = lock.on_start(); + } else { + let _ = lock.on_stop(); + } + is_enabled = !is_enabled; + } + } + if right_clicked { + right_clicked = false; + module_state.is_expanded = !module_state.is_expanded; + } + } + } + + // Calculate visual height including expansions + let mut expanded_height = 0.0; + if module_state.expand_anim > 0.01 { + if let Some(m) = DarkClient::instance() + .modules + .read() + .unwrap() + .get(module_name) + { + let lock = m.lock().unwrap(); + let settings_count = lock.get_module_data().settings.len() as f32; + expanded_height = settings_count * 14.0 * scale_f; + } + } + + let mod_total_visual_h = mod_act_h + (module_state.expand_anim * expanded_height); + // Interpolate quad stretch let t_top = ((mod_y - body_top_y) / body_h).clamp(0.0, 1.0); - let t_bot = ((mod_y + mod_h - body_top_y) / body_h).clamp(0.0, 1.0); + let t_bot = ((mod_y + mod_total_visual_h - body_top_y) / body_h).clamp(0.0, 1.0); let mtl_x = mod_x + dx * t_top; let mtl_y = mod_y + dy * t_top; @@ -167,32 +343,82 @@ pub fn render_gui(renderer: &mut Renderer) { let mtr_y = mod_y + dy * t_top; let mbl_x = mod_x + dx * t_bot; - let mbl_y = mod_y + mod_h + dy * t_bot; + let mbl_y = mod_y + mod_total_visual_h + dy * t_bot; let mbr_x = mod_x + mod_w + dx * t_bot; - let mbr_y = mod_y + mod_h + dy * t_bot; + let mbr_y = mod_y + mod_total_visual_h + dy * t_bot; - renderer.set_color( - theme.module_bg.0, - theme.module_bg.1, - theme.module_bg.2, - theme.module_bg.3 * alpha, - ); + let bg_color = if is_hovering { + theme.module_bg_hover + } else { + theme.module_bg + }; + + renderer.set_color(bg_color.0, bg_color.1, bg_color.2, bg_color.3 * alpha); renderer.draw_quad(mtl_x, mtl_y, mtr_x, mtr_y, mbl_x, mbl_y, mbr_x, mbr_y); // Module text - let text_t = ((mod_y + mod_h / 2.0 - body_top_y) / body_h).clamp(0.0, 1.0); + let text_t = ((mod_y + mod_act_h / 2.0 - body_top_y) / body_h).clamp(0.0, 1.0); let t_dx = dx * text_t; let t_dy = dy * text_t; let mod_t_width = get_text_width(module_name, text_scale); let mod_t_x = mod_x + t_dx + (mod_w - mod_t_width as f32) / 2.0; - let mod_t_y = mod_y + t_dy + (mod_h - (7.0 * scale_f)) / 2.0; + let mod_t_y = mod_y + t_dy + (mod_act_h - (7.0 * scale_f)) / 2.0; + + let text_color = if is_enabled { + theme.text_accent + } else { + theme.text_primary + }; draw_text( renderer, module_name, mod_t_x as i32, mod_t_y as i32, + text_color.0, + text_color.1, + text_color.2, + alpha, + text_scale, + ); + + // Draw arrow box + let t_dx_box = dx * text_t; + let t_dy_box = dy * text_t; + let actual_box_x = box_x + t_dx_box; + let actual_box_y = box_y + t_dy_box; + + let box_bg = if is_box_hovering { + theme.module_bg_hover + } else { + theme.module_bg + }; + + // Border separator + renderer.set_color(theme.border.0, theme.border.1, theme.border.2, alpha * 0.5); + renderer.draw_rect( + actual_box_x as i32 - 1, + actual_box_y as i32, + box_side as i32 + 1, + box_side as i32, + ); + + renderer.set_color(box_bg.0, box_bg.1, box_bg.2, box_bg.3 * alpha); + renderer.draw_rect( + actual_box_x as i32, + actual_box_y as i32, + box_side as i32, + box_side as i32, + ); + + let arrow_char = if module_state.is_expanded { "^" } else { "v" }; + let arrow_w = get_text_width(arrow_char, text_scale); + draw_text( + renderer, + arrow_char, + (actual_box_x + (box_side - arrow_w as f32) / 2.0) as i32, + (actual_box_y + (box_side - 7.0 * scale_f) / 2.0) as i32, theme.text_primary.0, theme.text_primary.1, theme.text_primary.2, @@ -200,7 +426,178 @@ pub fn render_gui(renderer: &mut Renderer) { text_scale, ); - mod_y += mod_h + (2.0 * scale_f); + // Draw settings + if module_state.expand_anim > 0.01 { + if let Some(m) = DarkClient::instance() + .modules + .read() + .unwrap() + .get(module_name) + { + let mut lock = m.lock().unwrap(); + let data = lock.get_module_data_mut(); + + let mut set_y = mod_y + mod_act_h; + + // Let's get mouse dragged state + let left_down = if let Ok(mouse) = MOUSE_STATE.lock() { + mouse.left_down + } else { + false + }; + + for setting in &mut data.settings { + let set_h = 14.0 * scale_f; + let text_t = + ((set_y + set_h / 2.0 - body_top_y) / body_h).clamp(0.0, 1.0); + let t_dx = dx * text_t; + let t_dy = dy * text_t; + + let set_x = mod_x + (10.0 * scale_f); // Indent + let set_w = mod_w - (10.0 * scale_f); + + // Calculate skewed positions for interaction + let actual_sx = set_x + t_dx; + let actual_sy = set_y + t_dy; + + let is_set_hovering = mx >= actual_sx + && mx <= actual_sx + set_w + && my >= actual_sy + && my <= actual_sy + set_h; + + let set_alpha = alpha * module_state.expand_anim; + + match setting { + crate::module::ModuleSetting::Toggle { name, value } => { + if is_set_hovering && left_clicked { + *value = !*value; + left_clicked = false; + } + + let t_color = if *value { + theme.text_accent + } else { + theme.text_primary + }; + + draw_text( + renderer, + &format!("{}: {}", name, if *value { "On" } else { "Off" }), + actual_sx as i32, + (actual_sy + (set_h - 7.0 * scale_f) / 2.0) as i32, + t_color.0, + t_color.1, + t_color.2, + set_alpha, + text_scale, + ); + } + crate::module::ModuleSetting::Slider { + name, + value, + min, + max, + } => { + let slider_w = set_w - (60.0 * scale_f); + let slider_x = actual_sx + (50.0 * scale_f); + + if is_set_hovering && left_down { + let relative_x = (mx - slider_x).clamp(0.0, slider_w); + let factor = relative_x / slider_w; + *value = *min + factor * (*max - *min); + // Optional: snap or round value here + } + + draw_text( + renderer, + &format!("{}: {:.1}", name, value), + actual_sx as i32, + (actual_sy + (set_h - 7.0 * scale_f) / 2.0) as i32, + theme.text_primary.0, + theme.text_primary.1, + theme.text_primary.2, + set_alpha, + text_scale, + ); + + // Draw thin slider line + renderer.set_color( + theme.text_primary.0, + theme.text_primary.1, + theme.text_primary.2, + set_alpha * 0.5, + ); + renderer.draw_rect( + slider_x as i32, + (actual_sy + set_h / 2.0) as i32, + slider_w as i32, + (2.0 * scale_f) as i32, + ); + + // Draw handle + let handle_x = + slider_x + ((*value - *min) / (*max - *min)) * slider_w; + renderer.set_color( + theme.text_accent.0, + theme.text_accent.1, + theme.text_accent.2, + set_alpha, + ); + renderer.draw_rect( + (handle_x - 2.0 * scale_f) as i32, + (actual_sy + set_h / 2.0 - 2.0 * scale_f) as i32, + (4.0 * scale_f) as i32, + (6.0 * scale_f) as i32, + ); + } + crate::module::ModuleSetting::Choice { + name, + value, + options, + } => { + if is_set_hovering && left_clicked { + *value = (*value + 1) % options.len(); + left_clicked = false; + } + let current_opt = if *value < options.len() { + &options[*value] + } else { + "Unknown" + }; + + draw_text( + renderer, + &format!("{}: {}", name, current_opt), + actual_sx as i32, + (actual_sy + (set_h - 7.0 * scale_f) / 2.0) as i32, + theme.text_primary.0, + theme.text_primary.1, + theme.text_primary.2, + set_alpha, + text_scale, + ); + } + crate::module::ModuleSetting::Color { name, .. } => { + draw_text( + renderer, + &format!("{}: [Color]", name), + actual_sx as i32, + (actual_sy + (set_h - 7.0 * scale_f) / 2.0) as i32, + theme.text_primary.0, + theme.text_primary.1, + theme.text_primary.2, + set_alpha, + text_scale, + ); + } + } + + set_y += set_h; + } + } + } + + mod_y += mod_total_visual_h + (2.0 * scale_f); } } } diff --git a/client/src/graphic/ui_manager.rs b/client/src/graphic/ui_manager.rs index 3216b18..9badfb2 100644 --- a/client/src/graphic/ui_manager.rs +++ b/client/src/graphic/ui_manager.rs @@ -1,4 +1,6 @@ +use crate::client::DarkClient; use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; +use crate::module::ModuleCategory; use lazy_static::lazy_static; use std::sync::Mutex; @@ -6,8 +8,15 @@ lazy_static! { pub static ref UI_MANAGER: Mutex = Mutex::new(UiManager::new()); } +pub struct ModuleUiState { + pub name: String, + pub is_expanded: bool, + pub expand_anim: f32, // 0.0 to 1.0 +} + pub struct WindowState { pub title: String, + pub category: ModuleCategory, pub x: f32, pub y: f32, pub render_x: f32, @@ -19,7 +28,9 @@ pub struct WindowState { pub is_dragging: bool, pub drag_offset_x: f32, pub drag_offset_y: f32, - pub modules: Vec, + pub scroll_y: f32, + pub scroll_vel: f32, + pub modules: Vec, } pub struct UiManager { @@ -35,49 +46,94 @@ impl UiManager { is_visible: false, windows: vec![ WindowState { - title: "Combat".to_string(), + title: ModuleCategory::COMBAT.display_name().to_string(), + category: ModuleCategory::COMBAT, x: 50.0, y: 50.0, render_x: 50.0, render_y: 50.0, vel_x: 0.0, vel_y: 0.0, - width: 120.0, + width: 160.0, + height: 200.0, + is_dragging: false, + drag_offset_x: 0.0, + drag_offset_y: 0.0, + scroll_y: 0.0, + scroll_vel: 0.0, + modules: vec![], + }, + WindowState { + title: ModuleCategory::MOVEMENT.display_name().to_string(), + category: ModuleCategory::MOVEMENT, + x: 230.0, + y: 50.0, + render_x: 230.0, + render_y: 50.0, + vel_x: 0.0, + vel_y: 0.0, + width: 160.0, + height: 200.0, + is_dragging: false, + drag_offset_x: 0.0, + drag_offset_y: 0.0, + scroll_y: 0.0, + scroll_vel: 0.0, + modules: vec![], + }, + WindowState { + title: ModuleCategory::RENDER.display_name().to_string(), + category: ModuleCategory::RENDER, + x: 410.0, + y: 50.0, + render_x: 410.0, + render_y: 50.0, + vel_x: 0.0, + vel_y: 0.0, + width: 160.0, height: 200.0, is_dragging: false, drag_offset_x: 0.0, drag_offset_y: 0.0, - modules: vec!["MobAura".to_string(), "Criticals".to_string()], + scroll_y: 0.0, + scroll_vel: 0.0, + modules: vec![], }, WindowState { - title: "Movement".to_string(), - x: 200.0, + title: ModuleCategory::PLAYER.display_name().to_string(), + category: ModuleCategory::PLAYER, + x: 590.0, y: 50.0, - render_x: 200.0, + render_x: 590.0, render_y: 50.0, vel_x: 0.0, vel_y: 0.0, - width: 120.0, + width: 160.0, height: 200.0, is_dragging: false, drag_offset_x: 0.0, drag_offset_y: 0.0, - modules: vec!["Sprint".to_string(), "Fly".to_string()], + scroll_y: 0.0, + scroll_vel: 0.0, + modules: vec![], }, WindowState { - title: "Render".to_string(), - x: 350.0, + title: ModuleCategory::WORLD.display_name().to_string(), + category: ModuleCategory::WORLD, + x: 770.0, y: 50.0, - render_x: 350.0, + render_x: 770.0, render_y: 50.0, vel_x: 0.0, vel_y: 0.0, - width: 120.0, + width: 160.0, height: 200.0, is_dragging: false, drag_offset_x: 0.0, drag_offset_y: 0.0, - modules: vec!["ESP".to_string(), "FullBright".to_string()], + scroll_y: 0.0, + scroll_vel: 0.0, + modules: vec![], }, ], } @@ -104,6 +160,48 @@ impl UiManager { return; } + // Sync real modules from DarkClient if empty + let client_modules_guard = DarkClient::instance().modules.read().unwrap(); + for window in &mut self.windows { + if window.modules.is_empty() { + let mut valid_modules: Vec<_> = client_modules_guard + .values() + .filter(|m| m.lock().unwrap().get_module_data().category == window.category) + .collect(); + + valid_modules.sort_by(|a, b| { + a.lock() + .unwrap() + .get_module_data() + .name + .cmp(&b.lock().unwrap().get_module_data().name) + }); + + window.modules = valid_modules + .into_iter() + .map(|m| ModuleUiState { + name: m.lock().unwrap().get_module_data().name.clone(), + is_expanded: false, + expand_anim: 0.0, + }) + .collect(); + } + + // Animate expansions + for m_state in &mut window.modules { + if m_state.is_expanded { + if m_state.expand_anim < 1.0 { + m_state.expand_anim += 0.1; + } + } else { + if m_state.expand_anim > 0.0 { + m_state.expand_anim -= 0.1; + } + } + m_state.expand_anim = m_state.expand_anim.clamp(0.0, 1.0); + } + } + // Handle Mouse state if let Ok(mouse) = MOUSE_STATE.lock() { let mx = mouse.x as f32; @@ -147,6 +245,33 @@ impl UiManager { } else { window.is_dragging = false; } + + // Dynamic window height expansion + let mut target_h = 20.0 + 5.0; // Title bar + padding + for m_state in &window.modules { + target_h += 16.0; // Base module height + + if m_state.expand_anim > 0.01 { + if let Some(m) = client_modules_guard.get(&m_state.name) { + let lock = m.lock().unwrap(); + let settings_count = lock.get_module_data().settings.len() as f32; + let expanded_h = settings_count * 14.0; + target_h += m_state.expand_anim * expanded_h; + } + } + target_h += 2.0; // Gap between modules + } + target_h += 5.0; // Bottom padding + + // Max limits and scrolling + let max_h = 300.0; // Cap to arbitrary viewport size before scrolling + if target_h > max_h { + target_h = max_h; + // todo scrolling bounds + } + + // Lerp window height + window.height += (target_h - window.height) * 0.25; } } } From 2bae4c95eb3e7ae5f0399d134f74b8cb0162e7e5 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Fri, 20 Feb 2026 15:27:49 +0100 Subject: [PATCH 04/16] Remove warnings --- client/src/graphic/hook.rs | 10 +++++----- client/src/lib.rs | 4 ++-- client/src/module/mod.rs | 1 - client/src/module/movement/fly.rs | 1 - 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/client/src/graphic/hook.rs b/client/src/graphic/hook.rs index 424a32b..43c13e7 100644 --- a/client/src/graphic/hook.rs +++ b/client/src/graphic/hook.rs @@ -147,12 +147,12 @@ fn check_tick() { let minecraft = Minecraft::instance(); - // Error check to avoid crashes - if minecraft.player.entity.is_null() { - return; - } + let player = match minecraft.get_player() { + Ok(p) => p, + Err(_) => return, + }; - let tick_count = match minecraft.player.entity.get_tick_count() { + let tick_count = match player.entity.get_tick_count() { Ok(t) => t, Err(_) => return, }; diff --git a/client/src/lib.rs b/client/src/lib.rs index 9d694f7..a6628a5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -66,7 +66,7 @@ pub extern "C" fn initialize_client() { info!("Starting DarkClient..."); let minecraft = Minecraft::instance(); - register_modules(minecraft); + register_modules(); start_keyboard_handler(); @@ -128,7 +128,7 @@ pub extern "C" fn cleanup_client() { info!("Client cleanup completed"); } -fn register_modules(minecraft: &'static Minecraft) { +fn register_modules() { let client = DarkClient::instance(); client.register_module(FlyModule::new()); diff --git a/client/src/module/mod.rs b/client/src/module/mod.rs index 5a68f43..364c016 100644 --- a/client/src/module/mod.rs +++ b/client/src/module/mod.rs @@ -1,4 +1,3 @@ -use crate::mapping::entity::player::LocalPlayer; use std::fmt::Debug; pub mod combat; diff --git a/client/src/module/movement/fly.rs b/client/src/module/movement/fly.rs index 97f38e5..884282f 100644 --- a/client/src/module/movement/fly.rs +++ b/client/src/module/movement/fly.rs @@ -1,5 +1,4 @@ use crate::mapping::client::minecraft::Minecraft; -use crate::mapping::entity::player::LocalPlayer; use crate::module::{KeyboardKey, Module, ModuleCategory, ModuleData, ModuleSetting}; #[derive(Debug)] From b1f821a2c24b3b85fd2146b5c917d0ad6444e2e1 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Fri, 20 Feb 2026 17:52:49 +0100 Subject: [PATCH 05/16] Merge input handling, check current screen for disabling cheat activation when not playing and clap tabs --- client/src/client.rs | 103 +------------- client/src/graphic/hook.rs | 7 + client/src/graphic/input.rs | 60 ++++++++ client/src/graphic/ui.rs | 183 +++++++++++++++++++------ client/src/graphic/ui_manager.rs | 132 +++++++++++++----- client/src/mapping/class_type.rs | 2 + client/src/mapping/client/minecraft.rs | 38 ++++- 7 files changed, 343 insertions(+), 182 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index 1db6334..d1786c0 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -95,108 +95,9 @@ impl DarkClient { // Module for handling keyboard inputs pub mod keyboard { - use super::*; - use crate::mapping::client::minecraft::Minecraft; - use jni::objects::JValue; - use jni::sys::jlong; - use log::info; - use std::collections::HashSet; - use std::sync::atomic::AtomicBool; - use std::thread; - use std::time::Duration; - - static RUNNING: OnceLock = OnceLock::new(); - pub fn start_keyboard_handler() { - if RUNNING.get().is_none() { - RUNNING.set(AtomicBool::new(true)).unwrap(); - } - thread::spawn(|| { - let minecraft = Minecraft::instance(); - let client = DarkClient::instance(); - let mut env = client.get_env().unwrap(); - - let glfw_window = match minecraft.window.get_window() { - Ok(window) => window, - Err(e) => { - error!("Failed to get GLFW window: {}", e); - return; - } - }; - - let mut keys: HashSet = HashSet::new(); - while RUNNING - .get() - .unwrap() - .load(std::sync::atomic::Ordering::Relaxed) - { - thread::sleep(Duration::from_millis(100)); - - client.modules.read().unwrap().values().for_each(|module| { - let mut module = module.lock().unwrap(); - let module_data = module.get_module_data(); - let key = module_data.key_bind as i32; - - if is_key_down(&mut env, glfw_window, key) { - if !keys.contains(&key) { - keys.insert(key); - - let enabled = !module_data.enabled; - info!( - "{} {}", - module_data.name, - if enabled { "enabled" } else { "disabled" } - ); - if enabled { - match module.on_start() { - Ok(_) => {} - Err(e) => error!( - "Failed to start module {}: {}", - module.get_module_data().name, - e - ), - } - } else { - match module.on_stop() { - Ok(_) => {} - Err(e) => error!( - "Failed to stop module {}: {}", - module.get_module_data().name, - e - ), - } - } - module.get_module_data_mut().set_enabled(enabled); - } - } else { - keys.remove(&key); - } - }); - } - }); + // Keyboard handling is now natively event-driven via GLFW inside graphic::input::my_key_callback } - pub fn stop_keyboard_handler() { - if RUNNING.get().is_none() { - return; - } - RUNNING - .get() - .unwrap() - .store(false, std::sync::atomic::Ordering::Relaxed); - } - - fn is_key_down(env: &mut JNIEnv, glfw_window: jlong, key: i32) -> bool { - let glfw = env.find_class("org/lwjgl/glfw/GLFW").unwrap(); - env.call_static_method( - glfw, - "glfwGetKey", - "(JI)I", - &[JValue::Long(glfw_window), JValue::Int(key)], - ) - .unwrap() - .i() - .unwrap() - == 1 - } + pub fn stop_keyboard_handler() {} } diff --git a/client/src/graphic/hook.rs b/client/src/graphic/hook.rs index 43c13e7..09be4c0 100644 --- a/client/src/graphic/hook.rs +++ b/client/src/graphic/hook.rs @@ -125,6 +125,13 @@ unsafe fn render_overlay() { return; } + if crate::mapping::client::minecraft::Minecraft::instance() + .get_player() + .is_err() + { + return; + } + // Initialize inputs when we are on the valid OpenGL thread context crate::graphic::input::init(); diff --git a/client/src/graphic/input.rs b/client/src/graphic/input.rs index f914d50..5dcd6fa 100644 --- a/client/src/graphic/input.rs +++ b/client/src/graphic/input.rs @@ -158,6 +158,36 @@ mod linux_input { return; } + // --- Module Toggling --- + // Only trigger on action == 1 (GLFW_PRESS) to prevent duplicates or release triggers + if action == 1 { + let minecraft = crate::mapping::client::minecraft::Minecraft::instance(); + if minecraft.current_screen_is_null() && minecraft.get_player().is_ok() { + let client = crate::client::DarkClient::instance(); + if let Ok(modules) = client.modules.read() { + for module in modules.values() { + let mut module = module.lock().unwrap(); + let module_data = module.get_module_data(); + + if module_data.key_bind as i32 == key { + let enabled = !module_data.enabled; + log::info!( + "{} {}", + module_data.name, + if enabled { "enabled" } else { "disabled" } + ); + if enabled { + let _ = module.on_start(); + } else { + let _ = module.on_stop(); + } + module.get_module_data_mut().set_enabled(enabled); + } + } + } + } + } + unsafe { if !ORIGINAL_KEY_CB.is_null() { let orig: GlfwKeyFun = std::mem::transmute(ORIGINAL_KEY_CB); @@ -393,6 +423,36 @@ mod windows_input { if GUI_OPEN.load(Ordering::Relaxed) { return; } + + // --- Module Toggling --- + if action == 1 { + let minecraft = crate::mapping::client::minecraft::Minecraft::instance(); + if minecraft.current_screen_is_null() && minecraft.get_player().is_ok() { + let client = crate::client::DarkClient::instance(); + if let Ok(modules) = client.modules.read() { + for module in modules.values() { + let mut module = module.lock().unwrap(); + let module_data = module.get_module_data(); + + if module_data.key_bind as i32 == key { + let enabled = !module_data.enabled; + log::info!( + "{} {}", + module_data.name, + if enabled { "enabled" } else { "disabled" } + ); + if enabled { + let _ = module.on_start(); + } else { + let _ = module.on_stop(); + } + module.get_module_data_mut().set_enabled(enabled); + } + } + } + } + } + unsafe { if !ORIGINAL_KEY_CB.is_null() { let orig: GlfwKeyFun = std::mem::transmute(ORIGINAL_KEY_CB); diff --git a/client/src/graphic/ui.rs b/client/src/graphic/ui.rs index bdce53e..0cef3e2 100644 --- a/client/src/graphic/ui.rs +++ b/client/src/graphic/ui.rs @@ -4,6 +4,23 @@ use crate::graphic::input::MOUSE_STATE; use crate::graphic::render::Renderer; use crate::graphic::ui_manager::UI_MANAGER; +// --- GUI CONFIGURATION CONSTANTS --- +pub const GUI_TITLE_HEIGHT: f32 = 20.0; +pub const GUI_MODULE_HEIGHT: f32 = 16.0; +pub const GUI_SETTING_HEIGHT: f32 = 14.0; +pub const GUI_PADDING: f32 = 5.0; +pub const GUI_MAX_STRETCH: f32 = 30.0; + +// Text Scales +pub const TEXT_SCALE_HUD_WATERMARK: f32 = 2.0; +pub const TEXT_SCALE_HUD_MODULES: f32 = 1.4; + +// Layout spacing +pub const HUD_PADDING_Y: f32 = 5.0; +pub const HUD_PADDING_X: f32 = 5.0; +pub const HUD_WATERMARK_MARGIN_BOTTOM: f32 = 24.0; +pub const HUD_MODULES_SPACING: f32 = 16.0; + /// Defines the colors and sizes for the custom DarkClient GUI. pub struct Theme { pub screen_bg: (f32, f32, f32, f32), @@ -65,9 +82,9 @@ unsafe fn draw_hud(renderer: &mut Renderer, scale_f: f32) { let watermark = "DarkClient"; let w_color = HudColor::Yellow.to_rgba(); - let mut hud_y = 5.0 * scale_f; - let hud_x = 5.0 * scale_f; - let text_scale = (1.2 * scale_f) as i32; + let mut hud_y = HUD_PADDING_Y * scale_f; + let hud_x = HUD_PADDING_X * scale_f; + let text_scale = (TEXT_SCALE_HUD_WATERMARK * scale_f) as i32; draw_text( renderer, @@ -81,7 +98,7 @@ unsafe fn draw_hud(renderer: &mut Renderer, scale_f: f32) { text_scale, ); - hud_y += 18.0 * scale_f; + hud_y += HUD_WATERMARK_MARGIN_BOTTOM * scale_f; if let Ok(modules_map) = DarkClient::instance().modules.read() { let mut active_mods: Vec = modules_map @@ -120,9 +137,9 @@ unsafe fn draw_hud(renderer: &mut Renderer, scale_f: f32) { color.1, color.2, color.3, - scale_f as i32, + (TEXT_SCALE_HUD_MODULES * scale_f) as i32, ); - hud_y += 14.0 * scale_f; + hud_y += HUD_MODULES_SPACING * scale_f; } } } @@ -140,7 +157,7 @@ pub fn render_gui(renderer: &mut Renderer) { let base_scale = (screen_h as f32 / 720.0).max(1.0).floor() as i32; let scale_f = base_scale as f32; - ui.update(scale_f); + ui.update(scale_f, screen_w as f32, screen_h as f32); unsafe { draw_hud(renderer, scale_f); @@ -161,36 +178,49 @@ pub fn render_gui(renderer: &mut Renderer) { theme.screen_bg.2, alpha, ); - renderer.draw_rect(0, 0, screen_w, screen_h); + renderer.draw_rect(0, 0, screen_w as i32, screen_h as i32); // Retrieve mouse state for bounds interactions - let (mx, my, mut left_clicked, mut right_clicked) = { - if let Ok(mut mouse) = MOUSE_STATE.lock() { - let l = mouse.left_clicked; - let r = mouse.right_clicked; - mouse.left_clicked = false; - mouse.right_clicked = false; - (mouse.x as f32, mouse.y as f32, l, r) - } else { - (0.0, 0.0, false, false) - } + let (mx, my, left_clicked, right_clicked) = { + let m = MOUSE_STATE.lock().unwrap(); + (m.x as f32, m.y as f32, m.left_clicked, m.right_clicked) }; - // Render each window - for window in &mut ui.windows { + let windows_len = ui.windows.len(); + for window_index in 0..windows_len { // Rigid Title Position - let wx = (window.x * scale_f) as f32; - let wy = (window.y * scale_f) as f32; - let ww = (window.width * scale_f) as f32; - let wh = (window.height * scale_f) as f32; - let title_h = 20.0 * scale_f; + let (wx, wy, ww, wh) = { + let window = &mut ui.windows[window_index]; + + let wx = (window.x * scale_f) as f32; + let wy = (window.y * scale_f) as f32; + let ww = (window.width * scale_f) as f32; + let wh = (window.height * scale_f) as f32; + (wx, wy, ww, wh) + }; + let title_h = GUI_TITLE_HEIGHT * scale_f; + + let mut is_topmost = true; + for i in (window_index + 1)..windows_len { + let higher_w = &ui.windows[i]; + let hwx = higher_w.render_x * scale_f; + let hwy = higher_w.render_y * scale_f; + let hww = higher_w.width * scale_f; + let hwh = higher_w.height * scale_f; + if mx >= hwx && mx <= hwx + hww && my >= hwy && my <= hwy + hwh { + is_topmost = false; + break; + } + } + + let window = &mut ui.windows[window_index]; // Veil trailing offset let mut dx = (window.render_x - window.x) * scale_f; let mut dy = (window.render_y - window.y) * scale_f; // Cap the stretch visual effect - let max_stretch = 30.0 * scale_f; + let max_stretch = GUI_MAX_STRETCH * scale_f; dx = dx.clamp(-max_stretch, max_stretch); dy = dy.clamp(-max_stretch, max_stretch); @@ -265,23 +295,29 @@ pub fn render_gui(renderer: &mut Renderer) { renderer.draw_quad(tl_x, tl_y, tr_x, tr_y, bl_x, bl_y, br_x, br_y); // Draw Modules inside Veil - let mut mod_y = body_top_y + (5.0 * scale_f); + let mut mod_y = body_top_y + (GUI_PADDING * scale_f); for module_state in &mut window.modules { let module_name = &module_state.name; - let mod_act_h = 16.0 * scale_f; // base height - let mod_w = ww - (10.0 * scale_f); - let mod_x = wx + (5.0 * scale_f); + let mod_act_h = GUI_MODULE_HEIGHT * scale_f; // base height + let mod_w = ww - (GUI_PADDING * 2.0 * scale_f); + let mod_x = wx + (GUI_PADDING * scale_f); let box_side = mod_act_h; let box_x = mod_x + mod_w - box_side; let box_y = mod_y; // Check interactions - let is_hovering = - mx >= mod_x && mx <= mod_x + mod_w && my >= mod_y && my <= mod_y + mod_act_h; - - let is_box_hovering = - mx >= box_x && mx <= box_x + box_side && my >= box_y && my <= box_y + box_side; + let is_hovering = is_topmost + && mx >= mod_x + && mx <= mod_x + mod_w + && my >= mod_y + && my <= mod_y + mod_act_h; + + let is_box_hovering = is_topmost + && mx >= box_x + && mx <= box_x + box_side + && my >= box_y + && my <= box_y + box_side; let mut is_enabled = false; if let Some(m) = DarkClient::instance() @@ -295,8 +331,6 @@ pub fn render_gui(renderer: &mut Renderer) { if is_hovering { if left_clicked { - left_clicked = false; - if is_box_hovering { module_state.is_expanded = !module_state.is_expanded; } else { @@ -310,7 +344,6 @@ pub fn render_gui(renderer: &mut Renderer) { } } if right_clicked { - right_clicked = false; module_state.is_expanded = !module_state.is_expanded; } } @@ -460,7 +493,8 @@ pub fn render_gui(renderer: &mut Renderer) { let actual_sx = set_x + t_dx; let actual_sy = set_y + t_dy; - let is_set_hovering = mx >= actual_sx + let is_set_hovering = is_topmost + && mx >= actual_sx && mx <= actual_sx + set_w && my >= actual_sy && my <= actual_sy + set_h; @@ -471,7 +505,6 @@ pub fn render_gui(renderer: &mut Renderer) { crate::module::ModuleSetting::Toggle { name, value } => { if is_set_hovering && left_clicked { *value = !*value; - left_clicked = false; } let t_color = if *value { @@ -557,7 +590,6 @@ pub fn render_gui(renderer: &mut Renderer) { } => { if is_set_hovering && left_clicked { *value = (*value + 1) % options.len(); - left_clicked = false; } let current_opt = if *value < options.len() { &options[*value] @@ -600,5 +632,74 @@ pub fn render_gui(renderer: &mut Renderer) { mod_y += mod_total_visual_h + (2.0 * scale_f); } } + + // DRAW CONTEXT BUTTONS (Top Right) + let btn_w = 60.0 * scale_f; + let btn_h = 20.0 * scale_f; + let btn_pad = 10.0 * scale_f; + + let reset_x = screen_w as f32 - btn_w - btn_pad; + let reset_y = btn_pad; + let panic_x = reset_x - btn_w - btn_pad; + let panic_y = btn_pad; + + // Reset UI Button + renderer.set_color( + theme.module_bg.0, + theme.module_bg.1, + theme.module_bg.2, + ui.background_alpha.min(0.8), + ); + renderer.draw_rect(reset_x as i32, reset_y as i32, btn_w as i32, btn_h as i32); + draw_text( + renderer, + "Reset UI", + (reset_x + 5.0 * scale_f) as i32, + (reset_y + 3.0 * scale_f) as i32, + theme.text_primary.0, + theme.text_primary.1, + theme.text_primary.2, + theme.text_primary.3, + base_scale, + ); + + // Panic Button + renderer.set_color(0.8, 0.2, 0.2, ui.background_alpha.min(0.8)); + renderer.draw_rect(panic_x as i32, panic_y as i32, btn_w as i32, btn_h as i32); + draw_text( + renderer, + "PANIC", + (panic_x + 12.0 * scale_f) as i32, + (panic_y + 3.0 * scale_f) as i32, + 1.0, + 1.0, + 1.0, + 1.0, + base_scale, + ); + + // Check clicks on buttons + let is_reset_hovering = + mx >= reset_x && mx <= reset_x + btn_w && my >= reset_y && my <= reset_y + btn_h; + let is_panic_hovering = + mx >= panic_x && mx <= panic_x + btn_w && my >= panic_y && my <= panic_y + btn_h; + + if left_clicked { + if is_reset_hovering { + ui.reset_ui(screen_w as f32, screen_h as f32); + } else if is_panic_hovering { + std::thread::spawn(|| crate::gui::call_panic()); + } + } + } + + // Consume clicks globally at end of frame + if let Ok(mut mouse) = MOUSE_STATE.lock() { + if mouse.left_clicked { + mouse.left_clicked = false; + } + if mouse.right_clicked { + mouse.right_clicked = false; + } } } diff --git a/client/src/graphic/ui_manager.rs b/client/src/graphic/ui_manager.rs index 9badfb2..5c2057b 100644 --- a/client/src/graphic/ui_manager.rs +++ b/client/src/graphic/ui_manager.rs @@ -1,10 +1,10 @@ use crate::client::DarkClient; use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; +use crate::graphic::ui::{GUI_MODULE_HEIGHT, GUI_PADDING, GUI_SETTING_HEIGHT, GUI_TITLE_HEIGHT}; use crate::module::ModuleCategory; -use lazy_static::lazy_static; use std::sync::Mutex; -lazy_static! { +lazy_static::lazy_static! { pub static ref UI_MANAGER: Mutex = Mutex::new(UiManager::new()); } @@ -37,13 +37,15 @@ pub struct UiManager { pub windows: Vec, pub background_alpha: f32, pub is_visible: bool, + pub initialized_layout: bool, } impl UiManager { pub fn new() -> Self { - Self { + let manager = Self { background_alpha: 0.0, is_visible: false, + initialized_layout: false, windows: vec![ WindowState { title: ModuleCategory::COMBAT.display_name().to_string(), @@ -136,10 +138,50 @@ impl UiManager { modules: vec![], }, ], + }; + manager + } + + pub fn reset_ui(&mut self, screen_w: f32, screen_h: f32) { + self.auto_wrap_windows(screen_w, screen_h); + } + + pub fn auto_wrap_windows(&mut self, screen_w: f32, _screen_h: f32) { + let start_x = 50.0; + let start_y = 50.0; + let gap_x = 20.0; + let gap_y = 20.0; + + let mut current_x = start_x; + let mut current_y = start_y; + let mut row_max_height = 0.0_f32; + + for window in &mut self.windows { + if current_x + window.width > screen_w && current_x > start_x { + // Wrap to next line + current_x = start_x; + current_y += row_max_height + gap_y; + row_max_height = 0.0; + } + + window.x = current_x; + window.y = current_y; + window.render_x = current_x; + window.render_y = current_y; + + current_x += window.width + gap_x; + if window.height > row_max_height { + row_max_height = window.height; + } } } - pub fn update(&mut self, scale_f: f32) { + pub fn update(&mut self, scale_f: f32, screen_w: f32, screen_h: f32) { + if !self.initialized_layout { + self.auto_wrap_windows(screen_w, screen_h); + self.initialized_layout = true; + } + // Handle visibility and animations let target_visible = GUI_OPEN.load(std::sync::atomic::Ordering::Relaxed); @@ -207,7 +249,56 @@ impl UiManager { let mx = mouse.x as f32; let my = mouse.y as f32; + // --- Click & Bring to Front logic --- + if mouse.left_clicked { + let mut clicked_idx = None; + for (i, window) in self.windows.iter_mut().enumerate().rev() { + let scaled_x = window.render_x * scale_f; + let scaled_y = window.render_y * scale_f; + let scaled_w = window.width * scale_f; + let scaled_h = window.height * scale_f; + + // Did we click inside the full window bounds? + if mx >= scaled_x + && mx <= scaled_x + scaled_w + && my >= scaled_y + && my <= scaled_y + scaled_h + { + clicked_idx = Some(i); + break; + } + } + + if let Some(idx) = clicked_idx { + let mut win = self.windows.remove(idx); + let scaled_x = win.render_x * scale_f; + let scaled_y = win.render_y * scale_f; + let title_height = GUI_TITLE_HEIGHT * scale_f; + + // Check if clicked exactly on the title bar for dragging + if my <= scaled_y + title_height { + win.is_dragging = true; + win.drag_offset_x = (mx - scaled_x) / scale_f; + win.drag_offset_y = (my - scaled_y) / scale_f; + } + self.windows.push(win); + } + } + for window in &mut self.windows { + if !mouse.left_down { + window.is_dragging = false; + } + + if window.is_dragging { + window.x = (mx / scale_f) - window.drag_offset_x; + window.y = (my / scale_f) - window.drag_offset_y; + + // Clamp dragging within screen bounds + window.x = window.x.clamp(0.0, (screen_w / scale_f) - window.width); + window.y = window.y.clamp(0.0, (screen_h / scale_f) - GUI_TITLE_HEIGHT); + } + // Spring Physics let stiffness = 0.25; // How strongly it pulls towards target let damping = 0.65; // Defines jelly bounce vs snap (lower = more bouncy) @@ -221,47 +312,22 @@ impl UiManager { window.render_x += window.vel_x; window.render_y += window.vel_y; - let scaled_x = window.render_x * scale_f; - let scaled_y = window.render_y * scale_f; - let scaled_w = window.width * scale_f; - let title_height = 20.0 * scale_f; - - let is_hovering_title = mx >= scaled_x - && mx <= scaled_x + scaled_w - && my >= scaled_y - && my <= scaled_y + title_height; - - if mouse.left_down { - if is_hovering_title && !window.is_dragging { - window.is_dragging = true; - window.drag_offset_x = (mx - scaled_x) / scale_f; - window.drag_offset_y = (my - scaled_y) / scale_f; - } - - if window.is_dragging { - window.x = (mx / scale_f) - window.drag_offset_x; - window.y = (my / scale_f) - window.drag_offset_y; - } - } else { - window.is_dragging = false; - } - // Dynamic window height expansion - let mut target_h = 20.0 + 5.0; // Title bar + padding + let mut target_h = GUI_TITLE_HEIGHT + GUI_PADDING; // Title bar + padding for m_state in &window.modules { - target_h += 16.0; // Base module height + target_h += GUI_MODULE_HEIGHT; // Base module height if m_state.expand_anim > 0.01 { if let Some(m) = client_modules_guard.get(&m_state.name) { let lock = m.lock().unwrap(); let settings_count = lock.get_module_data().settings.len() as f32; - let expanded_h = settings_count * 14.0; + let expanded_h = settings_count * GUI_SETTING_HEIGHT; target_h += m_state.expand_anim * expanded_h; } } target_h += 2.0; // Gap between modules } - target_h += 5.0; // Bottom padding + target_h += GUI_PADDING; // Bottom padding // Max limits and scrolling let max_h = 300.0; // Cap to arbitrary viewport size before scrolling diff --git a/client/src/mapping/class_type.rs b/client/src/mapping/class_type.rs index 36d5a05..01f84ab 100644 --- a/client/src/mapping/class_type.rs +++ b/client/src/mapping/class_type.rs @@ -15,6 +15,7 @@ pub enum MinecraftClassType { Iterable, Iterator, Mob, + Screen, } impl MinecraftClassType { @@ -34,6 +35,7 @@ impl MinecraftClassType { MinecraftClassType::Iterable => "java/lang/Iterable", MinecraftClassType::Iterator => "java/util/Iterator", MinecraftClassType::Mob => "net/minecraft/world/entity/Mob", + MinecraftClassType::Screen => "net/minecraft/client/gui/screens/Screen", } } } diff --git a/client/src/mapping/client/minecraft.rs b/client/src/mapping/client/minecraft.rs index af555e3..2f71ac3 100644 --- a/client/src/mapping/client/minecraft.rs +++ b/client/src/mapping/client/minecraft.rs @@ -1,8 +1,8 @@ use crate::mapping::client::gamemode::MultiPlayerGameMode; use crate::mapping::client::window::Window; use crate::mapping::client::world::World; -use crate::mapping::entity::Entity; use crate::mapping::entity::player::{Abilities, LocalPlayer}; +use crate::mapping::entity::Entity; use crate::mapping::{FieldType, GameContext, Mapping, MinecraftClassType}; use jni::objects::GlobalRef; use log::error; @@ -77,7 +77,8 @@ impl Minecraft { } pub fn get_player(&self) -> anyhow::Result { - let player_obj = self.mapping + let player_obj = self + .mapping .get_field( MinecraftClassType::Minecraft, self.jni_ref.as_obj(), @@ -91,23 +92,46 @@ impl Minecraft { } { - let read_guard = self.player.read().map_err(|_| anyhow::anyhow!("Lock poisoned"))?; - if self.mapping.get_env()?.is_same_object(&read_guard.jni_ref, &player_obj)? { + let read_guard = self + .player + .read() + .map_err(|_| anyhow::anyhow!("Lock poisoned"))?; + if self + .mapping + .get_env()? + .is_same_object(&read_guard.jni_ref, &player_obj)? + { return Ok(read_guard.clone()); } } - - let mut write_guard = self.player.write().map_err(|_| anyhow::anyhow!("Lock poisoned"))?; + + let mut write_guard = self + .player + .write() + .map_err(|_| anyhow::anyhow!("Lock poisoned"))?; let jni_ref = self.mapping.new_global_ref(player_obj)?; *write_guard = LocalPlayer { jni_ref: jni_ref.clone(), abilities: Abilities::new(jni_ref.clone(), &self.mapping)?, - entity: Entity::new(jni_ref) + entity: Entity::new(jni_ref), }; Ok(write_guard.clone()) } + pub fn current_screen_is_null(&self) -> bool { + if let Ok(screen_obj) = self.mapping.get_field( + MinecraftClassType::Minecraft, + self.jni_ref.as_obj(), + "screen", + FieldType::Object(MinecraftClassType::Screen, &self.mapping), + ) { + if let Ok(l) = screen_obj.l() { + return l.is_null(); + } + } + true + } } impl Deref for Minecraft { From 327120abaeeea41f2f423fd6b07fd5ba9a6520ed Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Sat, 21 Feb 2026 00:12:47 +0100 Subject: [PATCH 06/16] Build ui with widget --- client/src/graphic/color.rs | 106 ++++ client/src/graphic/font.rs | 9 +- client/src/graphic/mod.rs | 2 + client/src/graphic/render.rs | 29 +- client/src/graphic/ui.rs | 643 ++---------------------- client/src/graphic/ui_manager.rs | 439 +++++++--------- client/src/graphic/widget/button.rs | 111 ++++ client/src/graphic/widget/hud.rs | 83 +++ client/src/graphic/widget/label.rs | 60 +++ client/src/graphic/widget/mod.rs | 76 +++ client/src/graphic/widget/module_btn.rs | 267 ++++++++++ client/src/graphic/widget/panel.rs | 128 +++++ client/src/graphic/widget/slider.rs | 144 ++++++ client/src/graphic/widget/toggle.rs | 136 +++++ client/src/graphic/widget/window.rs | 227 +++++++++ 15 files changed, 1580 insertions(+), 880 deletions(-) create mode 100644 client/src/graphic/color.rs create mode 100644 client/src/graphic/widget/button.rs create mode 100644 client/src/graphic/widget/hud.rs create mode 100644 client/src/graphic/widget/label.rs create mode 100644 client/src/graphic/widget/mod.rs create mode 100644 client/src/graphic/widget/module_btn.rs create mode 100644 client/src/graphic/widget/panel.rs create mode 100644 client/src/graphic/widget/slider.rs create mode 100644 client/src/graphic/widget/toggle.rs create mode 100644 client/src/graphic/widget/window.rs diff --git a/client/src/graphic/color.rs b/client/src/graphic/color.rs new file mode 100644 index 0000000..9c6a25e --- /dev/null +++ b/client/src/graphic/color.rs @@ -0,0 +1,106 @@ +use std::ops::{Deref, DerefMut}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Color { + Yellow, + Green, + Red, + Blue, + White, + Purple, + Cyan, + Black, +} + +impl Color { + pub fn to_rgb(&self) -> Rgb { + match self { + Color::Yellow => Rgb::new(1.0, 1.0, 0.0), + Color::Green => Rgb::new(0.0, 1.0, 0.0), + Color::Red => Rgb::new(1.0, 0.0, 0.0), + Color::Blue => Rgb::new(0.0, 0.0, 1.0), + Color::White => Rgb::new(1.0, 1.0, 1.0), + Color::Purple => Rgb::new(1.0, 0.0, 1.0), + Color::Cyan => Rgb::new(0.0, 1.0, 1.0), + Color::Black => Rgb::new(0.0, 0.0, 0.0), + } + } + + pub fn to_rgba(&self, a: f32) -> Rgba { + Rgba::new(self.to_rgb(), a) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rgb { + pub r: f32, + pub g: f32, + pub b: f32, +} + +impl Rgb { + pub fn new(r: f32, g: f32, b: f32) -> Self { + Self { r, g, b } + } + + pub fn with_alpha(self, a: f32) -> Rgba { + Rgba::new(self, a) + } +} + +impl From<(f32, f32, f32)> for Rgb { + fn from(value: (f32, f32, f32)) -> Self { + Self::new(value.0, value.1, value.2) + } +} + +impl From for (f32, f32, f32) { + fn from(value: Rgb) -> Self { + (value.r, value.g, value.b) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rgba { + pub rgb: Rgb, + pub a: f32, +} + +impl Rgba { + pub fn new(rgb: Rgb, a: f32) -> Self { + Self { rgb, a } + } + + pub fn new_rgb(r: f32, g: f32, b: f32, a: f32) -> Self { + Self { + rgb: Rgb { r, g, b }, + a, + } + } +} + +impl From<(f32, f32, f32, f32)> for Rgba { + fn from(value: (f32, f32, f32, f32)) -> Self { + Self::new_rgb(value.0, value.1, value.2, value.3) + } +} + +impl From for (f32, f32, f32, f32) { + fn from(value: Rgba) -> Self { + (value.r, value.g, value.b, value.a) + } +} + +impl Deref for Rgba { + type Target = Rgb; + + fn deref(&self) -> &Self::Target { + &self.rgb + } +} + +impl DerefMut for Rgba { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.rgb + } +} diff --git a/client/src/graphic/font.rs b/client/src/graphic/font.rs index 2107f27..439c5cf 100644 --- a/client/src/graphic/font.rs +++ b/client/src/graphic/font.rs @@ -1,4 +1,4 @@ -use crate::graphic::render::Renderer; +use crate::graphic::{color::Rgba, render::Renderer}; /// A simple 5x7 bitmap font for ASCII characters 32 to 127. /// Each byte represents a column of the character (5 columns per character). @@ -108,13 +108,10 @@ pub unsafe fn draw_text( text: &str, start_x: i32, start_y: i32, - r: f32, - g: f32, - b: f32, - a: f32, + rgba: Rgba, scale: i32, ) { - renderer.set_color(r, g, b, a); + renderer.set_color(rgba); let char_width = 5 * scale; let char_space = scale; diff --git a/client/src/graphic/mod.rs b/client/src/graphic/mod.rs index 87739bb..f1999e1 100644 --- a/client/src/graphic/mod.rs +++ b/client/src/graphic/mod.rs @@ -1,6 +1,8 @@ +pub mod color; pub mod font; pub mod hook; pub mod input; pub mod render; pub mod ui; pub mod ui_manager; +pub mod widget; diff --git a/client/src/graphic/render.rs b/client/src/graphic/render.rs index 585f19b..634b74f 100644 --- a/client/src/graphic/render.rs +++ b/client/src/graphic/render.rs @@ -1,4 +1,7 @@ -use crate::gl; +use crate::{ + gl, + graphic::color::{Rgb, Rgba}, +}; use std::ffi::CString; static mut SHADER_PROGRAM: u32 = 0; @@ -114,7 +117,7 @@ pub struct Renderer { old_depth_test: bool, old_cull_face: bool, old_scissor_test: bool, - current_color: (f32, f32, f32, f32), + current_color: Rgba, } impl Renderer { @@ -175,12 +178,12 @@ impl Renderer { old_depth_test, old_cull_face, old_scissor_test, - current_color: (1.0, 1.0, 1.0, 1.0), + current_color: Rgba::new(Rgb::new(1.0, 1.0, 1.0), 1.0), } } - pub unsafe fn set_color(&mut self, r: f32, g: f32, b: f32, a: f32) { - self.current_color = (r, g, b, a); + pub unsafe fn set_color(&mut self, rgba: Rgba) { + self.current_color = rgba; } pub unsafe fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32) { @@ -213,10 +216,10 @@ impl Renderer { if color_loc >= 0 { gl::Uniform4f( color_loc, - self.current_color.0, - self.current_color.1, - self.current_color.2, - self.current_color.3, + self.current_color.r, + self.current_color.g, + self.current_color.b, + self.current_color.a, ); } @@ -281,10 +284,10 @@ impl Renderer { if color_loc >= 0 { gl::Uniform4f( color_loc, - self.current_color.0, - self.current_color.1, - self.current_color.2, - self.current_color.3, + self.current_color.r, + self.current_color.g, + self.current_color.b, + self.current_color.a, ); } diff --git a/client/src/graphic/ui.rs b/client/src/graphic/ui.rs index 0cef3e2..b2b319e 100644 --- a/client/src/graphic/ui.rs +++ b/client/src/graphic/ui.rs @@ -1,5 +1,4 @@ -use crate::client::DarkClient; -use crate::graphic::font::{draw_text, get_text_width}; +use crate::graphic::color::{Rgb, Rgba}; use crate::graphic::input::MOUSE_STATE; use crate::graphic::render::Renderer; use crate::graphic::ui_manager::UI_MANAGER; @@ -23,33 +22,33 @@ pub const HUD_MODULES_SPACING: f32 = 16.0; /// Defines the colors and sizes for the custom DarkClient GUI. pub struct Theme { - pub screen_bg: (f32, f32, f32, f32), - pub window_bg: (f32, f32, f32, f32), - pub title_bg: (f32, f32, f32, f32), - pub border: (f32, f32, f32, f32), - pub text_primary: (f32, f32, f32, f32), - pub text_accent: (f32, f32, f32, f32), - pub module_bg: (f32, f32, f32, f32), - pub module_bg_hover: (f32, f32, f32, f32), + pub screen_bg: Rgba, + pub window_bg: Rgba, + pub title_bg: Rgba, + pub border: Rgba, + pub text_primary: Rgba, + pub text_accent: Rgba, + pub module_bg: Rgba, + pub module_bg_hover: Rgba, } impl Default for Theme { fn default() -> Self { Self { // Full screen dim overlay - screen_bg: (0.0, 0.0, 0.0, 0.5), + screen_bg: Rgba::new(Rgb::new(0.0, 0.0, 0.0), 0.5), // Window background - window_bg: (0.1, 0.1, 0.1, 0.95), + window_bg: Rgba::new(Rgb::new(0.1, 0.1, 0.1), 0.95), // Title bar - title_bg: (0.05, 0.05, 0.05, 1.0), + title_bg: Rgba::new(Rgb::new(0.05, 0.05, 0.05), 1.0), // Accent border - border: (0.5, 0.0, 1.0, 1.0), + border: Rgba::new(Rgb::new(0.5, 0.0, 1.0), 1.0), // Texts - text_primary: (1.0, 1.0, 1.0, 1.0), - text_accent: (1.0, 1.0, 0.0, 1.0), + text_primary: Rgba::new(Rgb::new(1.0, 1.0, 1.0), 1.0), + text_accent: Rgba::new(Rgb::new(1.0, 1.0, 0.0), 1.0), // Module rects - module_bg: (0.15, 0.15, 0.15, 1.0), - module_bg_hover: (0.25, 0.25, 0.25, 1.0), + module_bg: Rgba::new(Rgb::new(0.15, 0.15, 0.15), 1.0), + module_bg_hover: Rgba::new(Rgb::new(0.25, 0.25, 0.25), 1.0), } } } @@ -65,81 +64,15 @@ pub enum HudColor { } impl HudColor { - pub fn to_rgba(&self) -> (f32, f32, f32, f32) { + pub fn to_rgba(&self) -> Rgba { match self { - HudColor::Yellow => (1.0, 1.0, 0.0, 1.0), - HudColor::Green => (0.2, 0.8, 0.2, 1.0), - HudColor::Red => (0.9, 0.2, 0.2, 1.0), - HudColor::Blue => (0.2, 0.4, 1.0, 1.0), - HudColor::White => (1.0, 1.0, 1.0, 1.0), - HudColor::Purple => (0.6, 0.2, 0.8, 1.0), - HudColor::Cyan => (0.2, 0.8, 0.9, 1.0), - } - } -} - -unsafe fn draw_hud(renderer: &mut Renderer, scale_f: f32) { - let watermark = "DarkClient"; - let w_color = HudColor::Yellow.to_rgba(); - - let mut hud_y = HUD_PADDING_Y * scale_f; - let hud_x = HUD_PADDING_X * scale_f; - let text_scale = (TEXT_SCALE_HUD_WATERMARK * scale_f) as i32; - - draw_text( - renderer, - watermark, - hud_x as i32, - hud_y as i32, - w_color.0, - w_color.1, - w_color.2, - w_color.3, - text_scale, - ); - - hud_y += HUD_WATERMARK_MARGIN_BOTTOM * scale_f; - - if let Ok(modules_map) = DarkClient::instance().modules.read() { - let mut active_mods: Vec = modules_map - .values() - .filter_map(|m| { - let lock = m.lock().unwrap(); - if lock.get_module_data().enabled { - Some(lock.get_module_data().name.clone()) - } else { - None - } - }) - .collect(); - - // Sort by length (longest first) - active_mods.sort_by(|a, b| b.len().cmp(&a.len())); - - let arraylist_colors = [ - HudColor::Purple, - HudColor::Cyan, - HudColor::Green, - HudColor::Red, - HudColor::Yellow, - HudColor::Blue, - HudColor::White, - ]; - - for (i, mod_name) in active_mods.iter().enumerate() { - let color = arraylist_colors[i % arraylist_colors.len()].to_rgba(); - draw_text( - renderer, - mod_name, - hud_x as i32, - hud_y as i32, - color.0, - color.1, - color.2, - color.3, - (TEXT_SCALE_HUD_MODULES * scale_f) as i32, - ); - hud_y += HUD_MODULES_SPACING * scale_f; + HudColor::Yellow => Rgba::new(Rgb::new(1.0, 1.0, 0.0), 1.0), + HudColor::Green => Rgba::new(Rgb::new(0.2, 0.8, 0.2), 1.0), + HudColor::Red => Rgba::new(Rgb::new(0.9, 0.2, 0.2), 1.0), + HudColor::Blue => Rgba::new(Rgb::new(0.2, 0.4, 1.0), 1.0), + HudColor::White => Rgba::new(Rgb::new(1.0, 1.0, 1.0), 1.0), + HudColor::Purple => Rgba::new(Rgb::new(0.6, 0.2, 0.8), 1.0), + HudColor::Cyan => Rgba::new(Rgb::new(0.2, 0.8, 0.9), 1.0), } } } @@ -159,9 +92,7 @@ pub fn render_gui(renderer: &mut Renderer) { ui.update(scale_f, screen_w as f32, screen_h as f32); - unsafe { - draw_hud(renderer, scale_f); - } + ui.hud_overlay.draw(renderer, &Theme::default(), scale_f); if !ui.is_visible { return; @@ -172,525 +103,17 @@ pub fn render_gui(renderer: &mut Renderer) { unsafe { // 1. Draw Full Screen Transparent Overlay - renderer.set_color( - theme.screen_bg.0, - theme.screen_bg.1, - theme.screen_bg.2, - alpha, - ); + renderer.set_color(theme.screen_bg.with_alpha(alpha)); renderer.draw_rect(0, 0, screen_w as i32, screen_h as i32); - // Retrieve mouse state for bounds interactions - let (mx, my, left_clicked, right_clicked) = { - let m = MOUSE_STATE.lock().unwrap(); - (m.x as f32, m.y as f32, m.left_clicked, m.right_clicked) - }; - - let windows_len = ui.windows.len(); - for window_index in 0..windows_len { - // Rigid Title Position - let (wx, wy, ww, wh) = { - let window = &mut ui.windows[window_index]; - - let wx = (window.x * scale_f) as f32; - let wy = (window.y * scale_f) as f32; - let ww = (window.width * scale_f) as f32; - let wh = (window.height * scale_f) as f32; - (wx, wy, ww, wh) - }; - let title_h = GUI_TITLE_HEIGHT * scale_f; - - let mut is_topmost = true; - for i in (window_index + 1)..windows_len { - let higher_w = &ui.windows[i]; - let hwx = higher_w.render_x * scale_f; - let hwy = higher_w.render_y * scale_f; - let hww = higher_w.width * scale_f; - let hwh = higher_w.height * scale_f; - if mx >= hwx && mx <= hwx + hww && my >= hwy && my <= hwy + hwh { - is_topmost = false; - break; - } - } - - let window = &mut ui.windows[window_index]; - - // Veil trailing offset - let mut dx = (window.render_x - window.x) * scale_f; - let mut dy = (window.render_y - window.y) * scale_f; - - // Cap the stretch visual effect - let max_stretch = GUI_MAX_STRETCH * scale_f; - dx = dx.clamp(-max_stretch, max_stretch); - dy = dy.clamp(-max_stretch, max_stretch); - - // --- Draw Title Bar --- - renderer.set_color(theme.border.0, theme.border.1, theme.border.2, alpha); - renderer.draw_rect( - wx as i32 - 1, - wy as i32 - 1, - ww as i32 + 2, - title_h as i32 + 2, - ); - - renderer.set_color( - theme.title_bg.0, - theme.title_bg.1, - theme.title_bg.2, - theme.title_bg.3 * alpha, - ); - renderer.draw_rect(wx as i32, wy as i32, ww as i32, title_h as i32); - - let text_scale = base_scale; - let t_width = get_text_width(&window.title, text_scale); - let text_x = wx as i32 + (ww as i32 - t_width) / 2; - let text_y = wy as i32 + (title_h as i32 - (7 * text_scale)) / 2; - - draw_text( - renderer, - &window.title, - text_x, - text_y, - theme.text_accent.0, - theme.text_accent.1, - theme.text_accent.2, - alpha, - text_scale, - ); - - // --- Draw Body (Veil) --- - let body_top_y = wy + title_h; - let body_h = wh - title_h; - - let tl_x = wx; - let tl_y = body_top_y; - let tr_x = wx + ww; - let tr_y = body_top_y; - - let bl_x = wx + dx; - let bl_y = body_top_y + body_h + dy; - let br_x = wx + ww + dx; - let br_y = body_top_y + body_h + dy; - - // Body Border (drawn slightly larger behind) - renderer.set_color(theme.border.0, theme.border.1, theme.border.2, alpha); - renderer.draw_quad( - tl_x - 1.0, - tl_y, - tr_x + 1.0, - tr_y, - bl_x - 1.0, - bl_y + 1.0, - br_x + 1.0, - br_y + 1.0, - ); - - // Body Background - renderer.set_color( - theme.window_bg.0, - theme.window_bg.1, - theme.window_bg.2, - theme.window_bg.3 * alpha, - ); - renderer.draw_quad(tl_x, tl_y, tr_x, tr_y, bl_x, bl_y, br_x, br_y); - - // Draw Modules inside Veil - let mut mod_y = body_top_y + (GUI_PADDING * scale_f); - for module_state in &mut window.modules { - let module_name = &module_state.name; - let mod_act_h = GUI_MODULE_HEIGHT * scale_f; // base height - let mod_w = ww - (GUI_PADDING * 2.0 * scale_f); - let mod_x = wx + (GUI_PADDING * scale_f); - - let box_side = mod_act_h; - let box_x = mod_x + mod_w - box_side; - let box_y = mod_y; - - // Check interactions - let is_hovering = is_topmost - && mx >= mod_x - && mx <= mod_x + mod_w - && my >= mod_y - && my <= mod_y + mod_act_h; - - let is_box_hovering = is_topmost - && mx >= box_x - && mx <= box_x + box_side - && my >= box_y - && my <= box_y + box_side; - - let mut is_enabled = false; - if let Some(m) = DarkClient::instance() - .modules - .read() - .unwrap() - .get(module_name) - { - let mut lock = m.lock().unwrap(); - is_enabled = lock.get_module_data().enabled; - - if is_hovering { - if left_clicked { - if is_box_hovering { - module_state.is_expanded = !module_state.is_expanded; - } else { - lock.get_module_data_mut().set_enabled(!is_enabled); - if !is_enabled { - let _ = lock.on_start(); - } else { - let _ = lock.on_stop(); - } - is_enabled = !is_enabled; - } - } - if right_clicked { - module_state.is_expanded = !module_state.is_expanded; - } - } - } - - // Calculate visual height including expansions - let mut expanded_height = 0.0; - if module_state.expand_anim > 0.01 { - if let Some(m) = DarkClient::instance() - .modules - .read() - .unwrap() - .get(module_name) - { - let lock = m.lock().unwrap(); - let settings_count = lock.get_module_data().settings.len() as f32; - expanded_height = settings_count * 14.0 * scale_f; - } - } - - let mod_total_visual_h = mod_act_h + (module_state.expand_anim * expanded_height); - - // Interpolate quad stretch - let t_top = ((mod_y - body_top_y) / body_h).clamp(0.0, 1.0); - let t_bot = ((mod_y + mod_total_visual_h - body_top_y) / body_h).clamp(0.0, 1.0); - - let mtl_x = mod_x + dx * t_top; - let mtl_y = mod_y + dy * t_top; - let mtr_x = mod_x + mod_w + dx * t_top; - let mtr_y = mod_y + dy * t_top; - - let mbl_x = mod_x + dx * t_bot; - let mbl_y = mod_y + mod_total_visual_h + dy * t_bot; - let mbr_x = mod_x + mod_w + dx * t_bot; - let mbr_y = mod_y + mod_total_visual_h + dy * t_bot; - - let bg_color = if is_hovering { - theme.module_bg_hover - } else { - theme.module_bg - }; - - renderer.set_color(bg_color.0, bg_color.1, bg_color.2, bg_color.3 * alpha); - renderer.draw_quad(mtl_x, mtl_y, mtr_x, mtr_y, mbl_x, mbl_y, mbr_x, mbr_y); - - // Module text - let text_t = ((mod_y + mod_act_h / 2.0 - body_top_y) / body_h).clamp(0.0, 1.0); - let t_dx = dx * text_t; - let t_dy = dy * text_t; - - let mod_t_width = get_text_width(module_name, text_scale); - let mod_t_x = mod_x + t_dx + (mod_w - mod_t_width as f32) / 2.0; - let mod_t_y = mod_y + t_dy + (mod_act_h - (7.0 * scale_f)) / 2.0; - - let text_color = if is_enabled { - theme.text_accent - } else { - theme.text_primary - }; - - draw_text( - renderer, - module_name, - mod_t_x as i32, - mod_t_y as i32, - text_color.0, - text_color.1, - text_color.2, - alpha, - text_scale, - ); - - // Draw arrow box - let t_dx_box = dx * text_t; - let t_dy_box = dy * text_t; - let actual_box_x = box_x + t_dx_box; - let actual_box_y = box_y + t_dy_box; - - let box_bg = if is_box_hovering { - theme.module_bg_hover - } else { - theme.module_bg - }; - - // Border separator - renderer.set_color(theme.border.0, theme.border.1, theme.border.2, alpha * 0.5); - renderer.draw_rect( - actual_box_x as i32 - 1, - actual_box_y as i32, - box_side as i32 + 1, - box_side as i32, - ); - - renderer.set_color(box_bg.0, box_bg.1, box_bg.2, box_bg.3 * alpha); - renderer.draw_rect( - actual_box_x as i32, - actual_box_y as i32, - box_side as i32, - box_side as i32, - ); - - let arrow_char = if module_state.is_expanded { "^" } else { "v" }; - let arrow_w = get_text_width(arrow_char, text_scale); - draw_text( - renderer, - arrow_char, - (actual_box_x + (box_side - arrow_w as f32) / 2.0) as i32, - (actual_box_y + (box_side - 7.0 * scale_f) / 2.0) as i32, - theme.text_primary.0, - theme.text_primary.1, - theme.text_primary.2, - alpha, - text_scale, - ); - - // Draw settings - if module_state.expand_anim > 0.01 { - if let Some(m) = DarkClient::instance() - .modules - .read() - .unwrap() - .get(module_name) - { - let mut lock = m.lock().unwrap(); - let data = lock.get_module_data_mut(); - - let mut set_y = mod_y + mod_act_h; - - // Let's get mouse dragged state - let left_down = if let Ok(mouse) = MOUSE_STATE.lock() { - mouse.left_down - } else { - false - }; - - for setting in &mut data.settings { - let set_h = 14.0 * scale_f; - let text_t = - ((set_y + set_h / 2.0 - body_top_y) / body_h).clamp(0.0, 1.0); - let t_dx = dx * text_t; - let t_dy = dy * text_t; - - let set_x = mod_x + (10.0 * scale_f); // Indent - let set_w = mod_w - (10.0 * scale_f); - - // Calculate skewed positions for interaction - let actual_sx = set_x + t_dx; - let actual_sy = set_y + t_dy; - - let is_set_hovering = is_topmost - && mx >= actual_sx - && mx <= actual_sx + set_w - && my >= actual_sy - && my <= actual_sy + set_h; - - let set_alpha = alpha * module_state.expand_anim; - - match setting { - crate::module::ModuleSetting::Toggle { name, value } => { - if is_set_hovering && left_clicked { - *value = !*value; - } - - let t_color = if *value { - theme.text_accent - } else { - theme.text_primary - }; - - draw_text( - renderer, - &format!("{}: {}", name, if *value { "On" } else { "Off" }), - actual_sx as i32, - (actual_sy + (set_h - 7.0 * scale_f) / 2.0) as i32, - t_color.0, - t_color.1, - t_color.2, - set_alpha, - text_scale, - ); - } - crate::module::ModuleSetting::Slider { - name, - value, - min, - max, - } => { - let slider_w = set_w - (60.0 * scale_f); - let slider_x = actual_sx + (50.0 * scale_f); - - if is_set_hovering && left_down { - let relative_x = (mx - slider_x).clamp(0.0, slider_w); - let factor = relative_x / slider_w; - *value = *min + factor * (*max - *min); - // Optional: snap or round value here - } - - draw_text( - renderer, - &format!("{}: {:.1}", name, value), - actual_sx as i32, - (actual_sy + (set_h - 7.0 * scale_f) / 2.0) as i32, - theme.text_primary.0, - theme.text_primary.1, - theme.text_primary.2, - set_alpha, - text_scale, - ); - - // Draw thin slider line - renderer.set_color( - theme.text_primary.0, - theme.text_primary.1, - theme.text_primary.2, - set_alpha * 0.5, - ); - renderer.draw_rect( - slider_x as i32, - (actual_sy + set_h / 2.0) as i32, - slider_w as i32, - (2.0 * scale_f) as i32, - ); - - // Draw handle - let handle_x = - slider_x + ((*value - *min) / (*max - *min)) * slider_w; - renderer.set_color( - theme.text_accent.0, - theme.text_accent.1, - theme.text_accent.2, - set_alpha, - ); - renderer.draw_rect( - (handle_x - 2.0 * scale_f) as i32, - (actual_sy + set_h / 2.0 - 2.0 * scale_f) as i32, - (4.0 * scale_f) as i32, - (6.0 * scale_f) as i32, - ); - } - crate::module::ModuleSetting::Choice { - name, - value, - options, - } => { - if is_set_hovering && left_clicked { - *value = (*value + 1) % options.len(); - } - let current_opt = if *value < options.len() { - &options[*value] - } else { - "Unknown" - }; - - draw_text( - renderer, - &format!("{}: {}", name, current_opt), - actual_sx as i32, - (actual_sy + (set_h - 7.0 * scale_f) / 2.0) as i32, - theme.text_primary.0, - theme.text_primary.1, - theme.text_primary.2, - set_alpha, - text_scale, - ); - } - crate::module::ModuleSetting::Color { name, .. } => { - draw_text( - renderer, - &format!("{}: [Color]", name), - actual_sx as i32, - (actual_sy + (set_h - 7.0 * scale_f) / 2.0) as i32, - theme.text_primary.0, - theme.text_primary.1, - theme.text_primary.2, - set_alpha, - text_scale, - ); - } - } - - set_y += set_h; - } - } - } - - mod_y += mod_total_visual_h + (2.0 * scale_f); - } + // Draw active windows and their nested modular dropdowns + for widget in &mut ui.windows { + widget.draw(renderer, &theme, scale_f); } - // DRAW CONTEXT BUTTONS (Top Right) - let btn_w = 60.0 * scale_f; - let btn_h = 20.0 * scale_f; - let btn_pad = 10.0 * scale_f; - - let reset_x = screen_w as f32 - btn_w - btn_pad; - let reset_y = btn_pad; - let panic_x = reset_x - btn_w - btn_pad; - let panic_y = btn_pad; - - // Reset UI Button - renderer.set_color( - theme.module_bg.0, - theme.module_bg.1, - theme.module_bg.2, - ui.background_alpha.min(0.8), - ); - renderer.draw_rect(reset_x as i32, reset_y as i32, btn_w as i32, btn_h as i32); - draw_text( - renderer, - "Reset UI", - (reset_x + 5.0 * scale_f) as i32, - (reset_y + 3.0 * scale_f) as i32, - theme.text_primary.0, - theme.text_primary.1, - theme.text_primary.2, - theme.text_primary.3, - base_scale, - ); - - // Panic Button - renderer.set_color(0.8, 0.2, 0.2, ui.background_alpha.min(0.8)); - renderer.draw_rect(panic_x as i32, panic_y as i32, btn_w as i32, btn_h as i32); - draw_text( - renderer, - "PANIC", - (panic_x + 12.0 * scale_f) as i32, - (panic_y + 3.0 * scale_f) as i32, - 1.0, - 1.0, - 1.0, - 1.0, - base_scale, - ); - - // Check clicks on buttons - let is_reset_hovering = - mx >= reset_x && mx <= reset_x + btn_w && my >= reset_y && my <= reset_y + btn_h; - let is_panic_hovering = - mx >= panic_x && mx <= panic_x + btn_w && my >= panic_y && my <= panic_y + btn_h; - - if left_clicked { - if is_reset_hovering { - ui.reset_ui(screen_w as f32, screen_h as f32); - } else if is_panic_hovering { - std::thread::spawn(|| crate::gui::call_panic()); - } - } + // Draw Top Right action buttons + ui.reset_btn.draw(renderer, &theme, scale_f); + ui.panic_btn.draw(renderer, &theme, scale_f); } // Consume clicks globally at end of frame diff --git a/client/src/graphic/ui_manager.rs b/client/src/graphic/ui_manager.rs index 5c2057b..f228db0 100644 --- a/client/src/graphic/ui_manager.rs +++ b/client/src/graphic/ui_manager.rs @@ -1,6 +1,7 @@ use crate::client::DarkClient; +use crate::graphic::color::Rgba; use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; -use crate::graphic::ui::{GUI_MODULE_HEIGHT, GUI_PADDING, GUI_SETTING_HEIGHT, GUI_TITLE_HEIGHT}; +use crate::graphic::widget::{Button, HudWidget, ModuleButton, Widget, Window}; use crate::module::ModuleCategory; use std::sync::Mutex; @@ -8,33 +9,11 @@ lazy_static::lazy_static! { pub static ref UI_MANAGER: Mutex = Mutex::new(UiManager::new()); } -pub struct ModuleUiState { - pub name: String, - pub is_expanded: bool, - pub expand_anim: f32, // 0.0 to 1.0 -} - -pub struct WindowState { - pub title: String, - pub category: ModuleCategory, - pub x: f32, - pub y: f32, - pub render_x: f32, - pub render_y: f32, - pub vel_x: f32, - pub vel_y: f32, - pub width: f32, - pub height: f32, - pub is_dragging: bool, - pub drag_offset_x: f32, - pub drag_offset_y: f32, - pub scroll_y: f32, - pub scroll_vel: f32, - pub modules: Vec, -} - pub struct UiManager { - pub windows: Vec, + pub windows: Vec, + pub reset_btn: Button, + pub panic_btn: Button, + pub hud_overlay: HudWidget, pub background_alpha: f32, pub is_visible: bool, pub initialized_layout: bool, @@ -42,101 +21,63 @@ pub struct UiManager { impl UiManager { pub fn new() -> Self { + let combat_win = Window::new( + 50.0, + 50.0, + 160.0, + 20.0, + ModuleCategory::COMBAT.display_name(), + ); + let move_win = Window::new( + 230.0, + 50.0, + 160.0, + 20.0, + ModuleCategory::MOVEMENT.display_name(), + ); + let render_win = Window::new( + 410.0, + 50.0, + 160.0, + 20.0, + ModuleCategory::RENDER.display_name(), + ); + let player_win = Window::new( + 590.0, + 50.0, + 160.0, + 20.0, + ModuleCategory::PLAYER.display_name(), + ); + let world_win = Window::new( + 770.0, + 50.0, + 160.0, + 20.0, + ModuleCategory::WORLD.display_name(), + ); + + let reset_btn = Button::new(0.0, 0.0, 60.0, 20.0, "Reset UI") + .with_bg_color(Rgba::new_rgb(0.2, 0.2, 0.2, 0.8)) + .with_text_color(Rgba::new_rgb(1.0, 1.0, 1.0, 1.0)); + + let panic_btn = Button::new(0.0, 0.0, 60.0, 20.0, "PANIC") + .with_bg_color(Rgba::new_rgb(0.8, 0.2, 0.2, 0.8)) + .with_text_color(Rgba::new_rgb(1.0, 1.0, 1.0, 1.0)); + let manager = Self { background_alpha: 0.0, is_visible: false, initialized_layout: false, + hud_overlay: HudWidget::new(), + reset_btn, + panic_btn, windows: vec![ - WindowState { - title: ModuleCategory::COMBAT.display_name().to_string(), - category: ModuleCategory::COMBAT, - x: 50.0, - y: 50.0, - render_x: 50.0, - render_y: 50.0, - vel_x: 0.0, - vel_y: 0.0, - width: 160.0, - height: 200.0, - is_dragging: false, - drag_offset_x: 0.0, - drag_offset_y: 0.0, - scroll_y: 0.0, - scroll_vel: 0.0, - modules: vec![], - }, - WindowState { - title: ModuleCategory::MOVEMENT.display_name().to_string(), - category: ModuleCategory::MOVEMENT, - x: 230.0, - y: 50.0, - render_x: 230.0, - render_y: 50.0, - vel_x: 0.0, - vel_y: 0.0, - width: 160.0, - height: 200.0, - is_dragging: false, - drag_offset_x: 0.0, - drag_offset_y: 0.0, - scroll_y: 0.0, - scroll_vel: 0.0, - modules: vec![], - }, - WindowState { - title: ModuleCategory::RENDER.display_name().to_string(), - category: ModuleCategory::RENDER, - x: 410.0, - y: 50.0, - render_x: 410.0, - render_y: 50.0, - vel_x: 0.0, - vel_y: 0.0, - width: 160.0, - height: 200.0, - is_dragging: false, - drag_offset_x: 0.0, - drag_offset_y: 0.0, - scroll_y: 0.0, - scroll_vel: 0.0, - modules: vec![], - }, - WindowState { - title: ModuleCategory::PLAYER.display_name().to_string(), - category: ModuleCategory::PLAYER, - x: 590.0, - y: 50.0, - render_x: 590.0, - render_y: 50.0, - vel_x: 0.0, - vel_y: 0.0, - width: 160.0, - height: 200.0, - is_dragging: false, - drag_offset_x: 0.0, - drag_offset_y: 0.0, - scroll_y: 0.0, - scroll_vel: 0.0, - modules: vec![], - }, - WindowState { - title: ModuleCategory::WORLD.display_name().to_string(), - category: ModuleCategory::WORLD, - x: 770.0, - y: 50.0, - render_x: 770.0, - render_y: 50.0, - vel_x: 0.0, - vel_y: 0.0, - width: 160.0, - height: 200.0, - is_dragging: false, - drag_offset_x: 0.0, - drag_offset_y: 0.0, - scroll_y: 0.0, - scroll_vel: 0.0, - modules: vec![], - }, + Widget::Window(combat_win), + Widget::Window(move_win), + Widget::Window(render_win), + Widget::Window(player_win), + Widget::Window(world_win), ], }; manager @@ -156,27 +97,77 @@ impl UiManager { let mut current_y = start_y; let mut row_max_height = 0.0_f32; - for window in &mut self.windows { - if current_x + window.width > screen_w && current_x > start_x { - // Wrap to next line - current_x = start_x; - current_y += row_max_height + gap_y; - row_max_height = 0.0; - } + for w_widget in &mut self.windows { + if let Widget::Window(window) = w_widget { + let mut calc_h = 25.0; + for child in &window.children { + match child { + Widget::Button(b) => calc_h += b.h + 2.0, + Widget::Label(_) => calc_h += 16.0, + Widget::ModuleButton(m) => calc_h += m.h + 2.0, + _ => {} + } + } + + if current_x + window.w > screen_w && current_x > start_x { + // Wrap to next line + current_x = start_x; + current_y += row_max_height + gap_y; + row_max_height = 0.0; + } - window.x = current_x; - window.y = current_y; - window.render_x = current_x; - window.render_y = current_y; + window.x = current_x; + window.y = current_y; + window.render_x = current_x; + window.render_y = current_y; - current_x += window.width + gap_x; - if window.height > row_max_height { - row_max_height = window.height; + current_x += window.w + gap_x; + if calc_h > row_max_height { + row_max_height = calc_h; + } } } } pub fn update(&mut self, scale_f: f32, screen_w: f32, screen_h: f32) { + // Sync real modules from DarkClient if empty (only triggers once) + for w_widget in &mut self.windows { + if let Widget::Window(window) = w_widget { + if window.children.is_empty() { + let client_modules_guard = DarkClient::instance().modules.read().unwrap(); + let target_category = match window.title.as_str() { + "Combat" => ModuleCategory::COMBAT, + "Movement" => ModuleCategory::MOVEMENT, + "Render" => ModuleCategory::RENDER, + "Player" => ModuleCategory::PLAYER, + "World" => ModuleCategory::WORLD, + _ => ModuleCategory::COMBAT, + }; + + let mut valid_modules: Vec<_> = client_modules_guard + .values() + .filter(|m| m.lock().unwrap().get_module_data().category == target_category) + .collect(); + + valid_modules.sort_by(|a, b| { + a.lock() + .unwrap() + .get_module_data() + .name + .cmp(&b.lock().unwrap().get_module_data().name) + }); + + window.children = valid_modules + .into_iter() + .map(|m| { + let name = m.lock().unwrap().get_module_data().name.clone(); + Widget::ModuleButton(ModuleButton::new(&name)) + }) + .collect(); + } + } + } + if !self.initialized_layout { self.auto_wrap_windows(screen_w, screen_h); self.initialized_layout = true; @@ -202,63 +193,30 @@ impl UiManager { return; } - // Sync real modules from DarkClient if empty - let client_modules_guard = DarkClient::instance().modules.read().unwrap(); - for window in &mut self.windows { - if window.modules.is_empty() { - let mut valid_modules: Vec<_> = client_modules_guard - .values() - .filter(|m| m.lock().unwrap().get_module_data().category == window.category) - .collect(); - - valid_modules.sort_by(|a, b| { - a.lock() - .unwrap() - .get_module_data() - .name - .cmp(&b.lock().unwrap().get_module_data().name) - }); - - window.modules = valid_modules - .into_iter() - .map(|m| ModuleUiState { - name: m.lock().unwrap().get_module_data().name.clone(), - is_expanded: false, - expand_anim: 0.0, - }) - .collect(); + // Handle interactions & logic updates + let (mx, my, left_clicked, right_clicked, left_down) = { + if let Ok(mut mouse) = MOUSE_STATE.lock() { + let l = mouse.left_clicked; + let r = mouse.right_clicked; + let ld = mouse.left_down; + mouse.left_clicked = false; + mouse.right_clicked = false; + (mouse.x as f32, mouse.y as f32, l, r, ld) + } else { + (0.0, 0.0, false, false, false) } + }; - // Animate expansions - for m_state in &mut window.modules { - if m_state.is_expanded { - if m_state.expand_anim < 1.0 { - m_state.expand_anim += 0.1; - } - } else { - if m_state.expand_anim > 0.0 { - m_state.expand_anim -= 0.1; - } - } - m_state.expand_anim = m_state.expand_anim.clamp(0.0, 1.0); - } - } + // Delegate clicks + if left_clicked || right_clicked { + let mut clicked_idx = None; + for (i, widget) in self.windows.iter_mut().enumerate().rev() { + if let Widget::Window(win) = widget { + let scaled_x = win.render_x * scale_f; + let scaled_y = win.render_y * scale_f; + let scaled_w = win.w * scale_f; + let scaled_h = win.h * scale_f; - // Handle Mouse state - if let Ok(mouse) = MOUSE_STATE.lock() { - let mx = mouse.x as f32; - let my = mouse.y as f32; - - // --- Click & Bring to Front logic --- - if mouse.left_clicked { - let mut clicked_idx = None; - for (i, window) in self.windows.iter_mut().enumerate().rev() { - let scaled_x = window.render_x * scale_f; - let scaled_y = window.render_y * scale_f; - let scaled_w = window.width * scale_f; - let scaled_h = window.height * scale_f; - - // Did we click inside the full window bounds? if mx >= scaled_x && mx <= scaled_x + scaled_w && my >= scaled_y @@ -268,76 +226,55 @@ impl UiManager { break; } } - - if let Some(idx) = clicked_idx { - let mut win = self.windows.remove(idx); - let scaled_x = win.render_x * scale_f; - let scaled_y = win.render_y * scale_f; - let title_height = GUI_TITLE_HEIGHT * scale_f; - - // Check if clicked exactly on the title bar for dragging - if my <= scaled_y + title_height { - win.is_dragging = true; - win.drag_offset_x = (mx - scaled_x) / scale_f; - win.drag_offset_y = (my - scaled_y) / scale_f; - } - self.windows.push(win); - } } - for window in &mut self.windows { - if !mouse.left_down { - window.is_dragging = false; - } - - if window.is_dragging { - window.x = (mx / scale_f) - window.drag_offset_x; - window.y = (my / scale_f) - window.drag_offset_y; - - // Clamp dragging within screen bounds - window.x = window.x.clamp(0.0, (screen_w / scale_f) - window.width); - window.y = window.y.clamp(0.0, (screen_h / scale_f) - GUI_TITLE_HEIGHT); - } - - // Spring Physics - let stiffness = 0.25; // How strongly it pulls towards target - let damping = 0.65; // Defines jelly bounce vs snap (lower = more bouncy) - - let fx = (window.x - window.render_x) * stiffness; - let fy = (window.y - window.render_y) * stiffness; - - window.vel_x = (window.vel_x + fx) * damping; - window.vel_y = (window.vel_y + fy) * damping; - - window.render_x += window.vel_x; - window.render_y += window.vel_y; - - // Dynamic window height expansion - let mut target_h = GUI_TITLE_HEIGHT + GUI_PADDING; // Title bar + padding - for m_state in &window.modules { - target_h += GUI_MODULE_HEIGHT; // Base module height - - if m_state.expand_anim > 0.01 { - if let Some(m) = client_modules_guard.get(&m_state.name) { - let lock = m.lock().unwrap(); - let settings_count = lock.get_module_data().settings.len() as f32; - let expanded_h = settings_count * GUI_SETTING_HEIGHT; - target_h += m_state.expand_anim * expanded_h; - } - } - target_h += 2.0; // Gap between modules - } - target_h += GUI_PADDING; // Bottom padding + if let Some(idx) = clicked_idx { + let mut top_win = self.windows.remove(idx); + top_win.handle_click(mx, my, left_clicked, right_clicked, scale_f); + self.windows.push(top_win); + } + } - // Max limits and scrolling - let max_h = 300.0; // Cap to arbitrary viewport size before scrolling - if target_h > max_h { - target_h = max_h; - // todo scrolling bounds - } + // Delegate updates + for widget in &mut self.windows { + widget.update(mx, my, left_down, scale_f); + } - // Lerp window height - window.height += (target_h - window.height) * 0.25; + // Layout Context Buttons + let btn_w = 60.0; + let btn_h = 20.0; + let btn_pad = 10.0; + + self.reset_btn.x = (screen_w / scale_f) - btn_w - btn_pad; + self.reset_btn.y = btn_pad; + self.reset_btn.w = btn_w; + self.reset_btn.h = btn_h; + + self.panic_btn.x = self.reset_btn.x - btn_w - btn_pad; + self.panic_btn.y = btn_pad; + self.panic_btn.w = btn_w; + self.panic_btn.h = btn_h; + + // Apply alpha to context buttons dynamically + let shared_alpha = self.background_alpha.min(0.8); + self.reset_btn.bg_color.a = shared_alpha; + self.panic_btn.bg_color.a = shared_alpha; + + self.reset_btn.update(mx, my, left_down, scale_f); + self.panic_btn.update(mx, my, left_down, scale_f); + + if left_clicked { + if self + .reset_btn + .handle_click(mx, my, left_clicked, right_clicked, scale_f) + { + self.reset_ui(screen_w, screen_h); + } + if self + .panic_btn + .handle_click(mx, my, left_clicked, right_clicked, scale_f) + { + std::thread::spawn(|| crate::gui::call_panic()); } } } diff --git a/client/src/graphic/widget/button.rs b/client/src/graphic/widget/button.rs new file mode 100644 index 0000000..b7c765f --- /dev/null +++ b/client/src/graphic/widget/button.rs @@ -0,0 +1,111 @@ +use crate::graphic::color::Rgba; +use crate::graphic::font::get_text_width; +use crate::graphic::render::Renderer; +use crate::graphic::ui::Theme; + +pub struct Button { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + pub text: String, + pub bg_color: Rgba, + pub text_color: Rgba, + pub on_click: Option>, + is_hovered: bool, +} + +impl Button { + pub fn new(x: f32, y: f32, w: f32, h: f32, text: &str) -> Self { + Self { + x, + y, + w, + h, + text: text.to_string(), + bg_color: Rgba::new_rgb(0.2, 0.2, 0.2, 1.0), + text_color: Rgba::new_rgb(1.0, 1.0, 1.0, 1.0), + on_click: None, + is_hovered: false, + } + } + + pub fn with_bg_color(mut self, rgba: Rgba) -> Self { + self.bg_color = rgba; + self + } + + pub fn with_text_color(mut self, rgba: Rgba) -> Self { + self.text_color = rgba; + self + } + + pub fn on_click(mut self, callback: F) -> Self + where + F: FnMut() + Send + Sync + 'static, + { + self.on_click = Some(Box::new(callback)); + self + } + + pub fn update(&mut self, mx: f32, my: f32, _left_down: bool, scale_f: f32) { + self.is_hovered = mx >= self.x * scale_f + && mx <= (self.x + self.w) * scale_f + && my >= self.y * scale_f + && my <= (self.y + self.h) * scale_f; + } + + pub fn handle_click( + &mut self, + _mx: f32, + _my: f32, + left_clicked: bool, + _right_clicked: bool, + _scale_f: f32, + ) -> bool { + if self.is_hovered && left_clicked { + if let Some(ref mut cb) = self.on_click { + cb(); + } + return true; + } + false + } + + pub fn draw(&mut self, renderer: &mut Renderer, _theme: &Theme, scale_f: f32) { + let actual_bg = if self.is_hovered { + Rgba::new_rgb( + (self.bg_color.r + 0.2).min(1.0), + (self.bg_color.g + 0.2).min(1.0), + (self.bg_color.b + 0.2).min(1.0), + self.bg_color.a, + ) + } else { + self.bg_color + }; + + unsafe { + renderer.set_color(actual_bg); + renderer.draw_rect( + (self.x * scale_f) as i32, + (self.y * scale_f) as i32, + (self.w * scale_f) as i32, + (self.h * scale_f) as i32, + ); + + let t_width = get_text_width(&self.text, scale_f as i32); + let text_x = (self.x * scale_f) as i32 + ((self.w * scale_f) as i32 - t_width) / 2; + let text_y = + (self.y * scale_f) as i32 + ((self.h * scale_f) as i32 - (7 * scale_f as i32)) / 2; + + crate::graphic::font::draw_text( + renderer, + &self.text, + text_x, + text_y, + self.text_color, + scale_f as i32, + ); + } + } +} diff --git a/client/src/graphic/widget/hud.rs b/client/src/graphic/widget/hud.rs new file mode 100644 index 0000000..7caa4fd --- /dev/null +++ b/client/src/graphic/widget/hud.rs @@ -0,0 +1,83 @@ +use crate::client::DarkClient; +use crate::graphic::font::draw_text; +use crate::graphic::render::Renderer; +use crate::graphic::ui::{HudColor, Theme}; + +pub struct HudWidget { + pub is_visible: bool, +} + +impl HudWidget { + pub fn new() -> Self { + Self { is_visible: true } + } + + pub fn draw(&mut self, renderer: &mut Renderer, _theme: &Theme, scale_f: f32) { + if !self.is_visible { + return; + } + + let watermark = "DarkClient"; + let w_color = HudColor::Yellow.to_rgba(); + + let mut hud_y = 5.0 * scale_f; + let hud_x = 5.0 * scale_f; + let text_scale = (2.0 * scale_f) as i32; // Using 2.0 multiplier for large HUD text + + unsafe { + draw_text( + renderer, + watermark, + hud_x as i32, + hud_y as i32, + w_color, + text_scale, + ); + } + + hud_y += 24.0 * scale_f; // watermark margin bottom + + if let Ok(modules_map) = DarkClient::instance().modules.read() { + let mut active_mods: Vec = modules_map + .values() + .filter_map(|m| { + let lock = m.lock().unwrap(); + if lock.get_module_data().enabled { + Some(lock.get_module_data().name.clone()) + } else { + None + } + }) + .collect(); + + // Sort by length (longest first) + active_mods.sort_by(|a, b| b.len().cmp(&a.len())); + + let arraylist_colors = [ + HudColor::Purple, + HudColor::Cyan, + HudColor::Green, + HudColor::Red, + HudColor::Yellow, + HudColor::Blue, + HudColor::White, + ]; + + for (i, mod_name) in active_mods.iter().enumerate() { + let color = arraylist_colors[i % arraylist_colors.len()].to_rgba(); + let m_scale = (1.5 * scale_f) as i32; // Font slightly smaller than watermark + unsafe { + draw_text( + renderer, + mod_name, + hud_x as i32, + hud_y as i32, + color, + m_scale, + ); + } + hud_y += 18.0 * scale_f; // module spacing + } + } + } +} diff --git a/client/src/graphic/widget/label.rs b/client/src/graphic/widget/label.rs new file mode 100644 index 0000000..5665d91 --- /dev/null +++ b/client/src/graphic/widget/label.rs @@ -0,0 +1,60 @@ +use crate::graphic::color::Rgba; +use crate::graphic::render::Renderer; +use crate::graphic::ui::Theme; + +pub struct Label { + pub x: f32, + pub y: f32, + pub text: String, + pub color: Rgba, + pub scale_mult: f32, +} + +impl Label { + pub fn new(x: f32, y: f32, text: &str) -> Self { + Self { + x, + y, + text: text.to_string(), + color: Rgba::new_rgb(1.0, 1.0, 1.0, 1.0), + scale_mult: 1.0, + } + } + + pub fn with_color(mut self, rgba: Rgba) -> Self { + self.color = rgba; + self + } + + pub fn with_scale(mut self, mult: f32) -> Self { + self.scale_mult = mult; + self + } + + pub fn update(&mut self, _mx: f32, _my: f32, _left_down: bool, _scale_f: f32) {} + + pub fn handle_click( + &mut self, + _mx: f32, + _my: f32, + _lc: bool, + _rc: bool, + _scale_f: f32, + ) -> bool { + false + } + + pub fn draw(&mut self, renderer: &mut Renderer, _theme: &Theme, scale_f: f32) { + unsafe { + let final_scale = (scale_f * self.scale_mult) as i32; + crate::graphic::font::draw_text( + renderer, + &self.text, + (self.x * scale_f) as i32, + (self.y * scale_f) as i32, + self.color, + final_scale, + ); + } + } +} diff --git a/client/src/graphic/widget/mod.rs b/client/src/graphic/widget/mod.rs new file mode 100644 index 0000000..b583177 --- /dev/null +++ b/client/src/graphic/widget/mod.rs @@ -0,0 +1,76 @@ +pub mod button; +pub mod label; +pub mod panel; + +use crate::graphic::render::Renderer; +use crate::graphic::ui::Theme; + +pub mod hud; +pub mod module_btn; +pub mod slider; +pub mod toggle; +pub mod window; + +pub use button::Button; +pub use hud::HudWidget; +pub use label::Label; +pub use module_btn::ModuleButton; +pub use panel::Panel; +pub use slider::Slider; +pub use toggle::Toggle; +pub use window::Window; + +pub enum Widget { + Button(Button), + Label(Label), + Panel(Panel), + Window(Window), + ModuleButton(ModuleButton), + Slider(Slider), + Toggle(Toggle), +} + +impl Widget { + pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { + match self { + Widget::Button(b) => b.draw(renderer, theme, scale_f), + Widget::Label(l) => l.draw(renderer, theme, scale_f), + Widget::Panel(p) => p.draw(renderer, theme, scale_f), + Widget::Window(w) => w.draw(renderer, theme, scale_f), + Widget::ModuleButton(m) => m.draw(renderer, theme, scale_f), + Widget::Slider(s) => s.draw(renderer, theme, scale_f), + Widget::Toggle(t) => t.draw(renderer, theme, scale_f), + } + } + + pub fn handle_click( + &mut self, + mx: f32, + my: f32, + left_clicked: bool, + right_clicked: bool, + scale_f: f32, + ) -> bool { + match self { + Widget::Button(b) => b.handle_click(mx, my, left_clicked, right_clicked, scale_f), + Widget::Label(l) => l.handle_click(mx, my, left_clicked, right_clicked, scale_f), + Widget::Panel(p) => p.handle_click(mx, my, left_clicked, right_clicked, scale_f), + Widget::Window(w) => w.handle_click(mx, my, left_clicked, right_clicked, scale_f), + Widget::ModuleButton(m) => m.handle_click(mx, my, left_clicked, right_clicked, scale_f), + Widget::Slider(s) => s.handle_click(mx, my, left_clicked, right_clicked, scale_f), + Widget::Toggle(t) => t.handle_click(mx, my, left_clicked, right_clicked, scale_f), + } + } + + pub fn update(&mut self, mx: f32, my: f32, left_down: bool, scale_f: f32) { + match self { + Widget::Button(b) => b.update(mx, my, left_down, scale_f), + Widget::Label(l) => l.update(mx, my, left_down, scale_f), + Widget::Panel(p) => p.update(mx, my, left_down, scale_f), + Widget::Window(w) => w.update(mx, my, left_down, scale_f), + Widget::ModuleButton(m) => m.update(mx, my, left_down, scale_f), + Widget::Slider(s) => s.update(mx, my, left_down, scale_f), + Widget::Toggle(t) => t.update(mx, my, left_down, scale_f), + } + } +} diff --git a/client/src/graphic/widget/module_btn.rs b/client/src/graphic/widget/module_btn.rs new file mode 100644 index 0000000..57153ba --- /dev/null +++ b/client/src/graphic/widget/module_btn.rs @@ -0,0 +1,267 @@ +use crate::client::DarkClient; +use crate::graphic::font::get_text_width; +use crate::graphic::render::Renderer; +use crate::graphic::ui::Theme; +use crate::graphic::widget::{Slider, Toggle, Widget}; +use crate::module::ModuleSetting; + +pub struct ModuleButton { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + pub name: String, + + // Internal States + pub is_expanded: bool, + pub expand_anim: f32, + pub settings: Vec, + + is_hovered: bool, + is_box_hovered: bool, +} + +impl ModuleButton { + pub fn new(name: &str) -> Self { + let mut settings_widgets = Vec::new(); + if let Ok(modules) = DarkClient::instance().modules.read() { + if let Some(m) = modules.get(name) { + let lock = m.lock().unwrap(); + for setting in &lock.get_module_data().settings { + match setting { + ModuleSetting::Toggle { name: s_name, .. } => { + settings_widgets.push(Widget::Toggle(Toggle::new(name, s_name))); + } + ModuleSetting::Slider { + name: s_name, + min, + max, + .. + } => { + settings_widgets + .push(Widget::Slider(Slider::new(name, s_name, *min, *max))); + } + _ => {} + } + } + } + } + + Self { + x: 0.0, + y: 0.0, + w: 160.0, + h: 16.0, + name: name.to_string(), + is_expanded: false, + expand_anim: 0.0, + settings: settings_widgets, + is_hovered: false, + is_box_hovered: false, + } + } + + pub fn update(&mut self, mx: f32, my: f32, _left_down: bool, scale_f: f32) { + let scaled_x = self.x * scale_f; + let scaled_y = self.y * scale_f; + let scaled_w = self.w * scale_f; + let scaled_h = self.h * scale_f; + + let box_side = 14.0 * scale_f; + let box_x = scaled_x + scaled_w - box_side - (2.0 * scale_f); + let box_y = scaled_y + (scaled_h - box_side) / 2.0; + + self.is_hovered = mx >= scaled_x + && mx <= scaled_x + scaled_w + && my >= scaled_y + && my <= scaled_y + scaled_h; + + self.is_box_hovered = + mx >= box_x && mx <= box_x + box_side && my >= box_y && my <= box_y + box_side; + + if self.is_expanded { + if self.expand_anim < 1.0 { + self.expand_anim = (self.expand_anim + 0.1).min(1.0); + } + } else { + if self.expand_anim > 0.0 { + self.expand_anim = (self.expand_anim - 0.1).max(0.0); + } + } + + // Calculate dynamic height footprint including settings + let mut target_h = 16.0; // Base height + if self.expand_anim > 0.01 { + let settings_count = self.settings.len() as f32; + target_h += self.expand_anim * (settings_count * 14.0); + } + self.h = target_h; + + // Propagate updates to settings + if self.expand_anim > 0.01 { + let mut current_set_y = self.y + 16.0; + let set_h = 14.0; + + for child in &mut self.settings { + match child { + Widget::Toggle(t) => { + t.x = self.x; + t.y = current_set_y; + t.w = self.w; + t.h = set_h; + t.is_expanded_anim = self.expand_anim; + t.update(mx, my, _left_down, scale_f); + } + Widget::Slider(s) => { + s.x = self.x; + s.y = current_set_y; + s.w = self.w; + s.h = set_h; + s.is_expanded_anim = self.expand_anim; + s.update(mx, my, _left_down, scale_f); + } + _ => {} + } + current_set_y += set_h; + } + } + } + + pub fn handle_click( + &mut self, + mx: f32, + my: f32, + left_clicked: bool, + right_clicked: bool, + scale_f: f32, + ) -> bool { + if self.expand_anim > 0.01 { + for child in self.settings.iter_mut().rev() { + if child.handle_click(mx, my, left_clicked, right_clicked, scale_f) { + return true; + } + } + } + + if self.is_hovered { + if left_clicked { + if self.is_box_hovered { + self.is_expanded = !self.is_expanded; + } else { + if let Some(m) = DarkClient::instance() + .modules + .read() + .unwrap() + .get(&self.name) + { + let mut glk = m.lock().unwrap(); + let currently_enabled = glk.get_module_data().enabled; + glk.get_module_data_mut().set_enabled(!currently_enabled); + if !currently_enabled { + let _ = glk.on_start(); + } else { + let _ = glk.on_stop(); + } + } + } + return true; + } + if right_clicked { + self.is_expanded = !self.is_expanded; + return true; + } + } + false + } + + pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { + let alpha = 1.0; + let bg_color = if self.is_hovered { + theme.module_bg_hover + } else { + theme.module_bg + }; + + unsafe { + // Background + renderer.set_color(bg_color.with_alpha(bg_color.a * alpha)); + renderer.draw_rect( + (self.x * scale_f) as i32, + (self.y * scale_f) as i32, + (self.w * scale_f) as i32, + (16.0 * scale_f) as i32, + ); + + // Fetch state + let mut is_enabled = false; + if let Some(m) = DarkClient::instance() + .modules + .read() + .unwrap() + .get(&self.name) + { + is_enabled = m.lock().unwrap().get_module_data().enabled; + } + + let text_color = if is_enabled { + theme.text_accent + } else { + theme.text_primary + }; + + let t_width = get_text_width(&self.name, scale_f as i32); + let text_x = (self.x * scale_f) as i32 + ((self.w * scale_f) as i32 - t_width) / 2; + let text_y = + (self.y * scale_f) as i32 + ((16.0 * scale_f) as i32 - (7 * scale_f as i32)) / 2; + + // Name + crate::graphic::font::draw_text( + renderer, + &self.name, + text_x, + text_y, + text_color.with_alpha(alpha), + scale_f as i32, + ); + + // Expansion Box + let box_side = 14.0 * scale_f; + let box_x = (self.x * scale_f) + (self.w * scale_f) - box_side - (2.0 * scale_f); + let box_y = (self.y * scale_f) + ((16.0 * scale_f) - box_side) / 2.0; + + let box_bg = if self.is_box_hovered { + theme.module_bg_hover + } else { + theme.module_bg + }; + + renderer.set_color(theme.border.with_alpha(alpha * 0.5)); + renderer.draw_rect( + box_x as i32 - 1, + box_y as i32, + box_side as i32 + 1, + box_side as i32, + ); + renderer.set_color(box_bg.with_alpha(box_bg.a * alpha)); + renderer.draw_rect(box_x as i32, box_y as i32, box_side as i32, box_side as i32); + + let arrow = if self.is_expanded { "^" } else { "v" }; + let aw = get_text_width(arrow, scale_f as i32); + crate::graphic::font::draw_text( + renderer, + arrow, + (box_x + (box_side - aw as f32) / 2.0) as i32, + (box_y + (box_side - 7.0 * scale_f) / 2.0) as i32, + theme.text_primary.with_alpha(alpha), + scale_f as i32, + ); + + // Draw Settings + if self.expand_anim > 0.01 { + for child in &mut self.settings { + child.draw(renderer, theme, scale_f); + } + } + } + } +} diff --git a/client/src/graphic/widget/panel.rs b/client/src/graphic/widget/panel.rs new file mode 100644 index 0000000..b6e954b --- /dev/null +++ b/client/src/graphic/widget/panel.rs @@ -0,0 +1,128 @@ +use super::Widget; +use crate::graphic::color::Rgba; +use crate::graphic::render::Renderer; +use crate::graphic::ui::Theme; + +pub struct Panel { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + pub bg_color: Rgba, + pub children: Vec, + pub is_draggable: bool, + + // Internal state + is_dragging: bool, + drag_offset_x: f32, + drag_offset_y: f32, +} + +impl Panel { + pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self { + Self { + x, + y, + w, + h, + bg_color: Rgba::new_rgb(0.1, 0.1, 0.1, 0.8), + children: Vec::new(), + is_draggable: true, + is_dragging: false, + drag_offset_x: 0.0, + drag_offset_y: 0.0, + } + } + + pub fn with_bg_color(mut self, rgba: Rgba) -> Self { + self.bg_color = rgba; + self + } + + pub fn with_draggable(mut self, drag: bool) -> Self { + self.is_draggable = drag; + self + } + + pub fn add_child(mut self, widget: Widget) -> Self { + self.children.push(widget); + self + } + + pub fn update(&mut self, mx: f32, my: f32, left_down: bool, scale_f: f32) { + if self.is_draggable { + if !left_down { + self.is_dragging = false; + } else if self.is_dragging { + self.x = (mx / scale_f) - self.drag_offset_x; + self.y = (my / scale_f) - self.drag_offset_y; + } else { + // If it's a new click within Draggable Title Area + if mx >= self.x * scale_f + && mx <= (self.x + self.w) * scale_f + && my >= self.y * scale_f + && my <= (self.y + 20.0) * scale_f + { + self.is_dragging = true; + self.drag_offset_x = (mx / scale_f) - self.x; + self.drag_offset_y = (my / scale_f) - self.y; + } + } + } + + for child in &mut self.children { + child.update(mx, my, left_down, scale_f); + } + } + + pub fn handle_click( + &mut self, + mx: f32, + my: f32, + left_clicked: bool, + right_clicked: bool, + scale_f: f32, + ) -> bool { + // First interact with children uniformly reversed (Top down) + let mut consumed = false; + for child in self.children.iter_mut().rev() { + if child.handle_click(mx, my, left_clicked, right_clicked, scale_f) { + consumed = true; + break; + } + } + + if consumed { + return true; + } + + // Then interact with Panel rect itself + if mx >= self.x * scale_f + && mx <= (self.x + self.w) * scale_f + && my >= self.y * scale_f + && my <= (self.y + self.h) * scale_f + { + if left_clicked || right_clicked { + return true; + } + } + + false + } + + pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { + unsafe { + renderer.set_color(self.bg_color); + renderer.draw_rect( + (self.x * scale_f) as i32, + (self.y * scale_f) as i32, + (self.w * scale_f) as i32, + (self.h * scale_f) as i32, + ); + } + + for child in &mut self.children { + child.draw(renderer, theme, scale_f); + } + } +} diff --git a/client/src/graphic/widget/slider.rs b/client/src/graphic/widget/slider.rs new file mode 100644 index 0000000..544af8a --- /dev/null +++ b/client/src/graphic/widget/slider.rs @@ -0,0 +1,144 @@ +use crate::client::DarkClient; +use crate::graphic::color::Color; +use crate::graphic::render::Renderer; +use crate::graphic::ui::Theme; + +pub struct Slider { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + pub module_name: String, + pub setting_name: String, + pub min: f32, + pub max: f32, + pub is_expanded_anim: f32, + is_hovered: bool, + is_dragging: bool, +} + +impl Slider { + pub fn new(module_name: &str, setting_name: &str, min: f32, max: f32) -> Self { + Self { + x: 0.0, + y: 0.0, + w: 160.0, // default width + h: 14.0, // default height + module_name: module_name.to_string(), + setting_name: setting_name.to_string(), + min, + max, + is_expanded_anim: 0.0, + is_hovered: false, + is_dragging: false, + } + } + + pub fn update(&mut self, mx: f32, my: f32, left_down: bool, scale_f: f32) { + let sx = self.x * scale_f; + let sy = self.y * scale_f; + let sw = self.w * scale_f; + let sh = self.h * scale_f; + + self.is_hovered = mx >= sx && mx <= sx + sw && my >= sy && my <= sy + sh; + + if !left_down { + self.is_dragging = false; + } else if self.is_dragging { + // Compute slider value based on mouse X constraint + let mut value_pct = (mx - sx) / sw; + value_pct = value_pct.clamp(0.0, 1.0); + let mut new_val = self.min + (self.max - self.min) * value_pct; + + // Rounding logic for aesthetics if it's acting like an int or single decimal bounds. + // Assuming 1 decimal point for general float sliders for clean UI. + new_val = (new_val * 10.0).round() / 10.0; + + if let Ok(modules) = DarkClient::instance().modules.read() { + if let Some(m) = modules.get(&self.module_name) { + if let Some(setting) = m + .lock() + .unwrap() + .get_module_data_mut() + .get_setting_mut(&self.setting_name) + { + setting.set_slider_value(new_val); + } + } + } + } + } + + pub fn handle_click( + &mut self, + _mx: f32, + _my: f32, + left_clicked: bool, + _right_clicked: bool, + _scale_f: f32, + ) -> bool { + if self.is_hovered && left_clicked { + self.is_dragging = true; + return true; + } + false + } + + pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { + if self.is_expanded_anim < 0.01 { + return; + } + + let mut val = self.min; + if let Ok(modules) = DarkClient::instance().modules.read() { + if let Some(m) = modules.get(&self.module_name) { + if let Some(setting) = m + .lock() + .unwrap() + .get_module_data() + .get_setting(&self.setting_name) + { + if let Some(v) = setting.get_slider_value() { + val = v; + } + } + } + } + + let bg_color = if self.is_hovered { + theme.module_bg_hover + } else { + theme.module_bg + }; + + let sx = self.x * scale_f; + let sy = self.y * scale_f; + let sw = self.w * scale_f; + let sh = self.h * scale_f; + + let pct = ((val - self.min) / (self.max - self.min)).clamp(0.0, 1.0); + let filled_w = sw * pct; + + unsafe { + // Draw Background Track + renderer.set_color(bg_color.with_alpha(bg_color.a * self.is_expanded_anim)); + renderer.draw_rect(sx as i32, sy as i32, sw as i32, sh as i32); + + // Draw Filled Track + let track_col = theme.text_accent; + renderer.set_color(track_col.with_alpha(self.is_expanded_anim * 0.8)); + renderer.draw_rect(sx as i32, sy as i32, filled_w as i32, sh as i32); + + // Draw Text + let display_text = format!("{}: {:.1}", self.setting_name, val); + crate::graphic::font::draw_text( + renderer, + &display_text, + (sx + 5.0 * scale_f) as i32, + (sy + (sh - 7.0 * scale_f) / 2.0) as i32, + Color::Black.to_rgba(self.is_expanded_anim), + scale_f as i32, + ); + } + } +} diff --git a/client/src/graphic/widget/toggle.rs b/client/src/graphic/widget/toggle.rs new file mode 100644 index 0000000..c525c2b --- /dev/null +++ b/client/src/graphic/widget/toggle.rs @@ -0,0 +1,136 @@ +use crate::client::DarkClient; +use crate::graphic::render::Renderer; +use crate::graphic::ui::Theme; + +pub struct Toggle { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + pub module_name: String, + pub setting_name: String, + pub is_expanded_anim: f32, + is_hovered: bool, +} + +impl Toggle { + pub fn new(module_name: &str, setting_name: &str) -> Self { + Self { + x: 0.0, + y: 0.0, + w: 160.0, + h: 14.0, + module_name: module_name.to_string(), + setting_name: setting_name.to_string(), + is_expanded_anim: 0.0, + is_hovered: false, + } + } + + pub fn update(&mut self, mx: f32, my: f32, _left_down: bool, scale_f: f32) { + let sx = self.x * scale_f; + let sy = self.y * scale_f; + let sw = self.w * scale_f; + let sh = self.h * scale_f; + + self.is_hovered = mx >= sx && mx <= sx + sw && my >= sy && my <= sy + sh; + } + + pub fn handle_click( + &mut self, + _mx: f32, + _my: f32, + left_clicked: bool, + _right_clicked: bool, + _scale_f: f32, + ) -> bool { + if self.is_hovered && left_clicked { + if let Ok(modules) = DarkClient::instance().modules.read() { + if let Some(m) = modules.get(&self.module_name) { + if let Some(setting) = m + .lock() + .unwrap() + .get_module_data_mut() + .get_setting_mut(&self.setting_name) + { + let current = setting.get_toggle_value().unwrap_or(false); + setting.set_toggle_value(!current); + } + } + } + return true; + } + false + } + + pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { + if self.is_expanded_anim < 0.01 { + return; + } + + let mut val = false; + if let Ok(modules) = DarkClient::instance().modules.read() { + if let Some(m) = modules.get(&self.module_name) { + if let Some(setting) = m + .lock() + .unwrap() + .get_module_data() + .get_setting(&self.setting_name) + { + if let Some(v) = setting.get_toggle_value() { + val = v; + } + } + } + } + + let bg_color = if self.is_hovered { + theme.module_bg_hover + } else { + theme.module_bg + }; + + let sx = self.x * scale_f; + let sy = self.y * scale_f; + let sw = self.w * scale_f; + let sh = self.h * scale_f; + + unsafe { + // Draw Background + renderer.set_color(bg_color.with_alpha(bg_color.a * self.is_expanded_anim)); + renderer.draw_rect(sx as i32, sy as i32, sw as i32, sh as i32); + + // Draw Checkbox Box + let box_s = 8.0 * scale_f; + let padding_r = 10.0 * scale_f; + let box_x = sx + sw - box_s - padding_r; + let box_y = sy + (sh - box_s) / 2.0; + + renderer.set_color(theme.border.with_alpha(self.is_expanded_anim * 0.5)); + renderer.draw_rect( + (box_x - 1.0) as i32, + (box_y - 1.0) as i32, + (box_s + 2.0) as i32, + (box_s + 2.0) as i32, + ); + + let box_bg = if val { + theme.text_accent + } else { + theme.window_bg + }; + renderer.set_color(box_bg.with_alpha(self.is_expanded_anim)); + renderer.draw_rect(box_x as i32, box_y as i32, box_s as i32, box_s as i32); + + // Draw Text + crate::graphic::font::draw_text( + renderer, + &self.setting_name, + (sx + 5.0 * scale_f) as i32, + (sy + (sh - 7.0 * scale_f) / 2.0) as i32, + theme.text_primary.with_alpha(self.is_expanded_anim), + scale_f as i32, + ); + } + } +} diff --git a/client/src/graphic/widget/window.rs b/client/src/graphic/widget/window.rs new file mode 100644 index 0000000..e2358bf --- /dev/null +++ b/client/src/graphic/widget/window.rs @@ -0,0 +1,227 @@ +use super::Widget; +use crate::graphic::font::get_text_width; +use crate::graphic::render::Renderer; +use crate::graphic::ui::Theme; + +pub struct Window { + pub x: f32, + pub y: f32, + pub render_x: f32, + pub render_y: f32, + pub vel_x: f32, + pub vel_y: f32, + pub w: f32, + pub h: f32, + pub title: String, + pub children: Vec, + + pub is_dragging: bool, + drag_offset_x: f32, + drag_offset_y: f32, +} + +impl Window { + pub fn new(x: f32, y: f32, w: f32, h: f32, title: &str) -> Self { + Self { + x, + y, + render_x: x, + render_y: y, + vel_x: 0.0, + vel_y: 0.0, + w, + h, + title: title.to_string(), + children: Vec::new(), + is_dragging: false, + drag_offset_x: 0.0, + drag_offset_y: 0.0, + } + } + + pub fn add_child(mut self, child: Widget) -> Self { + self.children.push(child); + self + } + + pub fn update(&mut self, mx: f32, my: f32, left_down: bool, scale_f: f32) { + if left_down { + if self.is_dragging { + self.x = (mx / scale_f) - self.drag_offset_x; + self.y = (my / scale_f) - self.drag_offset_y; + } + } else { + self.is_dragging = false; + } + + // Spring Physics + let stiffness = 0.25; + let damping = 0.65; + + let fx = (self.x - self.render_x) * stiffness; + let fy = (self.y - self.render_y) * stiffness; + + self.vel_x = (self.vel_x + fx) * damping; + self.vel_y = (self.vel_y + fy) * damping; + + self.render_x += self.vel_x; + self.render_y += self.vel_y; + + // Propagate update to children iteratively adjusting Y offset + let mut child_y = self.render_y + 25.0; // below title + padding + for child in &mut self.children { + match child { + Widget::Button(b) => { + b.x = self.render_x + 5.0; + b.y = child_y; + b.update(mx, my, left_down, scale_f); + child_y += b.h + 2.0; + } + Widget::Label(l) => { + l.x = self.render_x + 5.0; + l.y = child_y; + l.update(mx, my, left_down, scale_f); + child_y += 14.0 + 2.0; + } + Widget::ModuleButton(m) => { + m.x = self.render_x + 5.0; + m.y = child_y; + m.w = self.w - 10.0; + m.update(mx, my, left_down, scale_f); + child_y += m.h + 2.0; + } + _ => {} + } + } + + let target_h = (child_y - self.render_y).max(25.0); + self.h += (target_h - self.h) * 0.25; + } + + pub fn handle_click( + &mut self, + mx: f32, + my: f32, + left_clicked: bool, + right_clicked: bool, + scale_f: f32, + ) -> bool { + let mut consumed = false; + for child in self.children.iter_mut().rev() { + if child.handle_click(mx, my, left_clicked, right_clicked, scale_f) { + consumed = true; + break; + } + } + + if consumed { + return true; + } + + let sx = self.render_x * scale_f; + let sy = self.render_y * scale_f; + let sw = self.w * scale_f; + let sh = self.h * scale_f; + + if mx >= sx && mx <= sx + sw && my >= sy && my <= sy + sh { + if left_clicked || right_clicked { + if left_clicked { + let title_height = 20.0 * scale_f; + if my <= sy + title_height { + self.is_dragging = true; + self.drag_offset_x = (mx - sx) / scale_f; + self.drag_offset_y = (my - sy) / scale_f; + } + } + return true; + } + } + false + } + + pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { + let wx = self.x * scale_f; + let wy = self.y * scale_f; + let ww = self.w * scale_f; + let wh = self.h * scale_f; + let title_h = 20.0 * scale_f; + + let mut dx = (self.render_x - self.x) * scale_f; + let mut dy = (self.render_y - self.y) * scale_f; + let max_s = 15.0 * scale_f; + dx = dx.clamp(-max_s, max_s); + dy = dy.clamp(-max_s, max_s); + + unsafe { + // Draw Title Bar + renderer.set_color(theme.border.with_alpha(1.0)); + renderer.draw_rect( + wx as i32 - 1, + wy as i32 - 1, + ww as i32 + 2, + title_h as i32 + 2, + ); + + renderer.set_color(theme.title_bg); + renderer.draw_rect(wx as i32, wy as i32, ww as i32, title_h as i32); + + let t_width = get_text_width(&self.title, scale_f as i32); + let text_x = wx as i32 + (ww as i32 - t_width) / 2; + let text_y = wy as i32 + (title_h as i32 - (7 * scale_f as i32)) / 2; + + crate::graphic::font::draw_text( + renderer, + &self.title, + text_x, + text_y, + theme.text_accent.with_alpha(1.0), + scale_f as i32, + ); + + // Draw Body (Veil) + let body_top_y = wy + title_h; + let body_h = wh - title_h; + + let tl_x = wx; + let tl_y = body_top_y; + let tr_x = wx + ww; + let tr_y = body_top_y; + let bl_x = wx + dx; + let bl_y = body_top_y + body_h + dy; + let br_x = wx + ww + dx; + let br_y = body_top_y + body_h + dy; + + renderer.set_color(theme.border.with_alpha(1.0)); + renderer.draw_quad( + tl_x - 1.0, + tl_y, + tr_x + 1.0, + tr_y, + bl_x - 1.0, + bl_y + 1.0, + br_x + 1.0, + br_y + 1.0, + ); + + renderer.set_color(theme.window_bg); + renderer.draw_quad(tl_x, tl_y, tr_x, tr_y, bl_x, bl_y, br_x, br_y); + + // Enable scissor bounding explicitly + let sx = tl_x.min(bl_x) as i32; + let sy = tl_y.min(bl_y) as i32; + let sw = (ww + dx.abs()) as i32; + let sh = (body_h + dy.max(0.0)) as i32; + + renderer.enable_scissor(sx, sy, sw, sh); + } + + // Draw children + for child in &mut self.children { + child.draw(renderer, theme, scale_f); + } + + unsafe { + renderer.disable_scissor(); + } + } +} From a8cc0cdaf4f8802d1e7c67086d4fb71eb75370aa Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Sat, 21 Feb 2026 12:39:58 +0100 Subject: [PATCH 07/16] Switch to internal GUI rendering with egui --- Cargo.lock | 55 +----- client/Cargo.toml | 6 +- client/src/client.rs | 9 - client/src/graphic/gui.rs | 24 +++ client/src/graphic/hook.rs | 15 +- client/src/graphic/hud.rs | 59 ++++++ client/src/graphic/menu.rs | 245 ++++++++++++++++++++++++ client/src/graphic/mod.rs | 4 + client/src/graphic/ui_engine.rs | 154 +++++++++++++++ client/src/graphic/ui_manager.rs | 24 ++- client/src/gui.rs | 311 ------------------------------- client/src/lib.rs | 38 +--- 12 files changed, 522 insertions(+), 422 deletions(-) create mode 100644 client/src/graphic/gui.rs create mode 100644 client/src/graphic/hud.rs create mode 100644 client/src/graphic/menu.rs create mode 100644 client/src/graphic/ui_engine.rs delete mode 100644 client/src/gui.rs diff --git a/Cargo.lock b/Cargo.lock index 5144826..1b2682b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,12 +216,6 @@ dependencies = [ "x11rb", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.6" @@ -667,9 +661,10 @@ version = "0.1.0" dependencies = [ "anyhow", "cfg-if", - "eframe", "egui", + "egui_glow", "gl_generator", + "glow", "ilhook", "jni", "lazy_static", @@ -679,7 +674,6 @@ dependencies = [ "serde", "serde_json", "simplelog", - "winit", ] [[package]] @@ -2777,19 +2771,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit", - "tiny-skia", -] - [[package]] name = "serde" version = "1.0.210" @@ -2991,12 +2972,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "911ece10388afa48417f99e01df038460b6249a3ee0255f6446a6881b702fbb4" -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" - [[package]] name = "syn" version = "1.0.109" @@ -3128,31 +3103,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "tinyvec" version = "1.8.0" @@ -4064,7 +4014,6 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.37", - "sctk-adwaita", "smithay-client-toolkit", "smol_str", "tracing", diff --git a/client/Cargo.toml b/client/Cargo.toml index 77ca2ab..257eb26 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,8 +9,10 @@ crate-type = ["cdylib"] [dependencies] egui.workspace = true -eframe.workspace = true -winit = "0.30" +#eframe.workspace = true +egui_glow = "0.29.0" +glow = "0.14.0" +#winit = "0.30" log.workspace = true simplelog.workspace = true jni = "0.21.1" diff --git a/client/src/client.rs b/client/src/client.rs index d1786c0..ed03887 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -92,12 +92,3 @@ impl DarkClient { } } } - -// Module for handling keyboard inputs -pub mod keyboard { - pub fn start_keyboard_handler() { - // Keyboard handling is now natively event-driven via GLFW inside graphic::input::my_key_callback - } - - pub fn stop_keyboard_handler() {} -} diff --git a/client/src/graphic/gui.rs b/client/src/graphic/gui.rs new file mode 100644 index 0000000..3583486 --- /dev/null +++ b/client/src/graphic/gui.rs @@ -0,0 +1,24 @@ +use egui::{Color32, Context, Rounding, Stroke, Style, Visuals}; +use crate::graphic::{hud, menu}; + +pub fn render_all(ctx: &Context) { + // 1. Applichiamo il tuo "Theme" direttamente ai Visuals di Egui + let mut style = Style::default(); + style.visuals = Visuals::dark(); + style.visuals.window_fill = Color32::from_rgba_unmultiplied(25, 25, 25, 240); // window_bg + style.visuals.window_stroke = Stroke::new(1.0, Color32::from_rgb(128, 0, 255)); // border viola + style.visuals.window_rounding = Rounding::same(8.0); + style.visuals.panel_fill = Color32::TRANSPARENT; + ctx.set_style(style); + + // 2. Disegniamo l'HUD (sempre visibile) + hud::draw(ctx); + + // 3. Disegniamo il ClickGUI (solo se aperto) + let is_open = crate::graphic::input::GUI_OPEN.load(std::sync::atomic::Ordering::Relaxed); + let anim_progress = ctx.animate_bool(egui::Id::new("menu_open_anim"), is_open); + + if anim_progress > 0.0 { + menu::draw(ctx, anim_progress); + } +} \ No newline at end of file diff --git a/client/src/graphic/hook.rs b/client/src/graphic/hook.rs index 09be4c0..69b7e6c 100644 --- a/client/src/graphic/hook.rs +++ b/client/src/graphic/hook.rs @@ -37,7 +37,7 @@ cfg_if! { use libc::c_void; // Helper to load OpenGL functions on Linux - fn get_proc_address(addr: &str) -> *const c_void { + pub fn get_proc_address(addr: &str) -> *const c_void { unsafe { let s = CString::new(addr).unwrap(); // Try first with glXGetProcAddress if available, otherwise dlsym @@ -120,12 +120,7 @@ use crate::graphic::render::Renderer; // === RENDERING LOGIC === unsafe fn render_overlay() { - // Skip if we are rendering on the Egui window - if crate::gui::IS_EGUI_THREAD.with(|f| f.get()) { - return; - } - - if crate::mapping::client::minecraft::Minecraft::instance() + if Minecraft::instance() .get_player() .is_err() { @@ -136,10 +131,12 @@ unsafe fn render_overlay() { crate::graphic::input::init(); // Initialize the renderer, which backs up OpenGL state - let mut renderer = Renderer::new(); + //let mut renderer = Renderer::new(); // Call our new custom OpenGL UI system - crate::graphic::ui::render_gui(&mut renderer); + //crate::graphic::ui::render_gui(&mut renderer); + + crate::graphic::ui_engine::render_egui_ui(); // the state is restored automatically when `renderer` goes out of scope and drops } diff --git a/client/src/graphic/hud.rs b/client/src/graphic/hud.rs new file mode 100644 index 0000000..d202b70 --- /dev/null +++ b/client/src/graphic/hud.rs @@ -0,0 +1,59 @@ +use crate::client::DarkClient; +use egui::{Align2, Color32, Context, Id, RichText}; + +pub fn draw(ctx: &Context) { + // --- WATERMARK (In alto a sinistra) --- + egui::Area::new(Id::new("hud_watermark")) + .fixed_pos(egui::pos2(5.0, 5.0)) + .interactable(false) // Non blocca i click + .show(ctx, |ui| { + ui.add( + egui::Label::new( + RichText::new("DarkClient").color(Color32::YELLOW).size(24.0).strong() + ) + .wrap_mode(egui::TextWrapMode::Extend) + ); + }); + + // --- ARRAYLIST / MODULI ATTIVI (Sotto il watermark) --- + egui::Area::new(Id::new("hud_arraylist")) + .fixed_pos(egui::pos2(5.0, 35.0)) + .interactable(false) + .show(ctx, |ui| { + if let Ok(modules_map) = DarkClient::instance().modules.read() { + let mut active_mods: Vec = modules_map + .values() + .filter_map(|m| { + let lock = m.lock().unwrap(); + if lock.get_module_data().enabled { + Some(lock.get_module_data().name.clone()) + } else { + None + } + }) + .collect(); + + active_mods.sort_by(|a, b| b.len().cmp(&a.len())); + + let colors = [ + Color32::from_rgb(153, 51, 204), // Purple + Color32::from_rgb(51, 204, 230), // Cyan + Color32::GREEN, + Color32::RED, + Color32::YELLOW, + Color32::from_rgb(51, 102, 255), // Blue + Color32::WHITE, + ]; + + for (i, mod_name) in active_mods.iter().enumerate() { + let color = colors[i % colors.len()]; + ui.add( + egui::Label::new( + RichText::new(mod_name).color(color).size(16.0) + ) + .wrap_mode(egui::TextWrapMode::Extend) // <--- Disabilita il word-wrap! + ); + } + } + }); +} \ No newline at end of file diff --git a/client/src/graphic/menu.rs b/client/src/graphic/menu.rs new file mode 100644 index 0000000..c8c7974 --- /dev/null +++ b/client/src/graphic/menu.rs @@ -0,0 +1,245 @@ +use crate::client::DarkClient; +use crate::module::{ModuleCategory, ModuleSetting}; +use egui::{Align2, Color32, Context, Id, Pos2, Rect, Rounding, Sense, Stroke, Vec2}; + +pub fn draw(ctx: &Context, anim_progress: f32) { + // 1. Sfondo scuro semitrasparente + egui::Area::new(Id::new("dark_overlay")) + .fixed_pos(Pos2::ZERO) + .order(egui::Order::Background) + .interactable(false) + .show(ctx, |ui| { + ui.painter().rect_filled( + ui.ctx().screen_rect(), + 0.0, + Color32::from_black_alpha((180.0 * anim_progress) as u8), + ); + }); + + // 2. Pulsanti Globali (In alto a destra) + egui::Area::new(Id::new("global_buttons")) + .anchor(Align2::RIGHT_TOP, Vec2::new(-10.0, 10.0)) + .show(ctx, |ui| { + ui.set_opacity(anim_progress); + ui.horizontal(|ui| { + if ui.button(egui::RichText::new("PANIC").color(Color32::RED)).clicked() { + std::thread::spawn(|| crate::graphic::ui_manager::call_panic()); + } + if ui.button("Reset UI").clicked() { + // Egui salva le posizioni in memoria. Per resettarle: + ctx.memory_mut(|mem| mem.reset_areas()); + } + }); + }); + + // 3. Finestre per ogni Categoria + let client_modules_guard = DarkClient::instance().modules.read().unwrap(); + + let categories = [ + ModuleCategory::COMBAT, + ModuleCategory::MOVEMENT, + ModuleCategory::RENDER, + ModuleCategory::PLAYER, + ModuleCategory::WORLD, + ]; + + let logical_width = ctx.screen_rect().width(); + + let mut curr_x = 50.0; + let mut curr_y = 50.0; + let win_w = 160.0; + let gap_x = 20.0; + let row_height = 280.0; + + for category in categories.iter() { + // Se la finestra successiva sfora lo schermo, scendiamo di una "riga" + if curr_x + win_w > logical_width && curr_x > 50.0 { + curr_x = 50.0; + curr_y += row_height; // Altezza stimata per evitare che si tocchino scendendo + } + let title = category.display_name(); + + // Offset iniziale animato + let y_offset = 20.0 * (1.0 - anim_progress); + let start_pos = Pos2::new(curr_x, curr_y + y_offset); + + egui::Window::new(title) + .id(Id::new(title)) + .default_pos(start_pos) + .default_width(win_w) + .min_width(win_w) + .max_width(win_w) + .resizable(false) + .collapsible(true) + .show(ctx, |ui| { + ui.set_opacity(anim_progress); + + // Filtriamo i moduli per questa finestra + let mut cat_modules: Vec<_> = client_modules_guard + .values() + .filter(|m| m.lock().unwrap().get_module_data().category == *category) + .collect(); + cat_modules.sort_by(|a, b| { + a.lock().unwrap().get_module_data().name.cmp(&b.lock().unwrap().get_module_data().name) + }); + + for module in cat_modules { + let (mod_name, is_enabled) = { + let mut lock = module.lock().unwrap(); + let data = lock.get_module_data_mut(); + let mod_name = data.name.clone(); + let is_enabled = data.enabled; + (mod_name, is_enabled) + }; + + // Creiamo una riga custom per gestire click SINISTRO (Toggle) e DESTRO (Espandi) + let (rect, response) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 20.0), Sense::click()); + + // Gestione Colori Background in base a hover + let bg_color = if response.hovered() { + Color32::from_rgb(64, 64, 64) + } else { + Color32::from_rgb(38, 38, 38) + }; + ui.painter().rect_filled(rect, Rounding::same(4.0), bg_color); + + // Testo Modulo + let text_color = if is_enabled { Color32::YELLOW } else { Color32::WHITE }; + let text_pos = rect.min + Vec2::new(5.0, 3.0); + ui.painter().text( + text_pos, + egui::Align2::LEFT_TOP, + &mod_name, + egui::FontId::proportional(14.0), + text_color, + ); + + let mut lock = module.lock().unwrap(); + + let is_expanded_id = Id::new(&mod_name).with("expanded"); + let mut is_expanded = ui.data(|d| d.get_temp::(is_expanded_id).unwrap_or(false)); + + // Definiamo un'area immaginaria di 25x20 pixel sull'estrema destra del rettangolo + let arrow_rect = Rect::from_min_max( + rect.max - Vec2::new(25.0, 20.0), + rect.max, + ); + + // Gestione avanzata del Click + if response.clicked() { + // Prendiamo le coordinate esatte del click + let click_pos = response.interact_pointer_pos().unwrap_or(Pos2::ZERO); + + // CASO 1: Click Destro, OPPURE Click Sinistro proprio sopra la freccetta + if response.clicked_by(egui::PointerButton::Secondary) || + (response.clicked_by(egui::PointerButton::Primary) && arrow_rect.contains(click_pos)) { + + is_expanded = !is_expanded; + ui.data_mut(|d| d.insert_temp(is_expanded_id, is_expanded)); + + // CASO 2: Click Sinistro sul resto del corpo del bottone + } else if response.clicked_by(egui::PointerButton::Primary) { + let new_state = !is_enabled; + lock.get_module_data_mut().set_enabled(new_state); + if new_state { let _ = lock.on_start(); } else { let _ = lock.on_stop(); } + } + } + + let data = lock.get_module_data_mut(); + + // Se ha settaggi, gestiamo l'espansione + if !data.settings.is_empty() { + let arrow = if is_expanded { "v" } else { ">" }; + + // Per feedback visivo, se il mouse è esattamente sopra l'area della freccia, la illuminiamo + let arrow_color = if arrow_rect.contains(ui.ctx().pointer_hover_pos().unwrap_or(Pos2::ZERO)) { + Color32::WHITE + } else { + Color32::GRAY + }; + + ui.painter().text( + rect.max - Vec2::new(15.0, 17.0), + egui::Align2::LEFT_TOP, + arrow, + egui::FontId::proportional(14.0), + arrow_color, + ); + + // Se è espanso, mostriamo i settaggi usando i widget nativi di egui + if is_expanded { + ui.horizontal(|ui| { + ui.add_space(10.0); // Indentazione + ui.vertical(|ui| { + // 1. Diciamo agli slider di essere larghi solo 60 pixel + ui.style_mut().spacing.slider_width = 60.0; + + // 2. Se un testo è troppo lungo, lo tagliamo coi puntini (...) anziché allargare la tab + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + + // 3. Assicuriamoci che i combobox non esplodano in larghezza + ui.style_mut().spacing.interact_size.x = 80.0; + + for setting in &mut data.settings { + match setting { + ModuleSetting::Toggle { name, value } => { + ui.checkbox(value, name.as_str()); + } + ModuleSetting::Slider { name, value, min, max } => { + ui.vertical(|ui| { + // Riga 1: Nome a sinistra, Valore a destra + ui.horizontal(|ui| { + ui.label(name.as_str()); + + // Spinge il valore tutto a destra + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Formattiamo a 2 decimali per evitare numeri infiniti + ui.label(format!("{:.2}", value)); + }); + }); + + // Riga 2: Lo slider vero e proprio + // Diciamo allo slider di prendere tutta la larghezza rimasta e nascondiamo + // il numerino di default (visto che l'abbiamo appena disegnato noi sopra) + let available_w = ui.available_width(); + ui.style_mut().spacing.slider_width = available_w; + + ui.add( + egui::Slider::new(value, min.clone()..=max.clone()) + .show_value(false) // Nasconde il testo del valore integrato + .text("") // Rimuove la label integrata + ); + }); + } + ModuleSetting::Choice { name, value, options } => { + ui.vertical(|ui| { + ui.label(name.as_str()); // Nome sopra + + // Combobox largo tutto lo spazio sotto + egui::ComboBox::from_id_salt(name.as_str()) // id_source invece di label per non mettere il testo a lato + .width(ui.available_width()) + .selected_text(options.get(*value).map(|s| s.as_str()).unwrap_or("??")) + .show_ui(ui, |ui| { + for (idx, opt) in options.iter().enumerate() { + ui.selectable_value(value, idx, opt.as_str()); + } + }); + }); + } + ModuleSetting::Color { name, .. } => { + ui.label(format!("{}: [Color Settings soon]", name)); + } + } + } + ui.add_space(5.0); + }); + }); + } + } + } + }); + + // Avanziamo verso destra per la prossima categoria + curr_x += win_w + gap_x; + } +} \ No newline at end of file diff --git a/client/src/graphic/mod.rs b/client/src/graphic/mod.rs index f1999e1..3ebf0c8 100644 --- a/client/src/graphic/mod.rs +++ b/client/src/graphic/mod.rs @@ -4,5 +4,9 @@ pub mod hook; pub mod input; pub mod render; pub mod ui; +pub mod ui_engine; pub mod ui_manager; pub mod widget; +mod gui; +mod hud; +mod menu; diff --git a/client/src/graphic/ui_engine.rs b/client/src/graphic/ui_engine.rs new file mode 100644 index 0000000..04f234a --- /dev/null +++ b/client/src/graphic/ui_engine.rs @@ -0,0 +1,154 @@ +use std::sync::atomic::Ordering; +use std::sync::Mutex; +use egui::Context; +use egui_glow::Painter; +use lazy_static::lazy_static; +use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; + +pub struct EguiState { + pub ctx: Context, + pub painter: Painter, + pub last_left_down: bool, + pub last_right_down: bool, + pub last_mouse_pos: egui::Pos2, + pub window_pos: egui::Pos2, +} + +lazy_static! { + pub static ref EGUI_STATE: Mutex> = Mutex::new(None); +} + +pub fn gather_egui_inputs(state: &mut EguiState, screen_width: f32, screen_height: f32, scale_factor: f32) -> egui::RawInput { + let mut raw_input = egui::RawInput::default(); + // 1. Configura il Viewport principale con il nostro moltiplicatore di scala + let mut viewport_info = egui::ViewportInfo::default(); + viewport_info.native_pixels_per_point = Some(scale_factor); + + // Inseriamo le info nella mappa usando l'ID di default (ROOT) + raw_input.viewports.insert(egui::ViewportId::ROOT, viewport_info); + + // 2. Passiamo le coordinate LOGICHE (divise per la scala) + let logical_width = screen_width / scale_factor; + let logical_height = screen_height / scale_factor; + + raw_input.screen_rect = Some(egui::Rect::from_min_max( + egui::pos2(0.0, 0.0), + egui::pos2(logical_width, logical_height), + )); + + // Se la GUI è chiusa, restituiamo input vuoti in modo che egui non interagisca + if !GUI_OPEN.load(Ordering::Relaxed) { + return raw_input; + } + + let mouse = MOUSE_STATE.lock().unwrap(); + let current_pos = egui::pos2( + (mouse.x as f32) / scale_factor, + (mouse.y as f32) / scale_factor + ); + + // 1. Movimento del Mouse + if current_pos != state.last_mouse_pos { + raw_input.events.push(egui::Event::PointerMoved(current_pos)); + state.last_mouse_pos = current_pos; + } + + // 2. Click Sinistro (Transizione Su/Giù) + if mouse.left_down != state.last_left_down { + raw_input.events.push(egui::Event::PointerButton { + pos: current_pos, + button: egui::PointerButton::Primary, + pressed: mouse.left_down, // true = appena premuto, false = appena rilasciato + modifiers: Default::default(), + }); + state.last_left_down = mouse.left_down; + } + + // 3. Click Destro (Transizione Su/Giù) + if mouse.right_down != state.last_right_down { + raw_input.events.push(egui::Event::PointerButton { + pos: current_pos, + button: egui::PointerButton::Secondary, + pressed: mouse.right_down, + modifiers: Default::default(), + }); + state.last_right_down = mouse.right_down; + } + + raw_input +} + +pub unsafe fn render_egui_ui() { + let mut viewport = [0; 4]; + crate::gl::GetIntegerv(crate::gl::VIEWPORT, viewport.as_mut_ptr()); + let screen_width = viewport[2] as f32; + let screen_height = viewport[3] as f32; + + let scale_factor = (screen_height / 720.0).max(1.0).floor(); + + let mut state_guard = EGUI_STATE.lock().unwrap(); + + // 1. Inizializzazione Aggiornata + if state_guard.is_none() { + let gl = glow::Context::from_loader_function(|s| { + crate::graphic::hook::get_proc_address(s) as *const _ + }); + let gl = std::sync::Arc::new(gl); + let ctx = egui::Context::default(); + + // CORREZIONE 1: Aggiunto `false` per il dithering finale + let painter = egui_glow::Painter::new(gl, "", None, false).unwrap(); + + *state_guard = Some(EguiState { + ctx, + painter, + last_left_down: false, + last_right_down: false, + last_mouse_pos: egui::pos2(0.0, 0.0), + // Inizializziamo la finestra al centro dello schermo + window_pos: egui::pos2(screen_width / 2.0 - 200.0, screen_height / 2.0 - 150.0), + }); + } + + let state = state_guard.as_mut().unwrap(); + let raw_input = gather_egui_inputs(state, screen_width, screen_height, scale_factor); + + let is_open = GUI_OPEN.load(Ordering::Relaxed); + + // Cloniamo il Context. È leggerissimo (usa Arc internamente) e ci permette + // di modificare `state` (come state.window_pos) dentro la closure `run`. + let ctx = state.ctx.clone(); + + // CORREZIONE 2: ctx.run invece di begin_frame / end_frame + let full_output = ctx.run(raw_input, |ctx| { + crate::graphic::gui::render_all(ctx); + }); // Fine di ctx.run + + // Il resto del rendering OpenGL rimane identico, full_output ora viene da ctx.run + let clipped_primitives = state.ctx.tessellate(full_output.shapes, full_output.pixels_per_point); + + // --- INIZIO FIX GEROGLIFICI --- + // Resettiamo lo stato di unpack dei pixel che Minecraft spesso corrompe + // prima di far caricare le texture dei font a egui + unsafe { + crate::gl::ActiveTexture(crate::gl::TEXTURE0); + crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); // Egui preferisce l'allineamento a 1 byte + crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); // Questo è il colpevole principale al 99%! + crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_PIXELS, 0); + crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_ROWS, 0); + + // Egui_glow salva e ripristina lo stato di blend e depth, ma per sicurezza: + crate::gl::Disable(crate::gl::CULL_FACE); + crate::gl::Disable(crate::gl::DEPTH_TEST); + crate::gl::Enable(crate::gl::BLEND); + } + // --- FINE FIX --- + + // Ora disegniamo in sicurezza + state.painter.paint_and_update_textures( + [screen_width as u32, screen_height as u32], + full_output.pixels_per_point, + &clipped_primitives, + &full_output.textures_delta, + ); +} diff --git a/client/src/graphic/ui_manager.rs b/client/src/graphic/ui_manager.rs index f228db0..41af26e 100644 --- a/client/src/graphic/ui_manager.rs +++ b/client/src/graphic/ui_manager.rs @@ -1,3 +1,4 @@ +use crate::cleanup_client; use crate::client::DarkClient; use crate::graphic::color::Rgba; use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; @@ -274,8 +275,29 @@ impl UiManager { .panic_btn .handle_click(mx, my, left_clicked, right_clicked, scale_f) { - std::thread::spawn(|| crate::gui::call_panic()); + std::thread::spawn(|| call_panic()); } } } } + +pub fn call_panic() { + let client = DarkClient::instance(); + client.modules.read().unwrap().values().for_each(|module| { + let mut module = module.lock().unwrap(); + if module.get_module_data().enabled { + module.get_module_data_mut().set_enabled(false); + match module.on_stop() { + Ok(_) => {} + Err(e) => { + log::error!( + "Failed to stop module {} on panic: {}", + module.get_module_data().name, + e + ); + } + } + } + }); + cleanup_client(); +} diff --git a/client/src/gui.rs b/client/src/gui.rs deleted file mode 100644 index da3cabc..0000000 --- a/client/src/gui.rs +++ /dev/null @@ -1,311 +0,0 @@ -use crate::client::DarkClient; -use crate::module::{ModuleCategory, ModuleSetting}; -use crate::{cleanup_client, RUNNING}; -use eframe::Frame; -use egui::{Context, ScrollArea, Ui}; -use std::cell::Cell; -use std::sync::atomic::Ordering::Relaxed; -#[cfg(target_os = "linux")] -use winit::platform::x11::EventLoopBuilderExtX11; - -thread_local! { - pub static IS_EGUI_THREAD: Cell = Cell::new(false); -} - -pub fn call_panic() { - let client = DarkClient::instance(); - client.modules.read().unwrap().values().for_each(|module| { - let mut module = module.lock().unwrap(); - if module.get_module_data().enabled { - module.get_module_data_mut().set_enabled(false); - match module.on_stop() { - Ok(_) => {} - Err(e) => { - log::error!( - "Failed to stop module {} on panic: {}", - module.get_module_data().name, - e - ); - } - } - } - }); - cleanup_client(); -} - -pub fn start_gui() -> anyhow::Result<()> { - let mut native_options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_inner_size([800.0, 600.0]) - .with_min_inner_size([700.0, 500.0]), - run_and_return: true, - ..Default::default() - }; - - #[cfg(target_os = "linux")] - { - native_options.event_loop_builder = Some(Box::new(|builder| { - builder.with_x11().with_any_thread(true); - })); - } - - match eframe::run_native( - "DarkClient Injector", - native_options, - Box::new(|_| Ok(Box::new(GUI::default()))), - ) { - Ok(_) => Ok(()), - Err(e) => Err(anyhow::anyhow!("Failed to run the GUI, {}", e)), - } -} - -pub struct GUI { - selected_category: ModuleCategory, -} - -impl Default for GUI { - fn default() -> Self { - Self { - selected_category: ModuleCategory::COMBAT, - } - } -} - -impl eframe::App for GUI { - fn update(&mut self, ctx: &Context, _frame: &mut Frame) { - crate::gui::IS_EGUI_THREAD.with(|f| f.set(true)); - - ctx.request_repaint(); - - if !RUNNING.load(Relaxed) { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("DarkClient"); - ui.separator(); - - ui.horizontal(|ui| { - ui.label("Status:"); - ui.colored_label(egui::Color32::GREEN, "Injected"); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("Panic").clicked() { - std::thread::spawn(|| call_panic()); - } - }); - }); - - ui.add_space(10.0); - - // Category selection - ui.horizontal(|ui| { - ui.label("Category:"); - if ui - .selectable_label(self.selected_category == ModuleCategory::COMBAT, "⚔ Combat") - .clicked() - { - self.selected_category = ModuleCategory::COMBAT; - } - if ui - .selectable_label( - self.selected_category == ModuleCategory::MOVEMENT, - "🏃 Movement", - ) - .clicked() - { - self.selected_category = ModuleCategory::MOVEMENT; - } - if ui - .selectable_label(self.selected_category == ModuleCategory::RENDER, "👁 Render") - .clicked() - { - self.selected_category = ModuleCategory::RENDER; - } - if ui - .selectable_label( - self.selected_category == ModuleCategory::PLAYER, - "🧍 Player", - ) - .clicked() - { - self.selected_category = ModuleCategory::PLAYER; - } - if ui - .selectable_label(self.selected_category == ModuleCategory::WORLD, "🌍 World") - .clicked() - { - self.selected_category = ModuleCategory::WORLD; - } - if ui - .selectable_label(self.selected_category == ModuleCategory::MISC, "🔧 Misc") - .clicked() - { - self.selected_category = ModuleCategory::MISC; - } - }); - - ui.separator(); - - // Modules list - ScrollArea::vertical().show(ui, |ui| { - self.render_modules(ui); - }); - }); - } -} - -impl GUI { - fn render_modules(&mut self, ui: &mut Ui) { - let client = DarkClient::instance(); - let modules = client.modules.read().unwrap(); - - let mut modules_in_category: Vec<_> = modules - .iter() - .filter(|(_, module)| { - module.lock().unwrap().get_module_data().category == self.selected_category - }) - .collect(); - - modules_in_category.sort_by(|a, b| { - a.1.lock() - .unwrap() - .get_module_data() - .name - .cmp(&b.1.lock().unwrap().get_module_data().name) - }); - - if modules_in_category.is_empty() { - ui.label("No modules in this category"); - return; - } - - for (_, module) in modules_in_category { - let mut module = module.lock().unwrap(); - - ui.group(|ui| { - ui.horizontal(|ui| { - let mut enabled = module.get_module_data().enabled; - if ui.checkbox(&mut enabled, "").changed() { - if enabled { - match module.on_start() { - Ok(_) => { - module.get_module_data_mut().set_enabled(true); - } - Err(e) => { - log::error!("Failed to start module: {}", e); - } - } - } else { - match module.on_stop() { - Ok(_) => { - module.get_module_data_mut().set_enabled(false); - } - Err(e) => { - log::error!("Failed to stop module: {}", e); - } - } - } - } - - let module_data = module.get_module_data(); - ui.vertical(|ui| { - ui.strong(&module_data.name); - ui.label(&module_data.description); - ui.label(format!("Keybind: {:?}", module_data.key_bind)); - }); - }); - - let module_data = module.get_module_data(); - // Render module settings - if module_data.enabled { - ui.separator(); - self.render_module_settings(ui, &mut **module); - } - }); - - ui.add_space(5.0); - } - } - - fn render_module_settings(&mut self, ui: &mut Ui, module: &mut dyn crate::module::Module) { - let module_data = module.get_module_data_mut(); - - if module_data.settings.is_empty() { - return; - } - - ui.label("⚙ Settings:"); - ui.indent("settings", |ui| { - let settings_len = module_data.settings.len(); - for i in 0..settings_len { - let setting = &mut module_data.settings[i]; - - match setting { - ModuleSetting::Slider { - name, - value, - min, - max, - } => { - ui.horizontal(|ui| { - ui.label(name.as_str()); - let mut temp_value = *value; - if ui - .add( - egui::Slider::new(&mut temp_value, *min..=*max) - .fixed_decimals(1), - ) - .changed() - { - *value = temp_value; - } - }); - } - ModuleSetting::Toggle { name, value } => { - ui.horizontal(|ui| { - let mut temp_value = *value; - if ui.checkbox(&mut temp_value, name.as_str()).changed() { - *value = temp_value; - } - }); - } - ModuleSetting::Choice { - name, - value, - options, - } => { - ui.horizontal(|ui| { - ui.label(name.as_str()); - egui::ComboBox::from_id_salt(format!("choice_{}", name)) - .selected_text(&options[*value]) - .show_ui(ui, |ui| { - for (idx, option) in options.iter().enumerate() { - ui.selectable_value(value, idx, option); - } - }); - }); - } - ModuleSetting::Color { name, value } => { - ui.horizontal(|ui| { - ui.label(name.as_str()); - let mut color = egui::Color32::from_rgba_unmultiplied( - (value[0] * 255.0) as u8, - (value[1] * 255.0) as u8, - (value[2] * 255.0) as u8, - (value[3] * 255.0) as u8, - ); - if ui.color_edit_button_srgba(&mut color).changed() { - let rgba = color.to_srgba_unmultiplied(); - value[0] = rgba[0] as f32 / 255.0; - value[1] = rgba[1] as f32 / 255.0; - value[2] = rgba[2] as f32 / 255.0; - value[3] = rgba[3] as f32 / 255.0; - } - }); - } - } - } - }); - } -} diff --git a/client/src/lib.rs b/client/src/lib.rs index a6628a5..9233b01 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -3,7 +3,6 @@ extern crate jni; mod client; mod graphic; -mod gui; mod mapping; mod module; @@ -11,10 +10,8 @@ pub mod gl { include!(concat!(env!("OUT_DIR"), "/bindings.rs")); } -use crate::client::keyboard::{start_keyboard_handler, stop_keyboard_handler}; use crate::client::DarkClient; use crate::graphic::hook::{install_hooks, uninstall_hooks}; -use crate::gui::start_gui; use crate::mapping::client::minecraft::Minecraft; use crate::module::combat::mobaura::MobAuraModule; use log::{error, info, LevelFilter}; @@ -24,18 +21,11 @@ use module::movement::fly::FlyModule; use simplelog::{Config, WriteLogger}; use std::fs::File; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Mutex, OnceLock}; use std::thread; -static GUI_THREAD: OnceLock>>> = OnceLock::new(); - // Flag to control if the client is running pub static RUNNING: AtomicBool = AtomicBool::new(false); -fn gui_thread() -> &'static Mutex>> { - GUI_THREAD.get_or_init(|| Mutex::new(None)) -} - #[no_mangle] pub extern "C" fn initialize_client() { // Make sure we can't initialize more than once @@ -68,22 +58,11 @@ pub extern "C" fn initialize_client() { register_modules(); - start_keyboard_handler(); - // Install hooks if let Err(e) = install_hooks() { error!("Failed to install hooks: {}", e); } - let gui_handle = thread::spawn(move || match start_gui() { - Ok(_) => info!("GUI thread started"), - Err(e) => error!("Error while starting GUI thread: {}", e), - }); - - // Memorize the thread handle in a thread-safe way - let mut gui_lock = gui_thread().lock().unwrap(); - *gui_lock = Some(gui_handle); - match minecraft.get_player() { Ok(player) => { if let Ok(pos) = player.entity.get_position() { @@ -103,27 +82,12 @@ pub extern "C" fn cleanup_client() { // Set the execution flag to false RUNNING.store(false, Ordering::SeqCst); - // RIMUOVI FISICAMENTE L'HOOK + // Remove the hooks uninstall_hooks(); - // Stop the keyboard handler - stop_keyboard_handler(); - // Unlock GLFW Input / Restore callbacks if GUI was open crate::graphic::input::cleanup(); - let gui_handle = { - let mut gui_lock = gui_thread().lock().unwrap(); - gui_lock.take() - }; - - if let Some(handle) = gui_handle { - // Give a short timeout for waiting - if let Err(e) = handle.join() { - error!("Error while waiting for gui thread: {:?}", e); - } - } - // Clean up other resources if necessary info!("Client cleanup completed"); } From 46d2774bbe7a34ec3f0d6efbe3bb28617c033240 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Sat, 21 Feb 2026 12:47:13 +0100 Subject: [PATCH 08/16] Modify style --- client/src/graphic/gui.rs | 48 ++++++++++++++++++++++++++++++++------ client/src/graphic/menu.rs | 24 ++++++++++++------- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/client/src/graphic/gui.rs b/client/src/graphic/gui.rs index 3583486..cb4e14d 100644 --- a/client/src/graphic/gui.rs +++ b/client/src/graphic/gui.rs @@ -1,14 +1,48 @@ -use egui::{Color32, Context, Rounding, Stroke, Style, Visuals}; use crate::graphic::{hud, menu}; +use egui::{Color32, Context, Margin, Rounding, Stroke, Style, Vec2, Visuals}; pub fn render_all(ctx: &Context) { // 1. Applichiamo il tuo "Theme" direttamente ai Visuals di Egui let mut style = Style::default(); - style.visuals = Visuals::dark(); - style.visuals.window_fill = Color32::from_rgba_unmultiplied(25, 25, 25, 240); // window_bg - style.visuals.window_stroke = Stroke::new(1.0, Color32::from_rgb(128, 0, 255)); // border viola - style.visuals.window_rounding = Rounding::same(8.0); - style.visuals.panel_fill = Color32::TRANSPARENT; + let mut visuals = Visuals::dark(); + // 1. Colori Base (Scuri e Piatti) + let bg_color = Color32::from_rgb(22, 22, 22); // Grigio scurissimo (quasi nero) + let panel_color = Color32::from_rgb(30, 30, 30); // Sfondo dei moduli + let accent_color = Color32::from_rgb(26, 171, 138); // Il classico Verde Acqua (Teal) di Vape + let border_color = Color32::from_rgb(45, 45, 45); // Bordino sottile + + // Sfondi Finestre + visuals.window_fill = bg_color; + visuals.panel_fill = panel_color; + visuals.window_stroke = Stroke::new(1.0, border_color); // Bordo sottile di 1px + + // 2. Colori Attivi (Quando uno slider o checkbox è attivo) + visuals.selection.bg_fill = accent_color; + visuals.selection.stroke = Stroke::NONE; + + // 3. Stile Widget (Pulsanti, Background degli Slider) + // Inattivo + visuals.widgets.inactive.bg_fill = Color32::from_rgb(35, 35, 35); + visuals.widgets.inactive.rounding = Rounding::same(2.0); // Leggermente smussato + visuals.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(210, 210, 210)); // Testo chiaro + + // Hover (Passaggio del mouse) + visuals.widgets.hovered.bg_fill = Color32::from_rgb(45, 45, 45); + visuals.widgets.hovered.rounding = Rounding::same(2.0); + visuals.widgets.hovered.fg_stroke = Stroke::new(1.0, Color32::WHITE); + + // Click + visuals.widgets.active.bg_fill = accent_color; + visuals.widgets.active.rounding = Rounding::same(2.0); + + // 4. Arrotondamento Generale (Vape è squadrata) + visuals.window_rounding = Rounding::same(3.0); + style.visuals = visuals; + + // 5. Spaziature (Vape è compattissima) + style.spacing.item_spacing = Vec2::new(8.0, 4.0); // Spazio ridotto tra gli elementi + style.spacing.window_margin = Margin::symmetric(0.0, 0.0); // Rimuove il padding ai bordi della finestra! + style.spacing.button_padding = Vec2::new(4.0, 2.0); ctx.set_style(style); // 2. Disegniamo l'HUD (sempre visibile) @@ -21,4 +55,4 @@ pub fn render_all(ctx: &Context) { if anim_progress > 0.0 { menu::draw(ctx, anim_progress); } -} \ No newline at end of file +} diff --git a/client/src/graphic/menu.rs b/client/src/graphic/menu.rs index c8c7974..a597bce 100644 --- a/client/src/graphic/menu.rs +++ b/client/src/graphic/menu.rs @@ -93,21 +93,24 @@ pub fn draw(ctx: &Context, anim_progress: f32) { }; // Creiamo una riga custom per gestire click SINISTRO (Toggle) e DESTRO (Espandi) - let (rect, response) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 20.0), Sense::click()); + let (rect, response) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 22.0), Sense::click()); // Gestione Colori Background in base a hover let bg_color = if response.hovered() { - Color32::from_rgb(64, 64, 64) + Color32::from_rgb(40, 40, 40) // Illumina un po' all'hover } else { - Color32::from_rgb(38, 38, 38) + Color32::TRANSPARENT // Sfondo invisibile, prende il colore della finestra }; - ui.painter().rect_filled(rect, Rounding::same(4.0), bg_color); + ui.painter().rect_filled(rect, Rounding::ZERO, bg_color); // Testo Modulo - let text_color = if is_enabled { Color32::YELLOW } else { Color32::WHITE }; - let text_pos = rect.min + Vec2::new(5.0, 3.0); + let text_color = if is_enabled { + Color32::from_rgb(26, 171, 138) // Accent Color + } else { + Color32::from_rgb(200, 200, 200) + }; let text_pos = rect.min + Vec2::new(5.0, 3.0); ui.painter().text( - text_pos, + rect.min + egui::vec2(8.0, 4.0), // Padding sinistro egui::Align2::LEFT_TOP, &mod_name, egui::FontId::proportional(14.0), @@ -168,8 +171,11 @@ pub fn draw(ctx: &Context, anim_progress: f32) { // Se è espanso, mostriamo i settaggi usando i widget nativi di egui if is_expanded { - ui.horizontal(|ui| { - ui.add_space(10.0); // Indentazione + let settings_frame = egui::Frame::none() + .fill(Color32::from_rgb(15, 15, 15)) // Più scuro della finestra + .inner_margin(egui::Margin::symmetric(8.0, 6.0)); + + settings_frame.show(ui, |ui| { ui.vertical(|ui| { // 1. Diciamo agli slider di essere larghi solo 60 pixel ui.style_mut().spacing.slider_width = 60.0; From 857e66fde34d67edcb1c9b2234a94d1722ff91b5 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Sat, 21 Feb 2026 12:49:56 +0100 Subject: [PATCH 09/16] Remove custom widget --- client/src/graphic/menu.rs | 9 +- client/src/graphic/mod.rs | 9 +- client/src/graphic/ui.rs | 128 ---------- client/src/graphic/ui_engine.rs | 54 ++++- client/src/graphic/ui_manager.rs | 303 ------------------------ client/src/graphic/widget/button.rs | 111 --------- client/src/graphic/widget/hud.rs | 83 ------- client/src/graphic/widget/label.rs | 60 ----- client/src/graphic/widget/mod.rs | 76 ------ client/src/graphic/widget/module_btn.rs | 267 --------------------- client/src/graphic/widget/panel.rs | 128 ---------- client/src/graphic/widget/slider.rs | 144 ----------- client/src/graphic/widget/toggle.rs | 136 ----------- client/src/graphic/widget/window.rs | 227 ------------------ 14 files changed, 53 insertions(+), 1682 deletions(-) delete mode 100644 client/src/graphic/ui.rs delete mode 100644 client/src/graphic/ui_manager.rs delete mode 100644 client/src/graphic/widget/button.rs delete mode 100644 client/src/graphic/widget/hud.rs delete mode 100644 client/src/graphic/widget/label.rs delete mode 100644 client/src/graphic/widget/mod.rs delete mode 100644 client/src/graphic/widget/module_btn.rs delete mode 100644 client/src/graphic/widget/panel.rs delete mode 100644 client/src/graphic/widget/slider.rs delete mode 100644 client/src/graphic/widget/toggle.rs delete mode 100644 client/src/graphic/widget/window.rs diff --git a/client/src/graphic/menu.rs b/client/src/graphic/menu.rs index a597bce..63bc459 100644 --- a/client/src/graphic/menu.rs +++ b/client/src/graphic/menu.rs @@ -22,8 +22,11 @@ pub fn draw(ctx: &Context, anim_progress: f32) { .show(ctx, |ui| { ui.set_opacity(anim_progress); ui.horizontal(|ui| { - if ui.button(egui::RichText::new("PANIC").color(Color32::RED)).clicked() { - std::thread::spawn(|| crate::graphic::ui_manager::call_panic()); + if ui + .button(egui::RichText::new("PANIC").color(Color32::RED)) + .clicked() + { + std::thread::spawn(|| crate::graphic::ui_engine::call_panic()); } if ui.button("Reset UI").clicked() { // Egui salva le posizioni in memoria. Per resettarle: @@ -248,4 +251,4 @@ pub fn draw(ctx: &Context, anim_progress: f32) { // Avanziamo verso destra per la prossima categoria curr_x += win_w + gap_x; } -} \ No newline at end of file +} diff --git a/client/src/graphic/mod.rs b/client/src/graphic/mod.rs index 3ebf0c8..04e8819 100644 --- a/client/src/graphic/mod.rs +++ b/client/src/graphic/mod.rs @@ -1,12 +1,9 @@ pub mod color; pub mod font; +pub mod gui; pub mod hook; +pub mod hud; pub mod input; +pub mod menu; pub mod render; -pub mod ui; pub mod ui_engine; -pub mod ui_manager; -pub mod widget; -mod gui; -mod hud; -mod menu; diff --git a/client/src/graphic/ui.rs b/client/src/graphic/ui.rs deleted file mode 100644 index b2b319e..0000000 --- a/client/src/graphic/ui.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::graphic::color::{Rgb, Rgba}; -use crate::graphic::input::MOUSE_STATE; -use crate::graphic::render::Renderer; -use crate::graphic::ui_manager::UI_MANAGER; - -// --- GUI CONFIGURATION CONSTANTS --- -pub const GUI_TITLE_HEIGHT: f32 = 20.0; -pub const GUI_MODULE_HEIGHT: f32 = 16.0; -pub const GUI_SETTING_HEIGHT: f32 = 14.0; -pub const GUI_PADDING: f32 = 5.0; -pub const GUI_MAX_STRETCH: f32 = 30.0; - -// Text Scales -pub const TEXT_SCALE_HUD_WATERMARK: f32 = 2.0; -pub const TEXT_SCALE_HUD_MODULES: f32 = 1.4; - -// Layout spacing -pub const HUD_PADDING_Y: f32 = 5.0; -pub const HUD_PADDING_X: f32 = 5.0; -pub const HUD_WATERMARK_MARGIN_BOTTOM: f32 = 24.0; -pub const HUD_MODULES_SPACING: f32 = 16.0; - -/// Defines the colors and sizes for the custom DarkClient GUI. -pub struct Theme { - pub screen_bg: Rgba, - pub window_bg: Rgba, - pub title_bg: Rgba, - pub border: Rgba, - pub text_primary: Rgba, - pub text_accent: Rgba, - pub module_bg: Rgba, - pub module_bg_hover: Rgba, -} - -impl Default for Theme { - fn default() -> Self { - Self { - // Full screen dim overlay - screen_bg: Rgba::new(Rgb::new(0.0, 0.0, 0.0), 0.5), - // Window background - window_bg: Rgba::new(Rgb::new(0.1, 0.1, 0.1), 0.95), - // Title bar - title_bg: Rgba::new(Rgb::new(0.05, 0.05, 0.05), 1.0), - // Accent border - border: Rgba::new(Rgb::new(0.5, 0.0, 1.0), 1.0), - // Texts - text_primary: Rgba::new(Rgb::new(1.0, 1.0, 1.0), 1.0), - text_accent: Rgba::new(Rgb::new(1.0, 1.0, 0.0), 1.0), - // Module rects - module_bg: Rgba::new(Rgb::new(0.15, 0.15, 0.15), 1.0), - module_bg_hover: Rgba::new(Rgb::new(0.25, 0.25, 0.25), 1.0), - } - } -} - -pub enum HudColor { - Yellow, - Green, - Red, - Blue, - White, - Purple, - Cyan, -} - -impl HudColor { - pub fn to_rgba(&self) -> Rgba { - match self { - HudColor::Yellow => Rgba::new(Rgb::new(1.0, 1.0, 0.0), 1.0), - HudColor::Green => Rgba::new(Rgb::new(0.2, 0.8, 0.2), 1.0), - HudColor::Red => Rgba::new(Rgb::new(0.9, 0.2, 0.2), 1.0), - HudColor::Blue => Rgba::new(Rgb::new(0.2, 0.4, 1.0), 1.0), - HudColor::White => Rgba::new(Rgb::new(1.0, 1.0, 1.0), 1.0), - HudColor::Purple => Rgba::new(Rgb::new(0.6, 0.2, 0.8), 1.0), - HudColor::Cyan => Rgba::new(Rgb::new(0.2, 0.8, 0.9), 1.0), - } - } -} - -/// Represents the main DarkClient GUI window and renders it. -pub fn render_gui(renderer: &mut Renderer) { - let mut ui = match UI_MANAGER.lock() { - Ok(guard) => guard, - Err(_) => return, - }; - - let screen_w = renderer.screen_width; - let screen_h = renderer.screen_height; - - let base_scale = (screen_h as f32 / 720.0).max(1.0).floor() as i32; - let scale_f = base_scale as f32; - - ui.update(scale_f, screen_w as f32, screen_h as f32); - - ui.hud_overlay.draw(renderer, &Theme::default(), scale_f); - - if !ui.is_visible { - return; - } - - let alpha = ui.background_alpha; - let theme = Theme::default(); - - unsafe { - // 1. Draw Full Screen Transparent Overlay - renderer.set_color(theme.screen_bg.with_alpha(alpha)); - renderer.draw_rect(0, 0, screen_w as i32, screen_h as i32); - - // Draw active windows and their nested modular dropdowns - for widget in &mut ui.windows { - widget.draw(renderer, &theme, scale_f); - } - - // Draw Top Right action buttons - ui.reset_btn.draw(renderer, &theme, scale_f); - ui.panic_btn.draw(renderer, &theme, scale_f); - } - - // Consume clicks globally at end of frame - if let Ok(mut mouse) = MOUSE_STATE.lock() { - if mouse.left_clicked { - mouse.left_clicked = false; - } - if mouse.right_clicked { - mouse.right_clicked = false; - } - } -} diff --git a/client/src/graphic/ui_engine.rs b/client/src/graphic/ui_engine.rs index 04f234a..30735af 100644 --- a/client/src/graphic/ui_engine.rs +++ b/client/src/graphic/ui_engine.rs @@ -1,9 +1,11 @@ -use std::sync::atomic::Ordering; -use std::sync::Mutex; +use crate::cleanup_client; +use crate::client::DarkClient; +use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; use egui::Context; use egui_glow::Painter; use lazy_static::lazy_static; -use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; +use std::sync::atomic::Ordering; +use std::sync::Mutex; pub struct EguiState { pub ctx: Context, @@ -18,14 +20,21 @@ lazy_static! { pub static ref EGUI_STATE: Mutex> = Mutex::new(None); } -pub fn gather_egui_inputs(state: &mut EguiState, screen_width: f32, screen_height: f32, scale_factor: f32) -> egui::RawInput { +pub fn gather_egui_inputs( + state: &mut EguiState, + screen_width: f32, + screen_height: f32, + scale_factor: f32, +) -> egui::RawInput { let mut raw_input = egui::RawInput::default(); // 1. Configura il Viewport principale con il nostro moltiplicatore di scala let mut viewport_info = egui::ViewportInfo::default(); viewport_info.native_pixels_per_point = Some(scale_factor); // Inseriamo le info nella mappa usando l'ID di default (ROOT) - raw_input.viewports.insert(egui::ViewportId::ROOT, viewport_info); + raw_input + .viewports + .insert(egui::ViewportId::ROOT, viewport_info); // 2. Passiamo le coordinate LOGICHE (divise per la scala) let logical_width = screen_width / scale_factor; @@ -44,12 +53,14 @@ pub fn gather_egui_inputs(state: &mut EguiState, screen_width: f32, screen_heigh let mouse = MOUSE_STATE.lock().unwrap(); let current_pos = egui::pos2( (mouse.x as f32) / scale_factor, - (mouse.y as f32) / scale_factor + (mouse.y as f32) / scale_factor, ); // 1. Movimento del Mouse if current_pos != state.last_mouse_pos { - raw_input.events.push(egui::Event::PointerMoved(current_pos)); + raw_input + .events + .push(egui::Event::PointerMoved(current_pos)); state.last_mouse_pos = current_pos; } @@ -125,15 +136,17 @@ pub unsafe fn render_egui_ui() { }); // Fine di ctx.run // Il resto del rendering OpenGL rimane identico, full_output ora viene da ctx.run - let clipped_primitives = state.ctx.tessellate(full_output.shapes, full_output.pixels_per_point); + let clipped_primitives = state + .ctx + .tessellate(full_output.shapes, full_output.pixels_per_point); // --- INIZIO FIX GEROGLIFICI --- // Resettiamo lo stato di unpack dei pixel che Minecraft spesso corrompe // prima di far caricare le texture dei font a egui unsafe { crate::gl::ActiveTexture(crate::gl::TEXTURE0); - crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); // Egui preferisce l'allineamento a 1 byte - crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); // Questo è il colpevole principale al 99%! + crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); // Egui preferisce l'allineamento a 1 byte + crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); // Questo è il colpevole principale al 99%! crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_PIXELS, 0); crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_ROWS, 0); @@ -152,3 +165,24 @@ pub unsafe fn render_egui_ui() { &full_output.textures_delta, ); } + +pub fn call_panic() { + let client = DarkClient::instance(); + client.modules.read().unwrap().values().for_each(|module| { + let mut module = module.lock().unwrap(); + if module.get_module_data().enabled { + module.get_module_data_mut().set_enabled(false); + match module.on_stop() { + Ok(_) => {} + Err(e) => { + log::error!( + "Failed to stop module {} on panic: {}", + module.get_module_data().name, + e + ); + } + } + } + }); + cleanup_client(); +} diff --git a/client/src/graphic/ui_manager.rs b/client/src/graphic/ui_manager.rs deleted file mode 100644 index 41af26e..0000000 --- a/client/src/graphic/ui_manager.rs +++ /dev/null @@ -1,303 +0,0 @@ -use crate::cleanup_client; -use crate::client::DarkClient; -use crate::graphic::color::Rgba; -use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; -use crate::graphic::widget::{Button, HudWidget, ModuleButton, Widget, Window}; -use crate::module::ModuleCategory; -use std::sync::Mutex; - -lazy_static::lazy_static! { - pub static ref UI_MANAGER: Mutex = Mutex::new(UiManager::new()); -} - -pub struct UiManager { - pub windows: Vec, - pub reset_btn: Button, - pub panic_btn: Button, - pub hud_overlay: HudWidget, - pub background_alpha: f32, - pub is_visible: bool, - pub initialized_layout: bool, -} - -impl UiManager { - pub fn new() -> Self { - let combat_win = Window::new( - 50.0, - 50.0, - 160.0, - 20.0, - ModuleCategory::COMBAT.display_name(), - ); - let move_win = Window::new( - 230.0, - 50.0, - 160.0, - 20.0, - ModuleCategory::MOVEMENT.display_name(), - ); - let render_win = Window::new( - 410.0, - 50.0, - 160.0, - 20.0, - ModuleCategory::RENDER.display_name(), - ); - let player_win = Window::new( - 590.0, - 50.0, - 160.0, - 20.0, - ModuleCategory::PLAYER.display_name(), - ); - let world_win = Window::new( - 770.0, - 50.0, - 160.0, - 20.0, - ModuleCategory::WORLD.display_name(), - ); - - let reset_btn = Button::new(0.0, 0.0, 60.0, 20.0, "Reset UI") - .with_bg_color(Rgba::new_rgb(0.2, 0.2, 0.2, 0.8)) - .with_text_color(Rgba::new_rgb(1.0, 1.0, 1.0, 1.0)); - - let panic_btn = Button::new(0.0, 0.0, 60.0, 20.0, "PANIC") - .with_bg_color(Rgba::new_rgb(0.8, 0.2, 0.2, 0.8)) - .with_text_color(Rgba::new_rgb(1.0, 1.0, 1.0, 1.0)); - - let manager = Self { - background_alpha: 0.0, - is_visible: false, - initialized_layout: false, - hud_overlay: HudWidget::new(), - reset_btn, - panic_btn, - windows: vec![ - Widget::Window(combat_win), - Widget::Window(move_win), - Widget::Window(render_win), - Widget::Window(player_win), - Widget::Window(world_win), - ], - }; - manager - } - - pub fn reset_ui(&mut self, screen_w: f32, screen_h: f32) { - self.auto_wrap_windows(screen_w, screen_h); - } - - pub fn auto_wrap_windows(&mut self, screen_w: f32, _screen_h: f32) { - let start_x = 50.0; - let start_y = 50.0; - let gap_x = 20.0; - let gap_y = 20.0; - - let mut current_x = start_x; - let mut current_y = start_y; - let mut row_max_height = 0.0_f32; - - for w_widget in &mut self.windows { - if let Widget::Window(window) = w_widget { - let mut calc_h = 25.0; - for child in &window.children { - match child { - Widget::Button(b) => calc_h += b.h + 2.0, - Widget::Label(_) => calc_h += 16.0, - Widget::ModuleButton(m) => calc_h += m.h + 2.0, - _ => {} - } - } - - if current_x + window.w > screen_w && current_x > start_x { - // Wrap to next line - current_x = start_x; - current_y += row_max_height + gap_y; - row_max_height = 0.0; - } - - window.x = current_x; - window.y = current_y; - window.render_x = current_x; - window.render_y = current_y; - - current_x += window.w + gap_x; - if calc_h > row_max_height { - row_max_height = calc_h; - } - } - } - } - - pub fn update(&mut self, scale_f: f32, screen_w: f32, screen_h: f32) { - // Sync real modules from DarkClient if empty (only triggers once) - for w_widget in &mut self.windows { - if let Widget::Window(window) = w_widget { - if window.children.is_empty() { - let client_modules_guard = DarkClient::instance().modules.read().unwrap(); - let target_category = match window.title.as_str() { - "Combat" => ModuleCategory::COMBAT, - "Movement" => ModuleCategory::MOVEMENT, - "Render" => ModuleCategory::RENDER, - "Player" => ModuleCategory::PLAYER, - "World" => ModuleCategory::WORLD, - _ => ModuleCategory::COMBAT, - }; - - let mut valid_modules: Vec<_> = client_modules_guard - .values() - .filter(|m| m.lock().unwrap().get_module_data().category == target_category) - .collect(); - - valid_modules.sort_by(|a, b| { - a.lock() - .unwrap() - .get_module_data() - .name - .cmp(&b.lock().unwrap().get_module_data().name) - }); - - window.children = valid_modules - .into_iter() - .map(|m| { - let name = m.lock().unwrap().get_module_data().name.clone(); - Widget::ModuleButton(ModuleButton::new(&name)) - }) - .collect(); - } - } - } - - if !self.initialized_layout { - self.auto_wrap_windows(screen_w, screen_h); - self.initialized_layout = true; - } - - // Handle visibility and animations - let target_visible = GUI_OPEN.load(std::sync::atomic::Ordering::Relaxed); - - if target_visible { - self.is_visible = true; - if self.background_alpha < 0.6 { - self.background_alpha += 0.05; // Fade in - } - } else { - if self.background_alpha > 0.0 { - self.background_alpha -= 0.05; // Fade out - } else { - self.is_visible = false; - } - } - - if !self.is_visible { - return; - } - - // Handle interactions & logic updates - let (mx, my, left_clicked, right_clicked, left_down) = { - if let Ok(mut mouse) = MOUSE_STATE.lock() { - let l = mouse.left_clicked; - let r = mouse.right_clicked; - let ld = mouse.left_down; - mouse.left_clicked = false; - mouse.right_clicked = false; - (mouse.x as f32, mouse.y as f32, l, r, ld) - } else { - (0.0, 0.0, false, false, false) - } - }; - - // Delegate clicks - if left_clicked || right_clicked { - let mut clicked_idx = None; - for (i, widget) in self.windows.iter_mut().enumerate().rev() { - if let Widget::Window(win) = widget { - let scaled_x = win.render_x * scale_f; - let scaled_y = win.render_y * scale_f; - let scaled_w = win.w * scale_f; - let scaled_h = win.h * scale_f; - - if mx >= scaled_x - && mx <= scaled_x + scaled_w - && my >= scaled_y - && my <= scaled_y + scaled_h - { - clicked_idx = Some(i); - break; - } - } - } - - if let Some(idx) = clicked_idx { - let mut top_win = self.windows.remove(idx); - top_win.handle_click(mx, my, left_clicked, right_clicked, scale_f); - self.windows.push(top_win); - } - } - - // Delegate updates - for widget in &mut self.windows { - widget.update(mx, my, left_down, scale_f); - } - - // Layout Context Buttons - let btn_w = 60.0; - let btn_h = 20.0; - let btn_pad = 10.0; - - self.reset_btn.x = (screen_w / scale_f) - btn_w - btn_pad; - self.reset_btn.y = btn_pad; - self.reset_btn.w = btn_w; - self.reset_btn.h = btn_h; - - self.panic_btn.x = self.reset_btn.x - btn_w - btn_pad; - self.panic_btn.y = btn_pad; - self.panic_btn.w = btn_w; - self.panic_btn.h = btn_h; - - // Apply alpha to context buttons dynamically - let shared_alpha = self.background_alpha.min(0.8); - self.reset_btn.bg_color.a = shared_alpha; - self.panic_btn.bg_color.a = shared_alpha; - - self.reset_btn.update(mx, my, left_down, scale_f); - self.panic_btn.update(mx, my, left_down, scale_f); - - if left_clicked { - if self - .reset_btn - .handle_click(mx, my, left_clicked, right_clicked, scale_f) - { - self.reset_ui(screen_w, screen_h); - } - if self - .panic_btn - .handle_click(mx, my, left_clicked, right_clicked, scale_f) - { - std::thread::spawn(|| call_panic()); - } - } - } -} - -pub fn call_panic() { - let client = DarkClient::instance(); - client.modules.read().unwrap().values().for_each(|module| { - let mut module = module.lock().unwrap(); - if module.get_module_data().enabled { - module.get_module_data_mut().set_enabled(false); - match module.on_stop() { - Ok(_) => {} - Err(e) => { - log::error!( - "Failed to stop module {} on panic: {}", - module.get_module_data().name, - e - ); - } - } - } - }); - cleanup_client(); -} diff --git a/client/src/graphic/widget/button.rs b/client/src/graphic/widget/button.rs deleted file mode 100644 index b7c765f..0000000 --- a/client/src/graphic/widget/button.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::graphic::color::Rgba; -use crate::graphic::font::get_text_width; -use crate::graphic::render::Renderer; -use crate::graphic::ui::Theme; - -pub struct Button { - pub x: f32, - pub y: f32, - pub w: f32, - pub h: f32, - pub text: String, - pub bg_color: Rgba, - pub text_color: Rgba, - pub on_click: Option>, - is_hovered: bool, -} - -impl Button { - pub fn new(x: f32, y: f32, w: f32, h: f32, text: &str) -> Self { - Self { - x, - y, - w, - h, - text: text.to_string(), - bg_color: Rgba::new_rgb(0.2, 0.2, 0.2, 1.0), - text_color: Rgba::new_rgb(1.0, 1.0, 1.0, 1.0), - on_click: None, - is_hovered: false, - } - } - - pub fn with_bg_color(mut self, rgba: Rgba) -> Self { - self.bg_color = rgba; - self - } - - pub fn with_text_color(mut self, rgba: Rgba) -> Self { - self.text_color = rgba; - self - } - - pub fn on_click(mut self, callback: F) -> Self - where - F: FnMut() + Send + Sync + 'static, - { - self.on_click = Some(Box::new(callback)); - self - } - - pub fn update(&mut self, mx: f32, my: f32, _left_down: bool, scale_f: f32) { - self.is_hovered = mx >= self.x * scale_f - && mx <= (self.x + self.w) * scale_f - && my >= self.y * scale_f - && my <= (self.y + self.h) * scale_f; - } - - pub fn handle_click( - &mut self, - _mx: f32, - _my: f32, - left_clicked: bool, - _right_clicked: bool, - _scale_f: f32, - ) -> bool { - if self.is_hovered && left_clicked { - if let Some(ref mut cb) = self.on_click { - cb(); - } - return true; - } - false - } - - pub fn draw(&mut self, renderer: &mut Renderer, _theme: &Theme, scale_f: f32) { - let actual_bg = if self.is_hovered { - Rgba::new_rgb( - (self.bg_color.r + 0.2).min(1.0), - (self.bg_color.g + 0.2).min(1.0), - (self.bg_color.b + 0.2).min(1.0), - self.bg_color.a, - ) - } else { - self.bg_color - }; - - unsafe { - renderer.set_color(actual_bg); - renderer.draw_rect( - (self.x * scale_f) as i32, - (self.y * scale_f) as i32, - (self.w * scale_f) as i32, - (self.h * scale_f) as i32, - ); - - let t_width = get_text_width(&self.text, scale_f as i32); - let text_x = (self.x * scale_f) as i32 + ((self.w * scale_f) as i32 - t_width) / 2; - let text_y = - (self.y * scale_f) as i32 + ((self.h * scale_f) as i32 - (7 * scale_f as i32)) / 2; - - crate::graphic::font::draw_text( - renderer, - &self.text, - text_x, - text_y, - self.text_color, - scale_f as i32, - ); - } - } -} diff --git a/client/src/graphic/widget/hud.rs b/client/src/graphic/widget/hud.rs deleted file mode 100644 index 7caa4fd..0000000 --- a/client/src/graphic/widget/hud.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::client::DarkClient; -use crate::graphic::font::draw_text; -use crate::graphic::render::Renderer; -use crate::graphic::ui::{HudColor, Theme}; - -pub struct HudWidget { - pub is_visible: bool, -} - -impl HudWidget { - pub fn new() -> Self { - Self { is_visible: true } - } - - pub fn draw(&mut self, renderer: &mut Renderer, _theme: &Theme, scale_f: f32) { - if !self.is_visible { - return; - } - - let watermark = "DarkClient"; - let w_color = HudColor::Yellow.to_rgba(); - - let mut hud_y = 5.0 * scale_f; - let hud_x = 5.0 * scale_f; - let text_scale = (2.0 * scale_f) as i32; // Using 2.0 multiplier for large HUD text - - unsafe { - draw_text( - renderer, - watermark, - hud_x as i32, - hud_y as i32, - w_color, - text_scale, - ); - } - - hud_y += 24.0 * scale_f; // watermark margin bottom - - if let Ok(modules_map) = DarkClient::instance().modules.read() { - let mut active_mods: Vec = modules_map - .values() - .filter_map(|m| { - let lock = m.lock().unwrap(); - if lock.get_module_data().enabled { - Some(lock.get_module_data().name.clone()) - } else { - None - } - }) - .collect(); - - // Sort by length (longest first) - active_mods.sort_by(|a, b| b.len().cmp(&a.len())); - - let arraylist_colors = [ - HudColor::Purple, - HudColor::Cyan, - HudColor::Green, - HudColor::Red, - HudColor::Yellow, - HudColor::Blue, - HudColor::White, - ]; - - for (i, mod_name) in active_mods.iter().enumerate() { - let color = arraylist_colors[i % arraylist_colors.len()].to_rgba(); - let m_scale = (1.5 * scale_f) as i32; // Font slightly smaller than watermark - unsafe { - draw_text( - renderer, - mod_name, - hud_x as i32, - hud_y as i32, - color, - m_scale, - ); - } - hud_y += 18.0 * scale_f; // module spacing - } - } - } -} diff --git a/client/src/graphic/widget/label.rs b/client/src/graphic/widget/label.rs deleted file mode 100644 index 5665d91..0000000 --- a/client/src/graphic/widget/label.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::graphic::color::Rgba; -use crate::graphic::render::Renderer; -use crate::graphic::ui::Theme; - -pub struct Label { - pub x: f32, - pub y: f32, - pub text: String, - pub color: Rgba, - pub scale_mult: f32, -} - -impl Label { - pub fn new(x: f32, y: f32, text: &str) -> Self { - Self { - x, - y, - text: text.to_string(), - color: Rgba::new_rgb(1.0, 1.0, 1.0, 1.0), - scale_mult: 1.0, - } - } - - pub fn with_color(mut self, rgba: Rgba) -> Self { - self.color = rgba; - self - } - - pub fn with_scale(mut self, mult: f32) -> Self { - self.scale_mult = mult; - self - } - - pub fn update(&mut self, _mx: f32, _my: f32, _left_down: bool, _scale_f: f32) {} - - pub fn handle_click( - &mut self, - _mx: f32, - _my: f32, - _lc: bool, - _rc: bool, - _scale_f: f32, - ) -> bool { - false - } - - pub fn draw(&mut self, renderer: &mut Renderer, _theme: &Theme, scale_f: f32) { - unsafe { - let final_scale = (scale_f * self.scale_mult) as i32; - crate::graphic::font::draw_text( - renderer, - &self.text, - (self.x * scale_f) as i32, - (self.y * scale_f) as i32, - self.color, - final_scale, - ); - } - } -} diff --git a/client/src/graphic/widget/mod.rs b/client/src/graphic/widget/mod.rs deleted file mode 100644 index b583177..0000000 --- a/client/src/graphic/widget/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -pub mod button; -pub mod label; -pub mod panel; - -use crate::graphic::render::Renderer; -use crate::graphic::ui::Theme; - -pub mod hud; -pub mod module_btn; -pub mod slider; -pub mod toggle; -pub mod window; - -pub use button::Button; -pub use hud::HudWidget; -pub use label::Label; -pub use module_btn::ModuleButton; -pub use panel::Panel; -pub use slider::Slider; -pub use toggle::Toggle; -pub use window::Window; - -pub enum Widget { - Button(Button), - Label(Label), - Panel(Panel), - Window(Window), - ModuleButton(ModuleButton), - Slider(Slider), - Toggle(Toggle), -} - -impl Widget { - pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { - match self { - Widget::Button(b) => b.draw(renderer, theme, scale_f), - Widget::Label(l) => l.draw(renderer, theme, scale_f), - Widget::Panel(p) => p.draw(renderer, theme, scale_f), - Widget::Window(w) => w.draw(renderer, theme, scale_f), - Widget::ModuleButton(m) => m.draw(renderer, theme, scale_f), - Widget::Slider(s) => s.draw(renderer, theme, scale_f), - Widget::Toggle(t) => t.draw(renderer, theme, scale_f), - } - } - - pub fn handle_click( - &mut self, - mx: f32, - my: f32, - left_clicked: bool, - right_clicked: bool, - scale_f: f32, - ) -> bool { - match self { - Widget::Button(b) => b.handle_click(mx, my, left_clicked, right_clicked, scale_f), - Widget::Label(l) => l.handle_click(mx, my, left_clicked, right_clicked, scale_f), - Widget::Panel(p) => p.handle_click(mx, my, left_clicked, right_clicked, scale_f), - Widget::Window(w) => w.handle_click(mx, my, left_clicked, right_clicked, scale_f), - Widget::ModuleButton(m) => m.handle_click(mx, my, left_clicked, right_clicked, scale_f), - Widget::Slider(s) => s.handle_click(mx, my, left_clicked, right_clicked, scale_f), - Widget::Toggle(t) => t.handle_click(mx, my, left_clicked, right_clicked, scale_f), - } - } - - pub fn update(&mut self, mx: f32, my: f32, left_down: bool, scale_f: f32) { - match self { - Widget::Button(b) => b.update(mx, my, left_down, scale_f), - Widget::Label(l) => l.update(mx, my, left_down, scale_f), - Widget::Panel(p) => p.update(mx, my, left_down, scale_f), - Widget::Window(w) => w.update(mx, my, left_down, scale_f), - Widget::ModuleButton(m) => m.update(mx, my, left_down, scale_f), - Widget::Slider(s) => s.update(mx, my, left_down, scale_f), - Widget::Toggle(t) => t.update(mx, my, left_down, scale_f), - } - } -} diff --git a/client/src/graphic/widget/module_btn.rs b/client/src/graphic/widget/module_btn.rs deleted file mode 100644 index 57153ba..0000000 --- a/client/src/graphic/widget/module_btn.rs +++ /dev/null @@ -1,267 +0,0 @@ -use crate::client::DarkClient; -use crate::graphic::font::get_text_width; -use crate::graphic::render::Renderer; -use crate::graphic::ui::Theme; -use crate::graphic::widget::{Slider, Toggle, Widget}; -use crate::module::ModuleSetting; - -pub struct ModuleButton { - pub x: f32, - pub y: f32, - pub w: f32, - pub h: f32, - pub name: String, - - // Internal States - pub is_expanded: bool, - pub expand_anim: f32, - pub settings: Vec, - - is_hovered: bool, - is_box_hovered: bool, -} - -impl ModuleButton { - pub fn new(name: &str) -> Self { - let mut settings_widgets = Vec::new(); - if let Ok(modules) = DarkClient::instance().modules.read() { - if let Some(m) = modules.get(name) { - let lock = m.lock().unwrap(); - for setting in &lock.get_module_data().settings { - match setting { - ModuleSetting::Toggle { name: s_name, .. } => { - settings_widgets.push(Widget::Toggle(Toggle::new(name, s_name))); - } - ModuleSetting::Slider { - name: s_name, - min, - max, - .. - } => { - settings_widgets - .push(Widget::Slider(Slider::new(name, s_name, *min, *max))); - } - _ => {} - } - } - } - } - - Self { - x: 0.0, - y: 0.0, - w: 160.0, - h: 16.0, - name: name.to_string(), - is_expanded: false, - expand_anim: 0.0, - settings: settings_widgets, - is_hovered: false, - is_box_hovered: false, - } - } - - pub fn update(&mut self, mx: f32, my: f32, _left_down: bool, scale_f: f32) { - let scaled_x = self.x * scale_f; - let scaled_y = self.y * scale_f; - let scaled_w = self.w * scale_f; - let scaled_h = self.h * scale_f; - - let box_side = 14.0 * scale_f; - let box_x = scaled_x + scaled_w - box_side - (2.0 * scale_f); - let box_y = scaled_y + (scaled_h - box_side) / 2.0; - - self.is_hovered = mx >= scaled_x - && mx <= scaled_x + scaled_w - && my >= scaled_y - && my <= scaled_y + scaled_h; - - self.is_box_hovered = - mx >= box_x && mx <= box_x + box_side && my >= box_y && my <= box_y + box_side; - - if self.is_expanded { - if self.expand_anim < 1.0 { - self.expand_anim = (self.expand_anim + 0.1).min(1.0); - } - } else { - if self.expand_anim > 0.0 { - self.expand_anim = (self.expand_anim - 0.1).max(0.0); - } - } - - // Calculate dynamic height footprint including settings - let mut target_h = 16.0; // Base height - if self.expand_anim > 0.01 { - let settings_count = self.settings.len() as f32; - target_h += self.expand_anim * (settings_count * 14.0); - } - self.h = target_h; - - // Propagate updates to settings - if self.expand_anim > 0.01 { - let mut current_set_y = self.y + 16.0; - let set_h = 14.0; - - for child in &mut self.settings { - match child { - Widget::Toggle(t) => { - t.x = self.x; - t.y = current_set_y; - t.w = self.w; - t.h = set_h; - t.is_expanded_anim = self.expand_anim; - t.update(mx, my, _left_down, scale_f); - } - Widget::Slider(s) => { - s.x = self.x; - s.y = current_set_y; - s.w = self.w; - s.h = set_h; - s.is_expanded_anim = self.expand_anim; - s.update(mx, my, _left_down, scale_f); - } - _ => {} - } - current_set_y += set_h; - } - } - } - - pub fn handle_click( - &mut self, - mx: f32, - my: f32, - left_clicked: bool, - right_clicked: bool, - scale_f: f32, - ) -> bool { - if self.expand_anim > 0.01 { - for child in self.settings.iter_mut().rev() { - if child.handle_click(mx, my, left_clicked, right_clicked, scale_f) { - return true; - } - } - } - - if self.is_hovered { - if left_clicked { - if self.is_box_hovered { - self.is_expanded = !self.is_expanded; - } else { - if let Some(m) = DarkClient::instance() - .modules - .read() - .unwrap() - .get(&self.name) - { - let mut glk = m.lock().unwrap(); - let currently_enabled = glk.get_module_data().enabled; - glk.get_module_data_mut().set_enabled(!currently_enabled); - if !currently_enabled { - let _ = glk.on_start(); - } else { - let _ = glk.on_stop(); - } - } - } - return true; - } - if right_clicked { - self.is_expanded = !self.is_expanded; - return true; - } - } - false - } - - pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { - let alpha = 1.0; - let bg_color = if self.is_hovered { - theme.module_bg_hover - } else { - theme.module_bg - }; - - unsafe { - // Background - renderer.set_color(bg_color.with_alpha(bg_color.a * alpha)); - renderer.draw_rect( - (self.x * scale_f) as i32, - (self.y * scale_f) as i32, - (self.w * scale_f) as i32, - (16.0 * scale_f) as i32, - ); - - // Fetch state - let mut is_enabled = false; - if let Some(m) = DarkClient::instance() - .modules - .read() - .unwrap() - .get(&self.name) - { - is_enabled = m.lock().unwrap().get_module_data().enabled; - } - - let text_color = if is_enabled { - theme.text_accent - } else { - theme.text_primary - }; - - let t_width = get_text_width(&self.name, scale_f as i32); - let text_x = (self.x * scale_f) as i32 + ((self.w * scale_f) as i32 - t_width) / 2; - let text_y = - (self.y * scale_f) as i32 + ((16.0 * scale_f) as i32 - (7 * scale_f as i32)) / 2; - - // Name - crate::graphic::font::draw_text( - renderer, - &self.name, - text_x, - text_y, - text_color.with_alpha(alpha), - scale_f as i32, - ); - - // Expansion Box - let box_side = 14.0 * scale_f; - let box_x = (self.x * scale_f) + (self.w * scale_f) - box_side - (2.0 * scale_f); - let box_y = (self.y * scale_f) + ((16.0 * scale_f) - box_side) / 2.0; - - let box_bg = if self.is_box_hovered { - theme.module_bg_hover - } else { - theme.module_bg - }; - - renderer.set_color(theme.border.with_alpha(alpha * 0.5)); - renderer.draw_rect( - box_x as i32 - 1, - box_y as i32, - box_side as i32 + 1, - box_side as i32, - ); - renderer.set_color(box_bg.with_alpha(box_bg.a * alpha)); - renderer.draw_rect(box_x as i32, box_y as i32, box_side as i32, box_side as i32); - - let arrow = if self.is_expanded { "^" } else { "v" }; - let aw = get_text_width(arrow, scale_f as i32); - crate::graphic::font::draw_text( - renderer, - arrow, - (box_x + (box_side - aw as f32) / 2.0) as i32, - (box_y + (box_side - 7.0 * scale_f) / 2.0) as i32, - theme.text_primary.with_alpha(alpha), - scale_f as i32, - ); - - // Draw Settings - if self.expand_anim > 0.01 { - for child in &mut self.settings { - child.draw(renderer, theme, scale_f); - } - } - } - } -} diff --git a/client/src/graphic/widget/panel.rs b/client/src/graphic/widget/panel.rs deleted file mode 100644 index b6e954b..0000000 --- a/client/src/graphic/widget/panel.rs +++ /dev/null @@ -1,128 +0,0 @@ -use super::Widget; -use crate::graphic::color::Rgba; -use crate::graphic::render::Renderer; -use crate::graphic::ui::Theme; - -pub struct Panel { - pub x: f32, - pub y: f32, - pub w: f32, - pub h: f32, - pub bg_color: Rgba, - pub children: Vec, - pub is_draggable: bool, - - // Internal state - is_dragging: bool, - drag_offset_x: f32, - drag_offset_y: f32, -} - -impl Panel { - pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self { - Self { - x, - y, - w, - h, - bg_color: Rgba::new_rgb(0.1, 0.1, 0.1, 0.8), - children: Vec::new(), - is_draggable: true, - is_dragging: false, - drag_offset_x: 0.0, - drag_offset_y: 0.0, - } - } - - pub fn with_bg_color(mut self, rgba: Rgba) -> Self { - self.bg_color = rgba; - self - } - - pub fn with_draggable(mut self, drag: bool) -> Self { - self.is_draggable = drag; - self - } - - pub fn add_child(mut self, widget: Widget) -> Self { - self.children.push(widget); - self - } - - pub fn update(&mut self, mx: f32, my: f32, left_down: bool, scale_f: f32) { - if self.is_draggable { - if !left_down { - self.is_dragging = false; - } else if self.is_dragging { - self.x = (mx / scale_f) - self.drag_offset_x; - self.y = (my / scale_f) - self.drag_offset_y; - } else { - // If it's a new click within Draggable Title Area - if mx >= self.x * scale_f - && mx <= (self.x + self.w) * scale_f - && my >= self.y * scale_f - && my <= (self.y + 20.0) * scale_f - { - self.is_dragging = true; - self.drag_offset_x = (mx / scale_f) - self.x; - self.drag_offset_y = (my / scale_f) - self.y; - } - } - } - - for child in &mut self.children { - child.update(mx, my, left_down, scale_f); - } - } - - pub fn handle_click( - &mut self, - mx: f32, - my: f32, - left_clicked: bool, - right_clicked: bool, - scale_f: f32, - ) -> bool { - // First interact with children uniformly reversed (Top down) - let mut consumed = false; - for child in self.children.iter_mut().rev() { - if child.handle_click(mx, my, left_clicked, right_clicked, scale_f) { - consumed = true; - break; - } - } - - if consumed { - return true; - } - - // Then interact with Panel rect itself - if mx >= self.x * scale_f - && mx <= (self.x + self.w) * scale_f - && my >= self.y * scale_f - && my <= (self.y + self.h) * scale_f - { - if left_clicked || right_clicked { - return true; - } - } - - false - } - - pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { - unsafe { - renderer.set_color(self.bg_color); - renderer.draw_rect( - (self.x * scale_f) as i32, - (self.y * scale_f) as i32, - (self.w * scale_f) as i32, - (self.h * scale_f) as i32, - ); - } - - for child in &mut self.children { - child.draw(renderer, theme, scale_f); - } - } -} diff --git a/client/src/graphic/widget/slider.rs b/client/src/graphic/widget/slider.rs deleted file mode 100644 index 544af8a..0000000 --- a/client/src/graphic/widget/slider.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::client::DarkClient; -use crate::graphic::color::Color; -use crate::graphic::render::Renderer; -use crate::graphic::ui::Theme; - -pub struct Slider { - pub x: f32, - pub y: f32, - pub w: f32, - pub h: f32, - pub module_name: String, - pub setting_name: String, - pub min: f32, - pub max: f32, - pub is_expanded_anim: f32, - is_hovered: bool, - is_dragging: bool, -} - -impl Slider { - pub fn new(module_name: &str, setting_name: &str, min: f32, max: f32) -> Self { - Self { - x: 0.0, - y: 0.0, - w: 160.0, // default width - h: 14.0, // default height - module_name: module_name.to_string(), - setting_name: setting_name.to_string(), - min, - max, - is_expanded_anim: 0.0, - is_hovered: false, - is_dragging: false, - } - } - - pub fn update(&mut self, mx: f32, my: f32, left_down: bool, scale_f: f32) { - let sx = self.x * scale_f; - let sy = self.y * scale_f; - let sw = self.w * scale_f; - let sh = self.h * scale_f; - - self.is_hovered = mx >= sx && mx <= sx + sw && my >= sy && my <= sy + sh; - - if !left_down { - self.is_dragging = false; - } else if self.is_dragging { - // Compute slider value based on mouse X constraint - let mut value_pct = (mx - sx) / sw; - value_pct = value_pct.clamp(0.0, 1.0); - let mut new_val = self.min + (self.max - self.min) * value_pct; - - // Rounding logic for aesthetics if it's acting like an int or single decimal bounds. - // Assuming 1 decimal point for general float sliders for clean UI. - new_val = (new_val * 10.0).round() / 10.0; - - if let Ok(modules) = DarkClient::instance().modules.read() { - if let Some(m) = modules.get(&self.module_name) { - if let Some(setting) = m - .lock() - .unwrap() - .get_module_data_mut() - .get_setting_mut(&self.setting_name) - { - setting.set_slider_value(new_val); - } - } - } - } - } - - pub fn handle_click( - &mut self, - _mx: f32, - _my: f32, - left_clicked: bool, - _right_clicked: bool, - _scale_f: f32, - ) -> bool { - if self.is_hovered && left_clicked { - self.is_dragging = true; - return true; - } - false - } - - pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { - if self.is_expanded_anim < 0.01 { - return; - } - - let mut val = self.min; - if let Ok(modules) = DarkClient::instance().modules.read() { - if let Some(m) = modules.get(&self.module_name) { - if let Some(setting) = m - .lock() - .unwrap() - .get_module_data() - .get_setting(&self.setting_name) - { - if let Some(v) = setting.get_slider_value() { - val = v; - } - } - } - } - - let bg_color = if self.is_hovered { - theme.module_bg_hover - } else { - theme.module_bg - }; - - let sx = self.x * scale_f; - let sy = self.y * scale_f; - let sw = self.w * scale_f; - let sh = self.h * scale_f; - - let pct = ((val - self.min) / (self.max - self.min)).clamp(0.0, 1.0); - let filled_w = sw * pct; - - unsafe { - // Draw Background Track - renderer.set_color(bg_color.with_alpha(bg_color.a * self.is_expanded_anim)); - renderer.draw_rect(sx as i32, sy as i32, sw as i32, sh as i32); - - // Draw Filled Track - let track_col = theme.text_accent; - renderer.set_color(track_col.with_alpha(self.is_expanded_anim * 0.8)); - renderer.draw_rect(sx as i32, sy as i32, filled_w as i32, sh as i32); - - // Draw Text - let display_text = format!("{}: {:.1}", self.setting_name, val); - crate::graphic::font::draw_text( - renderer, - &display_text, - (sx + 5.0 * scale_f) as i32, - (sy + (sh - 7.0 * scale_f) / 2.0) as i32, - Color::Black.to_rgba(self.is_expanded_anim), - scale_f as i32, - ); - } - } -} diff --git a/client/src/graphic/widget/toggle.rs b/client/src/graphic/widget/toggle.rs deleted file mode 100644 index c525c2b..0000000 --- a/client/src/graphic/widget/toggle.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::client::DarkClient; -use crate::graphic::render::Renderer; -use crate::graphic::ui::Theme; - -pub struct Toggle { - pub x: f32, - pub y: f32, - pub w: f32, - pub h: f32, - pub module_name: String, - pub setting_name: String, - pub is_expanded_anim: f32, - is_hovered: bool, -} - -impl Toggle { - pub fn new(module_name: &str, setting_name: &str) -> Self { - Self { - x: 0.0, - y: 0.0, - w: 160.0, - h: 14.0, - module_name: module_name.to_string(), - setting_name: setting_name.to_string(), - is_expanded_anim: 0.0, - is_hovered: false, - } - } - - pub fn update(&mut self, mx: f32, my: f32, _left_down: bool, scale_f: f32) { - let sx = self.x * scale_f; - let sy = self.y * scale_f; - let sw = self.w * scale_f; - let sh = self.h * scale_f; - - self.is_hovered = mx >= sx && mx <= sx + sw && my >= sy && my <= sy + sh; - } - - pub fn handle_click( - &mut self, - _mx: f32, - _my: f32, - left_clicked: bool, - _right_clicked: bool, - _scale_f: f32, - ) -> bool { - if self.is_hovered && left_clicked { - if let Ok(modules) = DarkClient::instance().modules.read() { - if let Some(m) = modules.get(&self.module_name) { - if let Some(setting) = m - .lock() - .unwrap() - .get_module_data_mut() - .get_setting_mut(&self.setting_name) - { - let current = setting.get_toggle_value().unwrap_or(false); - setting.set_toggle_value(!current); - } - } - } - return true; - } - false - } - - pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { - if self.is_expanded_anim < 0.01 { - return; - } - - let mut val = false; - if let Ok(modules) = DarkClient::instance().modules.read() { - if let Some(m) = modules.get(&self.module_name) { - if let Some(setting) = m - .lock() - .unwrap() - .get_module_data() - .get_setting(&self.setting_name) - { - if let Some(v) = setting.get_toggle_value() { - val = v; - } - } - } - } - - let bg_color = if self.is_hovered { - theme.module_bg_hover - } else { - theme.module_bg - }; - - let sx = self.x * scale_f; - let sy = self.y * scale_f; - let sw = self.w * scale_f; - let sh = self.h * scale_f; - - unsafe { - // Draw Background - renderer.set_color(bg_color.with_alpha(bg_color.a * self.is_expanded_anim)); - renderer.draw_rect(sx as i32, sy as i32, sw as i32, sh as i32); - - // Draw Checkbox Box - let box_s = 8.0 * scale_f; - let padding_r = 10.0 * scale_f; - let box_x = sx + sw - box_s - padding_r; - let box_y = sy + (sh - box_s) / 2.0; - - renderer.set_color(theme.border.with_alpha(self.is_expanded_anim * 0.5)); - renderer.draw_rect( - (box_x - 1.0) as i32, - (box_y - 1.0) as i32, - (box_s + 2.0) as i32, - (box_s + 2.0) as i32, - ); - - let box_bg = if val { - theme.text_accent - } else { - theme.window_bg - }; - renderer.set_color(box_bg.with_alpha(self.is_expanded_anim)); - renderer.draw_rect(box_x as i32, box_y as i32, box_s as i32, box_s as i32); - - // Draw Text - crate::graphic::font::draw_text( - renderer, - &self.setting_name, - (sx + 5.0 * scale_f) as i32, - (sy + (sh - 7.0 * scale_f) / 2.0) as i32, - theme.text_primary.with_alpha(self.is_expanded_anim), - scale_f as i32, - ); - } - } -} diff --git a/client/src/graphic/widget/window.rs b/client/src/graphic/widget/window.rs deleted file mode 100644 index e2358bf..0000000 --- a/client/src/graphic/widget/window.rs +++ /dev/null @@ -1,227 +0,0 @@ -use super::Widget; -use crate::graphic::font::get_text_width; -use crate::graphic::render::Renderer; -use crate::graphic::ui::Theme; - -pub struct Window { - pub x: f32, - pub y: f32, - pub render_x: f32, - pub render_y: f32, - pub vel_x: f32, - pub vel_y: f32, - pub w: f32, - pub h: f32, - pub title: String, - pub children: Vec, - - pub is_dragging: bool, - drag_offset_x: f32, - drag_offset_y: f32, -} - -impl Window { - pub fn new(x: f32, y: f32, w: f32, h: f32, title: &str) -> Self { - Self { - x, - y, - render_x: x, - render_y: y, - vel_x: 0.0, - vel_y: 0.0, - w, - h, - title: title.to_string(), - children: Vec::new(), - is_dragging: false, - drag_offset_x: 0.0, - drag_offset_y: 0.0, - } - } - - pub fn add_child(mut self, child: Widget) -> Self { - self.children.push(child); - self - } - - pub fn update(&mut self, mx: f32, my: f32, left_down: bool, scale_f: f32) { - if left_down { - if self.is_dragging { - self.x = (mx / scale_f) - self.drag_offset_x; - self.y = (my / scale_f) - self.drag_offset_y; - } - } else { - self.is_dragging = false; - } - - // Spring Physics - let stiffness = 0.25; - let damping = 0.65; - - let fx = (self.x - self.render_x) * stiffness; - let fy = (self.y - self.render_y) * stiffness; - - self.vel_x = (self.vel_x + fx) * damping; - self.vel_y = (self.vel_y + fy) * damping; - - self.render_x += self.vel_x; - self.render_y += self.vel_y; - - // Propagate update to children iteratively adjusting Y offset - let mut child_y = self.render_y + 25.0; // below title + padding - for child in &mut self.children { - match child { - Widget::Button(b) => { - b.x = self.render_x + 5.0; - b.y = child_y; - b.update(mx, my, left_down, scale_f); - child_y += b.h + 2.0; - } - Widget::Label(l) => { - l.x = self.render_x + 5.0; - l.y = child_y; - l.update(mx, my, left_down, scale_f); - child_y += 14.0 + 2.0; - } - Widget::ModuleButton(m) => { - m.x = self.render_x + 5.0; - m.y = child_y; - m.w = self.w - 10.0; - m.update(mx, my, left_down, scale_f); - child_y += m.h + 2.0; - } - _ => {} - } - } - - let target_h = (child_y - self.render_y).max(25.0); - self.h += (target_h - self.h) * 0.25; - } - - pub fn handle_click( - &mut self, - mx: f32, - my: f32, - left_clicked: bool, - right_clicked: bool, - scale_f: f32, - ) -> bool { - let mut consumed = false; - for child in self.children.iter_mut().rev() { - if child.handle_click(mx, my, left_clicked, right_clicked, scale_f) { - consumed = true; - break; - } - } - - if consumed { - return true; - } - - let sx = self.render_x * scale_f; - let sy = self.render_y * scale_f; - let sw = self.w * scale_f; - let sh = self.h * scale_f; - - if mx >= sx && mx <= sx + sw && my >= sy && my <= sy + sh { - if left_clicked || right_clicked { - if left_clicked { - let title_height = 20.0 * scale_f; - if my <= sy + title_height { - self.is_dragging = true; - self.drag_offset_x = (mx - sx) / scale_f; - self.drag_offset_y = (my - sy) / scale_f; - } - } - return true; - } - } - false - } - - pub fn draw(&mut self, renderer: &mut Renderer, theme: &Theme, scale_f: f32) { - let wx = self.x * scale_f; - let wy = self.y * scale_f; - let ww = self.w * scale_f; - let wh = self.h * scale_f; - let title_h = 20.0 * scale_f; - - let mut dx = (self.render_x - self.x) * scale_f; - let mut dy = (self.render_y - self.y) * scale_f; - let max_s = 15.0 * scale_f; - dx = dx.clamp(-max_s, max_s); - dy = dy.clamp(-max_s, max_s); - - unsafe { - // Draw Title Bar - renderer.set_color(theme.border.with_alpha(1.0)); - renderer.draw_rect( - wx as i32 - 1, - wy as i32 - 1, - ww as i32 + 2, - title_h as i32 + 2, - ); - - renderer.set_color(theme.title_bg); - renderer.draw_rect(wx as i32, wy as i32, ww as i32, title_h as i32); - - let t_width = get_text_width(&self.title, scale_f as i32); - let text_x = wx as i32 + (ww as i32 - t_width) / 2; - let text_y = wy as i32 + (title_h as i32 - (7 * scale_f as i32)) / 2; - - crate::graphic::font::draw_text( - renderer, - &self.title, - text_x, - text_y, - theme.text_accent.with_alpha(1.0), - scale_f as i32, - ); - - // Draw Body (Veil) - let body_top_y = wy + title_h; - let body_h = wh - title_h; - - let tl_x = wx; - let tl_y = body_top_y; - let tr_x = wx + ww; - let tr_y = body_top_y; - let bl_x = wx + dx; - let bl_y = body_top_y + body_h + dy; - let br_x = wx + ww + dx; - let br_y = body_top_y + body_h + dy; - - renderer.set_color(theme.border.with_alpha(1.0)); - renderer.draw_quad( - tl_x - 1.0, - tl_y, - tr_x + 1.0, - tr_y, - bl_x - 1.0, - bl_y + 1.0, - br_x + 1.0, - br_y + 1.0, - ); - - renderer.set_color(theme.window_bg); - renderer.draw_quad(tl_x, tl_y, tr_x, tr_y, bl_x, bl_y, br_x, br_y); - - // Enable scissor bounding explicitly - let sx = tl_x.min(bl_x) as i32; - let sy = tl_y.min(bl_y) as i32; - let sw = (ww + dx.abs()) as i32; - let sh = (body_h + dy.max(0.0)) as i32; - - renderer.enable_scissor(sx, sy, sw, sh); - } - - // Draw children - for child in &mut self.children { - child.draw(renderer, theme, scale_f); - } - - unsafe { - renderer.disable_scissor(); - } - } -} From a1f796d8da6ceab3853cc1cd7a88dddf4263b0a2 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Sun, 22 Feb 2026 11:10:37 +0100 Subject: [PATCH 10/16] Improve UI quality --- client/src/graphic/color.rs | 106 --------- client/src/graphic/font.rs | 177 -------------- client/src/graphic/gui.rs | 49 ++-- client/src/graphic/hook.rs | 9 +- client/src/graphic/hud.rs | 21 +- client/src/graphic/menu.rs | 403 +++++++++++++++++++------------- client/src/graphic/mod.rs | 3 - client/src/graphic/render.rs | 330 -------------------------- client/src/graphic/ui_engine.rs | 55 +++-- 9 files changed, 298 insertions(+), 855 deletions(-) delete mode 100644 client/src/graphic/color.rs delete mode 100644 client/src/graphic/font.rs delete mode 100644 client/src/graphic/render.rs diff --git a/client/src/graphic/color.rs b/client/src/graphic/color.rs deleted file mode 100644 index 9c6a25e..0000000 --- a/client/src/graphic/color.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Color { - Yellow, - Green, - Red, - Blue, - White, - Purple, - Cyan, - Black, -} - -impl Color { - pub fn to_rgb(&self) -> Rgb { - match self { - Color::Yellow => Rgb::new(1.0, 1.0, 0.0), - Color::Green => Rgb::new(0.0, 1.0, 0.0), - Color::Red => Rgb::new(1.0, 0.0, 0.0), - Color::Blue => Rgb::new(0.0, 0.0, 1.0), - Color::White => Rgb::new(1.0, 1.0, 1.0), - Color::Purple => Rgb::new(1.0, 0.0, 1.0), - Color::Cyan => Rgb::new(0.0, 1.0, 1.0), - Color::Black => Rgb::new(0.0, 0.0, 0.0), - } - } - - pub fn to_rgba(&self, a: f32) -> Rgba { - Rgba::new(self.to_rgb(), a) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Rgb { - pub r: f32, - pub g: f32, - pub b: f32, -} - -impl Rgb { - pub fn new(r: f32, g: f32, b: f32) -> Self { - Self { r, g, b } - } - - pub fn with_alpha(self, a: f32) -> Rgba { - Rgba::new(self, a) - } -} - -impl From<(f32, f32, f32)> for Rgb { - fn from(value: (f32, f32, f32)) -> Self { - Self::new(value.0, value.1, value.2) - } -} - -impl From for (f32, f32, f32) { - fn from(value: Rgb) -> Self { - (value.r, value.g, value.b) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Rgba { - pub rgb: Rgb, - pub a: f32, -} - -impl Rgba { - pub fn new(rgb: Rgb, a: f32) -> Self { - Self { rgb, a } - } - - pub fn new_rgb(r: f32, g: f32, b: f32, a: f32) -> Self { - Self { - rgb: Rgb { r, g, b }, - a, - } - } -} - -impl From<(f32, f32, f32, f32)> for Rgba { - fn from(value: (f32, f32, f32, f32)) -> Self { - Self::new_rgb(value.0, value.1, value.2, value.3) - } -} - -impl From for (f32, f32, f32, f32) { - fn from(value: Rgba) -> Self { - (value.r, value.g, value.b, value.a) - } -} - -impl Deref for Rgba { - type Target = Rgb; - - fn deref(&self) -> &Self::Target { - &self.rgb - } -} - -impl DerefMut for Rgba { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.rgb - } -} diff --git a/client/src/graphic/font.rs b/client/src/graphic/font.rs deleted file mode 100644 index 439c5cf..0000000 --- a/client/src/graphic/font.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::graphic::{color::Rgba, render::Renderer}; - -/// A simple 5x7 bitmap font for ASCII characters 32 to 127. -/// Each byte represents a column of the character (5 columns per character). -/// 1 means pixel is on, 0 means pixel is off. -static FONT_5X7: [[u8; 5]; 95] = [ - [0x00, 0x00, 0x00, 0x00, 0x00], // 32 Space - [0x00, 0x00, 0x5f, 0x00, 0x00], // 33 ! - [0x00, 0x07, 0x00, 0x07, 0x00], // 34 " - [0x14, 0x7f, 0x14, 0x7f, 0x14], // 35 # - [0x24, 0x2a, 0x7f, 0x2a, 0x12], // 36 $ - [0x23, 0x13, 0x08, 0x64, 0x62], // 37 % - [0x36, 0x49, 0x55, 0x22, 0x50], // 38 & - [0x00, 0x05, 0x03, 0x00, 0x00], // 39 ' - [0x00, 0x1c, 0x22, 0x41, 0x00], // 40 ( - [0x00, 0x41, 0x22, 0x1c, 0x00], // 41 ) - [0x14, 0x08, 0x3e, 0x08, 0x14], // 42 * - [0x08, 0x08, 0x3e, 0x08, 0x08], // 43 + - [0x00, 0x50, 0x30, 0x00, 0x00], // 44 , - [0x08, 0x08, 0x08, 0x08, 0x08], // 45 - - [0x00, 0x60, 0x60, 0x00, 0x00], // 46 . - [0x20, 0x10, 0x08, 0x04, 0x02], // 47 / - [0x3e, 0x51, 0x49, 0x45, 0x3e], // 48 0 - [0x00, 0x42, 0x7f, 0x40, 0x00], // 49 1 - [0x42, 0x61, 0x51, 0x49, 0x46], // 50 2 - [0x21, 0x41, 0x45, 0x4b, 0x31], // 51 3 - [0x18, 0x14, 0x12, 0x7f, 0x10], // 52 4 - [0x27, 0x45, 0x45, 0x45, 0x39], // 53 5 - [0x3c, 0x4a, 0x49, 0x49, 0x30], // 54 6 - [0x01, 0x71, 0x09, 0x05, 0x03], // 55 7 - [0x36, 0x49, 0x49, 0x49, 0x36], // 56 8 - [0x06, 0x49, 0x49, 0x29, 0x1e], // 57 9 - [0x00, 0x36, 0x36, 0x00, 0x00], // 58 : - [0x00, 0x56, 0x36, 0x00, 0x00], // 59 ; - [0x08, 0x14, 0x22, 0x41, 0x00], // 60 < - [0x14, 0x14, 0x14, 0x14, 0x14], // 61 = - [0x00, 0x41, 0x22, 0x14, 0x08], // 62 > - [0x02, 0x01, 0x51, 0x09, 0x06], // 63 ? - [0x32, 0x49, 0x79, 0x41, 0x3e], // 64 @ - [0x7e, 0x11, 0x11, 0x11, 0x7e], // 65 A - [0x7f, 0x49, 0x49, 0x49, 0x36], // 66 B - [0x3e, 0x41, 0x41, 0x41, 0x22], // 67 C - [0x7f, 0x41, 0x41, 0x22, 0x1c], // 68 D - [0x7f, 0x49, 0x49, 0x49, 0x41], // 69 E - [0x7f, 0x09, 0x09, 0x09, 0x01], // 70 F - [0x3e, 0x41, 0x49, 0x49, 0x7a], // 71 G - [0x7f, 0x08, 0x08, 0x08, 0x7f], // 72 H - [0x00, 0x41, 0x7f, 0x41, 0x00], // 73 I - [0x20, 0x40, 0x41, 0x3f, 0x01], // 74 J - [0x7f, 0x08, 0x14, 0x22, 0x41], // 75 K - [0x7f, 0x40, 0x40, 0x40, 0x40], // 76 L - [0x7f, 0x02, 0x0c, 0x02, 0x7f], // 77 M - [0x7f, 0x04, 0x08, 0x10, 0x7f], // 78 N - [0x3e, 0x41, 0x41, 0x41, 0x3e], // 79 O - [0x7f, 0x09, 0x09, 0x09, 0x06], // 80 P - [0x3e, 0x41, 0x51, 0x21, 0x5e], // 81 Q - [0x7f, 0x09, 0x19, 0x29, 0x46], // 82 R - [0x46, 0x49, 0x49, 0x49, 0x31], // 83 S - [0x01, 0x01, 0x7f, 0x01, 0x01], // 84 T - [0x3f, 0x40, 0x40, 0x40, 0x3f], // 85 U - [0x1f, 0x20, 0x40, 0x20, 0x1f], // 86 V - [0x3f, 0x40, 0x38, 0x40, 0x3f], // 87 W - [0x63, 0x14, 0x08, 0x14, 0x63], // 88 X - [0x07, 0x08, 0x70, 0x08, 0x07], // 89 Y - [0x61, 0x51, 0x49, 0x45, 0x43], // 90 Z - [0x00, 0x7f, 0x41, 0x41, 0x00], // 91 [ - [0x02, 0x04, 0x08, 0x10, 0x20], // 92 \ - [0x00, 0x41, 0x41, 0x7f, 0x00], // 93 ] - [0x04, 0x02, 0x01, 0x02, 0x04], // 94 ^ - [0x40, 0x40, 0x40, 0x40, 0x40], // 95 _ - [0x00, 0x01, 0x02, 0x04, 0x00], // 96 ` - [0x20, 0x54, 0x54, 0x54, 0x78], // 97 a - [0x7f, 0x48, 0x44, 0x44, 0x38], // 98 b - [0x38, 0x44, 0x44, 0x44, 0x20], // 99 c - [0x38, 0x44, 0x44, 0x48, 0x7f], // 100 d - [0x38, 0x54, 0x54, 0x54, 0x18], // 101 e - [0x08, 0x7e, 0x09, 0x01, 0x02], // 102 f - [0x18, 0xa4, 0xa4, 0xa4, 0x7c], // 103 g - [0x7f, 0x08, 0x04, 0x04, 0x78], // 104 h - [0x00, 0x44, 0x7d, 0x40, 0x00], // 105 i - [0x40, 0x80, 0x84, 0x7d, 0x00], // 106 j - [0x7f, 0x10, 0x28, 0x44, 0x00], // 107 k - [0x00, 0x41, 0x7f, 0x40, 0x00], // 108 l - [0x7c, 0x04, 0x18, 0x04, 0x78], // 109 m - [0x7c, 0x08, 0x04, 0x04, 0x78], // 110 n - [0x38, 0x44, 0x44, 0x44, 0x38], // 111 o - [0xfe, 0x14, 0x14, 0x14, 0x08], // 112 p - [0x08, 0x14, 0x14, 0x18, 0xfe], // 113 q - [0x7c, 0x08, 0x04, 0x04, 0x08], // 114 r - [0x48, 0x54, 0x54, 0x54, 0x20], // 115 s - [0x04, 0x3f, 0x44, 0x40, 0x20], // 116 t - [0x3c, 0x40, 0x40, 0x20, 0x7c], // 117 u - [0x1c, 0x20, 0x40, 0x20, 0x1c], // 118 v - [0x3c, 0x40, 0x30, 0x40, 0x3c], // 119 w - [0x44, 0x28, 0x10, 0x28, 0x44], // 120 x - [0x1c, 0xa0, 0xa0, 0xa0, 0x7c], // 121 y - [0x44, 0x64, 0x54, 0x4c, 0x44], // 122 z - [0x00, 0x08, 0x36, 0x41, 0x00], // 123 { - [0x00, 0x00, 0x7f, 0x00, 0x00], // 124 | - [0x00, 0x41, 0x36, 0x08, 0x00], // 125 } - [0x02, 0x01, 0x02, 0x04, 0x02], // 126 ~ -]; - -/// Draws a text string at (x, y) with the given scale and color. -/// It uses a horizontal grouping algorithm to drastically reduce OpenGL clear calls. -pub unsafe fn draw_text( - renderer: &mut Renderer, - text: &str, - start_x: i32, - start_y: i32, - rgba: Rgba, - scale: i32, -) { - renderer.set_color(rgba); - - let char_width = 5 * scale; - let char_space = scale; - - let mut current_x = start_x; - - for ch in text.chars() { - let ch_num = ch as usize; - if ch_num >= 32 && ch_num <= 126 { - let glyph = &FONT_5X7[ch_num - 32]; - - // Render each row grouping contiguous pixels horizontally - for row in 0..8 { - let mut col = 0; - while col < 5 { - // Check if current pixel is on - if (glyph[col] & (1 << row)) != 0 { - // Find how many contiguous pixels are on in this row - let mut width = 1; - while col + width < 5 && (glyph[col + width] & (1 << row)) != 0 { - width += 1; - } - - // Dispatch a unified block for grouped pixels - renderer.draw_rect( - current_x + (col as i32 * scale), - start_y + (row as i32 * scale), - (width as i32) * scale, - scale, - ); - - // Skip the pixels we just grouped - col += width; - } else { - col += 1; - } - } - } - } - current_x += char_width + char_space; - } -} - -/// Helper function to calculate the total width of a string in pixels at a given scale. -pub fn get_text_width(text: &str, scale: i32) -> i32 { - let char_width = 5 * scale; - let char_space = scale; - - let valid_chars = text - .chars() - .filter(|ch| { - let n = *ch as usize; - n >= 32 && n <= 126 - }) - .count() as i32; - - if valid_chars == 0 { - return 0; - } - - // Include space between characters, but not after the last one - (valid_chars * char_width) + ((valid_chars - 1) * char_space) -} diff --git a/client/src/graphic/gui.rs b/client/src/graphic/gui.rs index cb4e14d..304889f 100644 --- a/client/src/graphic/gui.rs +++ b/client/src/graphic/gui.rs @@ -1,58 +1,49 @@ +use crate::graphic::ui_engine::WindowAnimState; use crate::graphic::{hud, menu}; use egui::{Color32, Context, Margin, Rounding, Stroke, Style, Vec2, Visuals}; +use std::collections::HashMap; -pub fn render_all(ctx: &Context) { - // 1. Applichiamo il tuo "Theme" direttamente ai Visuals di Egui +pub fn render_all(ctx: &Context, window_anim_states: &mut HashMap) { let mut style = Style::default(); let mut visuals = Visuals::dark(); - // 1. Colori Base (Scuri e Piatti) - let bg_color = Color32::from_rgb(22, 22, 22); // Grigio scurissimo (quasi nero) - let panel_color = Color32::from_rgb(30, 30, 30); // Sfondo dei moduli - let accent_color = Color32::from_rgb(26, 171, 138); // Il classico Verde Acqua (Teal) di Vape - let border_color = Color32::from_rgb(45, 45, 45); // Bordino sottile - // Sfondi Finestre + let bg_color = Color32::from_rgb(18, 18, 18); + let panel_color = Color32::from_rgb(25, 25, 25); + let accent_color = Color32::from_rgb(26, 171, 138); // Vape's Teal + let border_color = Color32::from_rgb(35, 35, 35); + visuals.window_fill = bg_color; visuals.panel_fill = panel_color; - visuals.window_stroke = Stroke::new(1.0, border_color); // Bordo sottile di 1px + visuals.window_stroke = Stroke::new(1.0, border_color); - // 2. Colori Attivi (Quando uno slider o checkbox è attivo) visuals.selection.bg_fill = accent_color; visuals.selection.stroke = Stroke::NONE; - // 3. Stile Widget (Pulsanti, Background degli Slider) - // Inattivo - visuals.widgets.inactive.bg_fill = Color32::from_rgb(35, 35, 35); - visuals.widgets.inactive.rounding = Rounding::same(2.0); // Leggermente smussato - visuals.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(210, 210, 210)); // Testo chiaro + visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; + visuals.widgets.inactive.rounding = Rounding::ZERO; + visuals.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(210, 210, 210)); - // Hover (Passaggio del mouse) - visuals.widgets.hovered.bg_fill = Color32::from_rgb(45, 45, 45); - visuals.widgets.hovered.rounding = Rounding::same(2.0); + visuals.widgets.hovered.bg_fill = Color32::from_rgb(35, 35, 35); + visuals.widgets.hovered.rounding = Rounding::ZERO; visuals.widgets.hovered.fg_stroke = Stroke::new(1.0, Color32::WHITE); - // Click visuals.widgets.active.bg_fill = accent_color; - visuals.widgets.active.rounding = Rounding::same(2.0); + visuals.widgets.active.rounding = Rounding::ZERO; - // 4. Arrotondamento Generale (Vape è squadrata) - visuals.window_rounding = Rounding::same(3.0); + visuals.window_rounding = Rounding::ZERO; style.visuals = visuals; - // 5. Spaziature (Vape è compattissima) - style.spacing.item_spacing = Vec2::new(8.0, 4.0); // Spazio ridotto tra gli elementi - style.spacing.window_margin = Margin::symmetric(0.0, 0.0); // Rimuove il padding ai bordi della finestra! - style.spacing.button_padding = Vec2::new(4.0, 2.0); + style.spacing.item_spacing = Vec2::new(0.0, 0.0); + style.spacing.window_margin = Margin::symmetric(0.0, 0.0); + style.spacing.button_padding = Vec2::new(5.0, 5.0); ctx.set_style(style); - // 2. Disegniamo l'HUD (sempre visibile) hud::draw(ctx); - // 3. Disegniamo il ClickGUI (solo se aperto) let is_open = crate::graphic::input::GUI_OPEN.load(std::sync::atomic::Ordering::Relaxed); let anim_progress = ctx.animate_bool(egui::Id::new("menu_open_anim"), is_open); if anim_progress > 0.0 { - menu::draw(ctx, anim_progress); + menu::draw(ctx, anim_progress, window_anim_states); } } diff --git a/client/src/graphic/hook.rs b/client/src/graphic/hook.rs index 69b7e6c..422f2cf 100644 --- a/client/src/graphic/hook.rs +++ b/client/src/graphic/hook.rs @@ -116,14 +116,11 @@ unsafe fn on_frame() { render_overlay(); } -use crate::graphic::render::Renderer; +// use crate::graphic::render::Renderer; - Removed // === RENDERING LOGIC === unsafe fn render_overlay() { - if Minecraft::instance() - .get_player() - .is_err() - { + if Minecraft::instance().get_player().is_err() { return; } @@ -135,7 +132,7 @@ unsafe fn render_overlay() { // Call our new custom OpenGL UI system //crate::graphic::ui::render_gui(&mut renderer); - + crate::graphic::ui_engine::render_egui_ui(); // the state is restored automatically when `renderer` goes out of scope and drops diff --git a/client/src/graphic/hud.rs b/client/src/graphic/hud.rs index d202b70..ec36100 100644 --- a/client/src/graphic/hud.rs +++ b/client/src/graphic/hud.rs @@ -1,21 +1,22 @@ use crate::client::DarkClient; -use egui::{Align2, Color32, Context, Id, RichText}; +use egui::{Color32, Context, Id, RichText}; pub fn draw(ctx: &Context) { - // --- WATERMARK (In alto a sinistra) --- egui::Area::new(Id::new("hud_watermark")) .fixed_pos(egui::pos2(5.0, 5.0)) - .interactable(false) // Non blocca i click + .interactable(false) .show(ctx, |ui| { ui.add( egui::Label::new( - RichText::new("DarkClient").color(Color32::YELLOW).size(24.0).strong() + RichText::new("DarkClient") + .color(Color32::YELLOW) + .size(24.0) + .strong(), ) - .wrap_mode(egui::TextWrapMode::Extend) + .wrap_mode(egui::TextWrapMode::Extend), ); }); - // --- ARRAYLIST / MODULI ATTIVI (Sotto il watermark) --- egui::Area::new(Id::new("hud_arraylist")) .fixed_pos(egui::pos2(5.0, 35.0)) .interactable(false) @@ -48,12 +49,10 @@ pub fn draw(ctx: &Context) { for (i, mod_name) in active_mods.iter().enumerate() { let color = colors[i % colors.len()]; ui.add( - egui::Label::new( - RichText::new(mod_name).color(color).size(16.0) - ) - .wrap_mode(egui::TextWrapMode::Extend) // <--- Disabilita il word-wrap! + egui::Label::new(RichText::new(mod_name).color(color).size(16.0)) + .wrap_mode(egui::TextWrapMode::Extend), ); } } }); -} \ No newline at end of file +} diff --git a/client/src/graphic/menu.rs b/client/src/graphic/menu.rs index 63bc459..fae3193 100644 --- a/client/src/graphic/menu.rs +++ b/client/src/graphic/menu.rs @@ -1,9 +1,14 @@ use crate::client::DarkClient; +use crate::graphic::ui_engine::WindowAnimState; use crate::module::{ModuleCategory, ModuleSetting}; -use egui::{Align2, Color32, Context, Id, Pos2, Rect, Rounding, Sense, Stroke, Vec2}; - -pub fn draw(ctx: &Context, anim_progress: f32) { - // 1. Sfondo scuro semitrasparente +use egui::{Align2, Color32, Context, Id, Pos2, Rect, Rounding, Sense, Vec2}; +use std::collections::HashMap; + +pub fn draw( + ctx: &Context, + anim_progress: f32, + window_anim_states: &mut HashMap, +) { egui::Area::new(Id::new("dark_overlay")) .fixed_pos(Pos2::ZERO) .order(egui::Order::Background) @@ -16,7 +21,6 @@ pub fn draw(ctx: &Context, anim_progress: f32) { ); }); - // 2. Pulsanti Globali (In alto a destra) egui::Area::new(Id::new("global_buttons")) .anchor(Align2::RIGHT_TOP, Vec2::new(-10.0, 10.0)) .show(ctx, |ui| { @@ -29,13 +33,11 @@ pub fn draw(ctx: &Context, anim_progress: f32) { std::thread::spawn(|| crate::graphic::ui_engine::call_panic()); } if ui.button("Reset UI").clicked() { - // Egui salva le posizioni in memoria. Per resettarle: ctx.memory_mut(|mem| mem.reset_areas()); } }); }); - // 3. Finestre per ogni Categoria let client_modules_guard = DarkClient::instance().modules.read().unwrap(); let categories = [ @@ -55,200 +57,273 @@ pub fn draw(ctx: &Context, anim_progress: f32) { let row_height = 280.0; for category in categories.iter() { - // Se la finestra successiva sfora lo schermo, scendiamo di una "riga" if curr_x + win_w > logical_width && curr_x > 50.0 { curr_x = 50.0; - curr_y += row_height; // Altezza stimata per evitare che si tocchino scendendo + curr_y += row_height; } let title = category.display_name(); - // Offset iniziale animato - let y_offset = 20.0 * (1.0 - anim_progress); - let start_pos = Pos2::new(curr_x, curr_y + y_offset); - - egui::Window::new(title) - .id(Id::new(title)) - .default_pos(start_pos) - .default_width(win_w) - .min_width(win_w) - .max_width(win_w) - .resizable(false) - .collapsible(true) - .show(ctx, |ui| { - ui.set_opacity(anim_progress); + let area_id = Id::new(title).with("area"); - // Filtriamo i moduli per questa finestra - let mut cat_modules: Vec<_> = client_modules_guard - .values() - .filter(|m| m.lock().unwrap().get_module_data().category == *category) - .collect(); - cat_modules.sort_by(|a, b| { - a.lock().unwrap().get_module_data().name.cmp(&b.lock().unwrap().get_module_data().name) - }); + let mut target_pos = Pos2::new(curr_x, curr_y); + target_pos = ctx + .data(|d| d.get_temp::(area_id)) + .unwrap_or(target_pos); - for module in cat_modules { - let (mod_name, is_enabled) = { - let mut lock = module.lock().unwrap(); - let data = lock.get_module_data_mut(); - let mod_name = data.name.clone(); - let is_enabled = data.enabled; - (mod_name, is_enabled) - }; - - // Creiamo una riga custom per gestire click SINISTRO (Toggle) e DESTRO (Espandi) - let (rect, response) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 22.0), Sense::click()); - - // Gestione Colori Background in base a hover - let bg_color = if response.hovered() { - Color32::from_rgb(40, 40, 40) // Illumina un po' all'hover - } else { - Color32::TRANSPARENT // Sfondo invisibile, prende il colore della finestra - }; - ui.painter().rect_filled(rect, Rounding::ZERO, bg_color); - - // Testo Modulo - let text_color = if is_enabled { - Color32::from_rgb(26, 171, 138) // Accent Color - } else { - Color32::from_rgb(200, 200, 200) - }; let text_pos = rect.min + Vec2::new(5.0, 3.0); - ui.painter().text( - rect.min + egui::vec2(8.0, 4.0), // Padding sinistro - egui::Align2::LEFT_TOP, - &mod_name, - egui::FontId::proportional(14.0), - text_color, - ); + let dt = ctx.input(|i| i.stable_dt).min(0.1); - let mut lock = module.lock().unwrap(); + let win_state = window_anim_states + .entry(title.to_string()) + .or_insert(WindowAnimState { + actual_pos: target_pos, + velocity: Vec2::ZERO, + }); - let is_expanded_id = Id::new(&mod_name).with("expanded"); - let mut is_expanded = ui.data(|d| d.get_temp::(is_expanded_id).unwrap_or(false)); + if anim_progress < 0.01 { + win_state.actual_pos = target_pos; + win_state.velocity = Vec2::ZERO; + } else { + let stiffness = 280.0; + let damping = 18.0; - // Definiamo un'area immaginaria di 25x20 pixel sull'estrema destra del rettangolo - let arrow_rect = Rect::from_min_max( - rect.max - Vec2::new(25.0, 20.0), - rect.max, - ); + let displacement = win_state.actual_pos - target_pos; + let spring_force = -stiffness * displacement; + let damping_force = -damping * win_state.velocity; + let acceleration = spring_force + damping_force; - // Gestione avanzata del Click - if response.clicked() { - // Prendiamo le coordinate esatte del click - let click_pos = response.interact_pointer_pos().unwrap_or(Pos2::ZERO); + win_state.velocity += acceleration * dt; + win_state.actual_pos += win_state.velocity * dt; + } + + let y_offset_spawn = Vec2::new(0.0, 20.0 * (1.0 - anim_progress)); + let final_render_pos = win_state.actual_pos + y_offset_spawn; + + egui::Area::new(area_id) + .current_pos(final_render_pos) // The whole UI draws here + .order(egui::Order::Middle) + .show(ctx, |ui| { + ui.set_opacity(anim_progress); - // CASO 1: Click Destro, OPPURE Click Sinistro proprio sopra la freccetta - if response.clicked_by(egui::PointerButton::Secondary) || - (response.clicked_by(egui::PointerButton::Primary) && arrow_rect.contains(click_pos)) { + let frame = egui::Frame::window(&ctx.style()) + .fill(Color32::from_rgb(22, 22, 22)) + .stroke(egui::Stroke::new(1.0, Color32::from_rgb(35, 35, 35))) + .rounding(egui::Rounding::ZERO) + .inner_margin(egui::Margin::symmetric(0.0, 0.0)); - is_expanded = !is_expanded; - ui.data_mut(|d| d.insert_temp(is_expanded_id, is_expanded)); + frame.show(ui, |ui| { + ui.set_min_width(win_w); + ui.set_max_width(win_w); - // CASO 2: Click Sinistro sul resto del corpo del bottone - } else if response.clicked_by(egui::PointerButton::Primary) { - let new_state = !is_enabled; - lock.get_module_data_mut().set_enabled(new_state); - if new_state { let _ = lock.on_start(); } else { let _ = lock.on_stop(); } - } + // Title Bar + let (title_rect, title_resp) = + ui.allocate_exact_size(Vec2::new(win_w, 24.0), Sense::drag()); + + if title_resp.dragged() { + target_pos += title_resp.drag_delta(); + ctx.data_mut(|d| d.insert_temp(area_id, target_pos)); } - let data = lock.get_module_data_mut(); + // Draw Title text + ui.painter().text( + title_rect.min + Vec2::new(8.0, 5.0), + Align2::LEFT_TOP, + title, + egui::FontId::proportional(14.0), + Color32::from_rgb(26, 171, 138), // Teal Accent for headers + ); + + // Separator line + ui.painter().line_segment( + [title_rect.left_bottom(), title_rect.right_bottom()], + egui::Stroke::new(1.0, Color32::from_rgb(35, 35, 35)), + ); + + let mut cat_modules: Vec<_> = client_modules_guard + .values() + .filter(|m| m.lock().unwrap().get_module_data().category == *category) + .collect(); + cat_modules.sort_by(|a, b| { + a.lock() + .unwrap() + .get_module_data() + .name + .cmp(&b.lock().unwrap().get_module_data().name) + }); + + for module in cat_modules { + let (mod_name, is_enabled) = { + let lock = module.lock().unwrap(); + let data = lock.get_module_data(); + (data.name.clone(), data.enabled) + }; - // Se ha settaggi, gestiamo l'espansione - if !data.settings.is_empty() { - let arrow = if is_expanded { "v" } else { ">" }; + let (rect, response) = + ui.allocate_exact_size(Vec2::new(win_w, 22.0), Sense::click()); + + let bg_color = if response.hovered() { + Color32::from_rgb(37, 37, 37) + } else { + Color32::TRANSPARENT + }; + ui.painter().rect_filled(rect, Rounding::ZERO, bg_color); - // Per feedback visivo, se il mouse è esattamente sopra l'area della freccia, la illuminiamo - let arrow_color = if arrow_rect.contains(ui.ctx().pointer_hover_pos().unwrap_or(Pos2::ZERO)) { - Color32::WHITE + let text_color = if is_enabled { + Color32::from_rgb(26, 171, 138) } else { - Color32::GRAY + Color32::from_rgb(200, 200, 200) }; ui.painter().text( - rect.max - Vec2::new(15.0, 17.0), + rect.min + egui::vec2(8.0, 4.0), egui::Align2::LEFT_TOP, - arrow, + &mod_name, egui::FontId::proportional(14.0), - arrow_color, + text_color, ); - // Se è espanso, mostriamo i settaggi usando i widget nativi di egui - if is_expanded { - let settings_frame = egui::Frame::none() - .fill(Color32::from_rgb(15, 15, 15)) // Più scuro della finestra - .inner_margin(egui::Margin::symmetric(8.0, 6.0)); - - settings_frame.show(ui, |ui| { - ui.vertical(|ui| { - // 1. Diciamo agli slider di essere larghi solo 60 pixel - ui.style_mut().spacing.slider_width = 60.0; - - // 2. Se un testo è troppo lungo, lo tagliamo coi puntini (...) anziché allargare la tab - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - - // 3. Assicuriamoci che i combobox non esplodano in larghezza - ui.style_mut().spacing.interact_size.x = 80.0; + let mut lock = module.lock().unwrap(); + let is_expanded_id = Id::new(&mod_name).with("expanded"); + let mut is_expanded = + ui.data(|d| d.get_temp::(is_expanded_id).unwrap_or(false)); + + let arrow_rect = + Rect::from_min_max(rect.max - Vec2::new(25.0, 20.0), rect.max); + + if response.clicked() { + let click_pos = response.interact_pointer_pos().unwrap_or(Pos2::ZERO); + if response.clicked_by(egui::PointerButton::Secondary) + || (response.clicked_by(egui::PointerButton::Primary) + && arrow_rect.contains(click_pos)) + { + is_expanded = !is_expanded; + ui.data_mut(|d| d.insert_temp(is_expanded_id, is_expanded)); + } else if response.clicked_by(egui::PointerButton::Primary) { + let new_state = !is_enabled; + lock.get_module_data_mut().set_enabled(new_state); + if new_state { + let _ = lock.on_start(); + } else { + let _ = lock.on_stop(); + } + } + } - for setting in &mut data.settings { - match setting { - ModuleSetting::Toggle { name, value } => { - ui.checkbox(value, name.as_str()); - } - ModuleSetting::Slider { name, value, min, max } => { - ui.vertical(|ui| { - // Riga 1: Nome a sinistra, Valore a destra - ui.horizontal(|ui| { - ui.label(name.as_str()); + let data = lock.get_module_data_mut(); - // Spinge il valore tutto a destra - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // Formattiamo a 2 decimali per evitare numeri infiniti - ui.label(format!("{:.2}", value)); + if !data.settings.is_empty() { + let arrow = if is_expanded { "v" } else { ">" }; + let arrow_color = if arrow_rect + .contains(ui.ctx().pointer_hover_pos().unwrap_or(Pos2::ZERO)) + { + Color32::WHITE + } else { + Color32::GRAY + }; + + ui.painter().text( + rect.max - Vec2::new(15.0, 17.0), + egui::Align2::LEFT_TOP, + arrow, + egui::FontId::proportional(14.0), + arrow_color, + ); + + if is_expanded { + let settings_frame = egui::Frame::none() + .fill(Color32::from_rgb(15, 15, 15)) + .inner_margin(egui::Margin::symmetric(8.0, 6.0)); + + settings_frame.show(ui, |ui| { + ui.vertical(|ui| { + ui.style_mut().spacing.slider_width = 60.0; + ui.style_mut().wrap_mode = + Some(egui::TextWrapMode::Truncate); + ui.style_mut().spacing.interact_size.x = 80.0; + + for setting in &mut data.settings { + match setting { + ModuleSetting::Toggle { name, value } => { + ui.checkbox(value, name.as_str()); + } + ModuleSetting::Slider { + name, + value, + min, + max, + } => { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label(name.as_str()); + ui.with_layout( + egui::Layout::right_to_left( + egui::Align::Center, + ), + |ui| { + ui.label(format!( + "{:.2}", + value + )); + }, + ); }); + let available_w = ui.available_width(); + ui.style_mut().spacing.slider_width = + available_w; + ui.add( + egui::Slider::new( + value, + min.clone()..=max.clone(), + ) + .show_value(false) + .text(""), + ); }); - - // Riga 2: Lo slider vero e proprio - // Diciamo allo slider di prendere tutta la larghezza rimasta e nascondiamo - // il numerino di default (visto che l'abbiamo appena disegnato noi sopra) - let available_w = ui.available_width(); - ui.style_mut().spacing.slider_width = available_w; - - ui.add( - egui::Slider::new(value, min.clone()..=max.clone()) - .show_value(false) // Nasconde il testo del valore integrato - .text("") // Rimuove la label integrata - ); - }); - } - ModuleSetting::Choice { name, value, options } => { - ui.vertical(|ui| { - ui.label(name.as_str()); // Nome sopra - - // Combobox largo tutto lo spazio sotto - egui::ComboBox::from_id_salt(name.as_str()) // id_source invece di label per non mettere il testo a lato - .width(ui.available_width()) - .selected_text(options.get(*value).map(|s| s.as_str()).unwrap_or("??")) - .show_ui(ui, |ui| { - for (idx, opt) in options.iter().enumerate() { - ui.selectable_value(value, idx, opt.as_str()); - } - }); - }); - } - ModuleSetting::Color { name, .. } => { - ui.label(format!("{}: [Color Settings soon]", name)); + } + ModuleSetting::Choice { + name, + value, + options, + } => { + ui.vertical(|ui| { + ui.label(name.as_str()); + egui::ComboBox::from_id_salt(name.as_str()) + .width(ui.available_width()) + .selected_text( + options + .get(*value) + .map(|s| s.as_str()) + .unwrap_or("??"), + ) + .show_ui(ui, |ui| { + for (idx, opt) in + options.iter().enumerate() + { + ui.selectable_value( + value, + idx, + opt.as_str(), + ); + } + }); + }); + } + ModuleSetting::Color { name, .. } => { + ui.label(format!( + "{}: [Color Settings soon]", + name + )); + } } } - } - ui.add_space(5.0); + ui.add_space(5.0); + }); }); - }); + } } } - } + }); }); - // Avanziamo verso destra per la prossima categoria curr_x += win_w + gap_x; } } diff --git a/client/src/graphic/mod.rs b/client/src/graphic/mod.rs index 04e8819..7a69fc4 100644 --- a/client/src/graphic/mod.rs +++ b/client/src/graphic/mod.rs @@ -1,9 +1,6 @@ -pub mod color; -pub mod font; pub mod gui; pub mod hook; pub mod hud; pub mod input; pub mod menu; -pub mod render; pub mod ui_engine; diff --git a/client/src/graphic/render.rs b/client/src/graphic/render.rs deleted file mode 100644 index 634b74f..0000000 --- a/client/src/graphic/render.rs +++ /dev/null @@ -1,330 +0,0 @@ -use crate::{ - gl, - graphic::color::{Rgb, Rgba}, -}; -use std::ffi::CString; - -static mut SHADER_PROGRAM: u32 = 0; -static mut VAO: u32 = 0; -static mut VBO: u32 = 0; -static mut INITIALIZED: bool = false; - -const VERTEX_SHADER: &str = r#" -#version 150 core -in vec2 position; -uniform vec4 color; -out vec4 fragColor; -void main() { - gl_Position = vec4(position, 0.0, 1.0); - fragColor = color; -} -"#; - -const FRAGMENT_SHADER: &str = r#" -#version 150 core -in vec4 fragColor; -out vec4 outColor; -void main() { - outColor = fragColor; -} -"#; - -unsafe fn compile_shader(type_: u32, source: &str) -> u32 { - let shader = gl::CreateShader(type_); - let c_str = CString::new(source.as_bytes()).unwrap(); - gl::ShaderSource(shader, 1, &c_str.as_ptr(), std::ptr::null()); - gl::CompileShader(shader); - - let mut success = 0; - gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut success); - if success == 0 { - let mut len = 0; - gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut len); - let mut buffer = vec![0u8; len as usize]; - gl::GetShaderInfoLog( - shader, - len, - std::ptr::null_mut(), - buffer.as_mut_ptr() as *mut i8, - ); - log::error!("Shader compile error: {}", String::from_utf8_lossy(&buffer)); - } - shader -} - -unsafe fn setup_opengl() { - let vs = compile_shader(gl::VERTEX_SHADER, VERTEX_SHADER); - let fs = compile_shader(gl::FRAGMENT_SHADER, FRAGMENT_SHADER); - - SHADER_PROGRAM = gl::CreateProgram(); - gl::AttachShader(SHADER_PROGRAM, vs); - gl::AttachShader(SHADER_PROGRAM, fs); - gl::LinkProgram(SHADER_PROGRAM); - - let mut success = 0; - gl::GetProgramiv(SHADER_PROGRAM, gl::LINK_STATUS, &mut success); - if success == 0 { - let mut len = 0; - gl::GetProgramiv(SHADER_PROGRAM, gl::INFO_LOG_LENGTH, &mut len); - let mut buffer = vec![0u8; len as usize]; - gl::GetProgramInfoLog( - SHADER_PROGRAM, - len, - std::ptr::null_mut(), - buffer.as_mut_ptr() as *mut i8, - ); - log::error!("Program link error: {}", String::from_utf8_lossy(&buffer)); - } - - gl::GenVertexArrays(1, std::ptr::addr_of_mut!(VAO)); - gl::GenBuffers(1, std::ptr::addr_of_mut!(VBO)); - - gl::BindVertexArray(VAO); - gl::BindBuffer(gl::ARRAY_BUFFER, VBO); - - // Pre-allocate buffer for 4 vertices (2 floats each) - gl::BufferData( - gl::ARRAY_BUFFER, - (4 * 2 * std::mem::size_of::()) as isize, - std::ptr::null(), - gl::DYNAMIC_DRAW, - ); - - let pos_loc = gl::GetAttribLocation(SHADER_PROGRAM, CString::new("position").unwrap().as_ptr()); - if pos_loc >= 0 { - gl::VertexAttribPointer(pos_loc as u32, 2, gl::FLOAT, gl::FALSE, 8, std::ptr::null()); - gl::EnableVertexAttribArray(pos_loc as u32); - } - - gl::BindBuffer(gl::ARRAY_BUFFER, 0); - gl::BindVertexArray(0); - - INITIALIZED = true; -} - -/// A state-restoring OpenGL renderer that supports 2D drawing with alpha blending natively. -pub struct Renderer { - pub screen_width: i32, - pub screen_height: i32, - old_blend_src_rgb: i32, - old_blend_dst_rgb: i32, - old_blend_src_alpha: i32, - old_blend_dst_alpha: i32, - is_blend_on: bool, - old_program: i32, - old_vao: i32, - old_vbo: i32, - old_depth_test: bool, - old_cull_face: bool, - old_scissor_test: bool, - current_color: Rgba, -} - -impl Renderer { - pub unsafe fn new() -> Self { - if !INITIALIZED { - setup_opengl(); - } - - let mut viewport = [0; 4]; - gl::GetIntegerv(gl::VIEWPORT, viewport.as_mut_ptr()); - let screen_width = viewport[2]; - let screen_height = viewport[3]; - - // Backup states - let is_blend_on = gl::IsEnabled(gl::BLEND) == gl::TRUE; - let mut old_blend_src_rgb = 0; - let mut old_blend_dst_rgb = 0; - let mut old_blend_src_alpha = 0; - let mut old_blend_dst_alpha = 0; - gl::GetIntegerv(gl::BLEND_SRC_RGB, &mut old_blend_src_rgb); - gl::GetIntegerv(gl::BLEND_DST_RGB, &mut old_blend_dst_rgb); - gl::GetIntegerv(gl::BLEND_SRC_ALPHA, &mut old_blend_src_alpha); - gl::GetIntegerv(gl::BLEND_DST_ALPHA, &mut old_blend_dst_alpha); - - let mut old_program = 0; - gl::GetIntegerv(gl::CURRENT_PROGRAM, &mut old_program); - let mut old_vao = 0; - gl::GetIntegerv(gl::VERTEX_ARRAY_BINDING, &mut old_vao); - let mut old_vbo = 0; - gl::GetIntegerv(gl::ARRAY_BUFFER_BINDING, &mut old_vbo); - - let old_depth_test = gl::IsEnabled(gl::DEPTH_TEST) == gl::TRUE; - let old_cull_face = gl::IsEnabled(gl::CULL_FACE) == gl::TRUE; - let old_scissor_test = gl::IsEnabled(gl::SCISSOR_TEST) == gl::TRUE; - - // Apply our states - gl::Enable(gl::BLEND); - gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); - gl::Disable(gl::DEPTH_TEST); - gl::Disable(gl::CULL_FACE); - - gl::UseProgram(SHADER_PROGRAM); - gl::BindVertexArray(VAO); - // Ensure VBO is bound for drawing - gl::BindBuffer(gl::ARRAY_BUFFER, VBO); - - Self { - screen_width, - screen_height, - is_blend_on, - old_blend_src_rgb, - old_blend_dst_rgb, - old_blend_src_alpha, - old_blend_dst_alpha, - old_program, - old_vao, - old_vbo, - old_depth_test, - old_cull_face, - old_scissor_test, - current_color: Rgba::new(Rgb::new(1.0, 1.0, 1.0), 1.0), - } - } - - pub unsafe fn set_color(&mut self, rgba: Rgba) { - self.current_color = rgba; - } - - pub unsafe fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32) { - let sc_w = self.screen_width as f32; - let sc_h = self.screen_height as f32; - - let ndc_x1 = (x as f32 / sc_w) * 2.0 - 1.0; - let ndc_y1 = 1.0 - (y as f32 / sc_h) * 2.0; - let ndc_x2 = ((x + w) as f32 / sc_w) * 2.0 - 1.0; - let ndc_y2 = 1.0 - ((y + h) as f32 / sc_h) * 2.0; - - let vertices: [f32; 8] = [ - ndc_x1, ndc_y1, // Top-left - ndc_x1, ndc_y2, // Bottom-left - ndc_x2, ndc_y1, // Top-right - ndc_x2, ndc_y2, // Bottom-right - ]; - - // Send new vertices to VBO - gl::BufferSubData( - gl::ARRAY_BUFFER, - 0, - (vertices.len() * std::mem::size_of::()) as isize, - vertices.as_ptr() as *const _, - ); - - // Upload uniform color - let color_loc = - gl::GetUniformLocation(SHADER_PROGRAM, CString::new("color").unwrap().as_ptr()); - if color_loc >= 0 { - gl::Uniform4f( - color_loc, - self.current_color.r, - self.current_color.g, - self.current_color.b, - self.current_color.a, - ); - } - - // Draw as Triangle Strip (4 vertices make 2 triangles / 1 quad) - gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4); - } - - pub unsafe fn enable_scissor(&self, x: i32, y: i32, w: i32, h: i32) { - gl::Enable(gl::SCISSOR_TEST); - let bottom_y = self.screen_height - (y + h); - gl::Scissor(x, bottom_y, w, h); - } - - pub unsafe fn disable_scissor(&self) { - gl::Disable(gl::SCISSOR_TEST); - } - - pub unsafe fn draw_quad( - &mut self, - x1: f32, - y1: f32, - x2: f32, - y2: f32, - x3: f32, - y3: f32, - x4: f32, - y4: f32, - ) { - let sc_w = self.screen_width as f32; - let sc_h = self.screen_height as f32; - - let ndc_x1 = (x1 / sc_w) * 2.0 - 1.0; - let ndc_y1 = 1.0 - (y1 / sc_h) * 2.0; - - let ndc_x2 = (x2 / sc_w) * 2.0 - 1.0; - let ndc_y2 = 1.0 - (y2 / sc_h) * 2.0; - - let ndc_x3 = (x3 / sc_w) * 2.0 - 1.0; - let ndc_y3 = 1.0 - (y3 / sc_h) * 2.0; - - let ndc_x4 = (x4 / sc_w) * 2.0 - 1.0; - let ndc_y4 = 1.0 - (y4 / sc_h) * 2.0; - - let vertices: [f32; 8] = [ - ndc_x1, ndc_y1, // Top-left - ndc_x3, ndc_y3, // Bottom-left - ndc_x2, ndc_y2, // Top-right - ndc_x4, ndc_y4, // Bottom-right - ]; - - // Send new vertices to VBO - gl::BufferSubData( - gl::ARRAY_BUFFER, - 0, - (vertices.len() * std::mem::size_of::()) as isize, - vertices.as_ptr() as *const _, - ); - - // Upload uniform color - let color_loc = - gl::GetUniformLocation(SHADER_PROGRAM, CString::new("color").unwrap().as_ptr()); - if color_loc >= 0 { - gl::Uniform4f( - color_loc, - self.current_color.r, - self.current_color.g, - self.current_color.b, - self.current_color.a, - ); - } - - // Draw as Triangle Strip - gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4); - } -} - -impl Drop for Renderer { - fn drop(&mut self) { - unsafe { - if self.old_depth_test { - gl::Enable(gl::DEPTH_TEST); - } - if self.old_cull_face { - gl::Enable(gl::CULL_FACE); - } - if !self.old_scissor_test { - gl::Disable(gl::SCISSOR_TEST); - } else { - gl::Enable(gl::SCISSOR_TEST); - } - - if !self.is_blend_on { - gl::Disable(gl::BLEND); - } else { - gl::BlendFuncSeparate( - self.old_blend_src_rgb as u32, - self.old_blend_dst_rgb as u32, - self.old_blend_src_alpha as u32, - self.old_blend_dst_alpha as u32, - ); - } - - gl::BindBuffer(gl::ARRAY_BUFFER, self.old_vbo as u32); - gl::BindVertexArray(self.old_vao as u32); - gl::UseProgram(self.old_program as u32); - } - } -} diff --git a/client/src/graphic/ui_engine.rs b/client/src/graphic/ui_engine.rs index 30735af..8d6289d 100644 --- a/client/src/graphic/ui_engine.rs +++ b/client/src/graphic/ui_engine.rs @@ -4,16 +4,23 @@ use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; use egui::Context; use egui_glow::Painter; use lazy_static::lazy_static; +use std::collections::HashMap; use std::sync::atomic::Ordering; use std::sync::Mutex; +#[derive(Clone, Copy)] +pub struct WindowAnimState { + pub actual_pos: egui::Pos2, + pub velocity: egui::Vec2, +} + pub struct EguiState { pub ctx: Context, pub painter: Painter, pub last_left_down: bool, pub last_right_down: bool, pub last_mouse_pos: egui::Pos2, - pub window_pos: egui::Pos2, + pub window_anim_states: HashMap, } lazy_static! { @@ -27,16 +34,14 @@ pub fn gather_egui_inputs( scale_factor: f32, ) -> egui::RawInput { let mut raw_input = egui::RawInput::default(); - // 1. Configura il Viewport principale con il nostro moltiplicatore di scala + let mut viewport_info = egui::ViewportInfo::default(); viewport_info.native_pixels_per_point = Some(scale_factor); - // Inseriamo le info nella mappa usando l'ID di default (ROOT) raw_input .viewports .insert(egui::ViewportId::ROOT, viewport_info); - // 2. Passiamo le coordinate LOGICHE (divise per la scala) let logical_width = screen_width / scale_factor; let logical_height = screen_height / scale_factor; @@ -45,7 +50,6 @@ pub fn gather_egui_inputs( egui::pos2(logical_width, logical_height), )); - // Se la GUI è chiusa, restituiamo input vuoti in modo che egui non interagisca if !GUI_OPEN.load(Ordering::Relaxed) { return raw_input; } @@ -56,7 +60,6 @@ pub fn gather_egui_inputs( (mouse.y as f32) / scale_factor, ); - // 1. Movimento del Mouse if current_pos != state.last_mouse_pos { raw_input .events @@ -64,18 +67,16 @@ pub fn gather_egui_inputs( state.last_mouse_pos = current_pos; } - // 2. Click Sinistro (Transizione Su/Giù) if mouse.left_down != state.last_left_down { raw_input.events.push(egui::Event::PointerButton { pos: current_pos, button: egui::PointerButton::Primary, - pressed: mouse.left_down, // true = appena premuto, false = appena rilasciato + pressed: mouse.left_down, modifiers: Default::default(), }); state.last_left_down = mouse.left_down; } - // 3. Click Destro (Transizione Su/Giù) if mouse.right_down != state.last_right_down { raw_input.events.push(egui::Event::PointerButton { pos: current_pos, @@ -99,7 +100,6 @@ pub unsafe fn render_egui_ui() { let mut state_guard = EGUI_STATE.lock().unwrap(); - // 1. Inizializzazione Aggiornata if state_guard.is_none() { let gl = glow::Context::from_loader_function(|s| { crate::graphic::hook::get_proc_address(s) as *const _ @@ -107,7 +107,16 @@ pub unsafe fn render_egui_ui() { let gl = std::sync::Arc::new(gl); let ctx = egui::Context::default(); - // CORREZIONE 1: Aggiunto `false` per il dithering finale + let fonts = egui::FontDefinitions::default(); + ctx.set_fonts(fonts); + + unsafe { + crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); + crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); + crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_PIXELS, 0); + crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_ROWS, 0); + } + let painter = egui_glow::Painter::new(gl, "", None, false).unwrap(); *state_guard = Some(EguiState { @@ -116,48 +125,36 @@ pub unsafe fn render_egui_ui() { last_left_down: false, last_right_down: false, last_mouse_pos: egui::pos2(0.0, 0.0), - // Inizializziamo la finestra al centro dello schermo - window_pos: egui::pos2(screen_width / 2.0 - 200.0, screen_height / 2.0 - 150.0), + window_anim_states: HashMap::new(), }); } let state = state_guard.as_mut().unwrap(); let raw_input = gather_egui_inputs(state, screen_width, screen_height, scale_factor); - let is_open = GUI_OPEN.load(Ordering::Relaxed); - - // Cloniamo il Context. È leggerissimo (usa Arc internamente) e ci permette - // di modificare `state` (come state.window_pos) dentro la closure `run`. let ctx = state.ctx.clone(); - // CORREZIONE 2: ctx.run invece di begin_frame / end_frame let full_output = ctx.run(raw_input, |ctx| { - crate::graphic::gui::render_all(ctx); - }); // Fine di ctx.run + crate::graphic::gui::render_all(ctx, &mut state.window_anim_states); + }); - // Il resto del rendering OpenGL rimane identico, full_output ora viene da ctx.run let clipped_primitives = state .ctx .tessellate(full_output.shapes, full_output.pixels_per_point); - // --- INIZIO FIX GEROGLIFICI --- - // Resettiamo lo stato di unpack dei pixel che Minecraft spesso corrompe - // prima di far caricare le texture dei font a egui + // Reset pixel unpack state that is often corrupted by Minecraft unsafe { crate::gl::ActiveTexture(crate::gl::TEXTURE0); - crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); // Egui preferisce l'allineamento a 1 byte - crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); // Questo è il colpevole principale al 99%! + crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); + crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_PIXELS, 0); crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_ROWS, 0); - // Egui_glow salva e ripristina lo stato di blend e depth, ma per sicurezza: crate::gl::Disable(crate::gl::CULL_FACE); crate::gl::Disable(crate::gl::DEPTH_TEST); crate::gl::Enable(crate::gl::BLEND); } - // --- FINE FIX --- - // Ora disegniamo in sicurezza state.painter.paint_and_update_textures( [screen_width as u32, screen_height as u32], full_output.pixels_per_point, From 2301ec4beccdedb7fdf4dd16f814f0d4b6840f9c Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Sun, 22 Feb 2026 12:33:29 +0100 Subject: [PATCH 11/16] Implement keyboard setting in ui --- client/src/graphic/gui.rs | 24 ++-- client/src/graphic/input.rs | 5 + client/src/graphic/menu.rs | 39 +++++++ client/src/module/mod.rs | 227 ++++++++++++++++++++++++++++++++++++ 4 files changed, 288 insertions(+), 7 deletions(-) diff --git a/client/src/graphic/gui.rs b/client/src/graphic/gui.rs index 304889f..49ff6f4 100644 --- a/client/src/graphic/gui.rs +++ b/client/src/graphic/gui.rs @@ -19,23 +19,33 @@ pub fn render_all(ctx: &Context, window_anim_states: &mut HashMap = Mutex::new(MouseState::default()); @@ -129,6 +130,10 @@ mod linux_input { action: i32, mods: i32, ) { + if action == 1 { + LAST_KEY_PRESSED.store(key, Ordering::Relaxed); + } + if key == 344 /* Right Shift */ && action == 1 { // Toggle GUI let current = GUI_OPEN.load(Ordering::Relaxed); diff --git a/client/src/graphic/menu.rs b/client/src/graphic/menu.rs index fae3193..3aa3c0a 100644 --- a/client/src/graphic/menu.rs +++ b/client/src/graphic/menu.rs @@ -240,6 +240,45 @@ pub fn draw( Some(egui::TextWrapMode::Truncate); ui.style_mut().spacing.interact_size.x = 80.0; + // 1. Static Keybind Row + ui.horizontal(|ui| { + ui.label("Bind"); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let mut keybind_str = data.key_bind.to_string(); + let binding_id = ui.id().with("binding"); + let is_binding = ui.data(|d| d.get_temp::(binding_id).unwrap_or(false)); + + if is_binding { + keybind_str = "_".to_string(); + // Consume any latest key press since they clicked "Bind" + let pressed = crate::graphic::input::LAST_KEY_PRESSED.swap(-1, std::sync::atomic::Ordering::Relaxed); + if pressed != -1 { + if crate::module::KeyboardKey::from(pressed) == crate::module::KeyboardKey::KeyEscape { + data.key_bind = crate::module::KeyboardKey::KeyNone; // Unbind + } else { + data.key_bind = crate::module::KeyboardKey::from(pressed); + } + ui.data_mut(|d| d.insert_temp(binding_id, false)); + } + } + + // Small clean minimal button for the bind + let btn = ui.add(egui::Button::new( + egui::RichText::new(&keybind_str).color(Color32::from_rgb(160, 160, 160)) + ).fill(Color32::from_rgb(25, 25, 25)).stroke(egui::Stroke::NONE)); + + if btn.clicked() { + // Toggle binding mode + let new_state = !is_binding; + ui.data_mut(|d| d.insert_temp(binding_id, new_state)); + if new_state { + // Flush any old presses + crate::graphic::input::LAST_KEY_PRESSED.store(-1, std::sync::atomic::Ordering::Relaxed); + } + } + }); + }); + for setting in &mut data.settings { match setting { ModuleSetting::Toggle { name, value } => { diff --git a/client/src/module/mod.rs b/client/src/module/mod.rs index 364c016..9db2f1c 100644 --- a/client/src/module/mod.rs +++ b/client/src/module/mod.rs @@ -239,3 +239,230 @@ pub enum KeyboardKey { KeyInsert = 260, KeyDelete = 261, } + +impl KeyboardKey { + pub fn from(key: i32) -> Self { + match key { + -1 => KeyboardKey::KeyNone, + 256 => KeyboardKey::KeyEscape, + 49 => KeyboardKey::Key1, + 50 => KeyboardKey::Key2, + 51 => KeyboardKey::Key3, + 52 => KeyboardKey::Key4, + 53 => KeyboardKey::Key5, + 54 => KeyboardKey::Key6, + 55 => KeyboardKey::Key7, + 56 => KeyboardKey::Key8, + 57 => KeyboardKey::Key9, + 48 => KeyboardKey::Key0, + 45 => KeyboardKey::KeyMinus, + 61 => KeyboardKey::KeyEquals, + 259 => KeyboardKey::KeyBack, + 258 => KeyboardKey::KeyTab, + 81 => KeyboardKey::KeyQ, + 87 => KeyboardKey::KeyW, + 69 => KeyboardKey::KeyE, + 82 => KeyboardKey::KeyR, + 84 => KeyboardKey::KeyT, + 89 => KeyboardKey::KeyY, + 85 => KeyboardKey::KeyU, + 73 => KeyboardKey::KeyI, + 79 => KeyboardKey::KeyO, + 80 => KeyboardKey::KeyP, + 91 => KeyboardKey::KeyLBracket, + 93 => KeyboardKey::KeyRBracket, + 257 => KeyboardKey::KeyReturn, + 341 => KeyboardKey::KeyLControl, + 65 => KeyboardKey::KeyA, + 83 => KeyboardKey::KeyS, + 68 => KeyboardKey::KeyD, + 70 => KeyboardKey::KeyF, + 71 => KeyboardKey::KeyG, + 72 => KeyboardKey::KeyH, + 74 => KeyboardKey::KeyJ, + 75 => KeyboardKey::KeyK, + 76 => KeyboardKey::KeyL, + 59 => KeyboardKey::KeySemicolon, + 39 => KeyboardKey::KeyApostrophe, + 96 => KeyboardKey::KeyGrave, + 340 => KeyboardKey::KeyLShift, + 92 => KeyboardKey::KeyBackSlash, + 90 => KeyboardKey::KeyZ, + 88 => KeyboardKey::KeyX, + 67 => KeyboardKey::KeyC, + 86 => KeyboardKey::KeyV, + 66 => KeyboardKey::KeyB, + 78 => KeyboardKey::KeyN, + 77 => KeyboardKey::KeyM, + 44 => KeyboardKey::KeyComma, + 46 => KeyboardKey::KeyPeriod, + 47 => KeyboardKey::KeySlash, + 344 => KeyboardKey::KeyRShift, + 332 => KeyboardKey::KeyMultiply, + 342 => KeyboardKey::KeyLAlt, + 32 => KeyboardKey::KeySpace, + 280 => KeyboardKey::KeyCapital, + 290 => KeyboardKey::KeyF1, + 291 => KeyboardKey::KeyF2, + 292 => KeyboardKey::KeyF3, + 293 => KeyboardKey::KeyF4, + 294 => KeyboardKey::KeyF5, + 295 => KeyboardKey::KeyF6, + 296 => KeyboardKey::KeyF7, + 297 => KeyboardKey::KeyF8, + 298 => KeyboardKey::KeyF9, + 299 => KeyboardKey::KeyF10, + 282 => KeyboardKey::KeyNumLock, + 281 => KeyboardKey::KeyScroll, + 327 => KeyboardKey::KeyNumpad7, + 328 => KeyboardKey::KeyNumpad8, + 329 => KeyboardKey::KeyNumpad9, + 333 => KeyboardKey::KeySubtract, + 324 => KeyboardKey::KeyNumpad4, + 325 => KeyboardKey::KeyNumpad5, + 326 => KeyboardKey::KeyNumpad6, + 334 => KeyboardKey::KeyAdd, + 321 => KeyboardKey::KeyNumpad1, + 322 => KeyboardKey::KeyNumpad2, + 323 => KeyboardKey::KeyNumpad3, + 320 => KeyboardKey::KeyNumpad0, + 300 => KeyboardKey::KeyF11, + 301 => KeyboardKey::KeyF12, + 302 => KeyboardKey::KeyF13, + 303 => KeyboardKey::KeyF14, + 304 => KeyboardKey::KeyF15, + 305 => KeyboardKey::KeyF16, + 306 => KeyboardKey::KeyF17, + 307 => KeyboardKey::KeyF18, + 308 => KeyboardKey::KeyF19, + 336 => KeyboardKey::KeyNumpadEquals, + 335 => KeyboardKey::KeyNumpadEnter, + 345 => KeyboardKey::KeyRControl, + 330 => KeyboardKey::KeyNumpadComma, + 331 => KeyboardKey::KeyDivide, + 284 => KeyboardKey::KeyPause, + 268 => KeyboardKey::KeyHome, + 265 => KeyboardKey::KeyUp, + 263 => KeyboardKey::KeyLeft, + 262 => KeyboardKey::KeyRight, + 269 => KeyboardKey::KeyEnd, + 264 => KeyboardKey::KeyDown, + 267 => KeyboardKey::KeyNext, + 260 => KeyboardKey::KeyInsert, + 261 => KeyboardKey::KeyDelete, + _ => KeyboardKey::KeyNone, + } + } + + pub fn to_string(&self) -> String { + match self { + KeyboardKey::KeyNone => str::to_string("None"), + KeyboardKey::KeyEscape => str::to_string("ESC"), + KeyboardKey::Key1 => str::to_string("1"), + KeyboardKey::Key2 => str::to_string("2"), + KeyboardKey::Key3 => str::to_string("3"), + KeyboardKey::Key4 => str::to_string("4"), + KeyboardKey::Key5 => str::to_string("5"), + KeyboardKey::Key6 => str::to_string("6"), + KeyboardKey::Key7 => str::to_string("7"), + KeyboardKey::Key8 => str::to_string("8"), + KeyboardKey::Key9 => str::to_string("9"), + KeyboardKey::Key0 => str::to_string("0"), + KeyboardKey::KeyMinus => str::to_string("Minus"), + KeyboardKey::KeyEquals => str::to_string("Equals"), + KeyboardKey::KeyBack => str::to_string("Back"), + KeyboardKey::KeyTab => str::to_string("Tab"), + KeyboardKey::KeyQ => str::to_string("Q"), + KeyboardKey::KeyW => str::to_string("W"), + KeyboardKey::KeyE => str::to_string("E"), + KeyboardKey::KeyR => str::to_string("R"), + KeyboardKey::KeyT => str::to_string("T"), + KeyboardKey::KeyY => str::to_string("Y"), + KeyboardKey::KeyU => str::to_string("U"), + KeyboardKey::KeyI => str::to_string("I"), + KeyboardKey::KeyO => str::to_string("O"), + KeyboardKey::KeyP => str::to_string("P"), + KeyboardKey::KeyLBracket => str::to_string("LBracket"), + KeyboardKey::KeyRBracket => str::to_string("RBracket"), + KeyboardKey::KeyReturn => str::to_string("Return"), + KeyboardKey::KeyLControl => str::to_string("LControl"), + KeyboardKey::KeyA => str::to_string("A"), + KeyboardKey::KeyS => str::to_string("S"), + KeyboardKey::KeyD => str::to_string("D"), + KeyboardKey::KeyF => str::to_string("F"), + KeyboardKey::KeyG => str::to_string("G"), + KeyboardKey::KeyH => str::to_string("H"), + KeyboardKey::KeyJ => str::to_string("J"), + KeyboardKey::KeyK => str::to_string("K"), + KeyboardKey::KeyL => str::to_string("L"), + KeyboardKey::KeySemicolon => str::to_string("Semicolon"), + KeyboardKey::KeyApostrophe => str::to_string("Apostrophe"), + KeyboardKey::KeyGrave => str::to_string("Grave"), + KeyboardKey::KeyLShift => str::to_string("LShift"), + KeyboardKey::KeyBackSlash => str::to_string("BackSlash"), + KeyboardKey::KeyZ => str::to_string("Z"), + KeyboardKey::KeyX => str::to_string("X"), + KeyboardKey::KeyC => str::to_string("C"), + KeyboardKey::KeyV => str::to_string("V"), + KeyboardKey::KeyB => str::to_string("B"), + KeyboardKey::KeyN => str::to_string("N"), + KeyboardKey::KeyM => str::to_string("M"), + KeyboardKey::KeyComma => str::to_string("Comma"), + KeyboardKey::KeyPeriod => str::to_string("Period"), + KeyboardKey::KeySlash => str::to_string("Slash"), + KeyboardKey::KeyRShift => str::to_string("RShift"), + KeyboardKey::KeyMultiply => str::to_string("Multiply"), + KeyboardKey::KeyLAlt => str::to_string("LAlt"), + KeyboardKey::KeySpace => str::to_string("Space"), + KeyboardKey::KeyCapital => str::to_string("Capital"), + KeyboardKey::KeyF1 => str::to_string("F1"), + KeyboardKey::KeyF2 => str::to_string("F2"), + KeyboardKey::KeyF3 => str::to_string("F3"), + KeyboardKey::KeyF4 => str::to_string("F4"), + KeyboardKey::KeyF5 => str::to_string("F5"), + KeyboardKey::KeyF6 => str::to_string("F6"), + KeyboardKey::KeyF7 => str::to_string("F7"), + KeyboardKey::KeyF8 => str::to_string("F8"), + KeyboardKey::KeyF9 => str::to_string("F9"), + KeyboardKey::KeyF10 => str::to_string("F10"), + KeyboardKey::KeyNumLock => str::to_string("NumLock"), + KeyboardKey::KeyScroll => str::to_string("Scroll"), + KeyboardKey::KeyNumpad7 => str::to_string("Numpad7"), + KeyboardKey::KeyNumpad8 => str::to_string("Numpad8"), + KeyboardKey::KeyNumpad9 => str::to_string("Numpad9"), + KeyboardKey::KeySubtract => str::to_string("Subtract"), + KeyboardKey::KeyNumpad4 => str::to_string("Numpad4"), + KeyboardKey::KeyNumpad5 => str::to_string("Numpad5"), + KeyboardKey::KeyNumpad6 => str::to_string("Numpad6"), + KeyboardKey::KeyAdd => str::to_string("Add"), + KeyboardKey::KeyNumpad1 => str::to_string("Numpad1"), + KeyboardKey::KeyNumpad2 => str::to_string("Numpad2"), + KeyboardKey::KeyNumpad3 => str::to_string("Numpad3"), + KeyboardKey::KeyNumpad0 => str::to_string("Numpad0"), + KeyboardKey::KeyF11 => str::to_string("F11"), + KeyboardKey::KeyF12 => str::to_string("F12"), + KeyboardKey::KeyF13 => str::to_string("F13"), + KeyboardKey::KeyF14 => str::to_string("F14"), + KeyboardKey::KeyF15 => str::to_string("F15"), + KeyboardKey::KeyF16 => str::to_string("F16"), + KeyboardKey::KeyF17 => str::to_string("F17"), + KeyboardKey::KeyF18 => str::to_string("F18"), + KeyboardKey::KeyF19 => str::to_string("F19"), + KeyboardKey::KeyNumpadEquals => str::to_string("NumpadEquals"), + KeyboardKey::KeyNumpadEnter => str::to_string("NumpadEnter"), + KeyboardKey::KeyRControl => str::to_string("RControl"), + KeyboardKey::KeyNumpadComma => str::to_string("NumpadComma"), + KeyboardKey::KeyDivide => str::to_string("Divide"), + KeyboardKey::KeyPause => str::to_string("Pause"), + KeyboardKey::KeyHome => str::to_string("Home"), + KeyboardKey::KeyUp => str::to_string("Up"), + KeyboardKey::KeyLeft => str::to_string("Left"), + KeyboardKey::KeyRight => str::to_string("Right"), + KeyboardKey::KeyEnd => str::to_string("End"), + KeyboardKey::KeyDown => str::to_string("Down"), + KeyboardKey::KeyNext => str::to_string("Next"), + KeyboardKey::KeyInsert => str::to_string("Insert"), + KeyboardKey::KeyDelete => str::to_string("Delete"), + } + } +} From 3dbad12332437fdd30148f1985372a92822fce8d Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Sun, 22 Feb 2026 13:52:28 +0100 Subject: [PATCH 12/16] Implement notification system --- client/src/graphic/gui.rs | 3 + client/src/graphic/menu.rs | 65 ++++++++--- client/src/graphic/mod.rs | 1 + client/src/graphic/notification.rs | 170 +++++++++++++++++++++++++++++ client/src/graphic/ui_engine.rs | 44 ++++++-- 5 files changed, 263 insertions(+), 20 deletions(-) create mode 100644 client/src/graphic/notification.rs diff --git a/client/src/graphic/gui.rs b/client/src/graphic/gui.rs index 49ff6f4..7c48bf1 100644 --- a/client/src/graphic/gui.rs +++ b/client/src/graphic/gui.rs @@ -56,4 +56,7 @@ pub fn render_all(ctx: &Context, window_anim_states: &mut HashMap 0.0 { menu::draw(ctx, anim_progress, window_anim_states); } + + // Always draw notifications on top + crate::graphic::notification::draw_notifications(ctx); } diff --git a/client/src/graphic/menu.rs b/client/src/graphic/menu.rs index 3aa3c0a..0ce68c7 100644 --- a/client/src/graphic/menu.rs +++ b/client/src/graphic/menu.rs @@ -32,8 +32,11 @@ pub fn draw( { std::thread::spawn(|| crate::graphic::ui_engine::call_panic()); } + ui.add_space(20.0); if ui.button("Reset UI").clicked() { + window_anim_states.clear(); ctx.memory_mut(|mem| mem.reset_areas()); + ctx.data_mut(|d| d.clear()); } }); }); @@ -86,13 +89,17 @@ pub fn draw( let stiffness = 280.0; let damping = 18.0; - let displacement = win_state.actual_pos - target_pos; - let spring_force = -stiffness * displacement; - let damping_force = -damping * win_state.velocity; - let acceleration = spring_force + damping_force; - - win_state.velocity += acceleration * dt; - win_state.actual_pos += win_state.velocity * dt; + let substeps = 4; + let sub_dt = dt / (substeps as f32); + for _ in 0..substeps { + let displacement = win_state.actual_pos - target_pos; + let spring_force = -stiffness * displacement; + let damping_force = -damping * win_state.velocity; + let acceleration = spring_force + damping_force; + + win_state.velocity += acceleration * sub_dt; + win_state.actual_pos += win_state.velocity * sub_dt; + } } let y_offset_spawn = Vec2::new(0.0, 20.0 * (1.0 - anim_progress)); @@ -114,12 +121,16 @@ pub fn draw( ui.set_min_width(win_w); ui.set_max_width(win_w); + ui.set_min_height(35.0); + // Title Bar let (title_rect, title_resp) = ui.allocate_exact_size(Vec2::new(win_w, 24.0), Sense::drag()); - if title_resp.dragged() { - target_pos += title_resp.drag_delta(); + let is_drag_active = title_resp.dragged() || ui.ctx().is_being_dragged(title_resp.id); + + if is_drag_active { + target_pos += ui.ctx().input(|i| i.pointer.delta()); ctx.data_mut(|d| d.insert_temp(area_id, target_pos)); } @@ -245,7 +256,7 @@ pub fn draw( ui.label("Bind"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let mut keybind_str = data.key_bind.to_string(); - let binding_id = ui.id().with("binding"); + let binding_id = ui.id().with(format!("{}_binding", mod_name)); let is_binding = ui.data(|d| d.get_temp::(binding_id).unwrap_or(false)); if is_binding { @@ -253,12 +264,40 @@ pub fn draw( // Consume any latest key press since they clicked "Bind" let pressed = crate::graphic::input::LAST_KEY_PRESSED.swap(-1, std::sync::atomic::Ordering::Relaxed); if pressed != -1 { - if crate::module::KeyboardKey::from(pressed) == crate::module::KeyboardKey::KeyEscape { + let new_key = crate::module::KeyboardKey::from(pressed); + + if new_key == crate::module::KeyboardKey::KeyEscape { data.key_bind = crate::module::KeyboardKey::KeyNone; // Unbind + ui.data_mut(|d| d.insert_temp(binding_id, false)); } else { - data.key_bind = crate::module::KeyboardKey::from(pressed); + // Check for duplicates + let mut is_duplicate = false; + let mut duplicate_name = String::new(); + + for other_mod in client_modules_guard.values() { + if std::sync::Arc::ptr_eq(module, other_mod) { + continue; + } + let other_lock = other_mod.lock().unwrap(); + let other_data = other_lock.get_module_data(); + if other_data.key_bind == new_key { + is_duplicate = true; + duplicate_name = other_data.name.clone(); + break; + } + } + + if is_duplicate { + crate::graphic::notification::Notification::send( + crate::graphic::notification::NotificationType::Alert, + "Keybind Conflict", + &format!("Cheat '{}' already uses key '{}'", duplicate_name, new_key.to_string()) + ); + } else { + data.key_bind = new_key; + ui.data_mut(|d| d.insert_temp(binding_id, false)); + } } - ui.data_mut(|d| d.insert_temp(binding_id, false)); } } diff --git a/client/src/graphic/mod.rs b/client/src/graphic/mod.rs index 7a69fc4..ded211b 100644 --- a/client/src/graphic/mod.rs +++ b/client/src/graphic/mod.rs @@ -3,4 +3,5 @@ pub mod hook; pub mod hud; pub mod input; pub mod menu; +pub mod notification; pub mod ui_engine; diff --git a/client/src/graphic/notification.rs b/client/src/graphic/notification.rs new file mode 100644 index 0000000..a0ca105 --- /dev/null +++ b/client/src/graphic/notification.rs @@ -0,0 +1,170 @@ +use egui::{Align2, Color32, Pos2, Rect, Rounding, Stroke, Vec2}; +use lazy_static::lazy_static; +use std::sync::Mutex; +use std::time::Instant; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NotificationType { + Info, + Warning, + Alert, +} + +#[derive(Clone)] +pub struct Notification { + pub notif_type: NotificationType, + pub title: String, + pub message: String, + pub spawn_time: Instant, + pub duration: f32, +} + +impl Notification { + pub fn send(notif_type: NotificationType, title: &str, message: &str) { + Self::send_with_time(notif_type, title, message, 3.0); + } + + pub fn send_with_time(notif_type: NotificationType, title: &str, message: &str, duration: f32) { + let mut queue = NOTIFICATIONS.lock().unwrap(); + queue.push(Notification { + notif_type, + title: title.to_string(), + message: message.to_string(), + spawn_time: Instant::now(), + duration, + }); + } +} + +lazy_static! { + pub static ref NOTIFICATIONS: Mutex> = Mutex::new(Vec::new()); +} + +pub fn draw_notifications(ctx: &egui::Context) { + let mut queue = NOTIFICATIONS.lock().unwrap(); + let now = Instant::now(); + + // Remove expired notifications + queue.retain(|n| now.duration_since(n.spawn_time).as_secs_f32() < n.duration); + + let screen_width = ctx.screen_rect().width(); + let pad = 15.0; + let width = 250.0; + let height = 60.0; + let mut current_y = pad; // Draw notifications from top right downwards + + for n in queue.iter() { + let elapsed = now.duration_since(n.spawn_time).as_secs_f32(); + + // Calculate animation offsets (slide in from right, then slide out to right) + let in_anim_dur = 0.3; + let out_anim_dur = 0.3; + + let slide_offset = if elapsed < in_anim_dur { + // Slide in from right (start off-screen right, move left to 0 offset) + let progress = elapsed / in_anim_dur; + // cubic ease out + let ease = 1.0 - (1.0 - progress).powi(3); + 300.0 * (1.0 - ease) + } else if elapsed > n.duration - out_anim_dur { + // Slide out to right + let progress = (elapsed - (n.duration - out_anim_dur)) / out_anim_dur; + // cubic ease in + let ease = progress.powi(3); + 300.0 * ease + } else { + 0.0 // Fully visible + }; + + // Define actual rect anchored to top-right + // x-coord = screen_width - pad - width + slide_offset + let rect_x = screen_width - pad - width + slide_offset; + + let rect = Rect::from_min_size(Pos2::new(rect_x, current_y), Vec2::new(width, height)); + + // Draw background + ctx.layer_painter(egui::LayerId::new( + egui::Order::Tooltip, + egui::Id::new("notifications"), + )) + .rect( + rect, + Rounding::same(4.0), + Color32::from_black_alpha(220), + Stroke::new(1.0, Color32::from_rgb(45, 45, 45)), + ); + + // Color coding by type + let (icon_color, icon_char) = match n.notif_type { + NotificationType::Info => (Color32::from_rgb(26, 171, 138), "i"), // Teal + NotificationType::Warning => (Color32::from_rgb(255, 165, 0), "!"), // Orange + NotificationType::Alert => (Color32::from_rgb(220, 50, 50), "X"), // Red + }; + + // Draw left accent bar + let accent_rect = Rect::from_min_size(rect.min, Vec2::new(4.0, height)); + ctx.layer_painter(egui::LayerId::new( + egui::Order::Tooltip, + egui::Id::new("notifications"), + )) + .rect_filled( + accent_rect, + Rounding { + nw: 4.0, + sw: 4.0, + ne: 0.0, + se: 0.0, + }, + icon_color, + ); + + // Draw Icon bg circle + let center_icon = rect.min + Vec2::new(25.0, height / 2.0); + ctx.layer_painter(egui::LayerId::new( + egui::Order::Tooltip, + egui::Id::new("notifications"), + )) + .circle_filled(center_icon, 12.0, Color32::from_white_alpha(20)); + + // Draw Icon text + ctx.layer_painter(egui::LayerId::new( + egui::Order::Tooltip, + egui::Id::new("notifications"), + )) + .text( + center_icon, + Align2::CENTER_CENTER, + icon_char, + egui::FontId::proportional(16.0), + icon_color, + ); + + // Draw Texts + let text_start = rect.min + Vec2::new(50.0, 10.0); + ctx.layer_painter(egui::LayerId::new( + egui::Order::Tooltip, + egui::Id::new("notifications"), + )) + .text( + text_start, + Align2::LEFT_TOP, + &n.title, + egui::FontId::proportional(14.0), + Color32::WHITE, + ); + + ctx.layer_painter(egui::LayerId::new( + egui::Order::Tooltip, + egui::Id::new("notifications"), + )) + .text( + text_start + Vec2::new(0.0, 18.0), + Align2::LEFT_TOP, + &n.message, + egui::FontId::proportional(12.0), + Color32::from_gray(180), + ); + + current_y += height + pad; + } +} diff --git a/client/src/graphic/ui_engine.rs b/client/src/graphic/ui_engine.rs index 8d6289d..c716219 100644 --- a/client/src/graphic/ui_engine.rs +++ b/client/src/graphic/ui_engine.rs @@ -144,6 +144,24 @@ pub unsafe fn render_egui_ui() { // Reset pixel unpack state that is often corrupted by Minecraft unsafe { + let mut last_texture = 0; + crate::gl::GetIntegerv(crate::gl::TEXTURE_BINDING_2D, &mut last_texture); + let mut last_active_texture = 0; + crate::gl::GetIntegerv(crate::gl::ACTIVE_TEXTURE, &mut last_active_texture); + let mut last_array_buffer = 0; + crate::gl::GetIntegerv(crate::gl::ARRAY_BUFFER_BINDING, &mut last_array_buffer); + let mut last_element_array_buffer = 0; + crate::gl::GetIntegerv( + crate::gl::ELEMENT_ARRAY_BUFFER_BINDING, + &mut last_element_array_buffer, + ); + let mut last_vertex_array = 0; + crate::gl::GetIntegerv(crate::gl::VERTEX_ARRAY_BINDING, &mut last_vertex_array); + let mut last_program = 0; + crate::gl::GetIntegerv(crate::gl::CURRENT_PROGRAM, &mut last_program); + + crate::gl::BindBuffer(crate::gl::PIXEL_UNPACK_BUFFER, 0); + crate::gl::ActiveTexture(crate::gl::TEXTURE0); crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); @@ -153,14 +171,26 @@ pub unsafe fn render_egui_ui() { crate::gl::Disable(crate::gl::CULL_FACE); crate::gl::Disable(crate::gl::DEPTH_TEST); crate::gl::Enable(crate::gl::BLEND); - } - state.painter.paint_and_update_textures( - [screen_width as u32, screen_height as u32], - full_output.pixels_per_point, - &clipped_primitives, - &full_output.textures_delta, - ); + state.painter.paint_and_update_textures( + [screen_width as u32, screen_height as u32], + full_output.pixels_per_point, + &clipped_primitives, + &full_output.textures_delta, + ); + + // Restore critical state to prevent Minecraft rendering corruption + // (and prevent Minecraft from corrupting our EGUI texture/vao on the next frame) + crate::gl::BindTexture(crate::gl::TEXTURE_2D, last_texture as u32); + crate::gl::BindBuffer(crate::gl::ARRAY_BUFFER, last_array_buffer as u32); + crate::gl::BindBuffer( + crate::gl::ELEMENT_ARRAY_BUFFER, + last_element_array_buffer as u32, + ); + crate::gl::BindVertexArray(last_vertex_array as u32); + crate::gl::UseProgram(last_program as u32); + crate::gl::ActiveTexture(last_active_texture as u32); + } } pub fn call_panic() { From 831bdb5bcabf51c2cc369fc284f6f22ed2ed94e7 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Wed, 20 May 2026 00:15:09 +0200 Subject: [PATCH 13/16] Refactoring UI --- CLAUDE.md | 63 +++ client/src/graphic/anim.rs | 202 +++++++ client/src/graphic/gui.rs | 82 +-- client/src/graphic/hud.rs | 181 +++++-- client/src/graphic/menu.rs | 839 ++++++++++++++++------------- client/src/graphic/mod.rs | 2 + client/src/graphic/notification.rs | 279 +++++----- client/src/graphic/theme.rs | 127 +++++ client/src/graphic/ui_engine.rs | 29 +- client/src/module/mod.rs | 2 +- 10 files changed, 1176 insertions(+), 630 deletions(-) create mode 100644 CLAUDE.md create mode 100644 client/src/graphic/anim.rs create mode 100644 client/src/graphic/theme.rs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..17ba05e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +DarkClient is a Minecraft (Java Edition, mappings target **1.21.10**) modification framework written in Rust. It injects native libraries into a running Minecraft JVM and drives the game through JNI. It is a Cargo workspace of three crates. + +## Build & Common Commands + +```bash +cargo build --release # build all three crates +cargo build -p client --release # build a single crate +cargo test -p client # tests live only in client/src/mapping/class.rs +cargo test -p client test_type_compatibility # run one test +cargo fmt +cargo clippy +python conversion.py # regenerate mappings.json (needs the `requests` package) +``` + +- **Always build `--release`.** The release profile (`opt-level = "s"`, `lto = true`) is what CI and runtime expect; debug builds also silence `dead_code` warnings via `lib.rs`. +- **Windows requires the nightly toolchain** (see `.github/workflows/build.yml`) and a discoverable `jvm.lib`. `client/build.rs` and `agent_loader/build.rs` locate it via `JAVA_HOME` or `JVM_LIB_DIR`; Linux links `libjvm.so` directly. JDK 21+ required. +- Running the `injector` needs root (`sudo`) on Linux / Administrator on Windows. `libagent_loader` and `libclient` must sit in the injector's working directory. + +## Crate Roles + +- **`injector/`** — standalone GUI binary (egui/eframe; `--tui` flag for a crossterm TUI). Finds Java processes whose command line contains `minecraft`, injects `agent_loader`, then drives the client. +- **`agent_loader/`** — `cdylib` injected first. A `#[ctor]` runs on load: starts a JVM health monitor and a TCP command server. Owns the lifecycle of the client library (load/unload/hot-reload). +- **`client/`** — `cdylib`, the actual mod framework. JNI-driven game interaction, OpenGL overlay, module system. + +## Injection & Hot-Reload Flow + +This is the core control flow and spans all three crates: + +1. `injector` injects `libagent_loader.so`/`.dll` into the JVM process — ptrace (`ptrace-inject`) on Linux, `dll-syringe` on Windows. +2. `agent_loader`'s `#[ctor]` `agent_onload()` starts a TCP server on **`127.0.0.1:7878`** (constant duplicated in `injector/src/platform/mod.rs::SOCKET_ADDRESS`). +3. `injector` connects and sends `reload `. +4. `agent_loader` copies the library to a temp file (avoids file locks), `dlopen`s it, and calls the exported `initialize_client`. +5. Re-injecting repeats step 3 → `reload_client_library` calls `cleanup_client` on the old library before loading the new one. This is the hot-reload path. + +`client` exposes exactly two `#[no_mangle] extern "C"` symbols: `initialize_client` and `cleanup_client`. `initialize_client` spawns a thread that builds `Minecraft::instance()`, calls `register_modules()`, and installs hooks. + +## client/ Internals + +**Rendering & ticking** (`graphic/hook.rs`): `install_hooks` hooks `glfwSwapBuffers` (via `ilhook`) so `on_frame` runs every frame — it renders the egui overlay (`ui_engine.rs`) and calls `check_tick`. `check_tick` compares the player's tick count to detect new game ticks and calls `DarkClient::tick()`, which ticks every enabled module. Tick logic runs on the render thread, not a Minecraft thread. + +**Input** (`graphic/input.rs`): swaps GLFW key/mouse/cursor callbacks. **Right Shift** (key `344`) toggles the GUI; while the GUI is open, input events are consumed instead of forwarded to Minecraft. Module keybinds toggle modules on key press. + +**Module system** (`module/mod.rs`): implement the `Module` trait (`on_start`/`on_stop`/`on_tick`, all returning `anyhow::Result<()>`) plus `ModuleData` accessors. Register new modules in `register_modules()` in `client/src/lib.rs`. Modules carry typed `ModuleSetting`s (Toggle/Slider/Choice/Color). Note: the trait example in `README.md` is stale — the real trait methods return `anyhow::Result<()>`. + +**Mapping system** (`mapping/`): handles Minecraft obfuscation. `mappings.json` (project root) and `java_mappings.json` are **`include_str!`'d into the binary at compile time** by `Mapping::new()` — changing mappings requires rebuilding `client`. `MinecraftClassType` enum maps deobfuscated class names to their JSON entries; `Mapping` resolves obfuscated names and wraps all JNI calls (`call_method`, `call_static_method`, `get_field`, `set_field`, etc.). `class.rs` does overload resolution by scoring argument-type compatibility against JNI signatures. + +**Lifecycle safety**: the global `RUNNING: AtomicBool` gates `on_frame` and the agent's loops. A panic hook in `initialize_client` calls `cleanup_client` so input/render hooks are always uninstalled and GLFW callbacks restored, even on panic. + +## Mappings + +`conversion.py` downloads official Mojang mappings for a chosen Minecraft version and writes the custom `mappings.json` format. The committed `mappings.json` is ~18 MB. `java_mappings.json` is a small hand-written supplement for `java.*` classes, merged in at load time. + +## Logs + +- `injector` → `app.log` (in its working directory) +- `agent_loader` → `agent_loader.log` +- `client` → `dark_client.log` (in `.minecraft`) diff --git a/client/src/graphic/anim.rs b/client/src/graphic/anim.rs new file mode 100644 index 0000000..3dedbb9 --- /dev/null +++ b/client/src/graphic/anim.rs @@ -0,0 +1,202 @@ +//! Frame-rate-independent animation toolkit. +//! +//! egui's built-in `animate_*` helpers advance on egui's own clock. This +//! module animates against the **real** elapsed time fed into `RawInput` +//! (see [`ui_engine`](crate::graphic::ui_engine)), so motion stays correct no +//! matter how the host paces frames — including while Minecraft is paused. +//! +//! All state lives in one global store keyed by [`egui::Id`], so adding an +//! animation is a single call — nothing to declare, own or thread through: +//! +//! ```ignore +//! let open = anim::toggle(ctx, Id::new("menu"), is_open, 0.18, Easing::InOut); +//! let pos = anim::spring_pos(ctx, Id::new("panel"), target, SpringCfg::PANEL); +//! let value = anim::ease_to(ctx, Id::new("bar"), target, 0.2, Easing::Out); +//! ``` +//! +//! Entries are reclaimed automatically: call [`gc`] once per frame. + +use egui::{Context, Id, Pos2, Vec2}; +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; + +/// Largest physics step integrated in a single frame. Caps spring blow-up +/// when the host stalls: a long hitch is absorbed as one ~15 FPS step. +const MAX_DT: f32 = 1.0 / 15.0; + +/// Spring integration substeps per frame — keeps stiff springs stable. +const SUBSTEPS: u32 = 4; + +/// Entries untouched for this many seconds are dropped by [`gc`]. +const STALE_AFTER: f64 = 3.0; + +// --- Easing ---------------------------------------------------------------- + +/// Easing curve applied to tweened progress. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Easing { + Linear, + In, + Out, + InOut, +} + +impl Easing { + /// Maps linear progress `t` (`0..=1`) through the curve. + pub fn apply(self, t: f32) -> f32 { + let t = t.clamp(0.0, 1.0); + match self { + Easing::Linear => t, + Easing::In => t * t * t, + Easing::Out => 1.0 - (1.0 - t).powi(3), + Easing::InOut => { + if t < 0.5 { + 4.0 * t * t * t + } else { + 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0 + } + } + } + } +} + +// --- Spring tuning --------------------------------------------------------- + +/// Tuning for a damped spring. +#[derive(Clone, Copy, Debug)] +pub struct SpringCfg { + pub stiffness: f32, + pub damping: f32, +} + +impl SpringCfg { + /// Smooth, weighty motion — draggable panels. + pub const PANEL: SpringCfg = SpringCfg { + stiffness: 320.0, + damping: 24.0, + }; + /// Quick, snappy motion — small UI reactions. + pub const SNAPPY: SpringCfg = SpringCfg { + stiffness: 520.0, + damping: 32.0, + }; +} + +// --- Global store ---------------------------------------------------------- + +struct Tween { + from: f32, + target: f32, + duration: f32, + easing: Easing, + start: f64, + last_seen: f64, +} + +struct SpringState { + value: Vec2, + velocity: Vec2, + last_seen: f64, +} + +#[derive(Default)] +struct Store { + tweens: HashMap, + springs: HashMap, +} + +fn store() -> &'static Mutex { + static STORE: OnceLock> = OnceLock::new(); + STORE.get_or_init(|| Mutex::new(Store::default())) +} + +/// Real frame time (seconds) and the clamped frame delta. +fn clock(ctx: &Context) -> (f64, f32) { + ctx.input(|i| (i.time, i.stable_dt.clamp(0.0, MAX_DT))) +} + +// --- Tweens ---------------------------------------------------------------- + +/// Eases a value toward `target` over `duration` seconds. +/// +/// Re-targeting mid-flight re-anchors from the currently displayed value, so +/// interrupted animations never jump. +pub fn ease_to(ctx: &Context, id: Id, target: f32, duration: f32, easing: Easing) -> f32 { + let (now, _) = clock(ctx); + let mut store = store().lock().unwrap(); + let tween = store.tweens.entry(id).or_insert(Tween { + from: target, + target, + duration, + easing, + start: now, + last_seen: now, + }); + tween.last_seen = now; + + if (tween.target - target).abs() > f32::EPSILON { + tween.from = sample(tween, now); + tween.target = target; + tween.duration = duration; + tween.easing = easing; + tween.start = now; + } + sample(tween, now) +} + +/// Animates a boolean into a `0..1` factor: eases to `1.0` while `on`. +pub fn toggle(ctx: &Context, id: Id, on: bool, duration: f32, easing: Easing) -> f32 { + ease_to(ctx, id, if on { 1.0 } else { 0.0 }, duration, easing) +} + +fn sample(t: &Tween, now: f64) -> f32 { + if t.duration <= 0.0 { + return t.target; + } + let progress = ((now - t.start) as f32 / t.duration).clamp(0.0, 1.0); + t.from + (t.target - t.from) * t.easing.apply(progress) +} + +// --- Springs --------------------------------------------------------------- + +/// Spring-smooths a scalar toward `target`. +pub fn spring(ctx: &Context, id: Id, target: f32, cfg: SpringCfg) -> f32 { + spring_vec2(ctx, id, Vec2::new(target, 0.0), cfg).x +} + +/// Spring-smooths a position toward `target`. +pub fn spring_pos(ctx: &Context, id: Id, target: Pos2, cfg: SpringCfg) -> Pos2 { + spring_vec2(ctx, id, target.to_vec2(), cfg).to_pos2() +} + +/// Spring-smooths a vector toward `target` with a sub-stepped integrator. +pub fn spring_vec2(ctx: &Context, id: Id, target: Vec2, cfg: SpringCfg) -> Vec2 { + let (now, dt) = clock(ctx); + let mut store = store().lock().unwrap(); + let spring = store.springs.entry(id).or_insert(SpringState { + value: target, + velocity: Vec2::ZERO, + last_seen: now, + }); + spring.last_seen = now; + + let sub_dt = dt / SUBSTEPS as f32; + for _ in 0..SUBSTEPS { + let displacement = spring.value - target; + let accel = -cfg.stiffness * displacement - cfg.damping * spring.velocity; + spring.velocity += accel * sub_dt; + spring.value += spring.velocity * sub_dt; + } + spring.value +} + +// --- Maintenance ----------------------------------------------------------- + +/// Drops animation state that has not been touched recently. Call once per +/// frame so transient ids (e.g. per-notification) cannot accumulate. +pub fn gc(ctx: &Context) { + let now = ctx.input(|i| i.time); + let mut store = store().lock().unwrap(); + store.tweens.retain(|_, t| now - t.last_seen < STALE_AFTER); + store.springs.retain(|_, s| now - s.last_seen < STALE_AFTER); +} diff --git a/client/src/graphic/gui.rs b/client/src/graphic/gui.rs index 7c48bf1..9f024d0 100644 --- a/client/src/graphic/gui.rs +++ b/client/src/graphic/gui.rs @@ -1,62 +1,30 @@ -use crate::graphic::ui_engine::WindowAnimState; -use crate::graphic::{hud, menu}; -use egui::{Color32, Context, Margin, Rounding, Stroke, Style, Vec2, Visuals}; -use std::collections::HashMap; - -pub fn render_all(ctx: &Context, window_anim_states: &mut HashMap) { - let mut style = Style::default(); - let mut visuals = Visuals::dark(); - - let bg_color = Color32::from_rgb(18, 18, 18); - let panel_color = Color32::from_rgb(25, 25, 25); - let accent_color = Color32::from_rgb(26, 171, 138); // Vape's Teal - let border_color = Color32::from_rgb(35, 35, 35); - - visuals.window_fill = bg_color; - visuals.panel_fill = panel_color; - visuals.window_stroke = Stroke::new(1.0, border_color); - - visuals.selection.bg_fill = accent_color; - visuals.selection.stroke = Stroke::NONE; - - // Minimalist Widget Styling - visuals.widgets.inactive.bg_fill = Color32::from_rgb(20, 20, 20); // Darker thin rails for sliders - visuals.widgets.inactive.bg_stroke = Stroke::new(1.0, Color32::from_rgb(30, 30, 30)); - visuals.widgets.inactive.rounding = Rounding::same(2.0); // Slight soft curves for interactive elements - visuals.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(180, 180, 180)); - visuals.widgets.inactive.expansion = 0.0; - - visuals.widgets.hovered.bg_fill = Color32::from_rgb(35, 35, 35); - visuals.widgets.hovered.bg_stroke = Stroke::new(1.0, Color32::from_rgb(45, 45, 45)); - visuals.widgets.hovered.rounding = Rounding::same(2.0); - visuals.widgets.hovered.fg_stroke = Stroke::new(1.0, Color32::WHITE); - visuals.widgets.hovered.expansion = 1.0; // slight pop on hover - - visuals.widgets.active.bg_fill = accent_color; - visuals.widgets.active.bg_stroke = Stroke::NONE; - visuals.widgets.active.rounding = Rounding::same(2.0); - visuals.widgets.active.expansion = -1.0; // click compression - - visuals.window_rounding = Rounding::ZERO; - style.visuals = visuals; - - style.spacing.item_spacing = Vec2::new(0.0, 4.0); // Slightly more breathing room vertically - style.spacing.window_margin = Margin::symmetric(0.0, 0.0); - style.spacing.button_padding = Vec2::new(6.0, 4.0); - - // Thinner slider rail, standard widget height - style.spacing.interact_size.y = 12.0; - ctx.set_style(style); - +//! Top-level overlay renderer. +//! +//! This module owns only the per-frame *dispatch order* — HUD underneath, +//! ClickGUI in the middle, notifications always on top. Visual styling lives +//! in [`theme`](crate::graphic::theme), animation in +//! [`anim`](crate::graphic::anim); each widget owns its own module. + +use crate::graphic::anim::{self, Easing}; +use crate::graphic::input::GUI_OPEN; +use crate::graphic::{hud, menu, notification}; +use egui::{Context, Id}; +use std::sync::atomic::Ordering; + +/// Renders the whole overlay for a single frame. +pub fn render_all(ctx: &Context) { hud::draw(ctx); - let is_open = crate::graphic::input::GUI_OPEN.load(std::sync::atomic::Ordering::Relaxed); - let anim_progress = ctx.animate_bool(egui::Id::new("menu_open_anim"), is_open); - - if anim_progress > 0.0 { - menu::draw(ctx, anim_progress, window_anim_states); + // A single tween turns the open/close toggle into the 0..1 factor the + // menu uses to fade and slide itself in. + let open = GUI_OPEN.load(Ordering::Relaxed); + let progress = anim::toggle(ctx, Id::new("clickgui_open"), open, 0.18, Easing::InOut); + if progress > 0.0 { + menu::draw(ctx, progress); } - // Always draw notifications on top - crate::graphic::notification::draw_notifications(ctx); + notification::draw(ctx); + + // Reclaim animation state for anything that stopped being drawn. + anim::gc(ctx); } diff --git a/client/src/graphic/hud.rs b/client/src/graphic/hud.rs index ec36100..7fc0de8 100644 --- a/client/src/graphic/hud.rs +++ b/client/src/graphic/hud.rs @@ -1,58 +1,135 @@ +//! Always-on HUD: the brand watermark and the active-module array list. +//! +//! Everything is drawn straight onto a background [`egui::Painter`] with +//! absolute screen coordinates — no layout passes, no per-widget `Area`s. + use crate::client::DarkClient; -use egui::{Color32, Context, Id, RichText}; +use crate::graphic::anim::{self, Easing}; +use crate::graphic::theme; +use egui::{Align2, Color32, Context, FontId, Id, LayerId, Order, Painter, Rect, Rounding, Stroke, Vec2}; + +/// Screen-edge padding shared by every HUD element. +const MARGIN: f32 = 10.0; +/// Draws the full HUD onto the background layer. pub fn draw(ctx: &Context) { - egui::Area::new(Id::new("hud_watermark")) - .fixed_pos(egui::pos2(5.0, 5.0)) - .interactable(false) - .show(ctx, |ui| { - ui.add( - egui::Label::new( - RichText::new("DarkClient") - .color(Color32::YELLOW) - .size(24.0) - .strong(), - ) - .wrap_mode(egui::TextWrapMode::Extend), - ); - }); + let painter = ctx.layer_painter(LayerId::new(Order::Background, Id::new("hud_layer"))); + draw_watermark(ctx, &painter); + draw_arraylist(ctx, &painter); +} + +/// Top-left brand badge: `Dark` + accented `Client` inside a rounded chip. +fn draw_watermark(ctx: &Context, painter: &Painter) { + let font = FontId::proportional(17.0); + let pad = Vec2::new(11.0, 6.0); + + // Measure both halves so the chip hugs the text exactly. + let (dark_w, text_h) = ctx.fonts(|f| { + let g = f.layout_no_wrap("Dark".to_owned(), font.clone(), theme::TEXT); + (g.size().x, g.size().y) + }); + let client_w = ctx.fonts(|f| { + f.layout_no_wrap("Client".to_owned(), font.clone(), theme::ACCENT) + .size() + .x + }); + + let size = Vec2::new(dark_w + client_w + pad.x * 2.0, text_h + pad.y * 2.0); + let rect = Rect::from_min_size(egui::pos2(MARGIN, MARGIN), size); + + painter.rect_filled(rect, Rounding::same(theme::RADIUS), Color32::from_black_alpha(165)); + painter.rect_stroke(rect, Rounding::same(theme::RADIUS), Stroke::new(1.0, theme::BORDER)); + + // Accent edge on the left side of the chip. + let edge = Rect::from_min_size(rect.min, Vec2::new(3.0, rect.height())); + painter.rect_filled( + edge, + Rounding { nw: theme::RADIUS, sw: theme::RADIUS, ne: 0.0, se: 0.0 }, + theme::ACCENT, + ); + + let anchor = egui::pos2(rect.min.x + pad.x, rect.center().y); + let after = painter.text(anchor, Align2::LEFT_CENTER, "Dark", font.clone(), theme::TEXT); + painter.text( + egui::pos2(after.max.x, anchor.y), + Align2::LEFT_CENTER, + "Client", + font, + theme::ACCENT, + ); +} + +/// Top-right list of enabled modules. Each row slides in/out smoothly when +/// its module is toggled, so nothing ever pops in abruptly. +fn draw_arraylist(ctx: &Context, painter: &Painter) { + const ROW_H: f32 = 19.0; + const PAD_X: f32 = 8.0; + let font = FontId::proportional(14.0); + + // One lock per module: snapshot just the name and enabled flag. + let snapshot: Vec<(String, bool)> = match DarkClient::instance().modules.read() { + Ok(guard) => guard + .values() + .map(|m| { + let data = m.lock().unwrap(); + let d = data.get_module_data(); + (d.name.clone(), d.enabled) + }) + .collect(), + Err(_) => return, + }; - egui::Area::new(Id::new("hud_arraylist")) - .fixed_pos(egui::pos2(5.0, 35.0)) - .interactable(false) - .show(ctx, |ui| { - if let Ok(modules_map) = DarkClient::instance().modules.read() { - let mut active_mods: Vec = modules_map - .values() - .filter_map(|m| { - let lock = m.lock().unwrap(); - if lock.get_module_data().enabled { - Some(lock.get_module_data().name.clone()) - } else { - None - } - }) - .collect(); - - active_mods.sort_by(|a, b| b.len().cmp(&a.len())); - - let colors = [ - Color32::from_rgb(153, 51, 204), // Purple - Color32::from_rgb(51, 204, 230), // Cyan - Color32::GREEN, - Color32::RED, - Color32::YELLOW, - Color32::from_rgb(51, 102, 255), // Blue - Color32::WHITE, - ]; - - for (i, mod_name) in active_mods.iter().enumerate() { - let color = colors[i % colors.len()]; - ui.add( - egui::Label::new(RichText::new(mod_name).color(color).size(16.0)) - .wrap_mode(egui::TextWrapMode::Extend), - ); - } - } + // Resolve a smooth presence factor for every module. Disabled modules + // keep a slot while their factor decays toward zero. + let mut rows: Vec<(String, f32, f32)> = Vec::new(); // (name, factor, text width) + for (name, enabled) in snapshot { + let factor = anim::toggle(ctx, Id::new("arraylist").with(&name), enabled, 0.22, Easing::Out); + if factor <= 0.001 { + continue; + } + let width = ctx.fonts(|f| { + f.layout_no_wrap(name.clone(), font.clone(), theme::TEXT) + .size() + .x }); + rows.push((name, factor, width)); + } + if rows.is_empty() { + return; + } + + // Longest entry on top — the classic staircase silhouette. + rows.sort_by(|a, b| b.2.total_cmp(&a.2)); + + let screen_w = ctx.screen_rect().width(); + let mut y = MARGIN; + + for (name, factor, text_w) in &rows { + // `factor` is already eased by `anim::toggle`. + let eased = *factor; + let row_w = text_w + PAD_X * 2.0; + // Slide the row in from beyond the right screen edge. + let x = screen_w - MARGIN - row_w * eased; + let rect = Rect::from_min_size(egui::pos2(x, y), Vec2::new(row_w, ROW_H)); + + painter.rect_filled( + rect, + Rounding::ZERO, + theme::with_alpha(Color32::from_black_alpha(175), eased), + ); + + // Accent tab welded to the right screen edge. + let tab = Rect::from_min_size(rect.right_top() - Vec2::new(2.0, 0.0), Vec2::new(2.0, ROW_H)); + painter.rect_filled(tab, Rounding::ZERO, theme::with_alpha(theme::ACCENT, eased)); + + painter.text( + egui::pos2(rect.min.x + PAD_X, rect.center().y), + Align2::LEFT_CENTER, + name, + font.clone(), + theme::with_alpha(theme::TEXT, eased), + ); + + y += ROW_H + 2.0; + } } diff --git a/client/src/graphic/menu.rs b/client/src/graphic/menu.rs index 0ce68c7..7722791 100644 --- a/client/src/graphic/menu.rs +++ b/client/src/graphic/menu.rs @@ -1,48 +1,66 @@ +//! The ClickGUI: a draggable, spring-animated panel per module category. +//! +//! The menu is split into small, single-purpose functions so the drawing +//! code reads top-down. Module mutexes are locked **exactly once** per module +//! per frame; all motion goes through [`anim`], so it is frame-rate +//! independent and needs no per-widget state threaded through the call tree. + use crate::client::DarkClient; -use crate::graphic::ui_engine::WindowAnimState; -use crate::module::{ModuleCategory, ModuleSetting}; -use egui::{Align2, Color32, Context, Id, Pos2, Rect, Rounding, Sense, Vec2}; +use crate::graphic::anim::{self, Easing, SpringCfg}; +use crate::graphic::input::LAST_KEY_PRESSED; +use crate::graphic::notification::{Notification, NotificationType}; +use crate::graphic::theme; +use crate::module::{KeyboardKey, ModuleCategory, ModuleData, ModuleSetting, ModuleType}; +use egui::{ + Align, Align2, Button, Color32, Context, FontId, Id, LayerId, Layout, Margin, Order, Painter, + Pos2, Rect, RichText, Rounding, Sense, Shape, Stroke, Ui, Vec2, +}; use std::collections::HashMap; - -pub fn draw( - ctx: &Context, - anim_progress: f32, - window_anim_states: &mut HashMap, -) { - egui::Area::new(Id::new("dark_overlay")) - .fixed_pos(Pos2::ZERO) - .order(egui::Order::Background) - .interactable(false) - .show(ctx, |ui| { - ui.painter().rect_filled( - ui.ctx().screen_rect(), - 0.0, - Color32::from_black_alpha((180.0 * anim_progress) as u8), - ); - }); - - egui::Area::new(Id::new("global_buttons")) - .anchor(Align2::RIGHT_TOP, Vec2::new(-10.0, 10.0)) - .show(ctx, |ui| { - ui.set_opacity(anim_progress); - ui.horizontal(|ui| { - if ui - .button(egui::RichText::new("PANIC").color(Color32::RED)) - .clicked() - { - std::thread::spawn(|| crate::graphic::ui_engine::call_panic()); - } - ui.add_space(20.0); - if ui.button("Reset UI").clicked() { - window_anim_states.clear(); - ctx.memory_mut(|mem| mem.reset_areas()); - ctx.data_mut(|d| d.clear()); - } - }); - }); - - let client_modules_guard = DarkClient::instance().modules.read().unwrap(); - +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; + +/// Width of a category panel. +const PANEL_W: f32 = 168.0; +/// Horizontal gap between panels in the auto-layout grid. +const GAP: f32 = 14.0; +/// Height of a panel's draggable title bar. +const TITLE_H: f32 = 30.0; +/// Height of a single module row. +const ROW_H: f32 = 24.0; +/// Vertical distance between grid rows when panels wrap. +const ROW_STRIDE: f32 = 320.0; +/// Top-left corner of the first panel slot. +const ORIGIN_X: f32 = 40.0; +const ORIGIN_Y: f32 = 58.0; + +/// A shared handle to one module. +type ModuleArc = Arc>; +/// The whole module registry, as borrowed from the read guard. +type ModuleMap = HashMap; + +/// Draws the entire ClickGUI. `progress` is the 0..1 open animation factor. +pub fn draw(ctx: &Context, progress: f32) { + draw_backdrop(ctx, progress); + + let registry = match DarkClient::instance().modules.read() { + Ok(guard) => guard, + Err(_) => return, + }; + + // Single lock per module: collect the data layout needs, nothing more. + let mut entries: Vec<(String, ModuleCategory)> = registry + .values() + .map(|arc| { + let module = arc.lock().unwrap(); + let data = module.get_module_data(); + (data.name.clone(), data.category) + }) + .collect(); + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + draw_toolbar(ctx, progress); + + // Auto-layout: place non-empty categories left-to-right, wrapping rows. let categories = [ ModuleCategory::COMBAT, ModuleCategory::MOVEMENT, @@ -50,358 +68,419 @@ pub fn draw( ModuleCategory::PLAYER, ModuleCategory::WORLD, ]; - - let logical_width = ctx.screen_rect().width(); - - let mut curr_x = 50.0; - let mut curr_y = 50.0; - let win_w = 160.0; - let gap_x = 20.0; - let row_height = 280.0; - - for category in categories.iter() { - if curr_x + win_w > logical_width && curr_x > 50.0 { - curr_x = 50.0; - curr_y += row_height; + let screen_w = ctx.screen_rect().width(); + let mut slot = Pos2::new(ORIGIN_X, ORIGIN_Y); + + for category in categories { + let members: Vec<(String, &ModuleArc)> = entries + .iter() + .filter(|(_, cat)| *cat == category) + .filter_map(|(name, _)| registry.get(name).map(|arc| (name.clone(), arc))) + .collect(); + if members.is_empty() { + continue; } - let title = category.display_name(); - - let area_id = Id::new(title).with("area"); - - let mut target_pos = Pos2::new(curr_x, curr_y); - target_pos = ctx - .data(|d| d.get_temp::(area_id)) - .unwrap_or(target_pos); - - let dt = ctx.input(|i| i.stable_dt).min(0.1); - - let win_state = window_anim_states - .entry(title.to_string()) - .or_insert(WindowAnimState { - actual_pos: target_pos, - velocity: Vec2::ZERO, - }); - - if anim_progress < 0.01 { - win_state.actual_pos = target_pos; - win_state.velocity = Vec2::ZERO; - } else { - let stiffness = 280.0; - let damping = 18.0; - - let substeps = 4; - let sub_dt = dt / (substeps as f32); - for _ in 0..substeps { - let displacement = win_state.actual_pos - target_pos; - let spring_force = -stiffness * displacement; - let damping_force = -damping * win_state.velocity; - let acceleration = spring_force + damping_force; - - win_state.velocity += acceleration * sub_dt; - win_state.actual_pos += win_state.velocity * sub_dt; - } + + if slot.x + PANEL_W > screen_w - 20.0 && slot.x > ORIGIN_X { + slot.x = ORIGIN_X; + slot.y += ROW_STRIDE; } - let y_offset_spawn = Vec2::new(0.0, 20.0 * (1.0 - anim_progress)); - let final_render_pos = win_state.actual_pos + y_offset_spawn; + draw_panel(ctx, progress, category, &members, slot, ®istry); + slot.x += PANEL_W + GAP; + } +} + +/// Dims the game behind the menu, fading with the open animation. +fn draw_backdrop(ctx: &Context, progress: f32) { + let painter = ctx.layer_painter(LayerId::new(Order::Middle, Id::new("clickgui_backdrop"))); + let alpha = (170.0 * progress) as u8; + painter.rect_filled( + ctx.screen_rect(), + Rounding::ZERO, + Color32::from_black_alpha(alpha), + ); +} - egui::Area::new(area_id) - .current_pos(final_render_pos) // The whole UI draws here - .order(egui::Order::Middle) - .show(ctx, |ui| { - ui.set_opacity(anim_progress); +/// Top-center bar: the brand title plus the Panic / Reset actions. +fn draw_toolbar(ctx: &Context, progress: f32) { + let slide = 16.0 * (1.0 - progress); + egui::Area::new(Id::new("clickgui_toolbar")) + .order(Order::Foreground) + .anchor(Align2::CENTER_TOP, Vec2::new(0.0, 12.0 - slide)) + .show(ctx, |ui| { + ui.set_opacity(progress); + egui::Frame::none() + .fill(theme::BASE) + .stroke(Stroke::new(1.0, theme::BORDER)) + .rounding(Rounding::same(theme::RADIUS)) + .inner_margin(Margin::symmetric(12.0, 7.0)) + .shadow(ui.style().visuals.window_shadow) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label(RichText::new("Dark").size(15.0).strong().color(theme::TEXT)); + ui.label( + RichText::new("Client") + .size(15.0) + .strong() + .color(theme::ACCENT), + ); + ui.add_space(16.0); + + let panic = Button::new( + RichText::new("Panic").size(12.5).color(theme::DANGER), + ) + .fill(theme::ELEVATED); + if ui.add(panic).clicked() { + std::thread::spawn(crate::graphic::ui_engine::call_panic); + } - let frame = egui::Frame::window(&ctx.style()) - .fill(Color32::from_rgb(22, 22, 22)) - .stroke(egui::Stroke::new(1.0, Color32::from_rgb(35, 35, 35))) - .rounding(egui::Rounding::ZERO) - .inner_margin(egui::Margin::symmetric(0.0, 0.0)); + ui.add_space(6.0); + let reset = Button::new( + RichText::new("Reset").size(12.5).color(theme::TEXT_DIM), + ) + .fill(theme::ELEVATED); + if ui.add(reset).clicked() { + // Drop stored panel targets — they spring back home. + ctx.memory_mut(|mem| mem.reset_areas()); + ctx.data_mut(|data| data.clear()); + } + }); + }); + }); +} - frame.show(ui, |ui| { - ui.set_min_width(win_w); - ui.set_max_width(win_w); +/// Draws one category panel: spring-positioned, draggable, with its rows. +fn draw_panel( + ctx: &Context, + progress: f32, + category: ModuleCategory, + members: &[(String, &ModuleArc)], + slot: Pos2, + registry: &ModuleMap, +) { + let name = category.display_name(); - ui.set_min_height(35.0); + // The drag target persists in egui's data store; the spring smooths the + // rendered position toward it, frame-rate independently. + let target_id = Id::new("panel_target").with(name); + let target = ctx.data_mut(|d| *d.get_temp_mut_or_insert_with(target_id, || slot)); + let pos = anim::spring_pos(ctx, Id::new("panel_pos").with(name), target, SpringCfg::PANEL); - // Title Bar - let (title_rect, title_resp) = - ui.allocate_exact_size(Vec2::new(win_w, 24.0), Sense::drag()); + // Spawn animation: slide the panel up into place as the menu opens. + let render_pos = pos + Vec2::new(0.0, 16.0 * (1.0 - progress)); - let is_drag_active = title_resp.dragged() || ui.ctx().is_being_dragged(title_resp.id); - - if is_drag_active { - target_pos += ui.ctx().input(|i| i.pointer.delta()); - ctx.data_mut(|d| d.insert_temp(area_id, target_pos)); + egui::Area::new(Id::new("clickgui_panel").with(name)) + .current_pos(render_pos) + .order(Order::Foreground) + .show(ctx, |ui| { + ui.set_opacity(progress); + egui::Frame::none() + .fill(theme::BASE) + .stroke(Stroke::new(1.0, theme::BORDER)) + .rounding(Rounding::same(theme::RADIUS)) + .shadow(ui.style().visuals.window_shadow) + .show(ui, |ui| { + ui.set_min_width(PANEL_W); + ui.set_max_width(PANEL_W); + draw_title_bar(ui, category, target_id); + for (module_name, arc) in members { + draw_module_row(ui, module_name, arc, registry); } + ui.add_space(6.0); + }); + }); +} - // Draw Title text - ui.painter().text( - title_rect.min + Vec2::new(8.0, 5.0), - Align2::LEFT_TOP, - title, - egui::FontId::proportional(14.0), - Color32::from_rgb(26, 171, 138), // Teal Accent for headers - ); - - // Separator line - ui.painter().line_segment( - [title_rect.left_bottom(), title_rect.right_bottom()], - egui::Stroke::new(1.0, Color32::from_rgb(35, 35, 35)), - ); - - let mut cat_modules: Vec<_> = client_modules_guard - .values() - .filter(|m| m.lock().unwrap().get_module_data().category == *category) - .collect(); - cat_modules.sort_by(|a, b| { - a.lock() - .unwrap() - .get_module_data() - .name - .cmp(&b.lock().unwrap().get_module_data().name) - }); +/// Draggable title bar with the category name and an accent underline. +fn draw_title_bar(ui: &mut Ui, category: ModuleCategory, target_id: Id) { + let (rect, response) = ui.allocate_exact_size(Vec2::new(PANEL_W, TITLE_H), Sense::drag()); + if response.dragged() { + let delta = ui.ctx().input(|i| i.pointer.delta()); + ui.ctx().data_mut(|d| { + let current = d.get_temp::(target_id).unwrap_or(rect.min); + d.insert_temp(target_id, current + delta); + }); + } - for module in cat_modules { - let (mod_name, is_enabled) = { - let lock = module.lock().unwrap(); - let data = lock.get_module_data(); - (data.name.clone(), data.enabled) - }; - - let (rect, response) = - ui.allocate_exact_size(Vec2::new(win_w, 22.0), Sense::click()); - - let bg_color = if response.hovered() { - Color32::from_rgb(37, 37, 37) - } else { - Color32::TRANSPARENT - }; - ui.painter().rect_filled(rect, Rounding::ZERO, bg_color); - - let text_color = if is_enabled { - Color32::from_rgb(26, 171, 138) - } else { - Color32::from_rgb(200, 200, 200) - }; - - ui.painter().text( - rect.min + egui::vec2(8.0, 4.0), - egui::Align2::LEFT_TOP, - &mod_name, - egui::FontId::proportional(14.0), - text_color, - ); + let painter = ui.painter(); + painter.rect_filled( + rect, + Rounding { + nw: theme::RADIUS, + ne: theme::RADIUS, + sw: 0.0, + se: 0.0, + }, + theme::ELEVATED, + ); + painter.text( + rect.left_center() + Vec2::new(12.0, 0.0), + Align2::LEFT_CENTER, + category.display_name(), + FontId::proportional(13.5), + theme::TEXT, + ); + let underline = Rect::from_min_size( + rect.left_bottom() - Vec2::new(0.0, 2.0), + Vec2::new(PANEL_W, 2.0), + ); + painter.rect_filled(underline, Rounding::ZERO, theme::ACCENT); +} - let mut lock = module.lock().unwrap(); - let is_expanded_id = Id::new(&mod_name).with("expanded"); - let mut is_expanded = - ui.data(|d| d.get_temp::(is_expanded_id).unwrap_or(false)); - - let arrow_rect = - Rect::from_min_max(rect.max - Vec2::new(25.0, 20.0), rect.max); - - if response.clicked() { - let click_pos = response.interact_pointer_pos().unwrap_or(Pos2::ZERO); - if response.clicked_by(egui::PointerButton::Secondary) - || (response.clicked_by(egui::PointerButton::Primary) - && arrow_rect.contains(click_pos)) - { - is_expanded = !is_expanded; - ui.data_mut(|d| d.insert_temp(is_expanded_id, is_expanded)); - } else if response.clicked_by(egui::PointerButton::Primary) { - let new_state = !is_enabled; - lock.get_module_data_mut().set_enabled(new_state); - if new_state { - let _ = lock.on_start(); - } else { - let _ = lock.on_stop(); - } - } - } +/// Draws a single module row and its (optional) expandable settings panel. +fn draw_module_row(ui: &mut Ui, name: &str, arc: &ModuleArc, registry: &ModuleMap) { + let mut module = arc.lock().unwrap(); + let enabled = module.get_module_data().enabled; + let has_settings = !module.get_module_data().settings.is_empty(); + + let (rect, response) = ui.allocate_exact_size(Vec2::new(PANEL_W, ROW_H), Sense::click()); + let arrow_zone = Rect::from_min_size( + Pos2::new(rect.max.x - 24.0, rect.min.y), + Vec2::new(24.0, ROW_H), + ); + + let ctx = ui.ctx(); + let hover = anim::toggle(ctx, Id::new("row_hov").with(name), response.hovered(), 0.12, Easing::Out); + let enable = anim::toggle(ctx, Id::new("row_en").with(name), enabled, 0.18, Easing::Out); + + // --- paint base row --- + { + let painter = ui.painter(); + painter.rect_filled( + rect, + Rounding::ZERO, + theme::lerp_color(Color32::TRANSPARENT, theme::SURFACE_HOVER, hover), + ); + if enable > 0.001 { + let bar = Rect::from_center_size( + Pos2::new(rect.min.x + 1.5, rect.center().y), + Vec2::new(3.0, ROW_H * enable), + ); + painter.rect_filled(bar, Rounding::ZERO, theme::ACCENT); + } + painter.text( + Pos2::new(rect.min.x + 12.0, rect.center().y), + Align2::LEFT_CENTER, + name, + FontId::proportional(13.0), + theme::lerp_color(theme::TEXT_DIM, theme::TEXT, hover.max(enable)), + ); + } - let data = lock.get_module_data_mut(); - - if !data.settings.is_empty() { - let arrow = if is_expanded { "v" } else { ">" }; - let arrow_color = if arrow_rect - .contains(ui.ctx().pointer_hover_pos().unwrap_or(Pos2::ZERO)) - { - Color32::WHITE - } else { - Color32::GRAY - }; - - ui.painter().text( - rect.max - Vec2::new(15.0, 17.0), - egui::Align2::LEFT_TOP, - arrow, - egui::FontId::proportional(14.0), - arrow_color, - ); - - if is_expanded { - let settings_frame = egui::Frame::none() - .fill(Color32::from_rgb(15, 15, 15)) - .inner_margin(egui::Margin::symmetric(8.0, 6.0)); - - settings_frame.show(ui, |ui| { - ui.vertical(|ui| { - ui.style_mut().spacing.slider_width = 60.0; - ui.style_mut().wrap_mode = - Some(egui::TextWrapMode::Truncate); - ui.style_mut().spacing.interact_size.x = 80.0; - - // 1. Static Keybind Row - ui.horizontal(|ui| { - ui.label("Bind"); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let mut keybind_str = data.key_bind.to_string(); - let binding_id = ui.id().with(format!("{}_binding", mod_name)); - let is_binding = ui.data(|d| d.get_temp::(binding_id).unwrap_or(false)); - - if is_binding { - keybind_str = "_".to_string(); - // Consume any latest key press since they clicked "Bind" - let pressed = crate::graphic::input::LAST_KEY_PRESSED.swap(-1, std::sync::atomic::Ordering::Relaxed); - if pressed != -1 { - let new_key = crate::module::KeyboardKey::from(pressed); - - if new_key == crate::module::KeyboardKey::KeyEscape { - data.key_bind = crate::module::KeyboardKey::KeyNone; // Unbind - ui.data_mut(|d| d.insert_temp(binding_id, false)); - } else { - // Check for duplicates - let mut is_duplicate = false; - let mut duplicate_name = String::new(); - - for other_mod in client_modules_guard.values() { - if std::sync::Arc::ptr_eq(module, other_mod) { - continue; - } - let other_lock = other_mod.lock().unwrap(); - let other_data = other_lock.get_module_data(); - if other_data.key_bind == new_key { - is_duplicate = true; - duplicate_name = other_data.name.clone(); - break; - } - } - - if is_duplicate { - crate::graphic::notification::Notification::send( - crate::graphic::notification::NotificationType::Alert, - "Keybind Conflict", - &format!("Cheat '{}' already uses key '{}'", duplicate_name, new_key.to_string()) - ); - } else { - data.key_bind = new_key; - ui.data_mut(|d| d.insert_temp(binding_id, false)); - } - } - } - } - - // Small clean minimal button for the bind - let btn = ui.add(egui::Button::new( - egui::RichText::new(&keybind_str).color(Color32::from_rgb(160, 160, 160)) - ).fill(Color32::from_rgb(25, 25, 25)).stroke(egui::Stroke::NONE)); - - if btn.clicked() { - // Toggle binding mode - let new_state = !is_binding; - ui.data_mut(|d| d.insert_temp(binding_id, new_state)); - if new_state { - // Flush any old presses - crate::graphic::input::LAST_KEY_PRESSED.store(-1, std::sync::atomic::Ordering::Relaxed); - } - } - }); - }); - - for setting in &mut data.settings { - match setting { - ModuleSetting::Toggle { name, value } => { - ui.checkbox(value, name.as_str()); - } - ModuleSetting::Slider { - name, - value, - min, - max, - } => { - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label(name.as_str()); - ui.with_layout( - egui::Layout::right_to_left( - egui::Align::Center, - ), - |ui| { - ui.label(format!( - "{:.2}", - value - )); - }, - ); - }); - let available_w = ui.available_width(); - ui.style_mut().spacing.slider_width = - available_w; - ui.add( - egui::Slider::new( - value, - min.clone()..=max.clone(), - ) - .show_value(false) - .text(""), - ); - }); - } - ModuleSetting::Choice { - name, - value, - options, - } => { - ui.vertical(|ui| { - ui.label(name.as_str()); - egui::ComboBox::from_id_salt(name.as_str()) - .width(ui.available_width()) - .selected_text( - options - .get(*value) - .map(|s| s.as_str()) - .unwrap_or("??"), - ) - .show_ui(ui, |ui| { - for (idx, opt) in - options.iter().enumerate() - { - ui.selectable_value( - value, - idx, - opt.as_str(), - ); - } - }); - }); - } - ModuleSetting::Color { name, .. } => { - ui.label(format!( - "{}: [Color Settings soon]", - name - )); - } - } - } - ui.add_space(5.0); - }); - }); - } - } + // --- interaction --- + let expand_id = Id::new("row_exp").with(name); + let mut expanded = ui.data(|d| d.get_temp::(expand_id).unwrap_or(false)); + + if response.clicked() || response.secondary_clicked() { + let pointer = response.interact_pointer_pos().unwrap_or(Pos2::ZERO); + let toggle_settings = + has_settings && (response.secondary_clicked() || arrow_zone.contains(pointer)); + if toggle_settings { + expanded = !expanded; + ui.data_mut(|d| d.insert_temp(expand_id, expanded)); + } else if response.clicked() { + let next = !enabled; + module.get_module_data_mut().set_enabled(next); + let _ = if next { + module.on_start() + } else { + module.on_stop() + }; + } + } + + // --- chevron + settings --- + if has_settings { + let expand = anim::toggle(ui.ctx(), expand_id, expanded, 0.2, Easing::InOut); + let hovered_arrow = arrow_zone.contains(ui.ctx().pointer_hover_pos().unwrap_or(Pos2::ZERO)); + let chevron_color = if hovered_arrow { theme::TEXT } else { theme::TEXT_MUTED }; + paint_chevron(ui.painter(), arrow_zone.center(), expand, chevron_color); + + if expand > 0.001 { + draw_settings(ui, module.get_module_data_mut(), expand, arc, registry); + } + } +} + +/// Draws a chevron that rotates from ▸ (collapsed) to ▾ (expanded). +fn paint_chevron(painter: &Painter, center: Pos2, open: f32, color: Color32) { + const S: f32 = 3.6; + let angle = open.clamp(0.0, 1.0) * std::f32::consts::FRAC_PI_2; + let (sin, cos) = angle.sin_cos(); + let rotate = |v: Vec2| Vec2::new(v.x * cos - v.y * sin, v.x * sin + v.y * cos); + let points = [ + Vec2::new(S, 0.0), + Vec2::new(-S * 0.7, -S), + Vec2::new(-S * 0.7, S), + ] + .into_iter() + .map(|v| center + rotate(v)) + .collect::>(); + painter.add(Shape::convex_polygon(points, color, Stroke::NONE)); +} + +/// Renders the keybind row and every [`ModuleSetting`] of an expanded module. +fn draw_settings( + ui: &mut Ui, + data: &mut ModuleData, + fade: f32, + arc: &ModuleArc, + registry: &ModuleMap, +) { + egui::Frame::none() + .fill(theme::SURFACE) + .inner_margin(Margin::symmetric(10.0, 8.0)) + .show(ui, |ui| { + ui.set_opacity(fade); + ui.set_min_width(PANEL_W - 20.0); + ui.set_max_width(PANEL_W - 20.0); + ui.spacing_mut().item_spacing.y = 7.0; + + keybind_row(ui, data, arc, registry); + + for setting in &mut data.settings { + match setting { + ModuleSetting::Toggle { name, value } => { + ui.checkbox(value, label(name)); } - }); - }); + ModuleSetting::Slider { + name, + value, + min, + max, + } => { + labelled_value(ui, name, &format!("{value:.2}")); + ui.spacing_mut().slider_width = ui.available_width(); + ui.add(egui::Slider::new(value, *min..=*max).show_value(false)); + } + ModuleSetting::Choice { + name, + value, + options, + } => { + ui.label(label(name)); + egui::ComboBox::from_id_salt(Id::new("choice").with(name.as_str())) + .width(ui.available_width()) + .selected_text( + RichText::new( + options.get(*value).map(String::as_str).unwrap_or("—"), + ) + .size(12.0), + ) + .show_ui(ui, |ui| { + for (idx, option) in options.iter().enumerate() { + ui.selectable_value(value, idx, option.as_str()); + } + }); + } + ModuleSetting::Color { name, value } => { + ui.horizontal(|ui| { + ui.label(label(name)); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let mut rgba = egui::Rgba::from_rgba_unmultiplied( + value[0], value[1], value[2], value[3], + ); + let changed = egui::color_picker::color_edit_button_rgba( + ui, + &mut rgba, + egui::color_picker::Alpha::OnlyBlend, + ) + .changed(); + if changed { + *value = rgba.to_array(); + } + }); + }); + } + } + } + ui.add_space(1.0); + }); +} + +/// The "Bind" row: click the button, then press a key (Esc unbinds). +fn keybind_row(ui: &mut Ui, data: &mut ModuleData, arc: &ModuleArc, registry: &ModuleMap) { + ui.horizontal(|ui| { + ui.label(label("Bind")); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let bind_id = Id::new("kb_listen").with(data.name.as_str()); + let listening = ui.data(|d| d.get_temp::(bind_id).unwrap_or(false)); + + let caption = if listening { + if capture_keybind(data, arc, registry) { + ui.data_mut(|d| d.insert_temp(bind_id, false)); + } + "press…".to_string() + } else { + data.key_bind.to_string() + }; + + let color = if listening { theme::ACCENT } else { theme::TEXT_DIM }; + let button = Button::new(RichText::new(caption).size(12.0).color(color)) + .fill(theme::ELEVATED) + .stroke(Stroke::NONE); + if ui.add(button).clicked() { + let next = !listening; + ui.data_mut(|d| d.insert_temp(bind_id, next)); + if next { + LAST_KEY_PRESSED.store(-1, Ordering::Relaxed); + } + } + }); + }); +} + +/// Consumes the latest key press while in bind mode. +/// +/// Returns `true` when listening should stop — a key was applied, the module +/// was unbound, or the chosen key was rejected as a duplicate. +fn capture_keybind(data: &mut ModuleData, arc: &ModuleArc, registry: &ModuleMap) -> bool { + let pressed = LAST_KEY_PRESSED.swap(-1, Ordering::Relaxed); + if pressed == -1 { + return false; + } + let key = KeyboardKey::from(pressed); - curr_x += win_w + gap_x; + if key == KeyboardKey::KeyEscape { + data.key_bind = KeyboardKey::KeyNone; + return true; } + + // Reject keys already taken by another module. + for other in registry.values() { + if Arc::ptr_eq(arc, other) { + continue; + } + let owner = other.lock().unwrap(); + if owner.get_module_data().key_bind == key { + let owner_name = owner.get_module_data().name.clone(); + drop(owner); + Notification::send( + NotificationType::Warning, + "Keybind in use", + &format!("'{}' is bound to {}", owner_name, key.to_string()), + ); + return true; + } + } + + data.key_bind = key; + true +} + +/// A dimmed 12px setting label. +fn label(text: &str) -> RichText { + RichText::new(text).size(12.0).color(theme::TEXT_DIM) +} + +/// Draws a `name … value` row, value accented and right-aligned. +fn labelled_value(ui: &mut Ui, name: &str, value: &str) { + ui.horizontal(|ui| { + ui.label(label(name)); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.label(RichText::new(value).size(12.0).color(theme::ACCENT)); + }); + }); } diff --git a/client/src/graphic/mod.rs b/client/src/graphic/mod.rs index ded211b..ed7cae2 100644 --- a/client/src/graphic/mod.rs +++ b/client/src/graphic/mod.rs @@ -1,7 +1,9 @@ +pub mod anim; pub mod gui; pub mod hook; pub mod hud; pub mod input; pub mod menu; pub mod notification; +pub mod theme; pub mod ui_engine; diff --git a/client/src/graphic/notification.rs b/client/src/graphic/notification.rs index a0ca105..a63531a 100644 --- a/client/src/graphic/notification.rs +++ b/client/src/graphic/notification.rs @@ -1,8 +1,21 @@ -use egui::{Align2, Color32, Pos2, Rect, Rounding, Stroke, Vec2}; +//! Toast notifications. +//! +//! `Notification::send` can be called from anywhere; the queue is drained and +//! rendered once per frame by [`draw`]. Cards slide in from the right, stack +//! with a smooth spring, and carry a countdown progress bar. + +use crate::graphic::anim::{self, Easing}; +use crate::graphic::theme; +use egui::{ + Align2, Color32, Context, FontId, Id, LayerId, Order, Painter, Pos2, Rect, Rounding, Stroke, + Vec2, +}; use lazy_static::lazy_static; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Mutex; use std::time::Instant; +/// Severity of a notification — drives its color and icon. #[derive(Clone, Copy, Debug, PartialEq)] pub enum NotificationType { Info, @@ -10,23 +23,53 @@ pub enum NotificationType { Alert, } -#[derive(Clone)] +impl NotificationType { + /// Accent color for this severity. + fn color(self) -> Color32 { + match self { + NotificationType::Info => theme::ACCENT, + NotificationType::Warning => theme::WARN, + NotificationType::Alert => theme::DANGER, + } + } + + /// Single-glyph icon for this severity. + fn icon(self) -> &'static str { + match self { + NotificationType::Info => "i", + NotificationType::Warning => "!", + NotificationType::Alert => "x", + } + } +} + +/// A single queued toast. pub struct Notification { - pub notif_type: NotificationType, - pub title: String, - pub message: String, - pub spawn_time: Instant, - pub duration: f32, + /// Stable id, used to keep per-card animations consistent across frames. + id: u64, + notif_type: NotificationType, + title: String, + message: String, + spawn_time: Instant, + duration: f32, +} + +static NEXT_ID: AtomicU64 = AtomicU64::new(0); + +lazy_static! { + static ref QUEUE: Mutex> = Mutex::new(Vec::new()); } impl Notification { + /// Queues a notification with the default 3-second lifetime. pub fn send(notif_type: NotificationType, title: &str, message: &str) { Self::send_with_time(notif_type, title, message, 3.0); } + /// Queues a notification that lives for `duration` seconds. pub fn send_with_time(notif_type: NotificationType, title: &str, message: &str, duration: f32) { - let mut queue = NOTIFICATIONS.lock().unwrap(); - queue.push(Notification { + QUEUE.lock().unwrap().push(Notification { + id: NEXT_ID.fetch_add(1, Ordering::Relaxed), notif_type, title: title.to_string(), message: message.to_string(), @@ -36,135 +79,113 @@ impl Notification { } } -lazy_static! { - pub static ref NOTIFICATIONS: Mutex> = Mutex::new(Vec::new()); -} - -pub fn draw_notifications(ctx: &egui::Context) { - let mut queue = NOTIFICATIONS.lock().unwrap(); +/// Card geometry. +const WIDTH: f32 = 258.0; +const HEIGHT: f32 = 58.0; +const MARGIN: f32 = 14.0; +const GAP: f32 = 8.0; +/// Duration of the slide-in / slide-out transition, in seconds. +const SLIDE: f32 = 0.28; + +/// Drains expired toasts and draws the rest. Call once per frame. +pub fn draw(ctx: &Context) { + let mut queue = QUEUE.lock().unwrap(); let now = Instant::now(); - - // Remove expired notifications queue.retain(|n| now.duration_since(n.spawn_time).as_secs_f32() < n.duration); + if queue.is_empty() { + return; + } - let screen_width = ctx.screen_rect().width(); - let pad = 15.0; - let width = 250.0; - let height = 60.0; - let mut current_y = pad; // Draw notifications from top right downwards + // One painter for every card — the old code rebuilt it seven times each. + let painter = ctx.layer_painter(LayerId::new(Order::Tooltip, Id::new("notifications"))); + let screen_w = ctx.screen_rect().width(); - for n in queue.iter() { + for (index, n) in queue.iter().enumerate() { let elapsed = now.duration_since(n.spawn_time).as_secs_f32(); + let remaining = n.duration - elapsed; - // Calculate animation offsets (slide in from right, then slide out to right) - let in_anim_dur = 0.3; - let out_anim_dur = 0.3; - - let slide_offset = if elapsed < in_anim_dur { - // Slide in from right (start off-screen right, move left to 0 offset) - let progress = elapsed / in_anim_dur; - // cubic ease out - let ease = 1.0 - (1.0 - progress).powi(3); - 300.0 * (1.0 - ease) - } else if elapsed > n.duration - out_anim_dur { - // Slide out to right - let progress = (elapsed - (n.duration - out_anim_dur)) / out_anim_dur; - // cubic ease in - let ease = progress.powi(3); - 300.0 * ease + // Horizontal travel: 0 = docked, 1 = fully off the right edge. + let offset = if elapsed < SLIDE { + 1.0 - Easing::Out.apply(elapsed / SLIDE) + } else if remaining < SLIDE { + 1.0 - Easing::In.apply(remaining / SLIDE) } else { - 0.0 // Fully visible + 0.0 }; - // Define actual rect anchored to top-right - // x-coord = screen_width - pad - width + slide_offset - let rect_x = screen_width - pad - width + slide_offset; - - let rect = Rect::from_min_size(Pos2::new(rect_x, current_y), Vec2::new(width, height)); - - // Draw background - ctx.layer_painter(egui::LayerId::new( - egui::Order::Tooltip, - egui::Id::new("notifications"), - )) - .rect( - rect, - Rounding::same(4.0), - Color32::from_black_alpha(220), - Stroke::new(1.0, Color32::from_rgb(45, 45, 45)), - ); - - // Color coding by type - let (icon_color, icon_char) = match n.notif_type { - NotificationType::Info => (Color32::from_rgb(26, 171, 138), "i"), // Teal - NotificationType::Warning => (Color32::from_rgb(255, 165, 0), "!"), // Orange - NotificationType::Alert => (Color32::from_rgb(220, 50, 50), "X"), // Red - }; + // Smooth vertical stacking so cards glide up as others expire. + let target_y = MARGIN + index as f32 * (HEIGHT + GAP); + let y = anim::ease_to(ctx, Id::new("notif_y").with(n.id), target_y, 0.2, Easing::Out); - // Draw left accent bar - let accent_rect = Rect::from_min_size(rect.min, Vec2::new(4.0, height)); - ctx.layer_painter(egui::LayerId::new( - egui::Order::Tooltip, - egui::Id::new("notifications"), - )) - .rect_filled( - accent_rect, - Rounding { - nw: 4.0, - sw: 4.0, - ne: 0.0, - se: 0.0, - }, - icon_color, - ); - - // Draw Icon bg circle - let center_icon = rect.min + Vec2::new(25.0, height / 2.0); - ctx.layer_painter(egui::LayerId::new( - egui::Order::Tooltip, - egui::Id::new("notifications"), - )) - .circle_filled(center_icon, 12.0, Color32::from_white_alpha(20)); - - // Draw Icon text - ctx.layer_painter(egui::LayerId::new( - egui::Order::Tooltip, - egui::Id::new("notifications"), - )) - .text( - center_icon, - Align2::CENTER_CENTER, - icon_char, - egui::FontId::proportional(16.0), - icon_color, - ); - - // Draw Texts - let text_start = rect.min + Vec2::new(50.0, 10.0); - ctx.layer_painter(egui::LayerId::new( - egui::Order::Tooltip, - egui::Id::new("notifications"), - )) - .text( - text_start, - Align2::LEFT_TOP, - &n.title, - egui::FontId::proportional(14.0), - Color32::WHITE, - ); - - ctx.layer_painter(egui::LayerId::new( - egui::Order::Tooltip, - egui::Id::new("notifications"), - )) - .text( - text_start + Vec2::new(0.0, 18.0), - Align2::LEFT_TOP, - &n.message, - egui::FontId::proportional(12.0), - Color32::from_gray(180), - ); - - current_y += height + pad; + let x = (screen_w - MARGIN - WIDTH) + (WIDTH + MARGIN) * offset; + let rect = Rect::from_min_size(Pos2::new(x, y), Vec2::new(WIDTH, HEIGHT)); + draw_card(&painter, rect, n, remaining); } } + +/// Paints one toast card into `rect`. +fn draw_card(painter: &Painter, rect: Rect, n: &Notification, remaining: f32) { + let accent = n.notif_type.color(); + let radius = Rounding::same(theme::RADIUS_INNER); + + painter.rect_filled(rect, radius, Color32::from_rgba_unmultiplied(16, 17, 21, 240)); + painter.rect_stroke(rect, radius, Stroke::new(1.0, theme::BORDER)); + + // Accent rail down the left edge. + let rail = Rect::from_min_size(rect.min, Vec2::new(4.0, rect.height())); + painter.rect_filled( + rail, + Rounding { + nw: theme::RADIUS_INNER, + sw: theme::RADIUS_INNER, + ne: 0.0, + se: 0.0, + }, + accent, + ); + + // Icon disc. + let icon_center = Pos2::new(rect.min.x + 30.0, rect.center().y); + painter.circle_filled(icon_center, 13.0, theme::with_alpha(accent, 0.18)); + painter.text( + icon_center, + Align2::CENTER_CENTER, + n.notif_type.icon(), + FontId::proportional(15.0), + accent, + ); + + // Title and message. + let text_x = rect.min.x + 52.0; + painter.text( + Pos2::new(text_x, rect.min.y + 13.0), + Align2::LEFT_TOP, + &n.title, + FontId::proportional(14.0), + theme::TEXT, + ); + painter.text( + Pos2::new(text_x, rect.min.y + 31.0), + Align2::LEFT_TOP, + &n.message, + FontId::proportional(12.0), + theme::TEXT_DIM, + ); + + // Countdown progress bar pinned to the bottom edge. + let fraction = (remaining / n.duration).clamp(0.0, 1.0); + let bar = Rect::from_min_size( + rect.left_bottom() - Vec2::new(0.0, 3.0), + Vec2::new(rect.width() * fraction, 3.0), + ); + painter.rect_filled( + bar, + Rounding { + nw: 0.0, + ne: 0.0, + sw: theme::RADIUS_INNER, + se: 0.0, + }, + theme::with_alpha(accent, 0.85), + ); +} diff --git a/client/src/graphic/theme.rs b/client/src/graphic/theme.rs new file mode 100644 index 0000000..7526ec1 --- /dev/null +++ b/client/src/graphic/theme.rs @@ -0,0 +1,127 @@ +//! Centralized visual theme for the in-game overlay. +//! +//! Every color, radius and the egui [`Style`] live here. The rest of the +//! `graphic` module must never hardcode a raw RGB value — pull it from this +//! module so the whole UI stays consistent and re-skinnable from one place. + +use egui::{Color32, Context, Margin, Rounding, Stroke, Style, Vec2, Visuals}; + +// --- Palette --------------------------------------------------------------- + +/// Brand color: highlights, enabled modules, headers, focus rings. +pub const ACCENT: Color32 = Color32::from_rgb(38, 198, 156); +/// Muted accent for secondary emphasis. +pub const ACCENT_DIM: Color32 = Color32::from_rgb(26, 120, 100); + +/// Backgrounds, darkest to lightest. +pub const BASE: Color32 = Color32::from_rgb(17, 18, 22); +pub const SURFACE: Color32 = Color32::from_rgb(24, 25, 31); +pub const SURFACE_HOVER: Color32 = Color32::from_rgb(33, 35, 43); +pub const ELEVATED: Color32 = Color32::from_rgb(29, 31, 38); + +/// Borders and separators. +pub const BORDER: Color32 = Color32::from_rgb(42, 44, 54); + +/// Text shades, brightest to dimmest. +pub const TEXT: Color32 = Color32::from_rgb(236, 237, 242); +pub const TEXT_DIM: Color32 = Color32::from_rgb(150, 152, 162); +pub const TEXT_MUTED: Color32 = Color32::from_rgb(92, 94, 104); + +/// Status colors for notifications. +pub const WARN: Color32 = Color32::from_rgb(240, 170, 60); +pub const DANGER: Color32 = Color32::from_rgb(226, 74, 74); + +// --- Metrics --------------------------------------------------------------- + +/// Corner radius for panels. +pub const RADIUS: f32 = 7.0; +/// Corner radius for inner widgets (rows, buttons, cards). +pub const RADIUS_INNER: f32 = 4.0; + +// --- Style installation ---------------------------------------------------- + +/// Builds and installs the overlay style on `ctx`. +/// +/// Call exactly once, right after the [`Context`] is created — egui keeps the +/// style in an `Arc`, so re-applying it every frame is pure waste. +pub fn apply(ctx: &Context) { + let mut style = Style::default(); + let mut v = Visuals::dark(); + + v.window_fill = BASE; + v.panel_fill = BASE; + v.window_stroke = Stroke::new(1.0, BORDER); + v.window_rounding = Rounding::same(RADIUS); + v.window_shadow = egui::epaint::Shadow { + offset: Vec2::new(0.0, 6.0), + blur: 24.0, + spread: 0.0, + color: Color32::from_black_alpha(140), + }; + v.popup_shadow = v.window_shadow; + + v.selection.bg_fill = ACCENT; + v.selection.stroke = Stroke::NONE; + v.hyperlink_color = ACCENT; + + let w = &mut v.widgets; + + w.noninteractive.bg_fill = SURFACE; + w.noninteractive.bg_stroke = Stroke::new(1.0, BORDER); + w.noninteractive.fg_stroke = Stroke::new(1.0, TEXT_DIM); + + w.inactive.bg_fill = SURFACE; + w.inactive.weak_bg_fill = SURFACE; + w.inactive.bg_stroke = Stroke::new(1.0, BORDER); + w.inactive.fg_stroke = Stroke::new(1.0, TEXT_DIM); + w.inactive.rounding = Rounding::same(RADIUS_INNER); + w.inactive.expansion = 0.0; + + w.hovered.bg_fill = SURFACE_HOVER; + w.hovered.weak_bg_fill = SURFACE_HOVER; + w.hovered.bg_stroke = Stroke::new(1.0, BORDER); + w.hovered.fg_stroke = Stroke::new(1.0, TEXT); + w.hovered.rounding = Rounding::same(RADIUS_INNER); + w.hovered.expansion = 1.0; + + w.active.bg_fill = ACCENT; + w.active.weak_bg_fill = ACCENT; + w.active.bg_stroke = Stroke::NONE; + w.active.fg_stroke = Stroke::new(1.0, Color32::BLACK); + w.active.rounding = Rounding::same(RADIUS_INNER); + w.active.expansion = -1.0; + + w.open.bg_fill = SURFACE_HOVER; + w.open.bg_stroke = Stroke::new(1.0, BORDER); + w.open.fg_stroke = Stroke::new(1.0, TEXT); + + style.visuals = v; + + style.spacing.item_spacing = Vec2::new(6.0, 6.0); + style.spacing.window_margin = Margin::same(0.0); + style.spacing.button_padding = Vec2::new(8.0, 4.0); + style.spacing.interact_size.y = 16.0; + style.spacing.slider_width = 100.0; + + ctx.set_style(style); +} + +// --- Shared helpers -------------------------------------------------------- + +/// Linearly interpolates between two colors. `t` is clamped to `0..=1`. +pub fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 { + let t = t.clamp(0.0, 1.0); + let mix = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round() as u8; + Color32::from_rgba_unmultiplied( + mix(a.r(), b.r()), + mix(a.g(), b.g()), + mix(a.b(), b.b()), + mix(a.a(), b.a()), + ) +} + +/// Returns `color` with its alpha scaled by `factor` (`0..=1`). +pub fn with_alpha(color: Color32, factor: f32) -> Color32 { + let a = (color.a() as f32 * factor.clamp(0.0, 1.0)).round() as u8; + Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), a) +} diff --git a/client/src/graphic/ui_engine.rs b/client/src/graphic/ui_engine.rs index c716219..e64ae8d 100644 --- a/client/src/graphic/ui_engine.rs +++ b/client/src/graphic/ui_engine.rs @@ -4,15 +4,9 @@ use crate::graphic::input::{GUI_OPEN, MOUSE_STATE}; use egui::Context; use egui_glow::Painter; use lazy_static::lazy_static; -use std::collections::HashMap; use std::sync::atomic::Ordering; -use std::sync::Mutex; - -#[derive(Clone, Copy)] -pub struct WindowAnimState { - pub actual_pos: egui::Pos2, - pub velocity: egui::Vec2, -} +use std::sync::{Mutex, OnceLock}; +use std::time::Instant; pub struct EguiState { pub ctx: Context, @@ -20,13 +14,22 @@ pub struct EguiState { pub last_left_down: bool, pub last_right_down: bool, pub last_mouse_pos: egui::Pos2, - pub window_anim_states: HashMap, } lazy_static! { pub static ref EGUI_STATE: Mutex> = Mutex::new(None); } +/// Monotonic seconds since the overlay first rendered. +/// +/// Fed to egui as `RawInput::time` so its clock tracks wall time instead of a +/// fixed predicted delta — the cure for animations stuttering when Minecraft +/// paces frames irregularly (most visibly while the game is paused). +fn elapsed_seconds() -> f64 { + static START: OnceLock = OnceLock::new(); + START.get_or_init(Instant::now).elapsed().as_secs_f64() +} + pub fn gather_egui_inputs( state: &mut EguiState, screen_width: f32, @@ -34,6 +37,7 @@ pub fn gather_egui_inputs( scale_factor: f32, ) -> egui::RawInput { let mut raw_input = egui::RawInput::default(); + raw_input.time = Some(elapsed_seconds()); let mut viewport_info = egui::ViewportInfo::default(); viewport_info.native_pixels_per_point = Some(scale_factor); @@ -110,6 +114,10 @@ pub unsafe fn render_egui_ui() { let fonts = egui::FontDefinitions::default(); ctx.set_fonts(fonts); + // Install the overlay theme once — egui stores the style in an Arc, + // so there is no reason to rebuild it every frame. + crate::graphic::theme::apply(&ctx); + unsafe { crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); @@ -125,7 +133,6 @@ pub unsafe fn render_egui_ui() { last_left_down: false, last_right_down: false, last_mouse_pos: egui::pos2(0.0, 0.0), - window_anim_states: HashMap::new(), }); } @@ -135,7 +142,7 @@ pub unsafe fn render_egui_ui() { let ctx = state.ctx.clone(); let full_output = ctx.run(raw_input, |ctx| { - crate::graphic::gui::render_all(ctx, &mut state.window_anim_states); + crate::graphic::gui::render_all(ctx); }); let clipped_primitives = state diff --git a/client/src/module/mod.rs b/client/src/module/mod.rs index 9db2f1c..6633794 100644 --- a/client/src/module/mod.rs +++ b/client/src/module/mod.rs @@ -6,7 +6,7 @@ pub mod movement; pub type ModuleType = Box; #[allow(dead_code)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ModuleCategory { COMBAT, MOVEMENT, From cc750c377f5046d6c936f510c938be0ef9b6ebfc Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Wed, 20 May 2026 09:58:20 +0200 Subject: [PATCH 14/16] Update to 26.1.2 --- CLAUDE.md | 11 +- README.md | 7 +- client/src/mapping/class.rs | 11 ++ client/src/mapping/minecraft_version.rs | 9 + client/src/mapping/mod.rs | 243 +++++++++++++++++------- client/src/mapping/reflect.rs | 119 ++++++++++++ 6 files changed, 326 insertions(+), 74 deletions(-) create mode 100644 client/src/mapping/reflect.rs diff --git a/CLAUDE.md b/CLAUDE.md index 17ba05e..15d788d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Overview -DarkClient is a Minecraft (Java Edition, mappings target **1.21.10**) modification framework written in Rust. It injects native libraries into a running Minecraft JVM and drives the game through JNI. It is a Cargo workspace of three crates. +DarkClient is a Minecraft (Java Edition) modification framework written in Rust. It injects native libraries into a running Minecraft JVM and drives the game through JNI. One build supports both **obfuscated** Minecraft (≤ 1.21.11, via bundled Mojmap mappings) and **unobfuscated** Minecraft (26.1+, via runtime JNI reflection) — see the mapping system below. It is a Cargo workspace of three crates. ## Build & Common Commands @@ -48,13 +48,18 @@ This is the core control flow and spans all three crates: **Module system** (`module/mod.rs`): implement the `Module` trait (`on_start`/`on_stop`/`on_tick`, all returning `anyhow::Result<()>`) plus `ModuleData` accessors. Register new modules in `register_modules()` in `client/src/lib.rs`. Modules carry typed `ModuleSetting`s (Toggle/Slider/Choice/Color). Note: the trait example in `README.md` is stale — the real trait methods return `anyhow::Result<()>`. -**Mapping system** (`mapping/`): handles Minecraft obfuscation. `mappings.json` (project root) and `java_mappings.json` are **`include_str!`'d into the binary at compile time** by `Mapping::new()` — changing mappings requires rebuilding `client`. `MinecraftClassType` enum maps deobfuscated class names to their JSON entries; `Mapping` resolves obfuscated names and wraps all JNI calls (`call_method`, `call_static_method`, `get_field`, `set_field`, etc.). `class.rs` does overload resolution by scoring argument-type compatibility against JNI signatures. +**Mapping system** (`mapping/`): bridges deobfuscated (Mojmap) names — what `MinecraftClassType` and the rest of the code use — to whatever the running JVM actually exposes. `Mapping::new()` auto-detects the build by probing `find_class("net/minecraft/client/Minecraft")` and picks one of two modes: + +- **Obfuscated** (`Mode::Obfuscated`): the probe fails. `mappings.json` and `java_mappings.json` (project root, **`include_str!`'d at compile time**) are parsed into a class map; names are translated deobfuscated → obfuscated. +- **Reflected** (`Mode::Reflected`): the probe succeeds (Minecraft 26.1+, unobfuscated). No JSON is used; class/method/field names are identity, and method signatures — still required by JNI — are discovered lazily via `java.lang.Class` reflection in `reflect.rs` and cached. No mapping file is ever needed for new versions. + +Both modes share one code path: a `RwLock>>` populated up-front (obfuscated) or lazily by reflection (reflected). `Mapping` wraps all JNI calls (`call_method`, `call_static_method`, `get_field`, `set_field`, etc.); `class.rs` does overload resolution by scoring argument-type compatibility against JNI signatures. **Lifecycle safety**: the global `RUNNING: AtomicBool` gates `on_frame` and the agent's loops. A panic hook in `initialize_client` calls `cleanup_client` so input/render hooks are always uninstalled and GLFW callbacks restored, even on panic. ## Mappings -`conversion.py` downloads official Mojang mappings for a chosen Minecraft version and writes the custom `mappings.json` format. The committed `mappings.json` is ~18 MB. `java_mappings.json` is a small hand-written supplement for `java.*` classes, merged in at load time. +`conversion.py` downloads official Mojang mappings for a chosen **obfuscated** Minecraft version (≤ 1.21.11) and writes the custom `mappings.json` format. The committed `mappings.json` is ~18 MB. `java_mappings.json` is a small hand-written supplement for `java.*` classes, merged in at load time. Unobfuscated versions (26.1+) need none of this — they go through the reflected mapping path. The 26.1 runtime requires JDK 25. ## Logs diff --git a/README.md b/README.md index 370b4e5..07e0e34 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,12 @@ A Minecraft hacked client built in Rust, using JNI (Java Native Interface) for seamless integration with Minecraft's Java runtime. DarkClient provides a robust architecture for developing game modifications through dynamic library injection. -### Minecraft Version Mappings: 1.21.10 +### Supported Minecraft Versions + +- **Obfuscated builds** (≤ 1.21.11): bundled Mojmap mappings (`mappings.json`, currently 1.21.10). +- **Unobfuscated builds** (26.1+): no mappings needed — names are resolved directly, method signatures via runtime JNI reflection. + +The build auto-detects which mode to use at runtime. A single binary works on both. ## 🚀 Features diff --git a/client/src/mapping/class.rs b/client/src/mapping/class.rs index 216a722..87095da 100644 --- a/client/src/mapping/class.rs +++ b/client/src/mapping/class.rs @@ -86,6 +86,17 @@ enum SignatureMatch { #[allow(dead_code)] impl MinecraftClass { + /// Builds a class entry from runtime reflection. Names are identity (no + /// obfuscation to translate); only signatures were discovered. Fields are + /// left empty — unobfuscated builds resolve field names directly. + pub fn from_reflection(name: String, methods: HashMap>) -> Self { + MinecraftClass { + name, + methods, + fields: HashMap::new(), + } + } + pub fn get_method(&self, name: &str) -> anyhow::Result<&Method> { match self.methods.get(name).unwrap().first() { Some(method) => Ok(method), diff --git a/client/src/mapping/minecraft_version.rs b/client/src/mapping/minecraft_version.rs index a5c5648..efb98d7 100644 --- a/client/src/mapping/minecraft_version.rs +++ b/client/src/mapping/minecraft_version.rs @@ -8,6 +8,15 @@ pub struct MinecraftVersion { } impl MinecraftVersion { + /// Sentinel "newest possible" version, used for unobfuscated builds where + /// no version string is parsed: it makes every `version < X` gate resolve + /// to the latest branch. + pub const LATEST: MinecraftVersion = MinecraftVersion { + major: u32::MAX, + minor: 0, + patch: 0, + }; + pub fn new(major: u32, minor: u32, patch: u32) -> MinecraftVersion { MinecraftVersion { major, diff --git a/client/src/mapping/mod.rs b/client/src/mapping/mod.rs index 844eff1..4dce7c1 100644 --- a/client/src/mapping/mod.rs +++ b/client/src/mapping/mod.rs @@ -5,9 +5,10 @@ use crate::mapping::client::minecraft::Minecraft; use crate::mapping::minecraft_version::MinecraftVersion; use jni::objects::{GlobalRef, JObject, JString, JValue, JValueOwned}; use jni::JNIEnv; -use log::error; +use log::{error, info}; use serde::Deserialize; use std::collections::HashMap; +use std::sync::{Arc, RwLock}; pub mod class; pub mod class_type; @@ -16,6 +17,7 @@ pub mod entity; pub mod java; mod method; mod minecraft_version; +mod reflect; pub trait GameContext { fn minecraft(&self) -> &'static Minecraft { @@ -27,13 +29,34 @@ pub trait GameContext { } } -/// Root structure containing all mapped Minecraft classes +/// On-disk JSON shape. Only obfuscated builds ship one of these. #[derive(Debug, Deserialize)] -pub struct Mapping { +struct MappingFile { version: MinecraftVersion, classes: HashMap, } +/// How class / method / field names are resolved to their runtime form. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + /// Obfuscated build: translate names through the bundled Mojmap JSON. + Obfuscated, + /// Unobfuscated build (Minecraft 26.1+): names are already real, method + /// signatures are discovered lazily via JNI reflection. + Reflected, +} + +/// Bridges deobfuscated (Mojmap) names to whatever the running JVM actually +/// uses, transparently for both obfuscated and unobfuscated Minecraft. +#[derive(Debug)] +pub struct Mapping { + mode: Mode, + version: MinecraftVersion, + /// In obfuscated mode every class is present up-front; in reflected mode + /// classes are discovered and cached on first use. + classes: RwLock>>, +} + #[allow(dead_code)] pub enum FieldType<'local> { Boolean, @@ -60,26 +83,63 @@ impl FieldType<'_> { FieldType::Float => String::from("F"), FieldType::Double => String::from("D"), FieldType::String => String::from("Ljava/lang/String;"), - FieldType::Object(minecraft_class_type, mapping) => { - let class_name = &mapping.get_class(minecraft_class_type.get_name())?.name; - format!("L{};", class_name) + FieldType::Object(class_type, mapping) => { + format!("L{};", mapping.runtime_class_name(*class_type)?) } }) } } +/// Probes whether the running Minecraft is unobfuscated. +/// +/// In an unobfuscated build the real Mojmap class name resolves directly; in +/// an obfuscated build that class only exists under its scrambled name, so the +/// lookup fails (and the resulting pending exception is cleared). +fn is_unobfuscated() -> bool { + match DarkClient::instance().get_env() { + Ok(mut env) => { + let found = env.find_class("net/minecraft/client/Minecraft").is_ok(); + if !found { + let _ = env.exception_clear(); + } + found + } + Err(_) => false, + } +} + #[allow(dead_code)] impl Mapping { pub fn new() -> anyhow::Result { - let contents = include_str!("../../../mappings.json"); - let mut mapping: Mapping = serde_json::from_str(contents)?; - - let java_contents = include_str!("../../../java_mappings.json"); - let java_classes: HashMap = serde_json::from_str(java_contents)?; - - mapping.classes.extend(java_classes); + if is_unobfuscated() { + info!("Unobfuscated Minecraft detected — using runtime reflection mapping"); + return Ok(Mapping { + mode: Mode::Reflected, + version: MinecraftVersion::LATEST, + classes: RwLock::new(HashMap::new()), + }); + } - Ok(mapping) + info!("Obfuscated Minecraft detected — using bundled Mojmap mappings"); + let mut file: MappingFile = serde_json::from_str(include_str!("../../../mappings.json"))?; + + // Standard `java.*` classes are never obfuscated; they live in a + // small hand-written supplement. + let java_classes: HashMap = + serde_json::from_str(include_str!("../../../java_mappings.json"))?; + file.classes.extend(java_classes); + + let classes = file + .classes + .into_iter() + .map(|(name, class)| (name, Arc::new(class))) + .collect(); + + Ok(Mapping { + mode: Mode::Obfuscated, + version: file.version, + classes: RwLock::new(classes), + }) } fn get_client(&self) -> &DarkClient { @@ -94,19 +154,59 @@ impl Mapping { self.version } - pub fn get_class(&self, name: &str) -> anyhow::Result<&MinecraftClass> { - match self.classes.get(name) { - Some(class) => Ok(class), - None => Err(anyhow::anyhow!("{} java class not found", name)), + /// Resolves a mapped class by its deobfuscated name. In reflected mode the + /// class is reflected from the JVM and cached on first request. + pub fn get_class(&self, name: &str) -> anyhow::Result> { + if let Some(class) = self.classes.read().unwrap().get(name) { + return Ok(Arc::clone(class)); + } + + match self.mode { + Mode::Obfuscated => Err(anyhow::anyhow!("{} java class not found", name)), + Mode::Reflected => { + let class = Arc::new(reflect::reflect_class(name)?); + self.classes + .write() + .unwrap() + .insert(name.to_owned(), Arc::clone(&class)); + Ok(class) + } } } - /// Find the real name of a class given his obfuscated name - fn find_class_by_obfuscated_name(&self, obfuscated_name: &str) -> Option<&str> { + /// Runtime (JVM) name of a class. + fn runtime_class_name(&self, class_type: MinecraftClassType) -> anyhow::Result { + match self.mode { + Mode::Reflected => Ok(class_type.get_name().to_owned()), + Mode::Obfuscated => Ok(self.get_class(class_type.get_name())?.name.clone()), + } + } + + /// Runtime (JVM) name of a field. + fn runtime_field_name( + &self, + class_type: MinecraftClassType, + field: &str, + ) -> anyhow::Result { + match self.mode { + Mode::Reflected => Ok(field.to_owned()), + Mode::Obfuscated => Ok(self + .get_class(class_type.get_name())? + .get_field(field)? + .name + .clone()), + } + } + + /// Finds the deobfuscated name of a class given its obfuscated name. + /// Only meaningful in obfuscated mode; used to prettify error messages. + fn find_class_by_obfuscated_name(&self, obfuscated_name: &str) -> Option { self.classes + .read() + .unwrap() .iter() - .find(|(_, class_data)| class_data.name == obfuscated_name) - .map(|(deobfuscated_name, _)| deobfuscated_name.as_str()) + .find(|(_, class)| class.name == obfuscated_name) + .map(|(deobfuscated_name, _)| deobfuscated_name.clone()) } fn translate_type_descriptor<'a>(&self, descriptor: &mut &'a str) -> String { @@ -121,12 +221,11 @@ impl Mapping { let obfuscated_name = &stripped[..end_index]; let deobfuscated_name = self .find_class_by_obfuscated_name(obfuscated_name) - .unwrap_or(obfuscated_name); + .unwrap_or_else(|| obfuscated_name.to_owned()); *descriptor = &stripped[end_index + 1..]; - deobfuscated_name.to_string() + deobfuscated_name } else { - // Malformed, return the rest of the string let rest = descriptor.to_string(); *descriptor = ""; rest @@ -163,13 +262,9 @@ impl Mapping { let translated_return = self.translate_type_descriptor(&mut return_type_str); - format!( - "({}) -> {}", - translated_params.join(", "), - translated_return - ) + format!("({}) -> {}", translated_params.join(", "), translated_return) } else { - signature.to_string() // Return the original signature if it's not a valid signature + signature.to_string() } } @@ -185,17 +280,19 @@ impl Mapping { let jclass = match env.find_class(&class.name) { Ok(jclass) => jclass, Err(_) => { + let _ = env.exception_clear(); return Err(anyhow::anyhow!( "Class {} ({}) not found", class_type.get_name(), class.name - )) + )); } }; let method = class.get_method_by_args(method_name, args)?; match env.call_static_method(jclass, &method.name, &method.signature, args) { Ok(value) => Ok(value), Err(_) => { + let _ = env.exception_clear(); let translated_signature = self.translate_signature(&method.signature); Err(anyhow::anyhow!( "Error calling static method {} ({}) in class {} ({}) with signature {} ({})", @@ -224,6 +321,7 @@ impl Mapping { match env.call_method(instance, &method.name, &method.signature, args) { Ok(value) => Ok(value), Err(_) => { + let _ = env.exception_clear(); let translated_signature = self.translate_signature(&method.signature); Err(anyhow::anyhow!( "Error calling method {} ({}) in class {} ({}) with signature {} ({})", @@ -246,27 +344,30 @@ impl Mapping { ) -> anyhow::Result> { let mut env = self.get_env()?; - let class = self.get_class(class_type.get_name())?; - let jclass = match env.find_class(&class.name) { + let class_name = self.runtime_class_name(class_type)?; + let jclass = match env.find_class(&class_name) { Ok(jclass) => jclass, Err(_) => { + let _ = env.exception_clear(); return Err(anyhow::anyhow!( "Class {} ({}) not found", class_type.get_name(), - class.name - )) + class_name + )); } }; - let field = class.get_field(field_name)?; - match env.get_static_field(jclass, &field.name, field_type.get_signature()?) { + let runtime_field = self.runtime_field_name(class_type, field_name)?; + match env.get_static_field(jclass, &runtime_field, field_type.get_signature()?) { Ok(value) => Ok(value), - Err(_) => Err(anyhow::anyhow!( - "Error getting static field {} ({}) from class {} ({})", - field_name, - field.name, - class_type.get_name(), - class.name - )), + Err(_) => { + let _ = env.exception_clear(); + Err(anyhow::anyhow!( + "Error getting static field {} ({}) from class {}", + field_name, + runtime_field, + class_type.get_name() + )) + } } } @@ -279,18 +380,18 @@ impl Mapping { ) -> anyhow::Result> { let mut env = self.get_env()?; - let class = self.get_class(class_type.get_name())?; - let field = class.get_field(field_name)?; - - match env.get_field(instance, &field.name, field_type.get_signature()?) { + let runtime_field = self.runtime_field_name(class_type, field_name)?; + match env.get_field(instance, &runtime_field, field_type.get_signature()?) { Ok(value) => Ok(value), - Err(_) => Err(anyhow::anyhow!( - "Error getting field {} ({}) from class {} ({})", - field_name, - field.name, - class_type.get_name(), - class.name - )), + Err(_) => { + let _ = env.exception_clear(); + Err(anyhow::anyhow!( + "Error getting field {} ({}) from class {}", + field_name, + runtime_field, + class_type.get_name() + )) + } } } @@ -304,17 +405,18 @@ impl Mapping { ) -> anyhow::Result<()> { let mut env = self.get_env()?; - let class = self.get_class(class_type.get_name())?; - let field = class.get_field(field_name)?; - match env.set_field(instance, &field.name, field_type.get_signature()?, value) { + let runtime_field = self.runtime_field_name(class_type, field_name)?; + match env.set_field(instance, &runtime_field, field_type.get_signature()?, value) { Ok(_) => Ok(()), - Err(_) => Err(anyhow::anyhow!( - "Error setting field {} ({}) in class {} ({})", - field_name, - field.name, - class_type.get_name(), - class.name - )), + Err(_) => { + let _ = env.exception_clear(); + Err(anyhow::anyhow!( + "Error setting field {} ({}) in class {}", + field_name, + runtime_field, + class_type.get_name() + )) + } } } @@ -341,15 +443,16 @@ impl Mapping { instance: &JObject, ) -> anyhow::Result { let mut env = self.get_env()?; - let class = self.get_class(class_type.get_name())?; - let jclass = match env.find_class(&class.name) { + let class_name = self.runtime_class_name(class_type)?; + let jclass = match env.find_class(&class_name) { Ok(jclass) => jclass, Err(_) => { + let _ = env.exception_clear(); return Err(anyhow::anyhow!( "Class {} ({}) not found", class_type.get_name(), - class.name - )) + class_name + )); } }; diff --git a/client/src/mapping/reflect.rs b/client/src/mapping/reflect.rs new file mode 100644 index 0000000..d14fc6e --- /dev/null +++ b/client/src/mapping/reflect.rs @@ -0,0 +1,119 @@ +//! Runtime method discovery for unobfuscated Minecraft builds (26.1+). +//! +//! Without an obfuscation map, class and method *names* are already the real +//! ones — but JNI still needs each method's **signature** to make a call. +//! This module reflects a class once through `java.lang.Class` and returns a +//! [`MinecraftClass`] with identity names and reflected signatures, which the +//! rest of the mapping layer then treats exactly like a JSON-parsed entry. + +use crate::client::DarkClient; +use crate::mapping::class::{Method, MinecraftClass}; +use jni::objects::{JObject, JObjectArray, JString}; +use jni::JNIEnv; +use std::collections::HashMap; + +/// Reflects every method declared on — or inherited as public by — +/// `class_name`, returning it as a [`MinecraftClass`]. +pub fn reflect_class(class_name: &str) -> anyhow::Result { + let mut env = DarkClient::instance().get_env()?; + + let jclass: JObject = env + .find_class(class_name) + .map_err(|_| { + let _ = env.exception_clear(); + anyhow::anyhow!("Class {} not found at runtime", class_name) + })? + .into(); + + let mut methods: HashMap> = HashMap::new(); + + // `getMethods` covers inherited public methods; `getDeclaredMethods` + // covers everything declared on this class, public or not. + for accessor in ["getMethods", "getDeclaredMethods"] { + collect_methods(&mut env, &jclass, accessor, &mut methods)?; + } + + Ok(MinecraftClass::from_reflection(class_name.to_owned(), methods)) +} + +/// Calls `accessor` (a `Method[]`-returning method of `Class`) and folds every +/// result into `out`, de-duplicating overloads by signature. +fn collect_methods( + env: &mut JNIEnv, + jclass: &JObject, + accessor: &str, + out: &mut HashMap>, +) -> anyhow::Result<()> { + let array = env + .call_method(jclass, accessor, "()[Ljava/lang/reflect/Method;", &[])? + .l()?; + let array = JObjectArray::from(array); + let count = env.get_array_length(&array)?; + + for index in 0..count { + // Each method spawns several temporary JNI refs — scope them so the + // local-reference table cannot overflow on large classes. + let (name, signature) = env.with_local_frame(64, |env| -> anyhow::Result<_> { + let method = env.get_object_array_element(&array, index)?; + describe_method(env, &method) + })?; + + let overloads = out.entry(name.clone()).or_default(); + if !overloads.iter().any(|m| m.signature == signature) { + overloads.push(Method { name, signature }); + } + } + Ok(()) +} + +/// Reads a `java.lang.reflect.Method` into its name and JNI signature. +fn describe_method(env: &mut JNIEnv, method: &JObject) -> anyhow::Result<(String, String)> { + let name = call_string(env, method, "getName")?; + + let params = env + .call_method(method, "getParameterTypes", "()[Ljava/lang/Class;", &[])? + .l()?; + let params = JObjectArray::from(params); + let param_count = env.get_array_length(¶ms)?; + + let mut signature = String::from("("); + for index in 0..param_count { + let param = env.get_object_array_element(¶ms, index)?; + signature.push_str(&type_descriptor(env, ¶m)?); + } + signature.push(')'); + + let return_type = env + .call_method(method, "getReturnType", "()Ljava/lang/Class;", &[])? + .l()?; + signature.push_str(&type_descriptor(env, &return_type)?); + + Ok((name, signature)) +} + +/// Builds the JNI type descriptor of a `java.lang.Class` instance. +fn type_descriptor(env: &mut JNIEnv, class: &JObject) -> anyhow::Result { + let name = call_string(env, class, "getName")?; + Ok(match name.as_str() { + "boolean" => "Z".to_owned(), + "byte" => "B".to_owned(), + "char" => "C".to_owned(), + "short" => "S".to_owned(), + "int" => "I".to_owned(), + "long" => "J".to_owned(), + "float" => "F".to_owned(), + "double" => "D".to_owned(), + "void" => "V".to_owned(), + // Array classes already report a descriptor, only dotted. + array if array.starts_with('[') => array.replace('.', "/"), + object => format!("L{};", object.replace('.', "/")), + }) +} + +/// Calls a no-argument `String`-returning method and reads the result. +fn call_string(env: &mut JNIEnv, obj: &JObject, method: &str) -> anyhow::Result { + let value = env + .call_method(obj, method, "()Ljava/lang/String;", &[])? + .l()?; + Ok(env.get_string(&JString::from(value))?.to_str()?.to_owned()) +} From de618a4e3402bb05c7e9718c12622475fe806c44 Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Wed, 20 May 2026 10:27:57 +0200 Subject: [PATCH 15/16] Fix Sampler Error (GL) --- client/src/graphic/ui_engine.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/client/src/graphic/ui_engine.rs b/client/src/graphic/ui_engine.rs index e64ae8d..3a63a3a 100644 --- a/client/src/graphic/ui_engine.rs +++ b/client/src/graphic/ui_engine.rs @@ -170,6 +170,19 @@ pub unsafe fn render_egui_ui() { crate::gl::BindBuffer(crate::gl::PIXEL_UNPACK_BUFFER, 0); crate::gl::ActiveTexture(crate::gl::TEXTURE0); + + // Modern Minecraft (Blaze3D, 26.x+) binds GL sampler objects to its + // texture units. A sampler object *overrides* the texture's own + // parameters, so a leftover Minecraft sampler — configured for its + // mipmapped atlas textures — makes egui's mipmap-less font atlas + // texture-incomplete, and an incomplete texture samples as opaque + // black. That black multiplies into every egui fragment (panels and + // text alike). Unbind it from unit 0 — the only unit egui samples — + // and restore it afterwards so Minecraft's own rendering is untouched. + let mut last_sampler = 0; + crate::gl::GetIntegerv(crate::gl::SAMPLER_BINDING, &mut last_sampler); + crate::gl::BindSampler(0, 0); + crate::gl::PixelStorei(crate::gl::UNPACK_ALIGNMENT, 1); crate::gl::PixelStorei(crate::gl::UNPACK_ROW_LENGTH, 0); crate::gl::PixelStorei(crate::gl::UNPACK_SKIP_PIXELS, 0); @@ -196,6 +209,7 @@ pub unsafe fn render_egui_ui() { ); crate::gl::BindVertexArray(last_vertex_array as u32); crate::gl::UseProgram(last_program as u32); + crate::gl::BindSampler(0, last_sampler as u32); crate::gl::ActiveTexture(last_active_texture as u32); } } From fb73483bfa2a356a608f0839748303f36ec28b2a Mon Sep 17 00:00:00 2001 From: TheDarkSword Date: Wed, 20 May 2026 13:03:20 +0200 Subject: [PATCH 16/16] Add ESP --- client/src/graphic/esp.rs | 1081 ++++++++++++++++++++++++ client/src/graphic/gui.rs | 5 +- client/src/graphic/mod.rs | 1 + client/src/lib.rs | 6 + client/src/mapping/class.rs | 19 + client/src/mapping/class_type.rs | 44 + client/src/mapping/mod.rs | 252 +++++- client/src/mapping/reflect.rs | 17 +- client/src/module/mod.rs | 1 + client/src/module/render/chest_esp.rs | 57 ++ client/src/module/render/mob_esp.rs | 70 ++ client/src/module/render/mod.rs | 10 + client/src/module/render/player_esp.rs | 70 ++ java_mappings.json | 20 + 14 files changed, 1605 insertions(+), 48 deletions(-) create mode 100644 client/src/graphic/esp.rs create mode 100644 client/src/module/render/chest_esp.rs create mode 100644 client/src/module/render/mob_esp.rs create mode 100644 client/src/module/render/mod.rs create mode 100644 client/src/module/render/player_esp.rs diff --git a/client/src/graphic/esp.rs b/client/src/graphic/esp.rs new file mode 100644 index 0000000..ff4a40b --- /dev/null +++ b/client/src/graphic/esp.rs @@ -0,0 +1,1081 @@ +//! ESP overlay: 3D wireframe boxes for players, mobs and containers. +//! +//! # Performance model +//! +//! The expensive part of an ESP is the JNI traffic — reading every entity's +//! position, type and stats. Doing that per frame is what tanks the FPS, so it +//! is **decoupled from the frame rate**: +//! +//! * [`gather`] (all the JNI work) runs at most ~20 Hz, throttled by wall time. +//! * Every frame only reads the camera (~6 JNI calls) and does pure-CPU +//! projection + egui drawing. +//! * Positions are interpolated between the last two gathers, so boxes move +//! smoothly even though the data behind them updates at 20 Hz. +//! * Every JNI scope is wrapped in a local-reference frame: without this the +//! JVM local-ref table grows unbounded, which is itself a slow FPS killer. +//! +//! The chest scan is heavier (it walks loaded chunks) so it runs even rarer, +//! every [`CHEST_SCAN_INTERVAL`]. + +use crate::client::DarkClient; +use crate::mapping::client::minecraft::Minecraft; +use crate::mapping::{FieldType, Mapping, MinecraftClassType as Cls}; +use crate::module::ModuleSetting; +use egui::{ + pos2, vec2, Align2, Color32, Context, FontId, Id, LayerId, Order, Painter, Pos2, Rect, + Rounding, Stroke, +}; +use jni::objects::{GlobalRef, JObject, JValue}; +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; +use std::time::{Duration, Instant}; + +/// Minimum wall-time between two entity gathers (≈15 Hz). Interpolation keeps +/// the boxes smooth between gathers, so a low rate costs nothing visually. +const GATHER_INTERVAL: Duration = Duration::from_millis(66); +/// Wall-time between two chest scans — chests do not move, so this is rare. +const CHEST_SCAN_INTERVAL: Duration = Duration::from_secs(2); +/// Half-extent, in chunks, of the area scanned for containers. +const CHEST_CHUNK_RADIUS: i32 = 8; +/// Box edge thickness, in points. +const LINE_WIDTH: f32 = 1.6; + +// --- math ------------------------------------------------------------------ + +/// A plain 3D vector — kept separate from egui/JNI types so the projection +/// math stays allocation- and dependency-free. +#[derive(Debug, Clone, Copy)] +struct V3 { + x: f64, + y: f64, + z: f64, +} + +impl V3 { + fn sub(self, o: V3) -> V3 { + V3 { + x: self.x - o.x, + y: self.y - o.y, + z: self.z - o.z, + } + } + + fn dot(self, o: V3) -> f64 { + self.x * o.x + self.y * o.y + self.z * o.z + } + + fn cross(self, o: V3) -> V3 { + V3 { + x: self.y * o.z - self.z * o.y, + y: self.z * o.x - self.x * o.z, + z: self.x * o.y - self.y * o.x, + } + } + + fn lerp(self, o: V3, t: f64) -> V3 { + V3 { + x: self.x + (o.x - self.x) * t, + y: self.y + (o.y - self.y) * t, + z: self.z + (o.z - self.z) * t, + } + } + + fn length(self) -> f64 { + self.dot(self).sqrt() + } +} + +/// A camera, reduced to exactly what world→screen projection needs. +struct View { + cam: V3, + fwd: V3, + right: V3, + up: V3, + tan_x: f64, + tan_y: f64, + w: f32, + h: f32, +} + +impl View { + /// Projects a world point to logical screen coordinates, or `None` if it + /// lies behind the camera (where a perspective divide is meaningless). + fn project(&self, p: V3) -> Option { + let r = p.sub(self.cam); + let depth = r.dot(self.fwd); + if depth < 0.05 { + return None; + } + let ndc_x = (r.dot(self.right) / depth) / self.tan_x; + let ndc_y = (r.dot(self.up) / depth) / self.tan_y; + let sx = (ndc_x * 0.5 + 0.5) * self.w as f64; + let sy = (0.5 - ndc_y * 0.5) * self.h as f64; + Some(pos2(sx as f32, sy as f32)) + } +} + +/// Builds a [`View`] from Minecraft's camera state. +fn build_view(cam: V3, yaw_deg: f32, pitch_deg: f32, fov_deg: f64, w: f32, h: f32) -> View { + let yaw = (yaw_deg as f64).to_radians(); + let pitch = (pitch_deg as f64).to_radians(); + let (sy, cy) = yaw.sin_cos(); + let (sp, cp) = pitch.sin_cos(); + + // Minecraft yaw: 0° faces +Z, increasing clockwise. Pitch: positive looks + // down. The forward vector follows directly from those conventions. + let fwd = V3 { + x: -sy * cp, + y: -sp, + z: cy * cp, + }; + let right = V3 { + x: -cy, + y: 0.0, + z: -sy, + }; + let up = right.cross(fwd); + + let tan_y = (fov_deg.to_radians() * 0.5).tan(); + let tan_x = tan_y * (w / h) as f64; + + View { + cam, + fwd, + right, + up, + tan_x, + tan_y, + w, + h, + } +} + +// --- gathered snapshot ----------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TargetKind { + Player, + Mob, +} + +/// One living target, as captured by the last [`gather`]. +#[derive(Debug, Clone)] +struct EntityTarget { + id: i32, + kind: TargetKind, + /// Position at the previous gather — the start of the interpolation. + prev: V3, + /// Position at the latest gather — the end of the interpolation. + pos: V3, + width: f64, + height: f64, + name: String, + health: f32, + max_health: f32, +} + +/// One container block, by its lower-corner block coordinates. +#[derive(Debug, Clone, Copy)] +struct ChestTarget { + pos: V3, +} + +/// Cross-frame ESP state: the cached camera handles and the latest snapshot. +struct EspState { + camera: Option, + entities: Vec, + chests: Vec, + prev_gather: Option, + last_gather: Option, + last_chest_scan: Option, + /// Field of view currently used for projection — eased toward `target_fov` + /// each frame so flying / sprinting transitions do not snap the boxes. + fov: f64, + /// Field of view the game is heading to, refreshed each gather. + target_fov: f64, + /// Set once camera resolution has failed, so the failure is logged once. + camera_logged: bool, +} + +impl EspState { + fn new() -> Self { + EspState { + camera: None, + entities: Vec::new(), + chests: Vec::new(), + prev_gather: None, + last_gather: None, + last_chest_scan: None, + fov: 70.0, + target_fov: 70.0, + camera_logged: false, + } + } +} + +fn state() -> &'static Mutex { + static STATE: OnceLock> = OnceLock::new(); + STATE.get_or_init(|| Mutex::new(EspState::new())) +} + +// --- module configuration -------------------------------------------------- + +/// Per-category drawing options, snapshotted from the modules once per frame. +struct EntityCfg { + enabled: bool, + color: Color32, + show_name: bool, + show_distance: bool, + show_health: bool, + /// Maximum distance, in blocks, an entity is drawn (and processed) at. + range: f32, +} + +struct ChestCfg { + enabled: bool, + color: Color32, + show_distance: bool, +} + +struct EspConfig { + player: EntityCfg, + mob: EntityCfg, + chest: ChestCfg, +} + +impl EspConfig { + fn any_enabled(&self) -> bool { + self.player.enabled || self.mob.enabled || self.chest.enabled + } +} + +fn color_setting(data: &crate::module::ModuleData, name: &str, fallback: Color32) -> Color32 { + match data.get_setting(name) { + Some(ModuleSetting::Color { value, .. }) => Color32::from_rgba_unmultiplied( + (value[0] * 255.0) as u8, + (value[1] * 255.0) as u8, + (value[2] * 255.0) as u8, + (value[3] * 255.0).max(40.0) as u8, + ), + _ => fallback, + } +} + +fn toggle_setting(data: &crate::module::ModuleData, name: &str, fallback: bool) -> bool { + data.get_setting(name) + .and_then(ModuleSetting::get_toggle_value) + .unwrap_or(fallback) +} + +fn slider_setting(data: &crate::module::ModuleData, name: &str, fallback: f32) -> f32 { + data.get_setting(name) + .and_then(ModuleSetting::get_slider_value) + .unwrap_or(fallback) +} + +/// Reads the three ESP modules' state with a single lock pass. +fn read_config() -> EspConfig { + let disabled_entity = || EntityCfg { + enabled: false, + color: Color32::WHITE, + show_name: false, + show_distance: false, + show_health: false, + range: 0.0, + }; + let mut cfg = EspConfig { + player: disabled_entity(), + mob: disabled_entity(), + chest: ChestCfg { + enabled: false, + color: Color32::WHITE, + show_distance: false, + }, + }; + + let registry = match DarkClient::instance().modules.read() { + Ok(guard) => guard, + Err(_) => return cfg, + }; + + if let Some(arc) = registry.get("Player ESP") { + if let Ok(module) = arc.lock() { + let data = module.get_module_data(); + cfg.player = EntityCfg { + enabled: data.enabled, + color: color_setting(data, "Color", Color32::from_rgb(255, 70, 70)), + show_name: toggle_setting(data, "Name", true), + show_distance: toggle_setting(data, "Distance", true), + show_health: toggle_setting(data, "Health", true), + range: slider_setting(data, "Range", 64.0), + }; + } + } + if let Some(arc) = registry.get("Mob ESP") { + if let Ok(module) = arc.lock() { + let data = module.get_module_data(); + cfg.mob = EntityCfg { + enabled: data.enabled, + color: color_setting(data, "Color", Color32::from_rgb(255, 215, 50)), + show_name: toggle_setting(data, "Name", true), + show_distance: toggle_setting(data, "Distance", true), + show_health: toggle_setting(data, "Health", true), + range: slider_setting(data, "Range", 64.0), + }; + } + } + if let Some(arc) = registry.get("Chest ESP") { + if let Ok(module) = arc.lock() { + let data = module.get_module_data(); + cfg.chest = ChestCfg { + enabled: data.enabled, + color: color_setting(data, "Color", Color32::from_rgb(255, 140, 30)), + show_distance: toggle_setting(data, "Distance", true), + }; + } + } + + cfg +} + +// --- public entry point ---------------------------------------------------- + +/// Draws the whole ESP overlay for the current frame. +/// +/// Called once per frame from the overlay renderer; cheap when every ESP +/// module is disabled. +pub fn draw(ctx: &Context) { + let cfg = read_config(); + let mut state = state().lock().unwrap(); + + if !cfg.any_enabled() { + state.entities.clear(); + state.chests.clear(); + return; + } + + let now = Instant::now(); + if state.last_gather.map_or(true, |t| now - t >= GATHER_INTERVAL) { + gather(&mut state, &cfg, now); + } + + let view = match read_view(&mut state, ctx) { + Some(view) => view, + None => return, + }; + let t = interp_factor(&state, now); + + let painter = ctx.layer_painter(LayerId::new(Order::Background, Id::new("esp_overlay"))); + + for entity in &state.entities { + draw_entity(&painter, &view, entity, t, &cfg); + } + for chest in &state.chests { + draw_chest(&painter, &view, chest, &cfg); + } +} + +/// Fraction of the way from the previous gather to the latest one. +fn interp_factor(state: &EspState, now: Instant) -> f64 { + match (state.prev_gather, state.last_gather) { + (Some(prev), Some(last)) => { + let span = (last - prev).as_secs_f64(); + if span <= 1e-4 { + 1.0 + } else { + ((now - last).as_secs_f64() / span).clamp(0.0, 1.0) + } + } + _ => 1.0, + } +} + +// --- camera ---------------------------------------------------------------- + +/// Resolves the current camera into a [`View`], caching the JNI handles. +fn read_view(state: &mut EspState, ctx: &Context) -> Option { + let mapping = Minecraft::instance().get_mapping(); + + if state.camera.is_none() { + match init_camera(mapping) { + Ok(camera) => state.camera = Some(camera), + Err(e) => { + log::debug!("ESP: camera unavailable: {e}"); + return None; + } + } + } + + let cam = state.camera.clone()?; + let rect = ctx.screen_rect(); + if rect.width() < 1.0 || rect.height() < 1.0 { + return None; + } + + let mut env = mapping.get_env().ok()?; + // The camera state is read from `Camera`'s fields, not getter methods: + // method names churn between versions, the plain fields are far stabler. + let read = env.with_local_frame(32, |_| -> anyhow::Result<(V3, f32, f32)> { + let pos = mapping + .get_field( + Cls::Camera, + cam.as_obj(), + "position", + FieldType::Object(Cls::Vec3, mapping), + )? + .l()?; + let cam_pos = V3 { + x: mapping.get_field(Cls::Vec3, &pos, "x", FieldType::Double)?.d()?, + y: mapping.get_field(Cls::Vec3, &pos, "y", FieldType::Double)?.d()?, + z: mapping.get_field(Cls::Vec3, &pos, "z", FieldType::Double)?.d()?, + }; + let yaw = mapping + .get_field(Cls::Camera, cam.as_obj(), "yRot", FieldType::Float)? + .f()?; + let pitch = mapping + .get_field(Cls::Camera, cam.as_obj(), "xRot", FieldType::Float)? + .f()?; + Ok((cam_pos, yaw, pitch)) + }); + + let (cam_pos, yaw, pitch) = match read { + Ok(values) => values, + Err(error) => { + if !state.camera_logged { + state.camera_logged = true; + log::warn!("ESP: camera read failed: {error}"); + } + return None; + } + }; + + // Ease the FOV toward its target — flying / sprinting transitions ramp + // smoothly instead of snapping the boxes (Minecraft eases it too, so an + // instant jump here would show up as a stutter). + let dt = ctx.input(|input| input.stable_dt).clamp(0.0, 0.1) as f64; + let blend = 1.0 - 0.5_f64.powf(dt / 0.05); + state.fov += (state.target_fov - state.fov) * blend; + + Some(build_view( + cam_pos, + yaw, + pitch, + state.fov, + rect.width(), + rect.height(), + )) +} + +/// Fetches the (session-stable) `Camera` handle via the game renderer. +fn init_camera(mapping: &Mapping) -> anyhow::Result { + let mc = Minecraft::instance(); + let mut env = mapping.get_env()?; + env.with_local_frame(16, |_| -> anyhow::Result { + let renderer = mapping + .get_field( + Cls::Minecraft, + mc.jni_ref.as_obj(), + "gameRenderer", + FieldType::Object(Cls::GameRenderer, mapping), + )? + .l()?; + let camera = mapping + .call_method(Cls::GameRenderer, &renderer, "getMainCamera", &[])? + .l()?; + mapping.new_global_ref(camera) + }) +} + +/// Reads the vertical field of view, in degrees, Minecraft is rendering with: +/// the options value scaled by the flying / sprinting modifiers Minecraft +/// itself applies. Without them the box drifts off entities while either is +/// active (`GameRenderer.getFov` would give this directly, but its signature +/// is not stable across versions). +fn read_fov(mapping: &Mapping) -> f64 { + let base = match read_option_fov(mapping) { + Ok(fov) if fov.is_finite() && (1.0..=179.0).contains(&fov) => fov, + _ => 70.0, + }; + (base * fov_modifier(mapping)).clamp(1.0, 179.0) +} + +/// The FOV multiplier Minecraft applies on top of the options value: ×1.1 +/// while flying and ≈×1.15 while sprinting — the constants from +/// `Player.getFieldOfViewModifier`. +fn fov_modifier(mapping: &Mapping) -> f64 { + let player = match Minecraft::instance().get_player() { + Ok(player) => player, + Err(_) => return 1.0, + }; + + let mut modifier = 1.0; + + let flying = mapping + .get_field( + Cls::Abilities, + player.abilities.jni_ref.as_obj(), + "flying", + FieldType::Boolean, + ) + .ok() + .and_then(|value| value.z().ok()) + .unwrap_or(false); + if flying { + modifier *= 1.1; + } + + let sprinting = mapping + .call_method(Cls::Entity, player.entity.jni_ref.as_obj(), "isSprinting", &[]) + .ok() + .and_then(|value| value.z().ok()) + .unwrap_or(false); + if sprinting { + modifier *= 1.15; + } + + modifier +} + +/// Reads the raw FOV slider value from the game options. +fn read_option_fov(mapping: &Mapping) -> anyhow::Result { + let mc = Minecraft::instance(); + let mut env = mapping.get_env()?; + env.with_local_frame(16, |_| -> anyhow::Result { + let options = mapping + .get_field( + Cls::Minecraft, + mc.jni_ref.as_obj(), + "options", + FieldType::Object(Cls::Options, mapping), + )? + .l()?; + let option = mapping + .get_field( + Cls::Options, + &options, + "fov", + FieldType::Object(Cls::OptionInstance, mapping), + )? + .l()?; + let value = mapping + .call_method(Cls::OptionInstance, &option, "get", &[])? + .l()?; + let fov = mapping + .call_method(Cls::Integer, &value, "intValue", &[])? + .i()?; + Ok(fov as f64) + }) +} + +// --- gather ---------------------------------------------------------------- + +/// Refreshes the snapshot: entities every call, chests on their own schedule. +fn gather(state: &mut EspState, cfg: &EspConfig, now: Instant) { + state.prev_gather = state.last_gather; + state.last_gather = Some(now); + state.target_fov = read_fov(Minecraft::instance().get_mapping()); + + if cfg.player.enabled || cfg.mob.enabled { + let mut range = 0.0_f32; + if cfg.player.enabled { + range = range.max(cfg.player.range); + } + if cfg.mob.enabled { + range = range.max(cfg.mob.range); + } + let range_sq = (range as f64) * (range as f64); + + let previous = std::mem::take(&mut state.entities); + match gather_entities(&previous, range_sq, cfg.player.enabled, cfg.mob.enabled) { + Ok(list) => state.entities = list, + Err(e) => log::debug!("ESP: entity gather failed: {e}"), + } + } else { + state.entities.clear(); + } + + if cfg.chest.enabled { + let due = state + .last_chest_scan + .map_or(true, |t| now - t >= CHEST_SCAN_INTERVAL); + if due { + state.last_chest_scan = Some(now); + match gather_chests() { + Ok(list) => state.chests = list, + Err(e) => log::debug!("ESP: chest scan failed: {e}"), + } + } + } else { + state.chests.clear(); + state.last_chest_scan = None; + } +} + +/// Walks `Level.entitiesForRendering()` once, classifying players and mobs. +fn gather_entities( + previous: &[EntityTarget], + range_sq: f64, + want_player: bool, + want_mob: bool, +) -> anyhow::Result> { + let mc = Minecraft::instance(); + let mapping = mc.get_mapping(); + + // Carry positions forward so the new snapshot can interpolate from them. + let prev_pos: HashMap = previous.iter().map(|e| (e.id, e.pos)).collect(); + + let mut env = mapping.get_env()?; + let mut out: Vec = Vec::new(); + + env.with_local_frame(32, |env| -> anyhow::Result<()> { + let (local_id, player_pos) = { + let player = mc.get_player()?; + let id = mapping + .call_method(Cls::Entity, player.entity.jni_ref.as_obj(), "getId", &[])? + .i()?; + let pos = player.entity.get_position()?; + (id, V3 { x: pos.0, y: pos.1, z: pos.2 }) + }; + + let level = mapping + .get_field( + Cls::Minecraft, + mc.jni_ref.as_obj(), + "level", + FieldType::Object(Cls::Level, mapping), + )? + .l()?; + if level.is_null() { + return Ok(()); + } + + let iterable = mapping + .call_method(Cls::Level, &level, "entitiesForRendering", &[])? + .l()?; + let iterator = mapping + .call_method(Cls::Iterable, &iterable, "iterator", &[])? + .l()?; + + loop { + if !mapping + .call_method(Cls::Iterator, &iterator, "hasNext", &[])? + .z()? + { + break; + } + // One frame per entity bounds the local-ref table no matter how + // many entities the world contains. + let target = env.with_local_frame(64, |_| -> anyhow::Result> { + let entity = mapping + .call_method(Cls::Iterator, &iterator, "next", &[])? + .l()?; + Ok(process_entity( + mapping, &entity, local_id, player_pos, range_sq, want_player, + want_mob, &prev_pos, + )) + })?; + if let Some(target) = target { + out.push(target); + } + } + Ok(()) + })?; + + Ok(out) +} + +/// Turns one entity object into an [`EntityTarget`], or `None` if it is not a +/// wanted target. Errors are swallowed per-field so one bad entity cannot +/// abort the whole gather. +#[allow(clippy::too_many_arguments)] +fn process_entity( + mapping: &Mapping, + entity: &JObject, + local_id: i32, + player_pos: V3, + range_sq: f64, + want_player: bool, + want_mob: bool, + prev_pos: &HashMap, +) -> Option { + // Cheap distance gate first — a far entity then costs just this one JNI + // call. Skipped entirely if `distanceToSqr` is not exposed by this build. + if let Some(dist_sq) = mapping + .call_method( + Cls::Entity, + entity, + "distanceToSqr", + &[ + JValue::Double(player_pos.x), + JValue::Double(player_pos.y), + JValue::Double(player_pos.z), + ], + ) + .ok() + .and_then(|value| value.d().ok()) + { + if dist_sq > range_sq { + return None; + } + } + + let kind = if want_player && mapping.is_instance_of(Cls::Player, entity).unwrap_or(false) { + TargetKind::Player + } else if want_mob && mapping.is_instance_of(Cls::Mob, entity).unwrap_or(false) { + TargetKind::Mob + } else { + return None; + }; + + let id = mapping + .call_method(Cls::Entity, entity, "getId", &[]) + .ok()? + .i() + .ok()?; + if id == local_id { + return None; + } + + let pos = read_vec3(mapping, entity, "position")?; + let width = mapping + .call_method(Cls::Entity, entity, "getBbWidth", &[]) + .ok()? + .f() + .ok()? as f64; + let height = mapping + .call_method(Cls::Entity, entity, "getBbHeight", &[]) + .ok()? + .f() + .ok()? as f64; + + let name = read_name(mapping, entity).unwrap_or_default(); + let (health, max_health) = read_health(mapping, entity).unwrap_or((0.0, 0.0)); + + Some(EntityTarget { + id, + kind, + prev: prev_pos.get(&id).copied().unwrap_or(pos), + pos, + width, + height, + name, + health, + max_health, + }) +} + +/// Calls a no-arg `Vec3`-returning method and reads its `x`/`y`/`z`. +fn read_vec3(mapping: &Mapping, obj: &JObject, method: &str) -> Option { + let vec3 = mapping + .call_method(Cls::Entity, obj, method, &[]) + .ok()? + .l() + .ok()?; + Some(V3 { + x: mapping.get_field(Cls::Vec3, &vec3, "x", FieldType::Double).ok()?.d().ok()?, + y: mapping.get_field(Cls::Vec3, &vec3, "y", FieldType::Double).ok()?.d().ok()?, + z: mapping.get_field(Cls::Vec3, &vec3, "z", FieldType::Double).ok()?.d().ok()?, + }) +} + +/// Reads an entity's display name via `getName().getString()`. +fn read_name(mapping: &Mapping, entity: &JObject) -> anyhow::Result { + let component = mapping + .call_method(Cls::Entity, entity, "getName", &[])? + .l()?; + if component.is_null() { + return Ok(String::new()); + } + let string = mapping + .call_method(Cls::Component, &component, "getString", &[])? + .l()?; + let mut name = mapping.get_string(string)?; + if name.chars().count() > 24 { + name = name.chars().take(24).collect(); + } + Ok(name) +} + +/// Reads `(health, maxHealth)` for a living entity. +fn read_health(mapping: &Mapping, entity: &JObject) -> anyhow::Result<(f32, f32)> { + let health = mapping + .call_method(Cls::LivingEntity, entity, "getHealth", &[])? + .f()?; + let max_health = mapping + .call_method(Cls::LivingEntity, entity, "getMaxHealth", &[])? + .f()?; + Ok((health, max_health)) +} + +/// Scans loaded chunks around the player for container block entities. +fn gather_chests() -> anyhow::Result> { + let mc = Minecraft::instance(); + let mapping = mc.get_mapping(); + + let mut env = mapping.get_env()?; + let mut out: Vec = Vec::new(); + + env.with_local_frame(32, |env| -> anyhow::Result<()> { + let level = mapping + .get_field( + Cls::Minecraft, + mc.jni_ref.as_obj(), + "level", + FieldType::Object(Cls::Level, mapping), + )? + .l()?; + if level.is_null() { + return Ok(()); + } + + let player_pos = mc.get_player()?.entity.get_position()?; + let pcx = (player_pos.0 / 16.0).floor() as i32; + let pcz = (player_pos.2 / 16.0).floor() as i32; + + for cx in (pcx - CHEST_CHUNK_RADIUS)..=(pcx + CHEST_CHUNK_RADIUS) { + for cz in (pcz - CHEST_CHUNK_RADIUS)..=(pcz + CHEST_CHUNK_RADIUS) { + // One frame per chunk keeps the block-entity locals bounded. + env.with_local_frame(128, |_| -> anyhow::Result<()> { + scan_chunk(mapping, &level, cx, cz, &mut out) + })?; + } + } + Ok(()) + })?; + + Ok(out) +} + +/// Adds every container block entity of one chunk to `out`. +fn scan_chunk( + mapping: &Mapping, + level: &JObject, + cx: i32, + cz: i32, + out: &mut Vec, +) -> anyhow::Result<()> { + let chunk = mapping + .call_method( + Cls::LevelReader, + level, + "getChunk", + &[JValue::Int(cx), JValue::Int(cz)], + )? + .l()?; + if chunk.is_null() { + return Ok(()); + } + + let map = mapping + .call_method(Cls::LevelChunk, &chunk, "getBlockEntities", &[])? + .l()?; + if map.is_null() { + return Ok(()); + } + let values = mapping.call_method(Cls::Map, &map, "values", &[])?.l()?; + let iterator = mapping + .call_method(Cls::Iterable, &values, "iterator", &[])? + .l()?; + + loop { + if !mapping + .call_method(Cls::Iterator, &iterator, "hasNext", &[])? + .z()? + { + break; + } + let block_entity = mapping + .call_method(Cls::Iterator, &iterator, "next", &[])? + .l()?; + if is_container(mapping, &block_entity) { + if let Some(pos) = block_entity_pos(mapping, &block_entity) { + out.push(ChestTarget { pos }); + } + } + } + Ok(()) +} + +/// True for chest / trapped chest / ender chest / barrel / shulker box. +fn is_container(mapping: &Mapping, block_entity: &JObject) -> bool { + // `ChestBlockEntity` already covers trapped chests (a subclass). + const KINDS: [Cls; 4] = [ + Cls::ChestBlockEntity, + Cls::EnderChestBlockEntity, + Cls::BarrelBlockEntity, + Cls::ShulkerBoxBlockEntity, + ]; + KINDS + .iter() + .any(|&kind| mapping.is_instance_of(kind, block_entity).unwrap_or(false)) +} + +/// Reads a block entity's `BlockPos` as a [`V3`]. +fn block_entity_pos(mapping: &Mapping, block_entity: &JObject) -> Option { + let block_pos = mapping + .call_method(Cls::BlockEntity, block_entity, "getBlockPos", &[]) + .ok()? + .l() + .ok()?; + let axis = |name: &str| -> Option { + Some(mapping.call_method(Cls::Vec3i, &block_pos, name, &[]).ok()?.i().ok()? as f64) + }; + Some(V3 { + x: axis("getX")?, + y: axis("getY")?, + z: axis("getZ")?, + }) +} + +// --- drawing --------------------------------------------------------------- + +/// The 12 edges of a box, as index pairs into an 8-corner array. +const EDGES: [(usize, usize); 12] = [ + (0, 1), (1, 2), (2, 3), (3, 0), // bottom + (4, 5), (5, 6), (6, 7), (7, 4), // top + (0, 4), (1, 5), (2, 6), (3, 7), // verticals +]; + +/// The 8 corners of an axis-aligned box `[min, max]`. +fn box_corners(min: V3, max: V3) -> [V3; 8] { + [ + V3 { x: min.x, y: min.y, z: min.z }, + V3 { x: max.x, y: min.y, z: min.z }, + V3 { x: max.x, y: min.y, z: max.z }, + V3 { x: min.x, y: min.y, z: max.z }, + V3 { x: min.x, y: max.y, z: min.z }, + V3 { x: max.x, y: max.y, z: min.z }, + V3 { x: max.x, y: max.y, z: max.z }, + V3 { x: min.x, y: max.y, z: max.z }, + ] +} + +/// Draws a wireframe box and returns its 2D screen bounds (for label +/// placement), or `None` if no corner is in front of the camera. +fn draw_wire_box(painter: &Painter, view: &View, corners: &[V3; 8], color: Color32) -> Option { + let projected: [Option; 8] = std::array::from_fn(|i| view.project(corners[i])); + let stroke = Stroke::new(LINE_WIDTH, color); + + for &(a, b) in &EDGES { + if let (Some(pa), Some(pb)) = (projected[a], projected[b]) { + painter.line_segment([pa, pb], stroke); + } + } + + let mut bounds: Option = None; + for point in projected.into_iter().flatten() { + bounds = Some(match bounds { + Some(rect) => rect.union(Rect::from_min_max(point, point)), + None => Rect::from_min_max(point, point), + }); + } + bounds +} + +fn draw_entity(painter: &Painter, view: &View, entity: &EntityTarget, t: f64, cfg: &EspConfig) { + let icfg = match entity.kind { + TargetKind::Player => &cfg.player, + TargetKind::Mob => &cfg.mob, + }; + + let feet = entity.prev.lerp(entity.pos, t); + let half = entity.width * 0.5; + let corners = box_corners( + V3 { x: feet.x - half, y: feet.y, z: feet.z - half }, + V3 { x: feet.x + half, y: feet.y + entity.height, z: feet.z + half }, + ); + + let rect = match draw_wire_box(painter, view, &corners, icfg.color) { + Some(rect) => rect, + None => return, + }; + + if icfg.show_health && entity.max_health > 0.0 { + draw_health_bar(painter, rect, entity.health / entity.max_health); + } + if icfg.show_name && !entity.name.is_empty() { + draw_label( + painter, + pos2(rect.center().x, rect.top() - 7.0), + Align2::CENTER_BOTTOM, + &entity.name, + icfg.color, + ); + } + if icfg.show_distance { + let distance = feet.sub(view.cam).length(); + draw_label( + painter, + pos2(rect.center().x, rect.bottom() + 3.0), + Align2::CENTER_TOP, + &format!("{distance:.0}m"), + Color32::from_rgb(214, 216, 224), + ); + } +} + +fn draw_chest(painter: &Painter, view: &View, chest: &ChestTarget, cfg: &EspConfig) { + let corners = box_corners( + chest.pos, + V3 { x: chest.pos.x + 1.0, y: chest.pos.y + 1.0, z: chest.pos.z + 1.0 }, + ); + let rect = match draw_wire_box(painter, view, &corners, cfg.chest.color) { + Some(rect) => rect, + None => return, + }; + + if cfg.chest.show_distance { + let center = V3 { + x: chest.pos.x + 0.5, + y: chest.pos.y + 0.5, + z: chest.pos.z + 0.5, + }; + let distance = center.sub(view.cam).length(); + draw_label( + painter, + pos2(rect.center().x, rect.bottom() + 3.0), + Align2::CENTER_TOP, + &format!("{distance:.0}m"), + cfg.chest.color, + ); + } +} + +/// A thin health bar straddling the top edge of `rect`, red→yellow→green. +fn draw_health_bar(painter: &Painter, rect: Rect, fraction: f32) { + let fraction = fraction.clamp(0.0, 1.0); + let top = rect.top() - 4.0; + let bottom = rect.top() - 1.5; + let background = Rect::from_min_max(pos2(rect.left(), top), pos2(rect.right(), bottom)); + painter.rect_filled(background, Rounding::ZERO, Color32::from_black_alpha(190)); + + let fill_width = rect.width() * fraction; + let fill = Rect::from_min_max( + pos2(rect.left(), top), + pos2(rect.left() + fill_width, bottom), + ); + painter.rect_filled(fill, Rounding::ZERO, health_color(fraction)); +} + +/// Interpolates red → yellow → green across `0..=1`. +fn health_color(fraction: f32) -> Color32 { + let (r, g) = if fraction < 0.5 { + (255.0, 255.0 * (fraction * 2.0)) + } else { + (255.0 * (1.0 - (fraction - 0.5) * 2.0), 255.0) + }; + Color32::from_rgb(r as u8, g as u8, 60) +} + +/// Draws text with a 1px drop shadow so it stays readable over any backdrop. +fn draw_label(painter: &Painter, pos: Pos2, anchor: Align2, text: &str, color: Color32) { + let font = FontId::proportional(11.0); + painter.text( + pos + vec2(0.8, 0.8), + anchor, + text, + font.clone(), + Color32::from_black_alpha(210), + ); + painter.text(pos, anchor, text, font, color); +} diff --git a/client/src/graphic/gui.rs b/client/src/graphic/gui.rs index 9f024d0..f68f54b 100644 --- a/client/src/graphic/gui.rs +++ b/client/src/graphic/gui.rs @@ -7,12 +7,15 @@ use crate::graphic::anim::{self, Easing}; use crate::graphic::input::GUI_OPEN; -use crate::graphic::{hud, menu, notification}; +use crate::graphic::{esp, hud, menu, notification}; use egui::{Context, Id}; use std::sync::atomic::Ordering; /// Renders the whole overlay for a single frame. pub fn render_all(ctx: &Context) { + // World-space ESP sits underneath every piece of 2D UI. + esp::draw(ctx); + hud::draw(ctx); // A single tween turns the open/close toggle into the 0..1 factor the diff --git a/client/src/graphic/mod.rs b/client/src/graphic/mod.rs index ed7cae2..b4cf845 100644 --- a/client/src/graphic/mod.rs +++ b/client/src/graphic/mod.rs @@ -1,4 +1,5 @@ pub mod anim; +pub mod esp; pub mod gui; pub mod hook; pub mod hud; diff --git a/client/src/lib.rs b/client/src/lib.rs index 9233b01..9ff1ec9 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -18,6 +18,9 @@ use log::{error, info, LevelFilter}; use module::combat::aimbot::AimbotModule; use module::combat::killaura::KillAuraModule; use module::movement::fly::FlyModule; +use module::render::chest_esp::ChestEspModule; +use module::render::mob_esp::MobEspModule; +use module::render::player_esp::PlayerEspModule; use simplelog::{Config, WriteLogger}; use std::fs::File; use std::sync::atomic::{AtomicBool, Ordering}; @@ -99,4 +102,7 @@ fn register_modules() { client.register_module(KillAuraModule::new()); client.register_module(MobAuraModule::new()); client.register_module(AimbotModule::new()); + client.register_module(PlayerEspModule::new()); + client.register_module(MobEspModule::new()); + client.register_module(ChestEspModule::new()); } diff --git a/client/src/mapping/class.rs b/client/src/mapping/class.rs index 87095da..ceb031a 100644 --- a/client/src/mapping/class.rs +++ b/client/src/mapping/class.rs @@ -6,6 +6,7 @@ use serde::de::{MapAccess, Visitor}; use serde::{Deserialize, Deserializer}; use std::collections::HashMap; use std::fmt; +use std::sync::OnceLock; /// Custom deserializer that handles both single Method and Vec formats fn deserialize_methods<'de, D>(deserializer: D) -> Result>, D::Error> @@ -68,8 +69,21 @@ pub struct MinecraftClass { pub struct Method { pub name: String, pub signature: String, + /// JNI method id, resolved on first call and cached here. The id is stable + /// for the life of the JVM, so this turns every later call into a direct + /// invocation with no name/signature lookup. + #[serde(skip)] + pub id: OnceLock, } +/// A cached JNI `jmethodID`. The id is a stable handle, valid from any thread +/// for as long as its class stays loaded, so sharing it is sound. +#[derive(Debug, Clone, Copy)] +pub struct MethodHandle(pub jni::sys::jmethodID); + +unsafe impl Send for MethodHandle {} +unsafe impl Sync for MethodHandle {} + /// Represents a field with its obfuscated name #[derive(Debug, Deserialize)] pub struct Field { @@ -512,6 +526,11 @@ impl MinecraftClass { } } + /// Names of every mapped method — for diagnostics only. + pub fn method_names(&self) -> Vec { + self.methods.keys().cloned().collect() + } + pub fn get_field(&self, name: &str) -> anyhow::Result<&Field> { match self.fields.get(name) { Some(fields) => Ok(fields), diff --git a/client/src/mapping/class_type.rs b/client/src/mapping/class_type.rs index 01f84ab..bf8e113 100644 --- a/client/src/mapping/class_type.rs +++ b/client/src/mapping/class_type.rs @@ -16,6 +16,23 @@ pub enum MinecraftClassType { Iterator, Mob, Screen, + GameRenderer, + Camera, + LivingEntity, + Component, + LevelReader, + LevelChunk, + BlockEntity, + ChestBlockEntity, + EnderChestBlockEntity, + BarrelBlockEntity, + ShulkerBoxBlockEntity, + BlockPos, + Vec3i, + Map, + Options, + OptionInstance, + Integer, } impl MinecraftClassType { @@ -36,6 +53,33 @@ impl MinecraftClassType { MinecraftClassType::Iterator => "java/util/Iterator", MinecraftClassType::Mob => "net/minecraft/world/entity/Mob", MinecraftClassType::Screen => "net/minecraft/client/gui/screens/Screen", + MinecraftClassType::GameRenderer => "net/minecraft/client/renderer/GameRenderer", + MinecraftClassType::Camera => "net/minecraft/client/Camera", + MinecraftClassType::LivingEntity => "net/minecraft/world/entity/LivingEntity", + MinecraftClassType::Component => "net/minecraft/network/chat/Component", + MinecraftClassType::LevelReader => "net/minecraft/world/level/LevelReader", + MinecraftClassType::LevelChunk => "net/minecraft/world/level/chunk/LevelChunk", + MinecraftClassType::BlockEntity => { + "net/minecraft/world/level/block/entity/BlockEntity" + } + MinecraftClassType::ChestBlockEntity => { + "net/minecraft/world/level/block/entity/ChestBlockEntity" + } + MinecraftClassType::EnderChestBlockEntity => { + "net/minecraft/world/level/block/entity/EnderChestBlockEntity" + } + MinecraftClassType::BarrelBlockEntity => { + "net/minecraft/world/level/block/entity/BarrelBlockEntity" + } + MinecraftClassType::ShulkerBoxBlockEntity => { + "net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity" + } + MinecraftClassType::BlockPos => "net/minecraft/core/BlockPos", + MinecraftClassType::Vec3i => "net/minecraft/core/Vec3i", + MinecraftClassType::Map => "java/util/Map", + MinecraftClassType::Options => "net/minecraft/client/Options", + MinecraftClassType::OptionInstance => "net/minecraft/client/OptionInstance", + MinecraftClassType::Integer => "java/lang/Integer", } } } diff --git a/client/src/mapping/mod.rs b/client/src/mapping/mod.rs index 4dce7c1..77a0020 100644 --- a/client/src/mapping/mod.rs +++ b/client/src/mapping/mod.rs @@ -1,9 +1,11 @@ use crate::client::DarkClient; -use crate::mapping::class::MinecraftClass; +use crate::mapping::class::{Method, MethodHandle, MinecraftClass}; pub use crate::mapping::class_type::MinecraftClassType; use crate::mapping::client::minecraft::Minecraft; use crate::mapping::minecraft_version::MinecraftVersion; -use jni::objects::{GlobalRef, JObject, JString, JValue, JValueOwned}; +use jni::objects::{GlobalRef, JClass, JMethodID, JObject, JString, JValue, JValueOwned}; +use jni::signature::{Primitive, ReturnType}; +use jni::sys::jvalue; use jni::JNIEnv; use log::{error, info}; use serde::Deserialize; @@ -55,6 +57,14 @@ pub struct Mapping { /// In obfuscated mode every class is present up-front; in reflected mode /// classes are discovered and cached on first use. classes: RwLock>>, + /// The class loader that loaded Minecraft, captured the first time a class + /// resolves. `JNIEnv::find_class` is classloader-sensitive and only works + /// from threads with a Minecraft Java frame on the stack; routing every + /// later lookup through this loader makes resolution thread-independent. + class_loader: RwLock>, + /// Cache of resolved JVM classes — and known-missing ones (`None`) — keyed + /// by JNI name, so a class is searched for at most once. + class_handles: RwLock>>, } #[allow(dead_code)] @@ -117,6 +127,8 @@ impl Mapping { mode: Mode::Reflected, version: MinecraftVersion::LATEST, classes: RwLock::new(HashMap::new()), + class_loader: RwLock::new(None), + class_handles: RwLock::new(HashMap::new()), }); } @@ -139,6 +151,8 @@ impl Mapping { mode: Mode::Obfuscated, version: file.version, classes: RwLock::new(classes), + class_loader: RwLock::new(None), + class_handles: RwLock::new(HashMap::new()), }) } @@ -164,7 +178,7 @@ impl Mapping { match self.mode { Mode::Obfuscated => Err(anyhow::anyhow!("{} java class not found", name)), Mode::Reflected => { - let class = Arc::new(reflect::reflect_class(name)?); + let class = Arc::new(reflect::reflect_class(self, name)?); self.classes .write() .unwrap() @@ -174,6 +188,103 @@ impl Mapping { } } + /// Resolves a JVM class by its JNI name, working from any thread. + /// + /// `JNIEnv::find_class` resolves against the class loader of the calling + /// thread; on a thread with no Minecraft Java frame on its stack (the + /// render thread, for instance) it cannot see `net.minecraft.*` classes. + /// Once the Minecraft class loader has been captured every lookup goes + /// through `ClassLoader.loadClass` on it instead. + /// + /// Results — hits *and* misses — are cached: a repeated lookup is one map + /// read, and a missing class is never searched twice (a failed `loadClass` + /// walks the whole classpath, ruinously slow to repeat per entity). + pub(crate) fn resolve_class<'a>( + &self, + env: &mut JNIEnv<'a>, + jni_name: &str, + ) -> anyhow::Result> { + if let Some(cached) = self.class_handles.read().unwrap().get(jni_name).cloned() { + return match cached { + Some(handle) => Ok(JClass::from(env.new_local_ref(handle.as_obj())?)), + None => Err(anyhow::anyhow!("Class {} not present at runtime", jni_name)), + }; + } + + let resolved = self.lookup_class(env, jni_name); + let handle = match &resolved { + Ok(jclass) => env.new_global_ref(jclass).ok(), + Err(_) => None, + }; + if handle.is_none() { + log::warn!( + "Mapping: class '{}' could not be resolved at runtime", + jni_name + ); + } + self.class_handles + .write() + .unwrap() + .insert(jni_name.to_owned(), handle); + resolved + } + + /// Looks a class up from scratch: through the captured Minecraft class + /// loader if available, otherwise through `find_class`. + fn lookup_class<'a>( + &self, + env: &mut JNIEnv<'a>, + jni_name: &str, + ) -> anyhow::Result> { + if let Some(loader) = self.class_loader.read().unwrap().clone() { + let binary_name = jni_name.replace('/', "."); + let name: JObject = env.new_string(binary_name)?.into(); + return match env.call_method( + loader.as_obj(), + "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;", + &[JValue::Object(&name)], + ) { + Ok(value) => Ok(JClass::from(value.l()?)), + Err(_) => { + let _ = env.exception_clear(); + Err(anyhow::anyhow!("Class {} not found at runtime", jni_name)) + } + }; + } + + // No loader captured yet: fall back to `find_class` and remember the + // loader that resolved it for every later lookup. + match env.find_class(jni_name) { + Ok(jclass) => { + self.capture_class_loader(env, &jclass); + Ok(jclass) + } + Err(_) => { + let _ = env.exception_clear(); + Err(anyhow::anyhow!("Class {} not found at runtime", jni_name)) + } + } + } + + /// Records the class loader of `jclass` as the Minecraft class loader. + fn capture_class_loader(&self, env: &mut JNIEnv, jclass: &JClass) { + if self.class_loader.read().unwrap().is_some() { + return; + } + let loader = env.call_method(jclass, "getClassLoader", "()Ljava/lang/ClassLoader;", &[]); + match loader.and_then(|value| value.l()) { + Ok(obj) if !obj.is_null() => { + if let Ok(global) = env.new_global_ref(obj) { + *self.class_loader.write().unwrap() = Some(global); + } + } + _ => { + let _ = env.exception_clear(); + } + } + } + /// Runtime (JVM) name of a class. fn runtime_class_name(&self, class_type: MinecraftClassType) -> anyhow::Result { match self.mode { @@ -277,17 +388,7 @@ impl Mapping { let mut env = self.get_env()?; let class = self.get_class(class_type.get_name())?; - let jclass = match env.find_class(&class.name) { - Ok(jclass) => jclass, - Err(_) => { - let _ = env.exception_clear(); - return Err(anyhow::anyhow!( - "Class {} ({}) not found", - class_type.get_name(), - class.name - )); - } - }; + let jclass = self.resolve_class(&mut env, &class.name)?; let method = class.get_method_by_args(method_name, args)?; match env.call_static_method(jclass, &method.name, &method.signature, args) { Ok(value) => Ok(value), @@ -318,7 +419,24 @@ impl Mapping { let class = self.get_class(class_type.get_name())?; let method = class.get_method_by_args(method_name, args)?; - match env.call_method(instance, &method.name, &method.signature, args) { + + // `call_method_unchecked` does not validate arity — so guard it here, + // since `get_method_by_args` skips that check for single-overload + // methods. + if signature_arg_count(&method.signature) != args.len() { + return Err(anyhow::anyhow!( + "Argument count mismatch calling {} ({}) — signature {}", + method_name, + method.name, + method.signature + )); + } + + let method_id = self.resolve_method_id(&mut env, class_type, method)?; + let return_type = parse_return_type(&method.signature); + let jni_args: Vec = args.iter().map(|arg| arg.as_jni()).collect(); + + match unsafe { env.call_method_unchecked(instance, method_id, return_type, &jni_args) } { Ok(value) => Ok(value), Err(_) => { let _ = env.exception_clear(); @@ -336,26 +454,46 @@ impl Mapping { } } - pub fn get_static_field( - &'_ self, + /// Resolves the JNI method id for `method`, caching it on the `Method` so + /// the costly name+signature lookup happens only once per method. + fn resolve_method_id( + &self, + env: &mut JNIEnv, class_type: MinecraftClassType, - field_name: &str, - field_type: FieldType, - ) -> anyhow::Result> { - let mut env = self.get_env()?; + method: &Method, + ) -> anyhow::Result { + if let Some(handle) = method.id.get() { + return Ok(unsafe { JMethodID::from_raw(handle.0) }); + } let class_name = self.runtime_class_name(class_type)?; - let jclass = match env.find_class(&class_name) { - Ok(jclass) => jclass, + let jclass = self.resolve_class(env, &class_name)?; + let id = match env.get_method_id(&jclass, &method.name, &method.signature) { + Ok(id) => id, Err(_) => { let _ = env.exception_clear(); return Err(anyhow::anyhow!( - "Class {} ({}) not found", - class_type.get_name(), + "Method {} {} not found on class {}", + method.name, + method.signature, class_name )); } }; + let _ = method.id.set(MethodHandle(id.into_raw())); + Ok(id) + } + + pub fn get_static_field( + &'_ self, + class_type: MinecraftClassType, + field_name: &str, + field_type: FieldType, + ) -> anyhow::Result> { + let mut env = self.get_env()?; + + let class_name = self.runtime_class_name(class_type)?; + let jclass = self.resolve_class(&mut env, &class_name)?; let runtime_field = self.runtime_field_name(class_type, field_name)?; match env.get_static_field(jclass, &runtime_field, field_type.get_signature()?) { Ok(value) => Ok(value), @@ -444,19 +582,8 @@ impl Mapping { ) -> anyhow::Result { let mut env = self.get_env()?; let class_name = self.runtime_class_name(class_type)?; - let jclass = match env.find_class(&class_name) { - Ok(jclass) => jclass, - Err(_) => { - let _ = env.exception_clear(); - return Err(anyhow::anyhow!( - "Class {} ({}) not found", - class_type.get_name(), - class_name - )); - } - }; - - Ok(env.is_instance_of(instance, jclass)?) + let jclass = self.resolve_class(&mut env, &class_name)?; + Ok(env.is_instance_of(instance, &jclass)?) } } @@ -468,3 +595,52 @@ impl Default for Mapping { }) } } + +/// Number of parameters in a JNI method signature, e.g. `(ILjava/lang/String;)V` +/// has 2. Used to guard the unchecked call path against arity mismatches. +fn signature_arg_count(signature: &str) -> usize { + let params = match (signature.find('('), signature.find(')')) { + (Some(open), Some(close)) if open < close => &signature[open + 1..close], + // Unparseable: return a count that can never match a real call. + _ => return usize::MAX, + }; + + let mut count = 0; + let mut chars = params.chars(); + while let Some(ch) = chars.next() { + match ch { + // Array prefix — the descriptor it belongs to is counted next. + '[' => continue, + // Object descriptor runs until its terminating ';'. + 'L' => { + for inner in chars.by_ref() { + if inner == ';' { + break; + } + } + count += 1; + } + // Any primitive. + _ => count += 1, + } + } + count +} + +/// Maps the return descriptor of a JNI signature to a [`ReturnType`]. +fn parse_return_type(signature: &str) -> ReturnType { + let return_descriptor = signature.rsplit(')').next().unwrap_or("V"); + match return_descriptor.chars().next() { + Some('Z') => ReturnType::Primitive(Primitive::Boolean), + Some('B') => ReturnType::Primitive(Primitive::Byte), + Some('C') => ReturnType::Primitive(Primitive::Char), + Some('S') => ReturnType::Primitive(Primitive::Short), + Some('I') => ReturnType::Primitive(Primitive::Int), + Some('J') => ReturnType::Primitive(Primitive::Long), + Some('F') => ReturnType::Primitive(Primitive::Float), + Some('D') => ReturnType::Primitive(Primitive::Double), + Some('[') => ReturnType::Array, + Some('L') => ReturnType::Object, + _ => ReturnType::Primitive(Primitive::Void), + } +} diff --git a/client/src/mapping/reflect.rs b/client/src/mapping/reflect.rs index d14fc6e..4daa30d 100644 --- a/client/src/mapping/reflect.rs +++ b/client/src/mapping/reflect.rs @@ -8,22 +8,17 @@ use crate::client::DarkClient; use crate::mapping::class::{Method, MinecraftClass}; +use crate::mapping::Mapping; use jni::objects::{JObject, JObjectArray, JString}; use jni::JNIEnv; use std::collections::HashMap; /// Reflects every method declared on — or inherited as public by — /// `class_name`, returning it as a [`MinecraftClass`]. -pub fn reflect_class(class_name: &str) -> anyhow::Result { +pub fn reflect_class(mapping: &Mapping, class_name: &str) -> anyhow::Result { let mut env = DarkClient::instance().get_env()?; - let jclass: JObject = env - .find_class(class_name) - .map_err(|_| { - let _ = env.exception_clear(); - anyhow::anyhow!("Class {} not found at runtime", class_name) - })? - .into(); + let jclass: JObject = mapping.resolve_class(&mut env, class_name)?.into(); let mut methods: HashMap> = HashMap::new(); @@ -60,7 +55,11 @@ fn collect_methods( let overloads = out.entry(name.clone()).or_default(); if !overloads.iter().any(|m| m.signature == signature) { - overloads.push(Method { name, signature }); + overloads.push(Method { + name, + signature, + id: std::sync::OnceLock::new(), + }); } } Ok(()) diff --git a/client/src/module/mod.rs b/client/src/module/mod.rs index 6633794..b18518a 100644 --- a/client/src/module/mod.rs +++ b/client/src/module/mod.rs @@ -2,6 +2,7 @@ use std::fmt::Debug; pub mod combat; pub mod movement; +pub mod render; pub type ModuleType = Box; diff --git a/client/src/module/render/chest_esp.rs b/client/src/module/render/chest_esp.rs new file mode 100644 index 0000000..5f934fb --- /dev/null +++ b/client/src/module/render/chest_esp.rs @@ -0,0 +1,57 @@ +use crate::module::{KeyboardKey, Module, ModuleCategory, ModuleData, ModuleSetting}; + +/// Highlights containers — chests, trapped chests, ender chests, barrels and +/// shulker boxes — with a 3D wireframe box. +/// +/// All rendering lives in [`crate::graphic::esp`]; this struct only carries the +/// toggle state and the visual settings. +#[derive(Debug)] +pub struct ChestEspModule { + pub module: ModuleData, +} + +impl ChestEspModule { + pub fn new() -> Self { + Self { + module: ModuleData { + name: "Chest ESP".to_string(), + description: "Draws a 3D box around containers".to_string(), + category: ModuleCategory::RENDER, + key_bind: KeyboardKey::KeyNone, + enabled: false, + settings: vec![ + ModuleSetting::Color { + name: "Color".to_string(), + value: [1.0, 0.55, 0.12, 1.0], + }, + ModuleSetting::Toggle { + name: "Distance".to_string(), + value: true, + }, + ], + }, + } + } +} + +impl Module for ChestEspModule { + fn on_start(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn on_stop(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn on_tick(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn get_module_data(&self) -> &ModuleData { + &self.module + } + + fn get_module_data_mut(&mut self) -> &mut ModuleData { + &mut self.module + } +} diff --git a/client/src/module/render/mob_esp.rs b/client/src/module/render/mob_esp.rs new file mode 100644 index 0000000..6a2f416 --- /dev/null +++ b/client/src/module/render/mob_esp.rs @@ -0,0 +1,70 @@ +use crate::module::{KeyboardKey, Module, ModuleCategory, ModuleData, ModuleSetting}; + +/// Highlights mobs (hostile and passive creatures) with a 3D wireframe box. +/// +/// All rendering lives in [`crate::graphic::esp`]; this struct only carries the +/// toggle state and the visual settings. +#[derive(Debug)] +pub struct MobEspModule { + pub module: ModuleData, +} + +impl MobEspModule { + pub fn new() -> Self { + Self { + module: ModuleData { + name: "Mob ESP".to_string(), + description: "Draws a 3D box around mobs".to_string(), + category: ModuleCategory::RENDER, + key_bind: KeyboardKey::KeyNone, + enabled: false, + settings: vec![ + ModuleSetting::Color { + name: "Color".to_string(), + value: [1.0, 0.84, 0.2, 1.0], + }, + ModuleSetting::Toggle { + name: "Name".to_string(), + value: true, + }, + ModuleSetting::Toggle { + name: "Distance".to_string(), + value: true, + }, + ModuleSetting::Toggle { + name: "Health".to_string(), + value: true, + }, + ModuleSetting::Slider { + name: "Range".to_string(), + value: 64.0, + min: 16.0, + max: 256.0, + }, + ], + }, + } + } +} + +impl Module for MobEspModule { + fn on_start(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn on_stop(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn on_tick(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn get_module_data(&self) -> &ModuleData { + &self.module + } + + fn get_module_data_mut(&mut self) -> &mut ModuleData { + &mut self.module + } +} diff --git a/client/src/module/render/mod.rs b/client/src/module/render/mod.rs new file mode 100644 index 0000000..6361411 --- /dev/null +++ b/client/src/module/render/mod.rs @@ -0,0 +1,10 @@ +//! Render-category modules. +//! +//! These modules are pure configuration holders: the actual drawing is done +//! every frame by [`crate::graphic::esp`], which reads their enabled state and +//! settings. Keeping the JNI/render work in one place is what lets the ESP run +//! without costing frame rate. + +pub mod chest_esp; +pub mod mob_esp; +pub mod player_esp; diff --git a/client/src/module/render/player_esp.rs b/client/src/module/render/player_esp.rs new file mode 100644 index 0000000..343eb2e --- /dev/null +++ b/client/src/module/render/player_esp.rs @@ -0,0 +1,70 @@ +use crate::module::{KeyboardKey, Module, ModuleCategory, ModuleData, ModuleSetting}; + +/// Highlights other players with a 3D wireframe box. +/// +/// All rendering lives in [`crate::graphic::esp`]; this struct only carries the +/// toggle state and the visual settings. +#[derive(Debug)] +pub struct PlayerEspModule { + pub module: ModuleData, +} + +impl PlayerEspModule { + pub fn new() -> Self { + Self { + module: ModuleData { + name: "Player ESP".to_string(), + description: "Draws a 3D box around players".to_string(), + category: ModuleCategory::RENDER, + key_bind: KeyboardKey::KeyNone, + enabled: false, + settings: vec![ + ModuleSetting::Color { + name: "Color".to_string(), + value: [1.0, 0.27, 0.27, 1.0], + }, + ModuleSetting::Toggle { + name: "Name".to_string(), + value: true, + }, + ModuleSetting::Toggle { + name: "Distance".to_string(), + value: true, + }, + ModuleSetting::Toggle { + name: "Health".to_string(), + value: true, + }, + ModuleSetting::Slider { + name: "Range".to_string(), + value: 64.0, + min: 16.0, + max: 256.0, + }, + ], + }, + } + } +} + +impl Module for PlayerEspModule { + fn on_start(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn on_stop(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn on_tick(&self) -> anyhow::Result<()> { + Ok(()) + } + + fn get_module_data(&self) -> &ModuleData { + &self.module + } + + fn get_module_data_mut(&mut self) -> &mut ModuleData { + &mut self.module + } +} diff --git a/java_mappings.json b/java_mappings.json index c3f2505..302f906 100644 --- a/java_mappings.json +++ b/java_mappings.json @@ -22,5 +22,25 @@ } }, "fields": {} + }, + "java/util/Map": { + "name": "java/util/Map", + "methods": { + "values": { + "name": "values", + "signature": "()Ljava/util/Collection;" + } + }, + "fields": {} + }, + "java/lang/Integer": { + "name": "java/lang/Integer", + "methods": { + "intValue": { + "name": "intValue", + "signature": "()I" + } + }, + "fields": {} } }