#include "GameOverlay.h" #include #include #include #include #include #include #include #include "Readers/EntityTypeCache.h" #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #include #include #include #pragma comment(lib, "iphlpapi.lib") #include #include #include "MenuBridge.h" // Per-frame bridge pointer consumed by the vendored Lumin menu (gui.cpp). MenuBridge* g_menu = nullptr; // ------------------------------------------------------------------------- // Bone interpolation / extrapolation helpers // ------------------------------------------------------------------------- int64_t GameOverlay::NowMs() { using namespace std::chrono; return duration_cast(steady_clock::now().time_since_epoch()).count(); } // Linearly interpolate (or extrapolate when t > 1) between two bone sets. static SkeletonBones LerpBones(const SkeletonBones& a, const SkeletonBones& b, float t) { auto lv = [t](const Vector3& p, const Vector3& q) -> Vector3 { return { p.x + t*(q.x-p.x), p.y + t*(q.y-p.y), p.z + t*(q.z-p.z) }; }; SkeletonBones out; out.valid = b.valid; out.neck = lv(a.neck, b.neck); out.head = lv(a.head, b.head); out.spine = lv(a.spine, b.spine); out.pelvis = lv(a.pelvis, b.pelvis); out.rightShoulder = lv(a.rightShoulder, b.rightShoulder); out.rightElbow = lv(a.rightElbow, b.rightElbow); out.rightHand = lv(a.rightHand, b.rightHand); out.leftShoulder = lv(a.leftShoulder, b.leftShoulder); out.leftElbow = lv(a.leftElbow, b.leftElbow); out.leftHand = lv(a.leftHand, b.leftHand); out.rightHip = lv(a.rightHip, b.rightHip); out.rightKnee = lv(a.rightKnee, b.rightKnee); out.rightAnkle = lv(a.rightAnkle, b.rightAnkle); out.leftHip = lv(a.leftHip, b.leftHip); out.leftKnee = lv(a.leftKnee, b.leftKnee); out.leftAnkle = lv(a.leftAnkle, b.leftAnkle); return out; } // Update the history when a new DMA read is detected (head bone moved). static void UpdateBoneHistory(GameOverlay::BoneHistory& h, const SkeletonBones& bones, int64_t nowMs) { if (!h.initialized) { h.prev = h.curr = bones; h.prevMs = h.currMs = nowMs; h.initialized = true; return; } const Vector3& o = h.curr.head; const Vector3& n = bones.head; float dx = o.x-n.x, dy = o.y-n.y, dz = o.z-n.z; float dist2 = dx*dx + dy*dy + dz*dz; // Reject implausible jumps: > 3 m in one update cycle can't happen at // any in-game speed — it means the scatter read returned garbage. // Keeping the old history prevents one stale read from corrupting the // extrapolation state; the DMA-side eviction (cache->valid=false) will // fix the pointer within the next boneRefreshMs window. if (dist2 > 9.0f) return; if (dist2 > 1e-8f) { h.prev = h.curr; h.prevMs = h.currMs; h.curr = bones; h.currMs = nowMs; } } // Return bones extrapolated to nowMs using the recorded velocity. // Alpha is clamped to 1.5 update intervals to limit overshoot on direction changes. static SkeletonBones GetSmoothedBones(const GameOverlay::BoneHistory& h, int64_t nowMs) { if (!h.initialized) return h.curr; int64_t dt = h.currMs - h.prevMs; if (dt <= 0) return h.curr; float alpha = static_cast(nowMs - h.prevMs) / static_cast(dt); alpha = std::min(alpha, 1.5f); return LerpBones(h.prev, h.curr, alpha); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- // SetWebRadarPort — resolve LAN IPs and build display URLs // ------------------------------------------------------------------------- void GameOverlay::SetWebRadarPort(int port) { m_webPort = port; m_webUrls.clear(); m_webUrls.push_back(std::format("http://localhost:{}", port)); // Enumerate all unicast IPv4 addresses. ULONG bufLen = 15000; std::vector buf(bufLen); auto* table = reinterpret_cast(buf.data()); DWORD ret = GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, nullptr, table, &bufLen); if (ret == ERROR_BUFFER_OVERFLOW) { buf.resize(bufLen); table = reinterpret_cast(buf.data()); ret = GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, nullptr, table, &bufLen); } if (ret == NO_ERROR) { for (auto* a = table; a; a = a->Next) { if (a->OperStatus != IfOperStatusUp) continue; for (auto* ua = a->FirstUnicastAddress; ua; ua = ua->Next) { auto* sin = reinterpret_cast(ua->Address.lpSockaddr); char ipStr[INET_ADDRSTRLEN] = {}; inet_ntop(AF_INET, &sin->sin_addr, ipStr, sizeof(ipStr)); std::string ip(ipStr); if (ip == "127.0.0.1") continue; m_webUrls.push_back(std::format("http://{}:{}", ip, port)); } } } } float GameOverlay::Dist(const Vector3& a, const Vector3& b) { float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z; return std::sqrtf(dx*dx + dy*dy + dz*dz); } // Returns true if the world position is in front of the camera (depth >= 0.65). // Uses the same depth calculation as WorldToScreen so the threshold is consistent. // Call this before WorldToScreen to skip the full projection for behind-camera entities. static bool IsFacingCamera(const CameraData& cam, const Vector3& pos) { float tx = pos.x - cam.translation.x; float ty = pos.y - cam.translation.y; float tz = pos.z - cam.translation.z; float depth = tx*cam.invertedViewForward.x + ty*cam.invertedViewForward.y + tz*cam.invertedViewForward.z; return depth >= 0.65f; } bool GameOverlay::WorldToScreen(const CameraData& cam, const Vector3& world, float& sx, float& sy, float w, float h) { float tx = world.x - cam.translation.x; float ty = world.y - cam.translation.y; float tz = world.z - cam.translation.z; float x = tx*cam.invertedViewRight.x + ty*cam.invertedViewRight.y + tz*cam.invertedViewRight.z; float y = tx*cam.invertedViewUp.x + ty*cam.invertedViewUp.y + tz*cam.invertedViewUp.z; float z = tx*cam.invertedViewForward.x + ty*cam.invertedViewForward.y + tz*cam.invertedViewForward.z; if (z < 0.65f) return false; if (cam.projectionD1x == 0.0f || cam.projectionD2y == 0.0f) return false; float nx = (x / cam.projectionD1x) / z; float ny = (y / cam.projectionD2y) / z; sx = roundf((w * 0.5f) + nx * (w * 0.5f)); sy = roundf((h * 0.5f) - ny * (h * 0.5f)); return sx >= 0.0f && sx <= w && sy >= 0.0f && sy <= h; } // Viewport-aware projection. // For stretch-to-fill (or when no render res is configured) the viewport // equals the full overlay — identical to calling WorldToScreen directly. // For maintain-aspect-ratio with render != display, computes the inset // viewport (letterbox top/bottom or pillarbox left/right) and offsets the // result so entities land on the correct monitor pixels. bool GameOverlay::Proj(const CameraData& cam, const Vector3& worldPos, float& sx, float& sy, float overlayW, float overlayH) const { float vx = 0.0f, vy = 0.0f, vw = overlayW, vh = overlayH; if (m_renderW > 0 && m_renderH > 0 && !m_stretchToFill) { float renderAr = static_cast(m_renderW) / static_cast(m_renderH); float displayAr = overlayW / overlayH; if (renderAr > displayAr + 0.01f) { // Render is wider than display → letterbox (black bars top/bottom) vw = overlayW; vh = overlayW / renderAr; vy = (overlayH - vh) * 0.5f; } else if (displayAr > renderAr + 0.01f) { // Display is wider than render → pillarbox (black bars left/right) vh = overlayH; vw = overlayH * renderAr; vx = (overlayW - vw) * 0.5f; } } // stretch-to-fill: vx=0, vy=0, vw=overlayW, vh=overlayH — no change if (!WorldToScreen(cam, worldPos, sx, sy, vw, vh)) return false; sx += vx; sy += vy; return sx >= 0.0f && sx <= overlayW && sy >= 0.0f && sy <= overlayH; } // ------------------------------------------------------------------------- // Draw — entry point called each ImGui frame // ------------------------------------------------------------------------- void GameOverlay::SyncConfig() { m_cfg.showPlayers = m_showPlayers; m_cfg.showAnimals = m_showAnimals; m_cfg.showZombies = m_showZombies; m_cfg.showItems = m_showItems; m_cfg.showBox = m_showBox; m_cfg.showSkeleton = m_showSkeleton; m_cfg.showHeadDot = m_showHeadDot; m_cfg.showCorpses = m_showCorpses; m_cfg.showWeapon = m_showWeapon; m_cfg.showHealthBar = m_showHealthBar; m_cfg.showHealthNumber = m_showHealthNumber; m_cfg.itemCategories = m_itemCategories; m_cfg.playerMaxDist = m_playerMaxDist; m_cfg.animalMaxDist = m_animalMaxDist; m_cfg.zombieMaxDist = m_zombieMaxDist; m_cfg.itemMaxDist = m_itemMaxDist; m_cfg.renderWidth = m_renderW; m_cfg.renderHeight = m_renderH; m_cfg.stretchToFill = m_stretchToFill; } void GameOverlay::Draw(float w, float h) { // Refresh snapshot once per frame — shared_ptr copy is 8 bytes, no vector alloc m_snapshot = m_service.GetLatestUpdate(); if (!m_snapshot) return; const RuntimeUpdate& u = *m_snapshot; // Record frame time once; used by all bone-smoothing calls this frame. m_frameTimeMs = NowMs(); // Feed new DMA bone reads into the per-entity history so extrapolation // always has the two most recent samples to work from. for (const auto& p : u.players) if (p.skeleton.valid) UpdateBoneHistory(m_playerBoneHistory[p.address], p.skeleton, m_frameTimeMs); for (const auto& z : u.zombies) if (z.skeleton.valid) UpdateBoneHistory(m_zombieBoneHistory[z.address], z.skeleton, m_frameTimeMs); // Prune bone histories for entities no longer present in the snapshot. // Prevents ghost extrapolation when a player logs out or leaves the scan zone: // their history is cleared immediately so stale bones can't persist. { std::unordered_set livePlayers, liveZombies; livePlayers.reserve(u.players.size()); liveZombies.reserve(u.zombies.size()); for (const auto& p : u.players) livePlayers.insert(p.address); for (const auto& z : u.zombies) liveZombies.insert(z.address); for (auto it = m_playerBoneHistory.begin(); it != m_playerBoneHistory.end(); ) it = livePlayers.count(it->first) ? std::next(it) : m_playerBoneHistory.erase(it); for (auto it = m_zombieBoneHistory.begin(); it != m_zombieBoneHistory.end(); ) it = liveZombies.count(it->first) ? std::next(it) : m_zombieBoneHistory.erase(it); } // INSERT key toggles the menu if (GetAsyncKeyState(VK_INSERT) & 1) { m_menuOpen = !m_menuOpen; m_menuAlpha = m_menuOpen ? 1.0f : 0.0f; if (!m_menuOpen) { // Persist settings back to config on close. SyncConfig(); m_cfg.Save(m_cfgPath); } } // ---- Lumin menu (vendored under external/lumin; driven via MenuBridge) ---- if (m_menuOpen) { static MenuBridge bridge; bridge.showPlayers = &m_showPlayers; bridge.showAnimals = &m_showAnimals; bridge.showZombies = &m_showZombies; bridge.showItems = &m_showItems; bridge.showBox = &m_showBox; bridge.showSkeleton = &m_showSkeleton; bridge.showHeadDot = &m_showHeadDot; bridge.showWeapon = &m_showWeapon; bridge.showHealthBar = &m_showHealthBar; bridge.showHealthNumber = &m_showHealthNumber; bridge.showCorpses = &m_showCorpses; bridge.debugSkeleton = &m_debugSkeleton; bridge.playerMaxDist = &m_playerMaxDist; bridge.animalMaxDist = &m_animalMaxDist; bridge.zombieMaxDist = &m_zombieMaxDist; bridge.itemMaxDist = &m_itemMaxDist; bridge.itemCategories = &m_itemCategories; bridge.pendingW = &m_pendingW; bridge.pendingH = &m_pendingH; bridge.pendingRW = &m_pendingRW; bridge.pendingRH = &m_pendingRH; bridge.stretchToFill = &m_stretchToFill; // Read-only info panels. bridge.connected = u.areBaseObjectsReady; bridge.serverName = u.serverName.value_or(""); bridge.mapName = u.serverMapName.value_or(""); bridge.status = u.status; if (u.localPlayerPosition.has_value()) { bridge.hasPos = true; bridge.px = u.localPlayerPosition->x; bridge.py = u.localPlayerPosition->y; bridge.pz = u.localPlayerPosition->z; } else { bridge.hasPos = false; } bridge.nPlayers = u.players.size(); bridge.nAnimals = u.animals.size(); bridge.nZombies = u.zombies.size(); bridge.nVehicles = u.carsAndBoats.size(); bridge.nItems = u.items.size(); bridge.nBullets = u.bullets.size(); bridge.webPort = m_webPort; bridge.webUrls = m_webUrls; bridge.onSaveConfig = [this]() { SyncConfig(); m_cfg.Save(m_cfgPath); }; bridge.onExit = [this]() { SyncConfig(); m_cfg.Save(m_cfgPath); if (m_exitCallback) m_exitCallback(); }; bridge.onApplyDisplayRes = [this]() { m_cfg.overlayWidth = m_pendingW; m_cfg.overlayHeight = m_pendingH; m_cfg.Save(m_cfgPath); if (m_resizeCallback) m_resizeCallback(m_pendingW, m_pendingH); }; bridge.onApplyRenderRes = [this]() { m_renderW = m_pendingRW; m_renderH = m_pendingRH; m_cfg.renderWidth = m_pendingRW; m_cfg.renderHeight = m_pendingRH; m_cfg.stretchToFill = m_stretchToFill; m_cfg.Save(m_cfgPath); }; g_menu = &bridge; RenderLuminMenu(); } // ---- ESP draw lists — camera read directly from service every frame ---- m_service.GetLatestCamera(m_liveCamera); DrawESP(w, h, u, m_liveCamera); } void GameOverlay::DrawESP(float w, float h, const RuntimeUpdate& u, const CameraData& cam) { if (!u.areBaseObjectsReady || !cam.valid) return; ImDrawList* dl = ImGui::GetBackgroundDrawList(); if (m_showPlayers) DrawPlayers(dl, u, cam, w, h); if (m_showAnimals) DrawAnimals(dl, u, cam, w, h); if (m_showZombies) DrawZombies(dl, u, cam, w, h); if (m_showItems) DrawItems (dl, u, cam, w, h); if (m_debugSkeleton) DrawSkeletonDebug(dl, u, cam, w, h); } // ------------------------------------------------------------------------- // DrawSkeleton — render 11 bone segments for one entity // ------------------------------------------------------------------------- void GameOverlay::DrawSkeleton(ImDrawList* dl, const SkeletonBones& bones, const CameraData& cam, float w, float h, unsigned int color, bool isZombie) const { // Project each unique bone ONCE, then draw segments from cached screen coords. // Old approach called WorldToScreen twice per segment; shared bones (neck, spine) // were projected 3-4 times each. 16 projections replaces 28-30. enum : int { B_Neck=0, B_Head, B_Spine, B_Pelvis, B_RShoulder, B_RElbow, B_RHand, B_LShoulder, B_LElbow, B_LHand, B_RHip, B_RKnee, B_RAnkle, B_LHip, B_LKnee, B_LAnkle, B_COUNT }; const Vector3* const kWorld[B_COUNT] = { &bones.neck, &bones.head, &bones.spine, &bones.pelvis, &bones.rightShoulder, &bones.rightElbow, &bones.rightHand, &bones.leftShoulder, &bones.leftElbow, &bones.leftHand, &bones.rightHip, &bones.rightKnee, &bones.rightAnkle, &bones.leftHip, &bones.leftKnee, &bones.leftAnkle, }; struct SP { float x, y; bool ok; } scr[B_COUNT]; for (int i = 0; i < B_COUNT; ++i) scr[i].ok = Proj(cam, *kWorld[i], scr[i].x, scr[i].y, w, h); struct Seg { int8_t a, b; }; static const Seg kPlayerSegs[14] = { {B_Neck,B_Head}, {B_Neck,B_RShoulder},{B_RShoulder,B_RElbow},{B_RElbow,B_RHand}, {B_Neck,B_LShoulder},{B_LShoulder,B_LElbow},{B_LElbow,B_LHand}, {B_Neck,B_Spine}, {B_Spine,B_RHip},{B_RHip,B_RKnee},{B_RKnee,B_RAnkle}, {B_Spine,B_LHip},{B_LHip,B_LKnee},{B_LKnee,B_LAnkle}, }; static const Seg kZombieSegs[15] = { {B_Spine,B_Neck},{B_Neck,B_Head}, {B_Spine,B_LShoulder},{B_LShoulder,B_LElbow},{B_LElbow,B_LHand}, {B_Spine,B_RShoulder},{B_RShoulder,B_RElbow},{B_RElbow,B_RHand}, {B_Spine,B_Pelvis}, {B_Pelvis,B_RHip},{B_RHip,B_RKnee},{B_RKnee,B_RAnkle}, {B_Pelvis,B_LHip},{B_LHip,B_LKnee},{B_LKnee,B_LAnkle}, }; const Seg* segs = isZombie ? kZombieSegs : kPlayerSegs; const int count = isZombie ? 15 : 14; for (int i = 0; i < count; ++i) { const SP& a = scr[segs[i].a]; const SP& b = scr[segs[i].b]; if (!a.ok || !b.ok) continue; dl->AddLine(ImVec2(a.x, a.y), ImVec2(b.x, b.y), color, 1.5f); } } // ------------------------------------------------------------------------- // DrawSkeletonDebug — named bone dots for the closest player // ------------------------------------------------------------------------- void GameOverlay::DrawSkeletonDebug(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) const { // Find the closest player that has skeleton data (valid or not). const DayZPlayerEntry* target = nullptr; float bestDist = 1e9f; for (const auto& p : u.players) { if (!p.position.has_value()) continue; float d = Dist(cam.translation, *p.position); if (d < 2.0f || d > m_playerMaxDist) continue; if (d < bestDist) { bestDist = d; target = &p; } } if (!target) return; // Top-left status block. const float pad = 10.0f; const float lineH = 16.0f; float tx = pad, ty = pad; auto txt = [&](ImU32 col, std::string s) { dl->AddText(ImVec2(tx, ty), col, s.c_str()); ty += lineH; }; const char* name = target->nickname.empty() ? "Player" : target->nickname.c_str(); txt(IM_COL32(255,255,255,220), std::format("SkelDebug: {} | dist={:.1f}m", name, bestDist)); txt(IM_COL32(200,200,200,180), std::format("skeleton.valid = {}", target->skeleton.valid ? "true" : "false")); if (!target->skeleton.valid) { txt(IM_COL32(255,100,100,200), "No valid skeleton data -- check offsets"); return; } // Named bones with distinct colors — mirrors Spectre stable player bone layout. struct BoneDef { const char* name; const Vector3& pos; ImU32 color; }; const SkeletonBones& sk = target->skeleton; const BoneDef bones[] = { { "neck", sk.neck, IM_COL32(255,100, 0,255) }, { "head", sk.head, IM_COL32(255, 60, 60,255) }, { "spine", sk.spine, IM_COL32(255,165, 0,255) }, { "rShoulder", sk.rightShoulder, IM_COL32( 80,255,180,255) }, { "rElbow", sk.rightElbow, IM_COL32( 50,220,140,255) }, { "rHand", sk.rightHand, IM_COL32( 30,200,120,255) }, { "lShoulder", sk.leftShoulder, IM_COL32( 80,200,255,255) }, { "lElbow", sk.leftElbow, IM_COL32( 50,160,230,255) }, { "lHand", sk.leftHand, IM_COL32( 30,120,255,255) }, { "rHip", sk.rightHip, IM_COL32(255,255, 0,255) }, { "rKnee", sk.rightKnee, IM_COL32(220,220, 0,255) }, { "rAnkle", sk.rightAnkle, IM_COL32(180,180, 0,255) }, { "lHip", sk.leftHip, IM_COL32(200,100,255,255) }, { "lKnee", sk.leftKnee, IM_COL32(170, 80,230,255) }, { "lAnkle", sk.leftAnkle, IM_COL32(140, 60,200,255) }, }; int projected = 0; for (const auto& b : bones) { float sx{}, sy{}; bool onScreen = Proj(cam, b.pos, sx, sy, w, h); txt(b.color, std::format(" {} ({:.1f},{:.1f},{:.1f}) {}", b.name, b.pos.x, b.pos.y, b.pos.z, onScreen ? "" : "[off]")); if (!onScreen) continue; ++projected; dl->AddCircleFilled(ImVec2(sx, sy), 4.0f, b.color); dl->AddCircle(ImVec2(sx, sy), 4.0f, IM_COL32(0,0,0,200), 0, 1.5f); dl->AddText(ImVec2(sx + 7.0f, sy - 6.0f), b.color, b.name); } txt(IM_COL32(180,255,180,200), std::format(" {}/17 bones on screen", projected)); } // ------------------------------------------------------------------------- // Players — red bounding box + name + held item + distance // ------------------------------------------------------------------------- void GameOverlay::DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) { for (const auto& p : u.players) { if (p.isDead && !m_showCorpses) continue; if (!p.position.has_value()) continue; // Skip the local player by address — more reliable than proximity check. if (u.localPlayerEntityAddress.has_value() && p.address == *u.localPlayerEntityAddress) continue; const Vector3& pos = *p.position; float dist = Dist(cam.translation, pos); if (dist > m_playerMaxDist) continue; if (!IsFacingCamera(cam, pos)) continue; const ImU32 kColor = p.isAdmin ? IM_COL32(255, 200, 0, 255) // gold : p.isDead ? IM_COL32(120, 120, 120, 160) // grey : IM_COL32(255, 60, 60, 220); // red // ---- Smoothed skeleton (extrapolated between DMA reads) ---- SkeletonBones smoothBones{}; bool hasSmoothBones = false; if (!p.isDead) { auto hit = m_playerBoneHistory.find(p.address); if (hit != m_playerBoneHistory.end() && hit->second.initialized && (m_frameTimeMs - hit->second.currMs) < 500LL) { smoothBones = GetSmoothedBones(hit->second, m_frameTimeMs); hasSmoothBones = true; } } // ---- Bounding box ---- // When the skeleton is valid, derive the box from the projected extents of // all 16 bones. This keeps the box perfectly in sync with the skeleton // (both come from the bone-scatter data, updated at ~60 Hz) instead of // lagging one update behind because it uses the entity position (50 Hz). float bx0 = 0.0f, by0 = 0.0f, bx1 = 0.0f, by1 = 0.0f; bool boxReady = false; if (hasSmoothBones) { const Vector3* const kBones[] = { &smoothBones.neck, &smoothBones.head, &smoothBones.spine, &smoothBones.pelvis, &smoothBones.rightShoulder, &smoothBones.rightElbow, &smoothBones.rightHand, &smoothBones.leftShoulder, &smoothBones.leftElbow, &smoothBones.leftHand, &smoothBones.rightHip, &smoothBones.rightKnee, &smoothBones.rightAnkle, &smoothBones.leftHip, &smoothBones.leftKnee, &smoothBones.leftAnkle, }; float minX = 1e9f, maxX = -1e9f; float minY = 1e9f, maxY = -1e9f; int hits = 0; for (const auto* bp : kBones) { // Reject bones whose world position is > 3 m from the entity root. // Garbage reads (zero-initialised bones, failed scatter reads) land // at or near world origin and would drag the box hundreds of pixels // off to the side of the actual player. float dbx = bp->x - pos.x, dby = bp->y - pos.y, dbz = bp->z - pos.z; if (dbx*dbx + dby*dby + dbz*dbz > 9.0f) continue; float bsx{}, bsy{}; if (!Proj(cam, *bp, bsx, bsy, w, h)) continue; if (bsx < minX) minX = bsx; if (bsx > maxX) maxX = bsx; if (bsy < minY) minY = bsy; if (bsy > maxY) maxY = bsy; ++hits; } if (hits >= 4) { constexpr float kPad = 4.0f; bx0 = minX - kPad; by0 = minY - kPad; bx1 = maxX + kPad; by1 = maxY + kPad; boxReady = true; } } if (!boxReady) { // Fallback: entity ground position + hardcoded 1.8 m head. // Used for corpses (no skeleton) and when bones haven't been read yet. float fx{}, fy{}, hx{}, hy{}; if (!Proj(cam, pos, fx, fy, w, h)) continue; Vector3 headPos{ pos.x, pos.y + 1.8f, pos.z }; if (!Proj(cam, headPos, hx, hy, w, h)) continue; float bH = fy - hy; if (bH < 2.0f) continue; float bW = bH * 0.4f; bx0 = hx - bW * 0.5f; by0 = hy; bx1 = hx + bW * 0.5f; by1 = fy; } // ---- Head circle (computed before the box so by0 can be adjusted) ---- float hbx{}, hby{}; float headRadius = 0.0f; bool hasHeadCircle = false; if (m_showHeadDot && !p.isDead && hasSmoothBones) { if (Proj(cam, smoothBones.head, hbx, hby, w, h)) { // Radius = 15% of the rendered body height so the circle scales // naturally with distance. The head-neck pixel-distance approach // collapses to minimum past ~30 m; box-height fraction stays // proportional to the target at any range. headRadius = std::clamp((by1 - by0) * 0.15f, 2.0f, 18.0f); hasHeadCircle = true; by0 = std::min(by0, hby - headRadius - 2.0f); } } if (by1 - by0 < 2.0f) continue; if (m_showBox) dl->AddRect(ImVec2(bx0, by0), ImVec2(bx1, by1), kColor, 0.0f, 0, 1.5f); // ---- Skeleton + head circle (use extrapolated bones) ---- if (!p.isDead && hasSmoothBones) { if (m_showSkeleton) DrawSkeleton(dl, smoothBones, cam, w, h, kColor, false); if (hasHeadCircle) { dl->AddCircleFilled(ImVec2(hbx, hby), headRadius, IM_COL32(0, 0, 0, 80)); dl->AddCircle(ImVec2(hbx, hby), headRadius, kColor, 0, 1.5f); } } // ---- Name + distance label (top-right of box) ---- char lblBuf[256]; const char* nick = p.nickname.empty() ? "Player" : p.nickname.c_str(); if (p.isAdmin && p.isDead) snprintf(lblBuf, sizeof(lblBuf), "[ADMIN] %s [DEAD] [%.0fm]", nick, dist); else if (p.isAdmin) snprintf(lblBuf, sizeof(lblBuf), "[ADMIN] %s [%.0fm]", nick, dist); else if (p.isDead) snprintf(lblBuf, sizeof(lblBuf), "%s [DEAD] [%.0fm]", nick, dist); else snprintf(lblBuf, sizeof(lblBuf), "%s [%.0fm]", nick, dist); dl->AddText(ImVec2(bx1 + 3.0f, by0), kColor, lblBuf); // ---- Right-side extras (weapon, health bar, health number) ---- const float kBarW = 4.0f; const float kBarGap = 5.0f; const float barX = bx1 + kBarGap; const float boxH = by1 - by0; if (m_showWeapon && !p.isDead && !p.itemInHands.empty()) { constexpr ImU32 kWeaponColor = IM_COL32(220, 220, 100, 200); dl->AddText(ImVec2(barX, by0 + 13.0f), kWeaponColor, p.itemInHands.c_str()); } const bool hasHealth = (p.health >= 0.0f); if ((m_showHealthBar || m_showHealthNumber) && !p.isDead && hasHealth) { const float hpFrac = std::min(p.health / 100.0f, 1.0f); if (m_showHealthBar) { dl->AddRectFilled( ImVec2(barX, by0), ImVec2(barX + kBarW, by1), IM_COL32(0, 0, 0, 140)); float r = hpFrac < 0.5f ? 1.0f : 2.0f * (1.0f - hpFrac); float g = hpFrac > 0.5f ? 1.0f : 2.0f * hpFrac; ImU32 hpColor = IM_COL32( static_cast(r * 220), static_cast(g * 220), 0, 220); float filledTop = by0 + boxH * (1.0f - hpFrac); dl->AddRectFilled( ImVec2(barX, filledTop), ImVec2(barX + kBarW, by1), hpColor); } if (m_showHealthNumber) { char hpBuf[32]; snprintf(hpBuf, sizeof(hpBuf), "%.0f/100", p.health); float numX = m_showHealthBar ? barX + kBarW + 2.0f : barX; dl->AddText(ImVec2(numX, by1 - 12.0f), IM_COL32(200, 200, 200, 200), hpBuf); } } } } // ------------------------------------------------------------------------- // Animals — green label // ------------------------------------------------------------------------- void GameOverlay::DrawAnimals(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) { constexpr ImU32 kGreen = IM_COL32(50, 230, 50, 200); for (const auto& a : u.animals) { if (!a.position.has_value()) continue; float dist = Dist(cam.translation, *a.position); if (dist > m_animalMaxDist) continue; if (!IsFacingCamera(cam, *a.position)) continue; float sx{}, sy{}; if (!Proj(cam, *a.position, sx, sy, w, h)) continue; std::string raw = a.entityName.empty() ? a.typeName : a.entityName; std::string fmtName = raw.empty() ? "Animal" : FormatEntityName(raw); char abuf[256]; snprintf(abuf, sizeof(abuf), "%s [%.0fm]", fmtName.c_str(), dist); dl->AddText(ImVec2(sx, sy), kGreen, abuf); } } // ------------------------------------------------------------------------- // Zombies — yellow label // ------------------------------------------------------------------------- void GameOverlay::DrawZombies(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) { constexpr ImU32 kYellow = IM_COL32(230, 230, 50, 180); for (const auto& z : u.zombies) { // Resolve position: use fresh read if available, fall back to cache. Vector3 pos; if (z.position.has_value()) { pos = *z.position; m_zombieLastPos[z.address] = pos; } else { auto it = m_zombieLastPos.find(z.address); if (it == m_zombieLastPos.end()) continue; pos = it->second; } float dist = Dist(cam.translation, pos); if (dist > m_zombieMaxDist) continue; if (!IsFacingCamera(cam, pos)) continue; float sx{}, sy{}; if (!Proj(cam, pos, sx, sy, w, h)) continue; auto zhit = m_zombieBoneHistory.find(z.address); if (zhit != m_zombieBoneHistory.end() && zhit->second.initialized && (m_frameTimeMs - zhit->second.currMs) < 500LL) { SkeletonBones zsm = GetSmoothedBones(zhit->second, m_frameTimeMs); if (m_showSkeleton) DrawSkeleton(dl, zsm, cam, w, h, kYellow, true); if (m_showHeadDot) { float hbx{}, hby{}; if (Proj(cam, zsm.head, hbx, hby, w, h)) { // Angular size of a ~25 cm head at the zombie's actual distance. // Scales from ~18px at 5m down to ~3px at 50m; clamp keeps it // always visible and never massive. const float projD2y = std::max(cam.projectionD2y, 0.1f); float zr = std::clamp((0.25f * h * 0.5f) / (dist * projD2y), 2.0f, 18.0f); dl->AddCircleFilled(ImVec2(hbx, hby), zr, IM_COL32(0, 0, 0, 80)); dl->AddCircle(ImVec2(hbx, hby), zr, kYellow, 0, 1.5f); } } } char zbuf[32]; snprintf(zbuf, sizeof(zbuf), "Z [%.0fm]", dist); dl->AddText(ImVec2(sx, sy), kYellow, zbuf); } } // ------------------------------------------------------------------------- // Items — white label // ------------------------------------------------------------------------- void GameOverlay::DrawItems(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) { constexpr ImU32 kWhite = IM_COL32(255, 255, 255, 200); const float maxDistSq = m_itemMaxDist * m_itemMaxDist; for (const auto& item : u.items) { // Per-category filter (missing key = enabled) if (!item.filterKey.empty()) { auto it = m_itemCategories.find(item.filterKey); if (it != m_itemCategories.end() && !it->second) continue; } if (!item.position.has_value()) continue; // Squared-distance cull avoids sqrtf for every item. float dx = item.position->x - cam.translation.x; float dy = item.position->y - cam.translation.y; float dz = item.position->z - cam.translation.z; float distSq = dx*dx + dy*dy + dz*dz; if (distSq > maxDistSq) continue; if (!IsFacingCamera(cam, *item.position)) continue; float sx{}, sy{}; if (!Proj(cam, *item.position, sx, sy, w, h)) continue; const std::string& name = item.cleanName.empty() ? item.entityName : item.cleanName; if (name.empty()) continue; float dist = std::sqrtf(distSq); // sqrt only for items that will actually be drawn // Two-tier font: larger close up, smaller at range. char ibuf[256]; snprintf(ibuf, sizeof(ibuf), "%s [%.0fm]", name.c_str(), dist); ImFont* font = (dist < 50.0f) ? m_fontLootClose : m_fontLootFar; if (font) ImGui::PushFont(font); dl->AddText(ImVec2(sx, sy), kWhite, ibuf); if (font) ImGui::PopFont(); } }