feat: add ballistic overlay and improve DMA stability
- Add ballistic aim dot and bullet trail rendering with menu toggles - Read local weapon ammo data for prediction inputs - Rework bone scatter reads to per-bone pages with cached skeleton fallback - Add DMA throughput stats to the overlay info panel - Improve signature scanning with page-based scatter reads - Cache encoded map tiles and support cross-origin/static web radar hosting - Add Caddy config for serving the web radar UI
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
# TODO
|
||||
|
||||
(none)
|
||||
item esp doesnt render past a certain distance
|
||||
inventory detection
|
||||
ballistic prediction
|
||||
combat mode on web radar
|
||||
create a grid coord system for web radar
|
||||
## TO FIX
|
||||
- item esp doesnt render past a certain distance
|
||||
- inventory detection
|
||||
- ballistic prediction
|
||||
- combat mode on web radar
|
||||
- combat mode makes web radar only show players and maybe does some other combat only visuals
|
||||
- create a grid coord system for web radar
|
||||
|
||||
## 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
|
||||
Vendored
+146
-76
@@ -220,47 +220,86 @@ static void lumin_kv(const char* label, const std::string& value, const c_vec4&
|
||||
draw->text_clipped(w->DrawList, font->get(inter_medium, 11), rect.Min, rect.Max - s_(12, 0), draw->get_clr(value_clr), value.data(), 0, 0, { 1.f, 0.5f });
|
||||
}
|
||||
|
||||
// ---- ESP / Items content --------------------------------------------------
|
||||
// ---- Tab content ----------------------------------------------------------
|
||||
|
||||
static void render_esp_tab(const visual_widget_filter& w, float section_height)
|
||||
static void render_aim_tab(const visual_widget_filter& w, float section_height)
|
||||
{
|
||||
if (!g_menu)
|
||||
return;
|
||||
|
||||
gui->begin_group();
|
||||
begin_full_section("Aim Assistance", section_height);
|
||||
{
|
||||
begin_visual_section("Entities", section_height);
|
||||
w.checkbox("Players", "Show player ESP", g_menu->showPlayers);
|
||||
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);
|
||||
w.checkbox("Corpses", "Show dead bodies", g_menu->showCorpses);
|
||||
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("Skeleton Debug", "Label every named bone", g_menu->debugSkeleton);
|
||||
end_visual_section();
|
||||
lumin_heading("Prediction");
|
||||
w.checkbox("Ballistic Dot", "Cyan dot showing where to aim to hit centre mass (accounts for gravity + drag)", g_menu->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);
|
||||
}
|
||||
gui->end_group();
|
||||
|
||||
gui->sameline();
|
||||
|
||||
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("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");
|
||||
end_visual_section();
|
||||
}
|
||||
gui->end_group();
|
||||
end_visual_section();
|
||||
}
|
||||
|
||||
static void render_items_tab(const visual_widget_filter& w, float section_height)
|
||||
static void render_visuals_tab(const visual_widget_filter& w, float section_height)
|
||||
{
|
||||
if (!g_menu)
|
||||
return;
|
||||
|
||||
if (var->gui.sub_tab_stored == 1)
|
||||
{
|
||||
// ---- Players sub-tab ----
|
||||
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("Skeleton Debug", "Label every named bone", g_menu->debugSkeleton);
|
||||
end_visual_section();
|
||||
}
|
||||
gui->end_group();
|
||||
|
||||
gui->sameline();
|
||||
|
||||
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");
|
||||
end_visual_section();
|
||||
}
|
||||
gui->end_group();
|
||||
}
|
||||
else
|
||||
{
|
||||
// ---- World sub-tab ----
|
||||
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);
|
||||
end_visual_section();
|
||||
}
|
||||
gui->end_group();
|
||||
|
||||
gui->sameline();
|
||||
|
||||
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");
|
||||
end_visual_section();
|
||||
}
|
||||
gui->end_group();
|
||||
}
|
||||
}
|
||||
|
||||
static void render_loot_tab(const visual_widget_filter& w, float section_height)
|
||||
{
|
||||
if (!g_menu || !g_menu->itemCategories)
|
||||
return;
|
||||
@@ -320,13 +359,17 @@ static void render_items_tab(const visual_widget_filter& w, float section_height
|
||||
gui->end_group();
|
||||
}
|
||||
|
||||
static constexpr const char* kRadarDomain = "radar.charliecharliekirky.christmas";
|
||||
|
||||
static void render_info_tab(float section_height)
|
||||
{
|
||||
const c_vec4 green = c_col(75, 225, 145).Value;
|
||||
const c_vec4 orange = c_col(255, 175, 75).Value;
|
||||
const c_vec4 red = c_col(225, 70, 70).Value;
|
||||
|
||||
begin_full_section("Info", section_height);
|
||||
begin_full_section("Info", section_height, true);
|
||||
{
|
||||
// ---- Server ----
|
||||
lumin_heading("Server");
|
||||
if (g_menu && g_menu->connected)
|
||||
{
|
||||
@@ -353,26 +396,39 @@ static void render_info_tab(float section_height)
|
||||
{
|
||||
lumin_kv("Status", (g_menu && !g_menu->status.empty()) ? g_menu->status : "Offline", orange);
|
||||
}
|
||||
}
|
||||
end_visual_section();
|
||||
}
|
||||
|
||||
static constexpr const char* kRadarDomain = "radar.charliecharliekirky.christmas";
|
||||
// ---- DMA Handle ----
|
||||
gui->dummy(c_vec2(0, s_(6)));
|
||||
lumin_heading("DMA Handle");
|
||||
if (g_menu)
|
||||
{
|
||||
lumin_kv("Handle", g_menu->dmaAttached ? "Attached" : "Detached",
|
||||
g_menu->dmaAttached ? green : red);
|
||||
|
||||
static void render_radar_tab(float section_height)
|
||||
{
|
||||
begin_full_section("Radar", section_height, true);
|
||||
{
|
||||
char buf[64];
|
||||
ImFormatString(buf, IM_ARRAYSIZE(buf), "%.2f MB/s", g_menu->dmaReadMBps);
|
||||
lumin_kv("Read Speed", buf, clr->accent.Value);
|
||||
|
||||
ImFormatString(buf, IM_ARRAYSIZE(buf), "%.0f /s", g_menu->dmaScatterOps);
|
||||
lumin_kv("Scatter Calls", buf, clr->accent.Value);
|
||||
|
||||
ImFormatString(buf, IM_ARRAYSIZE(buf), "%.3f GB", g_menu->dmaTotalGB);
|
||||
lumin_kv("Total Read", buf, clr->text.Value);
|
||||
|
||||
ImFormatString(buf, IM_ARRAYSIZE(buf), "%llu", static_cast<unsigned long long>(g_menu->dmaTotalOps));
|
||||
lumin_kv("Total Ops", buf, clr->text.Value);
|
||||
}
|
||||
|
||||
// ---- Web Radar ----
|
||||
gui->dummy(c_vec2(0, s_(6)));
|
||||
lumin_heading("Web Radar");
|
||||
lumin_note("Open this address in a browser:");
|
||||
gui->dummy(c_vec2(0, s_(4)));
|
||||
if (widgets->action_button(kRadarDomain, "copy"))
|
||||
ImGui::SetClipboardText(kRadarDomain);
|
||||
|
||||
if (g_menu)
|
||||
{
|
||||
gui->dummy(c_vec2(0, s_(6)));
|
||||
lumin_heading("Connection");
|
||||
gui->dummy(c_vec2(0, s_(4)));
|
||||
lumin_kv("Port", std::to_string(g_menu->webPort), clr->accent.Value);
|
||||
lumin_kv("Password", "Set in config.cfg", clr->text.Value);
|
||||
}
|
||||
@@ -380,24 +436,7 @@ static void render_radar_tab(float section_height)
|
||||
end_visual_section();
|
||||
}
|
||||
|
||||
static void render_exit_tab(float section_height)
|
||||
{
|
||||
begin_full_section("Exit", section_height);
|
||||
{
|
||||
lumin_heading("Exit Application");
|
||||
lumin_note("This will close the overlay and exit the program.");
|
||||
gui->dummy(c_vec2(0, s_(10)));
|
||||
|
||||
const c_col saved_accent = clr->accent;
|
||||
clr->accent = c_col(225, 70, 70);
|
||||
if (widgets->primary_button("Exit", "back") && g_menu && g_menu->onExit)
|
||||
g_menu->onExit();
|
||||
clr->accent = saved_accent;
|
||||
}
|
||||
end_visual_section();
|
||||
}
|
||||
|
||||
static void render_settings_tab(float section_height)
|
||||
static void render_config_tab(float section_height)
|
||||
{
|
||||
if (!g_menu)
|
||||
return;
|
||||
@@ -416,7 +455,7 @@ static void render_settings_tab(float section_height)
|
||||
buffers_ready = true;
|
||||
}
|
||||
|
||||
begin_full_section("Settings", section_height, true);
|
||||
begin_full_section("Config", section_height, true);
|
||||
{
|
||||
lumin_heading("Overlay (Monitor) Resolution");
|
||||
lumin_note("Set to your MONITOR size. 0 x 0 = auto-detect.");
|
||||
@@ -449,6 +488,16 @@ static void render_settings_tab(float section_height)
|
||||
gui->dummy(c_vec2(0, s_(4)));
|
||||
if (widgets->primary_button("Save Config", "active") && g_menu->onSaveConfig)
|
||||
g_menu->onSaveConfig();
|
||||
|
||||
gui->dummy(c_vec2(0, s_(10)));
|
||||
lumin_heading("Exit");
|
||||
lumin_note("This will close the overlay and exit the program.");
|
||||
gui->dummy(c_vec2(0, s_(4)));
|
||||
const c_col saved_accent = clr->accent;
|
||||
clr->accent = c_col(225, 70, 70);
|
||||
if (widgets->primary_button("Exit", "back") && g_menu->onExit)
|
||||
g_menu->onExit();
|
||||
clr->accent = saved_accent;
|
||||
}
|
||||
end_visual_section();
|
||||
}
|
||||
@@ -491,7 +540,7 @@ void c_gui::render()
|
||||
gui->set_pos(c_vec2(s_(visual_outer_padding), s_(top_row_y)), pos_all);
|
||||
gui->begin_content("TopBrand", c_vec2(s_(visual_sidebar_width), s_(top_row_height)), s_(0, 0), s_(0, 0), window_flags_no_scrollbar | window_flags_no_background, child_flags_none);
|
||||
{
|
||||
widgets->brand_header("KarachiHook", var->gui.profile_name[0] != '\0' ? var->gui.profile_name : "DayZ Overlay");
|
||||
widgets->brand_header("DayZ DMA", var->gui.profile_name[0] != '\0' ? var->gui.profile_name : "DayZ Overlay");
|
||||
}
|
||||
gui->end_content();
|
||||
|
||||
@@ -512,12 +561,11 @@ void c_gui::render()
|
||||
}
|
||||
}
|
||||
|
||||
widgets->tab_button("ESP", "visuals", 1);
|
||||
widgets->tab_button("Items", "loot", 2);
|
||||
widgets->tab_button("Radar", "world", 3);
|
||||
widgets->tab_button("Settings", "polish", 4);
|
||||
widgets->tab_button("Exit", "back", 5);
|
||||
widgets->tab_button("Info", "stats", 6);
|
||||
widgets->tab_button("Aim", "aim", 1);
|
||||
widgets->tab_button("Visuals", "visuals", 2);
|
||||
widgets->tab_button("Loot", "loot", 3);
|
||||
widgets->tab_button("Info", "stats", 4);
|
||||
widgets->tab_button("Config", "polish", 5);
|
||||
|
||||
{
|
||||
static c_vec4 sidebar_indicator = c_vec4(0, 0, 0, 0);
|
||||
@@ -555,6 +603,29 @@ void c_gui::render()
|
||||
gui->begin_content("FeatureHeader", c_vec2(s_(feature_width), s_(feature_header_height)), s_(8, 4), s_(8, 0), window_flags_no_scrollbar | window_flags_no_background, child_flags_none);
|
||||
{
|
||||
const float search_width = s_(178.f);
|
||||
|
||||
if (var->gui.tab == 2) // Visuals — show sub-tab row
|
||||
{
|
||||
// Animated pill highlight behind the selected sub_tab_button.
|
||||
static c_vec4 pill_anim = c_vec4(0, 0, 0, 0);
|
||||
c_window* pill_win = gui->get_window();
|
||||
gui->easing(pill_anim, g_pill_selected_rect, 18.f, dynamic_easing);
|
||||
if (pill_anim.z > pill_anim.x + 1.f)
|
||||
draw->rect_filled(pill_win->DrawList,
|
||||
c_vec2(pill_anim.x, pill_anim.y), c_vec2(pill_anim.z, pill_anim.w),
|
||||
draw->get_clr(clr->widget), s_(9.1f));
|
||||
|
||||
widgets->sub_tab_button("Players", "player", 1);
|
||||
gui->sameline(0.f, s_(4.f));
|
||||
widgets->sub_tab_button("World", "world", 2);
|
||||
// Keep on the same row; jump cursor to right edge for search.
|
||||
ImGui::SameLine(ImGui::GetContentRegionMax().x - search_width);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - search_width);
|
||||
}
|
||||
|
||||
widgets->search_field("Search", var->gui.feature_search, IM_ARRAYSIZE(var->gui.feature_search), c_vec2(search_width, s_(32.f)));
|
||||
}
|
||||
gui->end_content();
|
||||
@@ -572,12 +643,11 @@ void c_gui::render()
|
||||
|
||||
switch (var->gui.tab)
|
||||
{
|
||||
case 1: render_esp_tab(visual_widgets, section_height); break;
|
||||
case 2: render_items_tab(visual_widgets, section_height); break;
|
||||
case 3: render_radar_tab(section_height); break;
|
||||
case 4: render_settings_tab(section_height); break;
|
||||
case 5: render_exit_tab(section_height); break;
|
||||
case 6: render_info_tab(section_height); break;
|
||||
case 1: render_aim_tab(visual_widgets, section_height); break;
|
||||
case 2: render_visuals_tab(visual_widgets, section_height); break;
|
||||
case 3: render_loot_tab(visual_widgets, section_height); break;
|
||||
case 4: render_info_tab(section_height); break;
|
||||
case 5: render_config_tab(section_height); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -17,11 +17,13 @@ struct OverlayConfig {
|
||||
bool showSkeleton = true; // bone skeleton lines
|
||||
|
||||
// ---- Extra ESP options ----
|
||||
bool showHeadDot = false; // dot at head bone for players/zombies
|
||||
bool showCorpses = false; // show dead players/zombies (dimmed grey)
|
||||
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 showHeadDot = false; // dot at head bone for players/zombies
|
||||
bool showCorpses = false; // show dead players/zombies (dimmed grey)
|
||||
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
|
||||
|
||||
// ---- Draw-distance limits (metres) ----
|
||||
float playerMaxDist = 1000.0f;
|
||||
|
||||
@@ -36,6 +36,14 @@ bool VmmAccessor::Initialize(bool useMemoryMap) {
|
||||
void VmmAccessor::ForceRefresh() {
|
||||
if (!IsInitialized()) return;
|
||||
VMMDLL_ConfigSet(m_dma->handle.get(), VMMDLL_OPT_REFRESH_ALL, 1);
|
||||
// VMMDLL_OPT_REFRESH_ALL resets per-process DTB back to the kernel default,
|
||||
// losing the fix_cr3 result. Re-apply immediately so subsequent reads still
|
||||
// use the correct UserDirectoryTableBase paging context.
|
||||
if (m_process && !m_processName.empty()) {
|
||||
if (!m_process->fix_cr3(m_processName)) {
|
||||
spdlog::warn("VmmAccessor: fix_cr3 re-apply failed after ForceRefresh.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -64,6 +72,7 @@ bool VmmAccessor::TryFindProcess(const std::string& processName,
|
||||
std::unique_lock<std::shared_mutex> lk(m_accessMutex);
|
||||
m_process = std::make_unique<Process>(*m_dma, processName);
|
||||
m_attachedPid = static_cast<uint32_t>(pid);
|
||||
m_processName = processName;
|
||||
outPid = m_attachedPid;
|
||||
|
||||
// Apply the CR3 fix so reads use UserDirectoryTableBase instead of the
|
||||
@@ -119,7 +128,9 @@ bool VmmAccessor::ReadRaw(uint32_t /*pid*/, uint64_t address,
|
||||
if (!buf || size == 0) return false;
|
||||
std::shared_lock<std::shared_mutex> lk(m_accessMutex);
|
||||
if (!m_process) return false;
|
||||
return m_process->read(address, buf, size);
|
||||
bool ok = m_process->read(address, buf, size);
|
||||
if (ok) m_bytesReadTotal.fetch_add(size, std::memory_order_relaxed);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -158,6 +169,12 @@ bool VmmAccessor::ScatterRead(uint32_t pid, std::vector<ScatterEntry>& entries)
|
||||
|
||||
m_process->execute_scatter(hScatter, static_cast<DWORD>(pid));
|
||||
m_process->close_scatter(hScatter);
|
||||
|
||||
// Account for all scheduled bytes (VolkDMA zero-fills failures; total is still representative).
|
||||
size_t totalBytes = 0;
|
||||
for (const auto& e : entries) totalBytes += e.size;
|
||||
m_bytesReadTotal.fetch_add(totalBytes, std::memory_order_relaxed);
|
||||
m_scatterOpsTotal.fetch_add(1, std::memory_order_relaxed);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -275,6 +292,39 @@ std::string VmmAccessor::ReadArmaString(uint32_t pid, uint64_t address,
|
||||
// ReadCString
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GetStats
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
DmaStats VmmAccessor::GetStats() const {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
const uint64_t bytes = m_bytesReadTotal.load(std::memory_order_relaxed);
|
||||
const uint64_t ops = m_scatterOpsTotal.load(std::memory_order_relaxed);
|
||||
|
||||
const auto now = Clock::now();
|
||||
const double dt = std::chrono::duration<double>(now - m_statsLastSample).count();
|
||||
|
||||
// Recompute at most 10 Hz to avoid thrashing on high-frequency overlay calls.
|
||||
if (dt >= 0.1) {
|
||||
const double bytesDelta = static_cast<double>(bytes - m_statsLastBytes);
|
||||
const double opsDelta = static_cast<double>(ops - m_statsLastOps);
|
||||
m_statsMBps = static_cast<float>(bytesDelta / dt / (1024.0 * 1024.0));
|
||||
m_statsOpsPs = static_cast<float>(opsDelta / dt);
|
||||
m_statsLastSample = now;
|
||||
m_statsLastBytes = bytes;
|
||||
m_statsLastOps = ops;
|
||||
}
|
||||
|
||||
DmaStats s;
|
||||
s.attached = IsAttached();
|
||||
s.readMBps = m_statsMBps;
|
||||
s.scatterOpsPs = m_statsOpsPs;
|
||||
s.totalGB = static_cast<float>(bytes) / (1024.f * 1024.f * 1024.f);
|
||||
s.totalOps = ops;
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string VmmAccessor::ReadCString(uint32_t pid, uint64_t address,
|
||||
size_t maxLength) {
|
||||
if (!MemoryValidation::IsValidUserAddress(address)) return {};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
@@ -7,6 +9,15 @@
|
||||
#include <type_traits>
|
||||
#include <cstring> // memcpy
|
||||
|
||||
// Rolling 1-second DMA throughput snapshot, refreshed by GetStats().
|
||||
struct DmaStats {
|
||||
float readMBps = 0.f; // bytes read per second (MB)
|
||||
float scatterOpsPs = 0.f; // scatter-read calls per second
|
||||
float totalGB = 0.f; // total bytes read since attach (GB)
|
||||
uint64_t totalOps = 0; // total scatter calls since attach
|
||||
bool attached = false;
|
||||
};
|
||||
|
||||
// Windows must come before vmmdll / VolkDMA headers.
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -63,6 +74,10 @@ public:
|
||||
/// Force a full refresh of VMMDLL's internal caches.
|
||||
void ForceRefresh();
|
||||
|
||||
/// Sample rolling throughput and return a DmaStats snapshot.
|
||||
/// Rates are computed over the interval since the previous call (min 100 ms).
|
||||
[[nodiscard]] DmaStats GetStats() const;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Memory reads
|
||||
// ------------------------------------------------------------------
|
||||
@@ -121,6 +136,7 @@ private:
|
||||
std::unique_ptr<DMA> m_dma;
|
||||
std::unique_ptr<Process> m_process;
|
||||
uint32_t m_attachedPid = 0;
|
||||
std::string m_processName; // stored so fix_cr3 can be re-run after ForceRefresh
|
||||
|
||||
// Guards m_process / m_dma lifetime against concurrent reads.
|
||||
// Reads (ReadRaw / ScatterRead) take a SHARED lock so they run in parallel;
|
||||
@@ -129,6 +145,17 @@ private:
|
||||
// runtime thread without a use-after-free when a reconnect re-creates Process.
|
||||
mutable std::shared_mutex m_accessMutex;
|
||||
|
||||
// Cumulative counters — incremented from any thread via atomics.
|
||||
std::atomic<uint64_t> m_bytesReadTotal{0};
|
||||
std::atomic<uint64_t> m_scatterOpsTotal{0};
|
||||
|
||||
// Mutable cache for GetStats() rolling-window computation (overlay thread only).
|
||||
mutable std::chrono::steady_clock::time_point m_statsLastSample{};
|
||||
mutable uint64_t m_statsLastBytes = 0;
|
||||
mutable uint64_t m_statsLastOps = 0;
|
||||
mutable float m_statsMBps = 0.f;
|
||||
mutable float m_statsOpsPs = 0.f;
|
||||
|
||||
/// Low-level read via VolkDMA Process. Returns false on read error.
|
||||
bool ReadRaw(uint32_t pid, uint64_t address, void* buf, size_t size);
|
||||
|
||||
|
||||
@@ -131,6 +131,15 @@ namespace Offsets {
|
||||
// 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]
|
||||
|
||||
+162
-20
@@ -65,10 +65,15 @@ static SkeletonBones LerpBones(const SkeletonBones& a, const SkeletonBones& b, f
|
||||
return out;
|
||||
}
|
||||
|
||||
// Update the history when a new DMA read is detected (head bone moved).
|
||||
// Update the history when a new DMA bone read arrives.
|
||||
// lastReadMs is always updated (drives the staleness gate — prevents blink for
|
||||
// stationary players). currMs / prevMs are only updated on positional change
|
||||
// so the extrapolation velocity is derived from actual player movement, not
|
||||
// every-frame DMA polling.
|
||||
static void UpdateBoneHistory(GameOverlay::BoneHistory& h,
|
||||
const SkeletonBones& bones, int64_t nowMs)
|
||||
{
|
||||
h.lastReadMs = nowMs; // always stamp: skeleton visible while reads keep arriving
|
||||
if (!h.initialized) {
|
||||
h.prev = h.curr = bones;
|
||||
h.prevMs = h.currMs = nowMs;
|
||||
@@ -81,9 +86,6 @@ static void UpdateBoneHistory(GameOverlay::BoneHistory& h,
|
||||
float dist2 = dx*dx + dy*dy + dz*dz;
|
||||
// Reject implausible jumps: > 3 m in one update cycle can't happen at
|
||||
// any in-game speed — it means the scatter read returned garbage.
|
||||
// Keeping the old history prevents one stale read from corrupting the
|
||||
// extrapolation state; the DMA-side eviction (cache->valid=false) will
|
||||
// fix the pointer within the next boneRefreshMs window.
|
||||
if (dist2 > 9.0f) return;
|
||||
if (dist2 > 1e-8f) {
|
||||
h.prev = h.curr;
|
||||
@@ -239,9 +241,11 @@ void GameOverlay::SyncConfig() {
|
||||
m_cfg.showSkeleton = m_showSkeleton;
|
||||
m_cfg.showHeadDot = m_showHeadDot;
|
||||
m_cfg.showCorpses = m_showCorpses;
|
||||
m_cfg.showWeapon = m_showWeapon;
|
||||
m_cfg.showHealthBar = m_showHealthBar;
|
||||
m_cfg.showHealthNumber = m_showHealthNumber;
|
||||
m_cfg.showWeapon = m_showWeapon;
|
||||
m_cfg.showHealthBar = m_showHealthBar;
|
||||
m_cfg.showHealthNumber = m_showHealthNumber;
|
||||
m_cfg.showBallisticDot = m_showBallisticDot;
|
||||
m_cfg.showBulletTrails = m_showBulletTrails;
|
||||
m_cfg.itemCategories = m_itemCategories;
|
||||
m_cfg.playerMaxDist = m_playerMaxDist;
|
||||
m_cfg.animalMaxDist = m_animalMaxDist;
|
||||
@@ -261,6 +265,9 @@ void GameOverlay::Draw(float w, float h) {
|
||||
// Record frame time once; used by all bone-smoothing calls this frame.
|
||||
m_frameTimeMs = NowMs();
|
||||
|
||||
// Feed live bullet positions into the overlay-owned trail tracker.
|
||||
m_bulletTracks.Update(u.bullets, m_frameTimeMs);
|
||||
|
||||
// Feed new DMA bone reads into the per-entity history so extrapolation
|
||||
// always has the two most recent samples to work from.
|
||||
for (const auto& p : u.players)
|
||||
@@ -308,11 +315,13 @@ void GameOverlay::Draw(float w, float h) {
|
||||
bridge.showBox = &m_showBox;
|
||||
bridge.showSkeleton = &m_showSkeleton;
|
||||
bridge.showHeadDot = &m_showHeadDot;
|
||||
bridge.showWeapon = &m_showWeapon;
|
||||
bridge.showHealthBar = &m_showHealthBar;
|
||||
bridge.showHealthNumber = &m_showHealthNumber;
|
||||
bridge.showCorpses = &m_showCorpses;
|
||||
bridge.debugSkeleton = &m_debugSkeleton;
|
||||
bridge.showWeapon = &m_showWeapon;
|
||||
bridge.showHealthBar = &m_showHealthBar;
|
||||
bridge.showHealthNumber = &m_showHealthNumber;
|
||||
bridge.showCorpses = &m_showCorpses;
|
||||
bridge.debugSkeleton = &m_debugSkeleton;
|
||||
bridge.showBallisticDot = &m_showBallisticDot;
|
||||
bridge.showBulletTrails = &m_showBulletTrails;
|
||||
|
||||
bridge.playerMaxDist = &m_playerMaxDist;
|
||||
bridge.animalMaxDist = &m_animalMaxDist;
|
||||
@@ -350,6 +359,15 @@ void GameOverlay::Draw(float w, float h) {
|
||||
bridge.webPort = m_webPort;
|
||||
bridge.webUrls = m_webUrls;
|
||||
|
||||
{
|
||||
auto ds = m_service.GetDmaStats();
|
||||
bridge.dmaAttached = ds.attached;
|
||||
bridge.dmaReadMBps = ds.readMBps;
|
||||
bridge.dmaScatterOps = ds.scatterOpsPs;
|
||||
bridge.dmaTotalGB = ds.totalGB;
|
||||
bridge.dmaTotalOps = ds.totalOps;
|
||||
}
|
||||
|
||||
bridge.onSaveConfig = [this]() {
|
||||
SyncConfig();
|
||||
m_cfg.Save(m_cfgPath);
|
||||
@@ -387,6 +405,7 @@ void GameOverlay::DrawESP(float w, float h, const RuntimeUpdate& u, const Camera
|
||||
if (!u.areBaseObjectsReady || !cam.valid) return;
|
||||
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
if (m_showBulletTrails) DrawBulletTrails(dl, cam, w, h);
|
||||
if (m_showPlayers) DrawPlayers(dl, u, cam, w, h);
|
||||
if (m_showAnimals) DrawAnimals(dl, u, cam, w, h);
|
||||
if (m_showZombies) DrawZombies(dl, u, cam, w, h);
|
||||
@@ -565,7 +584,7 @@ void GameOverlay::DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const Came
|
||||
if (!p.isDead) {
|
||||
auto hit = m_playerBoneHistory.find(p.address);
|
||||
if (hit != m_playerBoneHistory.end() && hit->second.initialized
|
||||
&& (m_frameTimeMs - hit->second.currMs) < 500LL) {
|
||||
&& (m_frameTimeMs - hit->second.lastReadMs) < 3000LL) {
|
||||
smoothBones = GetSmoothedBones(hit->second, m_frameTimeMs);
|
||||
hasSmoothBones = true;
|
||||
}
|
||||
@@ -610,7 +629,7 @@ void GameOverlay::DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const Came
|
||||
}
|
||||
|
||||
if (hits >= 4) {
|
||||
constexpr float kPad = 4.0f;
|
||||
constexpr float kPad = 8.0f;
|
||||
bx0 = minX - kPad;
|
||||
by0 = minY - kPad;
|
||||
bx1 = maxX + kPad;
|
||||
@@ -642,11 +661,8 @@ void GameOverlay::DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const Came
|
||||
|
||||
if (m_showHeadDot && !p.isDead && hasSmoothBones) {
|
||||
if (Proj(cam, smoothBones.head, hbx, hby, w, h)) {
|
||||
// Radius = 15% of the rendered body height so the circle scales
|
||||
// naturally with distance. The head-neck pixel-distance approach
|
||||
// collapses to minimum past ~30 m; box-height fraction stays
|
||||
// proportional to the target at any range.
|
||||
headRadius = std::clamp((by1 - by0) * 0.15f, 2.0f, 18.0f);
|
||||
// Radius ≈ 12.75% of box height (15% × 0.85 = −15% of original).
|
||||
headRadius = std::clamp((by1 - by0) * 0.128f, 2.0f, 15.0f);
|
||||
hasHeadCircle = true;
|
||||
by0 = std::min(by0, hby - headRadius - 2.0f);
|
||||
}
|
||||
@@ -724,6 +740,132 @@ void GameOverlay::DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const Came
|
||||
IM_COL32(200, 200, 200, 200), hpBuf);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Ballistic prediction dot ----
|
||||
if (m_showBallisticDot && !p.isDead && u.localWeaponInitSpeed > 1.0f
|
||||
&& u.localPlayerPosition.has_value())
|
||||
{
|
||||
// --- Velocity estimate (EMA of position delta) ---
|
||||
constexpr float kVelAlpha = 0.35f;
|
||||
Vector3 vel{};
|
||||
auto& ve = m_playerVel[p.address];
|
||||
if (ve.ms > 0 && (m_frameTimeMs - ve.ms) < 2000) {
|
||||
float dt = static_cast<float>(m_frameTimeMs - ve.ms) * 0.001f;
|
||||
if (dt > 0.016f) {
|
||||
float vx = (pos.x - ve.pos.x) / dt;
|
||||
float vy = (pos.y - ve.pos.y) / dt;
|
||||
float vz = (pos.z - ve.pos.z) / dt;
|
||||
vel.x = kVelAlpha * vx + (1.0f - kVelAlpha) * ve.vel.x;
|
||||
vel.y = kVelAlpha * vy + (1.0f - kVelAlpha) * ve.vel.y;
|
||||
vel.z = kVelAlpha * vz + (1.0f - kVelAlpha) * ve.vel.z;
|
||||
} else {
|
||||
vel = ve.vel;
|
||||
}
|
||||
}
|
||||
ve.pos = pos;
|
||||
ve.ms = m_frameTimeMs;
|
||||
ve.vel = vel;
|
||||
|
||||
// --- Time of flight with drag (continuous model) ---
|
||||
const Vector3& lp = *u.localPlayerPosition;
|
||||
float dx = pos.x - lp.x;
|
||||
float dz = pos.z - lp.z;
|
||||
float horizDist = std::sqrt(dx*dx + dz*dz);
|
||||
|
||||
float V0 = u.localWeaponInitSpeed;
|
||||
float k = u.localWeaponAirFriction;
|
||||
float T;
|
||||
if (k > 0.0001f) {
|
||||
float ratio = horizDist * k / V0;
|
||||
T = (ratio < 0.99f) ? (-std::log(1.0f - ratio) / k)
|
||||
: (horizDist / (V0 * 0.5f));
|
||||
} else {
|
||||
T = horizDist / V0;
|
||||
}
|
||||
T = std::min(T, 8.0f);
|
||||
|
||||
// --- Aim point: center mass + lead + drop compensation ---
|
||||
constexpr float kGravity = 9.81f;
|
||||
constexpr float kCenterMassY = 1.0f; // m above entity root
|
||||
Vector3 aimPt;
|
||||
aimPt.x = pos.x + vel.x * T;
|
||||
aimPt.y = pos.y + kCenterMassY + 0.5f * kGravity * T * T;
|
||||
aimPt.z = pos.z + vel.z * T;
|
||||
|
||||
float adx{}, ady{};
|
||||
if (Proj(cam, aimPt, adx, ady, w, h)) {
|
||||
dl->AddCircleFilled(ImVec2(adx, ady), 5.0f, IM_COL32(0, 0, 0, 160));
|
||||
dl->AddCircleFilled(ImVec2(adx, ady), 3.5f, IM_COL32(0, 255, 220, 240));
|
||||
dl->AddCircle (ImVec2(adx, ady), 3.5f, IM_COL32(0, 0, 0, 180), 0, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prune velocity entries for players that left the snapshot.
|
||||
// Called implicitly each DrawPlayers pass via the map auto-insert; no explicit prune
|
||||
// needed since the map entries are tiny and the set stays bounded by server pop.
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DrawBulletTrails — draws a line strip from each bullet's origin to its
|
||||
// current or final position, with alpha fade for completed/phantom tracks.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
void GameOverlay::DrawBulletTrails(ImDrawList* dl, const CameraData& cam, float w, float h) {
|
||||
auto tracks = m_bulletTracks.GetSnapshot();
|
||||
|
||||
for (const auto& t : tracks) {
|
||||
// Pick color + alpha based on track state.
|
||||
ImU32 lineColor, originColor;
|
||||
if (t.isCompleted) {
|
||||
int64_t age = m_frameTimeMs - t.finalizedMs;
|
||||
int alpha = static_cast<int>(210.0f * std::max(0.0f,
|
||||
1.0f - static_cast<float>(age) / 1500.0f));
|
||||
if (alpha <= 0) continue;
|
||||
lineColor = IM_COL32(255, 140, 30, alpha);
|
||||
originColor = IM_COL32(255, 60, 60, alpha);
|
||||
} else if (t.isPhantom) {
|
||||
int64_t age = m_frameTimeMs - t.lastSeenMs;
|
||||
int alpha = static_cast<int>(160.0f * std::max(0.0f,
|
||||
1.0f - static_cast<float>(age) / 4000.0f));
|
||||
if (alpha <= 0) continue;
|
||||
lineColor = IM_COL32(200, 100, 40, alpha);
|
||||
originColor = IM_COL32(200, 60, 60, alpha);
|
||||
} else {
|
||||
// Active bullet — bright yellow-orange.
|
||||
lineColor = IM_COL32(255, 210, 50, 230);
|
||||
originColor = IM_COL32(255, 80, 80, 230);
|
||||
}
|
||||
|
||||
// Collect screen-space points: origin → history → endpoint.
|
||||
std::vector<ImVec2> pts;
|
||||
pts.reserve(t.points.size() + 2);
|
||||
|
||||
auto pushPt = [&](const Vector3& wp) {
|
||||
float sx{}, sy{};
|
||||
if (Proj(cam, wp, sx, sy, w, h))
|
||||
pts.push_back(ImVec2(sx, sy));
|
||||
};
|
||||
|
||||
pushPt(t.initialPos);
|
||||
for (const auto& [ts, pos] : t.points)
|
||||
pushPt(pos);
|
||||
pushPt(t.isCompleted ? t.finalPos : t.currentPos);
|
||||
|
||||
if (pts.size() < 2) continue;
|
||||
|
||||
// Draw line strip.
|
||||
for (size_t i = 1; i < pts.size(); ++i)
|
||||
dl->AddLine(pts[i - 1], pts[i], lineColor, 1.5f);
|
||||
|
||||
// Origin dot.
|
||||
dl->AddCircleFilled(pts[0], 3.0f, originColor);
|
||||
|
||||
// Endpoint dot for completed tracks.
|
||||
if (t.isCompleted && pts.size() >= 2) {
|
||||
int alpha = static_cast<int>((lineColor >> IM_COL32_A_SHIFT) & 0xFF);
|
||||
dl->AddCircleFilled(pts.back(), 3.0f, IM_COL32(255, 255, 100, alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,7 +921,7 @@ void GameOverlay::DrawZombies(ImDrawList* dl, const RuntimeUpdate& u, const Came
|
||||
|
||||
auto zhit = m_zombieBoneHistory.find(z.address);
|
||||
if (zhit != m_zombieBoneHistory.end() && zhit->second.initialized
|
||||
&& (m_frameTimeMs - zhit->second.currMs) < 500LL) {
|
||||
&& (m_frameTimeMs - zhit->second.lastReadMs) < 3000LL) {
|
||||
SkeletonBones zsm = GetSmoothedBones(zhit->second, m_frameTimeMs);
|
||||
if (m_showSkeleton)
|
||||
DrawSkeleton(dl, zsm, cam, w, h, kYellow, true);
|
||||
|
||||
+31
-13
@@ -8,6 +8,7 @@
|
||||
#include <imgui.h>
|
||||
#include "Config.h"
|
||||
#include "Runtime/DayZRuntimeService.h"
|
||||
#include "Web/BulletTrackCache.h"
|
||||
|
||||
struct ImDrawList;
|
||||
|
||||
@@ -18,7 +19,8 @@ public:
|
||||
SkeletonBones prev{};
|
||||
SkeletonBones curr{};
|
||||
int64_t prevMs = 0;
|
||||
int64_t currMs = 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;
|
||||
};
|
||||
|
||||
@@ -37,9 +39,11 @@ public:
|
||||
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_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;
|
||||
@@ -115,10 +119,12 @@ private:
|
||||
bool m_showSkeleton = true;
|
||||
bool m_showHeadDot = false;
|
||||
bool m_showCorpses = false;
|
||||
bool m_showWeapon = true;
|
||||
bool m_showHealthBar = true;
|
||||
bool m_showHealthNumber = false;
|
||||
bool m_debugSkeleton = false; // draws named bone dots for the closest player
|
||||
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
|
||||
|
||||
// Per-category item enabled map (key = filterKey, missing = enabled)
|
||||
std::map<std::string, bool> m_itemCategories;
|
||||
@@ -131,6 +137,17 @@ private:
|
||||
// Used to avoid a single failed position read causing a visible blink.
|
||||
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
|
||||
};
|
||||
std::unordered_map<uint64_t, PlayerVelEntry> m_playerVel;
|
||||
|
||||
// Bullet trail tracker — owned by the overlay, fed from each update's bullet list.
|
||||
BulletTrackCache m_bulletTracks;
|
||||
|
||||
// ---- Bone interpolation / extrapolation ----
|
||||
std::unordered_map<uint64_t, BoneHistory> m_playerBoneHistory;
|
||||
std::unordered_map<uint64_t, BoneHistory> m_zombieBoneHistory;
|
||||
@@ -173,11 +190,12 @@ private:
|
||||
// back into m_cfg prior to a Save.
|
||||
void SyncConfig();
|
||||
|
||||
void DrawESP (float w, float h, const RuntimeUpdate& u, const CameraData& cam);
|
||||
void DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
|
||||
void DrawAnimals(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
|
||||
void DrawZombies(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
|
||||
void DrawItems (ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
|
||||
void DrawESP (float w, float h, const RuntimeUpdate& u, const CameraData& cam);
|
||||
void DrawPlayers (ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
|
||||
void DrawAnimals (ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
|
||||
void DrawZombies (ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
|
||||
void DrawItems (ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
|
||||
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,
|
||||
|
||||
@@ -24,11 +24,13 @@ struct MenuBridge {
|
||||
bool* showBox = nullptr;
|
||||
bool* showSkeleton = nullptr;
|
||||
bool* showHeadDot = nullptr;
|
||||
bool* showWeapon = nullptr;
|
||||
bool* showHealthBar = nullptr;
|
||||
bool* showHealthNumber = nullptr;
|
||||
bool* showCorpses = nullptr;
|
||||
bool* debugSkeleton = nullptr;
|
||||
bool* showWeapon = nullptr;
|
||||
bool* showHealthBar = nullptr;
|
||||
bool* showHealthNumber = nullptr;
|
||||
bool* showCorpses = nullptr;
|
||||
bool* debugSkeleton = nullptr;
|
||||
bool* showBallisticDot = nullptr;
|
||||
bool* showBulletTrails = nullptr;
|
||||
|
||||
// ---- ESP draw-distance limits ----
|
||||
float* playerMaxDist = nullptr;
|
||||
@@ -49,6 +51,13 @@ struct MenuBridge {
|
||||
std::size_t nPlayers = 0, nAnimals = 0, nZombies = 0,
|
||||
nVehicles = 0, nItems = 0, nBullets = 0;
|
||||
|
||||
// ---- DMA handle stats (read-only, refreshed each frame) ----
|
||||
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
|
||||
|
||||
// ---- Web radar ----
|
||||
int webPort = 7777;
|
||||
std::vector<std::string> webUrls;
|
||||
|
||||
@@ -87,7 +87,7 @@ bool OverlayWindow::CreateWin32Window() {
|
||||
|
||||
m_hwnd = CreateWindowExW(
|
||||
WS_EX_APPWINDOW,
|
||||
kWndClass, L"KarachiHook",
|
||||
kWndClass, L"DayZ DMA",
|
||||
WS_POPUP | WS_VISIBLE,
|
||||
0, 0, m_w, m_h,
|
||||
nullptr, nullptr, wc.hInstance, nullptr
|
||||
|
||||
+350
-105
@@ -626,10 +626,11 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
|
||||
auto healthResolver = [&](uint64_t addr) -> float {
|
||||
if (Offsets::Player::Health == 0) return -1.0f;
|
||||
float hp = -1.0f;
|
||||
// Quality field stores [0.25, 0.50, 0.75, 1.0] — scale to [0, 100].
|
||||
if (!m_memory.TryReadValue<float>(pid, addr + Offsets::Player::Health, hp)
|
||||
|| hp < 0.0f || hp > 100.0f)
|
||||
|| hp <= 0.0f || hp > 1.01f)
|
||||
return -1.0f;
|
||||
return hp;
|
||||
return hp * 100.0f;
|
||||
};
|
||||
|
||||
// --- wornClothesResolver ---
|
||||
@@ -695,6 +696,71 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
|
||||
update.localPlayerEntityAddress = m_localPlayer.entityAddress;
|
||||
update.localPlayerPosition = m_localPlayer.position;
|
||||
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.
|
||||
uint64_t inv = 0, weapon = 0, mag = 0, ammoType = 0;
|
||||
uint8_t handsValid = 0;
|
||||
bool invOk = m_memory.TryReadPointer(pid,
|
||||
m_localPlayer.entityAddress + Offsets::Inventory::Base, inv)
|
||||
&& MemoryValidation::IsValidUserAddress(inv);
|
||||
bool handOk = invOk
|
||||
&& m_memory.TryReadValue<uint8_t>(pid,
|
||||
inv + Offsets::Inventory::HandItemValid, handsValid)
|
||||
&& handsValid;
|
||||
bool weaponOk = handOk
|
||||
&& m_memory.TryReadPointer(pid,
|
||||
inv + Offsets::Inventory::Hands, weapon)
|
||||
&& MemoryValidation::IsValidUserAddress(weapon);
|
||||
bool magOk = weaponOk
|
||||
&& m_memory.TryReadPointer(pid,
|
||||
weapon + Offsets::Weapon::MagazinePtr, mag)
|
||||
&& MemoryValidation::IsValidUserAddress(mag);
|
||||
bool ammoOk = magOk
|
||||
&& m_memory.TryReadPointer(pid,
|
||||
mag + Offsets::Magazine::AmmoTypePtr, ammoType)
|
||||
&& 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) {
|
||||
update.localWeaponInitSpeed = initSpeed;
|
||||
update.localWeaponAirFriction = std::max(0.0f, airFriction);
|
||||
} else if (!m_ammoChainLoggedOnce) {
|
||||
m_ammoChainLoggedOnce = true;
|
||||
spdlog::warn("BallisticDot: ammo chain resolved but initSpeed={:.1f} "
|
||||
"(ammoType=0x{:X}+0x{:X}) — check AmmoType::InitSpeed offset",
|
||||
initSpeed, ammoType, Offsets::AmmoType::InitSpeed);
|
||||
}
|
||||
} else if (!m_ammoChainLoggedOnce) {
|
||||
if (!invOk) {
|
||||
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) {
|
||||
m_ammoChainLoggedOnce = true;
|
||||
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);
|
||||
} else {
|
||||
m_ammoChainLoggedOnce = true;
|
||||
spdlog::warn("BallisticDot: weapon chain fail at Magazine::AmmoTypePtr "
|
||||
"(mag=0x{:X}+0x{:X}) — verify offset", mag, Offsets::Magazine::AmmoTypePtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_camera.valid) update.camera = m_camera;
|
||||
@@ -789,25 +855,31 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
|
||||
}
|
||||
}
|
||||
|
||||
// Store as baseline for the 16ms bone-scatter refresh.
|
||||
// Store as baseline for the bone-scatter refresh. Do NOT publish
|
||||
// here — bones are still skeleton.valid=false at this point. The
|
||||
// bone tick (below, every boneRefreshMs) copies m_liveUpdate, fills
|
||||
// in the skeletons, and is the sole publisher. This prevents the
|
||||
// overlay from ever seeing a bones=0 frame mid-session.
|
||||
m_liveUpdate = std::move(update);
|
||||
PublishUpdate(m_liveUpdate);
|
||||
|
||||
m_state.nextPlayersRefresh =
|
||||
Clock::now() + Ms(m_config.playersRefreshMs);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 11b. Bone-scatter refresh (~16 ms / ~60 Hz).
|
||||
// Re-reads all skeleton bones using the warm pointer cache and
|
||||
// a single DMA scatter call, then publishes with fresh bones.
|
||||
// 11b. Bone-scatter refresh (~4 ms / ~250 Hz).
|
||||
// Copies the current entity baseline, re-reads all skeleton bones
|
||||
// via DMA scatter, then publishes the full snapshot (players,
|
||||
// zombies, items, bullets, etc.) with fresh bones attached.
|
||||
// This is the ONLY place PublishUpdate is called during the live
|
||||
// loop, so the overlay never sees a bones=0 snapshot.
|
||||
// ------------------------------------------------------------------
|
||||
if (now >= m_state.nextBoneRefresh) {
|
||||
if (!m_liveUpdate.players.empty() || !m_liveUpdate.zombies.empty()) {
|
||||
RuntimeUpdate boneTick = m_liveUpdate; // copy entity list
|
||||
RuntimeUpdate boneTick = m_liveUpdate; // copy full entity baseline
|
||||
if (!boneTick.players.empty() || !boneTick.zombies.empty()) {
|
||||
RefreshBonesScatter(pid, boneTick.players, boneTick.zombies);
|
||||
PublishUpdate(boneTick);
|
||||
}
|
||||
PublishUpdate(boneTick); // always publish — includes items/bullets
|
||||
m_state.nextBoneRefresh = Clock::now() + Ms(m_config.boneRefreshMs);
|
||||
}
|
||||
|
||||
@@ -990,6 +1062,8 @@ void DayZRuntimeService::ResetAllReaders() {
|
||||
m_boneAnimOffset = -1;
|
||||
m_boneMatrixOffset = -1;
|
||||
m_bonePointerCache.clear();
|
||||
m_ammoChainLoggedOnce = false;
|
||||
m_boneDiagCount = 0;
|
||||
|
||||
m_state.scoreboard.clear();
|
||||
m_state.nearEntities.clear();
|
||||
@@ -1042,27 +1116,29 @@ void DayZRuntimeService::RefreshBonesScatter(
|
||||
8, 11, 13 // rightUpLeg(9), rightLeg(12), rightFoot(14)
|
||||
};
|
||||
|
||||
// Player max bone index = 99 (lHand), zombie = 60 (rHand).
|
||||
// Read the full contiguous block from BoneTranslationOffset to cover all
|
||||
// needed indices in one scatter entry instead of 15-16 individual reads.
|
||||
// Block size: (maxIndex+1) * BoneStride bytes; zombies fit within players' block.
|
||||
static constexpr int kMaxBoneIndex = 100;
|
||||
static constexpr size_t kBoneBlockSize = kMaxBoneIndex * Offsets::Skeleton::BoneStride; // 4800 B
|
||||
|
||||
// Per-entity bone read buffers. Must be stable in memory before we point
|
||||
// scatter entries at them — reserve upfront, then only push_back.
|
||||
//
|
||||
// We used to read bones as a single 4800-byte contiguous block per entity.
|
||||
// That block spans 2 OS pages; when any page is not physically contiguous in
|
||||
// RAM the VMMDLL scatter read silently zero-fills it — same failure mode as
|
||||
// the SigScanner's 4 MB sequential reads. The fix: one 12-byte scatter entry
|
||||
// per bone (xyz translation only). Small reads are always single-page and
|
||||
// reliably succeed even under DMA page fragmentation.
|
||||
static constexpr int kMaxBones = 16; // max(15 player, 16 zombie) bones per entity
|
||||
|
||||
struct PerEntityData {
|
||||
float vsMatrix[12]; // VS transform (48 B) at vsAddr+8
|
||||
uint8_t boneBlock[kBoneBlockSize]; // contiguous bone matrix block
|
||||
float vsMatrix[12]; // VS transform (48 B) at vsAddr+8
|
||||
float boneXYZ[kMaxBones][3]; // individual local bone translations
|
||||
BonePointerCache* cache;
|
||||
bool isZombie;
|
||||
SkeletonBones* outSkel;
|
||||
std::optional<Vector3>* outPos; // pointer back into boneTick entity list
|
||||
std::optional<Vector3>* outPos; // pointer back into boneTick entity list
|
||||
int numBones;
|
||||
const int* boneIdxs;
|
||||
Vector3 entityPos; // fallback position if VS translation is zero
|
||||
uint64_t addr; // entity address (for loss logging)
|
||||
const std::string* label; // player nickname; nullptr for zombies
|
||||
Vector3 entityPos; // fallback position if VS translation is zero
|
||||
uint64_t addr; // entity address (for loss logging)
|
||||
const std::string* label; // player nickname; nullptr for zombies
|
||||
};
|
||||
|
||||
std::vector<PerEntityData> eds;
|
||||
@@ -1090,35 +1166,20 @@ void DayZRuntimeService::RefreshBonesScatter(
|
||||
spdlog::info("Skeleton lost: '{}' — {} [0x{:X}]", who, reason, addr);
|
||||
};
|
||||
|
||||
// matBase is re-read from animClass every this many bone ticks while the cache
|
||||
// entry is valid. At the default 4 ms bone cadence, 60 ticks ≈ 240 ms.
|
||||
constexpr uint32_t kMatBaseResyncTicks = 60;
|
||||
|
||||
// Resolve and cache the pointer chain for one entity (sequential reads,
|
||||
// amortised over the lifetime of the entity).
|
||||
// matBase freshening is handled by the pre-pass scatter below — ensureCached
|
||||
// only needs to establish the animClass pointer and initial matBase once.
|
||||
auto ensureCached = [&](uint64_t entAddr, bool isZombie) -> BonePointerCache* {
|
||||
{
|
||||
auto it = m_bonePointerCache.find(entAddr);
|
||||
if (it != m_bonePointerCache.end()) {
|
||||
auto& c = it->second;
|
||||
if (c.valid) {
|
||||
if (++c.syncAge >= kMatBaseResyncTicks) {
|
||||
c.syncAge = 0;
|
||||
uint64_t freshMat = 0;
|
||||
if (m_memory.TryReadValue<uint64_t>(pid,
|
||||
c.animClass + static_cast<uint64_t>(m_boneMatrixOffset),
|
||||
freshMat) && freshMat) {
|
||||
c.matBase = freshMat;
|
||||
} else {
|
||||
c.valid = false; // animClass itself went stale
|
||||
}
|
||||
}
|
||||
if (c.valid) return &c;
|
||||
}
|
||||
if (c.valid) return &c;
|
||||
|
||||
// Cache was invalidated (stale matBase). If animClass is still
|
||||
// cached we can skip the full 4-read pointer walk and just re-read
|
||||
// vsAddr + matBase — 2 reads instead of 4, no probe retry needed.
|
||||
// Cache was invalidated (stale matBase/animClass).
|
||||
// If animClass is still plausibly valid, try a quick 2-read recovery
|
||||
// before falling through to the full probe.
|
||||
if (c.animClass != 0 && m_boneAnimOffset >= 0 && m_boneMatrixOffset >= 0) {
|
||||
uint64_t freshVs = 0, freshMat = 0;
|
||||
if (m_memory.TryReadValue<uint64_t>(pid,
|
||||
@@ -1131,7 +1192,6 @@ void DayZRuntimeService::RefreshBonesScatter(
|
||||
c.valid = true;
|
||||
return &c;
|
||||
}
|
||||
// animClass also stale — clear it and fall through to full re-resolve.
|
||||
c.animClass = 0;
|
||||
}
|
||||
}
|
||||
@@ -1229,8 +1289,33 @@ void DayZRuntimeService::RefreshBonesScatter(
|
||||
|
||||
BonePointerCache* cache = ensureCached(e.address, isZombie);
|
||||
if (!cache) {
|
||||
// Pointer chain unresolved this tick — if the entity previously
|
||||
// had a skeleton, that's a real disappearance worth logging.
|
||||
// Pointer chain unresolved — engine deactivated animation for this
|
||||
// entity (out of frustum / LOD). If we have a recent cached pose,
|
||||
// apply it here so the skeleton doesn't blink off between updates.
|
||||
auto cit = m_bonePointerCache.find(e.address);
|
||||
if (cit != m_bonePointerCache.end()) {
|
||||
auto& c = cit->second;
|
||||
if (c.lastSkelMs > 0
|
||||
&& nowMsBones - c.lastSkelMs < 3000
|
||||
&& c.lastSkel.valid)
|
||||
{
|
||||
// Use entity list position as the current reference; apply delta.
|
||||
const float odx = e.position ? e.position->x - c.lastEntityPos.x : 0.f;
|
||||
const float ody = e.position ? e.position->y - c.lastEntityPos.y : 0.f;
|
||||
const float odz = e.position ? e.position->z - c.lastEntityPos.z : 0.f;
|
||||
SkeletonBones adj = c.lastSkel;
|
||||
auto shift = [&](Vector3& v){ v.x+=odx; v.y+=ody; v.z+=odz; };
|
||||
shift(adj.neck); shift(adj.head); shift(adj.spine);
|
||||
shift(adj.pelvis);
|
||||
shift(adj.leftShoulder); shift(adj.leftElbow); shift(adj.leftHand);
|
||||
shift(adj.rightShoulder); shift(adj.rightElbow); shift(adj.rightHand);
|
||||
shift(adj.leftHip); shift(adj.leftKnee); shift(adj.leftAnkle);
|
||||
shift(adj.rightHip); shift(adj.rightKnee); shift(adj.rightAnkle);
|
||||
e.skeleton = adj;
|
||||
noteBoneOk(e.address);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
noteBoneLoss(e.address, label, isZombie, "pointer chain unresolved");
|
||||
continue;
|
||||
}
|
||||
@@ -1246,8 +1331,8 @@ void DayZRuntimeService::RefreshBonesScatter(
|
||||
ed.entityPos = *e.position;
|
||||
ed.addr = e.address;
|
||||
ed.label = label;
|
||||
std::memset(ed.vsMatrix, 0, sizeof(ed.vsMatrix));
|
||||
std::memset(ed.boneBlock, 0, sizeof(ed.boneBlock));
|
||||
std::memset(ed.vsMatrix, 0, sizeof(ed.vsMatrix));
|
||||
std::memset(ed.boneXYZ, 0, sizeof(ed.boneXYZ));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1256,41 +1341,204 @@ void DayZRuntimeService::RefreshBonesScatter(
|
||||
collect(players, false, pd * pd);
|
||||
collect(zombies, true, zd * zd);
|
||||
|
||||
const bool diag = (m_boneDiagCount < kBoneDiagPasses);
|
||||
if (diag)
|
||||
spdlog::info("BoneDiag[{}] collect: eds={} players={} zombies={}",
|
||||
m_boneDiagCount, eds.size(), players.size(), zombies.size());
|
||||
|
||||
if (eds.empty()) return;
|
||||
|
||||
// Build scatter list — 2 entries per entity:
|
||||
// [0] VS matrix (48 B at vsAddr+8)
|
||||
// [1] bone block (4800 B at matBase+BoneTranslationOffset, covers indices 0-99)
|
||||
// Previously: 17 entries per entity (1 VS + 16 individual bone reads).
|
||||
// Now: 2 entries per entity regardless of bone count — 85% fewer DMA operations.
|
||||
// ── Pre-pass: freshen matBase + vsAddr for every entity in one scatter round-trip ──
|
||||
// matBase at animClass+matrixOff is double-buffered by the engine animation system
|
||||
// and can change on every frame (~16ms at 60Hz). vsAddr (VisualState pointer) can
|
||||
// also change when the entity re-spawns or its visual representation is rebuilt.
|
||||
// Reading both here — immediately before the bone scatter — ensures the subsequent
|
||||
// read uses the current buffer pointer and the correct VS matrix address.
|
||||
{
|
||||
// Two 8-byte reads per entity (matBase + vsAddr); store in parallel vectors so
|
||||
// ScatterEntry destination pointers remain stable during the scatter.
|
||||
std::vector<uint64_t> freshMatBases(eds.size(), 0);
|
||||
std::vector<uint64_t> freshVsAddrs(eds.size(), 0);
|
||||
std::vector<VmmAccessor::ScatterEntry> prescan;
|
||||
prescan.reserve(eds.size() * 2);
|
||||
|
||||
for (size_t i = 0; i < eds.size(); ++i) {
|
||||
if (eds[i].cache->animClass == 0) continue;
|
||||
prescan.push_back({
|
||||
eds[i].cache->animClass + static_cast<uint64_t>(m_boneMatrixOffset),
|
||||
&freshMatBases[i],
|
||||
sizeof(uint64_t)
|
||||
});
|
||||
prescan.push_back({
|
||||
eds[i].addr + Offsets::Common::VisualState,
|
||||
&freshVsAddrs[i],
|
||||
sizeof(uint64_t)
|
||||
});
|
||||
}
|
||||
|
||||
if (!prescan.empty()) {
|
||||
m_memory.ScatterRead(pid, prescan);
|
||||
int freshOk = 0, freshZero = 0;
|
||||
for (size_t i = 0; i < eds.size(); ++i) {
|
||||
if (eds[i].cache->animClass == 0) continue;
|
||||
|
||||
const uint64_t fresh = freshMatBases[i];
|
||||
if (fresh) {
|
||||
eds[i].cache->matBase = fresh;
|
||||
++freshOk;
|
||||
} else {
|
||||
++freshZero;
|
||||
}
|
||||
|
||||
const uint64_t freshVs = freshVsAddrs[i];
|
||||
if (freshVs && MemoryValidation::IsValidUserAddress(freshVs)) {
|
||||
eds[i].cache->vsAddr = freshVs;
|
||||
}
|
||||
}
|
||||
if (diag)
|
||||
spdlog::info("BoneDiag[{}] pre-pass: freshOk={} freshZero={} "
|
||||
"sample animClass=0x{:X} matBase=0x{:X}",
|
||||
m_boneDiagCount, freshOk, freshZero,
|
||||
eds.empty() ? 0 : eds[0].cache->animClass,
|
||||
eds.empty() ? 0 : eds[0].cache->matBase);
|
||||
}
|
||||
|
||||
const size_t edsBefore = eds.size();
|
||||
// Only drop entities with no matBase at all (never populated this tick).
|
||||
eds.erase(std::remove_if(eds.begin(), eds.end(),
|
||||
[](const PerEntityData& ed) {
|
||||
return !ed.cache->matBase;
|
||||
}), eds.end());
|
||||
if (diag)
|
||||
spdlog::info("BoneDiag[{}] post-filter: eds={} (dropped {} with matBase=0)",
|
||||
m_boneDiagCount, eds.size(), edsBefore - eds.size());
|
||||
}
|
||||
|
||||
if (eds.empty()) return;
|
||||
|
||||
// Build scatter list — 1 + numBones entries per entity:
|
||||
// [0] VS matrix (48 B at vsAddr+8)
|
||||
// [1-N] bone XYZ (12 B each — translation only, one entry per bone index)
|
||||
// Each bone read is 12 bytes (one float3), well within a single OS page and
|
||||
// immune to the cross-page zero-fill that the old 4800-byte block suffered.
|
||||
std::vector<VmmAccessor::ScatterEntry> scatter;
|
||||
scatter.reserve(eds.size() * 2);
|
||||
scatter.reserve(eds.size() * (1 + kMaxBones));
|
||||
|
||||
for (auto& ed : eds) {
|
||||
scatter.push_back({ ed.cache->vsAddr + 8,
|
||||
ed.vsMatrix,
|
||||
sizeof(ed.vsMatrix) });
|
||||
scatter.push_back({ ed.cache->matBase + Offsets::Skeleton::BoneTranslationOffset,
|
||||
ed.boneBlock,
|
||||
kBoneBlockSize });
|
||||
for (int i = 0; i < ed.numBones; ++i) {
|
||||
const uint64_t boneAddr = ed.cache->matBase
|
||||
+ Offsets::Skeleton::BoneTranslationOffset
|
||||
+ static_cast<uint64_t>(ed.boneIdxs[i]) * Offsets::Skeleton::BoneStride;
|
||||
scatter.push_back({ boneAddr, ed.boneXYZ[i], 12 });
|
||||
}
|
||||
}
|
||||
|
||||
// Single DMA round-trip for ALL entities.
|
||||
m_memory.ScatterRead(pid, scatter);
|
||||
|
||||
// Diagnostic: inspect scatter results for the first entity.
|
||||
if (diag && !eds.empty()) {
|
||||
const auto& ed0 = eds[0];
|
||||
int nz = 0;
|
||||
for (int i = 0; i < ed0.numBones; ++i)
|
||||
if (ed0.boneXYZ[i][0]!=0||ed0.boneXYZ[i][1]!=0||ed0.boneXYZ[i][2]!=0) ++nz;
|
||||
spdlog::info("BoneDiag[{}] scatter[0]: matBase=0x{:X} vsAddr=0x{:X} "
|
||||
"vsT=({:.1f},{:.1f},{:.1f}) nz={}/{} "
|
||||
"bone0=({:.4f},{:.4f},{:.4f}) bone1=({:.4f},{:.4f},{:.4f})",
|
||||
m_boneDiagCount,
|
||||
ed0.cache->matBase, ed0.cache->vsAddr,
|
||||
ed0.vsMatrix[9], ed0.vsMatrix[10], ed0.vsMatrix[11],
|
||||
nz, ed0.numBones,
|
||||
ed0.boneXYZ[0][0], ed0.boneXYZ[0][1], ed0.boneXYZ[0][2],
|
||||
ed0.boneXYZ[1][0], ed0.boneXYZ[1][1], ed0.boneXYZ[1][2]);
|
||||
}
|
||||
|
||||
// How long to show a cached (stale) skeleton before hiding it entirely.
|
||||
// DayZ only populates bone matrices when the client decides the entity needs
|
||||
// full animation (nearby LOD, frame budget, etc.). During gaps the buffer
|
||||
// is zeroed. We keep the last valid world-space pose and translate it by
|
||||
// the entity's movement delta so skeletons stay visible between sparse reads.
|
||||
static constexpr int64_t kSkelPersistMs = 3000;
|
||||
|
||||
// Helper: apply a cached skeleton to ed.outSkel, translated by the delta
|
||||
// between the current VS position and the position when it was captured.
|
||||
auto applyCachedSkel = [&](PerEntityData& ed, float vsX, float vsY, float vsZ) -> bool {
|
||||
if (ed.cache->lastSkelMs == 0) return false;
|
||||
if (nowMsBones - ed.cache->lastSkelMs >= kSkelPersistMs) return false;
|
||||
if (!ed.cache->lastSkel.valid) return false;
|
||||
|
||||
const float dx = vsX - ed.cache->lastEntityPos.x;
|
||||
const float dy = vsY - ed.cache->lastEntityPos.y;
|
||||
const float dz = vsZ - ed.cache->lastEntityPos.z;
|
||||
|
||||
SkeletonBones adj = ed.cache->lastSkel;
|
||||
auto shift = [&](Vector3& v) { v.x += dx; v.y += dy; v.z += dz; };
|
||||
shift(adj.neck); shift(adj.head); shift(adj.spine);
|
||||
shift(adj.pelvis);
|
||||
shift(adj.leftShoulder); shift(adj.leftElbow); shift(adj.leftHand);
|
||||
shift(adj.rightShoulder);shift(adj.rightElbow); shift(adj.rightHand);
|
||||
shift(adj.leftHip); shift(adj.leftKnee); shift(adj.leftAnkle);
|
||||
shift(adj.rightHip); shift(adj.rightKnee); shift(adj.rightAnkle);
|
||||
*ed.outSkel = adj;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Transform local bone translations → world space using the VS matrix.
|
||||
for (auto& ed : eds) {
|
||||
const float* m = ed.vsMatrix;
|
||||
|
||||
// Extract bone i's translation from the contiguous block.
|
||||
// boneBlock starts at matBase+BoneTranslationOffset; bone i's entry
|
||||
// begins at boneIdxs[slot]*BoneStride within the block, and the first
|
||||
// 12 bytes are the local translation [x, y, z].
|
||||
// ---- Always extract VS translation first ----
|
||||
// Even when bone data is stale the VS matrix gives the entity's current
|
||||
// world position, which we write back so the ESP box stays on the player.
|
||||
const float vsX = m[9];
|
||||
const float vsY = m[10];
|
||||
const float vsZ = m[11];
|
||||
const bool vsOk = (vsX != 0.0f || vsY != 0.0f || vsZ != 0.0f);
|
||||
if (vsOk && ed.outPos)
|
||||
*ed.outPos = Vector3{ vsX, vsY, vsZ };
|
||||
|
||||
// ---- Pre-transform stale matBase check ----
|
||||
// Count how many of the sampled bone slots have a non-zero local translation.
|
||||
// When the engine's LOD/animation scheduler hasn't populated the buffer for
|
||||
// this entity (far players, CPU budget cuts), all entries are zero.
|
||||
// Detecting this directly on the raw boneBlock avoids the indirect world-space
|
||||
// head.y ≈ vsY check, which false-triggers for prone/crawling players whose
|
||||
// head is only ~20–30 cm above the entity root in world Y.
|
||||
{
|
||||
int localNonZero = 0;
|
||||
for (int i = 0; i < ed.numBones; ++i) {
|
||||
if (ed.boneXYZ[i][0] != 0.f || ed.boneXYZ[i][1] != 0.f || ed.boneXYZ[i][2] != 0.f)
|
||||
++localNonZero;
|
||||
}
|
||||
if (localNonZero < 2) {
|
||||
// Buffer empty — matBase pointed to an unpopulated buffer.
|
||||
// Keep animClass so the fast 2-read recovery in ensureCached can
|
||||
// pick up a fresh matBase next tick without the expensive full probe.
|
||||
ed.cache->valid = false;
|
||||
ed.cache->matBase = 0;
|
||||
|
||||
// Show the last-known-good pose (translated by entity delta) so
|
||||
// the skeleton doesn't blink out during intermittent LOD gaps.
|
||||
if (vsOk && applyCachedSkel(ed, vsX, vsY, vsZ)) {
|
||||
noteBoneOk(ed.addr);
|
||||
} else {
|
||||
ed.outSkel->valid = false;
|
||||
noteBoneLoss(ed.addr, ed.label, ed.isZombie,
|
||||
"collapsed skeleton (stale matBase)");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform bone slot's local translation → world space via VS matrix.
|
||||
// boneXYZ[slot] was read directly from the individual scatter entry above.
|
||||
auto transform = [&](int slot, Vector3& pos) {
|
||||
const size_t off = static_cast<size_t>(ed.boneIdxs[slot])
|
||||
* Offsets::Skeleton::BoneStride;
|
||||
const float* t = reinterpret_cast<const float*>(ed.boneBlock + off);
|
||||
const float bx = t[0], by = t[1], bz = t[2];
|
||||
const float bx = ed.boneXYZ[slot][0];
|
||||
const float by = ed.boneXYZ[slot][1];
|
||||
const float bz = ed.boneXYZ[slot][2];
|
||||
pos.x = bx * m[0] + by * m[3] + bz * m[6] + m[9];
|
||||
pos.y = bx * m[1] + by * m[4] + bz * m[7] + m[10];
|
||||
pos.z = bx * m[2] + by * m[5] + bz * m[8] + m[11];
|
||||
@@ -1327,26 +1575,9 @@ void DayZRuntimeService::RefreshBonesScatter(
|
||||
|
||||
skel.valid = (ok >= 4);
|
||||
const char* lossReason = skel.valid ? nullptr : "insufficient bones in scatter";
|
||||
|
||||
// ---- Update entity position from VS matrix translation ----
|
||||
// The VS matrix is freshly read this scatter; its translation column
|
||||
// (m[9..11]) is the entity's current visual position in world space.
|
||||
// Writing it back to boneTick.players[i].position means every 60Hz bone
|
||||
// tick publishes an up-to-date position, not the stale scan-time value.
|
||||
// This eliminates the "ghost at old position" artifact between entity scans.
|
||||
const float vsX = ed.vsMatrix[9];
|
||||
const float vsY = ed.vsMatrix[10];
|
||||
const float vsZ = ed.vsMatrix[11];
|
||||
const bool vsOk = (vsX != 0.0f || vsY != 0.0f || vsZ != 0.0f);
|
||||
if (vsOk && ed.outPos)
|
||||
*ed.outPos = Vector3{ vsX, vsY, vsZ };
|
||||
bool headDrift = false;
|
||||
|
||||
// ---- Validate head bone against the FRESH VS translation ----
|
||||
// Previous code used ed.entityPos (scan-time, possibly seconds old).
|
||||
// A player running between scans can move >3 m, causing false evictions
|
||||
// that continuously re-resolved the pointer cache and dropped skeletons.
|
||||
// Using the VS translation (same scatter, same timestamp as the bones)
|
||||
// makes this check accurate regardless of entity scan frequency.
|
||||
if (skel.valid) {
|
||||
const float refX = vsOk ? vsX : ed.entityPos.x;
|
||||
const float refY = vsOk ? vsY : ed.entityPos.y;
|
||||
@@ -1355,32 +1586,46 @@ void DayZRuntimeService::RefreshBonesScatter(
|
||||
const float hy = skel.head.y - refY;
|
||||
const float hz = skel.head.z - refZ;
|
||||
if (hx*hx + hy*hy + hz*hz > 9.0f) { // > 3 m from VS origin → stale pointer
|
||||
skel.valid = false;
|
||||
ed.cache->valid = false;
|
||||
ed.cache->animClass = 0; // skip fast-path; force full re-probe
|
||||
lossReason = "stale pointer (head drift > 3m)";
|
||||
skel.valid = false;
|
||||
ed.cache->valid = false;
|
||||
headDrift = true;
|
||||
lossReason = "stale pointer (head drift > 3m)";
|
||||
// Don't clear animClass yet — might be a DMA double-buffer race rather
|
||||
// than a genuinely bad pointer chain. Only evict if cachedSkel also
|
||||
// fails (no fallback), meaning the chain is consistently broken.
|
||||
}
|
||||
}
|
||||
|
||||
// Detect stale matBase via "collapsed skeleton": when the boneBlock scatter
|
||||
// returns all-zero local translations, the transform sets every bone to
|
||||
// (vsX, vsY, vsZ). ok reaches 15 (all "non-zero") so the valid flag stays
|
||||
// set, but head.y ≈ vsY — impossible for a standing or crouching player
|
||||
// (head is always 1+ m above the entity root in DayZ). Evicting the cache
|
||||
// here causes ensureCached to re-read a fresh matBase on the next tick via
|
||||
// the fast 2-read path (animClass is still cached).
|
||||
if (skel.valid && vsOk && (skel.head.y - vsY) < 0.3f) {
|
||||
skel.valid = false;
|
||||
ed.cache->valid = false;
|
||||
ed.cache->animClass = 0; // stale matBase implies animClass may also be bad; force full re-probe
|
||||
lossReason = "collapsed skeleton (stale matBase)";
|
||||
}
|
||||
|
||||
// Record presence / log the valid→invalid edge for diagnostics.
|
||||
if (skel.valid)
|
||||
if (skel.valid) {
|
||||
// Save last-known-good skeleton with the current VS position.
|
||||
ed.cache->lastSkel = skel;
|
||||
ed.cache->lastEntityPos = Vector3{ vsX, vsY, vsZ };
|
||||
ed.cache->lastSkelMs = nowMsBones;
|
||||
noteBoneOk(ed.addr);
|
||||
else
|
||||
noteBoneLoss(ed.addr, ed.label, ed.isZombie, lossReason);
|
||||
} else {
|
||||
// Transform failed (race, stale buffer, or bad indices). Try the cached
|
||||
// skeleton before publishing an invalid frame so the overlay doesn't blink
|
||||
// on transient single-tick failures.
|
||||
if (vsOk && applyCachedSkel(ed, vsX, vsY, vsZ)) {
|
||||
noteBoneOk(ed.addr);
|
||||
} else {
|
||||
if (headDrift)
|
||||
ed.cache->animClass = 0; // chain is genuinely bad — force re-probe
|
||||
noteBoneLoss(ed.addr, ed.label, ed.isZombie, lossReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary diagnostic after transform.
|
||||
if (diag) {
|
||||
int validCount = 0, staleCount = 0, driftCount = 0;
|
||||
for (const auto& ed : eds) {
|
||||
if (ed.outSkel->valid) ++validCount;
|
||||
}
|
||||
spdlog::info("BoneDiag[{}] transform done: eds={} valid={} m_bonesTracked={}",
|
||||
m_boneDiagCount, eds.size(), validCount, m_bonesTracked.size());
|
||||
++m_boneDiagCount;
|
||||
}
|
||||
|
||||
// Drop loss-debounce timestamps for entities no longer tracked or pending,
|
||||
|
||||
@@ -44,6 +44,11 @@ struct RuntimeUpdate {
|
||||
std::optional<Vector3> localPlayerPosition;
|
||||
std::optional<float> localPlayerLookDirection;
|
||||
|
||||
// Ammo type data for the weapon currently in local player's hands.
|
||||
// 0.0f = no weapon / unresolved. Used by the ballistic prediction overlay.
|
||||
float localWeaponInitSpeed = 0.0f; // m/s
|
||||
float localWeaponAirFriction = 0.0f; // per-second drag coefficient
|
||||
|
||||
std::vector<DayZScoreboardPlayer> scoreboardPlayers;
|
||||
std::vector<DayZPlayerEntry> players;
|
||||
std::vector<DayZAnimalEntry> animals;
|
||||
@@ -133,6 +138,9 @@ public:
|
||||
/// Register a callback invoked (from the background thread) on each update.
|
||||
void SetUpdateCallback(UpdateCallback cb);
|
||||
|
||||
/// Live DMA throughput snapshot — safe to call from the overlay thread each frame.
|
||||
[[nodiscard]] DmaStats GetDmaStats() const { return m_memory.GetStats(); }
|
||||
|
||||
private:
|
||||
// ------------------------------------------------------------------
|
||||
// Configuration
|
||||
@@ -177,7 +185,15 @@ private:
|
||||
uint64_t animClass = 0;
|
||||
uint64_t matBase = 0;
|
||||
bool valid = false;
|
||||
uint32_t syncAge = 0; // bone-tick counter; matBase re-read when this reaches kMatBaseResyncTicks
|
||||
|
||||
// Last-known-good world-space skeleton for gap-filling.
|
||||
// DayZ only populates bone matrices when the client believes the entity
|
||||
// needs full animation (LOD, distance, frame scheduling). Caching the
|
||||
// last valid pose and adjusting it by the entity's movement delta lets
|
||||
// the overlay show continuous skeletons between sparse bone updates.
|
||||
SkeletonBones lastSkel;
|
||||
Vector3 lastEntityPos; // world pos when lastSkel was captured
|
||||
int64_t lastSkelMs = 0; // steady_clock ms when lastSkel was captured
|
||||
};
|
||||
std::unordered_map<uint64_t, BonePointerCache> m_bonePointerCache;
|
||||
|
||||
@@ -191,6 +207,12 @@ private:
|
||||
// addr → steady-clock ms of the last "skeleton lost" log, for debouncing
|
||||
// flapping skeletons to at most one message every few seconds.
|
||||
std::unordered_map<uint64_t, int64_t> m_boneLostLogAt;
|
||||
// Emitted once per session so a broken weapon ammo chain doesn't flood the log.
|
||||
bool m_ammoChainLoggedOnce = false;
|
||||
// Bone-scatter diagnostic pass counter. Logs detailed bone pipeline info
|
||||
// for the first kBoneDiagPasses calls to RefreshBonesScatter each session.
|
||||
int m_boneDiagCount = 0;
|
||||
static constexpr int kBoneDiagPasses = 8;
|
||||
|
||||
// Separate lightweight camera state for the overlay.
|
||||
// Updated by the dedicated camera thread (RunCameraLoop) at high frequency,
|
||||
|
||||
@@ -13,8 +13,14 @@
|
||||
// Constants
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static constexpr size_t kChunkSize = 4 * 1024 * 1024; // 4 MB per DMA read
|
||||
static constexpr size_t kOverlap = 512; // cross-boundary safety
|
||||
// Large sequential reads via Process::read() reliably fail on DayZ's module
|
||||
// because its code/data sections are not contiguous in physical memory.
|
||||
// Scatter reads handle non-contiguous pages natively and zero-fill failures,
|
||||
// so we scan using one scatter entry per 4 KB OS page (matching VMM granularity).
|
||||
static constexpr size_t kPageSize = 0x1000; // 4 KB — one OS page per scatter entry
|
||||
static constexpr size_t kPagesPerBatch = 256; // 256 pages = 1 MB per scatter call
|
||||
static constexpr size_t kBatchSize = kPagesPerBatch * kPageSize;
|
||||
static constexpr size_t kOverlap = 512; // cross-boundary safety (unchanged)
|
||||
static constexpr size_t kDefaultScanSize = 64 * 1024 * 1024; // fallback if PE read fails
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -195,10 +201,13 @@ SigScanner::Scan(VmmAccessor& mem, uint32_t pid, uint64_t moduleBase)
|
||||
"48 8B 8B ? ? ? ? 48 8B F8 48 85 C9");
|
||||
|
||||
// ====================================================================
|
||||
// Chunk scan
|
||||
// Chunk scan — scatter-based, one 4 KB scatter entry per OS page.
|
||||
// ====================================================================
|
||||
std::vector<uint8_t> chunk;
|
||||
chunk.reserve(kChunkSize + kOverlap);
|
||||
chunk.reserve(kBatchSize);
|
||||
|
||||
std::vector<VmmAccessor::ScatterEntry> pages;
|
||||
pages.reserve(kPagesPerBatch);
|
||||
|
||||
size_t offset = 0;
|
||||
while (offset < scanSize) {
|
||||
@@ -218,11 +227,23 @@ SigScanner::Scan(VmmAccessor& mem, uint32_t pid, uint64_t moduleBase)
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t readSize = std::min(kChunkSize, scanSize - offset);
|
||||
if (!mem.ReadBytes(pid, moduleBase + offset, readSize, chunk)) {
|
||||
offset += readSize;
|
||||
continue;
|
||||
const size_t batchBytes = std::min(kBatchSize, scanSize - offset);
|
||||
const size_t numPages = (batchBytes + kPageSize - 1) / kPageSize;
|
||||
|
||||
// Zero-initialise: failed scatter pages stay zero and won't match any pattern.
|
||||
chunk.assign(batchBytes, 0);
|
||||
pages.clear();
|
||||
for (size_t p = 0; p < numPages; ++p) {
|
||||
const size_t pageOff = p * kPageSize;
|
||||
const size_t rdLen = std::min(kPageSize, batchBytes - pageOff);
|
||||
pages.push_back({ moduleBase + offset + pageOff,
|
||||
chunk.data() + pageOff,
|
||||
rdLen });
|
||||
}
|
||||
mem.ScatterRead(pid, pages); // zero-fills any pages that fail to read
|
||||
|
||||
// readSize drives the overlap-advance at the bottom of the loop.
|
||||
const size_t readSize = batchBytes;
|
||||
|
||||
// -- MovCs --
|
||||
if (!result.baseWorld) {
|
||||
|
||||
@@ -95,6 +95,19 @@ std::vector<uint8_t> MapTileService::GetTile(const MapInfo& map, int tileX,
|
||||
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
|
||||
// Tile PNG cache — keyed by "mapId:tx:ty". PNG encoding is expensive
|
||||
// (zlib compression on a 512×512 RGBA block); cache the result so repeated
|
||||
// requests for the same tile (pan/zoom, reconnect, second browser tab) are
|
||||
// just a map lookup + memcpy instead of a full re-encode.
|
||||
std::string tileKey = map.id + ':' + std::to_string(tileX) + ':' + std::to_string(tileY);
|
||||
{
|
||||
auto it = m_tileCache.find(tileKey);
|
||||
if (it != m_tileCache.end()) {
|
||||
errCode = 200;
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
const Image* img = LoadOrGet(map.id);
|
||||
if (!img || img->pixels.empty() || img->w == 0 || img->h == 0) {
|
||||
errCode = 404;
|
||||
@@ -153,6 +166,8 @@ std::vector<uint8_t> MapTileService::GetTile(const MapInfo& map, int tileX,
|
||||
return {};
|
||||
}
|
||||
|
||||
m_tileCache.emplace(std::move(tileKey), png);
|
||||
|
||||
errCode = 200;
|
||||
return png;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ private:
|
||||
};
|
||||
|
||||
std::mutex m_mutex;
|
||||
std::unordered_map<std::string, Image> m_cache; // mapId → loaded image
|
||||
std::unordered_map<std::string, Image> m_cache; // mapId → loaded image
|
||||
std::unordered_map<std::string, std::vector<uint8_t>> m_tileCache; // "mapId:tx:ty" → PNG bytes
|
||||
|
||||
/// Return cached Image or attempt to load it. Returns nullptr on failure.
|
||||
const Image* LoadOrGet(const std::string& mapId);
|
||||
|
||||
@@ -147,8 +147,16 @@ void WebRadarServer::PushSnapshot(const RuntimeUpdate& update) {
|
||||
// SetupRoutes
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Attach CORS headers that allow the UI to be served from a different origin
|
||||
// (e.g. Caddy on :8080 or a remote static host) while the API runs here.
|
||||
static void AddCors(httplib::Response& res) {
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
res.set_header("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||
res.set_header("Access-Control-Allow-Headers", "Content-Type");
|
||||
}
|
||||
|
||||
void WebRadarServer::SetupRoutes() {
|
||||
// ---- Static files ---------------------------------------------------
|
||||
// ---- Static files (fallback — normally served by Caddy or another static host) ---
|
||||
|
||||
m_server->Get("/", [this](const httplib::Request& /*req*/,
|
||||
httplib::Response& res) {
|
||||
@@ -170,10 +178,20 @@ void WebRadarServer::SetupRoutes() {
|
||||
ServeFile(res, "favicon.ico", "image/x-icon");
|
||||
});
|
||||
|
||||
// ---- CORS preflight -------------------------------------------------
|
||||
// Browsers send an OPTIONS preflight for cross-origin requests.
|
||||
|
||||
m_server->Options(".*",
|
||||
[](const httplib::Request& /*req*/, httplib::Response& res) {
|
||||
AddCors(res);
|
||||
res.status = 204;
|
||||
});
|
||||
|
||||
// ---- Bootstrap ------------------------------------------------------
|
||||
|
||||
m_server->Get("/api/bootstrap",
|
||||
[this](const httplib::Request& req, httplib::Response& res) {
|
||||
AddCors(res);
|
||||
if (!Authorise(req.remote_addr,
|
||||
req.get_param_value("password"))) {
|
||||
res.status = 401;
|
||||
@@ -187,6 +205,7 @@ void WebRadarServer::SetupRoutes() {
|
||||
|
||||
m_server->Get("/api/state",
|
||||
[this](const httplib::Request& req, httplib::Response& res) {
|
||||
AddCors(res);
|
||||
if (!Authorise(req.remote_addr,
|
||||
req.get_param_value("password"))) {
|
||||
res.status = 401;
|
||||
@@ -200,6 +219,7 @@ void WebRadarServer::SetupRoutes() {
|
||||
|
||||
m_server->Get("/events",
|
||||
[this](const httplib::Request& req, httplib::Response& res) {
|
||||
AddCors(res);
|
||||
if (!Authorise(req.remote_addr,
|
||||
req.get_param_value("password"))) {
|
||||
res.status = 401;
|
||||
@@ -246,6 +266,7 @@ void WebRadarServer::SetupRoutes() {
|
||||
|
||||
m_server->Get("/tile",
|
||||
[this](const httplib::Request& req, httplib::Response& res) {
|
||||
AddCors(res);
|
||||
if (!Authorise(req.remote_addr,
|
||||
req.get_param_value("password"))) {
|
||||
res.status = 401;
|
||||
@@ -294,6 +315,7 @@ void WebRadarServer::SetupRoutes() {
|
||||
|
||||
m_server->Get("/map-image",
|
||||
[this](const httplib::Request& req, httplib::Response& res) {
|
||||
AddCors(res);
|
||||
if (!Authorise(req.remote_addr,
|
||||
req.get_param_value("password"))) {
|
||||
res.status = 401;
|
||||
@@ -335,6 +357,7 @@ void WebRadarServer::SetupRoutes() {
|
||||
|
||||
m_server->Get("/api/debug",
|
||||
[this](const httplib::Request& /*req*/, httplib::Response& res) {
|
||||
AddCors(res);
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
wchar_t exeBuf[MAX_PATH] = {};
|
||||
|
||||
+7
-2
@@ -1,6 +1,10 @@
|
||||
const storageKey = "dayz-web-map-settings";
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const password = params.get("password") || "";
|
||||
// Optional ?server=http://host:port param — lets the UI be served from a static
|
||||
// host (Caddy, GitHub Pages, local file) while the C++ backend runs elsewhere.
|
||||
// Leave empty when the UI is served directly by the C++ server (same origin).
|
||||
const serverOrigin = (params.get("server") || "").replace(/\/$/, "");
|
||||
const favoriteLootColor = "#ef4444";
|
||||
const favoriteLootFilterDefinition = { key: "favoriteLoot", label: "Favorites", visibleKey: "showFavoriteLoot", kind: "loot", category: "favorite", color: favoriteLootColor, markerSize: 10, textSize: 14, showLabel: true };
|
||||
const lootPalette = ["#f43f5e", "#f59e0b", "#22c55e", "#eab308", "#84cc16", "#ec4899", "#0ea5e9", "#14b8a6", "#c084fc", "#f97316", "#d946ef", "#dc2626", "#fbbf24", "#fb923c", "#0891b2", "#64748b"];
|
||||
@@ -300,7 +304,8 @@ function toggleFavoriteLoot(itemName) {
|
||||
}
|
||||
|
||||
function apiUrl(path) {
|
||||
return password ? `${path}?password=${encodeURIComponent(password)}` : path;
|
||||
const base = serverOrigin + path;
|
||||
return password ? `${base}?password=${encodeURIComponent(password)}` : base;
|
||||
}
|
||||
|
||||
function tileUrl(tileX, tileY, retry = 0) {
|
||||
@@ -311,7 +316,7 @@ function tileUrl(tileX, tileY, retry = 0) {
|
||||
if (password) {
|
||||
query.set("password", password);
|
||||
}
|
||||
return `/tile?${query.toString()}`;
|
||||
return `${serverOrigin}/tile?${query.toString()}`;
|
||||
}
|
||||
|
||||
function getViewportMetrics() {
|
||||
|
||||
Reference in New Issue
Block a user