Sign in Sign up
kretrod/lodestone Public
Branches
master
429 lines (386 loc) · 14.3 KB Raw
//! Lodestone client — injected into Minecraft, drawn inside its own window.
//!
//! Flow:
//!   DllMain            spawns a thread so the loader lock is released at once
//!   init               waits for glfw.dll, then hooks glfwSwapBuffers
//!   swap_buffers       runs once per frame, on the game's render thread:
//!                        - first call sets up the window hook, GL and JNI
//!                        - applies the enabled modules through JNI
//!                        - draws the menu into the frame the game just built
//!                        - calls the real glfwSwapBuffers
//!
//! Running on the render thread is what makes the JNI side simple: it is a
//! Java thread, already attached, and the game is not concurrently mutating the
//! objects we touch.

#![cfg(windows)]

pub mod blocks;
pub mod capture;
pub mod config;
pub mod cheats;
pub mod hook;
pub mod input;
pub mod jni;
pub mod mc;
pub mod overlay;
pub mod state;

use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::OnceLock;

use windows_sys::Win32::Foundation::{HMODULE, HWND};
use windows_sys::Win32::System::LibraryLoader::{
    DisableThreadLibraryCalls, GetModuleHandleA, GetProcAddress,
};
use windows_sys::Win32::System::Threading::CreateThread;

use crate::jni::Jni;
use crate::mc::{Mc, World};

static HOOK: OnceLock<hook::Hook> = OnceLock::new();
static MODULE: std::sync::atomic::AtomicIsize = std::sync::atomic::AtomicIsize::new(0);
static UNLOADING: AtomicBool = AtomicBool::new(false);
/// How many threads are currently executing inside our detour. The module must
/// not be freed while this is above zero.
static IN_DETOUR: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
static FRAMES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

// ---------------------------------------------------------------------------
// entry point
// ---------------------------------------------------------------------------

#[no_mangle]
pub extern "system" fn DllMain(module: HMODULE, reason: u32, _reserved: *mut c_void) -> i32 {
    const DLL_PROCESS_ATTACH: u32 = 1;
    if reason == DLL_PROCESS_ATTACH {
        MODULE.store(module as isize, Ordering::SeqCst);
        // SAFETY: both calls are documented as safe from DllMain, and the
        // thread body does all the real work outside the loader lock.
        unsafe {
            DisableThreadLibraryCalls(module);
            CreateThread(
                std::ptr::null(),
                0,
                Some(init),
                std::ptr::null_mut(),
                0,
                std::ptr::null_mut(),
            );
        }
    }
    1
}

pub fn log(msg: &str) {
    use std::io::Write;
    if let Ok(mut f) = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open("C:\\lodestone\\client.log")
    {
        let _ = writeln!(f, "{msg}");
    }
}

unsafe extern "system" fn init(_: *mut c_void) -> u32 {
    log("--- lodestone client starting ---");
    // A panic inside the render hook would otherwise vanish into catch_unwind.
    std::panic::set_hook(Box::new(|info| {
        log(&format!("PANIC: {info}"));
    }));

    // The game loads GLFW early, but not necessarily before us.
    let mut glfw = std::ptr::null_mut();
    for _ in 0..600 {
        glfw = GetModuleHandleA(c"glfw.dll".as_ptr() as *const u8);
        if !glfw.is_null() {
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
    if glfw.is_null() {
        log("glfw.dll never appeared — giving up");
        return 1;
    }

    let Some(swap) = GetProcAddress(glfw, c"glfwSwapBuffers".as_ptr() as *const u8) else {
        log("glfw.dll does not export glfwSwapBuffers");
        return 1;
    };
    log(&format!("glfwSwapBuffers at {:p}", swap as *const c_void));

    let hooked = match hook::install(swap as *mut c_void, swap_buffers as *const c_void) {
        Ok(h) => {
            let _ = HOOK.set(h);
            log("hook installed");
            state::with(|s| s.status = "hooked".into());
            true
        }
        Err(e) => {
            log(&format!("hook failed: {e}"));
            state::with(|s| s.status = format!("hook failed: {e}"));
            false
        }
    };


    // Watchdog: the loader drops a marker file to ask us to leave, which is how
    // a rebuild gets the old copy out of the way without restarting the game.
    loop {
        std::thread::sleep(std::time::Duration::from_millis(250));
        if std::path::Path::new(UNLOAD_MARKER).exists() {
            if hooked {
                // Let the render thread unwind the hooks in the right order.
                state::with(|s| s.eject = true);
            } else {
                log("unload marker seen; nothing was hooked");
            }
            return 0;
        }
    }
}

const UNLOAD_MARKER: &str = "C:\\lodestone\\unload";

// ---------------------------------------------------------------------------
// per-frame
// ---------------------------------------------------------------------------

/// Everything that may only be touched from the render thread.
struct Render {
    overlay: overlay::Overlay,
    jni: Jni,
    mc: Option<Mc>,
    world: Option<World>,
    blocks: Option<blocks::Blocks>,
    saved: cheats::Saved,
    window: *mut c_void,
    hwnd: HWND,
    menu_was_open: bool,
    saved_cursor_mode: i32,
}

/// Only ever read or written inside `swap_buffers`, which the game calls from a
/// single thread.
static mut RENDER: Option<Render> = None;

unsafe extern "C" fn swap_buffers(window: *mut c_void) {
    IN_DETOUR.fetch_add(1, Ordering::SeqCst);
    if !UNLOADING.load(Ordering::SeqCst) {
        let n = FRAMES.fetch_add(1, Ordering::Relaxed);
        if n % 600 == 0 {
            log(&format!("frame {n}"));
        }
        // A panic must never unwind into the game's C frame.
        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| frame(window)));
    }

    if let Some(h) = HOOK.get() {
        let original: unsafe extern "C" fn(*mut c_void) = std::mem::transmute(h.trampoline);
        original(window);
    }
    IN_DETOUR.fetch_sub(1, Ordering::SeqCst);
}

