Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
223 lines (198 loc) · 9.2 KB Raw
// Drawing the scanned entities into the world.
//
// The projection has to match the one the game rendered with, or the boxes sit
// beside the things they are tracking. Minecraft's yaw is zero looking south
// (+Z) and increases clockwise, so forward is
// (-sin yaw · cos pitch, -sin pitch, cos yaw · cos pitch); right is forward
// crossed with world-up, and the rest is an ordinary pinhole projection.
//
// One known limitation: this uses the *player's* eye and rotation rather than
// the render camera. They are the same in first person, which is the case that
// matters now; third person and freecam will need GameRenderer.mainCamera
// bound before they line up.
#pragma once

#include <cmath>
#include <string>

#include "imgui.h"
#include "state.hpp"

namespace lodestone::esp {

namespace {

ImU32 col(const state::Colour& c) {
    return ImGui::ColorConvertFloat4ToU32(ImVec4(c[0], c[1], c[2], c[3]));
}

double dot(const state::Vec3& a, const state::Vec3& b) {
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

/// World-to-screen for one frame's camera.
struct View {
    state::Vec3 camera, forward, right, up;
    float half_w = 0, half_h = 0;
    double tan_half_fov = 1.0, aspect = 1.0;

    View(const state::Vec3& eye, float yaw, float pitch, float fov, const ImVec2& size) {
        camera = eye;
        double y = yaw * 3.14159265358979323846 / 180.0;
        double p = pitch * 3.14159265358979323846 / 180.0;
        double sy = std::sin(y), cy = std::cos(y), sp = std::sin(p), cp = std::cos(p);
        forward = {-sy * cp, -sp, cy * cp};
        // forward x (0,1,0), flattened and normalised.
        state::Vec3 r{-forward.z, 0.0, forward.x};
        double rl = std::sqrt(r.x * r.x + r.z * r.z);
        if (rl < 1e-9) rl = 1e-9;
        right = {r.x / rl, 0.0, r.z / rl};
        // up = right x forward
        up = {right.y * forward.z - right.z * forward.y, right.z * forward.x - right.x * forward.z,
              right.x * forward.y - right.y * forward.x};
        half_w = size.x * 0.5f;
        half_h = size.y * 0.5f;
        double f = fov < 1.0f ? 1.0 : static_cast<double>(fov);
        tan_half_fov = std::tan(f * 0.5 * 3.14159265358979323846 / 180.0);
        aspect = size.y > 1.0f ? static_cast<double>(size.x) / static_cast<double>(size.y) : 1.0;
    }

    bool project(const state::Vec3& world, ImVec2* out) const {
        state::Vec3 d{world.x - camera.x, world.y - camera.y, world.z - camera.z};
        double z = dot(d, forward);
        if (z <= 0.05) return false;  // at or behind the eye
        double x = dot(d, right);
        double y = dot(d, up);
        out->x = half_w * static_cast<float>(1.0 + x / (z * tan_half_fov * aspect));
        out->y = half_h * static_cast<float>(1.0 - y / (z * tan_half_fov));
        return true;
    }

    /// The screen rectangle covering all eight corners of a world box.
    bool project_box(const state::Vec3& lo, const state::Vec3& hi, ImVec2* out_min,
                     ImVec2* out_max) const {
        ImVec2 mn(1e9f, 1e9f), mx(-1e9f, -1e9f);
        for (int i = 0; i < 8; ++i) {
            state::Vec3 corner{(i & 1) ? hi.x : lo.x, (i & 2) ? hi.y : lo.y, (i & 4) ? hi.z : lo.z};
            ImVec2 p;
            if (!project(corner, &p)) return false;  // any corner behind: skip it
            mn.x = p.x < mn.x ? p.x : mn.x;
            mn.y = p.y < mn.y ? p.y : mn.y;
            mx.x = p.x > mx.x ? p.x : mx.x;
            mx.y = p.y > mx.y ? p.y : mx.y;
        }
        *out_min = mn;
        *out_max = mx;
        return true;
    }
};

bool shows(const state::Esp& cfg, state::TargetKind kind) {
    switch (kind) {
        case state::TargetKind::Player: return cfg.players;
        case state::TargetKind::Mob: return cfg.mobs;
        case state::TargetKind::Animal: return cfg.animals;
        case state::TargetKind::Item: return cfg.items;
        case state::TargetKind::Other: return false;
    }
    return false;
}

const state::Colour& colour_of(const state::Esp& cfg, const state::Target& t) {
    // Something whose own reach already covers you is the one thing worth
    // seeing first, so it outranks every other colour.
    if (t.can_reach_you && cfg.show_threat) return cfg.color_threat;
    if (t.invisible && cfg.show_invis) return cfg.color_invis;
    switch (t.kind) {
        case state::TargetKind::Player: return cfg.color_player;
        case state::TargetKind::Mob: return cfg.color_mob;
        case state::TargetKind::Animal: return cfg.color_animal;
        case state::TargetKind::Item: return cfg.color_item;
        case state::TargetKind::Other: break;
    }
    return cfg.color_player;
}

void draw_box(ImDrawList* dl, const ImVec2& a, const ImVec2& b, ImU32 c, state::BoxStyle style,
              bool fill) {
    if (style == state::BoxStyle::Corners) {
        float lx = (b.x - a.x) * 0.28f, ly = (b.y - a.y) * 0.22f;
        const float corners[4][4] = {
            {a.x, a.y, 1.0f, 1.0f}, {b.x, a.y, -1.0f, 1.0f},
            {a.x, b.y, 1.0f, -1.0f}, {b.x, b.y, -1.0f, -1.0f}};
        for (const auto& k : corners) {
            dl->AddLine(ImVec2(k[0], k[1]), ImVec2(k[0] + lx * k[2], k[1]), c, 1.5f);
            dl->AddLine(ImVec2(k[0], k[1]), ImVec2(k[0], k[1] + ly * k[3]), c, 1.5f);
        }
        return;
    }
    if (fill || style == state::BoxStyle::Filled) {
        ImVec4 f = ImGui::ColorConvertU32ToFloat4(c);
        f.w = 0.13f;
        dl->AddRectFilled(a, b, ImGui::ColorConvertFloat4ToU32(f));
    }
    // A dark outline keeps the box readable against bright terrain.
    dl->AddRect(ImVec2(a.x - 1, a.y - 1), ImVec2(b.x + 1, b.y + 1), IM_COL32(0, 0, 0, 150));
    dl->AddRect(a, b, c);
}

}  // namespace

/// Draw the world overlay. Runs whether or not the menu is open.
inline void draw(const state::Shared& s) {
    const state::Esp& cfg = s.cfg.esp;
    const state::GameState& game = s.game;
    if (!game.in_world || game.targets.empty()) return;

    ImVec2 size = ImGui::GetIO().DisplaySize;
    if (size.x < 1.0f || size.y < 1.0f) return;

    // What the world was actually rendered with, unless the custom-FOV module
    // is overriding it.
    float fov = s.cfg.visuals.fov ? s.cfg.visuals.fov_value
                                  : (game.fov > 1.0f ? game.fov : 70.0f);
    View view(game.eye, game.yaw, game.pitch, fov, size);
    ImDrawList* dl = ImGui::GetBackgroundDrawList();

    for (const auto& t : game.targets) {
        if (!shows(cfg, t.kind)) continue;
        if (t.distance > cfg.distance) continue;

        ImVec2 a, b;
        if (!view.project_box(t.min, t.max, &a, &b)) continue;
        if (b.x < 0 || b.y < 0 || a.x > size.x || a.y > size.y) continue;

        ImU32 c = col(colour_of(cfg, t));
        if (cfg.boxes) draw_box(dl, a, b, c, cfg.box_style, cfg.box_fill);

        if (cfg.tracers) {
            dl->AddLine(ImVec2(size.x * 0.5f, size.y), ImVec2((a.x + b.x) * 0.5f, b.y), c);
        }

        if (cfg.health_bars && t.max_health > 0.0f) {
            float frac = t.health / t.max_health;
            frac = frac < 0 ? 0 : (frac > 1 ? 1 : frac);
            float x = a.x - 5.0f;
            dl->AddRectFilled(ImVec2(x - 1, a.y), ImVec2(x + 1.5f, b.y), IM_COL32(0, 0, 0, 150));
            float top = b.y - (b.y - a.y) * frac;
            ImU32 hc = IM_COL32(static_cast<int>(255 * (1.0f - frac)), static_cast<int>(220 * frac),
                                80, 255);
            dl->AddRectFilled(ImVec2(x - 1, top), ImVec2(x + 1.5f, b.y), hc);
        }

        if (cfg.nametags && !t.name.empty()) {
            char label[128];
            if (cfg.show_ping && t.ping >= 0) {
                std::snprintf(label, sizeof(label), "%s  %.0fm  %dms", t.name.c_str(),
                              static_cast<double>(t.distance), t.ping);
            } else {
                std::snprintf(label, sizeof(label), "%s  %.0fm", t.name.c_str(),
                              static_cast<double>(t.distance));
            }
            float tw = ImGui::CalcTextSize(label).x;
            float cx = (a.x + b.x) * 0.5f;
            dl->AddRectFilled(ImVec2(cx - tw * 0.5f - 3, a.y - 16), ImVec2(cx + tw * 0.5f + 3, a.y - 2),
                              IM_COL32(0, 0, 0, 150), 2.0f);
            dl->AddText(ImVec2(cx - tw * 0.5f, a.y - 15), c, label);
        }

        // What they are holding and wearing, above the name. Armour is shown
        // even with an empty hand — bare-handed in full diamond still matters.
        if (cfg.show_gear && (!t.held.empty() || t.armor > 0 || !t.effects.empty())) {
            std::string gear = t.held;
            if (t.armor > 0) {
                char armour[16];
                std::snprintf(armour, sizeof(armour), "[%da]", t.armor);
                if (!gear.empty()) gear += "  ";
                gear += armour;
            }
            // Only the first few: a long potion stack would cover the screen.
            for (size_t i = 0; i < t.effects.size() && i < 3; ++i) {
                gear += (gear.empty() ? "" : "  ");
                gear += t.effects[i];
            }
            float gw = ImGui::CalcTextSize(gear.c_str()).x;
            float cx = (a.x + b.x) * 0.5f;
            dl->AddText(ImVec2(cx - gw * 0.5f + 1, a.y - 29), IM_COL32(0, 0, 0, 160), gear.c_str());
            dl->AddText(ImVec2(cx - gw * 0.5f, a.y - 30), IM_COL32(200, 200, 210, 255),
                        gear.c_str());
        }
    }
}

}  // namespace lodestone::esp