Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
312 lines (287 loc) · 13.5 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: reads the
//                  game through JNI, draws the menu into the frame the game
//                  just built, then calls on
//
// 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.

#include <windows.h>

#include <atomic>
#include <cmath>
#include <string>
#include <vector>

#include "MinHook.h"
#include "actions.hpp"
#include "cheats.hpp"
#include "config.hpp"
#include "interact.hpp"
#include "jni.hpp"
#include "log.hpp"
#include "mc.hpp"
#include "overlay.hpp"
#include "state.hpp"
#include "world.hpp"

namespace {

using PFNSwapBuffers = void (*)(void*);
PFNSwapBuffers g_original = nullptr;
void* g_target = nullptr;

std::atomic<bool> g_unloading{false};
std::atomic<uint64_t> g_frames{0};
/// How many threads are inside the detour. The hook must not be removed from
/// another thread while this is above zero.
std::atomic<int> g_in_detour{0};

constexpr const char* kUnloadMarker = "unload";
/// A busy world can hold thousands of entities and this runs every frame.
constexpr int kMaxScan = 4096;

/// Everything that may only be touched from the render thread.
struct Render {
    lodestone::jni::Env env;
    lodestone::mc::Mc mc;
    lodestone::mc::World world;
    lodestone::mc::Interact interact;
    lodestone::mc::Actions actions;
    lodestone::cheats::Saved saved;
    bool ready = false;
    bool failed = false;
    bool probed = false;
};
Render g_render;

/// One-time report that the inventory bindings actually found anything. These
/// are optional lookups, so a silent null would otherwise look identical to
/// "you are not carrying one".
void probe_inventory(const lodestone::jni::Env& j, jobject player) {
    if (g_render.probed) return;
    g_render.probed = true;
    lodestone::logf("interact: totem=%d crystal=%d mace=%d selected=%d",
                    g_render.interact.find_totem(j, player),
                    g_render.interact.find_crystal(j, player),
                    g_render.interact.find_mace(j, player),
                    g_render.interact.selected_slot(j, player));
}

/// Pull this frame's game state across into the shared state the menu reads.
void read_game() {
    using namespace lodestone;
    if (g_render.failed) return;

    if (!g_render.ready) {
        JavaVM* vm = jni::java_vm();
        if (!vm) return;  // the JVM is there; it may simply not be up yet
        g_render.env = jni::Env::current(vm);
        if (!g_render.env) {
            log("this thread is not attached to the JVM");
            g_render.failed = true;
            return;
        }
        std::vector<std::string> missing;
        if (!g_render.mc.resolve(g_render.env, missing)) {
            log("minecraft bindings failed — version too different");
            g_render.failed = true;
            return;
        }
        g_render.world.resolve(g_render.env, g_render.mc, missing);
        g_render.interact.resolve(g_render.env, g_render.mc, missing);
        g_render.actions.resolve(g_render.env, g_render.mc, missing);
        for (const auto& m : missing) log("unresolved: " + m);
        if (missing.empty()) log("all minecraft names resolved");
        // Pick up settings from the last session, if there are any.
        state::with([](state::Shared& s) {
            if (config::load(s)) log("config loaded");
        });
        g_render.ready = true;
    }

    const jni::Env& j = g_render.env;
    // Every frame inside a local reference frame, so the references made while
    // scanning are released together instead of leaking the heap away.
    jni::Frame frame(j, 256);
    if (!frame) return;

    jobject instance = g_render.mc.instance(j);
    if (!instance) return;
    // One copy of the settings for the whole frame, taken before the scan so
    // the scan and the modules cannot disagree about what is switched on — and
    // so the lock is never held while we are inside JNI.
    state::Config cfg = state::with([](state::Shared& s) { return s.cfg; });
    jobject player = g_render.mc.player(j, instance);
    bool screen = g_render.mc.screen_open(j, instance);

    state::GameState game;
    game.screen_open = screen;
    game.in_world = player != nullptr;
    if (player) {
        g_render.mc.position(j, player, &game.pos);
        game.eye = game.pos;
        game.eye.y += 1.62;  // standing eye height
        // The FOV the world is actually rendered with, so the ESP projection
        // matches instead of assuming the default.
        float fov = g_render.mc.fov(j, instance);
        if (fov > 1.0f) game.fov = fov;
        game.yaw = g_render.mc.yaw(j, player);
        game.pitch = g_render.mc.pitch(j, player);
        game.on_ground = g_render.mc.on_ground(j, player);
        game.health = g_render.mc.health(j, player);
        // Counts down from 10 after damage lands, so a rise is a hit arriving —
        // which is what the totem and the shield react to.
        game.hurt_time = g_render.world.hurt_time(j, player);
        game.sprinting = g_render.actions.sprinting(j, player);
        probe_inventory(j, player);

        // Entity scan. This is what the ESP and the aura will both read from,
        // so it happens once per frame rather than once per feature.
        if (jobject level = g_render.mc.level(j, instance)) {
            if (jobject iterator = g_render.world.entity_iterator(j, level)) {
                for (int n = 0; n < kMaxScan; ++n) {
                    jobject entity = g_render.world.iter_next(j, iterator);
                    if (!entity) break;
                    if (!j.same_object(entity, player) && g_render.mc.is_alive(j, entity)) {
                        state::Target t;
                        if (g_render.world.bounding_box(j, entity, &t.min, &t.max)) {
                            t.kind = g_render.world.classify(j, g_render.mc, entity);
                            // Everything extending Player classifies as one,
                            // which is how NPCs, holograms and duplicate clone
                            // entities end up drawn as people. Only something
                            // the server lists in the tab list is really a
                            // player; when the lookup cannot answer, leave it
                            // alone rather than risk hiding someone real.
                            if (t.kind == state::TargetKind::Player) {
                                bool known = false;
                                bool listed =
                                    g_render.world.tab_player(j, player, entity, &known);
                                if (known && !listed) t.kind = state::TargetKind::Other;
                            }
                            double cx = (t.min.x + t.max.x) * 0.5;
                            double cy = (t.min.y + t.max.y) * 0.5;
                            double cz = (t.min.z + t.max.z) * 0.5;
                            double dx = cx - game.eye.x, dy = cy - game.eye.y,
                                   dz = cz - game.eye.z;
                            t.distance = static_cast<float>(std::sqrt(dx * dx + dy * dy + dz * dz));
                            t.invisible = g_render.world.is_invisible(j, entity);
                            // Health only exists on living things, and without
                            // it the ESP's health bars have nothing to draw.
                            if (j.is_instance(entity, g_render.mc.living_class())) {
                                t.health = g_render.mc.health(j, entity);
                                t.max_health = g_render.mc.max_health(j, entity);
                            }
                            if (t.kind == state::TargetKind::Player) {
                                t.name = g_render.world.name(j, entity);
                                // Round-trip time the server already reports in
                                // the tab list — nothing is probed for it.
                                if (cfg.esp.show_ping) {
                                    t.ping = g_render.world.ping_of(j, player, entity);
                                }
                                // Whether its *own* reach already covers you,
                                // taken from its synced attribute rather than a
                                // guess at vanilla's three blocks.
                                if (cfg.esp.show_threat) {
                                    double reach = g_render.world.attribute(
                                        j, entity, g_render.world.holder_entity_reach());
                                    t.can_reach_you =
                                        reach > 0.0 &&
                                        static_cast<double>(t.distance) <= reach + 0.6;
                                }
                                if (cfg.esp.show_gear) {
                                    g_render.world.entity_intel(j, entity, &t.held, &t.armor,
                                                                &t.effects);
                                }
                            }
                            game.targets.push_back(std::move(t));
                        }
                    }
                    j.delete_local(entity);
                }
                j.delete_local(iterator);
            }
        }
    }

    state::with([&](state::Shared& s) { s.game = game; });

    // Modules run against the copy taken above, so the lock is never held while
    // we are inside JNI — the window procedure has to stay responsive.
    cheats::apply(j, g_render.mc, g_render.world, g_render.interact, g_render.actions, instance,
                  player, cfg, game, g_render.saved);
}

void swap_buffers(void* window) {
    g_in_detour.fetch_add(1, std::memory_order_seq_cst);
    if (!g_unloading.load(std::memory_order_seq_cst)) {
        uint64_t n = g_frames.fetch_add(1, std::memory_order_relaxed);
        if (n % 600 == 0) lodestone::logf("frame %llu", static_cast<unsigned long long>(n));
        read_game();
        lodestone::overlay::frame(window);
    }
    if (g_original) g_original(window);
    g_in_detour.fetch_sub(1, std::memory_order_seq_cst);
}

bool marker_present() {
    std::string path = lodestone::client_dir() + kUnloadMarker;
    return GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES;
}

DWORD WINAPI init(LPVOID) {
    lodestone::log("--- lodestone c++ client starting ---");

    // The game loads GLFW early, but not necessarily before us.
    HMODULE glfw = nullptr;
    for (int i = 0; i < 600 && !glfw; ++i) {
        glfw = GetModuleHandleA("glfw.dll");
        if (!glfw) Sleep(100);
    }
    if (!glfw) {
        lodestone::log("glfw.dll never appeared — giving up");
        return 1;
    }
    g_target = reinterpret_cast<void*>(GetProcAddress(glfw, "glfwSwapBuffers"));
    if (!g_target) {
        lodestone::log("glfw.dll does not export glfwSwapBuffers");
        return 1;
    }
    lodestone::logf("glfwSwapBuffers at %p", g_target);

    bool hooked = false;
    if (MH_Initialize() == MH_OK &&
        MH_CreateHook(g_target, reinterpret_cast<void*>(&swap_buffers),
                      reinterpret_cast<void**>(&g_original)) == MH_OK &&
        MH_EnableHook(g_target) == MH_OK) {
        hooked = true;
        lodestone::log("hook installed");
    } else {
        lodestone::log("hook failed");
    }

    // 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.
    for (;;) {
        Sleep(250);
        if (!marker_present()) continue;
        if (!hooked) {
            lodestone::log("unload marker seen; nothing was hooked");
            return 0;
        }
        // If the detour has never run, the game is not drawing and the render
        // thread will never see the request — which used to strand the hook
        // until the game was restarted, blocking every later build. Nothing can
        // be inside a detour that has never executed, so in that one case it is
        // safe to pull the hook from here rather than wait for a frame that is
        // not coming.
        if (g_frames.load(std::memory_order_seq_cst) == 0) {
            g_unloading.store(true, std::memory_order_seq_cst);
            MH_DisableHook(g_target);
            lodestone::log("unload marker seen with no frame ever drawn; hook removed off-thread");
            return 0;
        }
        g_unloading.store(true, std::memory_order_seq_cst);
        // Let any thread already inside the detour leave before the hook goes.
        for (int i = 0; i < 200 && g_in_detour.load(std::memory_order_seq_cst) > 0; ++i) Sleep(5);
        lodestone::overlay::shutdown();
        MH_DisableHook(g_target);
        lodestone::log("unloaded: hook removed — module stays resident");
        return 0;
    }
}

}  // namespace

// The module is deliberately never unmapped. A DLL that patched a window
// procedure and left thread-locals behind cannot prove nothing still points
// into its code, and unmapping it while one pointer survives crashes the game.
// Unloading unhooks and goes inert; a game restart is what clears it.
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID) {
    if (reason == DLL_PROCESS_ATTACH) {
        lodestone::g_module = module;
        DisableThreadLibraryCalls(module);
        CreateThread(nullptr, 0, init, nullptr, 0, nullptr);
    }
    return TRUE;
}