unsafe fn frame(window: *mut c_void) {
    let slot = &raw mut RENDER;
    if (*slot).is_none() {
        match setup(window) {
            Ok(r) => {
                *slot = Some(r);
                log("render state ready");
            }
            Err(e) => {
                log(&format!("setup failed: {e}"));
                state::with(|s| s.status = e.clone());
                // Try again next frame: the world may not exist yet.
                return;
            }
        }
    }
    let Some(r) = (*slot).as_mut() else { return };

    // Cursor: while the menu is open the game must release the mouse, and get
    // it back exactly as it was when the menu closes.
    let open = state::with(|s| s.menu_open).unwrap_or(false);
    if open != r.menu_was_open {
        if open {
            r.saved_cursor_mode = glfw::get_input_mode(r.window, glfw::CURSOR);
            glfw::set_input_mode(r.window, glfw::CURSOR, glfw::CURSOR_NORMAL);
        } else if r.saved_cursor_mode != 0 {
            glfw::set_input_mode(r.window, glfw::CURSOR, r.saved_cursor_mode);
        }
        r.menu_was_open = open;
    }

    // Modules, inside a local frame so nothing leaks.
    if let Some(mcx) = r.mc.as_ref() {
        if let Some(_guard) = r.jni.frame(256) {
            let cfg = state::with(|s| s.cfg.clone());
            if let Some(cfg) = cfg {
                let mut game =
                    cheats::apply(&r.jni, mcx, r.world.as_ref(), &cfg, &mut r.saved);
                // Blocks are scanned incrementally, so this only costs a slice
                // of the volume per frame.
                if let (Some(scan), true) = (r.blocks.as_mut(), game.in_world) {
                    if let Some(instance) = mcx.instance(&r.jni) {
                        if let Some(level) = mcx.level(&r.jni, instance) {
                            scan.step(
                                &r.jni,
                                level,
                                game.pos,
                                cfg.esp.block_radius as i32,
                                cfg.esp.xray,
                                cfg.esp.containers,
                                2048,
                            );
                            game.blocks = scan.found.clone();
                        }
                    }
                }
                state::with(|s| s.game = game);
            }
        }
    }

    let (w, h) = glfw::framebuffer_size(r.window);
    if w > 0 && h > 0 {
        let fps = r.overlay.fps();
        state::with(|s| {
            s.game.fps = fps;
            r.overlay.draw(w, h, s);
        });
    }

    if state::with(|s| s.eject).unwrap_or(false) {
        log("unload requested");
        unload(r);
    }
}

