WIP: Web radar implementation in progress — currently broken

The web radar component is undergoing refactoring and is non-functional.
Core memory reading and overlay systems remain operational.
This commit is contained in:
67
2026-06-22 16:15:09 +08:00
parent 89bb2c3e38
commit 4f5df4d1c9
24 changed files with 3991 additions and 2495 deletions
+18
View File
@@ -13,3 +13,21 @@ out/
# Keep these specific files tracked
!README.md
!FORMATS.md
!CLAUDE.md
# 3D map data directories (user-supplied / generated)
data/raw/
data/processed/
# WASM engine build
engine/build/
engine/_deps/
# Python dataprep
dataprep/__pycache__/
dataprep/*.egg-info/
dataprep/dayzmap/__pycache__/
.venv/
node_modules/
package-lock.json
+18 -5
View File
@@ -34,9 +34,8 @@ FetchContent_Declare(spdlog
# cpp-httplib — header-only HTTP/1.1 server used by WebRadarServer
FetchContent_Declare(httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
GIT_TAG v0.18.1
GIT_SHALLOW TRUE
URL https://github.com/yhirose/cpp-httplib/archive/refs/tags/v0.18.1.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# stb — single-file image load/write libraries
@@ -88,6 +87,7 @@ file(GLOB_RECURSE PROJECT_SOURCES "src/*.cpp")
# Explicit list ensures new files are picked up without a CMake cache reset.
list(APPEND PROJECT_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/SigScanner/SigScanner.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/Overlay/BoneInterpolator.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/Overlay/OverlayWindow.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/Overlay/GameOverlay.cpp"
)
@@ -116,6 +116,8 @@ set(EMBEDDED_MAPS_CPP "${CMAKE_CURRENT_BINARY_DIR}/EmbeddedMaps.cpp")
set(MAP_IDS chernarusplus livonia namalsk banov deadfall deerisle esseker lux sakhal takistan alteria)
set(MAP_RIDS 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011)
# ── Base map RCDATA embedding ─────────────────────────────────────────────────
set(RC_LINES "")
set(CPP_CASES "")
foreach(MAP_ID MAP_RID IN ZIP_LISTS MAP_IDS MAP_RIDS)
@@ -128,9 +130,9 @@ foreach(MAP_ID MAP_RID IN ZIP_LISTS MAP_IDS MAP_RIDS)
endif()
endforeach()
file(WRITE "${EMBEDDED_MAPS_RC}" "// Auto-generated by CMake do not edit.\n${RC_LINES}")
file(WRITE "${EMBEDDED_MAPS_RC}" "// Auto-generated by CMake - do not edit.\n${RC_LINES}")
file(WRITE "${EMBEDDED_MAPS_CPP}"
"// Auto-generated by CMake do not edit.
"// Auto-generated by CMake - do not edit.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
@@ -284,6 +286,17 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/maps")
)
endif()
# Copy data/processed/ next to the executable so the 3D HTTP routes can serve it.
# Only copy when the directory exists (requires running: py -m dayzmap fakedata).
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data/processed")
add_custom_command(TARGET dayz-memory-cpp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/data/processed"
"$<TARGET_FILE_DIR:dayz-memory-cpp>/data/processed"
COMMENT "Copying processed map data to output directory"
)
endif()
# -------------------------------------------------------------------------
# Compiler definitions
# -------------------------------------------------------------------------
+8
View File
@@ -11,3 +11,11 @@
## Before P2C
- change config to use custom pkl format maybe? then apply base 85 encoding after maybe?
- make backend for p2c? include keyauth db hwid and do drm
## Web Radar UI Overhaul
- Redesign filter panel: collapsible sections, icon-based entity toggles
- Entity list sidebar with live counts and click-to-focus
- Player detail popover on marker click (worn items, health, distance)
- Minimap inset (fixed position, independent zoom level)
- Keyboard shortcuts: G=grid, T=topo, C=combat, P=POIs
- Route/waypoint drawing tool (manual overlay path)
+49 -33
View File
@@ -224,23 +224,26 @@ static void lumin_kv(const char* label, const std::string& value, const c_vec4&
static void render_aim_tab(const visual_widget_filter& w, float section_height)
{
if (!g_menu)
if (!g_menu || !g_menu->cfg)
return;
begin_full_section("Aim Assistance", section_height);
{
lumin_heading("Combat Mode");
w.checkbox("Combat Mode", "Hides loot and animals; shortens bullet trail fade for cleaner combat view", &g_menu->cfg->combatMode);
gui->dummy(c_vec2(0, s_(6)));
lumin_heading("Prediction");
w.checkbox("Ballistic Dot", "Cyan dot showing where to aim to hit centre mass (accounts for gravity + drag)", g_menu->showBallisticDot);
w.checkbox("Ballistic Dot", "Cyan dot showing where to aim to hit centre mass (accounts for gravity + drag)", &g_menu->cfg->showBallisticDot);
gui->dummy(c_vec2(0, s_(6)));
lumin_heading("Trails");
w.checkbox("Bullet Trails", "Draw flight path of each bullet from origin to impact or despawn", g_menu->showBulletTrails);
w.checkbox("Bullet Trails", "Draw flight path of each bullet from origin to impact or despawn", &g_menu->cfg->showBulletTrails);
}
end_visual_section();
}
static void render_visuals_tab(const visual_widget_filter& w, float section_height)
{
if (!g_menu)
if (!g_menu || !g_menu->cfg)
return;
if (var->gui.sub_tab_stored == 1)
@@ -249,14 +252,14 @@ static void render_visuals_tab(const visual_widget_filter& w, float section_heig
gui->begin_group();
{
begin_visual_section("Players", section_height);
w.checkbox("Show Players", "Show player ESP", g_menu->showPlayers);
w.checkbox("Bounding Box", "Draw entity box", g_menu->showBox);
w.checkbox("Skeleton", "Draw bone skeleton", g_menu->showSkeleton);
w.checkbox("Head Circle", "Draw head highlight", g_menu->showHeadDot);
w.checkbox("Weapon In Hand", "Show held weapon name", g_menu->showWeapon);
w.checkbox("Health Bar", "Draw player health bar", g_menu->showHealthBar);
w.checkbox("Health Number", "Draw numeric health", g_menu->showHealthNumber);
w.checkbox("Corpses", "Show dead bodies", g_menu->showCorpses);
w.checkbox("Show Players", "Show player ESP", &g_menu->cfg->showPlayers);
w.checkbox("Bounding Box", "Draw entity box", &g_menu->cfg->showBox);
w.checkbox("Skeleton", "Draw bone skeleton", &g_menu->cfg->showSkeleton);
w.checkbox("Head Circle", "Draw head highlight", &g_menu->cfg->showHeadDot);
w.checkbox("Weapon In Hand", "Show held weapon name", &g_menu->cfg->showWeapon);
w.checkbox("Health Bar", "Draw player health bar", &g_menu->cfg->showHealthBar);
w.checkbox("Health Number", "Draw numeric health", &g_menu->cfg->showHealthNumber);
w.checkbox("Corpses", "Show dead bodies", &g_menu->cfg->showCorpses);
w.checkbox("Skeleton Debug", "Label every named bone", g_menu->debugSkeleton);
end_visual_section();
}
@@ -267,7 +270,7 @@ static void render_visuals_tab(const visual_widget_filter& w, float section_heig
gui->begin_group();
{
begin_visual_section("Draw Distance", section_height);
w.slider("Players", "Max player draw distance", g_menu->playerMaxDist, 50.f, 1000.f, "%.0f m");
w.slider("Players", "Max player draw distance", &g_menu->cfg->playerMaxDist, 50.f, 1000.f, "%.0f m");
end_visual_section();
}
gui->end_group();
@@ -278,9 +281,14 @@ static void render_visuals_tab(const visual_widget_filter& w, float section_heig
gui->begin_group();
{
begin_visual_section("Entities", section_height);
w.checkbox("Animals", "Show animal ESP", g_menu->showAnimals);
w.checkbox("Zombies", "Show infected ESP", g_menu->showZombies);
w.checkbox("Items", "Show loot ESP", g_menu->showItems);
// Animals and Items are suppressed in combat mode; grey them out.
ImGui::BeginDisabled(g_menu->cfg->combatMode);
w.checkbox("Animals", "Show animal ESP", &g_menu->cfg->showAnimals);
ImGui::EndDisabled();
w.checkbox("Zombies", "Show infected ESP", &g_menu->cfg->showZombies);
ImGui::BeginDisabled(g_menu->cfg->combatMode);
w.checkbox("Items", "Show loot ESP", &g_menu->cfg->showItems);
ImGui::EndDisabled();
end_visual_section();
}
gui->end_group();
@@ -290,9 +298,13 @@ static void render_visuals_tab(const visual_widget_filter& w, float section_heig
gui->begin_group();
{
begin_visual_section("Draw Distance", section_height);
w.slider("Animals", "Max animal draw distance", g_menu->animalMaxDist, 50.f, 1000.f, "%.0f m");
w.slider("Zombies", "Max zombie draw distance", g_menu->zombieMaxDist, 50.f, 500.f, "%.0f m");
w.slider("Items", "Max loot draw distance", g_menu->itemMaxDist, 20.f, 200.f, "%.0f m");
ImGui::BeginDisabled(g_menu->cfg->combatMode);
w.slider("Animals", "Max animal draw distance", &g_menu->cfg->animalMaxDist, 50.f, 1000.f, "%.0f m");
ImGui::EndDisabled();
w.slider("Zombies", "Max zombie draw distance", &g_menu->cfg->zombieMaxDist, 50.f, 500.f, "%.0f m");
ImGui::BeginDisabled(g_menu->cfg->combatMode);
w.slider("Items", "Max loot draw distance", &g_menu->cfg->itemMaxDist, 20.f, 200.f, "%.0f m");
ImGui::EndDisabled();
end_visual_section();
}
gui->end_group();
@@ -301,7 +313,7 @@ static void render_visuals_tab(const visual_widget_filter& w, float section_heig
static void render_loot_tab(const visual_widget_filter& w, float section_height)
{
if (!g_menu || !g_menu->itemCategories)
if (!g_menu || !g_menu->cfg)
return;
struct cat_t { const char* key; const char* label; };
@@ -325,7 +337,9 @@ static void render_loot_tab(const visual_widget_filter& w, float section_height)
{ "isOtherLoot", "Other" },
} };
auto& cats = *g_menu->itemCategories;
// Loot categories are irrelevant when combat mode suppresses all loot.
ImGui::BeginDisabled(g_menu->cfg->combatMode);
auto& cats = g_menu->cfg->itemCategories;
auto draw_category = [&](const cat_t& c)
{
@@ -357,6 +371,8 @@ static void render_loot_tab(const visual_widget_filter& w, float section_height)
end_visual_section();
}
gui->end_group();
ImGui::EndDisabled();
}
static constexpr const char* kRadarDomain = "radar.charliecharliekirky.christmas";
@@ -438,20 +454,20 @@ static void render_info_tab(float section_height)
static void render_config_tab(float section_height)
{
if (!g_menu)
if (!g_menu || !g_menu->cfg)
return;
// Persistent text buffers backing the four resolution fields. Seeded once
// from the live values, then treated as the source of truth (parsed back
// into the int pointers every frame).
// from the live cfg values; parsed back into cfg every frame so Apply picks
// up whatever the user typed.
static char ovr_w[8], ovr_h[8], rnd_w[8], rnd_h[8];
static bool buffers_ready = false;
if (!buffers_ready)
{
ImFormatString(ovr_w, IM_ARRAYSIZE(ovr_w), "%d", *g_menu->pendingW);
ImFormatString(ovr_h, IM_ARRAYSIZE(ovr_h), "%d", *g_menu->pendingH);
ImFormatString(rnd_w, IM_ARRAYSIZE(rnd_w), "%d", *g_menu->pendingRW);
ImFormatString(rnd_h, IM_ARRAYSIZE(rnd_h), "%d", *g_menu->pendingRH);
ImFormatString(ovr_w, IM_ARRAYSIZE(ovr_w), "%d", g_menu->cfg->overlayWidth);
ImFormatString(ovr_h, IM_ARRAYSIZE(ovr_h), "%d", g_menu->cfg->overlayHeight);
ImFormatString(rnd_w, IM_ARRAYSIZE(rnd_w), "%d", g_menu->cfg->renderWidth);
ImFormatString(rnd_h, IM_ARRAYSIZE(rnd_h), "%d", g_menu->cfg->renderHeight);
buffers_ready = true;
}
@@ -463,8 +479,8 @@ static void render_config_tab(float section_height)
widgets->text_field("Width", ovr_w, IM_ARRAYSIZE(ovr_w));
widgets->text_field("Height", ovr_h, IM_ARRAYSIZE(ovr_h));
gui->pop_id();
*g_menu->pendingW = ImMax(0, atoi(ovr_w));
*g_menu->pendingH = ImMax(0, atoi(ovr_h));
g_menu->cfg->overlayWidth = ImMax(0, atoi(ovr_w));
g_menu->cfg->overlayHeight = ImMax(0, atoi(ovr_h));
if (widgets->primary_button("Apply Monitor Resolution") && g_menu->onApplyDisplayRes)
g_menu->onApplyDisplayRes();
@@ -476,9 +492,9 @@ static void render_config_tab(float section_height)
widgets->text_field("Width", rnd_w, IM_ARRAYSIZE(rnd_w));
widgets->text_field("Height", rnd_h, IM_ARRAYSIZE(rnd_h));
gui->pop_id();
*g_menu->pendingRW = ImMax(0, atoi(rnd_w));
*g_menu->pendingRH = ImMax(0, atoi(rnd_h));
widgets->checkbox("Stretch to fill", "GPU stretches game to fill the monitor", g_menu->stretchToFill);
g_menu->cfg->renderWidth = ImMax(0, atoi(rnd_w));
g_menu->cfg->renderHeight = ImMax(0, atoi(rnd_h));
widgets->checkbox("Stretch to fill", "GPU stretches game to fill the monitor", &g_menu->cfg->stretchToFill);
if (widgets->primary_button("Apply Render Resolution") && g_menu->onApplyRenderRes)
g_menu->onApplyRenderRes();
+6
View File
@@ -38,6 +38,9 @@ OverlayConfig OverlayConfig::Load(const std::string& path) {
if (j.contains("showWeapon")) cfg.showWeapon = j["showWeapon"].get<bool>();
if (j.contains("showHealthBar")) cfg.showHealthBar = j["showHealthBar"].get<bool>();
if (j.contains("showHealthNumber")) cfg.showHealthNumber = j["showHealthNumber"].get<bool>();
if (j.contains("showBallisticDot")) cfg.showBallisticDot = j["showBallisticDot"].get<bool>();
if (j.contains("showBulletTrails")) cfg.showBulletTrails = j["showBulletTrails"].get<bool>();
if (j.contains("combatMode")) cfg.combatMode = j["combatMode"].get<bool>();
// Distances
if (j.contains("playerMaxDist")) cfg.playerMaxDist = j["playerMaxDist"].get<float>();
@@ -97,6 +100,9 @@ void OverlayConfig::Save(const std::string& path) const {
j["showWeapon"] = showWeapon;
j["showHealthBar"] = showHealthBar;
j["showHealthNumber"] = showHealthNumber;
j["showBallisticDot"] = showBallisticDot;
j["showBulletTrails"] = showBulletTrails;
j["combatMode"] = combatMode;
// Distances
j["playerMaxDist"] = playerMaxDist;
+6 -2
View File
@@ -22,8 +22,12 @@ struct OverlayConfig {
bool showWeapon = true; // item-in-hands label below player name
bool showHealthBar = true; // vertical health bar to the right of the box
bool showHealthNumber = false; // numeric "xxx/100" beside the bar
bool showBallisticDot = false; // cyan dot showing where to aim for center-mass hit
bool showBulletTrails = false; // draws traced path of each bullet in flight
bool showBallisticDot = true; // cyan dot showing where to aim for center-mass hit
bool showBulletTrails = true; // draws traced path of each bullet in flight
// ---- Combat mode ----
// When on: animals and items are suppressed; bullet trail fade is shortened.
bool combatMode = false;
// ---- Draw-distance limits (metres) ----
float playerMaxDist = 1000.0f;
+5
View File
@@ -9,3 +9,8 @@
// — it points into the exe's memory-mapped image, so no copy is made.
// Returns {nullptr, 0} if the map was not compiled into the binary.
std::pair<const uint8_t*, size_t> GetEmbeddedMap(const std::string& mapId);
// Topo maps are not embedded — served from maps/topo/<id>.png on disk only.
inline std::pair<const uint8_t*, size_t> GetEmbeddedTopoMap(const std::string&) {
return {nullptr, 0};
}
+43
View File
@@ -325,6 +325,49 @@ DmaStats VmmAccessor::GetStats() const {
return s;
}
// -------------------------------------------------------------------------
// ReadKernelBytes
// -------------------------------------------------------------------------
bool VmmAccessor::ReadKernelBytes(uint64_t address, void* buf, size_t size) {
if (!buf || size == 0 || !IsInitialized()) return false;
std::shared_lock<std::shared_mutex> lk(m_accessMutex);
if (!m_dma || !m_dma->is_initialized()) return false;
// win32kbase.sys lives in session space, not global kernel space.
// Reading through PID 4 (System/Session 0) won't translate session-space VAs
// because the System process has no session mapping for it.
// Use the attached game-process PID (same Windows session as the user desktop)
// so the page-table walk goes through the right session CR3.
// Fall back to PID 4 if no process is attached yet.
DWORD readPid = (m_attachedPid != 0) ? static_cast<DWORD>(m_attachedPid) : 4;
BOOL ok = VMMDLL_MemRead(m_dma->handle.get(),
readPid,
address,
static_cast<PBYTE>(buf),
static_cast<DWORD>(size));
if (ok) m_bytesReadTotal.fetch_add(size, std::memory_order_relaxed);
return ok == TRUE;
}
// -------------------------------------------------------------------------
// TryGetKernelModuleBase
// -------------------------------------------------------------------------
bool VmmAccessor::TryGetKernelModuleBase(const std::string& moduleName,
uint64_t& outBase) {
if (!IsInitialized()) return false;
std::shared_lock<std::shared_mutex> lk(m_accessMutex);
if (!m_dma || !m_dma->is_initialized()) return false;
uint64_t base = VMMDLL_ProcessGetModuleBaseU(m_dma->handle.get(),
4 /*System PID*/,
moduleName.c_str());
if (!base) return false;
outBase = base;
return true;
}
std::string VmmAccessor::ReadCString(uint32_t pid, uint64_t address,
size_t maxLength) {
if (!MemoryValidation::IsValidUserAddress(address)) return {};
+11
View File
@@ -119,6 +119,17 @@ public:
std::string ReadCString(uint32_t pid, uint64_t address,
size_t maxLength = 256);
// ------------------------------------------------------------------
// Kernel reads — read from game PC kernel address space (PID 4).
// Used by KeyStateReader to access win32kbase.sys global state.
// ------------------------------------------------------------------
/// Read bytes from the game PC's kernel virtual address space.
bool ReadKernelBytes(uint64_t address, void* buf, size_t size);
/// Look up a kernel module base address by name (e.g. "win32kbase.sys").
bool TryGetKernelModuleBase(const std::string& moduleName, uint64_t& outBase);
// ------------------------------------------------------------------
// State
// ------------------------------------------------------------------
+21 -22
View File
@@ -128,21 +128,16 @@ namespace Offsets {
}
// Weapon entity struct offsets (entity is already an InventoryItem with a type ptr at +0x180).
// Chain to magazine: entity+0x1B0 (ChamberedPtr) → chambered item → magazine.
namespace Weapon {
constexpr uint64_t ChamberedPtr = 0x1B0; // v1.29 [UC dumper r15 2026-06-15]
// Layout (UC dumper r15 2026-06-15, Spectre confirmed):
// 0x6A0: uint64_t MagazinePtr (8-byte ptr, 0x6A00x6A7)
// 0x6A8: uint32_t ??? (4-byte field, identity TBD)
// 0x6AC: uint32_t AmmoMagCount (4-byte count, UC confirmed)
// 0x6B0: uint32_t AmmoCapacityA
// 0x6B4: uint32_t AmmoCapacityB
// Prior value was 0x6A8, which overlapped with AmmoMagCount (0x6AC is
// only 4 bytes later, but a uint64_t ptr needs 8 bytes → wrong offset).
constexpr uint64_t MagazinePtr = 0x6A0; // v1.29 — 8-byte ptr to loaded magazine
constexpr uint64_t AmmoMagCount = 0x6AC; // v1.29 [UC dumper r15 2026-06-15] — loaded magazine ammo count
constexpr uint64_t AmmoCapacityA = 0x6B0; // v1.29 [UC dumper r15 2026-06-15]
constexpr uint64_t AmmoCapacityB = 0x6B4; // v1.29 [UC dumper r15 2026-06-15]
// Live-probed 2026-06-20 with MP7A2 + 20rnd mag loaded:
// +0x6A0 = 0x0 (null — unknown field)
// +0x6A8 = 0x8E34DBC90 (valid heap ptr → magazine entity) ← MagazinePtr
// reading uint32 at +0x6AC returns the high 4 bytes of the 0x6A8 ptr, NOT AmmoMagCount
// +0x6B0 = 0x100000001 (two uint32s both 1 — identity unknown)
// The UC dumper r15 note that placed MagazinePtr at 0x6A0 was based on a
// mis-read layout; 0x6A8 is the empirically confirmed correct offset.
// ChamberedPtr at 0x1B0 also probed wrong (returns 0x155, a non-pointer integer).
constexpr uint64_t MagazinePtr = 0x6A8; // v1.29 [live-probed 2026-06-20]
}
// Magazine entity struct offsets.
@@ -195,19 +190,23 @@ namespace Offsets {
constexpr uint64_t ZombieOffset = ZombieCand[0];
// Probed candidates: skeleton → animClass pointer (tried in order).
// 0x118 confirmed v1.29 (Spectre stable); 0xA8 confirmed by external source.
constexpr uint64_t AnimCand[4] = { 0x118, 0xA8, 0x98, 0xB0 };
// 0x110 confirmed v1.29 [IDA sub_1404CF250: *(a1+272), 272=0x110]
// 0x118 previously used; 0xA8 confirmed by external source.
constexpr uint64_t AnimCand[5] = { 0x110, 0x118, 0xA8, 0x98, 0xB0 };
// Probed candidates: animClass → matrixArray pointer (tried in order).
// 0xBE8 confirmed v1.29 (Spectre stable); 0xBF0 confirmed by external source.
constexpr uint64_t MatrixCand[5] = { 0xBE8, 0xBF0, 0xBD8, 0xB40, 0xB30 };
// Each bone entry is 48 bytes; translation (x,y,z) starts at byte offset 0x54
// from the matrixArray base (effective: matrixArray + 0x54 + boneIndex*0x30).
// This gives boneIndex N the same world position as their boneIndex N+1
// (the +0x54 vs +0x24 difference is exactly one stride).
constexpr uint32_t BoneStride = 48; // v1.29
constexpr uint32_t BoneTranslationOffset = 0x54; // v1.29
// Each bone entry is a column-major 3×4 float matrix (48 bytes).
// Translation (x,y,z) is in the 4th column at byte offset 0x24 (floats [9,10,11]).
// Formula: matBase + 0x24 + boneIndex * 0x30
// IDA confirmed: sub_1402A4F10 returns *(a1+112) + 48*boneIdx;
// sub_1404D9C40 writes translation at result+36 (=0x24) within 48-byte stride.
// Bone indices in DayZRuntimeService::RefreshBonesScatter use the original Enfusion
// PlayerBoneId / InfectedBoneId enum values directly (no -1 adjustment).
constexpr uint32_t BoneStride = 48; // v1.29 [IDA confirmed — 3×4 matrix]
constexpr uint32_t BoneTranslationOffset = 0x24; // v1.29 [IDA confirmed — col3 of 3×4]
}
} // namespace Offsets
File diff suppressed because it is too large Load Diff
+65 -123
View File
@@ -1,12 +1,13 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <imgui.h>
#include "Config.h"
#include "Overlay/BoneInterpolator.h"
#include "Runtime/DayZRuntimeService.h"
#include "Web/BulletTrackCache.h"
@@ -14,14 +15,25 @@ 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; // time of the last positional change (for extrapolation velocity)
int64_t lastReadMs = 0; // time of the most recent valid read (for staleness gate)
bool initialized = false;
// Pre-projected per-entity bone screen coordinates — computed once per
// entity per frame and shared by box, skeleton, head-circle draw calls.
struct PlayerScreenData {
enum BoneIdx : 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 = 16
};
struct SP { float x = 0.f, y = 0.f; bool ok = false; };
SP bones[B_COUNT]{};
float bx0 = 0.f, by0 = 0.f, bx1 = 0.f, by1 = 0.f;
float hbx = 0.f, hby = 0.f, headRadius = 0.f;
bool boxReady = false;
bool hasHead = false;
float dist = 0.f;
};
explicit GameOverlay(DayZRuntimeService& service,
@@ -30,165 +42,85 @@ public:
: 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_showBallisticDot = m_cfg.showBallisticDot;
m_showBulletTrails = m_cfg.showBulletTrails;
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.
// Called each ImGui frame.
void Draw(float w, float h);
bool IsMenuOpen() const { return m_menuOpen; }
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()
std::vector<std::string> m_webUrls;
// 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;
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;
bool m_debugSkeleton = false; // not persisted — debug-only
// 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_showBallisticDot = false;
bool m_showBulletTrails = false;
bool m_debugSkeleton = false; // draws named bone dots for the closest player
// Loot fonts (set once via SetLootFonts after atlas build)
ImFont* m_fontLootClose = nullptr;
ImFont* m_fontLootFar = nullptr;
// 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.
// Zombie last-known positions — fallback when position read fails and no
// bone data is available (cold start for newly entered scan radius).
std::unordered_map<uint64_t, Vector3> m_zombieLastPos;
// Per-player velocity estimate — updated each frame from position delta.
struct PlayerVelEntry {
Vector3 pos;
int64_t ms = 0;
Vector3 vel; // smoothed m/s, EMA α=0.35
};
// Per-player velocity for ballistic prediction (EMA α=0.35)
struct PlayerVelEntry { Vector3 pos; int64_t ms = 0; Vector3 vel; };
std::unordered_map<uint64_t, PlayerVelEntry> m_playerVel;
// Bullet trail tracker — owned by the overlay, fed from each update's bullet list.
// Bullet trail tracker
BulletTrackCache m_bulletTracks;
// ---- 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
// Bone interpolators — one per entity class
BoneInterpolator m_playerBones;
BoneInterpolator m_zombieBones;
int64_t m_frameTimeMs = 0;
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();
// Project all bone positions and compute the bounding box once per entity.
// smoothBones may be nullptr (corpses, bone-cold entities) → falls back to
// ground-pos + hardcoded 1.8 m head. includeHeadCircle extends by0 to
// encompass the head circle radius when showHeadDot is active.
// Returns nullopt if the entity is entirely off-screen or box height < 2 px.
std::optional<PlayerScreenData> ComputeScreenData(
const Vector3& entityPos,
const SkeletonBones* smoothBones,
bool includeHeadCircle,
const CameraData& cam,
float w, float h) const;
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);
@@ -197,12 +129,22 @@ private:
void DrawItems (ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
void DrawBulletTrails (ImDrawList* dl, 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,
// Per-player sub-draw functions — all take a pre-computed PlayerScreenData.
void DrawPlayerBox (ImDrawList* dl, const PlayerScreenData& sd, ImU32 color) const;
void DrawPlayerLabels (ImDrawList* dl, const PlayerScreenData& sd,
const DayZPlayerEntry& p, ImU32 color) const;
void DrawPlayerHealth (ImDrawList* dl, const PlayerScreenData& sd,
const DayZPlayerEntry& p) const;
void DrawPlayerBallistic(ImDrawList* dl,
const DayZPlayerEntry& p, const Vector3& pos,
const RuntimeUpdate& u,
const CameraData& cam, float w, float h);
// Draw skeleton line segments using pre-projected bone screen coords.
// No Proj() calls inside — caller pre-computes via ComputeScreenData.
void DrawSkeleton(ImDrawList* dl, const PlayerScreenData::SP* bones,
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;
};
+26 -51
View File
@@ -3,84 +3,59 @@
// 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.
// GameOverlay sets cfg = &m_cfg each frame so the menu reads/writes all
// persisted settings directly through the OverlayConfig pointer. The
// remaining fields are either debug-only state or read-only info panels
// that GameOverlay populates each frame.
// -------------------------------------------------------------------------
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <string>
#include <vector>
#include "Config.h"
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;
// All persisted ESP / resolution settings — menu reads/writes through cfg.
OverlayConfig* cfg = nullptr;
// Debug skeleton overlay — not persisted, toggled per-session only.
bool* debugSkeleton = nullptr;
bool* showBallisticDot = nullptr;
bool* showBulletTrails = 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) ----
// ---- Read-only info panels (refreshed each frame by GameOverlay) ----
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;
std::size_t nPlayers = 0;
std::size_t nAnimals = 0;
std::size_t nZombies = 0;
std::size_t nVehicles = 0;
std::size_t nItems = 0;
std::size_t nBullets = 0;
// ---- DMA handle stats (read-only, refreshed each frame) ----
// ---- DMA handle stats (read-only) ----
bool dmaAttached = false;
float dmaReadMBps = 0.f; // MB/s rolling 1 s
float dmaScatterOps = 0.f; // scatter calls/s rolling 1 s
float dmaTotalGB = 0.f; // cumulative GB read since attach
uint64_t dmaTotalOps = 0; // cumulative scatter calls since attach
float dmaReadMBps = 0.f;
float dmaScatterOps = 0.f;
float dmaTotalGB = 0.f;
uint64_t dmaTotalOps = 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()> onApplyDisplayRes; // resize overlay window
std::function<void()> onApplyRenderRes; // apply stretched-res correction
std::function<void()> onSaveConfig;
std::function<void()> onExit;
};
// Set by GameOverlay before each gui->render() call; read by the menu.
// Set by GameOverlay before each RenderLuminMenu() 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().
// Thin shim implemented in external/lumin/framework/gui.cpp.
void RenderLuminMenu();
+72 -30
View File
@@ -100,6 +100,7 @@ DayZRuntimeService::DayZRuntimeService(RuntimeConfig config)
m_state.nextNetworkMetadataRefresh = past;
m_state.nextBoneRefresh = past;
m_state.nextVmmRefresh = past;
m_state.nextKeyRefresh = past;
}
DayZRuntimeService::~DayZRuntimeService() {
@@ -698,8 +699,9 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
update.localPlayerLookDirection = m_localPlayer.lookDirection;
// Weapon ammo chain: entity→inventory→hands(weapon)→magazine→ammoType
// Logged once per session if it fails with a weapon in hand (handsValid=1)
// so offset bugs are diagnosable without a debugger.
// Falls back to chambered-item chain when no magazine is attached.
// Logged once per weapon-equip session; gate resets when no weapon is
// in hand so the next equip always gets fresh diagnostic output.
uint64_t inv = 0, weapon = 0, mag = 0, ammoType = 0;
uint8_t handsValid = 0;
bool invOk = m_memory.TryReadPointer(pid,
@@ -709,6 +711,13 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
&& m_memory.TryReadValue<uint8_t>(pid,
inv + Offsets::Inventory::HandItemValid, handsValid)
&& handsValid;
// No weapon in hand: reset the "logged once" gate so the next equip
// triggers fresh failure logging (only when inv is readable, to avoid
// resetting on a transient DMA miss that also blanks handOk).
if (invOk && !handOk)
m_ammoChainLoggedOnce = false;
bool weaponOk = handOk
&& m_memory.TryReadPointer(pid,
inv + Offsets::Inventory::Hands, weapon)
@@ -723,13 +732,19 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
&& MemoryValidation::IsValidUserAddress(ammoType);
if (ammoOk) {
m_ammoChainLoggedOnce = false; // reset so failures log again next equip
float initSpeed = 0.0f, airFriction = 0.0f;
m_memory.TryReadValue<float>(pid,
ammoType + Offsets::AmmoType::InitSpeed, initSpeed);
m_memory.TryReadValue<float>(pid,
ammoType + Offsets::AmmoType::AirFriction, airFriction);
if (initSpeed > 1.0f) {
if (!m_ammoChainLoggedOnce) {
m_ammoChainLoggedOnce = true;
spdlog::info("BallisticDot: ammo chain OK — "
"initSpeed={:.1f} m/s airFriction={:.4f} "
"(mag=0x{:X} ammoType=0x{:X})",
initSpeed, airFriction, mag, ammoType);
}
update.localWeaponInitSpeed = initSpeed;
update.localWeaponAirFriction = std::max(0.0f, airFriction);
} else if (!m_ammoChainLoggedOnce) {
@@ -738,27 +753,30 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
"(ammoType=0x{:X}+0x{:X}) — check AmmoType::InitSpeed offset",
initSpeed, ammoType, Offsets::AmmoType::InitSpeed);
}
} else if (!m_ammoChainLoggedOnce) {
if (!invOk) {
} else if (!invOk && !m_ammoChainLoggedOnce) {
m_ammoChainLoggedOnce = true;
spdlog::warn("BallisticDot: inventory ptr invalid "
"(entity=0x{:X}+0x{:X}) — check Inventory::Base offset",
m_localPlayer.entityAddress, Offsets::Inventory::Base);
} else if (!handOk) {
// handsValid==0 = no weapon equipped — silence further "no weapon" noise.
m_ammoChainLoggedOnce = true;
} else if (!weaponOk) {
} else if (handOk && !m_ammoChainLoggedOnce) {
m_ammoChainLoggedOnce = true;
if (!weaponOk) {
spdlog::warn("BallisticDot: weapon chain fail at Inventory::Hands "
"(inv=0x{:X}+0x{:X})", inv, Offsets::Inventory::Hands);
} else if (!magOk) {
m_ammoChainLoggedOnce = true;
spdlog::warn("BallisticDot: weapon chain fail at Weapon::MagazinePtr "
"(weapon=0x{:X}+0x{:X}) — verify offset", weapon, Offsets::Weapon::MagazinePtr);
uint64_t rawMagVal = 0;
m_memory.TryReadValue<uint64_t>(pid,
weapon + Offsets::Weapon::MagazinePtr, rawMagVal);
spdlog::warn("BallisticDot: mag+chambered both fail "
"(weapon=0x{:X}+0x{:X}) magRaw=0x{:X} — {}",
weapon, Offsets::Weapon::MagazinePtr, rawMagVal,
rawMagVal == 0
? "no magazine loaded and no chambered round"
: "verify MagazinePtr offset");
} else {
m_ammoChainLoggedOnce = true;
spdlog::warn("BallisticDot: weapon chain fail at Magazine::AmmoTypePtr "
"(mag=0x{:X}+0x{:X}) — verify offset", mag, Offsets::Magazine::AmmoTypePtr);
"(mag=0x{:X}+0x{:X}) — verify offset",
mag, Offsets::Magazine::AmmoTypePtr);
}
}
}
@@ -879,6 +897,10 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
if (!boneTick.players.empty() || !boneTick.zombies.empty()) {
RefreshBonesScatter(pid, boneTick.players, boneTick.zombies);
}
// Stamp with the time bones were actually read so the overlay can
// compute how old this data is and extrapolate positions forward.
boneTick.snapshotMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
PublishUpdate(boneTick); // always publish — includes items/bullets
m_state.nextBoneRefresh = Clock::now() + Ms(m_config.boneRefreshMs);
}
@@ -910,7 +932,21 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
}
// ------------------------------------------------------------------
// 13. Consecutive-failure check.
// 13. Game-PC keyboard state poll (gafAsyncKeyState via DMA).
// Poll at ~20 Hz — fast enough for key-press detection, cheap enough
// to ignore if win32kbase.sys isn't found (e.g. pre-attach).
// ------------------------------------------------------------------
if (now >= m_state.nextKeyRefresh) {
if (!m_keyReader.IsReady()) {
m_keyReader.TryInit(m_memory);
} else {
m_keyReader.Update(m_memory);
}
m_state.nextKeyRefresh = Clock::now() + Ms(50);
}
// ------------------------------------------------------------------
// 14. Consecutive-failure check.
// ------------------------------------------------------------------
if (anyFailure) {
++consecutiveFailures;
@@ -1043,6 +1079,7 @@ void DayZRuntimeService::ResetAllReaders() {
m_bulletReader.Reset();
m_itemReader.Reset();
m_scoreboardReader.Reset();
m_keyReader.Reset();
// Park the camera thread before tearing down camera state so it stops
// reading through a Process handle that's about to be re-created.
@@ -1083,6 +1120,7 @@ void DayZRuntimeService::ResetAllReaders() {
m_state.nextNetworkMetadataRefresh = past;
m_state.nextBoneRefresh = past;
m_state.nextVmmRefresh = past;
m_state.nextKeyRefresh = past;
}
// -------------------------------------------------------------------------
@@ -1094,26 +1132,30 @@ void DayZRuntimeService::RefreshBonesScatter(
std::vector<DayZPlayerEntry>& players,
std::vector<DayZZombieEntry>& zombies)
{
// Bone indices for GetBonePositionWS scatter reads.
// Due to BoneTranslationOffset=0x54 vs the source's 0x24 (difference = 1 stride),
// our index N reads the same world position as their index N+1. All values below
// are already adjusted for this (-1 from the Enfusion PlayerBoneId/InfectedBoneId enums).
// Bone indices — direct Enfusion PlayerBoneId / InfectedBoneId enum values.
// With BoneStride=64 and BoneTranslationOffset=0x30 the formula is:
// matBase + 0x30 + boneIndex * 0x40
// No index adjustment needed (old -1 adjustment was a 3×4 vs 4×4 stride artefact).
//
// boneOut order: neck head spine | rShoulder rElbow rHand | lShoulder lElbow lHand |
// rHip rKnee rAnkle | lHip lKnee lAnkle
// Player: 15 bones. Verified against PlayerBoneId enum 2026-06-13.
static const int kPlayerBones[15] = {
21, 23, 18, // neck(22), head(24), spine1(19)
60, 63, 65, // leftShoulder(61), leftForearm(64), leftHand(66)
93, 97, 99, // rightShoulder(94), rightForearm(98), rightHand(100)
1, 4, 6, // leftUpLeg(2), leftLeg(5), leftFoot(7)
9, 12, 14 // rightUpLeg(10), rightLeg(13), rightFoot(15)
22, 24, 19, // neck, head, spine1
94, 98, 100, // rightShoulder, rightForearm, rightHand
61, 64, 66, // leftShoulder, leftForearm, leftHand
2, 5, 7, // rightUpLeg, rightLeg, rightFoot (maps to rHip/rKnee/rAnkle)
10, 13, 15 // leftUpLeg, leftLeg, leftFoot (maps to lHip/lKnee/lAnkle)
};
// Zombie: 16 bones. Verified against InfectedBoneId enum 2026-06-13.
// boneOut order: neck head spine pelvis | lShoulder lElbow lHand | rShoulder rElbow rHand |
// rHip rKnee rAnkle | lHip lKnee lAnkle
static const int kZombieBones[16] = {
19, 21, 18, 0, // neck(20), head(22), spine3(19), pelvis(1)
23, 26, 28, // leftShoulder(24), leftForearm(27), leftHand(29)
55, 59, 61, // rightShoulder(56), rightForearm(60), rightHand(62)
1, 4, 6, // leftUpLeg(2), leftLeg(5), leftFoot(7)
8, 11, 13 // rightUpLeg(9), rightLeg(12), rightFoot(14)
20, 22, 19, 1, // neck, head, spine3, pelvis
24, 27, 29, // leftShoulder, leftForearm, leftHand (→ boneOut[4..6] = lShoulder/Elbow/Hand)
56, 60, 62, // rightShoulder, rightForearm, rightHand (→ boneOut[7..9] = rShoulder/Elbow/Hand)
2, 5, 7, // rightUpLeg, rightLeg, rightFoot (→ boneOut[10..12] = rHip/Knee/Ankle)
9, 12, 14 // leftUpLeg, leftLeg, leftFoot (→ boneOut[13..15] = lHip/Knee/Ankle)
};
// Per-entity bone read buffers. Must be stable in memory before we point
@@ -1234,7 +1276,7 @@ void DayZRuntimeService::RefreshBonesScatter(
struct Vec3f { float x, y, z; };
Vec3f test{};
const uint64_t testAddr = matBase + Offsets::Skeleton::BoneTranslationOffset
+ 21 * Offsets::Skeleton::BoneStride;
+ 22 * Offsets::Skeleton::BoneStride;
if (!m_memory.TryReadValue<Vec3f>(pid, testAddr, test)) continue;
// Reject all-zero (uninitialised page) and out-of-range garbage.
// Local bone translations are always within ±5 m of the skeleton root.
+16
View File
@@ -20,6 +20,7 @@
#include "Readers/FarEntityListReader.h"
#include "Readers/ItemFilterCatalog.h"
#include "Readers/ItemListReader.h"
#include "Readers/KeyStateReader.h"
#include "Readers/NearEntityListReader.h"
#include "Readers/SlowEntityListReader.h"
#include "Resolvers/BaseObjectResolver.h"
@@ -62,6 +63,11 @@ struct RuntimeUpdate {
std::vector<DayZSlowEntityEntry> slowEntities;
std::optional<CameraData> camera;
// Steady-clock ms when this snapshot's bone data was captured.
// Set immediately after RefreshBonesScatter so overlay extrapolation
// knows how old the positions are and can predict forward accurately.
int64_t snapshotMs = 0;
};
// -------------------------------------------------------------------------
@@ -141,6 +147,14 @@ public:
/// Live DMA throughput snapshot — safe to call from the overlay thread each frame.
[[nodiscard]] DmaStats GetDmaStats() const { return m_memory.GetStats(); }
/// True while the given virtual key is held on the game PC's keyboard.
/// Sourced from gafAsyncKeyState in win32kbase.sys via DMA.
/// Returns false if KeyStateReader has not yet resolved the address.
[[nodiscard]] bool IsMainKeyDown(int vk) const { return m_keyReader.IsDown(vk); }
/// True for one poll interval after a key transitions up→down on the game PC.
[[nodiscard]] bool IsMainKeyPressed(int vk) const { return m_keyReader.IsPressed(vk); }
private:
// ------------------------------------------------------------------
// Configuration
@@ -161,6 +175,7 @@ private:
BulletTableReader m_bulletReader;
ItemListReader m_itemReader;
ClientScoreboardReader m_scoreboardReader;
KeyStateReader m_keyReader;
// Last module base address passed to the sig scanner.
uint64_t m_lastScannedBase = 0;
@@ -265,6 +280,7 @@ private:
std::chrono::steady_clock::time_point nextNetworkMetadataRefresh;
std::chrono::steady_clock::time_point nextBoneRefresh;
std::chrono::steady_clock::time_point nextVmmRefresh;
std::chrono::steady_clock::time_point nextKeyRefresh;
} m_state;
// Cached metadata, player data, and camera populated during session
+4 -3
View File
@@ -49,15 +49,16 @@ const MapTileService::Image* MapTileService::LoadOrGet(const std::string& mapId)
}
// Disk file takes priority — allows map updates without rebuilding.
// Falls back to the PNG baked into the binary as a Windows RCDATA resource.
std::string path = (ExeDir() / "maps" / (mapId + ".png")).string();
// Falls back to the PNG baked into the binary as a Windows RCDATA resource
// (only for the default "maps" subdir — topo tiles have no embedded fallback).
std::string path = (ExeDir() / m_subdir / (mapId + ".png")).string();
int w = 0, h = 0, channels = 0;
unsigned char* raw = nullptr;
if (std::filesystem::exists(path)) {
raw = stbi_load(path.c_str(), &w, &h, &channels, kChannels);
} else {
} else if (m_subdir == "maps") {
auto [ptr, sz] = GetEmbeddedMap(mapId);
if (ptr)
raw = stbi_load_from_memory(ptr, static_cast<int>(sz),
+5
View File
@@ -15,6 +15,9 @@
class MapTileService {
public:
/// subdir is relative to the exe directory (e.g. "maps" or "maps/topo").
explicit MapTileService(std::string subdir = "maps") : m_subdir(std::move(subdir)) {}
/// Return PNG-encoded bytes for the requested tile.
/// errCode is set to one of: 200, 400, 404, 500.
/// An empty vector is returned for any non-200 result.
@@ -22,6 +25,8 @@ public:
int& errCode);
private:
std::string m_subdir;
struct Image {
std::vector<uint8_t> pixels; // RGBA, row-major
int w = 0;
+147
View File
@@ -311,6 +311,49 @@ void WebRadarServer::SetupRoutes() {
}
});
// ---- Topo tile (optional overlay layer) --------------------------------
m_server->Get("/topo-tile",
[this](const httplib::Request& req, httplib::Response& res) {
AddCors(res);
if (!Authorise(req.remote_addr,
req.get_param_value("password"))) {
res.status = 401;
return;
}
const MapInfo* tileMap = nullptr;
std::string mapIdParam = req.get_param_value("mapId");
if (!mapIdParam.empty())
tileMap = MapRegistry::Resolve(mapIdParam);
if (!tileMap)
tileMap = m_currentMap;
if (!tileMap) {
res.status = 404;
return;
}
int tx = 0, ty = 0;
try {
tx = std::stoi(req.get_param_value("x"));
ty = std::stoi(req.get_param_value("y"));
} catch (...) {
res.status = 400;
return;
}
int code = 200;
auto bytes = m_topoTiles.GetTile(*tileMap, tx, ty, code);
res.status = code;
if (code == 200) {
res.set_header("Cache-Control", "public, max-age=86400");
res.set_content(
std::string(bytes.begin(), bytes.end()),
"image/png");
}
});
// ---- Full map image -------------------------------------------------
m_server->Get("/map-image",
@@ -350,6 +393,66 @@ void WebRadarServer::SetupRoutes() {
res.set_content(std::move(content), "image/png");
});
// ---- 3D engine static files -----------------------------------------
m_server->Get("/engine.js",
[this](const httplib::Request& /*req*/, httplib::Response& res) {
ServeFile(res, "engine.js", "application/javascript; charset=utf-8");
});
m_server->Get("/engine.wasm",
[this](const httplib::Request& /*req*/, httplib::Response& res) {
std::string path = WebrootPath("engine.wasm");
std::ifstream f(path, std::ios::binary);
if (!f) { res.status = 404; return; }
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
res.set_header("Cache-Control", "no-cache");
res.set_content(std::move(content), "application/wasm");
});
// ---- Processed data: manifest / terrain / sat / markers -------------
// All routes resolve to: data/processed/<mapId>/... relative to exe.
m_server->Get("/data/:mapId/manifest",
[this](const httplib::Request& req, httplib::Response& res) {
AddCors(res);
std::string mapId = req.path_params.at("mapId");
std::string path = DataPath(mapId, mapId + ".manifest.json");
ServeDataFile(res, path, "application/json");
});
m_server->Get("/data/:mapId/terrain/:file",
[this](const httplib::Request& req, httplib::Response& res) {
AddCors(res);
std::string mapId = req.path_params.at("mapId");
std::string file = req.path_params.at("file");
std::string path = DataPath(mapId, "terrain/" + file);
res.set_header("Cache-Control", "public, max-age=86400");
ServeDataFile(res, path, "application/octet-stream");
});
m_server->Get(R"(/data/([^/]+)/sat/(\d+)/(\d+)/(\d+))",
[this](const httplib::Request& req, httplib::Response& res) {
AddCors(res);
std::string mapId = req.matches[1];
std::string z = req.matches[2];
std::string x = req.matches[3];
std::string y = req.matches[4];
std::string path = DataPath(mapId, "sat/" + z + "/" + x + "/" + y + ".webp");
res.set_header("Cache-Control", "public, max-age=604800");
ServeDataFile(res, path, "image/webp");
});
m_server->Get("/data/:mapId/markers/:file",
[this](const httplib::Request& req, httplib::Response& res) {
AddCors(res);
std::string mapId = req.path_params.at("mapId");
std::string file = req.path_params.at("file");
std::string path = DataPath(mapId, "markers/" + file);
ServeDataFile(res, path, "application/json");
});
// ---- Debug ----------------------------------------------------------
// No auth — purely diagnostic. Open /api/debug in a browser to see
// what the server sees: exe dir, maps/ path, which PNGs exist, and what
@@ -475,6 +578,50 @@ std::string WebRadarServer::WebrootPath(const std::string& file) const {
return exeDir + "webroot\\" + file;
}
// -------------------------------------------------------------------------
// DataPath — exe_dir/data/processed/<mapId>/<relative>
// -------------------------------------------------------------------------
std::string WebRadarServer::DataPath(const std::string& mapId,
const std::string& relative) const
{
wchar_t buf[MAX_PATH] = {};
GetModuleFileNameW(nullptr, buf, MAX_PATH);
std::wstring wpath(buf);
auto lastSlash = wpath.find_last_of(L"\\/");
if (lastSlash != std::wstring::npos)
wpath = wpath.substr(0, lastSlash + 1);
std::string exeDir(wpath.begin(), wpath.end());
// Sanitise mapId and relative to prevent directory traversal.
// Both must be non-empty and must not contain ".." segments.
if (mapId.empty() || mapId.find("..") != std::string::npos ||
relative.find("..") != std::string::npos)
return "";
// Replace forward-slashes with backslashes for Windows.
std::string rel = relative;
for (char& c : rel) if (c == '/') c = '\\';
return exeDir + "data\\processed\\" + mapId + "\\" + rel;
}
// -------------------------------------------------------------------------
// ServeDataFile — serve an arbitrary filesystem path
// -------------------------------------------------------------------------
void WebRadarServer::ServeDataFile(httplib::Response& res,
const std::string& path,
const std::string& contentType)
{
if (path.empty()) { res.status = 400; return; }
std::ifstream f(path, std::ios::binary);
if (!f) { res.status = 404; return; }
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
res.set_content(std::move(content), contentType);
}
// -------------------------------------------------------------------------
// NowMs
// -------------------------------------------------------------------------
+11
View File
@@ -52,6 +52,7 @@ private:
WebSnapshotService m_snapshotSvc;
BulletTrackCache m_bullets;
MapTileService m_tiles;
MapTileService m_topoTiles{"maps/topo"};
const MapInfo* m_currentMap = nullptr;
std::unique_ptr<httplib::Server> m_server;
@@ -80,5 +81,15 @@ private:
/// Absolute path to webroot/<file> relative to the executable directory.
std::string WebrootPath(const std::string& file) const;
/// Absolute path to data/processed/<mapId>/<relative> relative to the exe.
/// Returns "" if mapId or relative contain ".." (traversal guard).
std::string DataPath(const std::string& mapId,
const std::string& relative) const;
/// Serve an arbitrary filesystem path with the given content-type.
void ServeDataFile(httplib::Response& res,
const std::string& path,
const std::string& contentType);
static int64_t NowMs();
};
+70
View File
@@ -1,9 +1,16 @@
#include "Web/WebSnapshotService.h"
#include <cmath>
#include <filesystem>
#include <format>
#include <fstream>
#include <string>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
@@ -14,6 +21,12 @@ using json = nlohmann::json;
static constexpr float kPi = 3.14159265358979323846f;
static std::filesystem::path ExeDir() {
wchar_t buf[MAX_PATH] = {};
GetModuleFileNameW(nullptr, buf, MAX_PATH);
return std::filesystem::path(buf).parent_path();
}
/// Convert a world address to a hex-string id (e.g. "1A2B3C4D").
static std::string AddrId(uint64_t address) {
return std::format("{:X}", address);
@@ -451,5 +464,62 @@ std::string WebSnapshotService::BuildBootstrapJson(const RuntimeUpdate& update,
j["filters"] = std::move(filters);
// Expose 3D manifest URL when processed data exists for the active map.
// Also expose satellite tile availability independently (no manifest required).
if (map) {
std::filesystem::path manifestPath =
ExeDir() / "data" / "processed" / map->id / (map->id + ".manifest.json");
if (std::filesystem::exists(manifestPath))
j["manifestUrl"] = "/data/" + map->id + "/manifest";
else
j["manifestUrl"] = nullptr;
std::filesystem::path satDir =
ExeDir() / "data" / "processed" / map->id / "sat";
if (std::filesystem::exists(satDir) && std::filesystem::is_directory(satDir)) {
j["satUrl"] = "/data/" + map->id + "/sat";
j["satMaxZoom"] = 7; // matches satellite.py default --max-zoom
} else {
j["satUrl"] = nullptr;
j["satMaxZoom"] = nullptr;
}
} else {
j["manifestUrl"] = nullptr;
j["satUrl"] = nullptr;
j["satMaxZoom"] = nullptr;
}
// Load POIs for the active map from webroot/pois/<mapId>.json.
// Coordinates are stored in world-space and translated here to pixel-space
// so the frontend can place them identically to entity markers.
if (map) {
std::filesystem::path poisPath =
ExeDir() / "webroot" / "pois" / (map->id + ".json");
if (std::filesystem::exists(poisPath)) {
std::ifstream f(poisPath);
if (f) {
auto raw = json::parse(f, nullptr, /*allow_exceptions=*/false);
if (!raw.is_discarded() && raw.is_array()) {
json poiArr = json::array();
for (const auto& p : raw) {
float px{}, py{};
MapRegistry::Translate(*map,
p.value("x", 0.0f),
p.value("z", 0.0f),
px, py);
poiArr.push_back({
{"id", p.value("id", "")},
{"label", p.value("label", "")},
{"type", p.value("type", "")},
{"x", px},
{"y", py}
});
}
j["pois"] = std::move(poiArr);
}
}
}
}
return j.dump();
}
+11
View File
@@ -84,12 +84,23 @@ int main() {
if (p.isAdmin) ++admins;
}
if (update.localWeaponInitSpeed > 1.0f) {
log->info("[Live] Players={} (bones {}/{}, admins {}) Zombies={} Vehicles={} "
"WeaponSpeed={:.0f}m/s Server='{}'",
update.players.size(),
skelOk, update.players.size(), admins,
update.zombies.size(),
update.carsAndBoats.size(),
update.localWeaponInitSpeed,
update.serverName.value_or("?"));
} else {
log->info("[Live] Players={} (bones {}/{}, admins {}) Zombies={} Vehicles={} Server='{}'",
update.players.size(),
skelOk, update.players.size(), admins,
update.zombies.size(),
update.carsAndBoats.size(),
update.serverName.value_or("?"));
}
});
service.Start();
+1609 -805
View File
File diff suppressed because it is too large Load Diff
+184 -85
View File
@@ -1,50 +1,130 @@
<!doctype html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DayZ Web Map</title>
<link rel="stylesheet" href="/style.css">
<title>DayZ Web Radar</title>
<link rel="stylesheet" href="style.css?v=7">
</head>
<body>
<button class="settings-toggle" id="settingsToggle" type="button" aria-label="Open filters"></button>
<div class="top-right-toggles">
<button class="players-toggle" id="labelSettingsToggle" type="button" aria-label="Open label settings">Labels</button>
<button class="players-toggle" id="playersToggle" type="button" aria-label="Open players">Players</button>
<button class="loot-toggle" id="lootToggle" type="button" aria-label="Open loot">Loot</button>
<!-- ── Header bar ─────────────────────────────────────────────────── -->
<header class="app-header" id="appHeader">
<button class="header-btn" id="sidebarToggle" title="Toggle sidebar (Esc)"></button>
<div class="header-server">
<span class="conn-dot" id="connectionDot"></span>
<span class="header-server-name" id="serverBadge">connecting…</span>
</div>
<aside class="label-settings-panel" id="labelSettingsPanel">
<div class="label-settings-header">
<div>
<strong>Label Settings</strong>
<div class="label-settings-subtitle">Tune ordinary loot label placement without touching filters.</div>
<div class="header-counts">
<button class="count-badge" id="badge-players" data-key="showPlayers" title="Toggle players (P)">P <span></span></button>
<button class="count-badge" id="badge-zombies" data-key="showZombies" title="Toggle zombies (Z)">Z <span></span></button>
<button class="count-badge" id="badge-animals" data-key="showAnimals" title="Toggle animals (A)">A <span></span></button>
<button class="count-badge" id="badge-vehicles" data-key="showVehicles" title="Toggle vehicles (V)">V <span></span></button>
<button class="count-badge" id="badge-bullets" data-key="showBullets" title="Toggle bullets (B)">B <span></span></button>
<button class="count-badge" id="badge-loot" data-key="showLoot" title="Toggle loot (L)">L <span></span></button>
</div>
<div class="label-settings-actions">
<button class="collapse-button" id="labelSettingsReset" type="button">Reset</button>
<button class="collapse-button" id="labelSettingsClose" type="button" aria-label="Close label settings"></button>
<button class="header-btn combat-btn" id="combatToggle" title="Combat mode (C)">⚔ Combat</button>
<button class="header-btn mode3d-btn" id="toggle3d" title="3D map mode" disabled>3D</button>
</header>
<!-- ── Sidebar ────────────────────────────────────────────────────── -->
<aside class="sidebar" id="sidebar">
<!-- Tab icons column -->
<nav class="sidebar-nav">
<button class="tab-btn active" data-tab="tab-map" title="Map">🗺</button>
<button class="tab-btn" data-tab="tab-entities" title="Entities">👤</button>
<button class="tab-btn" data-tab="tab-loot" title="Loot">📦</button>
<button class="tab-btn" data-tab="tab-settings" title="Settings"></button>
</nav>
<!-- Tab panels -->
<div class="sidebar-content" id="sidebarContent">
<!-- Tab: Map -------------------------------------------------- -->
<section class="tab-panel active" id="tab-map">
<div class="tab-status-row">
<span id="status" class="status-text">Connecting…</span>
</div>
<div id="serverInfo" class="server-info-text"></div>
<div class="tab-section">
<label class="toggle-row"><input type="checkbox" id="followPlayer"><span>Follow player <kbd>F</kbd></span></label>
<label class="toggle-row"><input type="checkbox" id="showLabels" checked><span>Labels</span></label>
<label class="toggle-row"><input type="checkbox" id="showPOIs" checked><span>Locations</span></label>
<label class="toggle-row"><input type="checkbox" id="showGrid"><span>Grid <kbd>G</kbd></span></label>
<label class="toggle-row"><input type="checkbox" id="showSatellite"><span>Satellite</span></label>
<label class="toggle-row"><input type="checkbox" id="showSatmap"><span>Sat map</span></label>
<label class="toggle-row"><input type="checkbox" id="showDistanceRings"><span>Distance rings</span></label>
<label class="toggle-row"><input type="checkbox" id="showMinimap"><span>Minimap</span></label>
</div>
<div class="tab-section">
<div class="section-label">Distance filter</div>
<div class="range-with-value">
<input type="range" id="distanceFilter" min="0" max="5000" step="50" value="2000">
<span id="distanceValue" class="range-val">2000 m</span>
</div>
</div>
<div class="label-settings-body" id="labelSettingsBody">
<label class="setting-card" for="ordinaryLootSpread">
<span class="setting-title">Ordinary loot spread</span>
<span class="setting-value" id="ordinaryLootSpreadValue">2.0x</span>
</section>
<!-- Tab: Entities --------------------------------------------- -->
<section class="tab-panel" id="tab-entities">
<div class="preset-bar">
<button class="preset-btn" data-preset="0" title="Load combat preset (1) — Shift+click to overwrite">Combat</button>
<button class="preset-btn" data-preset="1" title="Load loot run preset (2) — Shift+click to overwrite">Loot</button>
<button class="preset-btn" data-preset="2" title="Load full preset (3) — Shift+click to overwrite">Full</button>
<button class="preset-btn" data-preset="3" title="Load custom preset (4) — Shift+click to overwrite">Custom</button>
</div>
<div class="quick-bar">
<button class="quick-btn" id="showAllEntities">All</button>
<button class="quick-btn" id="hideAllEntities">None</button>
</div>
<div id="entityFilterList" class="filter-list"></div>
<div class="tab-section">
<div class="section-label">Players on server</div>
<div class="players-list" id="playersList"></div>
</div>
</section>
<!-- Tab: Loot ------------------------------------------------- -->
<section class="tab-panel" id="tab-loot">
<div class="loot-search-bar">
<input type="search" id="lootSearch" placeholder="Search loot…" autocomplete="off" spellcheck="false">
</div>
<div class="tab-section">
<label class="toggle-row"><input type="checkbox" id="showLoot" checked><span>Show all loot</span></label>
</div>
<div id="lootFilterList" class="filter-list loot-filter-list"></div>
<div class="tab-section">
<div id="lootInfo" class="loot-info-text"></div>
<div class="loot-list" id="lootList"></div>
</div>
</section>
<!-- Tab: Settings --------------------------------------------- -->
<section class="tab-panel" id="tab-settings">
<div class="tab-section">
<div class="section-label">Label placement</div>
<label class="setting-row">
<span class="setting-name">Ordinary loot spread</span>
<span id="ordinaryLootSpreadValue" class="setting-val">2.0x</span>
<input type="range" id="ordinaryLootSpread" min="1" max="4" step="0.1" value="2">
<span class="setting-help">Controls how far regular loot labels may move away from the point to avoid collisions.</span>
</label>
<label class="setting-card" for="groupedLootSpread">
<span class="setting-title">Grouped loot spread</span>
<span class="setting-value" id="groupedLootSpreadValue">2.0x</span>
<label class="setting-row">
<span class="setting-name">Grouped loot spread</span>
<span id="groupedLootSpreadValue" class="setting-val">2.0x</span>
<input type="range" id="groupedLootSpread" min="1" max="4" step="0.1" value="2">
<span class="setting-help">Controls how far merged same-name loot labels may sit from the cluster center.</span>
</label>
<label class="setting-card" for="sameLootMergeRadius">
<span class="setting-title">Same-name merge radius</span>
<span class="setting-value" id="sameLootMergeRadiusValue">110 px</span>
<label class="setting-row">
<span class="setting-name">Same-name merge radius</span>
<span id="sameLootMergeRadiusValue" class="setting-val">110 px</span>
<input type="range" id="sameLootMergeRadius" min="20" max="240" step="5" value="110">
<span class="setting-help">Items with the same name inside this screen radius share one text label with multiple connector lines.</span>
</label>
<label class="setting-card" for="lineAnchorMode">
<span class="setting-title">Line anchor on text</span>
<label class="setting-row">
<span class="setting-name">Line anchor</span>
<select id="lineAnchorMode">
<option value="auto-nearest">Auto nearest</option>
<option value="top-left">Top left</option>
@@ -56,79 +136,98 @@
<option value="bottom-left">Bottom left</option>
<option value="bottom-right">Bottom right</option>
</select>
<span class="setting-help">Chooses from which corner or side midpoint of the text the connector line starts. Auto nearest selects the closest standard point on the text rectangle to the item point.</span>
</label>
<label class="setting-card toggle-setting">
<span class="setting-title">Merge same-name loot labels</span>
<label class="setting-row toggle-row">
<span class="setting-name">Merge same-name loot</span>
<input type="checkbox" id="mergeSameLootLabels" checked>
<span class="setting-help">When enabled, nearby ordinary loot with the same name is shown as one label with several lines.</span>
</label>
</div>
</aside>
<aside class="players-panel" id="playersPanel">
<div class="players-panel-header">
<strong>Players</strong>
<button class="collapse-button" id="playersClose" type="button" aria-label="Close players"></button>
</div>
<div class="players-server" id="serverInfo">Server: loading...</div>
<div class="players-list" id="playersList"></div>
</aside>
<aside class="loot-panel" id="lootPanel">
<div class="players-panel-header">
<strong>Ground Loot</strong>
<button class="collapse-button" id="lootClose" type="button" aria-label="Close loot"></button>
</div>
<div class="players-server" id="lootInfo">Loot: loading...</div>
<div class="loot-list" id="lootList"></div>
</aside>
<div class="shell">
<aside class="panel" id="settingsPanel">
<div class="panel-header">
<div>
<h1>DayZ Web Map</h1>
<div id="status">Connecting...</div>
<div id="serverBadge" class="server-badge">Server: loading...</div>
</div>
<button class="collapse-button" id="collapseButton" type="button" aria-label="Hide filters"></button>
</div>
<div class="panel-body" id="settingsPanelBody">
<div class="group">
<div class="group-title">Controls</div>
<label class="toggle-row"><input type="checkbox" id="followPlayer"><span>Follow player</span></label>
<label class="toggle-row"><input type="checkbox" id="showLabels" checked><span>Global labels</span></label>
<label class="toggle-row"><input type="checkbox" id="showLoot" checked><span>All loot</span></label>
<button class="btn-sm" id="labelSettingsReset">Reset defaults</button>
</div>
<div class="group">
<div class="group-title">Entity Filters</div>
<div id="entityFilterList" class="filter-list"></div>
<div class="tab-section">
<div class="section-label">Default text size</div>
<div class="range-with-value">
<input type="range" id="textSize" min="10" max="24" step="1" value="14">
<span id="textSizeValue" class="range-val">14 px</span>
</div>
</div>
<div class="group">
<div class="group-title">Loot Filters</div>
<div id="lootFilterList" class="filter-list"></div>
<div class="tab-section">
<div class="section-label">Theme</div>
<div class="theme-bar">
<button class="theme-btn active" data-theme="dark">Dark</button>
<button class="theme-btn" data-theme="darker">Darker</button>
<button class="theme-btn" data-theme="solarized">Solarized</button>
</div>
</div>
<div class="group">
<div class="group-title">Display</div>
<label class="range-row" for="distanceFilter"><span>Distance filter</span><input type="range" id="distanceFilter" min="0" max="5000" step="50" value="2000"></label>
<div id="distanceValue">2000 m</div>
<label class="range-row" for="textSize"><span>Default text size</span><input type="range" id="textSize" min="10" max="24" step="1" value="14"></label>
<div id="textSizeValue">14 px</div>
<div class="tab-section shortcuts-section">
<div class="section-label">Keyboard shortcuts</div>
<div class="shortcut-grid">
<kbd>P</kbd><span>Players</span>
<kbd>Z</kbd><span>Zombies</span>
<kbd>A</kbd><span>Animals</span>
<kbd>V</kbd><span>Vehicles</span>
<kbd>L</kbd><span>Loot</span>
<kbd>B</kbd><span>Bullets</span>
<kbd>C</kbd><span>Combat mode</span>
<kbd>F</kbd><span>Follow player</span>
<kbd>G</kbd><span>Grid overlay</span>
<kbd>M</kbd><span>Measure tool</span>
<kbd>Space</kbd><span>Re-center</span>
<kbd>Esc</kbd><span>Close / cancel</span>
<kbd>14</kbd><span>Load preset</span>
</div>
</div>
</section>
<div class="hint">Drag to move. Use wheel or pinch to zoom. Open a filter card to tune color, marker size, text size and labels.</div>
</div>
</div><!-- /sidebar-content -->
</aside>
<!-- ── Map viewport ───────────────────────────────────────────────── -->
<main class="viewport" id="viewport">
<!-- 3D canvas — hidden until 3D mode is activated -->
<canvas id="canvas3d"></canvas>
<div class="canvas" id="canvas">
<div id="tiles"></div>
<div id="sat-tiles"></div>
<div id="topo-tiles"></div>
<svg id="paths"></svg>
<div id="itemLabels"></div>
<div id="markers"></div>
</div>
</main>
<!-- Minimap (bottom-right corner of viewport) -->
<div class="minimap" id="minimapEl" style="display:none">
<img class="minimap-img" id="minimapImg" alt="">
<canvas class="minimap-canvas" id="minimapCanvas"></canvas>
</div>
<script src="/app.js"></script>
<!-- Right-click context menu -->
<div class="ctx-menu" id="ctxMenu">
<button class="ctx-item" id="ctxAddWaypoint">Add waypoint</button>
<button class="ctx-item" id="ctxCopyCoords">Copy coordinates</button>
</div>
<!-- Screen-space measurement overlay -->
<canvas class="measure-layer" id="measureLayer"></canvas>
</main>
<!-- ── Coord bar ──────────────────────────────────────────────────── -->
<footer class="coord-bar" id="coordBar">
<span id="coordDisplay"></span>
<span class="coord-sep">|</span>
<span id="gridDisplay"></span>
<span class="coord-sep">|</span>
<span id="zoomDisplay"></span>
<span class="coord-bar-spacer"></span>
<button class="measure-toggle-btn" id="measureToggle" title="Measure distance (M)">⟺ Measure</button>
</footer>
<!-- ── Toast notifications ────────────────────────────────────────── -->
<div class="toast-stack" id="toastStack"></div>
<script src="app.js?v=7"></script>
</body>
</html>
+958 -543
View File
File diff suppressed because it is too large Load Diff