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:
+18
@@ -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
@@ -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
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
Vendored
+49
-33
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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
@@ -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, 0x6A0–0x6A7)
|
||||
// 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
|
||||
|
||||
+548
-713
File diff suppressed because it is too large
Load Diff
+64
-122
@@ -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,78 +42,33 @@ 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;
|
||||
|
||||
// Menu state
|
||||
@@ -109,86 +76,51 @@ private:
|
||||
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
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+184
-85
@@ -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>1–4</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>
|
||||
|
||||
+949
-534
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user