unsafe fn setup(window: *mut c_void) -> Result<Render, String> {
    let hwnd = glfw::win32_window(window);
    if hwnd == 0 {
        return Err("glfwGetWin32Window returned null".into());
    }
    capture::set_window(hwnd as HWND);
    if !input::install(hwnd as HWND) {
        log("window procedure hook failed");
    }

    let overlay = overlay::Overlay::new()?;
    log("gl painter created");

    let vm = jni::java_vm().ok_or("no JVM in this process")?;
    let jni = Jni::current(vm).ok_or("this thread is not attached to the JVM")?;
    log("jni environment acquired");

    let mut world = None;
    let mut block_scan = None;
    let mc = match Mc::resolve(&jni) {
        Ok(m) => {
            if m.missing.is_empty() {
                log("all minecraft names resolved");
            } else {
                log(&format!("unresolved: {}", m.missing.join(", ")));
            }
            let mut missing = m.missing.clone();
            world = World::resolve(&jni, &m, &mut missing);
            block_scan = blocks::Blocks::resolve(&jni, &mut missing);
            if block_scan.is_none() {
                log("block scanning unavailable: x-ray and container esp are off");
            }
            if world.is_none() {
                log("world bindings unavailable: combat and ESP are off");
            }
            if !missing.is_empty() {
                log(&format!("unresolved: {}", missing.join(", ")));
            }
            state::with(|s| {
                s.missing = missing;
                s.status = "ready".into();
            });
            Some(m)
        }
        Err(e) => {
            log(&format!("minecraft bindings failed: {e}"));
            state::with(|s| s.status = format!("bindings: {e}"));
            None
        }
    };

    // Pick up settings from the last session, if there are any.
    match config::load_into_shared() {
        Ok(true) => log("config loaded"),
        Ok(false) => {}
        Err(e) => log(&format!("config load failed: {e}")),
    }

    Ok(Render {
        overlay,
        jni,
        mc,
        world,
        blocks: block_scan,
        saved: cheats::Saved::default(),
        window,
        hwnd: hwnd as HWND,
        menu_was_open: false,
        saved_cursor_mode: 0,
    })
}

/// Stop the client: hooks out, game state restored, nothing drawn.
///
/// It deliberately does **not** unmap the module. A DLL that patched a window
/// procedure, installed a panic hook and left Rust thread-locals behind cannot
/// prove that nothing in the process still points into its code, and unmapping
/// it while one pointer survives crashes the game — which is exactly what an
/// earlier version of this did. A few megabytes of idle address space is a
/// much better trade. Restart the game to be rid of it entirely.
unsafe fn unload(r: &mut Render) {
    UNLOADING.store(true, Ordering::SeqCst);

    if let Some(mcx) = r.mc.as_ref() {
        if let Some(_guard) = r.jni.frame(32) {
            cheats::restore(&r.jni, mcx, r.world.as_ref(), &mut r.saved);
        }
    }
    capture::apply(false);
    let restored = input::remove(r.hwnd);
    if r.saved_cursor_mode != 0 {
        glfw::set_input_mode(r.window, glfw::CURSOR, r.saved_cursor_mode);
    }
    if let Some(h) = HOOK.get() {
        h.remove();
    }
    state::with(|s| {
        s.eject = false;
        s.menu_open = false;
        s.status = "unloaded".into();
    });
    log(&format!(
        "unloaded: hooks removed, settings restored, window procedure {} — module stays resident",
        if restored { "restored" } else { "kept (something subclassed after us)" }
    ));
}

// ---------------------------------------------------------------------------
// the few GLFW entry points we need
// ---------------------------------------------------------------------------

mod glfw {
    use std::ffi::c_void;

    use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};

    pub const CURSOR: i32 = 0x0003_3001;
    pub const CURSOR_NORMAL: i32 = 0x0003_4001;

    unsafe fn proc(name: &[u8]) -> Option<unsafe extern "system" fn() -> isize> {
        let glfw = GetModuleHandleA(c"glfw.dll".as_ptr() as *const u8);
        if glfw.is_null() {
            return None;
        }
        GetProcAddress(glfw, name.as_ptr())
    }

    pub unsafe fn win32_window(window: *mut c_void) -> isize {
        match proc(b"glfwGetWin32Window\0") {
            Some(f) => {
                let f: unsafe extern "C" fn(*mut c_void) -> isize = std::mem::transmute(f);
                f(window)
            }
            None => 0,
        }
    }

    pub unsafe fn framebuffer_size(window: *mut c_void) -> (i32, i32) {
        match proc(b"glfwGetFramebufferSize\0") {
            Some(f) => {
                let f: unsafe extern "C" fn(*mut c_void, *mut i32, *mut i32) =
                    std::mem::transmute(f);
                let (mut w, mut h) = (0i32, 0i32);
                f(window, &mut w, &mut h);
                (w, h)
            }
            None => (0, 0),
        }
    }

    pub unsafe fn get_input_mode(window: *mut c_void, mode: i32) -> i32 {
        match proc(b"glfwGetInputMode\0") {
            Some(f) => {
                let f: unsafe extern "C" fn(*mut c_void, i32) -> i32 = std::mem::transmute(f);
                f(window, mode)
            }
            None => 0,
        }
    }

    pub unsafe fn set_input_mode(window: *mut c_void, mode: i32, value: i32) {
        if let Some(f) = proc(b"glfwSetInputMode\0") {
            let f: unsafe extern "C" fn(*mut c_void, i32, i32) = std::mem::transmute(f);
            f(window, mode, value);
        }
    }
}