Initial commit: DayZ memory C++ port with DMA backend and overlay
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user