Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
169 lines (145 loc) · 5.3 KB Raw
#include "overlay.hpp"

#include <windows.h>

#include "backends/imgui_impl_opengl3.h"
#include "capture.hpp"
#include "esp.hpp"
#include "gl.hpp"
#include "imgui.h"
#include "input.hpp"
#include "log.hpp"
#include "menu.hpp"
#include "state.hpp"

namespace lodestone::overlay {
namespace {

bool g_ready = false;
bool g_failed = false;
bool g_logged_state = false;
HWND g_hwnd = nullptr;
LARGE_INTEGER g_freq{}, g_last{};

using PFNGetFramebufferSize = void (*)(void*, int*, int*);
using PFNGetWin32Window = void* (*)(void*);
PFNGetFramebufferSize g_framebuffer_size = nullptr;

void* glfw_proc(const char* name) {
    HMODULE glfw = GetModuleHandleA("glfw.dll");
    if (!glfw) return nullptr;
    return reinterpret_cast<void*>(GetProcAddress(glfw, name));
}

bool init(void* window) {
    gl::load();
    if (!gl::g_api.GetIntegerv || !gl::g_api.BindFramebuffer) {
        log("gl entry points missing — is this a core profile context?");
        return false;
    }
    g_framebuffer_size =
        reinterpret_cast<PFNGetFramebufferSize>(glfw_proc("glfwGetFramebufferSize"));

    // The window procedure is what gives us Insert and swallows input while the
    // menu is open; without the HWND there is nothing to subclass.
    if (auto get_hwnd = reinterpret_cast<PFNGetWin32Window>(glfw_proc("glfwGetWin32Window"))) {
        g_hwnd = static_cast<HWND>(get_hwnd(window));
        if (g_hwnd) {
            capture::set_window(g_hwnd);
            if (!input::install(g_hwnd)) log("window procedure hook failed");
        }
    }

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();
    io.IniFilename = nullptr;  // no imgui.ini next to the game
    io.LogFilename = nullptr;
    ImGui::StyleColorsDark();

    // The font atlas is uploaded during backend setup, and that upload reads the
    // *game's* pixel-transfer state — so it has to happen with the state
    // neutralised or the glyphs come out as noise (the "cursed text" bug).
    gl::State saved = gl::State::save();
    gl::State::neutralise();
    bool ok = ImGui_ImplOpenGL3_Init("#version 130");
    if (ok) ok = ImGui_ImplOpenGL3_CreateDeviceObjects();
    saved.restore();

    if (!ok) {
        log("ImGui_ImplOpenGL3_Init failed");
        return false;
    }
    QueryPerformanceFrequency(&g_freq);
    QueryPerformanceCounter(&g_last);
    log("imgui renderer created");
    return true;
}

/// Feed the queued Win32 input into ImGui. The window hook has already decided
/// what belongs to the menu, so anything here is ours.
void pump_input(ImGuiIO& io) {
    auto events = state::with([](state::Shared& s) {
        auto copy = s.events;
        s.events.clear();
        return copy;
    });
    for (const auto& e : events) {
        using K = state::UiInput::Kind;
        switch (e.kind) {
            case K::MouseMove: io.AddMousePosEvent(e.x, e.y); break;
            case K::MouseButton: io.AddMouseButtonEvent(e.button, e.down); break;
            case K::Wheel: io.AddMouseWheelEvent(0.0f, e.y); break;
            case K::Char: io.AddInputCharacter(e.codepoint); break;
            case K::Key: break;  // module binds are handled before ImGui sees them
        }
    }
}

}  // namespace

void frame(void* window) {
    if (g_failed) return;
    if (!g_ready) {
        if (!init(window)) {
            g_failed = true;
            return;
        }
        g_ready = true;
    }

    int w = 0, h = 0;
    if (g_framebuffer_size) g_framebuffer_size(window, &w, &h);
    if (w <= 0 || h <= 0) return;

    ImGuiIO& io = ImGui::GetIO();
    io.DisplaySize = ImVec2(static_cast<float>(w), static_cast<float>(h));

    LARGE_INTEGER now;
    QueryPerformanceCounter(&now);
    double dt =
        static_cast<double>(now.QuadPart - g_last.QuadPart) / static_cast<double>(g_freq.QuadPart);
    g_last = now;
    io.DeltaTime = dt > 0.0 ? static_cast<float>(dt) : 1.0f / 60.0f;

    pump_input(io);

    ImGui_ImplOpenGL3_NewFrame();
    ImGui::NewFrame();

    // One lock for both: the ESP draws every frame, the menu only when open.
    // Holding it across the draw is what the render thread does anyway, and the
    // window procedure only ever touches the state briefly.
    state::with([](state::Shared& s) {
        esp::draw(s);
        menu::hud(s);
        if (s.menu_open) menu::draw(s);
        // All-or-nothing on the whole window, so this only touches it when the
        // switch actually changes rather than once a frame.
        static bool capture_hidden = false;
        if (s.cfg.misc.hide_from_capture != capture_hidden) {
            capture_hidden = s.cfg.misc.hide_from_capture;
            capture::apply(capture_hidden);
        }
    });

    ImGui::Render();

    gl::State saved = gl::State::save();
    if (!g_logged_state) {
        g_logged_state = true;
        logf("gl at swap: fbo=%d program=%d sampler0=%d unpackBuf=%d rowLen=%d srgb=%d",
             saved.draw_framebuffer, saved.program, saved.sampler0, saved.unpack_buffer,
             saved.unpack_row_length, static_cast<int>(saved.srgb));
    }
    gl::State::neutralise();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
    saved.restore();
}

void shutdown() {
    if (!g_ready) return;
    if (g_hwnd) input::remove(g_hwnd);
    capture::apply(false);
    ImGui_ImplOpenGL3_Shutdown();
    ImGui::DestroyContext();
    g_ready = false;
}

}  // namespace lodestone::overlay