Initial commit: DayZ memory C++ port with DMA backend and overlay

This commit is contained in:
67
2026-06-16 15:18:44 +08:00
commit f04e38b8ae
163 changed files with 163380 additions and 0 deletions
+850
View File
@@ -0,0 +1,850 @@
#include "GameOverlay.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <format>
#include <string>
#include <unordered_set>
#include <vector>
#include "Readers/EntityTypeCache.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
#include <imgui.h>
#include <imgui_internal.h>
#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<milliseconds>(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<float>(nowMs - h.prevMs) / static_cast<float>(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<BYTE> buf(bufLen);
auto* table = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(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<IP_ADAPTER_ADDRESSES*>(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<sockaddr_in*>(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<float>(m_renderW) / static_cast<float>(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<uint64_t> 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<int>(r * 220),
static_cast<int>(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();
}
}
+190
View File
@@ -0,0 +1,190 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <imgui.h>
#include "Config.h"
#include "Runtime/DayZRuntimeService.h"
struct ImDrawList;
class GameOverlay {
public:
// Exposed so static helpers in GameOverlay.cpp can take it by reference.
struct BoneHistory {
SkeletonBones prev{};
SkeletonBones curr{};
int64_t prevMs = 0;
int64_t currMs = 0;
bool initialized = false;
};
explicit GameOverlay(DayZRuntimeService& service,
OverlayConfig cfg,
std::string cfgPath)
: m_service(service)
, m_cfg(std::move(cfg))
, m_cfgPath(std::move(cfgPath))
{
m_showPlayers = m_cfg.showPlayers;
m_showAnimals = m_cfg.showAnimals;
m_showZombies = m_cfg.showZombies;
m_showItems = m_cfg.showItems;
m_showBox = m_cfg.showBox;
m_showSkeleton = m_cfg.showSkeleton;
m_showHeadDot = m_cfg.showHeadDot;
m_showCorpses = m_cfg.showCorpses;
m_showWeapon = m_cfg.showWeapon;
m_showHealthBar = m_cfg.showHealthBar;
m_showHealthNumber = m_cfg.showHealthNumber;
m_itemCategories = m_cfg.itemCategories;
m_playerMaxDist = m_cfg.playerMaxDist;
m_animalMaxDist = m_cfg.animalMaxDist;
m_zombieMaxDist = m_cfg.zombieMaxDist;
m_itemMaxDist = m_cfg.itemMaxDist;
m_pendingW = m_cfg.overlayWidth;
m_pendingH = m_cfg.overlayHeight;
m_renderW = m_cfg.renderWidth;
m_renderH = m_cfg.renderHeight;
m_stretchToFill = m_cfg.stretchToFill;
m_pendingRW = m_cfg.renderWidth;
m_pendingRH = m_cfg.renderHeight;
}
/// Called once after the web radar server has started so the Radar tab
/// knows what URL(s) to display.
void SetWebRadarPort(int port);
/// Called once after ImGui fonts are built so DrawItems can tier by distance.
void SetLootFonts(ImFont* close, ImFont* lootFar) {
m_fontLootClose = close;
m_fontLootFar = lootFar;
}
/// Register a callback invoked when the user clicks the exit button.
/// The caller should set its stop flag inside the callback.
void SetExitCallback(std::function<void()> cb) { m_exitCallback = std::move(cb); }
/// Register a callback invoked when the user applies a new overlay resolution.
void SetResizeCallback(std::function<void(int,int)> cb) { m_resizeCallback = std::move(cb); }
// Called each frame. w/h a re the overlay window pixel dimensions.
void Draw(float w, float h);
private:
DayZRuntimeService& m_service;
OverlayConfig m_cfg;
std::string m_cfgPath;
// Cached entity snapshot (updated once per frame via GetLatestUpdate).
std::shared_ptr<const RuntimeUpdate> m_snapshot;
public:
bool IsMenuOpen() const { return m_menuOpen; }
private:
// Live camera — updated every frame directly from the service,
// bypassing the entity snapshot so it reflects the latest DMA read.
CameraData m_liveCamera;
// Web radar
int m_webPort = 7777;
std::vector<std::string> m_webUrls; // populated by SetWebRadarPort()
// Exit callback — invoked when the user presses the exit button.
std::function<void()> m_exitCallback;
// Resize callback — invoked when the user applies a new resolution.
std::function<void(int, int)> m_resizeCallback;
// Menu state
bool m_menuOpen = false;
float m_menuAlpha = 0.0f;
int m_tab = 0;
int m_subtab = 0;
// ESP toggles (initialised from config in constructor)
bool m_showPlayers = true;
bool m_showAnimals = true;
bool m_showZombies = true;
bool m_showItems = true;
bool m_showBox = true;
bool m_showSkeleton = true;
bool m_showHeadDot = false;
bool m_showCorpses = false;
bool m_showWeapon = true;
bool m_showHealthBar = true;
bool m_showHealthNumber = false;
bool m_debugSkeleton = false; // draws named bone dots for the closest player
// Per-category item enabled map (key = filterKey, missing = enabled)
std::map<std::string, bool> m_itemCategories;
// Loot fonts — set once via SetLootFonts() after ImGui atlas is built.
ImFont* m_fontLootClose = nullptr; // 16 px — used for items within 50 m
ImFont* m_fontLootFar = nullptr; // 11 px — used for items >= 50 m
// Last-known positions for zombies keyed by entity address.
// Used to avoid a single failed position read causing a visible blink.
std::unordered_map<uint64_t, Vector3> m_zombieLastPos;
// ---- Bone interpolation / extrapolation ----
std::unordered_map<uint64_t, BoneHistory> m_playerBoneHistory;
std::unordered_map<uint64_t, BoneHistory> m_zombieBoneHistory;
int64_t m_frameTimeMs = 0; // set once per Draw() call
static int64_t NowMs();
// Pending resolution override — edited in menu, applied on button press.
int m_pendingW = 0;
int m_pendingH = 0;
int m_pendingRW = 0; // render width (stretched res)
int m_pendingRH = 0; // render height (stretched res)
// Stretched resolution state (active values, applied on "Apply")
int m_renderW = 0;
int m_renderH = 0;
bool m_stretchToFill = true;
// ESP distance limits (initialised from config in constructor)
float m_playerMaxDist = 1000.0f;
float m_animalMaxDist = 1000.0f;
float m_zombieMaxDist = 500.0f;
float m_itemMaxDist = 200.0f;
// Low-level NDC projection helper — maps a world position to overlay pixels.
// Returns false if the position is behind the camera or off-screen.
static bool WorldToScreen(const CameraData& cam,
const Vector3& worldPos,
float& sx, float& sy,
float w, float h);
// Viewport-aware projection wrapper. Use this instead of WorldToScreen
// directly — it applies stretched-resolution / letterbox offsets.
bool Proj(const CameraData& cam, const Vector3& worldPos,
float& sx, float& sy, float overlayW, float overlayH) const;
static float Dist(const Vector3& a, const Vector3& b);
// Copy live menu state (m_show*, distances, item categories, resolution)
// back into m_cfg prior to a Save.
void SyncConfig();
void DrawESP (float w, float h, const RuntimeUpdate& u, const CameraData& cam);
void DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
void DrawAnimals(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
void DrawZombies(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
void DrawItems (ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
// Draw skeleton bone segments for a single entity. color = ImU32 (unsigned int).
void DrawSkeleton(ImDrawList* dl, const SkeletonBones& bones,
const CameraData& cam, float w, float h,
unsigned int color, bool isZombie) const;
// Debug: draw every named bone as a labeled dot for the closest player.
void DrawSkeletonDebug(ImDrawList* dl, const RuntimeUpdate& u,
const CameraData& cam, float w, float h) const;
};
+77
View File
@@ -0,0 +1,77 @@
#pragma once
// -------------------------------------------------------------------------
// MenuBridge — the seam between the vendored Lumin ImGui menu (external/lumin)
// and this project's overlay state (GameOverlay).
//
// GameOverlay fills one of these every frame (pointers into its own members
// for the editable values, plain values for the read-only info panels) and
// the menu's render() reads/writes through the global pointer below. This
// keeps all the actual ESP state owned by GameOverlay while letting Lumin's
// widgets drive it.
// -------------------------------------------------------------------------
#include <cstddef>
#include <functional>
#include <map>
#include <string>
#include <vector>
struct MenuBridge {
// ---- ESP visibility toggles (point at GameOverlay members) ----
bool* showPlayers = nullptr;
bool* showAnimals = nullptr;
bool* showZombies = nullptr;
bool* showItems = nullptr;
bool* showBox = nullptr;
bool* showSkeleton = nullptr;
bool* showHeadDot = nullptr;
bool* showWeapon = nullptr;
bool* showHealthBar = nullptr;
bool* showHealthNumber = nullptr;
bool* showCorpses = nullptr;
bool* debugSkeleton = nullptr;
// ---- ESP draw-distance limits ----
float* playerMaxDist = nullptr;
float* animalMaxDist = nullptr;
float* zombieMaxDist = nullptr;
float* itemMaxDist = nullptr;
// ---- Per-category loot toggles (key = filterKey) ----
std::map<std::string, bool>* itemCategories = nullptr;
// ---- Read-only info (refreshed each frame) ----
bool connected = false;
std::string serverName;
std::string mapName;
std::string status;
bool hasPos = false;
float px = 0.f, py = 0.f, pz = 0.f;
std::size_t nPlayers = 0, nAnimals = 0, nZombies = 0,
nVehicles = 0, nItems = 0, nBullets = 0;
// ---- Web radar ----
int webPort = 7777;
std::vector<std::string> webUrls;
// ---- Resolution settings (point at GameOverlay members) ----
int* pendingW = nullptr;
int* pendingH = nullptr;
int* pendingRW = nullptr;
int* pendingRH = nullptr;
bool* stretchToFill = nullptr;
// ---- Actions ----
std::function<void()> onApplyDisplayRes; // applies pendingW/H + resize
std::function<void()> onApplyRenderRes; // applies pendingRW/RH + stretch
std::function<void()> onSaveConfig;
std::function<void()> onExit;
};
// Set by GameOverlay before each gui->render() call; read by the menu.
extern MenuBridge* g_menu;
// Thin shim implemented in external/lumin/framework/gui.cpp. Lets GameOverlay
// drive the Lumin menu without pulling the whole framework header (and its
// `using namespace ImGui`) into GameOverlay's translation unit. Must be called
// between ImGui::NewFrame() and ImGui::Render().
void RenderLuminMenu();
+296
View File
@@ -0,0 +1,296 @@
#include "OverlayWindow.h"
#include <imgui.h>
#include <imgui_impl_win32.h>
#include <imgui_impl_dx11.h>
// Vendored Lumin framework — provides the FreeType-backed font system (`font`),
// shared state (`var`) and the embedded font data (inter_*, icon_font).
#include "framework/headers/includes.h"
#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")
#include <spdlog/spdlog.h>
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
static constexpr wchar_t kWndClass[] = L"DayZEspWindow";
// ESP fonts go through the Lumin font system so they survive its atlas
// rebuilds. dpi is locked to 1.0 (variables.h) so these map to exact pixels.
static const char* kEspFontPath = "C:\\Windows\\Fonts\\tahoma.ttf";
// Register every font size the Lumin menu can request, so the atlas is built
// up front and (ideally) never rebuilt mid-run. Any size missed here is still
// handled gracefully by the per-frame rebuild check in Run().
static void WarmMenuFonts() {
font->get(inter_medium, 10);
font->get(inter_medium, 11);
font->get(inter_semibold, 8);
font->get(inter_semibold, 10);
font->get(inter_semibold, 11);
font->get(inter_semibold, 12);
font->get(inter_semibold, 13);
font->get(inter_semibold, 16);
font->get(inter_semibold, 18);
font->get(icon_font, 13);
font->get(icon_font, 14);
font->get(icon_font, 15);
font->get_file(flaticon_uicons_regular_rounded_path, 12.0f, true);
font->get_file(flaticon_uicons_regular_rounded_path, 12.5f, true);
font->get_file("C:\\Windows\\Fonts\\segmdl2.ttf", 11.f, true);
}
// -------------------------------------------------------------------------
// WndProc
// -------------------------------------------------------------------------
LRESULT WINAPI OverlayWindow::WndProc(HWND h, UINT msg, WPARAM w, LPARAM l) {
if (ImGui_ImplWin32_WndProcHandler(h, msg, w, l)) return true;
switch (msg) {
case WM_SIZE:
if (w != SIZE_MINIMIZED) {
// Store pending resize; handled at top of render loop
if (auto* self = reinterpret_cast<OverlayWindow*>(GetWindowLongPtrW(h, GWLP_USERDATA))) {
self->m_resizeW = LOWORD(l);
self->m_resizeH = HIWORD(l);
}
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_SYSCOMMAND:
if ((w & 0xFFF0) == SC_KEYMENU) return 0;
break;
}
return DefWindowProcW(h, msg, w, l);
}
// -------------------------------------------------------------------------
// CreateWin32Window
// -------------------------------------------------------------------------
bool OverlayWindow::CreateWin32Window() {
m_w = (m_overrideW > 0) ? m_overrideW : GetSystemMetrics(SM_CXSCREEN);
m_h = (m_overrideH > 0) ? m_overrideH : GetSystemMetrics(SM_CYSCREEN);
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.style = CS_CLASSDC;
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandle(nullptr);
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.lpszClassName = kWndClass;
if (!RegisterClassExW(&wc)) return false;
m_hwnd = CreateWindowExW(
WS_EX_APPWINDOW,
kWndClass, L"KarachiHook",
WS_POPUP | WS_VISIBLE,
0, 0, m_w, m_h,
nullptr, nullptr, wc.hInstance, nullptr
);
if (!m_hwnd) return false;
// Store 'this' so WndProc can find us for resize events
SetWindowLongPtrW(m_hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
ShowWindow(m_hwnd, SW_SHOWDEFAULT);
UpdateWindow(m_hwnd);
return true;
}
// -------------------------------------------------------------------------
// ResizeTo
// -------------------------------------------------------------------------
void OverlayWindow::ResizeTo(int w, int h) {
if (!m_hwnd || w <= 0 || h <= 0) return;
SetWindowPos(m_hwnd, nullptr, 0, 0, w, h,
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
// WM_SIZE will be posted; the render loop picks it up via m_resizeW/m_resizeH.
}
// -------------------------------------------------------------------------
// CreateDX11
// -------------------------------------------------------------------------
bool OverlayWindow::CreateDX11() {
DXGI_SWAP_CHAIN_DESC sd{};
sd.BufferCount = 2;
sd.BufferDesc.Width = 0;
sd.BufferDesc.Height = 0;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = m_hwnd;
sd.SampleDesc.Count = 1;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
const D3D_FEATURE_LEVEL fla[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0 };
D3D_FEATURE_LEVEL fl{};
HRESULT hr = D3D11CreateDeviceAndSwapChain(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0,
fla, 2, D3D11_SDK_VERSION,
&sd, &m_sc, &m_dev, &fl, &m_ctx
);
if (FAILED(hr))
hr = D3D11CreateDeviceAndSwapChain(
nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0,
fla, 2, D3D11_SDK_VERSION,
&sd, &m_sc, &m_dev, &fl, &m_ctx);
return SUCCEEDED(hr);
}
// -------------------------------------------------------------------------
// RTV helpers
// -------------------------------------------------------------------------
void OverlayWindow::CreateRTV() {
ID3D11Texture2D* back = nullptr;
if (SUCCEEDED(m_sc->GetBuffer(0, IID_PPV_ARGS(&back)))) {
m_dev->CreateRenderTargetView(back, nullptr, &m_rtv);
back->Release();
}
}
void OverlayWindow::CleanupRTV() {
if (m_rtv) { m_rtv->Release(); m_rtv = nullptr; }
}
// -------------------------------------------------------------------------
// Destroy
// -------------------------------------------------------------------------
void OverlayWindow::Destroy() {
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
if (ImGui::GetCurrentContext()) ImGui::DestroyContext();
CleanupRTV();
if (m_sc) { m_sc->Release(); m_sc = nullptr; }
if (m_ctx) { m_ctx->Release(); m_ctx = nullptr; }
if (m_dev) { m_dev->Release(); m_dev = nullptr; }
if (m_hwnd){ DestroyWindow(m_hwnd); m_hwnd = nullptr; }
UnregisterClassW(kWndClass, GetModuleHandle(nullptr));
}
// -------------------------------------------------------------------------
// Run
// -------------------------------------------------------------------------
void OverlayWindow::Run(std::atomic<bool>& stopFlag) {
if (!CreateWin32Window() || !CreateDX11()) {
spdlog::error("OverlayWindow: init failed.");
Destroy();
stopFlag.store(true);
return;
}
CreateRTV();
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
ImGui::StyleColorsDark();
// Make window backgrounds semi-transparent so the game is partially visible
ImGui::GetStyle().Colors[ImGuiCol_WindowBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f);
ImGui_ImplWin32_Init(m_hwnd);
ImGui_ImplDX11_Init(m_dev, m_ctx);
// ---- Fonts via the Lumin FreeType atlas (must run after backend init) ----
WarmMenuFonts();
font->get_file(kEspFontPath, 14.f); // ESP base -> io.FontDefault
font->get_file(kEspFontPath, 16.f); // loot close (<50 m)
font->get_file(kEspFontPath, 11.f); // loot far (>=50 m)
var->gui.dpi_changed = true;
font->update(); // build the atlas once
if (ImFont* base = font->get_file(kEspFontPath, 14.f)) io.FontDefault = base;
ImFont* fontLootClose = font->get_file(kEspFontPath, 16.f);
ImFont* fontLootFar = font->get_file(kEspFontPath, 11.f);
// Notify caller that fonts are ready — must be done after the atlas build
// so the ImFont pointers are fully populated.
if (m_onFontReady) m_onFontReady(fontLootClose, fontLootFar);
spdlog::info("ESP window running ({}x{}).", m_w, m_h);
MSG msg{};
bool done = false;
while (!stopFlag.load() && !done) {
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
if (msg.message == WM_QUIT) { done = true; break; }
}
if (done) break;
// Handle deferred resize
if (m_resizeW != 0 && m_resizeH != 0) {
CleanupRTV();
m_sc->ResizeBuffers(0, m_resizeW, m_resizeH, DXGI_FORMAT_UNKNOWN, 0);
m_w = static_cast<int>(m_resizeW);
m_h = static_cast<int>(m_resizeH);
m_resizeW = m_resizeH = 0;
CreateRTV();
}
// The Lumin menu registers font sizes lazily; rebuild the atlas when a
// new one appears (must happen outside a frame) and refresh the cached
// ESP font pointers so they never dangle across a rebuild.
if (var->gui.dpi_changed) {
font->update();
if (ImFont* base = font->get_file(kEspFontPath, 14.f)) io.FontDefault = base;
if (m_onFontReady)
m_onFontReady(font->get_file(kEspFontPath, 16.f),
font->get_file(kEspFontPath, 11.f));
}
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
if (m_draw) m_draw(static_cast<float>(m_w), static_cast<float>(m_h));
// Toggle interactivity: remove WS_EX_NOACTIVATE when the menu is open
// so ImGui can receive mouse/keyboard input, restore it when closed so
// the overlay doesn't steal focus during gameplay.
if (m_queryInput) {
bool wantInput = m_queryInput();
if (wantInput != m_prevWantInput) {
LONG_PTR ex = GetWindowLongPtrW(m_hwnd, GWL_EXSTYLE);
if (wantInput) {
ex &= ~static_cast<LONG_PTR>(WS_EX_NOACTIVATE);
SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, ex);
SetForegroundWindow(m_hwnd);
} else {
ex |= WS_EX_NOACTIVATE;
SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, ex);
}
m_prevWantInput = wantInput;
}
}
ImGui::Render();
// Pure black background
const float clear[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
m_ctx->OMSetRenderTargets(1, &m_rtv, nullptr);
m_ctx->ClearRenderTargetView(m_rtv, clear);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
DwmFlush(); // sync to DWM compositor vblank before present
m_sc->Present(1, 0); // 1 = vsync interval
}
Destroy();
stopFlag.store(true);
}
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <atomic>
#include <functional>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <d3d11.h>
#include <imgui.h>
// Standard ImGui + DX11 window, always-on-top, fullscreen popup.
// The game is visible through the mostly-dark background while ImGui
// draw-list calls render the ESP data on top.
class OverlayWindow {
public:
using DrawFn = std::function<void(float w, float h)>;
using InputQueryFn = std::function<bool()>;
using FontReadyFn = std::function<void(ImFont* lootClose, ImFont* lootFar)>;
OverlayWindow() = default;
~OverlayWindow() = default;
OverlayWindow(const OverlayWindow&) = delete;
OverlayWindow& operator=(const OverlayWindow&) = delete;
void SetDrawCallback(DrawFn fn) { m_draw = std::move(fn); }
void SetInputQueryCallback(InputQueryFn fn) { m_queryInput = std::move(fn); }
// Called once after ImGui fonts are built, before the first frame.
void SetFontReadyCallback(FontReadyFn fn) { m_onFontReady = std::move(fn); }
// Override the window size used at creation (0 = use GetSystemMetrics).
void SetResolutionOverride(int w, int h) { m_overrideW = w; m_overrideH = h; }
// Resize the live window; triggers WM_SIZE which the render loop handles.
void ResizeTo(int w, int h);
// Blocks until stopFlag becomes true or the window is closed.
void Run(std::atomic<bool>& stopFlag);
// Called from WndProc for WM_SIZE
UINT m_resizeW = 0;
UINT m_resizeH = 0;
// Resolution override — set before Run(); 0 = auto from screen metrics.
int m_overrideW = 0;
int m_overrideH = 0;
private:
DrawFn m_draw;
InputQueryFn m_queryInput;
FontReadyFn m_onFontReady;
bool m_prevWantInput = false;
HWND m_hwnd = nullptr;
ID3D11Device* m_dev = nullptr;
ID3D11DeviceContext* m_ctx = nullptr;
IDXGISwapChain* m_sc = nullptr;
ID3D11RenderTargetView* m_rtv = nullptr;
int m_w = 0;
int m_h = 0;
bool CreateWin32Window();
bool CreateDX11();
void CreateRTV();
void CleanupRTV();
void Destroy();
static LRESULT WINAPI WndProc(HWND h, UINT msg, WPARAM w, LPARAM l);
};