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

This commit is contained in:
67
2026-06-16 15:18:44 +08:00
commit f04e38b8ae
163 changed files with 163380 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
#include "Config.h"
#include <filesystem>
#include <fstream>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
using json = nlohmann::json;
static constexpr const char* kPath = "config/overlay.json";
// -------------------------------------------------------------------------
// Load
// -------------------------------------------------------------------------
OverlayConfig OverlayConfig::Load(const std::string& path) {
OverlayConfig cfg;
std::ifstream f(path);
if (!f.is_open()) {
spdlog::info("Config: {} not found, using defaults", path);
return cfg;
}
try {
json j = json::parse(f);
// ESP toggles
if (j.contains("showPlayers")) cfg.showPlayers = j["showPlayers"].get<bool>();
if (j.contains("showAnimals")) cfg.showAnimals = j["showAnimals"].get<bool>();
if (j.contains("showZombies")) cfg.showZombies = j["showZombies"].get<bool>();
if (j.contains("showItems")) cfg.showItems = j["showItems"].get<bool>();
if (j.contains("showBox")) cfg.showBox = j["showBox"].get<bool>();
if (j.contains("showSkeleton")) cfg.showSkeleton = j["showSkeleton"].get<bool>();
if (j.contains("showHeadDot")) cfg.showHeadDot = j["showHeadDot"].get<bool>();
if (j.contains("showCorpses")) cfg.showCorpses = j["showCorpses"].get<bool>();
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>();
// Distances
if (j.contains("playerMaxDist")) cfg.playerMaxDist = j["playerMaxDist"].get<float>();
if (j.contains("animalMaxDist")) cfg.animalMaxDist = j["animalMaxDist"].get<float>();
if (j.contains("zombieMaxDist")) cfg.zombieMaxDist = j["zombieMaxDist"].get<float>();
if (j.contains("itemMaxDist")) cfg.itemMaxDist = j["itemMaxDist"].get<float>();
// Per-category item toggles
if (j.contains("itemCategories") && j["itemCategories"].is_object()) {
for (auto& [key, val] : j["itemCategories"].items()) {
if (val.is_boolean()) cfg.itemCategories[key] = val.get<bool>();
}
}
// Resolution override
if (j.contains("overlayWidth")) cfg.overlayWidth = j["overlayWidth"].get<int>();
if (j.contains("overlayHeight")) cfg.overlayHeight = j["overlayHeight"].get<int>();
// Stretched resolution
if (j.contains("renderWidth")) cfg.renderWidth = j["renderWidth"].get<int>();
if (j.contains("renderHeight")) cfg.renderHeight = j["renderHeight"].get<int>();
if (j.contains("stretchToFill")) cfg.stretchToFill = j["stretchToFill"].get<bool>();
// Web radar
if (j.contains("webBindAddress")) cfg.webBindAddress = j["webBindAddress"].get<std::string>();
if (j.contains("webPort")) cfg.webPort = j["webPort"].get<int>();
if (j.contains("webPassword")) cfg.webPassword = j["webPassword"].get<std::string>();
spdlog::info("Config: loaded from {}", path);
} catch (const std::exception& ex) {
spdlog::warn("Config: parse error in {} — {} — using defaults", path, ex.what());
}
return cfg;
}
// -------------------------------------------------------------------------
// Save
// -------------------------------------------------------------------------
void OverlayConfig::Save(const std::string& path) const {
try {
std::filesystem::create_directories(
std::filesystem::path(path).parent_path());
json j;
// ESP toggles
j["showPlayers"] = showPlayers;
j["showAnimals"] = showAnimals;
j["showZombies"] = showZombies;
j["showItems"] = showItems;
j["showBox"] = showBox;
j["showSkeleton"] = showSkeleton;
j["showHeadDot"] = showHeadDot;
j["showCorpses"] = showCorpses;
j["showWeapon"] = showWeapon;
j["showHealthBar"] = showHealthBar;
j["showHealthNumber"] = showHealthNumber;
// Distances
j["playerMaxDist"] = playerMaxDist;
j["animalMaxDist"] = animalMaxDist;
j["zombieMaxDist"] = zombieMaxDist;
j["itemMaxDist"] = itemMaxDist;
// Per-category item toggles
json cats = json::object();
for (const auto& [key, enabled] : itemCategories)
cats[key] = enabled;
j["itemCategories"] = cats;
// Resolution override
j["overlayWidth"] = overlayWidth;
j["overlayHeight"] = overlayHeight;
// Stretched resolution
j["renderWidth"] = renderWidth;
j["renderHeight"] = renderHeight;
j["stretchToFill"] = stretchToFill;
// Web radar
j["webBindAddress"] = webBindAddress;
j["webPort"] = webPort;
j["webPassword"] = webPassword;
std::ofstream f(path);
f << j.dump(2);
spdlog::debug("Config: saved to {}", path);
} catch (const std::exception& ex) {
spdlog::warn("Config: failed to save {} — {}", path, ex.what());
}
}
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <map>
#include <string>
// -------------------------------------------------------------------------
// OverlayConfig — all user-editable runtime settings.
// Persisted as config/overlay.json (nlohmann/json).
// -------------------------------------------------------------------------
struct OverlayConfig {
// ---- ESP toggles ----
bool showPlayers = true;
bool showAnimals = true;
bool showZombies = true;
bool showItems = true;
bool showBox = true; // bounding box around players
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
// ---- Draw-distance limits (metres) ----
float playerMaxDist = 1000.0f;
float animalMaxDist = 1000.0f;
float zombieMaxDist = 500.0f;
float itemMaxDist = 200.0f;
// ---- Per-category item toggles (key = filterKey from item_filters.json) ----
// Missing key → enabled by default.
std::map<std::string, bool> itemCategories;
// ---- Overlay resolution override ----
// 0 = auto-detect from GetSystemMetrics (default behaviour).
// Set both to your MONITOR (display) resolution, not the game render resolution.
int overlayWidth = 0;
int overlayHeight = 0;
// ---- Stretched resolution support ----
// renderWidth / renderHeight: the resolution DayZ actually renders at
// (e.g. 1280x960 when running 4:3 stretched on a 1920x1080 monitor).
// Set to 0 to disable (assumes render == display).
//
// stretchToFill: true = GPU stretches the render to fill the monitor (no black bars).
// false = game maintains aspect ratio with letterbox/pillarbox bars.
//
// overlayWidth/Height should ALWAYS equal your monitor resolution regardless of mode.
int renderWidth = 0;
int renderHeight = 0;
bool stretchToFill = true;
// ---- Web radar ----
std::string webBindAddress = "0.0.0.0";
int webPort = 7777;
std::string webPassword = "";
// ------------------------------------------------------------------
// Load from path (returns default-constructed config on any error).
static OverlayConfig Load(const std::string& path);
// Save to path (creates parent directories if needed).
void Save(const std::string& path) const;
};
+234
View File
@@ -0,0 +1,234 @@
#pragma once
#include <cstdint>
#include <string>
#include <optional>
// -------------------------------------------------------------------------
// Primitive math types
// -------------------------------------------------------------------------
struct Vector3 {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
};
// -------------------------------------------------------------------------
// Local player
// -------------------------------------------------------------------------
struct LocalPlayerData {
uint64_t entityAddress = 0;
uint64_t visualStateAddress = 0;
Vector3 position;
float lookDirection = 0.0f; // heading in degrees
};
// -------------------------------------------------------------------------
// Network / server metadata
// -------------------------------------------------------------------------
struct NetworkMetadata {
std::string serverName;
std::string gameVersion;
std::optional<std::string> serverMapName;
};
// -------------------------------------------------------------------------
// Scoreboard
// -------------------------------------------------------------------------
struct DayZScoreboardPlayer {
uint32_t networkId = 0;
std::string steamId;
std::string playerName;
};
// -------------------------------------------------------------------------
// Near entity list
// -------------------------------------------------------------------------
struct DayZNearEntityEntry {
uint64_t address = 0;
uint32_t networkId = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
std::string entityName;
std::string typeName;
std::string configName;
std::string modelName;
};
// -------------------------------------------------------------------------
// Far entity list (same schema as near)
// -------------------------------------------------------------------------
struct DayZFarEntityEntry {
uint64_t address = 0;
uint32_t networkId = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
std::string entityName;
std::string typeName;
std::string configName;
std::string modelName;
};
// -------------------------------------------------------------------------
// Slow entity list (no networkId)
// -------------------------------------------------------------------------
struct DayZSlowEntityEntry {
uint64_t address = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
std::string entityName;
std::string typeName;
std::string configName;
std::string modelName;
};
// -------------------------------------------------------------------------
// Skeleton bones (12 named bones used for segment rendering)
// Layout matches Spectre PLAYER_BONE_IDS / INFECTED_BONE_IDS segment topology.
// -------------------------------------------------------------------------
// Bone layout mirrors Spectre stable segments.
// Player: spine→neck→head; neck→shoulders→elbows→hands; spine→hips→knees→ankles
// Zombie: spine→neck→head; spine→shoulders→elbows→hands; spine→pelvis→hips→knees→ankles
struct SkeletonBones {
Vector3 neck;
Vector3 head;
Vector3 spine; // chest hub (player bone 18, zombie bone 19)
Vector3 pelvis; // zombie only — connects spine to hips
Vector3 rightShoulder;
Vector3 rightElbow;
Vector3 rightHand;
Vector3 leftShoulder;
Vector3 leftElbow;
Vector3 leftHand;
Vector3 rightHip;
Vector3 rightKnee;
Vector3 rightAnkle;
Vector3 leftHip;
Vector3 leftKnee;
Vector3 leftAnkle;
bool valid = false;
};
// -------------------------------------------------------------------------
// Player entry (full)
// -------------------------------------------------------------------------
struct DayZPlayerEntry {
uint64_t address = 0;
uint32_t networkId = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
bool isDead = false;
bool isAdmin = false; // model matches a known invisible/admin model path
std::string nickname;
std::string itemInHands;
std::string typeName;
std::string configName;
std::string modelName;
SkeletonBones skeleton;
// health in the range [0, 100]. -1.0f = not yet read / offset unknown.
float health = -1.0f;
};
// -------------------------------------------------------------------------
// Bullet entry
// -------------------------------------------------------------------------
struct DayZBulletEntry {
uint64_t address = 0;
std::optional<Vector3> position;
std::optional<Vector3> direction;
std::optional<float> headingDegrees;
std::string directionSource; // e.g. "visual", "raw"
};
// -------------------------------------------------------------------------
// Item list entry
// -------------------------------------------------------------------------
struct DayZItemListEntry {
uint64_t address = 0;
std::optional<Vector3> position;
std::string entityName;
std::string cleanName;
std::string typeName;
std::string configName;
std::string modelName;
std::string filterKey; // lower-case composite for filtering
};
// -------------------------------------------------------------------------
// Specialised slow-list entity types
// -------------------------------------------------------------------------
struct DayZAnimalEntry {
uint64_t address = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
std::string entityName;
std::string typeName;
std::string configName;
std::string modelName;
};
struct DayZZombieEntry {
uint64_t address = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
std::string entityName;
std::string typeName;
std::string configName;
std::string modelName;
SkeletonBones skeleton;
};
// Vehicles: cars, boats, helicopters, etc.
enum class VehicleKind : uint8_t {
Unknown = 0,
Car,
Boat,
Helicopter,
Plane,
};
struct DayZCarAndBoatEntry {
uint64_t address = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
std::string entityName;
std::string typeName;
std::string configName;
std::string modelName;
VehicleKind kind = VehicleKind::Unknown;
};
struct DayZOtherEntityEntry {
uint64_t address = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
std::string entityName;
std::string typeName;
std::string configName;
std::string modelName;
};
// -------------------------------------------------------------------------
// Camera data (read from World+0x1B8 inline struct each frame)
// -------------------------------------------------------------------------
struct CameraData {
Vector3 invertedViewRight;
Vector3 invertedViewUp;
Vector3 invertedViewForward;
Vector3 translation;
float projectionD1x = 0.0f; // horizontal FOV scale
float projectionD2y = 0.0f; // vertical FOV scale
bool valid = false;
};
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <cstdint>
struct RuntimeSession {
uint32_t processId = 0;
uint64_t gameBaseAddress = 0;
uint64_t worldAddress = 0;
uint64_t networkManagerAddress = 0;
};
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
// Returns a direct pointer into the Windows RCDATA resource for the given map
// ID, plus its byte length. The pointer is valid for the entire process lifetime
// — 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);
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <memory>
#include <string>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h> // needed before spdlog on MSVC to avoid winsock conflicts
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
// -------------------------------------------------------------------------
// AppLogger
// Call AppLogger::Init() once from main() before anything else.
// Then use spdlog::get("dayz") or the DAYZ_LOG_* macros everywhere.
// -------------------------------------------------------------------------
namespace AppLogger {
inline void Init(spdlog::level::level_enum level = spdlog::level::info) {
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(level);
console_sink->set_pattern("[%H:%M:%S.%e] [%^%l%$] [%n] %v");
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/dayz-memory.log", /*truncate=*/true);
file_sink->set_level(spdlog::level::trace);
file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%n] %v");
auto logger = std::make_shared<spdlog::logger>(
"dayz",
spdlog::sinks_init_list{console_sink, file_sink}
);
logger->set_level(spdlog::level::trace);
logger->flush_on(spdlog::level::warn);
spdlog::register_logger(logger);
spdlog::set_default_logger(logger);
}
inline std::shared_ptr<spdlog::logger> Get() {
return spdlog::get("dayz");
}
} // namespace AppLogger
+156
View File
@@ -0,0 +1,156 @@
#pragma once
#include <cstdint>
#include <cmath>
#include <string>
#include <algorithm>
#include <numbers> // std::numbers::pi (C++20)
#include "../Core/Models.h"
// -------------------------------------------------------------------------
// Header-only port of C# MemoryValidation.cs
// -------------------------------------------------------------------------
namespace MemoryValidation {
// -------------------------------------------------------------------------
// Address sanity
// -------------------------------------------------------------------------
/// Returns true if the address falls in the valid user-mode range:
/// [0x0000000100000000, 0x0000FFFF00000000)
[[nodiscard]] inline bool IsValidUserAddress(uint64_t address) noexcept {
constexpr uint64_t kLow = 0x0000000100000000ULL;
constexpr uint64_t kHigh = 0x0000FFFF00000000ULL;
return address >= kLow && address < kHigh;
}
// -------------------------------------------------------------------------
// Vector plausibility
// -------------------------------------------------------------------------
/// Returns true when all three components are finite and within ±100 000 units.
[[nodiscard]] inline bool IsValidVector(const Vector3& v) noexcept {
constexpr float kLimit = 100000.0f;
return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z)
&& std::abs(v.x) < kLimit
&& std::abs(v.y) < kLimit
&& std::abs(v.z) < kLimit;
}
// -------------------------------------------------------------------------
// String helpers
// -------------------------------------------------------------------------
/// Returns true when the string is non-empty and not all whitespace.
[[nodiscard]] inline bool IsUsableString(const std::string& s) noexcept {
if (s.empty()) return false;
for (char c : s) {
if (c != ' ' && c != '\t' && c != '\r' && c != '\n') return true;
}
return false;
}
namespace detail {
/// Classifies a char as "readable": printable ASCII (0x20-0x7E) or high-byte
/// (Latin-1 / Windows-1251 printable range 0xA0-0xFF).
[[nodiscard]] inline bool IsReadableChar(unsigned char c) noexcept {
return (c >= 0x20 && c <= 0x7E) || (c >= 0xA0);
}
/// Returns the fraction of chars that are readable (0.01.0).
[[nodiscard]] inline double ReadableRatio(const std::string& s) noexcept {
if (s.empty()) return 0.0;
int readable = 0;
for (unsigned char c : s) {
if (IsReadableChar(c)) ++readable;
}
return static_cast<double>(readable) / static_cast<double>(s.size());
}
/// Returns true when the string contains at least one control character
/// (excluding normal whitespace: space, tab, CR, LF).
[[nodiscard]] inline bool HasUnwantedControlChar(const std::string& s) noexcept {
for (unsigned char c : s) {
if (c < 0x20 && c != '\t' && c != '\r' && c != '\n') return true;
}
return false;
}
} // namespace detail
/// Returns true when the server name is plausible:
/// - length 396
/// - no control characters
/// - readable-char ratio > 50 %
/// - contains at least one ASCII alphanumeric character
[[nodiscard]] inline bool IsPlausibleServerName(const std::string& s) noexcept {
if (s.size() < 3 || s.size() > 96) return false;
if (detail::HasUnwantedControlChar(s)) return false;
if (detail::ReadableRatio(s) < 0.5) return false;
for (char c : s) {
if (std::isalnum(static_cast<unsigned char>(c))) return true;
}
return false;
}
/// Returns true when the game version string is plausible:
/// - length 332
/// - contains at least one digit
/// - contains at least one of '.', '-', '_'
[[nodiscard]] inline bool IsPlausibleGameVersion(const std::string& s) noexcept {
if (s.size() < 3 || s.size() > 32) return false;
bool hasDigit = false;
bool hasSeparator = false;
for (char c : s) {
if (std::isdigit(static_cast<unsigned char>(c))) hasDigit = true;
if (c == '.' || c == '-' || c == '_') hasSeparator = true;
}
return hasDigit && hasSeparator;
}
/// Sanitizes an entity string read from memory.
/// Returns empty string if the content has unwanted control characters
/// or if the readable-char ratio is below 50 %.
/// Otherwise returns the original string.
[[nodiscard]] inline std::string SanitizeEntityString(const std::string& s) {
if (s.empty()) return {};
if (detail::HasUnwantedControlChar(s)) return {};
if (detail::ReadableRatio(s) < 0.5) return {};
return s;
}
// -------------------------------------------------------------------------
// Heading helpers
// -------------------------------------------------------------------------
/// Normalises an arbitrary heading (degrees) into [0, 360).
[[nodiscard]] inline float NormalizeHeading(float degrees) noexcept {
float result = std::fmod(degrees, 360.0f);
if (result < 0.0f) result += 360.0f;
return result;
}
/// Computes a world heading (degrees, 0 = north/+Z) from a 2-D direction
/// vector (dirX, dirY) where dirY typically corresponds to the forward axis.
///
/// Formula mirrors the C# original:
/// heading = atan2(dirY, dirX) * 180 / PI + 90
/// then normalized to [0, 360).
///
/// Returns false when the direction vector is degenerate (near-zero length).
[[nodiscard]] inline bool TryGetCorrectedHeadingFromDirection(
float dirX, float dirY, float& heading) noexcept
{
constexpr float kEpsilon = 1e-6f;
if (std::abs(dirX) < kEpsilon && std::abs(dirY) < kEpsilon) {
heading = 0.0f;
return false;
}
constexpr float kRadToDeg = 180.0f / static_cast<float>(std::numbers::pi);
heading = NormalizeHeading(std::atan2(dirY, dirX) * kRadToDeg + 90.0f);
return true;
}
} // namespace MemoryValidation
+299
View File
@@ -0,0 +1,299 @@
#include "VmmAccessor.h"
// VmmAccessor.h includes <Windows.h> with WIN32_LEAN_AND_MEAN, which strips
// <winternl.h>. vmmdll.h needs NTSTATUS from <winternl.h>, so pull it in
// explicitly here before including vmmdll.h.
#include <winternl.h>
// vmmdll.h is needed here only for VMMDLL_ConfigSet / VMMDLL_OPT_REFRESH_ALL.
#include "vmmdll.h"
#include <algorithm> // std::find
#include <spdlog/spdlog.h>
// -------------------------------------------------------------------------
// Initialize
// -------------------------------------------------------------------------
bool VmmAccessor::Initialize(bool useMemoryMap) {
std::unique_lock<std::shared_mutex> lk(m_accessMutex);
m_process.reset();
m_attachedPid = 0;
spdlog::trace("VmmAccessor: initialising DMA (useMemoryMap={})...", useMemoryMap);
m_dma = std::make_unique<DMA>(useMemoryMap);
if (!m_dma->is_initialized()) {
spdlog::error("VmmAccessor: DMA initialisation failed.");
return false;
}
spdlog::info("VmmAccessor: DMA initialised.");
return true;
}
// -------------------------------------------------------------------------
// ForceRefresh
// -------------------------------------------------------------------------
void VmmAccessor::ForceRefresh() {
if (!IsInitialized()) return;
VMMDLL_ConfigSet(m_dma->handle.get(), VMMDLL_OPT_REFRESH_ALL, 1);
}
// -------------------------------------------------------------------------
// TryFindProcess
// -------------------------------------------------------------------------
bool VmmAccessor::TryFindProcess(const std::string& processName,
uint32_t& outPid) {
if (!IsInitialized()) return false;
DWORD pid = m_dma->get_process_id(processName);
if (pid == 0) return false;
// Already attached to this exact PID — reuse the existing Process handle.
// WaitForSession re-attaches every 3 s while waiting for spawn; without this
// early-out we'd needlessly tear down a valid handle and re-run the CR3 fix
// (and log it) on every one of those ticks.
if (m_process && m_attachedPid == static_cast<uint32_t>(pid)) {
outPid = m_attachedPid;
return true;
}
// (Re-)create Process bound to this PID so all subsequent reads use it.
// Unique lock: blocks the camera thread's reads while the handle is swapped
// so it can never dereference a half-destroyed Process.
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);
outPid = m_attachedPid;
// Apply the CR3 fix so reads use UserDirectoryTableBase instead of the
// kernel DTB. Required for games (including DayZ) that run user-mode
// code under a separate paging context. No-op if already on the right DTB.
if (!m_process->fix_cr3(processName)) {
spdlog::warn("VmmAccessor: fix_cr3 failed for '{}' — reads may be unreliable.", processName);
} else {
spdlog::info("VmmAccessor: CR3 fix applied for '{}'.", processName);
}
spdlog::trace("VmmAccessor: attached to '{}' PID={}", processName, m_attachedPid);
return true;
}
// -------------------------------------------------------------------------
// TryGetModuleBase
// -------------------------------------------------------------------------
bool VmmAccessor::TryGetModuleBase(uint32_t /*pid*/,
const std::string& moduleName,
uint64_t& outBase) {
if (!IsAttached()) return false;
uint64_t base = m_process->get_base_address(moduleName);
if (base == 0) {
spdlog::warn("VmmAccessor: module '{}' base not found.", moduleName);
return false;
}
spdlog::trace("VmmAccessor: module '{}' base=0x{:X}", moduleName, base);
outBase = base;
return true;
}
// -------------------------------------------------------------------------
// IsProcessAlive
// -------------------------------------------------------------------------
bool VmmAccessor::IsProcessAlive(uint32_t pid,
const std::string& processName) {
if (!IsInitialized()) return false;
DWORD livePid = m_dma->get_process_id(processName);
return livePid != 0 && static_cast<uint32_t>(livePid) == pid;
}
// -------------------------------------------------------------------------
// ReadRaw (private)
// -------------------------------------------------------------------------
bool VmmAccessor::ReadRaw(uint32_t /*pid*/, uint64_t address,
void* buf, size_t size) {
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);
}
// -------------------------------------------------------------------------
// TryReadPointer
// -------------------------------------------------------------------------
bool VmmAccessor::TryReadPointer(uint32_t pid, uint64_t address,
uint64_t& outPointer) {
if (!MemoryValidation::IsValidUserAddress(address)) return false;
uint64_t ptr = 0;
if (!ReadRaw(pid, address, &ptr, sizeof(ptr))) return false;
if (!MemoryValidation::IsValidUserAddress(ptr)) return false;
outPointer = ptr;
return true;
}
// -------------------------------------------------------------------------
// ScatterRead
// -------------------------------------------------------------------------
bool VmmAccessor::ScatterRead(uint32_t pid, std::vector<ScatterEntry>& entries) {
if (entries.empty()) return false;
std::shared_lock<std::shared_mutex> lk(m_accessMutex);
if (!m_process) return false;
VMMDLL_SCATTER_HANDLE hScatter = m_process->create_scatter(static_cast<DWORD>(pid));
if (!hScatter) return false;
for (auto& e : entries) {
if (e.buffer && e.size > 0 && MemoryValidation::IsValidUserAddress(e.address))
m_process->add_read_scatter(hScatter, e.address, e.buffer, e.size);
}
m_process->execute_scatter(hScatter, static_cast<DWORD>(pid));
m_process->close_scatter(hScatter);
return true;
}
// -------------------------------------------------------------------------
// ReadBytes
// -------------------------------------------------------------------------
bool VmmAccessor::ReadBytes(uint32_t pid,
uint64_t address,
size_t count,
std::vector<uint8_t>& outBytes) {
if (count == 0) {
outBytes.clear();
return true;
}
outBytes.resize(count);
if (!ReadRaw(pid, address, outBytes.data(), count)) {
outBytes.clear();
return false;
}
return true;
}
// -------------------------------------------------------------------------
// DecodeArmaBytes (static, private)
// -------------------------------------------------------------------------
std::string VmmAccessor::DecodeArmaBytes(const std::vector<uint8_t>& bytes) {
if (bytes.empty()) return {};
// Determine effective length (stop at first null terminator).
size_t len = 0;
while (len < bytes.size() && bytes[len] != 0) ++len;
if (len == 0) return {};
// --- Attempt 1: validate as UTF-8 ---
auto isValidUtf8 = [](const uint8_t* data, size_t n) -> bool {
size_t i = 0;
while (i < n) {
uint8_t b = data[i];
int seqLen = 0;
if ((b & 0x80) == 0x00) seqLen = 1;
else if ((b & 0xE0) == 0xC0) seqLen = 2;
else if ((b & 0xF0) == 0xE0) seqLen = 3;
else if ((b & 0xF8) == 0xF0) seqLen = 4;
else return false;
if (i + seqLen > n) return false;
for (int j = 1; j < seqLen; ++j) {
if ((data[i + j] & 0xC0) != 0x80) return false;
}
i += seqLen;
}
return true;
};
if (isValidUtf8(bytes.data(), len)) {
return std::string(reinterpret_cast<const char*>(bytes.data()), len);
}
// --- Attempt 2: treat as Windows CP1251, convert to UTF-8 ---
int wideLen = MultiByteToWideChar(1251 /*CP1251*/, 0,
reinterpret_cast<LPCCH>(bytes.data()),
static_cast<int>(len),
nullptr, 0);
if (wideLen <= 0) {
return std::string(reinterpret_cast<const char*>(bytes.data()), len);
}
std::wstring wide(static_cast<size_t>(wideLen), L'\0');
MultiByteToWideChar(1251, 0,
reinterpret_cast<LPCCH>(bytes.data()),
static_cast<int>(len),
wide.data(), wideLen);
int utf8Len = WideCharToMultiByte(CP_UTF8, 0,
wide.data(), wideLen,
nullptr, 0,
nullptr, nullptr);
if (utf8Len <= 0) {
return std::string(reinterpret_cast<const char*>(bytes.data()), len);
}
std::string utf8(static_cast<size_t>(utf8Len), '\0');
WideCharToMultiByte(CP_UTF8, 0,
wide.data(), wideLen,
utf8.data(), utf8Len,
nullptr, nullptr);
return utf8;
}
// -------------------------------------------------------------------------
// ReadArmaString
// -------------------------------------------------------------------------
std::string VmmAccessor::ReadArmaString(uint32_t pid, uint64_t address,
size_t maxLength) {
if (!MemoryValidation::IsValidUserAddress(address)) return {};
// Read the 16-bit length field at address + 0x8
uint16_t strLen = 0;
if (!ReadRaw(pid, address + 0x8, &strLen, sizeof(strLen))) return {};
if (strLen == 0) return {};
size_t readLen = static_cast<size_t>(strLen);
if (readLen > maxLength) readLen = maxLength;
// Read the character data at address + 0x10
std::vector<uint8_t> buf(readLen);
if (!ReadRaw(pid, address + 0x10, buf.data(), readLen)) return {};
return DecodeArmaBytes(buf);
}
// -------------------------------------------------------------------------
// ReadCString
// -------------------------------------------------------------------------
std::string VmmAccessor::ReadCString(uint32_t pid, uint64_t address,
size_t maxLength) {
if (!MemoryValidation::IsValidUserAddress(address)) return {};
if (maxLength == 0 || !IsAttached()) return {};
std::vector<uint8_t> buf(maxLength);
if (!ReadRaw(pid, address, buf.data(), maxLength)) {
// Partial reads: try progressively smaller chunks down to 1 page.
// If even that fails, give up.
if (maxLength <= 64 || !ReadRaw(pid, address, buf.data(), 64)) {
return {};
}
buf.resize(64);
}
// Truncate at null terminator if present.
auto nullPos = std::find(buf.begin(), buf.end(), uint8_t{0});
buf.erase(nullPos, buf.end());
if (buf.empty()) return {};
return DecodeArmaBytes(buf);
}
+152
View File
@@ -0,0 +1,152 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <memory>
#include <shared_mutex>
#include <type_traits>
#include <cstring> // memcpy
// Windows must come before vmmdll / VolkDMA headers.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
// VolkDMA wrapper classes (DMA handles FPGA init, Process handles per-process reads)
#include <VolkDMA/dma.hh>
#include <VolkDMA/process.hh>
#include "MemoryValidation.h"
// -------------------------------------------------------------------------
// VmmAccessor
// Thin wrapper around VolkDMA's DMA + Process classes providing the same
// read-oriented API used throughout this project.
// -------------------------------------------------------------------------
class VmmAccessor {
public:
VmmAccessor() = default;
~VmmAccessor() = default;
// Non-copyable, movable
VmmAccessor(const VmmAccessor&) = delete;
VmmAccessor& operator=(const VmmAccessor&) = delete;
VmmAccessor(VmmAccessor&&) = default;
VmmAccessor& operator=(VmmAccessor&&) = default;
// ------------------------------------------------------------------
// Lifecycle
// ------------------------------------------------------------------
/// Initialise the DMA handle.
/// @param useMemoryMap When true, VolkDMA auto-generates / loads memory_map.txt
/// for faster physical-memory resolution (recommended).
bool Initialize(bool useMemoryMap = true);
/// Find a running process by name, store its PID, and create the
/// internal Process object. Must be called before any reads.
bool TryFindProcess(const std::string& processName, uint32_t& outPid);
/// Get the base virtual address of a named module inside the attached process.
bool TryGetModuleBase(uint32_t pid,
const std::string& moduleName,
uint64_t& outBase);
/// Returns true when the process is still alive (re-queries the DMA layer).
bool IsProcessAlive(uint32_t pid, const std::string& processName);
/// Force a full refresh of VMMDLL's internal caches.
void ForceRefresh();
// ------------------------------------------------------------------
// Memory reads
// ------------------------------------------------------------------
/// Read a 64-bit pointer. Validates both the source and result address.
bool TryReadPointer(uint32_t pid, uint64_t address, uint64_t& outPointer);
/// Read a plain-old-data value of type T.
template<typename T>
bool TryReadValue(uint32_t pid, uint64_t address, T& outValue);
/// Read raw bytes into a caller-supplied vector.
bool ReadBytes(uint32_t pid,
uint64_t address,
size_t count,
std::vector<uint8_t>& outBytes);
// ------------------------------------------------------------------
// Scatter reads — batch multiple reads into one DMA round-trip.
// ------------------------------------------------------------------
struct ScatterEntry {
uint64_t address;
void* buffer;
size_t size;
};
/// Queue all entries and execute them in a single DMA transaction.
/// Returns false only if the scatter handle itself couldn't be created.
/// Individual failed reads zero-fill their buffer (VMMDLL guarantee).
bool ScatterRead(uint32_t pid, std::vector<ScatterEntry>& entries);
/// Read an Arma/DayZ engine string (uint16_t length at addr+0x8,
/// data at addr+0x10). Falls back from UTF-8 to CP1251 if needed.
std::string ReadArmaString(uint32_t pid, uint64_t address,
size_t maxLength = 256);
/// Read a null-terminated C string.
std::string ReadCString(uint32_t pid, uint64_t address,
size_t maxLength = 256);
// ------------------------------------------------------------------
// State
// ------------------------------------------------------------------
[[nodiscard]] bool IsInitialized() const noexcept {
return m_dma && m_dma->is_initialized();
}
[[nodiscard]] bool IsAttached() const noexcept {
return m_process != nullptr;
}
private:
std::unique_ptr<DMA> m_dma;
std::unique_ptr<Process> m_process;
uint32_t m_attachedPid = 0;
// Guards m_process / m_dma lifetime against concurrent reads.
// Reads (ReadRaw / ScatterRead) take a SHARED lock so they run in parallel;
// Initialize / TryFindProcess take a UNIQUE lock while swapping the handle.
// This lets the dedicated camera thread read concurrently with the main
// runtime thread without a use-after-free when a reconnect re-creates Process.
mutable std::shared_mutex m_accessMutex;
/// Low-level read via VolkDMA Process. Returns false on read error.
bool ReadRaw(uint32_t pid, uint64_t address, void* buf, size_t size);
/// Attempt to decode a byte buffer as UTF-8; if that fails,
/// re-interpret as Windows CP1251 and convert to UTF-8.
static std::string DecodeArmaBytes(const std::vector<uint8_t>& bytes);
};
// -------------------------------------------------------------------------
// Template implementation (must live in the header)
// -------------------------------------------------------------------------
template<typename T>
bool VmmAccessor::TryReadValue(uint32_t pid, uint64_t address, T& outValue) {
static_assert(std::is_trivially_copyable_v<T>,
"TryReadValue requires a trivially-copyable type");
T tmp{};
if (!ReadRaw(pid, address, &tmp, sizeof(T))) return false;
outValue = tmp;
return true;
}
+166
View File
@@ -0,0 +1,166 @@
#pragma once
#include <cstdint>
// ============================================================================
// Offsets.h — DayZ Enfusion engine memory offsets
//
// Verification log (update whenever you confirm offsets against a live client):
//
// DayZ 1.29 | 2026-06-13
// Base::* — SigScanner-scanned; patterns in SigScanner.cpp
// World::* — live-debugged via Spectre stable
// Common::*, Network::* — live-debugged; cross-checked with C# port
// Player::*, Inventory::* — live-debugged; SigScanner covers IsDead/Inventory
// Skeleton::* — animClass/matrixClass probed at runtime (see cands)
//
// When the game patches, update the SigScanner patterns first (they cover the
// most volatile offsets). Hard-coded offsets that the scanner does NOT cover
// are marked [manual] and must be verified by hand after each patch.
// ============================================================================
namespace Offsets {
// [manual] Module-relative RVAs — also covered by SigScanner (World, NetworkManager)
namespace Base {
constexpr uint64_t World = 0x4264028; // v1.29
constexpr uint64_t NetworkManager = 0x100FC10; // v1.29
constexpr uint64_t Tick = 0xFF4998; // v1.29 [manual]
constexpr uint64_t Fov = 0x100A7D8; // v1.29 [manual]
}
// World struct member offsets — also covered by SigScanner
namespace World {
constexpr uint64_t NearEntityList = 0xF48; // v1.29
constexpr uint64_t NearTableSize = 0xF50; // v1.29
constexpr uint64_t FarEntityList = 0x1090; // v1.29
constexpr uint64_t FarTableSize = 0x1098; // v1.29
constexpr uint64_t SlowEntityList = 0x2010; // v1.29 [manual]
constexpr uint64_t SlowTableAllocCount= 0x2018; // v1.29 [manual]
constexpr uint64_t SlowTableValidCount= 0x1FA0; // v1.29 [manual]
constexpr uint64_t LocalPlayer = 0x2960; // v1.29 [manual]
constexpr uint64_t ItemList = 0x2060; // v1.29
constexpr uint64_t ItemTableAllocCount= 0x2068; // v1.29
constexpr uint64_t ItemTableValidCount= 0x2070; // v1.29
constexpr uint64_t BulletTable = 0xE00; // v1.29
constexpr uint64_t BulletCount = 0xE08; // v1.29
constexpr uint64_t Camera = 0x1B8; // v1.29 [manual]
}
// [manual] Common entity struct offsets — VisualState also covered by SigScanner
namespace Common {
constexpr uint64_t Item = 0x18; // v1.29
constexpr uint64_t Path = 0xB0; // v1.29
constexpr uint64_t Size = 0x8; // v1.29
constexpr uint64_t Type = 0x180; // v1.29
constexpr uint64_t VisualState = 0x1C8; // v1.29
constexpr uint64_t FutureVisualState = 0x120; // v1.29 [manual]
constexpr uint64_t DirectionX = 0x20; // v1.29 [manual]
constexpr uint64_t DirectionY = 0x28; // v1.29 [manual]
constexpr uint64_t Position = 0x2C; // v1.29 [manual]
}
// [manual] Network manager struct offsets
namespace Network {
constexpr uint64_t ManagerNetworkClient = 0x50; // v1.29
constexpr uint64_t EntityNetworkId = 0x6DC; // v1.29
constexpr uint64_t ClientScoreboard = 0x18; // v1.29
constexpr uint64_t ClientPlayerCount = 0x24; // v1.29
constexpr uint64_t ClientPlayerIdentitySize = 0x170; // v1.29
constexpr uint64_t PlayerIdentityNetworkId = 0x30; // v1.29
constexpr uint64_t PlayerIdentitySteamId = 0xA0; // v1.29
constexpr uint64_t PlayerIdentityPlayerName = 0xF8; // v1.29
constexpr uint64_t PlayerCount = 0x24; // v1.29
constexpr uint64_t ServerName = 0x308; // v1.29 [manual]
constexpr uint64_t GameVersion = 0x350; // v1.29 [manual]
constexpr uint64_t MapName = 0x38; // v1.29 [manual]
constexpr uint64_t ThirdPerson = 0x9C; // v1.29 [manual]
constexpr uint64_t Crosshair = 0xA0; // v1.29 [manual]
}
// [manual] Entity type object offsets
namespace EntityType {
constexpr uint64_t ModelName = 0xB0; // v1.29
constexpr uint64_t ConfigName = 0x98; // v1.29
constexpr uint64_t TypeName = 0xD0; // v1.29
constexpr uint64_t CleanName = 0x518; // v1.29
}
// [manual] Camera struct offsets (relative to World::Camera pointer target)
namespace Camera {
constexpr uint64_t InvertedViewRight = 0x8; // v1.29
constexpr uint64_t InvertedViewUp = 0x14; // v1.29
constexpr uint64_t InvertedViewForward = 0x20; // v1.29
constexpr uint64_t Translation = 0x2C; // v1.29
constexpr uint64_t ViewPortSize = 0x58; // v1.29
constexpr uint64_t ProjectionD1 = 0xD0; // v1.29
constexpr uint64_t ProjectionD2 = 0xDC; // v1.29
}
namespace CameraFov {
// Camera FOV offsets (derived from Base::Fov)
// Populated as needed; base offset is Base::Fov
}
// Player entity struct offsets — IsDead/Inventory also covered by SigScanner
namespace Player {
constexpr uint64_t Skeleton = 0x7E0; // v1.29 [manual]; alt: 0x810 (external source)
constexpr uint64_t InputController = 0x7E8; // v1.29 [manual]
constexpr uint64_t IsDead = 0xE2; // v1.29
constexpr uint64_t EntityDead = 0x15D; // v1.29 [manual]
// TODO: verify health offset for current game version.
// Set to 0 to disable health reading; set to the correct entity offset
// for a float in the range 0..100 once confirmed via RE.
constexpr uint64_t Health = 0x0;
}
namespace Infected {
// Infected share common entity offsets; extend here as needed
// Skeleton pointer: see Skeleton::ZombieCand
}
// Inventory struct offsets — Base/Hands also covered by SigScanner
namespace Inventory {
constexpr uint64_t Base = 0x650; // v1.29
constexpr uint64_t ItemQuality = 0x194; // v1.29 [manual]
constexpr uint64_t Hands = 0x1B0; // v1.29
constexpr uint64_t HandItemValid = 0x1CC; // v1.29 [manual]
constexpr uint64_t WornClothes = 0x150; // v1.29 [manual] — ptr to weared-clothes grid object
constexpr uint64_t PlayerCargoGrid = 0x150; // alias kept for compatibility
constexpr uint64_t CargoGridCount = 0xC; // v1.29 [manual] — uint32 count at grid+0xC
constexpr uint64_t ItemPtr = 0x8; // v1.29 [manual] — ptr to items array at grid+0x8
constexpr uint64_t ItemSize = 0x10; // v1.29 [manual] — stride per item slot (16 bytes)
}
namespace SlowTable {
constexpr uint64_t EntrySize = 0x18; // v1.29
constexpr uint64_t EntryPointerOffset = 0x8; // v1.29
}
namespace Skeleton {
// Entity → skeleton pointer offset candidates (probed at runtime, first match wins).
// v1.29 confirmed: player=0x7E0, zombie=0x670.
// External source (unknown version): player=0x810, zombie=0x680.
constexpr uint64_t PlayerCand[2] = { 0x7E0, 0x810 };
constexpr uint64_t ZombieCand[2] = { 0x670, 0x680 };
// Convenience aliases (first candidate = last-known-good).
constexpr uint64_t PlayerOffset = PlayerCand[0];
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 };
// 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
}
} // namespace Offsets
+850
View File
@@ -0,0 +1,850 @@
#include "GameOverlay.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <format>
#include <string>
#include <unordered_set>
#include <vector>
#include "Readers/EntityTypeCache.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
#include <imgui.h>
#include <imgui_internal.h>
#include "MenuBridge.h"
// Per-frame bridge pointer consumed by the vendored Lumin menu (gui.cpp).
MenuBridge* g_menu = nullptr;
// -------------------------------------------------------------------------
// Bone interpolation / extrapolation helpers
// -------------------------------------------------------------------------
int64_t GameOverlay::NowMs() {
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
// Linearly interpolate (or extrapolate when t > 1) between two bone sets.
static SkeletonBones LerpBones(const SkeletonBones& a, const SkeletonBones& b, float t) {
auto lv = [t](const Vector3& p, const Vector3& q) -> Vector3 {
return { p.x + t*(q.x-p.x), p.y + t*(q.y-p.y), p.z + t*(q.z-p.z) };
};
SkeletonBones out;
out.valid = b.valid;
out.neck = lv(a.neck, b.neck);
out.head = lv(a.head, b.head);
out.spine = lv(a.spine, b.spine);
out.pelvis = lv(a.pelvis, b.pelvis);
out.rightShoulder = lv(a.rightShoulder, b.rightShoulder);
out.rightElbow = lv(a.rightElbow, b.rightElbow);
out.rightHand = lv(a.rightHand, b.rightHand);
out.leftShoulder = lv(a.leftShoulder, b.leftShoulder);
out.leftElbow = lv(a.leftElbow, b.leftElbow);
out.leftHand = lv(a.leftHand, b.leftHand);
out.rightHip = lv(a.rightHip, b.rightHip);
out.rightKnee = lv(a.rightKnee, b.rightKnee);
out.rightAnkle = lv(a.rightAnkle, b.rightAnkle);
out.leftHip = lv(a.leftHip, b.leftHip);
out.leftKnee = lv(a.leftKnee, b.leftKnee);
out.leftAnkle = lv(a.leftAnkle, b.leftAnkle);
return out;
}
// Update the history when a new DMA read is detected (head bone moved).
static void UpdateBoneHistory(GameOverlay::BoneHistory& h,
const SkeletonBones& bones, int64_t nowMs)
{
if (!h.initialized) {
h.prev = h.curr = bones;
h.prevMs = h.currMs = nowMs;
h.initialized = true;
return;
}
const Vector3& o = h.curr.head;
const Vector3& n = bones.head;
float dx = o.x-n.x, dy = o.y-n.y, dz = o.z-n.z;
float dist2 = dx*dx + dy*dy + dz*dz;
// Reject implausible jumps: > 3 m in one update cycle can't happen at
// any in-game speed — it means the scatter read returned garbage.
// Keeping the old history prevents one stale read from corrupting the
// extrapolation state; the DMA-side eviction (cache->valid=false) will
// fix the pointer within the next boneRefreshMs window.
if (dist2 > 9.0f) return;
if (dist2 > 1e-8f) {
h.prev = h.curr;
h.prevMs = h.currMs;
h.curr = bones;
h.currMs = nowMs;
}
}
// Return bones extrapolated to nowMs using the recorded velocity.
// Alpha is clamped to 1.5 update intervals to limit overshoot on direction changes.
static SkeletonBones GetSmoothedBones(const GameOverlay::BoneHistory& h, int64_t nowMs) {
if (!h.initialized) return h.curr;
int64_t dt = h.currMs - h.prevMs;
if (dt <= 0) return h.curr;
float alpha = static_cast<float>(nowMs - h.prevMs) / static_cast<float>(dt);
alpha = std::min(alpha, 1.5f);
return LerpBones(h.prev, h.curr, alpha);
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// SetWebRadarPort — resolve LAN IPs and build display URLs
// -------------------------------------------------------------------------
void GameOverlay::SetWebRadarPort(int port) {
m_webPort = port;
m_webUrls.clear();
m_webUrls.push_back(std::format("http://localhost:{}", port));
// Enumerate all unicast IPv4 addresses.
ULONG bufLen = 15000;
std::vector<BYTE> buf(bufLen);
auto* table = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buf.data());
DWORD ret = GetAdaptersAddresses(AF_INET,
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
nullptr, table, &bufLen);
if (ret == ERROR_BUFFER_OVERFLOW) {
buf.resize(bufLen);
table = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buf.data());
ret = GetAdaptersAddresses(AF_INET,
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
nullptr, table, &bufLen);
}
if (ret == NO_ERROR) {
for (auto* a = table; a; a = a->Next) {
if (a->OperStatus != IfOperStatusUp) continue;
for (auto* ua = a->FirstUnicastAddress; ua; ua = ua->Next) {
auto* sin = reinterpret_cast<sockaddr_in*>(ua->Address.lpSockaddr);
char ipStr[INET_ADDRSTRLEN] = {};
inet_ntop(AF_INET, &sin->sin_addr, ipStr, sizeof(ipStr));
std::string ip(ipStr);
if (ip == "127.0.0.1") continue;
m_webUrls.push_back(std::format("http://{}:{}", ip, port));
}
}
}
}
float GameOverlay::Dist(const Vector3& a, const Vector3& b) {
float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
return std::sqrtf(dx*dx + dy*dy + dz*dz);
}
// Returns true if the world position is in front of the camera (depth >= 0.65).
// Uses the same depth calculation as WorldToScreen so the threshold is consistent.
// Call this before WorldToScreen to skip the full projection for behind-camera entities.
static bool IsFacingCamera(const CameraData& cam, const Vector3& pos) {
float tx = pos.x - cam.translation.x;
float ty = pos.y - cam.translation.y;
float tz = pos.z - cam.translation.z;
float depth = tx*cam.invertedViewForward.x
+ ty*cam.invertedViewForward.y
+ tz*cam.invertedViewForward.z;
return depth >= 0.65f;
}
bool GameOverlay::WorldToScreen(const CameraData& cam,
const Vector3& world,
float& sx, float& sy,
float w, float h)
{
float tx = world.x - cam.translation.x;
float ty = world.y - cam.translation.y;
float tz = world.z - cam.translation.z;
float x = tx*cam.invertedViewRight.x + ty*cam.invertedViewRight.y + tz*cam.invertedViewRight.z;
float y = tx*cam.invertedViewUp.x + ty*cam.invertedViewUp.y + tz*cam.invertedViewUp.z;
float z = tx*cam.invertedViewForward.x + ty*cam.invertedViewForward.y + tz*cam.invertedViewForward.z;
if (z < 0.65f) return false;
if (cam.projectionD1x == 0.0f || cam.projectionD2y == 0.0f) return false;
float nx = (x / cam.projectionD1x) / z;
float ny = (y / cam.projectionD2y) / z;
sx = roundf((w * 0.5f) + nx * (w * 0.5f));
sy = roundf((h * 0.5f) - ny * (h * 0.5f));
return sx >= 0.0f && sx <= w && sy >= 0.0f && sy <= h;
}
// Viewport-aware projection.
// For stretch-to-fill (or when no render res is configured) the viewport
// equals the full overlay — identical to calling WorldToScreen directly.
// For maintain-aspect-ratio with render != display, computes the inset
// viewport (letterbox top/bottom or pillarbox left/right) and offsets the
// result so entities land on the correct monitor pixels.
bool GameOverlay::Proj(const CameraData& cam, const Vector3& worldPos,
float& sx, float& sy,
float overlayW, float overlayH) const
{
float vx = 0.0f, vy = 0.0f, vw = overlayW, vh = overlayH;
if (m_renderW > 0 && m_renderH > 0 && !m_stretchToFill) {
float renderAr = static_cast<float>(m_renderW) / static_cast<float>(m_renderH);
float displayAr = overlayW / overlayH;
if (renderAr > displayAr + 0.01f) {
// Render is wider than display → letterbox (black bars top/bottom)
vw = overlayW;
vh = overlayW / renderAr;
vy = (overlayH - vh) * 0.5f;
} else if (displayAr > renderAr + 0.01f) {
// Display is wider than render → pillarbox (black bars left/right)
vh = overlayH;
vw = overlayH * renderAr;
vx = (overlayW - vw) * 0.5f;
}
}
// stretch-to-fill: vx=0, vy=0, vw=overlayW, vh=overlayH — no change
if (!WorldToScreen(cam, worldPos, sx, sy, vw, vh)) return false;
sx += vx;
sy += vy;
return sx >= 0.0f && sx <= overlayW && sy >= 0.0f && sy <= overlayH;
}
// -------------------------------------------------------------------------
// Draw — entry point called each ImGui frame
// -------------------------------------------------------------------------
void GameOverlay::SyncConfig() {
m_cfg.showPlayers = m_showPlayers;
m_cfg.showAnimals = m_showAnimals;
m_cfg.showZombies = m_showZombies;
m_cfg.showItems = m_showItems;
m_cfg.showBox = m_showBox;
m_cfg.showSkeleton = m_showSkeleton;
m_cfg.showHeadDot = m_showHeadDot;
m_cfg.showCorpses = m_showCorpses;
m_cfg.showWeapon = m_showWeapon;
m_cfg.showHealthBar = m_showHealthBar;
m_cfg.showHealthNumber = m_showHealthNumber;
m_cfg.itemCategories = m_itemCategories;
m_cfg.playerMaxDist = m_playerMaxDist;
m_cfg.animalMaxDist = m_animalMaxDist;
m_cfg.zombieMaxDist = m_zombieMaxDist;
m_cfg.itemMaxDist = m_itemMaxDist;
m_cfg.renderWidth = m_renderW;
m_cfg.renderHeight = m_renderH;
m_cfg.stretchToFill = m_stretchToFill;
}
void GameOverlay::Draw(float w, float h) {
// Refresh snapshot once per frame — shared_ptr copy is 8 bytes, no vector alloc
m_snapshot = m_service.GetLatestUpdate();
if (!m_snapshot) return;
const RuntimeUpdate& u = *m_snapshot;
// Record frame time once; used by all bone-smoothing calls this frame.
m_frameTimeMs = NowMs();
// Feed new DMA bone reads into the per-entity history so extrapolation
// always has the two most recent samples to work from.
for (const auto& p : u.players)
if (p.skeleton.valid)
UpdateBoneHistory(m_playerBoneHistory[p.address], p.skeleton, m_frameTimeMs);
for (const auto& z : u.zombies)
if (z.skeleton.valid)
UpdateBoneHistory(m_zombieBoneHistory[z.address], z.skeleton, m_frameTimeMs);
// Prune bone histories for entities no longer present in the snapshot.
// Prevents ghost extrapolation when a player logs out or leaves the scan zone:
// their history is cleared immediately so stale bones can't persist.
{
std::unordered_set<uint64_t> livePlayers, liveZombies;
livePlayers.reserve(u.players.size());
liveZombies.reserve(u.zombies.size());
for (const auto& p : u.players) livePlayers.insert(p.address);
for (const auto& z : u.zombies) liveZombies.insert(z.address);
for (auto it = m_playerBoneHistory.begin(); it != m_playerBoneHistory.end(); )
it = livePlayers.count(it->first) ? std::next(it) : m_playerBoneHistory.erase(it);
for (auto it = m_zombieBoneHistory.begin(); it != m_zombieBoneHistory.end(); )
it = liveZombies.count(it->first) ? std::next(it) : m_zombieBoneHistory.erase(it);
}
// INSERT key toggles the menu
if (GetAsyncKeyState(VK_INSERT) & 1) {
m_menuOpen = !m_menuOpen;
m_menuAlpha = m_menuOpen ? 1.0f : 0.0f;
if (!m_menuOpen) {
// Persist settings back to config on close.
SyncConfig();
m_cfg.Save(m_cfgPath);
}
}
// ---- Lumin menu (vendored under external/lumin; driven via MenuBridge) ----
if (m_menuOpen) {
static MenuBridge bridge;
bridge.showPlayers = &m_showPlayers;
bridge.showAnimals = &m_showAnimals;
bridge.showZombies = &m_showZombies;
bridge.showItems = &m_showItems;
bridge.showBox = &m_showBox;
bridge.showSkeleton = &m_showSkeleton;
bridge.showHeadDot = &m_showHeadDot;
bridge.showWeapon = &m_showWeapon;
bridge.showHealthBar = &m_showHealthBar;
bridge.showHealthNumber = &m_showHealthNumber;
bridge.showCorpses = &m_showCorpses;
bridge.debugSkeleton = &m_debugSkeleton;
bridge.playerMaxDist = &m_playerMaxDist;
bridge.animalMaxDist = &m_animalMaxDist;
bridge.zombieMaxDist = &m_zombieMaxDist;
bridge.itemMaxDist = &m_itemMaxDist;
bridge.itemCategories = &m_itemCategories;
bridge.pendingW = &m_pendingW;
bridge.pendingH = &m_pendingH;
bridge.pendingRW = &m_pendingRW;
bridge.pendingRH = &m_pendingRH;
bridge.stretchToFill = &m_stretchToFill;
// Read-only info panels.
bridge.connected = u.areBaseObjectsReady;
bridge.serverName = u.serverName.value_or("");
bridge.mapName = u.serverMapName.value_or("");
bridge.status = u.status;
if (u.localPlayerPosition.has_value()) {
bridge.hasPos = true;
bridge.px = u.localPlayerPosition->x;
bridge.py = u.localPlayerPosition->y;
bridge.pz = u.localPlayerPosition->z;
} else {
bridge.hasPos = false;
}
bridge.nPlayers = u.players.size();
bridge.nAnimals = u.animals.size();
bridge.nZombies = u.zombies.size();
bridge.nVehicles = u.carsAndBoats.size();
bridge.nItems = u.items.size();
bridge.nBullets = u.bullets.size();
bridge.webPort = m_webPort;
bridge.webUrls = m_webUrls;
bridge.onSaveConfig = [this]() {
SyncConfig();
m_cfg.Save(m_cfgPath);
};
bridge.onExit = [this]() {
SyncConfig();
m_cfg.Save(m_cfgPath);
if (m_exitCallback) m_exitCallback();
};
bridge.onApplyDisplayRes = [this]() {
m_cfg.overlayWidth = m_pendingW;
m_cfg.overlayHeight = m_pendingH;
m_cfg.Save(m_cfgPath);
if (m_resizeCallback) m_resizeCallback(m_pendingW, m_pendingH);
};
bridge.onApplyRenderRes = [this]() {
m_renderW = m_pendingRW;
m_renderH = m_pendingRH;
m_cfg.renderWidth = m_pendingRW;
m_cfg.renderHeight = m_pendingRH;
m_cfg.stretchToFill = m_stretchToFill;
m_cfg.Save(m_cfgPath);
};
g_menu = &bridge;
RenderLuminMenu();
}
// ---- ESP draw lists — camera read directly from service every frame ----
m_service.GetLatestCamera(m_liveCamera);
DrawESP(w, h, u, m_liveCamera);
}
void GameOverlay::DrawESP(float w, float h, const RuntimeUpdate& u, const CameraData& cam) {
if (!u.areBaseObjectsReady || !cam.valid) return;
ImDrawList* dl = ImGui::GetBackgroundDrawList();
if (m_showPlayers) DrawPlayers(dl, u, cam, w, h);
if (m_showAnimals) DrawAnimals(dl, u, cam, w, h);
if (m_showZombies) DrawZombies(dl, u, cam, w, h);
if (m_showItems) DrawItems (dl, u, cam, w, h);
if (m_debugSkeleton) DrawSkeletonDebug(dl, u, cam, w, h);
}
// -------------------------------------------------------------------------
// DrawSkeleton — render 11 bone segments for one entity
// -------------------------------------------------------------------------
void GameOverlay::DrawSkeleton(ImDrawList* dl, const SkeletonBones& bones,
const CameraData& cam,
float w, float h, unsigned int color,
bool isZombie) const
{
// Project each unique bone ONCE, then draw segments from cached screen coords.
// Old approach called WorldToScreen twice per segment; shared bones (neck, spine)
// were projected 3-4 times each. 16 projections replaces 28-30.
enum : int {
B_Neck=0, B_Head, B_Spine, B_Pelvis,
B_RShoulder, B_RElbow, B_RHand,
B_LShoulder, B_LElbow, B_LHand,
B_RHip, B_RKnee, B_RAnkle,
B_LHip, B_LKnee, B_LAnkle,
B_COUNT
};
const Vector3* const kWorld[B_COUNT] = {
&bones.neck, &bones.head, &bones.spine, &bones.pelvis,
&bones.rightShoulder, &bones.rightElbow, &bones.rightHand,
&bones.leftShoulder, &bones.leftElbow, &bones.leftHand,
&bones.rightHip, &bones.rightKnee, &bones.rightAnkle,
&bones.leftHip, &bones.leftKnee, &bones.leftAnkle,
};
struct SP { float x, y; bool ok; } scr[B_COUNT];
for (int i = 0; i < B_COUNT; ++i)
scr[i].ok = Proj(cam, *kWorld[i], scr[i].x, scr[i].y, w, h);
struct Seg { int8_t a, b; };
static const Seg kPlayerSegs[14] = {
{B_Neck,B_Head},
{B_Neck,B_RShoulder},{B_RShoulder,B_RElbow},{B_RElbow,B_RHand},
{B_Neck,B_LShoulder},{B_LShoulder,B_LElbow},{B_LElbow,B_LHand},
{B_Neck,B_Spine},
{B_Spine,B_RHip},{B_RHip,B_RKnee},{B_RKnee,B_RAnkle},
{B_Spine,B_LHip},{B_LHip,B_LKnee},{B_LKnee,B_LAnkle},
};
static const Seg kZombieSegs[15] = {
{B_Spine,B_Neck},{B_Neck,B_Head},
{B_Spine,B_LShoulder},{B_LShoulder,B_LElbow},{B_LElbow,B_LHand},
{B_Spine,B_RShoulder},{B_RShoulder,B_RElbow},{B_RElbow,B_RHand},
{B_Spine,B_Pelvis},
{B_Pelvis,B_RHip},{B_RHip,B_RKnee},{B_RKnee,B_RAnkle},
{B_Pelvis,B_LHip},{B_LHip,B_LKnee},{B_LKnee,B_LAnkle},
};
const Seg* segs = isZombie ? kZombieSegs : kPlayerSegs;
const int count = isZombie ? 15 : 14;
for (int i = 0; i < count; ++i) {
const SP& a = scr[segs[i].a];
const SP& b = scr[segs[i].b];
if (!a.ok || !b.ok) continue;
dl->AddLine(ImVec2(a.x, a.y), ImVec2(b.x, b.y), color, 1.5f);
}
}
// -------------------------------------------------------------------------
// DrawSkeletonDebug — named bone dots for the closest player
// -------------------------------------------------------------------------
void GameOverlay::DrawSkeletonDebug(ImDrawList* dl, const RuntimeUpdate& u,
const CameraData& cam, float w, float h) const
{
// Find the closest player that has skeleton data (valid or not).
const DayZPlayerEntry* target = nullptr;
float bestDist = 1e9f;
for (const auto& p : u.players) {
if (!p.position.has_value()) continue;
float d = Dist(cam.translation, *p.position);
if (d < 2.0f || d > m_playerMaxDist) continue;
if (d < bestDist) { bestDist = d; target = &p; }
}
if (!target) return;
// Top-left status block.
const float pad = 10.0f;
const float lineH = 16.0f;
float tx = pad, ty = pad;
auto txt = [&](ImU32 col, std::string s) {
dl->AddText(ImVec2(tx, ty), col, s.c_str());
ty += lineH;
};
const char* name = target->nickname.empty() ? "Player" : target->nickname.c_str();
txt(IM_COL32(255,255,255,220), std::format("SkelDebug: {} | dist={:.1f}m", name, bestDist));
txt(IM_COL32(200,200,200,180), std::format("skeleton.valid = {}", target->skeleton.valid ? "true" : "false"));
if (!target->skeleton.valid) {
txt(IM_COL32(255,100,100,200), "No valid skeleton data -- check offsets");
return;
}
// Named bones with distinct colors — mirrors Spectre stable player bone layout.
struct BoneDef { const char* name; const Vector3& pos; ImU32 color; };
const SkeletonBones& sk = target->skeleton;
const BoneDef bones[] = {
{ "neck", sk.neck, IM_COL32(255,100, 0,255) },
{ "head", sk.head, IM_COL32(255, 60, 60,255) },
{ "spine", sk.spine, IM_COL32(255,165, 0,255) },
{ "rShoulder", sk.rightShoulder, IM_COL32( 80,255,180,255) },
{ "rElbow", sk.rightElbow, IM_COL32( 50,220,140,255) },
{ "rHand", sk.rightHand, IM_COL32( 30,200,120,255) },
{ "lShoulder", sk.leftShoulder, IM_COL32( 80,200,255,255) },
{ "lElbow", sk.leftElbow, IM_COL32( 50,160,230,255) },
{ "lHand", sk.leftHand, IM_COL32( 30,120,255,255) },
{ "rHip", sk.rightHip, IM_COL32(255,255, 0,255) },
{ "rKnee", sk.rightKnee, IM_COL32(220,220, 0,255) },
{ "rAnkle", sk.rightAnkle, IM_COL32(180,180, 0,255) },
{ "lHip", sk.leftHip, IM_COL32(200,100,255,255) },
{ "lKnee", sk.leftKnee, IM_COL32(170, 80,230,255) },
{ "lAnkle", sk.leftAnkle, IM_COL32(140, 60,200,255) },
};
int projected = 0;
for (const auto& b : bones) {
float sx{}, sy{};
bool onScreen = Proj(cam, b.pos, sx, sy, w, h);
txt(b.color, std::format(" {} ({:.1f},{:.1f},{:.1f}) {}",
b.name, b.pos.x, b.pos.y, b.pos.z,
onScreen ? "" : "[off]"));
if (!onScreen) continue;
++projected;
dl->AddCircleFilled(ImVec2(sx, sy), 4.0f, b.color);
dl->AddCircle(ImVec2(sx, sy), 4.0f, IM_COL32(0,0,0,200), 0, 1.5f);
dl->AddText(ImVec2(sx + 7.0f, sy - 6.0f), b.color, b.name);
}
txt(IM_COL32(180,255,180,200), std::format(" {}/17 bones on screen", projected));
}
// -------------------------------------------------------------------------
// Players — red bounding box + name + held item + distance
// -------------------------------------------------------------------------
void GameOverlay::DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) {
for (const auto& p : u.players) {
if (p.isDead && !m_showCorpses) continue;
if (!p.position.has_value()) continue;
// Skip the local player by address — more reliable than proximity check.
if (u.localPlayerEntityAddress.has_value()
&& p.address == *u.localPlayerEntityAddress) continue;
const Vector3& pos = *p.position;
float dist = Dist(cam.translation, pos);
if (dist > m_playerMaxDist) continue;
if (!IsFacingCamera(cam, pos)) continue;
const ImU32 kColor = p.isAdmin ? IM_COL32(255, 200, 0, 255) // gold
: p.isDead ? IM_COL32(120, 120, 120, 160) // grey
: IM_COL32(255, 60, 60, 220); // red
// ---- Smoothed skeleton (extrapolated between DMA reads) ----
SkeletonBones smoothBones{};
bool hasSmoothBones = false;
if (!p.isDead) {
auto hit = m_playerBoneHistory.find(p.address);
if (hit != m_playerBoneHistory.end() && hit->second.initialized
&& (m_frameTimeMs - hit->second.currMs) < 500LL) {
smoothBones = GetSmoothedBones(hit->second, m_frameTimeMs);
hasSmoothBones = true;
}
}
// ---- Bounding box ----
// When the skeleton is valid, derive the box from the projected extents of
// all 16 bones. This keeps the box perfectly in sync with the skeleton
// (both come from the bone-scatter data, updated at ~60 Hz) instead of
// lagging one update behind because it uses the entity position (50 Hz).
float bx0 = 0.0f, by0 = 0.0f, bx1 = 0.0f, by1 = 0.0f;
bool boxReady = false;
if (hasSmoothBones) {
const Vector3* const kBones[] = {
&smoothBones.neck, &smoothBones.head,
&smoothBones.spine, &smoothBones.pelvis,
&smoothBones.rightShoulder, &smoothBones.rightElbow, &smoothBones.rightHand,
&smoothBones.leftShoulder, &smoothBones.leftElbow, &smoothBones.leftHand,
&smoothBones.rightHip, &smoothBones.rightKnee, &smoothBones.rightAnkle,
&smoothBones.leftHip, &smoothBones.leftKnee, &smoothBones.leftAnkle,
};
float minX = 1e9f, maxX = -1e9f;
float minY = 1e9f, maxY = -1e9f;
int hits = 0;
for (const auto* bp : kBones) {
// Reject bones whose world position is > 3 m from the entity root.
// Garbage reads (zero-initialised bones, failed scatter reads) land
// at or near world origin and would drag the box hundreds of pixels
// off to the side of the actual player.
float dbx = bp->x - pos.x, dby = bp->y - pos.y, dbz = bp->z - pos.z;
if (dbx*dbx + dby*dby + dbz*dbz > 9.0f) continue;
float bsx{}, bsy{};
if (!Proj(cam, *bp, bsx, bsy, w, h)) continue;
if (bsx < minX) minX = bsx;
if (bsx > maxX) maxX = bsx;
if (bsy < minY) minY = bsy;
if (bsy > maxY) maxY = bsy;
++hits;
}
if (hits >= 4) {
constexpr float kPad = 4.0f;
bx0 = minX - kPad;
by0 = minY - kPad;
bx1 = maxX + kPad;
by1 = maxY + kPad;
boxReady = true;
}
}
if (!boxReady) {
// Fallback: entity ground position + hardcoded 1.8 m head.
// Used for corpses (no skeleton) and when bones haven't been read yet.
float fx{}, fy{}, hx{}, hy{};
if (!Proj(cam, pos, fx, fy, w, h)) continue;
Vector3 headPos{ pos.x, pos.y + 1.8f, pos.z };
if (!Proj(cam, headPos, hx, hy, w, h)) continue;
float bH = fy - hy;
if (bH < 2.0f) continue;
float bW = bH * 0.4f;
bx0 = hx - bW * 0.5f;
by0 = hy;
bx1 = hx + bW * 0.5f;
by1 = fy;
}
// ---- Head circle (computed before the box so by0 can be adjusted) ----
float hbx{}, hby{};
float headRadius = 0.0f;
bool hasHeadCircle = false;
if (m_showHeadDot && !p.isDead && hasSmoothBones) {
if (Proj(cam, smoothBones.head, hbx, hby, w, h)) {
// Radius = 15% of the rendered body height so the circle scales
// naturally with distance. The head-neck pixel-distance approach
// collapses to minimum past ~30 m; box-height fraction stays
// proportional to the target at any range.
headRadius = std::clamp((by1 - by0) * 0.15f, 2.0f, 18.0f);
hasHeadCircle = true;
by0 = std::min(by0, hby - headRadius - 2.0f);
}
}
if (by1 - by0 < 2.0f) continue;
if (m_showBox)
dl->AddRect(ImVec2(bx0, by0), ImVec2(bx1, by1), kColor, 0.0f, 0, 1.5f);
// ---- Skeleton + head circle (use extrapolated bones) ----
if (!p.isDead && hasSmoothBones) {
if (m_showSkeleton)
DrawSkeleton(dl, smoothBones, cam, w, h, kColor, false);
if (hasHeadCircle) {
dl->AddCircleFilled(ImVec2(hbx, hby), headRadius, IM_COL32(0, 0, 0, 80));
dl->AddCircle(ImVec2(hbx, hby), headRadius, kColor, 0, 1.5f);
}
}
// ---- Name + distance label (top-right of box) ----
char lblBuf[256];
const char* nick = p.nickname.empty() ? "Player" : p.nickname.c_str();
if (p.isAdmin && p.isDead)
snprintf(lblBuf, sizeof(lblBuf), "[ADMIN] %s [DEAD] [%.0fm]", nick, dist);
else if (p.isAdmin)
snprintf(lblBuf, sizeof(lblBuf), "[ADMIN] %s [%.0fm]", nick, dist);
else if (p.isDead)
snprintf(lblBuf, sizeof(lblBuf), "%s [DEAD] [%.0fm]", nick, dist);
else
snprintf(lblBuf, sizeof(lblBuf), "%s [%.0fm]", nick, dist);
dl->AddText(ImVec2(bx1 + 3.0f, by0), kColor, lblBuf);
// ---- Right-side extras (weapon, health bar, health number) ----
const float kBarW = 4.0f;
const float kBarGap = 5.0f;
const float barX = bx1 + kBarGap;
const float boxH = by1 - by0;
if (m_showWeapon && !p.isDead && !p.itemInHands.empty()) {
constexpr ImU32 kWeaponColor = IM_COL32(220, 220, 100, 200);
dl->AddText(ImVec2(barX, by0 + 13.0f), kWeaponColor, p.itemInHands.c_str());
}
const bool hasHealth = (p.health >= 0.0f);
if ((m_showHealthBar || m_showHealthNumber) && !p.isDead && hasHealth) {
const float hpFrac = std::min(p.health / 100.0f, 1.0f);
if (m_showHealthBar) {
dl->AddRectFilled(
ImVec2(barX, by0),
ImVec2(barX + kBarW, by1),
IM_COL32(0, 0, 0, 140));
float r = hpFrac < 0.5f ? 1.0f : 2.0f * (1.0f - hpFrac);
float g = hpFrac > 0.5f ? 1.0f : 2.0f * hpFrac;
ImU32 hpColor = IM_COL32(
static_cast<int>(r * 220),
static_cast<int>(g * 220),
0, 220);
float filledTop = by0 + boxH * (1.0f - hpFrac);
dl->AddRectFilled(
ImVec2(barX, filledTop),
ImVec2(barX + kBarW, by1),
hpColor);
}
if (m_showHealthNumber) {
char hpBuf[32];
snprintf(hpBuf, sizeof(hpBuf), "%.0f/100", p.health);
float numX = m_showHealthBar ? barX + kBarW + 2.0f : barX;
dl->AddText(ImVec2(numX, by1 - 12.0f),
IM_COL32(200, 200, 200, 200), hpBuf);
}
}
}
}
// -------------------------------------------------------------------------
// Animals — green label
// -------------------------------------------------------------------------
void GameOverlay::DrawAnimals(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) {
constexpr ImU32 kGreen = IM_COL32(50, 230, 50, 200);
for (const auto& a : u.animals) {
if (!a.position.has_value()) continue;
float dist = Dist(cam.translation, *a.position);
if (dist > m_animalMaxDist) continue;
if (!IsFacingCamera(cam, *a.position)) continue;
float sx{}, sy{};
if (!Proj(cam, *a.position, sx, sy, w, h)) continue;
std::string raw = a.entityName.empty() ? a.typeName : a.entityName;
std::string fmtName = raw.empty() ? "Animal" : FormatEntityName(raw);
char abuf[256];
snprintf(abuf, sizeof(abuf), "%s [%.0fm]", fmtName.c_str(), dist);
dl->AddText(ImVec2(sx, sy), kGreen, abuf);
}
}
// -------------------------------------------------------------------------
// Zombies — yellow label
// -------------------------------------------------------------------------
void GameOverlay::DrawZombies(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) {
constexpr ImU32 kYellow = IM_COL32(230, 230, 50, 180);
for (const auto& z : u.zombies) {
// Resolve position: use fresh read if available, fall back to cache.
Vector3 pos;
if (z.position.has_value()) {
pos = *z.position;
m_zombieLastPos[z.address] = pos;
} else {
auto it = m_zombieLastPos.find(z.address);
if (it == m_zombieLastPos.end()) continue;
pos = it->second;
}
float dist = Dist(cam.translation, pos);
if (dist > m_zombieMaxDist) continue;
if (!IsFacingCamera(cam, pos)) continue;
float sx{}, sy{};
if (!Proj(cam, pos, sx, sy, w, h)) continue;
auto zhit = m_zombieBoneHistory.find(z.address);
if (zhit != m_zombieBoneHistory.end() && zhit->second.initialized
&& (m_frameTimeMs - zhit->second.currMs) < 500LL) {
SkeletonBones zsm = GetSmoothedBones(zhit->second, m_frameTimeMs);
if (m_showSkeleton)
DrawSkeleton(dl, zsm, cam, w, h, kYellow, true);
if (m_showHeadDot) {
float hbx{}, hby{};
if (Proj(cam, zsm.head, hbx, hby, w, h)) {
// Angular size of a ~25 cm head at the zombie's actual distance.
// Scales from ~18px at 5m down to ~3px at 50m; clamp keeps it
// always visible and never massive.
const float projD2y = std::max(cam.projectionD2y, 0.1f);
float zr = std::clamp((0.25f * h * 0.5f) / (dist * projD2y), 2.0f, 18.0f);
dl->AddCircleFilled(ImVec2(hbx, hby), zr, IM_COL32(0, 0, 0, 80));
dl->AddCircle(ImVec2(hbx, hby), zr, kYellow, 0, 1.5f);
}
}
}
char zbuf[32];
snprintf(zbuf, sizeof(zbuf), "Z [%.0fm]", dist);
dl->AddText(ImVec2(sx, sy), kYellow, zbuf);
}
}
// -------------------------------------------------------------------------
// Items — white label
// -------------------------------------------------------------------------
void GameOverlay::DrawItems(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h) {
constexpr ImU32 kWhite = IM_COL32(255, 255, 255, 200);
const float maxDistSq = m_itemMaxDist * m_itemMaxDist;
for (const auto& item : u.items) {
// Per-category filter (missing key = enabled)
if (!item.filterKey.empty()) {
auto it = m_itemCategories.find(item.filterKey);
if (it != m_itemCategories.end() && !it->second) continue;
}
if (!item.position.has_value()) continue;
// Squared-distance cull avoids sqrtf for every item.
float dx = item.position->x - cam.translation.x;
float dy = item.position->y - cam.translation.y;
float dz = item.position->z - cam.translation.z;
float distSq = dx*dx + dy*dy + dz*dz;
if (distSq > maxDistSq) continue;
if (!IsFacingCamera(cam, *item.position)) continue;
float sx{}, sy{};
if (!Proj(cam, *item.position, sx, sy, w, h)) continue;
const std::string& name = item.cleanName.empty()
? item.entityName
: item.cleanName;
if (name.empty()) continue;
float dist = std::sqrtf(distSq); // sqrt only for items that will actually be drawn
// Two-tier font: larger close up, smaller at range.
char ibuf[256];
snprintf(ibuf, sizeof(ibuf), "%s [%.0fm]", name.c_str(), dist);
ImFont* font = (dist < 50.0f) ? m_fontLootClose : m_fontLootFar;
if (font) ImGui::PushFont(font);
dl->AddText(ImVec2(sx, sy), kWhite, ibuf);
if (font) ImGui::PopFont();
}
}
+190
View File
@@ -0,0 +1,190 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <imgui.h>
#include "Config.h"
#include "Runtime/DayZRuntimeService.h"
struct ImDrawList;
class GameOverlay {
public:
// Exposed so static helpers in GameOverlay.cpp can take it by reference.
struct BoneHistory {
SkeletonBones prev{};
SkeletonBones curr{};
int64_t prevMs = 0;
int64_t currMs = 0;
bool initialized = false;
};
explicit GameOverlay(DayZRuntimeService& service,
OverlayConfig cfg,
std::string cfgPath)
: m_service(service)
, m_cfg(std::move(cfg))
, m_cfgPath(std::move(cfgPath))
{
m_showPlayers = m_cfg.showPlayers;
m_showAnimals = m_cfg.showAnimals;
m_showZombies = m_cfg.showZombies;
m_showItems = m_cfg.showItems;
m_showBox = m_cfg.showBox;
m_showSkeleton = m_cfg.showSkeleton;
m_showHeadDot = m_cfg.showHeadDot;
m_showCorpses = m_cfg.showCorpses;
m_showWeapon = m_cfg.showWeapon;
m_showHealthBar = m_cfg.showHealthBar;
m_showHealthNumber = m_cfg.showHealthNumber;
m_itemCategories = m_cfg.itemCategories;
m_playerMaxDist = m_cfg.playerMaxDist;
m_animalMaxDist = m_cfg.animalMaxDist;
m_zombieMaxDist = m_cfg.zombieMaxDist;
m_itemMaxDist = m_cfg.itemMaxDist;
m_pendingW = m_cfg.overlayWidth;
m_pendingH = m_cfg.overlayHeight;
m_renderW = m_cfg.renderWidth;
m_renderH = m_cfg.renderHeight;
m_stretchToFill = m_cfg.stretchToFill;
m_pendingRW = m_cfg.renderWidth;
m_pendingRH = m_cfg.renderHeight;
}
/// Called once after the web radar server has started so the Radar tab
/// knows what URL(s) to display.
void SetWebRadarPort(int port);
/// Called once after ImGui fonts are built so DrawItems can tier by distance.
void SetLootFonts(ImFont* close, ImFont* lootFar) {
m_fontLootClose = close;
m_fontLootFar = lootFar;
}
/// Register a callback invoked when the user clicks the exit button.
/// The caller should set its stop flag inside the callback.
void SetExitCallback(std::function<void()> cb) { m_exitCallback = std::move(cb); }
/// Register a callback invoked when the user applies a new overlay resolution.
void SetResizeCallback(std::function<void(int,int)> cb) { m_resizeCallback = std::move(cb); }
// Called each frame. w/h a re the overlay window pixel dimensions.
void Draw(float w, float h);
private:
DayZRuntimeService& m_service;
OverlayConfig m_cfg;
std::string m_cfgPath;
// Cached entity snapshot (updated once per frame via GetLatestUpdate).
std::shared_ptr<const RuntimeUpdate> m_snapshot;
public:
bool IsMenuOpen() const { return m_menuOpen; }
private:
// Live camera — updated every frame directly from the service,
// bypassing the entity snapshot so it reflects the latest DMA read.
CameraData m_liveCamera;
// Web radar
int m_webPort = 7777;
std::vector<std::string> m_webUrls; // populated by SetWebRadarPort()
// Exit callback — invoked when the user presses the exit button.
std::function<void()> m_exitCallback;
// Resize callback — invoked when the user applies a new resolution.
std::function<void(int, int)> m_resizeCallback;
// Menu state
bool m_menuOpen = false;
float m_menuAlpha = 0.0f;
int m_tab = 0;
int m_subtab = 0;
// ESP toggles (initialised from config in constructor)
bool m_showPlayers = true;
bool m_showAnimals = true;
bool m_showZombies = true;
bool m_showItems = true;
bool m_showBox = true;
bool m_showSkeleton = true;
bool m_showHeadDot = false;
bool m_showCorpses = false;
bool m_showWeapon = true;
bool m_showHealthBar = true;
bool m_showHealthNumber = false;
bool m_debugSkeleton = false; // draws named bone dots for the closest player
// Per-category item enabled map (key = filterKey, missing = enabled)
std::map<std::string, bool> m_itemCategories;
// Loot fonts — set once via SetLootFonts() after ImGui atlas is built.
ImFont* m_fontLootClose = nullptr; // 16 px — used for items within 50 m
ImFont* m_fontLootFar = nullptr; // 11 px — used for items >= 50 m
// Last-known positions for zombies keyed by entity address.
// Used to avoid a single failed position read causing a visible blink.
std::unordered_map<uint64_t, Vector3> m_zombieLastPos;
// ---- Bone interpolation / extrapolation ----
std::unordered_map<uint64_t, BoneHistory> m_playerBoneHistory;
std::unordered_map<uint64_t, BoneHistory> m_zombieBoneHistory;
int64_t m_frameTimeMs = 0; // set once per Draw() call
static int64_t NowMs();
// Pending resolution override — edited in menu, applied on button press.
int m_pendingW = 0;
int m_pendingH = 0;
int m_pendingRW = 0; // render width (stretched res)
int m_pendingRH = 0; // render height (stretched res)
// Stretched resolution state (active values, applied on "Apply")
int m_renderW = 0;
int m_renderH = 0;
bool m_stretchToFill = true;
// ESP distance limits (initialised from config in constructor)
float m_playerMaxDist = 1000.0f;
float m_animalMaxDist = 1000.0f;
float m_zombieMaxDist = 500.0f;
float m_itemMaxDist = 200.0f;
// Low-level NDC projection helper — maps a world position to overlay pixels.
// Returns false if the position is behind the camera or off-screen.
static bool WorldToScreen(const CameraData& cam,
const Vector3& worldPos,
float& sx, float& sy,
float w, float h);
// Viewport-aware projection wrapper. Use this instead of WorldToScreen
// directly — it applies stretched-resolution / letterbox offsets.
bool Proj(const CameraData& cam, const Vector3& worldPos,
float& sx, float& sy, float overlayW, float overlayH) const;
static float Dist(const Vector3& a, const Vector3& b);
// Copy live menu state (m_show*, distances, item categories, resolution)
// back into m_cfg prior to a Save.
void SyncConfig();
void DrawESP (float w, float h, const RuntimeUpdate& u, const CameraData& cam);
void DrawPlayers(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
void DrawAnimals(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
void DrawZombies(ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
void DrawItems (ImDrawList* dl, const RuntimeUpdate& u, const CameraData& cam, float w, float h);
// Draw skeleton bone segments for a single entity. color = ImU32 (unsigned int).
void DrawSkeleton(ImDrawList* dl, const SkeletonBones& bones,
const CameraData& cam, float w, float h,
unsigned int color, bool isZombie) const;
// Debug: draw every named bone as a labeled dot for the closest player.
void DrawSkeletonDebug(ImDrawList* dl, const RuntimeUpdate& u,
const CameraData& cam, float w, float h) const;
};
+77
View File
@@ -0,0 +1,77 @@
#pragma once
// -------------------------------------------------------------------------
// MenuBridge — the seam between the vendored Lumin ImGui menu (external/lumin)
// and this project's overlay state (GameOverlay).
//
// GameOverlay fills one of these every frame (pointers into its own members
// for the editable values, plain values for the read-only info panels) and
// the menu's render() reads/writes through the global pointer below. This
// keeps all the actual ESP state owned by GameOverlay while letting Lumin's
// widgets drive it.
// -------------------------------------------------------------------------
#include <cstddef>
#include <functional>
#include <map>
#include <string>
#include <vector>
struct MenuBridge {
// ---- ESP visibility toggles (point at GameOverlay members) ----
bool* showPlayers = nullptr;
bool* showAnimals = nullptr;
bool* showZombies = nullptr;
bool* showItems = nullptr;
bool* showBox = nullptr;
bool* showSkeleton = nullptr;
bool* showHeadDot = nullptr;
bool* showWeapon = nullptr;
bool* showHealthBar = nullptr;
bool* showHealthNumber = nullptr;
bool* showCorpses = nullptr;
bool* debugSkeleton = nullptr;
// ---- ESP draw-distance limits ----
float* playerMaxDist = nullptr;
float* animalMaxDist = nullptr;
float* zombieMaxDist = nullptr;
float* itemMaxDist = nullptr;
// ---- Per-category loot toggles (key = filterKey) ----
std::map<std::string, bool>* itemCategories = nullptr;
// ---- Read-only info (refreshed each frame) ----
bool connected = false;
std::string serverName;
std::string mapName;
std::string status;
bool hasPos = false;
float px = 0.f, py = 0.f, pz = 0.f;
std::size_t nPlayers = 0, nAnimals = 0, nZombies = 0,
nVehicles = 0, nItems = 0, nBullets = 0;
// ---- Web radar ----
int webPort = 7777;
std::vector<std::string> webUrls;
// ---- Resolution settings (point at GameOverlay members) ----
int* pendingW = nullptr;
int* pendingH = nullptr;
int* pendingRW = nullptr;
int* pendingRH = nullptr;
bool* stretchToFill = nullptr;
// ---- Actions ----
std::function<void()> onApplyDisplayRes; // applies pendingW/H + resize
std::function<void()> onApplyRenderRes; // applies pendingRW/RH + stretch
std::function<void()> onSaveConfig;
std::function<void()> onExit;
};
// Set by GameOverlay before each gui->render() call; read by the menu.
extern MenuBridge* g_menu;
// Thin shim implemented in external/lumin/framework/gui.cpp. Lets GameOverlay
// drive the Lumin menu without pulling the whole framework header (and its
// `using namespace ImGui`) into GameOverlay's translation unit. Must be called
// between ImGui::NewFrame() and ImGui::Render().
void RenderLuminMenu();
+296
View File
@@ -0,0 +1,296 @@
#include "OverlayWindow.h"
#include <imgui.h>
#include <imgui_impl_win32.h>
#include <imgui_impl_dx11.h>
// Vendored Lumin framework — provides the FreeType-backed font system (`font`),
// shared state (`var`) and the embedded font data (inter_*, icon_font).
#include "framework/headers/includes.h"
#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")
#include <spdlog/spdlog.h>
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
static constexpr wchar_t kWndClass[] = L"DayZEspWindow";
// ESP fonts go through the Lumin font system so they survive its atlas
// rebuilds. dpi is locked to 1.0 (variables.h) so these map to exact pixels.
static const char* kEspFontPath = "C:\\Windows\\Fonts\\tahoma.ttf";
// Register every font size the Lumin menu can request, so the atlas is built
// up front and (ideally) never rebuilt mid-run. Any size missed here is still
// handled gracefully by the per-frame rebuild check in Run().
static void WarmMenuFonts() {
font->get(inter_medium, 10);
font->get(inter_medium, 11);
font->get(inter_semibold, 8);
font->get(inter_semibold, 10);
font->get(inter_semibold, 11);
font->get(inter_semibold, 12);
font->get(inter_semibold, 13);
font->get(inter_semibold, 16);
font->get(inter_semibold, 18);
font->get(icon_font, 13);
font->get(icon_font, 14);
font->get(icon_font, 15);
font->get_file(flaticon_uicons_regular_rounded_path, 12.0f, true);
font->get_file(flaticon_uicons_regular_rounded_path, 12.5f, true);
font->get_file("C:\\Windows\\Fonts\\segmdl2.ttf", 11.f, true);
}
// -------------------------------------------------------------------------
// WndProc
// -------------------------------------------------------------------------
LRESULT WINAPI OverlayWindow::WndProc(HWND h, UINT msg, WPARAM w, LPARAM l) {
if (ImGui_ImplWin32_WndProcHandler(h, msg, w, l)) return true;
switch (msg) {
case WM_SIZE:
if (w != SIZE_MINIMIZED) {
// Store pending resize; handled at top of render loop
if (auto* self = reinterpret_cast<OverlayWindow*>(GetWindowLongPtrW(h, GWLP_USERDATA))) {
self->m_resizeW = LOWORD(l);
self->m_resizeH = HIWORD(l);
}
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_SYSCOMMAND:
if ((w & 0xFFF0) == SC_KEYMENU) return 0;
break;
}
return DefWindowProcW(h, msg, w, l);
}
// -------------------------------------------------------------------------
// CreateWin32Window
// -------------------------------------------------------------------------
bool OverlayWindow::CreateWin32Window() {
m_w = (m_overrideW > 0) ? m_overrideW : GetSystemMetrics(SM_CXSCREEN);
m_h = (m_overrideH > 0) ? m_overrideH : GetSystemMetrics(SM_CYSCREEN);
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.style = CS_CLASSDC;
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandle(nullptr);
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.lpszClassName = kWndClass;
if (!RegisterClassExW(&wc)) return false;
m_hwnd = CreateWindowExW(
WS_EX_APPWINDOW,
kWndClass, L"KarachiHook",
WS_POPUP | WS_VISIBLE,
0, 0, m_w, m_h,
nullptr, nullptr, wc.hInstance, nullptr
);
if (!m_hwnd) return false;
// Store 'this' so WndProc can find us for resize events
SetWindowLongPtrW(m_hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
ShowWindow(m_hwnd, SW_SHOWDEFAULT);
UpdateWindow(m_hwnd);
return true;
}
// -------------------------------------------------------------------------
// ResizeTo
// -------------------------------------------------------------------------
void OverlayWindow::ResizeTo(int w, int h) {
if (!m_hwnd || w <= 0 || h <= 0) return;
SetWindowPos(m_hwnd, nullptr, 0, 0, w, h,
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
// WM_SIZE will be posted; the render loop picks it up via m_resizeW/m_resizeH.
}
// -------------------------------------------------------------------------
// CreateDX11
// -------------------------------------------------------------------------
bool OverlayWindow::CreateDX11() {
DXGI_SWAP_CHAIN_DESC sd{};
sd.BufferCount = 2;
sd.BufferDesc.Width = 0;
sd.BufferDesc.Height = 0;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = m_hwnd;
sd.SampleDesc.Count = 1;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
const D3D_FEATURE_LEVEL fla[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0 };
D3D_FEATURE_LEVEL fl{};
HRESULT hr = D3D11CreateDeviceAndSwapChain(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0,
fla, 2, D3D11_SDK_VERSION,
&sd, &m_sc, &m_dev, &fl, &m_ctx
);
if (FAILED(hr))
hr = D3D11CreateDeviceAndSwapChain(
nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0,
fla, 2, D3D11_SDK_VERSION,
&sd, &m_sc, &m_dev, &fl, &m_ctx);
return SUCCEEDED(hr);
}
// -------------------------------------------------------------------------
// RTV helpers
// -------------------------------------------------------------------------
void OverlayWindow::CreateRTV() {
ID3D11Texture2D* back = nullptr;
if (SUCCEEDED(m_sc->GetBuffer(0, IID_PPV_ARGS(&back)))) {
m_dev->CreateRenderTargetView(back, nullptr, &m_rtv);
back->Release();
}
}
void OverlayWindow::CleanupRTV() {
if (m_rtv) { m_rtv->Release(); m_rtv = nullptr; }
}
// -------------------------------------------------------------------------
// Destroy
// -------------------------------------------------------------------------
void OverlayWindow::Destroy() {
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
if (ImGui::GetCurrentContext()) ImGui::DestroyContext();
CleanupRTV();
if (m_sc) { m_sc->Release(); m_sc = nullptr; }
if (m_ctx) { m_ctx->Release(); m_ctx = nullptr; }
if (m_dev) { m_dev->Release(); m_dev = nullptr; }
if (m_hwnd){ DestroyWindow(m_hwnd); m_hwnd = nullptr; }
UnregisterClassW(kWndClass, GetModuleHandle(nullptr));
}
// -------------------------------------------------------------------------
// Run
// -------------------------------------------------------------------------
void OverlayWindow::Run(std::atomic<bool>& stopFlag) {
if (!CreateWin32Window() || !CreateDX11()) {
spdlog::error("OverlayWindow: init failed.");
Destroy();
stopFlag.store(true);
return;
}
CreateRTV();
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
ImGui::StyleColorsDark();
// Make window backgrounds semi-transparent so the game is partially visible
ImGui::GetStyle().Colors[ImGuiCol_WindowBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f);
ImGui_ImplWin32_Init(m_hwnd);
ImGui_ImplDX11_Init(m_dev, m_ctx);
// ---- Fonts via the Lumin FreeType atlas (must run after backend init) ----
WarmMenuFonts();
font->get_file(kEspFontPath, 14.f); // ESP base -> io.FontDefault
font->get_file(kEspFontPath, 16.f); // loot close (<50 m)
font->get_file(kEspFontPath, 11.f); // loot far (>=50 m)
var->gui.dpi_changed = true;
font->update(); // build the atlas once
if (ImFont* base = font->get_file(kEspFontPath, 14.f)) io.FontDefault = base;
ImFont* fontLootClose = font->get_file(kEspFontPath, 16.f);
ImFont* fontLootFar = font->get_file(kEspFontPath, 11.f);
// Notify caller that fonts are ready — must be done after the atlas build
// so the ImFont pointers are fully populated.
if (m_onFontReady) m_onFontReady(fontLootClose, fontLootFar);
spdlog::info("ESP window running ({}x{}).", m_w, m_h);
MSG msg{};
bool done = false;
while (!stopFlag.load() && !done) {
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
if (msg.message == WM_QUIT) { done = true; break; }
}
if (done) break;
// Handle deferred resize
if (m_resizeW != 0 && m_resizeH != 0) {
CleanupRTV();
m_sc->ResizeBuffers(0, m_resizeW, m_resizeH, DXGI_FORMAT_UNKNOWN, 0);
m_w = static_cast<int>(m_resizeW);
m_h = static_cast<int>(m_resizeH);
m_resizeW = m_resizeH = 0;
CreateRTV();
}
// The Lumin menu registers font sizes lazily; rebuild the atlas when a
// new one appears (must happen outside a frame) and refresh the cached
// ESP font pointers so they never dangle across a rebuild.
if (var->gui.dpi_changed) {
font->update();
if (ImFont* base = font->get_file(kEspFontPath, 14.f)) io.FontDefault = base;
if (m_onFontReady)
m_onFontReady(font->get_file(kEspFontPath, 16.f),
font->get_file(kEspFontPath, 11.f));
}
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
if (m_draw) m_draw(static_cast<float>(m_w), static_cast<float>(m_h));
// Toggle interactivity: remove WS_EX_NOACTIVATE when the menu is open
// so ImGui can receive mouse/keyboard input, restore it when closed so
// the overlay doesn't steal focus during gameplay.
if (m_queryInput) {
bool wantInput = m_queryInput();
if (wantInput != m_prevWantInput) {
LONG_PTR ex = GetWindowLongPtrW(m_hwnd, GWL_EXSTYLE);
if (wantInput) {
ex &= ~static_cast<LONG_PTR>(WS_EX_NOACTIVATE);
SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, ex);
SetForegroundWindow(m_hwnd);
} else {
ex |= WS_EX_NOACTIVATE;
SetWindowLongPtrW(m_hwnd, GWL_EXSTYLE, ex);
}
m_prevWantInput = wantInput;
}
}
ImGui::Render();
// Pure black background
const float clear[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
m_ctx->OMSetRenderTargets(1, &m_rtv, nullptr);
m_ctx->ClearRenderTargetView(m_rtv, clear);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
DwmFlush(); // sync to DWM compositor vblank before present
m_sc->Present(1, 0); // 1 = vsync interval
}
Destroy();
stopFlag.store(true);
}
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <atomic>
#include <functional>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <d3d11.h>
#include <imgui.h>
// Standard ImGui + DX11 window, always-on-top, fullscreen popup.
// The game is visible through the mostly-dark background while ImGui
// draw-list calls render the ESP data on top.
class OverlayWindow {
public:
using DrawFn = std::function<void(float w, float h)>;
using InputQueryFn = std::function<bool()>;
using FontReadyFn = std::function<void(ImFont* lootClose, ImFont* lootFar)>;
OverlayWindow() = default;
~OverlayWindow() = default;
OverlayWindow(const OverlayWindow&) = delete;
OverlayWindow& operator=(const OverlayWindow&) = delete;
void SetDrawCallback(DrawFn fn) { m_draw = std::move(fn); }
void SetInputQueryCallback(InputQueryFn fn) { m_queryInput = std::move(fn); }
// Called once after ImGui fonts are built, before the first frame.
void SetFontReadyCallback(FontReadyFn fn) { m_onFontReady = std::move(fn); }
// Override the window size used at creation (0 = use GetSystemMetrics).
void SetResolutionOverride(int w, int h) { m_overrideW = w; m_overrideH = h; }
// Resize the live window; triggers WM_SIZE which the render loop handles.
void ResizeTo(int w, int h);
// Blocks until stopFlag becomes true or the window is closed.
void Run(std::atomic<bool>& stopFlag);
// Called from WndProc for WM_SIZE
UINT m_resizeW = 0;
UINT m_resizeH = 0;
// Resolution override — set before Run(); 0 = auto from screen metrics.
int m_overrideW = 0;
int m_overrideH = 0;
private:
DrawFn m_draw;
InputQueryFn m_queryInput;
FontReadyFn m_onFontReady;
bool m_prevWantInput = false;
HWND m_hwnd = nullptr;
ID3D11Device* m_dev = nullptr;
ID3D11DeviceContext* m_ctx = nullptr;
IDXGISwapChain* m_sc = nullptr;
ID3D11RenderTargetView* m_rtv = nullptr;
int m_w = 0;
int m_h = 0;
bool CreateWin32Window();
bool CreateDX11();
void CreateRTV();
void CleanupRTV();
void Destroy();
static LRESULT WINAPI WndProc(HWND h, UINT msg, WPARAM w, LPARAM l);
};
+261
View File
@@ -0,0 +1,261 @@
#include "Readers/BulletTableReader.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <numbers>
#include <unordered_set>
#include "Offsets.h"
#include "RuntimeOffsets.h"
#include "Memory/MemoryValidation.h"
// -------------------------------------------------------------------------
// TryRead
// -------------------------------------------------------------------------
bool BulletTableReader::TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZBulletEntry>& entries)
{
entries.clear();
const uint32_t pid = session.processId;
// 1. Bullet count — read first; the table pointer is null when count==0
int32_t rawCount = 0;
if (!mem.TryReadValue<int32_t>(pid,
session.worldAddress + RuntimeOffsets::World::BulletCount,
rawCount))
{
return false;
}
const int count = std::clamp(rawCount, 0, kMaxBullets);
// 2. Nothing alive — table pointer may be null, safe to return early
if (count == 0) {
m_tableUnavailable = false;
m_lastPositions.clear();
return true;
}
// 3. Bullet table pointer (only valid when count > 0)
uint64_t bulletTablePtr = 0;
if (!mem.TryReadPointer(pid,
session.worldAddress + RuntimeOffsets::World::BulletTable,
bulletTablePtr))
{
m_tableUnavailable = true;
return false;
}
m_tableUnavailable = false;
// 4. Bulk-read the pointer table (count * 8 bytes)
const size_t tableBytes = static_cast<size_t>(count) * 8;
std::vector<uint8_t> buf;
buf.reserve(tableBytes);
if (!mem.ReadBytes(pid, bulletTablePtr, tableBytes, buf)) {
return false;
}
// 5. Iterate pointers, build entries for valid distinct addresses
std::unordered_set<uint64_t> seen;
seen.reserve(static_cast<size_t>(count));
for (int i = 0; i < count; ++i) {
uint64_t addr = 0;
std::memcpy(&addr, buf.data() + static_cast<size_t>(i) * 8, sizeof(uint64_t));
if (!MemoryValidation::IsValidUserAddress(addr)) continue;
if (!seen.insert(addr).second) continue; // skip duplicates
entries.push_back(BuildBulletEntry(mem, session, addr));
}
// 6. Sort by address for deterministic ordering
std::sort(entries.begin(), entries.end(),
[](const DayZBulletEntry& a, const DayZBulletEntry& b) {
return a.address < b.address;
});
// 7. Sync last-position cache
UpdateLastPositions(entries);
return true;
}
// -------------------------------------------------------------------------
// Reset
// -------------------------------------------------------------------------
void BulletTableReader::Reset()
{
m_lastPositions.clear();
m_tableUnavailable = false;
}
// -------------------------------------------------------------------------
// BuildBulletEntry
// -------------------------------------------------------------------------
DayZBulletEntry BulletTableReader::BuildBulletEntry(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t address)
{
DayZBulletEntry entry;
entry.address = address;
const uint32_t pid = session.processId;
// 1. VisualState pointer
uint64_t visualStateAddr = 0;
if (!mem.TryReadPointer(pid, address + Offsets::Common::VisualState, visualStateAddr)) {
return entry;
}
// 2. Position
Vector3 pos{};
if (TryReadBulletPosition(mem, pid, visualStateAddr, pos)) {
entry.position = pos;
}
// 3. Direction from stored visual vectors
Vector3 dir{};
float heading = 0.0f;
if (TryReadBulletDirection(mem, pid, visualStateAddr, dir, heading)) {
entry.direction = dir;
entry.headingDegrees = heading;
entry.directionSource = "visual";
return entry;
}
// 4. Direction fallback: flight delta from previous position
if (entry.position.has_value()) {
auto prevIt = m_lastPositions.find(address);
if (prevIt != m_lastPositions.end()) {
Vector3 flightDir{};
float flightHeading = 0.0f;
if (TryGetFlightDirection(prevIt->second, *entry.position,
flightDir, flightHeading))
{
entry.direction = flightDir;
entry.headingDegrees = flightHeading;
entry.directionSource = "raw";
}
}
}
return entry;
}
// -------------------------------------------------------------------------
// TryReadBulletPosition
// -------------------------------------------------------------------------
bool BulletTableReader::TryReadBulletPosition(VmmAccessor& mem,
uint32_t pid,
uint64_t visualStateAddr,
Vector3& pos)
{
if (!mem.TryReadValue<Vector3>(pid,
visualStateAddr + Offsets::Common::Position,
pos))
{
return false;
}
return MemoryValidation::IsValidVector(pos);
}
// -------------------------------------------------------------------------
// TryReadBulletDirection
// -------------------------------------------------------------------------
bool BulletTableReader::TryReadBulletDirection(VmmAccessor& mem,
uint32_t pid,
uint64_t visualStateAddr,
Vector3& dir,
float& heading)
{
// DirectionX and DirectionY are stored as float[3] each (x,y,z components
// of the engine's view-matrix row). We use X-component from each row to
// reconstruct a 2-D horizontal direction that matches the C# original.
float dirX = 0.0f;
float dirY = 0.0f;
if (!mem.TryReadValue<float>(pid, visualStateAddr + Offsets::Common::DirectionX, dirX))
return false;
if (!mem.TryReadValue<float>(pid, visualStateAddr + Offsets::Common::DirectionY, dirY))
return false;
// Construct a normalized 3-D direction in the horizontal plane
const float len = std::sqrt(dirX * dirX + dirY * dirY);
if (len < kMinDirectionDelta) return false;
dir.x = dirX / len;
dir.y = 0.0f;
dir.z = dirY / len;
constexpr float kRadToDeg = 180.0f / static_cast<float>(std::numbers::pi);
heading = MemoryValidation::NormalizeHeading(
std::atan2(dirY, dirX) * kRadToDeg + 90.0f);
return true;
}
// -------------------------------------------------------------------------
// TryGetFlightDirection
// -------------------------------------------------------------------------
bool BulletTableReader::TryGetFlightDirection(const Vector3& prev,
const Vector3& curr,
Vector3& dir,
float& heading)
{
const float dx = curr.x - prev.x;
const float dy = curr.y - prev.y;
const float dz = curr.z - prev.z;
const float lenSq = dx * dx + dy * dy + dz * dz;
if (lenSq < kMinDirectionDelta * kMinDirectionDelta) return false;
// Horizontal components must also be meaningful
const float hDelta = std::sqrt(dx * dx + dz * dz);
if (hDelta < kMinDirectionDelta) return false;
const float len = std::sqrt(lenSq);
dir.x = dx / len;
dir.y = dy / len;
dir.z = dz / len;
constexpr float kRadToDeg = 180.0f / static_cast<float>(std::numbers::pi);
heading = MemoryValidation::NormalizeHeading(
std::atan2(dz, dx) * kRadToDeg + 90.0f);
return true;
}
// -------------------------------------------------------------------------
// UpdateLastPositions
// -------------------------------------------------------------------------
void BulletTableReader::UpdateLastPositions(const std::vector<DayZBulletEntry>& entries)
{
// Collect the set of currently-alive bullet addresses
std::unordered_set<uint64_t> alive;
alive.reserve(entries.size());
for (const auto& e : entries) {
alive.insert(e.address);
if (e.position.has_value()) {
m_lastPositions[e.address] = *e.position;
}
}
// Remove entries that are no longer alive
for (auto it = m_lastPositions.begin(); it != m_lastPositions.end(); ) {
if (alive.find(it->first) == alive.end()) {
it = m_lastPositions.erase(it);
} else {
++it;
}
}
}
+73
View File
@@ -0,0 +1,73 @@
#pragma once
#include <vector>
#include <unordered_map>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
// -------------------------------------------------------------------------
// BulletTableReader
// Reads the world's active bullet table each call (non-incremental).
// Direction is derived from the visual state's stored direction vectors;
// if those are unavailable the reader falls back to computing a flight
// delta from the previous frame's position.
// -------------------------------------------------------------------------
class BulletTableReader {
public:
/// Read all active bullets into @p entries.
/// Returns false only when the bullet table pointer cannot be read.
bool TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZBulletEntry>& entries);
/// Reset cached position state. Call when the game session restarts.
void Reset();
private:
// ------------------------------------------------------------------
// Per-bullet state (previous-frame position for flight-delta fallback)
// ------------------------------------------------------------------
std::unordered_map<uint64_t, Vector3> m_lastPositions;
bool m_tableUnavailable = false;
// ------------------------------------------------------------------
// Entry construction
// ------------------------------------------------------------------
DayZBulletEntry BuildBulletEntry(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t address);
/// Read the bullet's current position from its visual state.
static bool TryReadBulletPosition(VmmAccessor& mem,
uint32_t pid,
uint64_t visualStateAddr,
Vector3& pos);
/// Read the stored direction vectors (DirectionX / DirectionY) from the
/// visual state and derive a normalized world direction + heading.
static bool TryReadBulletDirection(VmmAccessor& mem,
uint32_t pid,
uint64_t visualStateAddr,
Vector3& dir,
float& heading);
/// Derive flight direction from the delta between previous and current
/// positions. Returns false when the delta is too small to be useful.
bool TryGetFlightDirection(const Vector3& prev,
const Vector3& curr,
Vector3& dir,
float& heading);
/// Sync m_lastPositions with the entries that were just built:
/// add/update entries still alive, remove entries no longer present.
void UpdateLastPositions(const std::vector<DayZBulletEntry>& entries);
// ------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------
static constexpr int kMaxBullets = 512;
static constexpr float kMinDirectionDelta = 0.001f;
};
+238
View File
@@ -0,0 +1,238 @@
#include "Readers/ClientScoreboardReader.h"
#include <algorithm>
#include <cstring>
#include <unordered_set>
#include "Offsets.h"
#include "Memory/MemoryValidation.h"
ClientScoreboardReader::ClientScoreboardReader(int refreshIntervalMs)
: m_networkIdRefreshInterval(refreshIntervalMs)
{}
// -------------------------------------------------------------------------
// RefreshNetworkId
// -------------------------------------------------------------------------
void ClientScoreboardReader::RefreshNetworkId(VmmAccessor& mem,
uint32_t pid,
uint64_t identityAddr,
IdentityMetadata& meta)
{
uint32_t id = 0;
if (mem.TryReadValue<uint32_t>(pid,
identityAddr + Offsets::Network::PlayerIdentityNetworkId,
id))
{
meta.networkId = id;
}
meta.lastNetworkIdReadTime = std::chrono::steady_clock::now();
}
// -------------------------------------------------------------------------
// TryRead
// -------------------------------------------------------------------------
bool ClientScoreboardReader::TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZScoreboardPlayer>& players)
{
const uint32_t pid = session.processId;
// -----------------------------------------------------------------
// 1. NetworkClient pointer
// -----------------------------------------------------------------
uint64_t networkClientAddr = 0;
if (!mem.TryReadPointer(pid,
session.networkManagerAddress + Offsets::Network::ManagerNetworkClient,
networkClientAddr))
{
return false;
}
// -----------------------------------------------------------------
// 2. Scoreboard base pointer
// -----------------------------------------------------------------
uint64_t scoreboardPtr = 0;
if (!mem.TryReadPointer(pid,
networkClientAddr + Offsets::Network::ClientScoreboard,
scoreboardPtr))
{
return false;
}
// -----------------------------------------------------------------
// 3. Player count
// -----------------------------------------------------------------
int32_t rawPlayerCount = 0;
if (!mem.TryReadValue<int32_t>(pid,
networkClientAddr + Offsets::Network::ClientPlayerCount,
rawPlayerCount))
{
return false;
}
const int playerCount = std::clamp(rawPlayerCount, 0, kMaxPlayers);
if (playerCount == 0) {
players.clear();
return true;
}
// -----------------------------------------------------------------
// 4. Bulk-read identity pointer array (kScoreboardCapacity * 8 bytes)
// -----------------------------------------------------------------
const int slotsToRead = std::min(playerCount, kScoreboardCapacity);
std::vector<uint8_t> ptrTable;
ptrTable.reserve(static_cast<size_t>(slotsToRead) * 8);
if (!mem.ReadBytes(pid, scoreboardPtr,
static_cast<size_t>(slotsToRead) * 8, ptrTable))
{
return false;
}
// -----------------------------------------------------------------
// 5. Build list of valid identity addresses from the table
// -----------------------------------------------------------------
std::vector<uint64_t> validAddresses;
validAddresses.reserve(static_cast<size_t>(slotsToRead));
for (int i = 0; i < slotsToRead; ++i) {
uint64_t identityAddr = 0;
std::memcpy(&identityAddr,
ptrTable.data() + static_cast<size_t>(i) * 8,
sizeof(uint64_t));
if (!MemoryValidation::IsValidUserAddress(identityAddr)) {
continue;
}
validAddresses.push_back(identityAddr);
}
// -----------------------------------------------------------------
// 6. Ensure every address has a cached metadata entry
// Read identity strings once; they are stable for the session.
// -----------------------------------------------------------------
for (uint64_t identityAddr : validAddresses) {
if (m_metadataByAddress.count(identityAddr)) {
continue; // already cached
}
IdentityMetadata meta;
// SteamId: read ptr at offset, then ArmaString
{
uint64_t steamIdPtr = 0;
if (mem.TryReadPointer(pid,
identityAddr + Offsets::Network::PlayerIdentitySteamId,
steamIdPtr))
{
meta.steamId = mem.ReadArmaString(pid, steamIdPtr);
}
}
// PlayerName: read ptr at offset, then ArmaString
{
uint64_t namePtr = 0;
if (mem.TryReadPointer(pid,
identityAddr + Offsets::Network::PlayerIdentityPlayerName,
namePtr))
{
meta.playerName = mem.ReadArmaString(pid, namePtr);
}
}
// NetworkId (initial read — will be refreshed on schedule)
RefreshNetworkId(mem, pid, identityAddr, meta);
m_metadataByAddress[identityAddr] = std::move(meta);
}
// -----------------------------------------------------------------
// 7. Round-robin refresh of stale NetworkIds
// -----------------------------------------------------------------
if (!validAddresses.empty()) {
const auto now = std::chrono::steady_clock::now();
int refreshed = 0;
const int total = static_cast<int>(validAddresses.size());
// Clamp cursor to valid range after potential list shrinkage
if (m_networkIdRefreshCursor >= total) {
m_networkIdRefreshCursor = 0;
}
for (int attempt = 0;
attempt < total && refreshed < kNetworkIdRefreshBatchSize;
++attempt)
{
const int idx = (m_networkIdRefreshCursor + attempt) % total;
uint64_t addr = validAddresses[static_cast<size_t>(idx)];
auto it = m_metadataByAddress.find(addr);
if (it == m_metadataByAddress.end()) continue;
IdentityMetadata& meta = it->second;
if (now - meta.lastNetworkIdReadTime >= m_networkIdRefreshInterval) {
RefreshNetworkId(mem, pid, addr, meta);
++refreshed;
}
}
m_networkIdRefreshCursor =
(m_networkIdRefreshCursor + refreshed) % total;
}
// -----------------------------------------------------------------
// 8. Build result, deduplicate by NetworkId (or playerName when id==0)
// -----------------------------------------------------------------
std::vector<DayZScoreboardPlayer> result;
result.reserve(validAddresses.size());
std::unordered_set<uint32_t> seenNetworkIds;
std::unordered_set<std::string> seenNames;
for (uint64_t identityAddr : validAddresses) {
auto it = m_metadataByAddress.find(identityAddr);
if (it == m_metadataByAddress.end()) continue;
const IdentityMetadata& meta = it->second;
if (meta.networkId != 0) {
if (!seenNetworkIds.insert(meta.networkId).second) {
continue; // duplicate
}
} else {
// No network ID yet — deduplicate by name
if (meta.playerName.empty()) continue;
if (!seenNames.insert(meta.playerName).second) {
continue;
}
}
DayZScoreboardPlayer player;
player.networkId = meta.networkId;
player.steamId = meta.steamId;
player.playerName = meta.playerName;
result.push_back(std::move(player));
}
// Sort by player name (case-sensitive, matches C# behaviour)
std::sort(result.begin(), result.end(),
[](const DayZScoreboardPlayer& a, const DayZScoreboardPlayer& b) {
return a.playerName < b.playerName;
});
players = std::move(result);
return true;
}
// -------------------------------------------------------------------------
// Reset
// -------------------------------------------------------------------------
void ClientScoreboardReader::Reset()
{
m_metadataByAddress.clear();
m_cachedResult.clear();
m_networkIdRefreshCursor = 0;
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <chrono>
#include <string>
#include <unordered_map>
#include <vector>
#include <cstdint>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
// -------------------------------------------------------------------------
// ClientScoreboardReader
// Reads the client-side player scoreboard from the network manager.
//
// Strategy
// --------
// - Player identity strings (steamId, playerName) are cached by identity
// pointer address: they are stable for the lifetime of a session.
// - NetworkIds are re-read in a round-robin fashion because the engine
// updates them after map-load. Up to 16 IDs are refreshed per call;
// the refresh interval is configurable (default 30 s).
// - The final result is de-duplicated by NetworkId (or by playerName when
// networkId == 0) and sorted by playerName.
// -------------------------------------------------------------------------
class ClientScoreboardReader {
public:
explicit ClientScoreboardReader(int refreshIntervalMs = 30000);
/// Populate @p players with the current scoreboard state.
/// Returns false only when the fundamental scoreboard reads fail.
bool TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZScoreboardPlayer>& players);
/// Clear all cached identity metadata. Call on session restart.
void Reset();
private:
// Maximum number of identity slots in the scoreboard array
static constexpr int kScoreboardCapacity = 120;
static constexpr int kMaxPlayers = 128;
// How many stale NetworkIds to refresh per TryRead call
static constexpr int kNetworkIdRefreshBatchSize = 16;
struct IdentityMetadata {
std::string steamId;
std::string playerName;
uint32_t networkId = 0;
std::chrono::steady_clock::time_point lastNetworkIdReadTime{};
};
std::unordered_map<uint64_t, IdentityMetadata> m_metadataByAddress;
std::vector<DayZScoreboardPlayer> m_cachedResult;
int m_networkIdRefreshCursor = 0;
std::chrono::milliseconds m_networkIdRefreshInterval;
/// Read or refresh the NetworkId for a single identity entry.
void RefreshNetworkId(VmmAccessor& mem,
uint32_t pid,
uint64_t identityAddr,
IdentityMetadata& meta);
};
+298
View File
@@ -0,0 +1,298 @@
#include "Readers/EntityCategoryProjector.h"
#include <algorithm>
#include <string_view>
#include <unordered_set>
// Returns true if the player entity itself looks like an admin-invisible entity.
// Currently: entity has no model at all (empty model name = never a real survivor).
static bool IsAdminEntityModel(const std::string& modelName)
{
return modelName.empty();
}
// -------------------------------------------------------------------------
// EnumerateEntities
// Merges near + far + slow lists, deduplicates by address, and returns a
// stable-sorted flat vector.
// -------------------------------------------------------------------------
std::vector<EntityCategoryProjector::EntityProjection>
EntityCategoryProjector::EnumerateEntities(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow)
{
std::unordered_set<uint64_t> seen;
std::vector<EntityProjection> result;
// Reserve a reasonable upper bound to avoid repeated reallocations.
result.reserve(near.size() + far.size() + slow.size());
// Lambda: insert an EntityProjection from a near or far entry (has networkId).
auto addWithId = [&](uint64_t address, uint32_t networkId,
const std::optional<Vector3>& pos,
const std::optional<float>& heading,
const std::string& entityName,
const std::string& typeName,
const std::string& configName,
const std::string& modelName)
{
if (address == 0) return;
if (!seen.insert(address).second) return; // duplicate
EntityProjection ep;
ep.address = address;
ep.networkId = networkId;
ep.position = pos;
ep.headingDegrees = heading;
ep.entityName = entityName;
ep.typeName = typeName;
ep.configName = configName;
ep.modelName = modelName;
result.push_back(std::move(ep));
};
for (const auto& e : near) {
addWithId(e.address, e.networkId, e.position, e.headingDegrees,
e.entityName, e.typeName, e.configName, e.modelName);
}
for (const auto& e : far) {
addWithId(e.address, e.networkId, e.position, e.headingDegrees,
e.entityName, e.typeName, e.configName, e.modelName);
}
// Slow entries have no networkId.
for (const auto& e : slow) {
if (e.address == 0) continue;
if (!seen.insert(e.address).second) continue;
EntityProjection ep;
ep.address = e.address;
ep.networkId = 0;
ep.position = e.position;
ep.headingDegrees = e.headingDegrees;
ep.entityName = e.entityName;
ep.typeName = e.typeName;
ep.configName = e.configName;
ep.modelName = e.modelName;
result.push_back(std::move(ep));
}
// Stable sort by (entityName, address) for deterministic output.
std::sort(result.begin(), result.end(),
[](const EntityProjection& a, const EntityProjection& b) {
if (a.entityName != b.entityName) return a.entityName < b.entityName;
return a.address < b.address;
});
return result;
}
// -------------------------------------------------------------------------
// IsKnownCategoryType
// -------------------------------------------------------------------------
bool EntityCategoryProjector::IsKnownCategoryType(const std::string& typeName) {
return typeName == kTypePlayer ||
typeName == kTypeAnimal ||
typeName == kTypeZombie ||
typeName == kTypeBoat ||
typeName == kTypeCar ||
typeName == kTypeHeli ||
typeName == kTypePlane;
}
// -------------------------------------------------------------------------
// GetVehicleKind
// -------------------------------------------------------------------------
std::optional<VehicleKind>
EntityCategoryProjector::GetVehicleKind(const std::string& typeName) {
if (typeName == kTypeCar) return VehicleKind::Car;
if (typeName == kTypeBoat) return VehicleKind::Boat;
if (typeName == kTypeHeli) return VehicleKind::Helicopter;
if (typeName == kTypePlane)return VehicleKind::Plane;
return std::nullopt;
}
// -------------------------------------------------------------------------
// BuildPlayers
// -------------------------------------------------------------------------
std::vector<DayZPlayerEntry> EntityCategoryProjector::BuildPlayers(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow,
const std::unordered_map<uint32_t, std::string>& scoreboardNames,
std::function<bool(uint64_t)> deadResolver,
std::function<std::string(uint64_t)> heldItemResolver,
std::function<float(uint64_t)> healthResolver,
std::function<bool(uint64_t)> adminResolver)
{
auto all = EnumerateEntities(near, far, slow);
std::vector<DayZPlayerEntry> result;
result.reserve(all.size());
for (const auto& ep : all) {
if (ep.typeName != kTypePlayer) continue;
DayZPlayerEntry entry;
entry.address = ep.address;
entry.networkId = ep.networkId;
entry.position = ep.position;
entry.headingDegrees = ep.headingDegrees;
entry.typeName = ep.typeName;
entry.configName = ep.configName;
entry.modelName = ep.modelName;
// Entity has no model → almost certainly admin-invisible; otherwise check clothes.
entry.isAdmin = IsAdminEntityModel(ep.modelName)
|| (adminResolver && adminResolver(ep.address));
// Nickname from scoreboard (keyed by networkId).
if (ep.networkId != 0) {
auto it = scoreboardNames.find(ep.networkId);
if (it != scoreboardNames.end()) {
entry.nickname = it->second;
}
}
// Dead state, held item, and health — resolved via caller-provided callbacks.
if (deadResolver) entry.isDead = deadResolver(ep.address);
if (heldItemResolver)entry.itemInHands = heldItemResolver(ep.address);
if (healthResolver) entry.health = healthResolver(ep.address);
result.push_back(std::move(entry));
}
return result;
}
// -------------------------------------------------------------------------
// BuildAnimals
// -------------------------------------------------------------------------
std::vector<DayZAnimalEntry> EntityCategoryProjector::BuildAnimals(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow)
{
auto all = EnumerateEntities(near, far, slow);
std::vector<DayZAnimalEntry> result;
result.reserve(all.size());
for (const auto& ep : all) {
if (ep.typeName != kTypeAnimal) continue;
DayZAnimalEntry entry;
entry.address = ep.address;
entry.position = ep.position;
entry.headingDegrees = ep.headingDegrees;
entry.entityName = ep.entityName;
entry.typeName = ep.typeName;
entry.configName = ep.configName;
entry.modelName = ep.modelName;
result.push_back(std::move(entry));
}
return result;
}
// -------------------------------------------------------------------------
// BuildZombies
// -------------------------------------------------------------------------
std::vector<DayZZombieEntry> EntityCategoryProjector::BuildZombies(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow)
{
auto all = EnumerateEntities(near, far, slow);
std::vector<DayZZombieEntry> result;
result.reserve(all.size());
for (const auto& ep : all) {
if (ep.typeName != kTypeZombie) continue;
DayZZombieEntry entry;
entry.address = ep.address;
entry.position = ep.position;
entry.headingDegrees = ep.headingDegrees;
entry.entityName = ep.entityName;
entry.typeName = ep.typeName;
entry.configName = ep.configName;
entry.modelName = ep.modelName;
result.push_back(std::move(entry));
}
return result;
}
// -------------------------------------------------------------------------
// BuildCarsAndBoats
// -------------------------------------------------------------------------
std::vector<DayZCarAndBoatEntry> EntityCategoryProjector::BuildCarsAndBoats(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow)
{
auto all = EnumerateEntities(near, far, slow);
std::vector<DayZCarAndBoatEntry> result;
result.reserve(all.size());
for (const auto& ep : all) {
auto kind = GetVehicleKind(ep.typeName);
if (!kind.has_value()) continue;
DayZCarAndBoatEntry entry;
entry.address = ep.address;
entry.position = ep.position;
entry.headingDegrees = ep.headingDegrees;
entry.entityName = ep.entityName;
entry.typeName = ep.typeName;
entry.configName = ep.configName;
entry.modelName = ep.modelName;
entry.kind = *kind;
result.push_back(std::move(entry));
}
return result;
}
// -------------------------------------------------------------------------
// BuildOtherEntities
// -------------------------------------------------------------------------
std::vector<DayZOtherEntityEntry> EntityCategoryProjector::BuildOtherEntities(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow)
{
auto all = EnumerateEntities(near, far, slow);
std::vector<DayZOtherEntityEntry> result;
result.reserve(all.size());
for (const auto& ep : all) {
// Skip entities that belong to an explicitly-handled category.
if (IsKnownCategoryType(ep.typeName)) continue;
DayZOtherEntityEntry entry;
entry.address = ep.address;
entry.position = ep.position;
entry.headingDegrees = ep.headingDegrees;
entry.entityName = ep.entityName;
entry.typeName = ep.typeName;
entry.configName = ep.configName;
entry.modelName = ep.modelName;
result.push_back(std::move(entry));
}
return result;
}
+93
View File
@@ -0,0 +1,93 @@
#pragma once
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "Core/Models.h"
// -------------------------------------------------------------------------
// EntityCategoryProjector
// Merges the three entity lists (near / far / slow) and projects them into
// typed category vectors. All methods are static — no instance state.
// -------------------------------------------------------------------------
class EntityCategoryProjector {
public:
// ------------------------------------------------------------------
// Public projection methods
// ------------------------------------------------------------------
static std::vector<DayZPlayerEntry> BuildPlayers(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow,
const std::unordered_map<uint32_t, std::string>& scoreboardNames,
std::function<bool(uint64_t)> deadResolver,
std::function<std::string(uint64_t)> heldItemResolver,
std::function<float(uint64_t)> healthResolver,
std::function<bool(uint64_t)> adminResolver = nullptr);
static std::vector<DayZAnimalEntry> BuildAnimals(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow);
static std::vector<DayZZombieEntry> BuildZombies(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow);
static std::vector<DayZCarAndBoatEntry> BuildCarsAndBoats(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow);
static std::vector<DayZOtherEntityEntry> BuildOtherEntities(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow);
private:
// ------------------------------------------------------------------
// Unified flattened view of one entity from any list
// ------------------------------------------------------------------
struct EntityProjection {
uint64_t address = 0;
uint32_t networkId = 0;
std::optional<Vector3> position;
std::optional<float> headingDegrees;
std::string entityName;
std::string typeName;
std::string configName;
std::string modelName;
};
// ------------------------------------------------------------------
// Internal helpers
// ------------------------------------------------------------------
/// Merge all three lists, deduplicate by address, and sort by
/// (entityName, address) for deterministic ordering.
static std::vector<EntityProjection> EnumerateEntities(
const std::vector<DayZNearEntityEntry>& near,
const std::vector<DayZFarEntityEntry>& far,
const std::vector<DayZSlowEntityEntry>& slow);
/// Returns true if typeName belongs to one of the explicitly-handled
/// categories (player, animal, zombie, vehicle).
static bool IsKnownCategoryType(const std::string& typeName);
/// Returns the VehicleKind for known vehicle type names, or nullopt.
static std::optional<VehicleKind> GetVehicleKind(const std::string& typeName);
// Type-name constants
static constexpr const char* kTypePlayer = "dayzplayer";
static constexpr const char* kTypeAnimal = "dayzanimal";
static constexpr const char* kTypeZombie = "dayzinfected";
static constexpr const char* kTypeBoat = "boat";
static constexpr const char* kTypeCar = "car";
static constexpr const char* kTypeHeli = "helicopter";
static constexpr const char* kTypePlane = "plane";
};
+204
View File
@@ -0,0 +1,204 @@
#include "Readers/EntityTypeCache.h"
#include <cctype>
#include <cstdio>
#include <string>
#include "Offsets.h"
#include "Memory/MemoryValidation.h"
// -------------------------------------------------------------------------
// FormatEntityName
// -------------------------------------------------------------------------
std::string FormatEntityName(const std::string& raw)
{
if (raw.empty()) return raw;
// Strip known prefixes (case-sensitive — DayZ uses "Dayz_" and "DayZ_")
std::string s = raw;
for (const char* prefix : { "DayZ_", "Dayz_", "dayz_", "DAYZ_" }) {
size_t len = std::strlen(prefix);
if (s.size() > len && s.compare(0, len, prefix) == 0) {
s = s.substr(len);
break;
}
}
// Split PascalCase: insert space before an uppercase letter that is
// preceded by a lowercase letter or digit.
std::string result;
result.reserve(s.size() + 8);
for (size_t i = 0; i < s.size(); ++i) {
unsigned char c = static_cast<unsigned char>(s[i]);
if (i > 0 && std::isupper(c)) {
unsigned char prev = static_cast<unsigned char>(s[i - 1]);
if (std::islower(prev) || std::isdigit(prev))
result += ' ';
}
result += static_cast<char>(c == '_' ? ' ' : c);
}
// Collapse multiple consecutive spaces (e.g. from "CQB_Rifle" → "CQB Rifle")
std::string out;
out.reserve(result.size());
bool prevSpace = false;
for (char c : result) {
if (c == ' ') { if (!prevSpace) { out += ' '; prevSpace = true; } }
else { out += c; prevSpace = false; }
}
// Trim leading/trailing space
size_t start = out.find_first_not_of(' ');
size_t end = out.find_last_not_of(' ');
return (start == std::string::npos) ? "" : out.substr(start, end - start + 1);
}
// -------------------------------------------------------------------------
// SanitizeEntityString
// -------------------------------------------------------------------------
std::string SanitizeEntityString(const std::string& value)
{
if (value.empty()) return {};
// Reject strings containing unwanted control characters
for (unsigned char c : value) {
if (c < 0x20 && c != '\t' && c != '\r' && c != '\n') {
return {};
}
}
// Count "readable" characters: letters, digits, whitespace, and common
// punctuation used in DayZ type names.
int readableCount = 0;
for (unsigned char c : value) {
if (std::isalpha(c) ||
std::isdigit(c) ||
std::isspace(c) ||
c == '.' || c == ',' || c == ':' || c == ';' ||
c == '-' || c == '_' || c == '+' || c == '/' ||
c == '\\' || c == '(' || c == ')' || c == '[' ||
c == ']' || c == '{' || c == '}' || c == '#')
{
++readableCount;
}
}
// Require readableCount * 2 >= length (i.e. >= 50 % readable)
if (readableCount * 2 < static_cast<int>(value.size())) {
return {};
}
return value;
}
// -------------------------------------------------------------------------
// GetBestEntityName
// -------------------------------------------------------------------------
std::string GetBestEntityName(const std::string& typeName,
const std::string& configName,
const std::string& cleanName,
const std::string& modelName,
uint64_t entityAddress)
{
if (!typeName.empty()) return typeName;
if (!configName.empty()) return configName;
if (!cleanName.empty()) return cleanName;
if (!modelName.empty()) return modelName;
// Last resort: hex address
char buf[32];
std::snprintf(buf, sizeof(buf), "0x%016llX",
static_cast<unsigned long long>(entityAddress));
return buf;
}
// -------------------------------------------------------------------------
// ReadEntityTypeString
// -------------------------------------------------------------------------
std::string ReadEntityTypeString(VmmAccessor& mem,
uint32_t pid,
uint64_t typeAddress,
uint64_t offset)
{
uint64_t strPtr = 0;
if (!mem.TryReadPointer(pid, typeAddress + offset, strPtr)) {
return {};
}
return mem.ReadArmaString(pid, strPtr);
}
// -------------------------------------------------------------------------
// ReadEntityTypeMetadata
// -------------------------------------------------------------------------
EntityTypeMetadata ReadEntityTypeMetadata(VmmAccessor& mem,
uint32_t pid,
uint64_t typeAddress)
{
EntityTypeMetadata meta;
meta.typeName = SanitizeEntityString(
ReadEntityTypeString(mem, pid, typeAddress, Offsets::EntityType::TypeName));
meta.configName = SanitizeEntityString(
ReadEntityTypeString(mem, pid, typeAddress, Offsets::EntityType::ConfigName));
meta.cleanName = SanitizeEntityString(
ReadEntityTypeString(mem, pid, typeAddress, Offsets::EntityType::CleanName));
meta.modelName = SanitizeEntityString(
ReadEntityTypeString(mem, pid, typeAddress, Offsets::EntityType::ModelName));
return meta;
}
// -------------------------------------------------------------------------
// ReadEntityTransform
// -------------------------------------------------------------------------
bool ReadEntityTransform(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t entityAddress,
std::optional<Vector3>& position,
std::optional<float>& headingDegrees)
{
const uint32_t pid = session.processId;
// VisualState pointer
uint64_t visualStateAddr = 0;
if (!mem.TryReadPointer(pid,
entityAddress + Offsets::Common::VisualState,
visualStateAddr))
{
return false;
}
// Position
Vector3 pos{};
if (!mem.TryReadValue<Vector3>(pid,
visualStateAddr + Offsets::Common::Position,
pos))
{
return false;
}
if (!MemoryValidation::IsValidVector(pos)) {
return false;
}
position = pos;
// Heading (non-fatal if direction reads fail)
float dirX = 0.0f;
float dirY = 0.0f;
if (mem.TryReadValue<float>(pid, visualStateAddr + Offsets::Common::DirectionX, dirX) &&
mem.TryReadValue<float>(pid, visualStateAddr + Offsets::Common::DirectionY, dirY))
{
float heading = 0.0f;
if (MemoryValidation::TryGetCorrectedHeadingFromDirection(dirX, dirY, heading)) {
headingDegrees = heading;
}
}
return true;
}
+91
View File
@@ -0,0 +1,91 @@
#pragma once
#include <string>
#include <unordered_map>
#include <optional>
#include <cstdint>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
// -------------------------------------------------------------------------
// EntityTypeMetadata
// All type-level strings read from the entity's type object.
// -------------------------------------------------------------------------
struct EntityTypeMetadata {
std::string typeName;
std::string configName;
std::string cleanName;
std::string modelName;
};
// -------------------------------------------------------------------------
// EntityTypeCache
// In-process cache keyed by type object address.
// -------------------------------------------------------------------------
class EntityTypeCache {
std::unordered_map<uint64_t, EntityTypeMetadata> m_cache;
public:
/// Return true (and populate @p out) if the address is already cached.
bool GetOrRead(uint64_t typeAddress, EntityTypeMetadata& out) {
auto it = m_cache.find(typeAddress);
if (it != m_cache.end()) { out = it->second; return true; }
return false;
}
void Set(uint64_t typeAddress, const EntityTypeMetadata& meta) {
m_cache[typeAddress] = meta;
}
void Clear() { m_cache.clear(); }
};
// -------------------------------------------------------------------------
// Free functions implemented in EntityTypeCache.cpp
// -------------------------------------------------------------------------
/// Format a raw PascalCase entity name into a human-readable string.
/// Strips prefixes like "Dayz_", splits on capital letter boundaries,
/// and replaces underscores with spaces.
/// e.g. "KizlyarKnife" → "Kizlyar Knife", "CivilianSedan" → "Civilian Sedan"
std::string FormatEntityName(const std::string& raw);
/// Sanitize an entity type string read from memory.
/// Returns empty string if the value contains control characters or the
/// fraction of readable characters (letters/digits/whitespace/punctuation) is
/// below 50 %.
std::string SanitizeEntityString(const std::string& value);
/// Pick the best display name from the four candidate strings.
/// Falls back through typeName -> configName -> cleanName -> modelName, then
/// to a hex address string if all are empty.
std::string GetBestEntityName(const std::string& typeName,
const std::string& configName,
const std::string& cleanName,
const std::string& modelName,
uint64_t entityAddress);
/// Read a single type string: dereference ptr at typeAddress+offset, then
/// call ReadArmaString on the resulting pointer.
std::string ReadEntityTypeString(VmmAccessor& mem,
uint32_t pid,
uint64_t typeAddress,
uint64_t offset);
/// Read all four type strings for a type object and return the populated
/// EntityTypeMetadata.
EntityTypeMetadata ReadEntityTypeMetadata(VmmAccessor& mem,
uint32_t pid,
uint64_t typeAddress);
/// Read the entity transform (position + heading) from the entity's
/// VisualState. Both outputs are optional; if a read or validation step
/// fails the corresponding optional remains empty.
/// Returns true if at least position was read successfully.
bool ReadEntityTransform(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t entityAddress,
std::optional<Vector3>& position,
std::optional<float>& headingDegrees);
+417
View File
@@ -0,0 +1,417 @@
#include "Readers/FarEntityListReader.h"
#include <algorithm>
#include <cstring>
#include "Offsets.h"
#include "RuntimeOffsets.h"
#include "Memory/MemoryValidation.h"
#include "Memory/VmmAccessor.h"
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
FarEntityListReader::FarEntityListReader(int batchSize,
int readBudgetMs,
int staleAfterMs)
: m_batchSize(batchSize)
, m_readBudget(readBudgetMs)
, m_staleAfter(staleAfterMs)
{}
// -------------------------------------------------------------------------
// TryRead
// -------------------------------------------------------------------------
bool FarEntityListReader::TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZFarEntityEntry>& entries)
{
const uint32_t pid = session.processId;
// 1. Far-entity list pointer
uint64_t farListPtr = 0;
if (!mem.TryReadPointer(pid,
session.worldAddress + RuntimeOffsets::World::FarEntityList,
farListPtr))
{
return false;
}
// 2. Table size
int32_t rawCount = 0;
if (!mem.TryReadValue<int32_t>(pid,
session.worldAddress + RuntimeOffsets::World::FarTableSize,
rawCount))
{
return false;
}
const int count = std::clamp(rawCount, 0, kMaxEntities);
// 3. If cursor has reached the end of the table, complete the current scan
if (m_cursorOffset >= count * 8) {
auto now = std::chrono::steady_clock::now();
CompleteScan(now);
PruneExpiredEntities(now);
entries = BuildResult();
return true;
}
// 4. Calculate chunk: how many pointer-sized slots remain vs batch limit
const int remaining = count - (m_cursorOffset / 8);
const int chunkEntries = std::min(remaining, m_batchSize);
// 5. Bulk-read chunk of pointers
const size_t chunkBytes = static_cast<size_t>(chunkEntries) * 8;
std::vector<uint8_t> tableBytes;
tableBytes.reserve(chunkBytes);
if (!mem.ReadBytes(pid, farListPtr + static_cast<uint64_t>(m_cursorOffset),
chunkBytes, tableBytes))
{
// On failure advance cursor to avoid getting stuck
m_cursorOffset += static_cast<int>(chunkBytes);
entries = BuildResult();
return true;
}
// 6. Process entities within the read budget
const auto batchStart = std::chrono::steady_clock::now();
for (int i = 0; i < chunkEntries; ++i) {
uint64_t entityAddr = 0;
std::memcpy(&entityAddr,
tableBytes.data() + static_cast<size_t>(i) * 8,
sizeof(uint64_t));
if (MemoryValidation::IsValidUserAddress(entityAddr)) {
auto now = std::chrono::steady_clock::now();
ProcessEntity(mem, session, entityAddr, now);
// Check time budget after each entity
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - batchStart);
if (elapsed >= m_readBudget) {
// Advance cursor to account for entities processed so far
m_cursorOffset += (i + 1) * 8;
PruneExpiredEntities(std::chrono::steady_clock::now());
entries = BuildResult();
return true;
}
}
}
// 7. Advance cursor by the full chunk
m_cursorOffset += chunkEntries * 8;
// 8. Prune stale entries
auto now = std::chrono::steady_clock::now();
PruneExpiredEntities(now);
// 9. If cursor reached end, complete scan
if (m_cursorOffset >= count * 8) {
CompleteScan(now);
}
entries = BuildResult();
return true;
}
// -------------------------------------------------------------------------
// Reset
// -------------------------------------------------------------------------
void FarEntityListReader::Reset()
{
m_entities.clear();
m_typeCache.Clear();
m_cachedResult.clear();
m_sortedAddresses.clear();
m_cursorOffset = 0;
m_scanGeneration = 1;
m_hasCompletedScan = false;
m_sortDirty = true;
m_resultDirty = true;
}
// -------------------------------------------------------------------------
// ProcessEntity
// -------------------------------------------------------------------------
void FarEntityListReader::ProcessEntity(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t entityAddr,
std::chrono::steady_clock::time_point now)
{
const uint32_t pid = session.processId;
// Read entity type pointer
uint64_t typeAddr = 0;
if (!mem.TryReadPointer(pid, entityAddr + Offsets::Common::Type, typeAddr)) {
MarkTypeReadFailure(entityAddr, nullptr, now);
return;
}
// Look up or read type metadata
EntityTypeMetadata typeMeta;
if (!m_typeCache.GetOrRead(typeAddr, typeMeta)) {
typeMeta = ReadEntityTypeMetadata(mem, pid, typeAddr);
if (typeMeta.typeName.empty() && typeMeta.configName.empty() &&
typeMeta.cleanName.empty() && typeMeta.modelName.empty())
{
// Type read produced nothing useful — track failure on cached entity
auto it = m_entities.find(entityAddr);
MarkTypeReadFailure(entityAddr,
(it != m_entities.end()) ? &it->second : nullptr,
now);
return;
}
m_typeCache.Set(typeAddr, typeMeta);
}
// Network ID
uint32_t networkId = 0;
mem.TryReadValue<uint32_t>(pid, entityAddr + Offsets::Network::EntityNetworkId, networkId);
// Transform — use cached position/heading if the read fails.
// Also cache the VS pointer for use in RefreshPositions.
std::optional<Vector3> position;
std::optional<float> heading;
uint64_t vsAddr = 0;
mem.TryReadValue<uint64_t>(pid, entityAddr + Offsets::Common::VisualState, vsAddr);
bool transformOk = ReadEntityTransform(mem, session, entityAddr, position, heading);
// Retrieve or create cached entry
auto& cached = m_entities[entityAddr];
if (vsAddr) cached.visualStateAddr = vsAddr;
if (!transformOk && cached.entry.position.has_value()) {
position = cached.entry.position;
heading = cached.entry.headingDegrees;
}
// Build display name
const std::string entityName = GetBestEntityName(typeMeta.typeName,
typeMeta.configName,
typeMeta.cleanName,
typeMeta.modelName,
entityAddr);
// Update cached entry
cached.entry.address = entityAddr;
cached.entry.networkId = networkId;
cached.entry.position = position;
cached.entry.headingDegrees = heading;
cached.entry.entityName = entityName;
cached.entry.typeName = typeMeta.typeName;
cached.entry.configName = typeMeta.configName;
cached.entry.modelName = typeMeta.modelName;
cached.lastSeenGeneration = m_scanGeneration;
cached.failedTypeReads = 0;
if (transformOk) {
cached.lastSuccessfulRead = now;
}
m_resultDirty = true;
}
// -------------------------------------------------------------------------
// CompleteScan
// -------------------------------------------------------------------------
void FarEntityListReader::CompleteScan(std::chrono::steady_clock::time_point /*now*/)
{
// Remove entities not seen in the current scan generation
for (auto it = m_entities.begin(); it != m_entities.end(); ) {
if (it->second.lastSeenGeneration != m_scanGeneration) {
it = m_entities.erase(it);
m_sortDirty = true;
m_resultDirty = true;
} else {
++it;
}
}
m_cursorOffset = 0;
m_hasCompletedScan = true;
++m_scanGeneration;
}
// -------------------------------------------------------------------------
// PruneExpiredEntities
// -------------------------------------------------------------------------
void FarEntityListReader::PruneExpiredEntities(std::chrono::steady_clock::time_point now)
{
for (auto it = m_entities.begin(); it != m_entities.end(); ) {
const auto age = std::chrono::duration_cast<std::chrono::milliseconds>(
now - it->second.lastSuccessfulRead);
if (age > m_staleAfter) {
it = m_entities.erase(it);
m_sortDirty = true;
m_resultDirty = true;
} else {
++it;
}
}
}
// -------------------------------------------------------------------------
// BuildResult
// -------------------------------------------------------------------------
std::vector<DayZFarEntityEntry>& FarEntityListReader::BuildResult()
{
if (!m_resultDirty) {
return m_cachedResult;
}
// Rebuild sorted address list when topology changes
if (m_sortDirty) {
m_sortedAddresses.clear();
m_sortedAddresses.reserve(m_entities.size());
for (const auto& kv : m_entities) {
m_sortedAddresses.push_back(kv.first);
}
std::sort(m_sortedAddresses.begin(), m_sortedAddresses.end(),
[this](uint64_t a, uint64_t b) {
const auto& ea = m_entities.at(a).entry;
const auto& eb = m_entities.at(b).entry;
if (ea.entityName != eb.entityName)
return ea.entityName < eb.entityName;
return a < b;
});
m_sortDirty = false;
}
m_cachedResult.clear();
m_cachedResult.reserve(m_sortedAddresses.size());
for (uint64_t addr : m_sortedAddresses) {
auto it = m_entities.find(addr);
if (it != m_entities.end()) {
m_cachedResult.push_back(it->second.entry);
}
}
m_resultDirty = false;
return m_cachedResult;
}
// -------------------------------------------------------------------------
// RefreshPositions
// Two-pass scatter read: all VS pointers in pass A, all positions in pass B.
// Entities with a stale/unknown VS pointer fall back to a sequential re-read.
// -------------------------------------------------------------------------
void FarEntityListReader::RefreshPositions(VmmAccessor& mem,
const RuntimeSession& session)
{
if (m_entities.empty()) return;
const uint32_t pid = session.processId;
// Collect entities that have a cached VS pointer and those that don't.
struct PosSlot {
uint64_t entityAddr;
uint64_t vsAddr;
Vector3 posOut;
bool ok = false;
};
std::vector<PosSlot> slots;
slots.reserve(m_entities.size());
// Pass A: scatter-read VS pointers for entities whose cached value is 0.
{
std::vector<VmmAccessor::ScatterEntry> vsScatter;
// We need a stable backing store for the scatter buffers.
// Use a parallel vector of uint64_t.
std::vector<uint64_t> vsBufs(m_entities.size(), 0);
int idx = 0;
for (auto& [addr, cached] : m_entities) {
PosSlot s;
s.entityAddr = addr;
s.vsAddr = cached.visualStateAddr;
slots.push_back(s);
if (cached.visualStateAddr == 0) {
vsScatter.push_back({ addr + Offsets::Common::VisualState,
&vsBufs[idx], sizeof(uint64_t) });
}
++idx;
}
if (!vsScatter.empty()) {
mem.ScatterRead(pid, vsScatter);
}
// Fill cached VS pointers from scatter result.
idx = 0;
for (auto& [addr, cached] : m_entities) {
if (cached.visualStateAddr == 0 && vsBufs[idx] != 0) {
cached.visualStateAddr = vsBufs[idx];
slots[idx].vsAddr = vsBufs[idx];
}
++idx;
}
}
// Pass B: scatter-read positions for all slots that have a VS pointer.
{
std::vector<VmmAccessor::ScatterEntry> posScatter;
posScatter.reserve(slots.size());
for (auto& slot : slots) {
if (slot.vsAddr == 0) continue;
posScatter.push_back({ slot.vsAddr + Offsets::Common::Position,
&slot.posOut, sizeof(Vector3) });
}
if (!posScatter.empty()) {
mem.ScatterRead(pid, posScatter);
}
}
// Apply results: update position on cached entities.
auto now = std::chrono::steady_clock::now();
for (auto& slot : slots) {
if (slot.vsAddr == 0) continue;
if (!MemoryValidation::IsValidVector(slot.posOut)) {
// Possibly stale VS pointer — invalidate so pass A re-reads it next time.
auto it = m_entities.find(slot.entityAddr);
if (it != m_entities.end())
it->second.visualStateAddr = 0;
continue;
}
auto it = m_entities.find(slot.entityAddr);
if (it != m_entities.end()) {
it->second.entry.position = slot.posOut;
it->second.lastSuccessfulRead = now;
m_resultDirty = true;
}
}
}
// -------------------------------------------------------------------------
// MarkTypeReadFailure
// -------------------------------------------------------------------------
void FarEntityListReader::MarkTypeReadFailure(uint64_t entityAddr,
CachedEntity* cached,
std::chrono::steady_clock::time_point now)
{
if (cached == nullptr) {
// Entity not yet in cache — nothing more to do
return;
}
++cached->failedTypeReads;
// Still mark it seen this generation so it isn't pruned by CompleteScan
cached->lastSeenGeneration = m_scanGeneration;
}
+87
View File
@@ -0,0 +1,87 @@
#pragma once
#include <vector>
#include <unordered_map>
#include <chrono>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
#include "Readers/EntityTypeCache.h"
// -------------------------------------------------------------------------
// FarEntityListReader
// Incremental reader for the world's far-entity pointer table.
// Processes entities in time-budgeted batches across calls, maintains a
// cached result set, and prunes stale entries by timestamp.
// -------------------------------------------------------------------------
class FarEntityListReader {
public:
FarEntityListReader(int batchSize = 64,
int readBudgetMs = 5,
int staleAfterMs = 5000);
/// Incrementally read far entities into @p entries.
/// Returns false only when the table pointer itself cannot be read.
bool TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZFarEntityEntry>& entries);
/// Position-only scatter refresh: re-reads positions for all entities
/// already known from previous TryRead calls without advancing the cursor.
/// Call this every frame / playersRefreshMs for smooth movement.
void RefreshPositions(VmmAccessor& mem, const RuntimeSession& session);
/// Reset all state. Call when the game session restarts.
void Reset();
private:
// ------------------------------------------------------------------
// Per-entity bookkeeping
// ------------------------------------------------------------------
struct CachedEntity {
DayZFarEntityEntry entry;
uint64_t visualStateAddr = 0; // cached for RefreshPositions
int lastSeenGeneration = 0;
std::chrono::steady_clock::time_point lastSuccessfulRead;
int failedTypeReads = 0;
};
// ------------------------------------------------------------------
// Internal helpers
// ------------------------------------------------------------------
void ProcessEntity(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t entityAddr,
std::chrono::steady_clock::time_point now);
void CompleteScan(std::chrono::steady_clock::time_point now);
void PruneExpiredEntities(std::chrono::steady_clock::time_point now);
std::vector<DayZFarEntityEntry>& BuildResult();
void MarkTypeReadFailure(uint64_t entityAddr,
CachedEntity* cached,
std::chrono::steady_clock::time_point now);
// ------------------------------------------------------------------
// State
// ------------------------------------------------------------------
std::unordered_map<uint64_t, CachedEntity> m_entities;
EntityTypeCache m_typeCache;
std::vector<DayZFarEntityEntry> m_cachedResult;
std::vector<uint64_t> m_sortedAddresses;
int m_cursorOffset = 0;
int m_scanGeneration = 1;
bool m_hasCompletedScan = false;
bool m_sortDirty = true;
bool m_resultDirty = true;
int m_batchSize;
std::chrono::milliseconds m_readBudget;
std::chrono::milliseconds m_staleAfter;
static constexpr int kMaxEntities = 4096;
};
+271
View File
@@ -0,0 +1,271 @@
#include "Readers/ItemFilterCatalog.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <iostream>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <nlohmann/json.hpp>
// -------------------------------------------------------------------------
// Default config JSON written when item_filters.json is absent
// -------------------------------------------------------------------------
static constexpr const char* kDefaultConfigJson = R"({
"isHouse": {
"desc": "Structures and storage",
"Color": "#06b6d4",
"ModelName": [
"\\containers\\",
"\\camping\\",
"\\cooking\\"
]
},
"isWeapon": {"desc": "Weapons", "Color": "#f43f5e", "TypeName": "weapon"},
"isAmmo": {"desc": "Ammo", "Color": "#f59e0b", "ModelName": "\\ammunition\\"},
"isFood": {"desc": "Food and drinks", "Color": "#22c55e", "ModelName": ["\\food\\", "\\drinks\\"]},
"isClothing": {"desc": "Clothing", "Color": "#eab308", "TypeName": "clothing"},
"isBackpack": {"desc": "Backpacks", "Color": "#84cc16", "TypeName": "backpack"},
"isMedical": {"desc": "Medical supplies", "Color": "#ec4899", "ModelName": "\\medical\\"},
"isVehiclePart": {"desc": "Vehicle parts", "Color": "#0ea5e9", "TypeName": "carwheel"},
"isTool": {"desc": "Tools", "Color": "#14b8a6", "ModelName": "\\tools\\"},
"isCrafting": {"desc": "Crafting materials", "Color": "#c084fc", "ModelName": "\\crafting\\"},
"isConsumables": {"desc": "Consumables", "Color": "#f97316", "ModelName": "\\consumables\\"},
"isOptics": {"desc": "Optics", "Color": "#d946ef", "TypeName": "itemoptics"},
"isMelee": {"desc": "Melee weapons", "Color": "#dc2626", "ModelName": "\\weapons\\melee\\"},
"isWeaponAttachments": {"desc": "Weapon attachments", "Color": "#fbbf24", "ModelName": "weapons\\attachments\\"},
"isExplosives": {"desc": "Explosives", "Color": "#fb923c", "ModelName": "\\explosives\\"},
"isForBuilding": {"desc": "Base building", "Color": "#0891b2", "ModelName": "\\codelock\\"},
"isOtherLoot": {"desc": "Other loot", "Color": "#64748b"}
})";
// -------------------------------------------------------------------------
// Helpers: resolve exe-relative path
// -------------------------------------------------------------------------
static std::string GetExeDir() {
char buf[MAX_PATH] = {};
GetModuleFileNameA(nullptr, buf, MAX_PATH);
std::filesystem::path p(buf);
return p.parent_path().string();
}
// -------------------------------------------------------------------------
// Constructors
// -------------------------------------------------------------------------
ItemFilterCatalog::ItemFilterCatalog()
: m_configPath(GetExeDir() + "/config/item_filters.json")
{
Load();
}
ItemFilterCatalog::ItemFilterCatalog(const std::string& configPath)
: m_configPath(configPath)
{
Load();
}
// -------------------------------------------------------------------------
// Load
// -------------------------------------------------------------------------
void ItemFilterCatalog::Load() {
namespace fs = std::filesystem;
// Write default config if the file does not exist.
if (!fs::exists(m_configPath)) {
try {
fs::create_directories(fs::path(m_configPath).parent_path());
} catch (const std::exception& e) {
std::cerr << "[ItemFilterCatalog] Could not create config directory: "
<< e.what() << "\n";
}
WriteDefaultConfig();
}
// Read and parse the JSON file.
std::ifstream ifs(m_configPath);
if (!ifs.is_open()) {
std::cerr << "[ItemFilterCatalog] Failed to open: " << m_configPath << "\n";
return;
}
nlohmann::json j;
try {
ifs >> j;
} catch (const nlohmann::json::parse_error& e) {
std::cerr << "[ItemFilterCatalog] JSON parse error in " << m_configPath
<< ": " << e.what() << "\n";
return;
}
m_filters.clear();
// JSON object preserves insertion order via nlohmann's ordered_json —
// but we use the standard json type here, so iterate as-is.
for (auto& [key, val] : j.items()) {
ItemFilterDefinition def;
def.key = key;
if (val.contains("desc") && val["desc"].is_string()) {
def.description = val["desc"].get<std::string>();
}
if (val.contains("Color") && val["Color"].is_string()) {
def.color = val["Color"].get<std::string>();
}
def.modelNames = NormalizeAll(ParseStringOrArray(val, "ModelName"));
def.configNames = NormalizeAll(ParseStringOrArray(val, "ConfigName"));
def.typeNames = NormalizeAll(ParseStringOrArray(val, "TypeName"));
def.names = NormalizeAll(ParseStringOrArray(val, "Name"));
m_filters.push_back(std::move(def));
}
}
// -------------------------------------------------------------------------
// WriteDefaultConfig
// -------------------------------------------------------------------------
void ItemFilterCatalog::WriteDefaultConfig() const {
std::ofstream ofs(m_configPath);
if (!ofs.is_open()) {
std::cerr << "[ItemFilterCatalog] Could not write default config to: "
<< m_configPath << "\n";
return;
}
ofs << kDefaultConfigJson;
std::cout << "[ItemFilterCatalog] Wrote default config to: " << m_configPath << "\n";
}
// -------------------------------------------------------------------------
// Classify
// -------------------------------------------------------------------------
std::string ItemFilterCatalog::Classify(const std::string& entityName,
const std::string& typeName,
const std::string& configName,
const std::string& modelName) const
{
const std::string normEntity = Normalize(entityName);
const std::string normType = Normalize(typeName);
const std::string normConfig = Normalize(configName);
const std::string normModel = Normalize(modelName);
for (const auto& filter : m_filters) {
// Skip the fallback entry during forward scanning.
if (filter.key == kFallbackKey) {
continue;
}
if (MatchesAny(filter.modelNames, normModel) ||
MatchesAny(filter.configNames, normConfig) ||
MatchesAny(filter.typeNames, normType) ||
MatchesAny(filter.names, normEntity))
{
return filter.key;
}
}
return kFallbackKey;
}
// -------------------------------------------------------------------------
// MatchesAny
// -------------------------------------------------------------------------
bool ItemFilterCatalog::MatchesAny(const std::vector<std::string>& patterns,
const std::string& value)
{
if (patterns.empty() || value.empty()) return false;
for (const auto& pattern : patterns) {
if (pattern.empty()) continue;
// Check if pattern is contained within value (substring match)
// or equals value exactly.
if (value.find(pattern) != std::string::npos) {
return true;
}
}
return false;
}
// -------------------------------------------------------------------------
// ParseStringOrArray
// -------------------------------------------------------------------------
std::vector<std::string> ItemFilterCatalog::ParseStringOrArray(
const nlohmann::json& j, const std::string& key)
{
if (!j.contains(key)) return {};
const auto& val = j[key];
if (val.is_string()) {
return { val.get<std::string>() };
}
if (val.is_array()) {
std::vector<std::string> result;
result.reserve(val.size());
for (const auto& elem : val) {
if (elem.is_string()) {
result.push_back(elem.get<std::string>());
}
}
return result;
}
return {};
}
// -------------------------------------------------------------------------
// Normalize
// -------------------------------------------------------------------------
std::string ItemFilterCatalog::Normalize(const std::string& value) {
// Trim leading/trailing whitespace, then convert to lowercase.
auto start = value.begin();
while (start != value.end() && std::isspace(static_cast<unsigned char>(*start))) {
++start;
}
auto end = value.end();
while (end != start) {
--end;
if (!std::isspace(static_cast<unsigned char>(*end))) {
++end;
break;
}
}
std::string result(start, end);
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
// -------------------------------------------------------------------------
// NormalizeAll
// -------------------------------------------------------------------------
std::vector<std::string> ItemFilterCatalog::NormalizeAll(
const std::vector<std::string>& values)
{
std::vector<std::string> result;
result.reserve(values.size());
for (const auto& v : values) {
result.push_back(Normalize(v));
}
return result;
}
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <string>
#include <vector>
#include <optional>
// nlohmann/json is included here only for the ParseStringOrArray helper.
// All other TUs that include this header will also pull in the JSON library,
// which is acceptable since nlohmann/json is header-only and already a project
// dependency.
#include <nlohmann/json.hpp>
struct ItemFilterDefinition {
std::string key;
std::string description;
std::optional<std::string> color;
std::vector<std::string> modelNames;
std::vector<std::string> configNames;
std::vector<std::string> typeNames;
std::vector<std::string> names;
};
// -------------------------------------------------------------------------
// ItemFilterCatalog
// Loads item-filter definitions from a JSON config file and classifies
// entities by matching their name/type/config/model strings against the
// patterns defined in each filter.
// -------------------------------------------------------------------------
class ItemFilterCatalog {
public:
/// Key used when no filter matches.
static constexpr const char* kFallbackKey = "isOtherLoot";
/// Default constructor — resolves config path relative to the executable.
ItemFilterCatalog();
/// Explicit config path constructor.
explicit ItemFilterCatalog(const std::string& configPath);
// Non-copyable, movable
ItemFilterCatalog(const ItemFilterCatalog&) = delete;
ItemFilterCatalog& operator=(const ItemFilterCatalog&) = delete;
ItemFilterCatalog(ItemFilterCatalog&&) = default;
ItemFilterCatalog& operator=(ItemFilterCatalog&&) = default;
/// Classify an entity against the loaded filters.
/// Returns the key of the first matching filter, or kFallbackKey.
std::string Classify(const std::string& entityName,
const std::string& typeName,
const std::string& configName,
const std::string& modelName) const;
[[nodiscard]] const std::vector<ItemFilterDefinition>& GetFilters() const {
return m_filters;
}
private:
std::vector<ItemFilterDefinition> m_filters;
std::string m_configPath;
void Load();
/// Returns true if any pattern is a substring of (or equals) value
/// after both are normalised to lowercase.
static bool MatchesAny(const std::vector<std::string>& patterns,
const std::string& value);
/// Parse a JSON field that may be either a string or an array of strings.
/// Returns an empty vector when the key is absent.
static std::vector<std::string> ParseStringOrArray(const nlohmann::json& j,
const std::string& key);
/// Trim whitespace and convert to lowercase.
static std::string Normalize(const std::string& value);
/// Apply Normalize to every element of a vector.
static std::vector<std::string> NormalizeAll(const std::vector<std::string>& values);
/// Write a minimal default config to m_configPath.
void WriteDefaultConfig() const;
};
+379
View File
@@ -0,0 +1,379 @@
#include "Readers/ItemListReader.h"
#include <algorithm>
#include <cstring>
#include "Offsets.h"
#include "RuntimeOffsets.h"
#include "Memory/MemoryValidation.h"
#include "Readers/ItemFilterCatalog.h"
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
ItemListReader::ItemListReader(ItemFilterCatalog& catalog,
int batchSize,
int readBudgetMs,
int staleAfterMs)
: m_filterCatalog(catalog)
, m_batchSize(batchSize)
, m_readBudget(readBudgetMs)
, m_staleAfter(staleAfterMs)
{}
// -------------------------------------------------------------------------
// TryRead
// -------------------------------------------------------------------------
bool ItemListReader::TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZItemListEntry>& items)
{
const uint32_t pid = session.processId;
// 1. Item list pointer
uint64_t itemListPtr = 0;
if (!mem.TryReadPointer(pid,
session.worldAddress + RuntimeOffsets::World::ItemList,
itemListPtr))
{
return false;
}
// 2. Alloc count (total slots) and valid count (live items)
int32_t rawAllocCount = 0;
int32_t rawValidCount = 0;
if (!mem.TryReadValue<int32_t>(pid,
session.worldAddress + RuntimeOffsets::World::ItemTableAllocCount,
rawAllocCount))
{
return false;
}
mem.TryReadValue<int32_t>(pid,
session.worldAddress + RuntimeOffsets::World::ItemTableValidCount,
rawValidCount);
const int allocCount = std::clamp(rawAllocCount, 0, kMaxItems);
const int safeValidCount = std::clamp(rawValidCount, 0, allocCount);
// 3. If cursor has reached the end, complete the current scan
if (m_cursorOffset >= allocCount) {
auto now = std::chrono::steady_clock::now();
CompleteScan(now);
PruneExpiredItems(now);
items = BuildResult();
return true;
}
// 4. Chunk size: max(batchSize, kMinTableChunkEntries), clamped to remaining
const int remaining = allocCount - m_cursorOffset;
const int chunkEntries = std::min(remaining,
std::max(m_batchSize, kMinTableChunkEntries));
// 5. Bulk-read chunk — each slot is Offsets::SlowTable::EntrySize bytes
const size_t entrySize = static_cast<size_t>(Offsets::SlowTable::EntrySize);
const size_t chunkBytes = static_cast<size_t>(chunkEntries) * entrySize;
const uint64_t chunkAddr = itemListPtr +
static_cast<uint64_t>(m_cursorOffset) * entrySize;
std::vector<uint8_t> tableBytes;
tableBytes.reserve(chunkBytes);
if (!mem.ReadBytes(pid, chunkAddr, chunkBytes, tableBytes)) {
m_cursorOffset += chunkEntries;
items = BuildResult();
return true;
}
// 6. Process entries within the read budget
const auto batchStart = std::chrono::steady_clock::now();
bool budgetExceeded = false;
for (int i = 0; i < chunkEntries; ++i) {
const uint8_t* slot = tableBytes.data() + static_cast<size_t>(i) * entrySize;
// Flag at offset 0 — uint16_t, valid == 1
uint16_t flag = 0;
std::memcpy(&flag, slot, sizeof(uint16_t));
if (flag != 1) continue;
// Entity pointer at offset 0x8
uint64_t itemAddr = 0;
std::memcpy(&itemAddr,
slot + static_cast<size_t>(Offsets::SlowTable::EntryPointerOffset),
sizeof(uint64_t));
if (!MemoryValidation::IsValidUserAddress(itemAddr)) continue;
++m_validEntriesSeenInScan;
auto now = std::chrono::steady_clock::now();
ProcessItem(mem, session, itemAddr, now);
// Stop early when all valid entries have been seen
if (safeValidCount > 0 && m_validEntriesSeenInScan >= safeValidCount) {
m_cursorOffset += (i + 1);
CompleteScan(now);
PruneExpiredItems(now);
items = BuildResult();
return true;
}
// Check time budget
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - batchStart);
if (elapsed >= m_readBudget) {
m_cursorOffset += (i + 1);
budgetExceeded = true;
break;
}
}
if (!budgetExceeded) {
m_cursorOffset += chunkEntries;
}
auto now = std::chrono::steady_clock::now();
PruneExpiredItems(now);
if (m_cursorOffset >= allocCount) {
CompleteScan(now);
}
items = BuildResult();
return true;
}
// -------------------------------------------------------------------------
// Reset
// -------------------------------------------------------------------------
void ItemListReader::Reset()
{
m_items.clear();
m_typeCache.Clear();
m_cachedResult.clear();
m_sortedAddresses.clear();
m_cursorOffset = 0;
m_validEntriesSeenInScan = 0;
m_scanGeneration = 1;
m_hasCompletedScan = false;
m_sortDirty = true;
m_resultDirty = true;
}
// -------------------------------------------------------------------------
// ProcessItem
// -------------------------------------------------------------------------
void ItemListReader::ProcessItem(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t itemAddr,
std::chrono::steady_clock::time_point now)
{
const uint32_t pid = session.processId;
// 1. Entity type pointer
uint64_t typeAddr = 0;
if (!mem.TryReadPointer(pid, itemAddr + Offsets::Common::Type, typeAddr)) {
auto it = m_items.find(itemAddr);
if (it != m_items.end()) {
++it->second.failedTypeReads;
it->second.lastSeenGeneration = m_scanGeneration;
}
return;
}
// 2. Type metadata (cached)
EntityTypeMetadata typeMeta;
if (!m_typeCache.GetOrRead(typeAddr, typeMeta)) {
typeMeta = ReadEntityTypeMetadata(mem, pid, typeAddr);
if (typeMeta.typeName.empty() && typeMeta.configName.empty() &&
typeMeta.cleanName.empty() && typeMeta.modelName.empty())
{
auto it = m_items.find(itemAddr);
if (it != m_items.end()) {
++it->second.failedTypeReads;
it->second.lastSeenGeneration = m_scanGeneration;
}
return;
}
m_typeCache.Set(typeAddr, typeMeta);
}
// 3. Position: VisualState ptr -> Vector3 at +Position
std::optional<Vector3> position;
uint64_t visualStateAddr = 0;
if (mem.TryReadPointer(pid, itemAddr + Offsets::Common::VisualState, visualStateAddr)) {
Vector3 pos{};
if (mem.TryReadValue<Vector3>(pid,
visualStateAddr + Offsets::Common::Position,
pos) &&
MemoryValidation::IsValidVector(pos))
{
position = pos;
}
}
// Use cached position if current read failed
auto& cached = m_items[itemAddr];
if (!position.has_value() && cached.entry.position.has_value()) {
position = cached.entry.position;
++cached.failedPositionReads;
} else if (!position.has_value()) {
++cached.failedPositionReads;
}
// 4. Build display name
const std::string entityName = GetBestItemName(typeMeta.cleanName,
typeMeta.configName,
typeMeta.modelName,
typeMeta.typeName,
itemAddr);
// 5. Classify via filter catalog
const std::string filterKey = m_filterCatalog.Classify(entityName,
typeMeta.typeName,
typeMeta.configName,
typeMeta.modelName);
// 6. Update cached entry
cached.entry.address = itemAddr;
cached.entry.position = position;
cached.entry.entityName = entityName;
cached.entry.cleanName = typeMeta.cleanName;
cached.entry.typeName = typeMeta.typeName;
cached.entry.configName = typeMeta.configName;
cached.entry.modelName = typeMeta.modelName;
cached.entry.filterKey = filterKey;
cached.lastSeenGeneration = m_scanGeneration;
cached.lastSeenTime = now;
cached.failedTypeReads = 0;
if (position.has_value()) {
cached.lastSuccessfulRead = now;
cached.failedPositionReads = 0;
}
m_resultDirty = true;
}
// -------------------------------------------------------------------------
// CompleteScan
// -------------------------------------------------------------------------
void ItemListReader::CompleteScan(std::chrono::steady_clock::time_point /*now*/)
{
for (auto it = m_items.begin(); it != m_items.end(); ) {
if (it->second.lastSeenGeneration != m_scanGeneration) {
it = m_items.erase(it);
m_sortDirty = true;
m_resultDirty = true;
} else {
++it;
}
}
m_cursorOffset = 0;
m_validEntriesSeenInScan = 0;
m_hasCompletedScan = true;
++m_scanGeneration;
}
// -------------------------------------------------------------------------
// PruneExpiredItems
// -------------------------------------------------------------------------
void ItemListReader::PruneExpiredItems(std::chrono::steady_clock::time_point now)
{
for (auto it = m_items.begin(); it != m_items.end(); ) {
const auto age = std::chrono::duration_cast<std::chrono::milliseconds>(
now - it->second.lastSuccessfulRead);
if (age > m_staleAfter) {
it = m_items.erase(it);
m_sortDirty = true;
m_resultDirty = true;
} else {
++it;
}
}
}
// -------------------------------------------------------------------------
// BuildResult
// -------------------------------------------------------------------------
std::vector<DayZItemListEntry>& ItemListReader::BuildResult()
{
if (!m_resultDirty) {
return m_cachedResult;
}
if (m_sortDirty) {
m_sortedAddresses.clear();
m_sortedAddresses.reserve(m_items.size());
for (const auto& kv : m_items) {
m_sortedAddresses.push_back(kv.first);
}
std::sort(m_sortedAddresses.begin(), m_sortedAddresses.end(),
[this](uint64_t a, uint64_t b) {
const auto& ea = m_items.at(a).entry;
const auto& eb = m_items.at(b).entry;
if (ea.entityName != eb.entityName)
return ea.entityName < eb.entityName;
return a < b;
});
m_sortDirty = false;
}
m_cachedResult.clear();
m_cachedResult.reserve(m_sortedAddresses.size());
for (uint64_t addr : m_sortedAddresses) {
auto it = m_items.find(addr);
if (it != m_items.end()) {
m_cachedResult.push_back(it->second.entry);
}
}
m_resultDirty = false;
return m_cachedResult;
}
// -------------------------------------------------------------------------
// GetBestItemName
// Priority: cleanName > configName > filename-from-modelName > typeName
// -------------------------------------------------------------------------
std::string ItemListReader::GetBestItemName(const std::string& cleanName,
const std::string& configName,
const std::string& modelName,
const std::string& typeName,
uint64_t itemAddress)
{
if (!cleanName.empty()) return cleanName;
if (!configName.empty()) return configName;
// Extract filename from model path (strip directory and extension)
if (!modelName.empty()) {
// Find last slash (forward or back)
const auto slashPos = modelName.find_last_of("/\\");
std::string filename = (slashPos != std::string::npos)
? modelName.substr(slashPos + 1)
: modelName;
// Strip extension
const auto dotPos = filename.rfind('.');
if (dotPos != std::string::npos) {
filename = filename.substr(0, dotPos);
}
if (!filename.empty()) return filename;
}
if (!typeName.empty()) return typeName;
// Last resort: hex address
char buf[32];
std::snprintf(buf, sizeof(buf), "0x%016llX",
static_cast<unsigned long long>(itemAddress));
return buf;
}
+97
View File
@@ -0,0 +1,97 @@
#pragma once
#include <vector>
#include <unordered_map>
#include <chrono>
#include <string>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
#include "Readers/EntityTypeCache.h"
// Forward declaration — full definition lives in ItemFilterCatalog.h
class ItemFilterCatalog;
// -------------------------------------------------------------------------
// ItemListReader
// Incremental reader for the world's item slow-table.
// Uses the same SlowTable wire format (0x18-byte slots, flag at +0x0,
// entity pointer at +0x8) as SlowEntityListReader.
// Only items whose entity name passes the ItemFilterCatalog are kept.
// -------------------------------------------------------------------------
class ItemListReader {
public:
explicit ItemListReader(ItemFilterCatalog& catalog,
int batchSize = 64,
int readBudgetMs = 10,
int staleAfterMs = 5000);
/// Incrementally read items into @p items.
/// Returns false only when the table pointer itself cannot be read.
bool TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZItemListEntry>& items);
/// Reset all state. Call when the game session restarts.
void Reset();
private:
// ------------------------------------------------------------------
// Per-item bookkeeping
// ------------------------------------------------------------------
struct CachedItem {
DayZItemListEntry entry;
int lastSeenGeneration = 0;
std::chrono::steady_clock::time_point lastSeenTime;
std::chrono::steady_clock::time_point lastSuccessfulRead;
int failedTypeReads = 0;
int failedPositionReads = 0;
};
// ------------------------------------------------------------------
// Internal helpers
// ------------------------------------------------------------------
void ProcessItem(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t itemAddr,
std::chrono::steady_clock::time_point now);
void CompleteScan(std::chrono::steady_clock::time_point now);
void PruneExpiredItems(std::chrono::steady_clock::time_point now);
std::vector<DayZItemListEntry>& BuildResult();
/// Choose the best display name for an item from its type strings.
/// Priority: cleanName > configName > filename-from-modelName > typeName.
static std::string GetBestItemName(const std::string& cleanName,
const std::string& configName,
const std::string& modelName,
const std::string& typeName,
uint64_t itemAddress);
// ------------------------------------------------------------------
// State
// ------------------------------------------------------------------
ItemFilterCatalog& m_filterCatalog;
std::unordered_map<uint64_t, CachedItem> m_items;
EntityTypeCache m_typeCache;
std::vector<DayZItemListEntry> m_cachedResult;
std::vector<uint64_t> m_sortedAddresses;
int m_cursorOffset = 0;
int m_validEntriesSeenInScan = 0;
int m_scanGeneration = 1;
bool m_hasCompletedScan = false;
bool m_sortDirty = true;
bool m_resultDirty = true;
int m_batchSize;
std::chrono::milliseconds m_readBudget;
std::chrono::milliseconds m_staleAfter;
static constexpr int kMinTableChunkEntries = 256;
static constexpr int kMaxItems = 4096;
};
+158
View File
@@ -0,0 +1,158 @@
#include "Readers/NearEntityListReader.h"
#include <algorithm>
#include <cstring>
#include "Offsets.h"
#include "RuntimeOffsets.h"
#include "Memory/MemoryValidation.h"
bool NearEntityListReader::TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZNearEntityEntry>& entries)
{
const uint32_t pid = session.processId;
// -----------------------------------------------------------------
// 1. Near-entity list base pointer
// -----------------------------------------------------------------
uint64_t nearListPtr = 0;
if (!mem.TryReadPointer(pid,
session.worldAddress + RuntimeOffsets::World::NearEntityList,
nearListPtr))
{
return false;
}
// -----------------------------------------------------------------
// 2. Table size
// -----------------------------------------------------------------
int32_t rawCount = 0;
if (!mem.TryReadValue<int32_t>(pid,
session.worldAddress + RuntimeOffsets::World::NearTableSize,
rawCount))
{
return false;
}
const int count = std::clamp(rawCount, 0, kMaxEntities);
if (count == 0) {
entries.clear();
return true;
}
// -----------------------------------------------------------------
// 3. Bulk-read pointer table (count * 8 bytes)
// -----------------------------------------------------------------
std::vector<uint8_t> tableBytes;
tableBytes.reserve(static_cast<size_t>(count) * 8);
if (!mem.ReadBytes(pid, nearListPtr, static_cast<size_t>(count) * 8, tableBytes)) {
return false;
}
// Decode entity addresses and filter invalid ones.
struct EntitySlot {
uint64_t entityAddr;
int tableIndex;
};
std::vector<EntitySlot> slots;
slots.reserve(static_cast<size_t>(count));
for (int i = 0; i < count; ++i) {
uint64_t addr = 0;
std::memcpy(&addr, tableBytes.data() + static_cast<size_t>(i) * 8, 8);
if (MemoryValidation::IsValidUserAddress(addr))
slots.push_back({ addr, i });
}
if (slots.empty()) {
entries.clear();
return true;
}
// -----------------------------------------------------------------
// 4. Scatter pass A — read Type pointer + VisualState pointer + NetworkId
// for all valid entities in one DMA round-trip.
// -----------------------------------------------------------------
struct PerEntity {
uint64_t typeAddr = 0;
uint64_t visualStatePtr = 0;
uint32_t networkId = 0;
uint32_t _pad = 0;
};
std::vector<PerEntity> perEntity(slots.size());
{
std::vector<VmmAccessor::ScatterEntry> scat;
scat.reserve(slots.size() * 3);
for (size_t i = 0; i < slots.size(); ++i) {
uint64_t base = slots[i].entityAddr;
scat.push_back({ base + Offsets::Common::Type,
&perEntity[i].typeAddr, sizeof(uint64_t) });
scat.push_back({ base + RuntimeOffsets::Common::VisualState,
&perEntity[i].visualStatePtr, sizeof(uint64_t) });
scat.push_back({ base + Offsets::Network::EntityNetworkId,
&perEntity[i].networkId, sizeof(uint32_t) });
}
mem.ScatterRead(pid, scat);
}
// -----------------------------------------------------------------
// 5. Scatter pass B — read position (Vector3) from each VisualState
// -----------------------------------------------------------------
std::vector<Vector3> positions(slots.size(), Vector3{});
{
std::vector<VmmAccessor::ScatterEntry> scat;
scat.reserve(slots.size());
for (size_t i = 0; i < slots.size(); ++i) {
if (!MemoryValidation::IsValidUserAddress(perEntity[i].visualStatePtr))
continue;
scat.push_back({ perEntity[i].visualStatePtr + Offsets::Common::Position,
&positions[i], sizeof(Vector3) });
}
mem.ScatterRead(pid, scat);
}
// -----------------------------------------------------------------
// 6. Assemble entries
// -----------------------------------------------------------------
entries.clear();
entries.reserve(slots.size());
for (size_t i = 0; i < slots.size(); ++i) {
if (!MemoryValidation::IsValidUserAddress(perEntity[i].typeAddr))
continue;
EntityTypeMetadata typeMeta;
if (!m_typeCache.GetOrRead(perEntity[i].typeAddr, typeMeta)) {
typeMeta = ReadEntityTypeMetadata(mem, pid, perEntity[i].typeAddr);
m_typeCache.Set(perEntity[i].typeAddr, typeMeta);
}
std::optional<Vector3> position;
if (MemoryValidation::IsValidVector(positions[i]))
position = positions[i];
DayZNearEntityEntry entry;
entry.address = slots[i].entityAddr;
entry.networkId = perEntity[i].networkId;
entry.position = position;
entry.headingDegrees = std::nullopt;
entry.typeName = typeMeta.typeName;
entry.configName = typeMeta.configName;
entry.modelName = typeMeta.modelName;
entry.entityName = GetBestEntityName(typeMeta.typeName,
typeMeta.configName,
typeMeta.cleanName,
typeMeta.modelName,
slots[i].entityAddr);
entries.push_back(std::move(entry));
}
return true;
}
void NearEntityListReader::Reset()
{
m_typeCache.Clear();
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <vector>
#include <string>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
#include "Readers/EntityTypeCache.h"
// -------------------------------------------------------------------------
// NearEntityListReader
// Reads the world's near-entity pointer table and builds a flat list of
// DayZNearEntityEntry values. Entity type metadata is cached across calls
// via an internal EntityTypeCache.
// -------------------------------------------------------------------------
class NearEntityListReader {
public:
/// Read the current near-entity list into @p entries.
/// Replaces the contents of @p entries on each successful call.
/// Returns false only when the initial table reads fail entirely.
bool TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZNearEntityEntry>& entries);
/// Clear cached type metadata. Call when the game session restarts.
void Reset();
private:
static constexpr int kMaxEntities = 4096;
EntityTypeCache m_typeCache;
};
+336
View File
@@ -0,0 +1,336 @@
#include "Readers/SlowEntityListReader.h"
#include <algorithm>
#include <cstring>
#include "Offsets.h"
#include "Memory/MemoryValidation.h"
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
SlowEntityListReader::SlowEntityListReader(int batchSize,
int readBudgetMs,
int staleAfterMs)
: m_batchSize(batchSize)
, m_readBudget(readBudgetMs)
, m_staleAfter(staleAfterMs)
{}
// -------------------------------------------------------------------------
// TryRead
// -------------------------------------------------------------------------
bool SlowEntityListReader::TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZSlowEntityEntry>& entries)
{
const uint32_t pid = session.processId;
// 1. Slow-entity list pointer
uint64_t slowListPtr = 0;
if (!mem.TryReadPointer(pid,
session.worldAddress + Offsets::World::SlowEntityList,
slowListPtr))
{
return false;
}
// 2. Alloc count (total slots) and valid count (live entities)
int32_t rawAllocCount = 0;
int32_t rawValidCount = 0;
if (!mem.TryReadValue<int32_t>(pid,
session.worldAddress + Offsets::World::SlowTableAllocCount,
rawAllocCount))
{
return false;
}
mem.TryReadValue<int32_t>(pid,
session.worldAddress + Offsets::World::SlowTableValidCount,
rawValidCount);
const int allocCount = std::clamp(rawAllocCount, 0, kMaxEntities);
const int safeValidCount = std::clamp(rawValidCount, 0, allocCount);
// 3. If cursor has reached the end of the table, complete the current scan
if (m_cursorOffset >= allocCount) {
auto now = std::chrono::steady_clock::now();
CompleteScan(now);
PruneExpiredEntities(now);
entries = BuildResult();
return true;
}
// 4. Chunk size: max(batchSize, kMinTableChunkEntries) entries, clamped to remaining
const int remaining = allocCount - m_cursorOffset;
const int chunkEntries = std::min(remaining,
std::max(m_batchSize, kMinTableChunkEntries));
// 5. Bulk-read chunk — each slot is Offsets::SlowTable::EntrySize (0x18) bytes
const size_t entrySize = static_cast<size_t>(Offsets::SlowTable::EntrySize);
const size_t chunkBytes = static_cast<size_t>(chunkEntries) * entrySize;
const uint64_t chunkAddr = slowListPtr +
static_cast<uint64_t>(m_cursorOffset) * entrySize;
std::vector<uint8_t> tableBytes;
tableBytes.reserve(chunkBytes);
if (!mem.ReadBytes(pid, chunkAddr, chunkBytes, tableBytes)) {
m_cursorOffset += chunkEntries;
entries = BuildResult();
return true;
}
// 6. Process entries within the read budget
const auto batchStart = std::chrono::steady_clock::now();
bool budgetExceeded = false;
for (int i = 0; i < chunkEntries; ++i) {
const uint8_t* slot = tableBytes.data() + static_cast<size_t>(i) * entrySize;
// Flag at offset 0 — uint16_t, valid == 1
uint16_t flag = 0;
std::memcpy(&flag, slot, sizeof(uint16_t));
if (flag != 1) continue;
// Entity pointer at offset 0x8
uint64_t entityAddr = 0;
std::memcpy(&entityAddr,
slot + static_cast<size_t>(Offsets::SlowTable::EntryPointerOffset),
sizeof(uint64_t));
if (!MemoryValidation::IsValidUserAddress(entityAddr)) continue;
++m_validEntriesSeenInScan;
auto now = std::chrono::steady_clock::now();
ProcessEntity(mem, session, entityAddr, now);
// Stop early when we have seen all valid entries
if (safeValidCount > 0 && m_validEntriesSeenInScan >= safeValidCount) {
m_cursorOffset += (i + 1);
CompleteScan(now);
PruneExpiredEntities(now);
entries = BuildResult();
return true;
}
// Check time budget
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - batchStart);
if (elapsed >= m_readBudget) {
m_cursorOffset += (i + 1);
budgetExceeded = true;
break;
}
}
if (!budgetExceeded) {
m_cursorOffset += chunkEntries;
}
auto now = std::chrono::steady_clock::now();
PruneExpiredEntities(now);
if (m_cursorOffset >= allocCount) {
CompleteScan(now);
}
entries = BuildResult();
return true;
}
// -------------------------------------------------------------------------
// Reset
// -------------------------------------------------------------------------
void SlowEntityListReader::Reset()
{
m_entities.clear();
m_typeCache.Clear();
m_cachedResult.clear();
m_sortedAddresses.clear();
m_cursorOffset = 0;
m_validEntriesSeenInScan = 0;
m_scanGeneration = 1;
m_hasCompletedScan = false;
m_sortDirty = true;
m_resultDirty = true;
}
// -------------------------------------------------------------------------
// ProcessEntity
// -------------------------------------------------------------------------
void SlowEntityListReader::ProcessEntity(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t entityAddr,
std::chrono::steady_clock::time_point now)
{
const uint32_t pid = session.processId;
// Read entity type pointer
uint64_t typeAddr = 0;
if (!mem.TryReadPointer(pid, entityAddr + Offsets::Common::Type, typeAddr)) {
auto it = m_entities.find(entityAddr);
MarkTypeReadFailure(entityAddr,
(it != m_entities.end()) ? &it->second : nullptr,
now);
return;
}
// Look up or read type metadata
EntityTypeMetadata typeMeta;
if (!m_typeCache.GetOrRead(typeAddr, typeMeta)) {
typeMeta = ReadEntityTypeMetadata(mem, pid, typeAddr);
if (typeMeta.typeName.empty() && typeMeta.configName.empty() &&
typeMeta.cleanName.empty() && typeMeta.modelName.empty())
{
auto it = m_entities.find(entityAddr);
MarkTypeReadFailure(entityAddr,
(it != m_entities.end()) ? &it->second : nullptr,
now);
return;
}
m_typeCache.Set(typeAddr, typeMeta);
}
// Skip entities with no model — slow list contains many invisible objects
if (typeMeta.modelName.empty()) {
return;
}
// Transform
std::optional<Vector3> position;
std::optional<float> heading;
bool transformOk = ReadEntityTransform(mem, session, entityAddr, position, heading);
// Retrieve or create cached entry
auto& cached = m_entities[entityAddr];
if (!transformOk && cached.entry.position.has_value()) {
position = cached.entry.position;
heading = cached.entry.headingDegrees;
}
// Build display name: configName first, then cleanName, typeName, modelName
const std::string entityName = GetBestEntityName(typeMeta.configName,
typeMeta.cleanName,
typeMeta.typeName,
typeMeta.modelName,
entityAddr);
// Update cached entry
cached.entry.address = entityAddr;
cached.entry.position = position;
cached.entry.headingDegrees = heading;
cached.entry.entityName = entityName;
cached.entry.typeName = typeMeta.typeName;
cached.entry.configName = typeMeta.configName;
cached.entry.modelName = typeMeta.modelName;
cached.lastSeenGeneration = m_scanGeneration;
cached.failedTypeReads = 0;
if (transformOk) {
cached.lastSuccessfulRead = now;
}
m_resultDirty = true;
}
// -------------------------------------------------------------------------
// CompleteScan
// -------------------------------------------------------------------------
void SlowEntityListReader::CompleteScan(std::chrono::steady_clock::time_point /*now*/)
{
// Remove entities not seen in the current scan generation
for (auto it = m_entities.begin(); it != m_entities.end(); ) {
if (it->second.lastSeenGeneration != m_scanGeneration) {
it = m_entities.erase(it);
m_sortDirty = true;
m_resultDirty = true;
} else {
++it;
}
}
m_cursorOffset = 0;
m_validEntriesSeenInScan = 0;
m_hasCompletedScan = true;
++m_scanGeneration;
}
// -------------------------------------------------------------------------
// PruneExpiredEntities
// -------------------------------------------------------------------------
void SlowEntityListReader::PruneExpiredEntities(std::chrono::steady_clock::time_point now)
{
for (auto it = m_entities.begin(); it != m_entities.end(); ) {
const auto age = std::chrono::duration_cast<std::chrono::milliseconds>(
now - it->second.lastSuccessfulRead);
if (age > m_staleAfter) {
it = m_entities.erase(it);
m_sortDirty = true;
m_resultDirty = true;
} else {
++it;
}
}
}
// -------------------------------------------------------------------------
// BuildResult
// -------------------------------------------------------------------------
std::vector<DayZSlowEntityEntry>& SlowEntityListReader::BuildResult()
{
if (!m_resultDirty) {
return m_cachedResult;
}
if (m_sortDirty) {
m_sortedAddresses.clear();
m_sortedAddresses.reserve(m_entities.size());
for (const auto& kv : m_entities) {
m_sortedAddresses.push_back(kv.first);
}
std::sort(m_sortedAddresses.begin(), m_sortedAddresses.end(),
[this](uint64_t a, uint64_t b) {
const auto& ea = m_entities.at(a).entry;
const auto& eb = m_entities.at(b).entry;
if (ea.entityName != eb.entityName)
return ea.entityName < eb.entityName;
return a < b;
});
m_sortDirty = false;
}
m_cachedResult.clear();
m_cachedResult.reserve(m_sortedAddresses.size());
for (uint64_t addr : m_sortedAddresses) {
auto it = m_entities.find(addr);
if (it != m_entities.end()) {
m_cachedResult.push_back(it->second.entry);
}
}
m_resultDirty = false;
return m_cachedResult;
}
// -------------------------------------------------------------------------
// MarkTypeReadFailure
// -------------------------------------------------------------------------
void SlowEntityListReader::MarkTypeReadFailure(uint64_t /*entityAddr*/,
CachedEntity* cached,
std::chrono::steady_clock::time_point /*now*/)
{
if (cached == nullptr) return;
++cached->failedTypeReads;
// Still mark it seen this generation so it isn't pruned by CompleteScan
cached->lastSeenGeneration = m_scanGeneration;
}
+86
View File
@@ -0,0 +1,86 @@
#pragma once
#include <vector>
#include <unordered_map>
#include <chrono>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
#include "Readers/EntityTypeCache.h"
// -------------------------------------------------------------------------
// SlowEntityListReader
// Incremental reader for the world's slow-entity table.
// Entries use the SlowTable format: 0x18-byte slots, uint16_t flag at +0x0
// (valid == 1), entity pointer at +0x8.
// Processing stops early when validEntriesSeenInScan reaches safeValidCount.
// -------------------------------------------------------------------------
class SlowEntityListReader {
public:
SlowEntityListReader(int batchSize = 64,
int readBudgetMs = 5,
int staleAfterMs = 5000);
/// Incrementally read slow entities into @p entries.
/// Returns false only when the table pointer itself cannot be read.
bool TryRead(VmmAccessor& mem,
const RuntimeSession& session,
std::vector<DayZSlowEntityEntry>& entries);
/// Reset all state. Call when the game session restarts.
void Reset();
private:
// ------------------------------------------------------------------
// Per-entity bookkeeping
// ------------------------------------------------------------------
struct CachedEntity {
DayZSlowEntityEntry entry;
int lastSeenGeneration = 0;
std::chrono::steady_clock::time_point lastSuccessfulRead;
int failedTypeReads = 0;
};
// ------------------------------------------------------------------
// Internal helpers
// ------------------------------------------------------------------
void ProcessEntity(VmmAccessor& mem,
const RuntimeSession& session,
uint64_t entityAddr,
std::chrono::steady_clock::time_point now);
void CompleteScan(std::chrono::steady_clock::time_point now);
void PruneExpiredEntities(std::chrono::steady_clock::time_point now);
std::vector<DayZSlowEntityEntry>& BuildResult();
void MarkTypeReadFailure(uint64_t entityAddr,
CachedEntity* cached,
std::chrono::steady_clock::time_point now);
// ------------------------------------------------------------------
// State
// ------------------------------------------------------------------
std::unordered_map<uint64_t, CachedEntity> m_entities;
EntityTypeCache m_typeCache;
std::vector<DayZSlowEntityEntry> m_cachedResult;
std::vector<uint64_t> m_sortedAddresses;
int m_cursorOffset = 0;
int m_validEntriesSeenInScan = 0;
int m_scanGeneration = 1;
bool m_hasCompletedScan = false;
bool m_sortDirty = true;
bool m_resultDirty = true;
int m_batchSize;
std::chrono::milliseconds m_readBudget;
std::chrono::milliseconds m_staleAfter;
// Minimum number of table entries to read per chunk (ensures we always
// make meaningful progress even with a small batchSize).
static constexpr int kMinTableChunkEntries = 256;
static constexpr int kMaxEntities = 4096;
};
+99
View File
@@ -0,0 +1,99 @@
#include "Resolvers/BaseObjectResolver.h"
#include "Offsets.h"
#include "RuntimeOffsets.h"
#include "Memory/MemoryValidation.h"
#include <spdlog/spdlog.h>
bool BaseObjectResolver::TryResolve(VmmAccessor& mem,
uint32_t pid,
uint64_t gameBase,
RuntimeSession& session,
NetworkMetadata& metadata,
LocalPlayerData& player)
{
// -----------------------------------------------------------------
// 1. Validate the game base address
// -----------------------------------------------------------------
if (!MemoryValidation::IsValidUserAddress(gameBase)) {
m_lastFailureReason = "gameBase is not a valid user address";
return false;
}
// -----------------------------------------------------------------
// 2. World pointer (indirect read)
// -----------------------------------------------------------------
uint64_t worldAddress = 0;
if (!mem.TryReadPointer(pid, gameBase + RuntimeOffsets::Base::World, worldAddress)) {
m_lastFailureReason = "Failed to read World pointer from gameBase";
return false;
}
// -----------------------------------------------------------------
// 3. NetworkManager address (direct offset, NOT a pointer read)
// -----------------------------------------------------------------
uint64_t networkManagerAddress = gameBase + RuntimeOffsets::Base::NetworkManager;
if (!MemoryValidation::IsValidUserAddress(networkManagerAddress)) {
m_lastFailureReason = "Computed networkManagerAddress is not a valid user address";
return false;
}
// -----------------------------------------------------------------
// 4. Build session
// -----------------------------------------------------------------
session = RuntimeSession{
.processId = pid,
.gameBaseAddress = gameBase,
.worldAddress = worldAddress,
.networkManagerAddress = networkManagerAddress,
};
// -----------------------------------------------------------------
// 5. Network metadata (non-fatal — null NetworkClient is normal while
// the game is loading or in the main menu; the live loop retries it)
// -----------------------------------------------------------------
if (!m_networkMetadataReader.TryRead(mem, session, metadata)) {
spdlog::debug("BaseObjectResolver: NetworkMetadata unavailable ({}); proceeding.",
m_networkMetadataReader.LastFailureReason());
}
// -----------------------------------------------------------------
// 6. Local player — required to confirm the player is in-game
// -----------------------------------------------------------------
if (!m_localPlayerResolver.TryResolve(mem, session, player)) {
m_lastFailureReason = "LocalPlayerResolver failed: "
+ m_localPlayerResolver.LastFailureReason();
return false;
}
m_lastFailureReason.clear();
return true;
}
bool BaseObjectResolver::IsStillValid(VmmAccessor& mem, const RuntimeSession& session)
{
const uint32_t pid = session.processId;
// Re-read world pointer and compare
uint64_t currentWorld = 0;
if (!mem.TryReadPointer(pid,
session.gameBaseAddress + RuntimeOffsets::Base::World,
currentWorld))
{
return false;
}
if (currentWorld != session.worldAddress) {
return false;
}
// Re-compute network manager and compare
uint64_t currentNetworkManager = session.gameBaseAddress + RuntimeOffsets::Base::NetworkManager;
if (currentNetworkManager != session.networkManagerAddress) {
return false;
}
return true;
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <string>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
#include "Resolvers/LocalPlayerResolver.h"
#include "Resolvers/NetworkMetadataReader.h"
// -------------------------------------------------------------------------
// BaseObjectResolver
// Entry-point resolver that:
// - validates the game base address
// - resolves worldAddress and networkManagerAddress
// - constructs a RuntimeSession
// - delegates to NetworkMetadataReader and LocalPlayerResolver
// -------------------------------------------------------------------------
class BaseObjectResolver {
public:
/// Build a fresh RuntimeSession and populate metadata and player data.
/// Returns true only when all sub-resolvers succeed.
bool TryResolve(VmmAccessor& mem,
uint32_t pid,
uint64_t gameBase,
RuntimeSession& session,
NetworkMetadata& metadata,
LocalPlayerData& player);
/// Validate that the currently-cached session is still coherent
/// (world pointer and network manager address have not changed).
bool IsStillValid(VmmAccessor& mem, const RuntimeSession& session);
[[nodiscard]] const std::string& LastFailureReason() const noexcept {
return m_lastFailureReason;
}
private:
NetworkMetadataReader m_networkMetadataReader;
LocalPlayerResolver m_localPlayerResolver;
std::string m_lastFailureReason;
};
+98
View File
@@ -0,0 +1,98 @@
#include "Resolvers/LocalPlayerResolver.h"
#include "Offsets.h"
#include "Memory/MemoryValidation.h"
bool LocalPlayerResolver::TryResolve(VmmAccessor& mem,
const RuntimeSession& session,
LocalPlayerData& player)
{
const uint32_t pid = session.processId;
// -----------------------------------------------------------------
// 1. Read the local player pointer from the world object
// -----------------------------------------------------------------
uint64_t localPlayerPointer = 0;
if (!mem.TryReadPointer(pid,
session.worldAddress + Offsets::World::LocalPlayer,
localPlayerPointer))
{
m_lastFailureReason = "Failed to read LocalPlayer pointer from world";
return false;
}
// -----------------------------------------------------------------
// 2. Second hop — the actual entity sits 0xA8 bytes before the
// pointed-to structure.
// -----------------------------------------------------------------
uint64_t secondHop = 0;
if (!mem.TryReadValue<uint64_t>(pid, localPlayerPointer + 0x8, secondHop)) {
m_lastFailureReason = "Failed to read secondHop from LocalPlayer";
return false;
}
if (secondHop <= 0xA8) {
m_lastFailureReason = "secondHop value too small";
return false;
}
uint64_t chainedEntity = secondHop - 0xA8;
if (!MemoryValidation::IsValidUserAddress(chainedEntity)) {
m_lastFailureReason = "chainedEntity address is not a valid user address";
return false;
}
// -----------------------------------------------------------------
// 3. VisualState pointer
// -----------------------------------------------------------------
uint64_t visualStateAddr = 0;
if (!mem.TryReadPointer(pid,
chainedEntity + Offsets::Common::VisualState,
visualStateAddr))
{
m_lastFailureReason = "Failed to read VisualState pointer";
return false;
}
// -----------------------------------------------------------------
// 4. Position
// -----------------------------------------------------------------
Vector3 pos{};
if (!mem.TryReadValue<Vector3>(pid,
visualStateAddr + Offsets::Common::Position,
pos))
{
m_lastFailureReason = "Failed to read position from VisualState";
return false;
}
if (!MemoryValidation::IsValidVector(pos)) {
m_lastFailureReason = "Position vector is not valid";
return false;
}
// -----------------------------------------------------------------
// 5. Heading from direction vector
// -----------------------------------------------------------------
float dirX = 0.0f;
float dirY = 0.0f;
mem.TryReadValue<float>(pid, visualStateAddr + Offsets::Common::DirectionX, dirX);
mem.TryReadValue<float>(pid, visualStateAddr + Offsets::Common::DirectionY, dirY);
float heading = 0.0f;
MemoryValidation::TryGetCorrectedHeadingFromDirection(dirX, dirY, heading);
// -----------------------------------------------------------------
// 6. Commit
// -----------------------------------------------------------------
player = LocalPlayerData{
.entityAddress = chainedEntity,
.visualStateAddress = visualStateAddr,
.position = pos,
.lookDirection = heading,
};
m_lastFailureReason.clear();
return true;
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <string>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
// -------------------------------------------------------------------------
// LocalPlayerResolver
// Resolves the local player entity from the world's LocalPlayer pointer
// and reads its transform (position + heading).
// -------------------------------------------------------------------------
class LocalPlayerResolver {
public:
/// Attempt to resolve and populate @p player.
/// Returns true only when all required reads succeed and the position
/// passes the IsValidVector check.
bool TryResolve(VmmAccessor& mem, const RuntimeSession& session, LocalPlayerData& player);
[[nodiscard]] const std::string& LastFailureReason() const noexcept {
return m_lastFailureReason;
}
private:
std::string m_lastFailureReason;
};
+109
View File
@@ -0,0 +1,109 @@
#include "Resolvers/NetworkMetadataReader.h"
#include "Offsets.h"
#include "Memory/MemoryValidation.h"
#include <cstring>
#include <spdlog/spdlog.h>
bool NetworkMetadataReader::TryRead(VmmAccessor& mem,
const RuntimeSession& session,
NetworkMetadata& meta)
{
const uint32_t pid = session.processId;
// -----------------------------------------------------------------
// 1. NetworkClient pointer
// -----------------------------------------------------------------
uint64_t probeAddr = session.networkManagerAddress + Offsets::Network::ManagerNetworkClient;
spdlog::trace("NetworkMetadataReader: reading NetworkClient ptr at 0x{:X} "
"(networkManager=0x{:X} + 0x{:X})",
probeAddr,
session.networkManagerAddress,
Offsets::Network::ManagerNetworkClient);
uint64_t networkClientAddr = 0;
if (!mem.TryReadPointer(pid, probeAddr, networkClientAddr))
{
// Read the raw 8 bytes to see what value was there (may be 0 or an
// out-of-range pointer that failed IsValidUserAddress).
std::vector<uint8_t> raw;
if (mem.ReadBytes(pid, probeAddr, sizeof(uint64_t), raw) && raw.size() == 8) {
uint64_t rawVal = 0;
std::memcpy(&rawVal, raw.data(), 8);
spdlog::debug("NetworkMetadataReader: NetworkClient ptr invalid "
"(raw=0x{:X}) at 0x{:X}", rawVal, probeAddr);
} else {
spdlog::debug("NetworkMetadataReader: NetworkClient ptr unreadable at 0x{:X}",
probeAddr);
}
m_lastFailureReason = "Failed to read NetworkClient pointer";
return false;
}
spdlog::debug("NetworkMetadataReader: NetworkClient=0x{:X}", networkClientAddr);
// -----------------------------------------------------------------
// 2. Server name (ptr at networkClient + offset, then ArmaString)
// -----------------------------------------------------------------
uint64_t serverNamePtr = 0;
if (!mem.TryReadPointer(pid,
networkClientAddr + Offsets::Network::ServerName,
serverNamePtr))
{
m_lastFailureReason = "Failed to read ServerName pointer";
return false;
}
std::string serverName = mem.ReadArmaString(pid, serverNamePtr);
if (!MemoryValidation::IsUsableString(serverName)) {
m_lastFailureReason = "ServerName is not a usable string";
return false;
}
// -----------------------------------------------------------------
// 3. Game version (ptr at networkClient + offset, then ArmaString)
// -----------------------------------------------------------------
uint64_t gameVersionPtr = 0;
if (!mem.TryReadPointer(pid,
networkClientAddr + Offsets::Network::GameVersion,
gameVersionPtr))
{
m_lastFailureReason = "Failed to read GameVersion pointer";
return false;
}
std::string gameVersion = mem.ReadArmaString(pid, gameVersionPtr);
if (!MemoryValidation::IsUsableString(gameVersion)) {
m_lastFailureReason = "GameVersion is not a usable string";
return false;
}
// -----------------------------------------------------------------
// 4. Map name (optional — failure is non-fatal)
// -----------------------------------------------------------------
std::optional<std::string> mapName;
{
uint64_t mapNamePtr = 0;
if (mem.TryReadPointer(pid,
networkClientAddr + Offsets::Network::MapName,
mapNamePtr))
{
std::string raw = mem.ReadArmaString(pid, mapNamePtr);
if (MemoryValidation::IsUsableString(raw)) {
mapName = std::move(raw);
}
}
}
// -----------------------------------------------------------------
// 5. Commit
// -----------------------------------------------------------------
meta = NetworkMetadata{
.serverName = std::move(serverName),
.gameVersion = std::move(gameVersion),
.serverMapName = std::move(mapName),
};
m_lastFailureReason.clear();
return true;
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <string>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
// -------------------------------------------------------------------------
// NetworkMetadataReader
// Reads server name, game version, and map name from the game's network
// manager structures.
// -------------------------------------------------------------------------
class NetworkMetadataReader {
public:
/// Attempt to read network metadata into @p meta.
/// Returns true only when serverName and gameVersion are both usable.
bool TryRead(VmmAccessor& mem, const RuntimeSession& session, NetworkMetadata& meta);
[[nodiscard]] const std::string& LastFailureReason() const noexcept {
return m_lastFailureReason;
}
private:
std::string m_lastFailureReason;
};
File diff suppressed because it is too large Load Diff
+296
View File
@@ -0,0 +1,296 @@
#pragma once
#include <atomic>
#include <chrono>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "Core/Models.h"
#include "Core/RuntimeSession.h"
#include "Memory/VmmAccessor.h"
#include "Readers/BulletTableReader.h"
#include "Readers/ClientScoreboardReader.h"
#include "Readers/EntityCategoryProjector.h"
#include "Readers/FarEntityListReader.h"
#include "Readers/ItemFilterCatalog.h"
#include "Readers/ItemListReader.h"
#include "Readers/NearEntityListReader.h"
#include "Readers/SlowEntityListReader.h"
#include "Resolvers/BaseObjectResolver.h"
#include "Resolvers/LocalPlayerResolver.h"
#include "Resolvers/NetworkMetadataReader.h"
#include "SigScanner/SigScanner.h"
// -------------------------------------------------------------------------
// RuntimeUpdate
// Snapshot of all reader state delivered to the callback on each cycle.
// -------------------------------------------------------------------------
struct RuntimeUpdate {
bool areBaseObjectsReady = false;
std::string status;
std::optional<std::string> serverName;
std::optional<std::string> gameVersion;
std::optional<std::string> serverMapName;
std::optional<uint64_t> localPlayerEntityAddress;
std::optional<Vector3> localPlayerPosition;
std::optional<float> localPlayerLookDirection;
std::vector<DayZScoreboardPlayer> scoreboardPlayers;
std::vector<DayZPlayerEntry> players;
std::vector<DayZAnimalEntry> animals;
std::vector<DayZZombieEntry> zombies;
std::vector<DayZCarAndBoatEntry> carsAndBoats;
std::vector<DayZOtherEntityEntry> otherEntities;
std::vector<DayZBulletEntry> bullets;
std::vector<DayZItemListEntry> items;
std::vector<DayZNearEntityEntry> nearEntities;
std::vector<DayZFarEntityEntry> farEntities;
std::vector<DayZSlowEntityEntry> slowEntities;
std::optional<CameraData> camera;
};
// -------------------------------------------------------------------------
// RuntimeConfig
// Tunable parameters for the service.
// -------------------------------------------------------------------------
struct RuntimeConfig {
std::string processName = "DayZ_x64.exe";
bool useMemoryMap = true; // passed to VolkDMA; auto-generates memory_map.txt
int networkMetadataRefreshMs = 30000;
int scoreboardRefreshMs = 30000;
int nearEntityRefreshMs = 20; // was 50 — 50 Hz keeps boxes smooth in combat
int farEntityRefreshMs = 50; // was 100
int slowEntityRefreshMs = 100;
int bulletRefreshMs = 50;
int itemRefreshMs = 200;
int playersRefreshMs = 16; // was 50 — matches bone-scatter cadence (60 Hz rebuild)
int liveLoopIdleMs = 1;
int reconnectThreshold = 5;
int reconnectGracePeriodMs = 5000;
int farEntityBatchSize = 64;
int slowEntityBatchSize = 64;
int itemBatchSize = 64;
int boneRefreshMs = 4; // bone-scatter cadence — 4 ms = 250 Hz
float bonePlayerMaxDist = 500.0f; // players beyond this skip bone reads
float boneZombieMaxDist = 250.0f; // zombies beyond this skip bone reads
int vmmRefreshMs = 30000; // VMMDLL_OPT_REFRESH_ALL cadence — must be called
// periodically during the live loop or the page cache
// goes stale and reads silently return garbage (~1 h)
float itemSpatialFilterRadius = 300.0f; // items beyond this distance from the local player are
// excluded from the published snapshot entirely
/// Load from an INI-style config.cfg (key = value, # comments).
/// Missing keys keep their default values.
static RuntimeConfig Load(const std::string& path);
};
// -------------------------------------------------------------------------
// DayZRuntimeService
// Background service that attaches to DayZ, reads all entity/bullet/item
// data in a throttled loop, and delivers snapshots via a callback.
// -------------------------------------------------------------------------
class DayZRuntimeService {
public:
using UpdateCallback = std::function<void(const RuntimeUpdate&)>;
explicit DayZRuntimeService(RuntimeConfig config = {});
~DayZRuntimeService();
// Non-copyable, non-movable (owns threads + mutexes)
DayZRuntimeService(const DayZRuntimeService&) = delete;
DayZRuntimeService& operator=(const DayZRuntimeService&) = delete;
DayZRuntimeService(DayZRuntimeService&&) = delete;
DayZRuntimeService& operator=(DayZRuntimeService&&) = delete;
/// Start the background thread. Safe to call only once.
void Start();
/// Signal the background thread to stop and join it.
void Stop();
/// Thread-safe snapshot of the most recent entity update (50 ms cadence).
[[nodiscard]] std::shared_ptr<const RuntimeUpdate> GetLatestUpdate() const;
/// Lightweight camera read — fills out with the latest camera data.
/// Updated every background loop tick (~1 ms), completely separate from the
/// entity snapshot so the overlay always gets the freshest camera.
/// Returns false if no valid camera is available yet.
bool GetLatestCamera(CameraData& out) const;
/// Register a callback invoked (from the background thread) on each update.
void SetUpdateCallback(UpdateCallback cb);
private:
// ------------------------------------------------------------------
// Configuration
// ------------------------------------------------------------------
RuntimeConfig m_config;
ItemFilterCatalog m_filterCatalog;
// ------------------------------------------------------------------
// Memory + resolvers
// ------------------------------------------------------------------
VmmAccessor m_memory;
BaseObjectResolver m_baseResolver;
LocalPlayerResolver m_localPlayerResolver;
NetworkMetadataReader m_networkMetadataReader;
NearEntityListReader m_nearReader;
FarEntityListReader m_farReader;
SlowEntityListReader m_slowReader;
BulletTableReader m_bulletReader;
ItemListReader m_itemReader;
ClientScoreboardReader m_scoreboardReader;
// Last module base address passed to the sig scanner.
uint64_t m_lastScannedBase = 0;
// Cached camera base pointer (resolved once per session; 0 = not yet known).
// Caching this avoids re-reading World+0x1B8 on every tick.
uint64_t m_cachedCamBase = 0;
// Resolved skeleton pointer-chain offsets (probed once, cached per session).
// Skel offsets are per entity-type (player vs zombie differ); anim/matrix are shared.
int64_t m_bonePlayerSkelOffset = -1;
int64_t m_boneZombieSkelOffset = -1;
int64_t m_boneAnimOffset = -1;
int64_t m_boneMatrixOffset = -1;
// Per-entity bone pointer cache: entity address → (vsAddr, animClass, matBase).
// animClass is stable for the entity lifetime; matBase is dynamic (can be
// reallocated by the engine's animation system). Caching animClass separately
// lets re-resolve after a stale matBase skip the full 4-read pointer walk.
struct BonePointerCache {
uint64_t vsAddr = 0;
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
};
std::unordered_map<uint64_t, BonePointerCache> m_bonePointerCache;
// ---- Diagnostic logging bookkeeping ----
// Admins already announced to the log (so each is reported once, not every
// cycle). Cleared when the player leaves the snapshot so a re-join re-logs.
std::unordered_set<uint64_t> m_adminAnnounced;
// Entities whose skeleton currently resolves to valid bones. Used to detect
// the valid→invalid edge so we can log when a skeleton drops out.
std::unordered_set<uint64_t> m_bonesTracked;
// 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;
// Separate lightweight camera state for the overlay.
// Updated by the dedicated camera thread (RunCameraLoop) at high frequency,
// decoupled from the heavy entity/bone reads so the overlay always projects
// through the freshest possible camera — this is what keeps boxes/skeletons
// glued to fast viewangle flicks instead of lagging a few ms behind.
mutable std::mutex m_camMutex;
CameraData m_latestCamForOverlay;
// Session handoff for the camera thread. Set when a live session starts,
// cleared (0) on reconnect so the camera thread idles while the Process
// handle is being re-created. Atomics: written by the runtime thread,
// read by the camera thread.
std::atomic<uint32_t> m_camPid{0};
std::atomic<uint64_t> m_camWorldAddr{0};
// ------------------------------------------------------------------
// Threading
// ------------------------------------------------------------------
std::thread m_runtimeThread;
std::thread m_cameraThread;
std::atomic<bool> m_stopRequested{false};
mutable std::mutex m_updateMutex;
std::shared_ptr<const RuntimeUpdate> m_latestUpdate;
UpdateCallback m_callback;
// ------------------------------------------------------------------
// Live update baseline — entity lists refreshed every playersRefreshMs,
// camera/localPlayer overwritten every loop tick.
RuntimeUpdate m_liveUpdate;
// ------------------------------------------------------------------
// Per-iteration reader state / throttle timestamps
// ------------------------------------------------------------------
struct ReaderState {
std::vector<DayZScoreboardPlayer> scoreboard;
std::vector<DayZNearEntityEntry> nearEntities;
std::vector<DayZFarEntityEntry> farEntities;
std::vector<DayZSlowEntityEntry> slowEntities;
std::vector<DayZBulletEntry> bullets;
std::vector<DayZItemListEntry> items;
std::chrono::steady_clock::time_point nextScoreboardRefresh;
std::chrono::steady_clock::time_point nextNearRefresh;
std::chrono::steady_clock::time_point nextFarRefresh;
std::chrono::steady_clock::time_point nextSlowRefresh;
std::chrono::steady_clock::time_point nextBulletRefresh;
std::chrono::steady_clock::time_point nextItemRefresh;
std::chrono::steady_clock::time_point nextPlayersRefresh;
std::chrono::steady_clock::time_point nextNetworkMetadataRefresh;
std::chrono::steady_clock::time_point nextBoneRefresh;
std::chrono::steady_clock::time_point nextVmmRefresh;
} m_state;
// Cached metadata, player data, and camera populated during session
NetworkMetadata m_metadata;
LocalPlayerData m_localPlayer;
CameraData m_camera;
// ------------------------------------------------------------------
// Exception used to trigger a reconnect from RunLiveLoop
// ------------------------------------------------------------------
struct ReconnectException {};
// ------------------------------------------------------------------
// Private methods
// ------------------------------------------------------------------
void Run();
/// Block until a valid session is established. Returns the session.
RuntimeSession WaitForSession();
/// Main live-data loop. Throws ReconnectException when the session
/// is no longer valid or the consecutive-failure threshold is reached.
void RunLiveLoop(const RuntimeSession& session);
/// Dedicated high-frequency camera reader. Runs for the whole program
/// lifetime; idles until m_camPid / m_camWorldAddr are set by a live session.
void RunCameraLoop();
/// Read the camera transform block into `out`. Resolves camBaseCache from
/// worldAddr when it is 0; resets it to 0 on a failed read to force a
/// re-resolve next call. Returns true on success.
bool ReadCameraInto(uint32_t pid, uint64_t worldAddr,
uint64_t& camBaseCache, CameraData& out);
/// Publish an update snapshot under the mutex and invoke the callback.
void PublishUpdate(const RuntimeUpdate& update);
/// Reset all readers and clear m_state vectors.
void ResetAllReaders();
/// Build a scoreboard lookup map (networkId -> playerName).
static std::unordered_map<uint32_t, std::string>
BuildScoreboardNames(const std::vector<DayZScoreboardPlayer>& scoreboard);
/// Scatter-read bones for all players/zombies in range using the pointer cache.
/// Cache misses resolve the pointer chain sequentially (one-time per entity).
/// All subsequent reads for cached entities are batched into one DMA call.
void RefreshBonesScatter(uint32_t pid,
std::vector<DayZPlayerEntry>& players,
std::vector<DayZZombieEntry>& zombies);
};
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include <cstdint>
#include "Offsets.h"
// ---------------------------------------------------------------------------
// RuntimeOffsets
//
// Mutable counterparts to the compile-time Offsets:: namespace.
// SigScanner::Apply() updates these at startup from scanned byte patterns.
// All memory readers use RuntimeOffsets:: so a single scanner run covers a
// game patch without needing a recompile.
//
// Defaults are the last-known good values from Offsets.h; the scanner will
// overwrite them when matching patterns are found in the game binary.
// ---------------------------------------------------------------------------
namespace RuntimeOffsets {
// -----------------------------------------------------------------------
// Base — module-relative RVAs of global variables
// (resolved via RIP-relative MOV/LEA, ScanType::MovCs)
// -----------------------------------------------------------------------
namespace Base {
inline uint64_t World = Offsets::Base::World;
inline uint64_t NetworkManager = Offsets::Base::NetworkManager;
}
// -----------------------------------------------------------------------
// World — struct member offsets within the World object
// (resolved via MOV reg, [reg+offset], ScanType::MovReg)
// -----------------------------------------------------------------------
namespace World {
inline uint64_t NearEntityList = Offsets::World::NearEntityList;
inline uint64_t NearTableSize = Offsets::World::NearTableSize;
inline uint64_t FarEntityList = Offsets::World::FarEntityList;
inline uint64_t FarTableSize = Offsets::World::FarTableSize;
inline uint64_t BulletTable = Offsets::World::BulletTable;
inline uint64_t BulletCount = Offsets::World::BulletCount;
inline uint64_t Camera = Offsets::World::Camera;
inline uint64_t ItemList = Offsets::World::ItemList;
inline uint64_t ItemTableAllocCount = Offsets::World::ItemTableAllocCount;
inline uint64_t ItemTableValidCount = Offsets::World::ItemTableValidCount;
}
// -----------------------------------------------------------------------
// Entity / common struct offsets
// -----------------------------------------------------------------------
namespace Common {
inline uint64_t VisualState = Offsets::Common::VisualState;
}
namespace Player {
inline uint64_t IsDead = Offsets::Player::IsDead;
}
namespace Inventory {
inline uint64_t Base = Offsets::Inventory::Base;
inline uint64_t Hands = Offsets::Inventory::Hands;
}
} // namespace RuntimeOffsets
+360
View File
@@ -0,0 +1,360 @@
#include "SigScanner/SigScanner.h"
#include "RuntimeOffsets.h"
#include "Offsets.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <sstream>
#include <spdlog/spdlog.h>
// -----------------------------------------------------------------------
// Constants
// -----------------------------------------------------------------------
static constexpr size_t kChunkSize = 4 * 1024 * 1024; // 4 MB per DMA read
static constexpr size_t kOverlap = 512; // cross-boundary safety
static constexpr size_t kDefaultScanSize = 64 * 1024 * 1024; // fallback if PE read fails
// -----------------------------------------------------------------------
// ParsePattern
// -----------------------------------------------------------------------
std::vector<SigScanner::PatternByte>
SigScanner::ParsePattern(const std::string& hex)
{
std::vector<PatternByte> out;
std::istringstream ss(hex);
std::string token;
while (ss >> token) {
if (token == "?" || token == "??") {
out.push_back({ 0x00, true });
} else {
uint8_t byte = static_cast<uint8_t>(std::stoul(token, nullptr, 16));
out.push_back({ byte, false });
}
}
return out;
}
// -----------------------------------------------------------------------
// FindFirst
// -----------------------------------------------------------------------
std::optional<size_t> SigScanner::FindFirst(
const std::vector<uint8_t>& haystack,
const std::vector<PatternByte>& pattern,
size_t startPos)
{
if (pattern.empty()) return std::nullopt;
const size_t patLen = pattern.size();
if (haystack.size() < patLen) return std::nullopt;
const size_t limit = haystack.size() - patLen;
for (size_t i = startPos; i <= limit; ++i) {
bool ok = true;
for (size_t j = 0; j < patLen; ++j) {
if (!pattern[j].wildcard && haystack[i + j] != pattern[j].value) {
ok = false;
break;
}
}
if (ok) return i;
}
return std::nullopt;
}
// -----------------------------------------------------------------------
// ReadU32
// -----------------------------------------------------------------------
uint32_t SigScanner::ReadU32(const std::vector<uint8_t>& bytes, size_t offset)
{
uint32_t v = 0;
std::memcpy(&v, bytes.data() + offset, sizeof(v));
return v;
}
// -----------------------------------------------------------------------
// ResolveRip (MovCs)
// target = matchRva + instrLen + sign_extend(disp32)
// -----------------------------------------------------------------------
uint64_t SigScanner::ResolveRip(uint64_t matchRva, int instrLen, uint32_t disp32Raw)
{
const int32_t disp = static_cast<int32_t>(disp32Raw);
const int64_t target = static_cast<int64_t>(matchRva)
+ static_cast<int64_t>(instrLen)
+ static_cast<int64_t>(disp);
return static_cast<uint64_t>(target);
}
// -----------------------------------------------------------------------
// ExtractMovReg
// Find the first wildcard byte in the pattern; read uint32 from that
// position in the original chunk. For DayZ, the 4 wildcard bytes that
// immediately follow the opcode prefix are the struct member offset.
// -----------------------------------------------------------------------
uint32_t SigScanner::ExtractMovReg(const std::vector<uint8_t>& chunk,
size_t matchPos,
const std::vector<PatternByte>& pattern)
{
for (size_t i = 0; i < pattern.size(); ++i) {
if (pattern[i].wildcard) {
return ReadU32(chunk, matchPos + i);
}
}
return 0;
}
// -----------------------------------------------------------------------
// ReadModuleSize
// -----------------------------------------------------------------------
size_t SigScanner::ReadModuleSize(VmmAccessor& mem, uint32_t pid, uint64_t moduleBase)
{
uint16_t magic = 0;
if (!mem.TryReadValue<uint16_t>(pid, moduleBase, magic) || magic != 0x5A4D) return 0;
uint32_t e_lfanew = 0;
if (!mem.TryReadValue<uint32_t>(pid, moduleBase + 0x3C, e_lfanew)) return 0;
uint32_t peSig = 0;
if (!mem.TryReadValue<uint32_t>(pid, moduleBase + e_lfanew, peSig) || peSig != 0x00004550) return 0;
// SizeOfImage: PE_header + 4 (PE sig) + 20 (COFF) + 0x38 (optional header offset)
uint32_t sizeOfImage = 0;
if (!mem.TryReadValue<uint32_t>(pid, moduleBase + e_lfanew + 24 + 0x38, sizeOfImage)) return 0;
return static_cast<size_t>(sizeOfImage);
}
// -----------------------------------------------------------------------
// Scan
// -----------------------------------------------------------------------
SigScanner::ScanResult
SigScanner::Scan(VmmAccessor& mem, uint32_t pid, uint64_t moduleBase)
{
ScanResult result;
// ---- Determine scan range ----
size_t scanSize = ReadModuleSize(mem, pid, moduleBase);
if (scanSize == 0) {
spdlog::warn("SigScanner: PE header unreadable — defaulting to {} MB",
kDefaultScanSize / (1024 * 1024));
scanSize = kDefaultScanSize;
} else {
spdlog::debug("SigScanner: SizeOfImage = {} MB", scanSize / (1024 * 1024));
}
// ====================================================================
// Pattern table — DayZ 1.29
// ====================================================================
// ---- MovCs: RIP-relative globals ----
// Modbase::World (48 8B 05 [disp32] 48 8D 54 24 ?? 48 8B 48 30)
auto patWorld = ParsePattern(
"48 8B 05 ? ? ? ? 48 8D 54 24 ? 48 8B 48 30");
// Modbase::Network (48 8D 0D [disp32] E8 ?? ?? ?? ?? 48 8B 1D ?? ?? ?? ?? 84 C0 74 20 41)
auto patNetMgr = ParsePattern(
"48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8B 1D ? ? ? ? 84 C0 74 20 41");
// ---- MovReg: World struct member offsets ----
// World::NearEntList (48 8B 83 [off32] 49 8B 14 06 48 3B D5)
auto patNear = ParsePattern(
"48 8B 83 ? ? ? ? 49 8B 14 06 48 3B D5");
// World::FarEntList (48 8B 83 [off32] 49 8B 0C 06 48 3B CD ...)
auto patFar = ParsePattern(
"48 8B 83 ? ? ? ? 49 8B 0C 06 48 3B CD 74 17 80 B9 ? ? ? ? ? 75 0E");
// World::BulletList (48 8B 83 [off32] 49 8B CF 48 03 0C F8)
auto patBullet = ParsePattern(
"48 8B 83 ? ? ? ? 49 8B CF 48 03 0C F8");
// World::Camera (4C 8B 83 [off32] 4C 8B 11 48 89 70 08)
auto patCamera = ParsePattern(
"4C 8B 83 ? ? ? ? 4C 8B 11 48 89 70 08");
// World::ItemList (48 89 85 [off32] 48 8D 15 ?? ?? ?? ?? 48 89 9D ?? ?? ?? ?? 48 8D 8D)
auto patItem = ParsePattern(
"48 89 85 ? ? ? ? 48 8D 15 ? ? ? ? 48 89 9D ? ? ? ? 48 8D 8D");
// ---- MovReg: entity struct offsets ----
// Human::VisualState (48 8B 9F [off32] 49 8B CE FF 90 ?? ?? ?? ?? 8B 10)
auto patVS = ParsePattern(
"48 8B 9F ? ? ? ? 49 8B CE FF 90 ? ? ? ? 8B 10");
// Human::IsDead (0F B6 9A [off32] 48 89 7C 24 20 41 0F B6 B8 ?? ?? ?? ?? FF 90 B0 00)
auto patDead = ParsePattern(
"0F B6 9A ? ? ? ? 48 89 7C 24 20 41 0F B6 B8 ? ? ? ? FF 90 B0 00");
// DayZPlayer::Inventory (48 8B 8B [off32] 48 8B 01 FF 90 ?? ?? ?? ?? EB 02)
auto patInv = ParsePattern(
"48 8B 8B ? ? ? ? 48 8B 01 FF 90 ? ? ? ? EB 02");
// DayZPlayerInventory::Hands (48 8B 8B [off32] 48 8B F8 48 85 C9)
auto patHands = ParsePattern(
"48 8B 8B ? ? ? ? 48 8B F8 48 85 C9");
// ====================================================================
// Chunk scan
// ====================================================================
std::vector<uint8_t> chunk;
chunk.reserve(kChunkSize + kOverlap);
size_t offset = 0;
while (offset < scanSize) {
// Early-out once everything is found
if (result.baseWorld &&
result.baseNetworkManager &&
result.worldNearEntList &&
result.worldFarEntList &&
result.worldBulletList &&
result.worldCamera &&
result.worldItemList &&
result.humanVisualState &&
result.humanIsDead &&
result.playerInventory &&
result.inventoryHands)
{
break;
}
const size_t readSize = std::min(kChunkSize, scanSize - offset);
if (!mem.ReadBytes(pid, moduleBase + offset, readSize, chunk)) {
offset += readSize;
continue;
}
// -- MovCs --
if (!result.baseWorld) {
if (auto pos = FindFirst(chunk, patWorld)) {
result.baseWorld = ResolveRip(offset + *pos, 7, ReadU32(chunk, *pos + 3));
}
}
if (!result.baseNetworkManager) {
if (auto pos = FindFirst(chunk, patNetMgr)) {
result.baseNetworkManager = ResolveRip(offset + *pos, 7, ReadU32(chunk, *pos + 3));
}
}
// -- MovReg (World struct) --
if (!result.worldNearEntList) {
if (auto pos = FindFirst(chunk, patNear)) {
result.worldNearEntList = ExtractMovReg(chunk, *pos, patNear);
}
}
if (!result.worldFarEntList) {
if (auto pos = FindFirst(chunk, patFar)) {
result.worldFarEntList = ExtractMovReg(chunk, *pos, patFar);
}
}
if (!result.worldBulletList) {
if (auto pos = FindFirst(chunk, patBullet)) {
result.worldBulletList = ExtractMovReg(chunk, *pos, patBullet);
}
}
if (!result.worldCamera) {
if (auto pos = FindFirst(chunk, patCamera)) {
result.worldCamera = ExtractMovReg(chunk, *pos, patCamera);
}
}
if (!result.worldItemList) {
if (auto pos = FindFirst(chunk, patItem)) {
result.worldItemList = ExtractMovReg(chunk, *pos, patItem);
}
}
// -- MovReg (entity struct) --
if (!result.humanVisualState) {
if (auto pos = FindFirst(chunk, patVS)) {
result.humanVisualState = ExtractMovReg(chunk, *pos, patVS);
}
}
if (!result.humanIsDead) {
if (auto pos = FindFirst(chunk, patDead)) {
result.humanIsDead = ExtractMovReg(chunk, *pos, patDead);
}
}
if (!result.playerInventory) {
if (auto pos = FindFirst(chunk, patInv)) {
result.playerInventory = ExtractMovReg(chunk, *pos, patInv);
}
}
if (!result.inventoryHands) {
if (auto pos = FindFirst(chunk, patHands)) {
result.inventoryHands = ExtractMovReg(chunk, *pos, patHands);
}
}
if (readSize <= kOverlap) break;
offset += readSize - kOverlap;
}
// ---- Report ----
auto log = [](const char* name, auto val) {
if (val) spdlog::info("SigScanner: {:35s} = 0x{:X}", name, *val);
else spdlog::warn("SigScanner: {:35s} NOT FOUND", name);
};
log("base::world", result.baseWorld);
log("base::network_manager", result.baseNetworkManager);
log("World::NearEntityList", result.worldNearEntList);
log("World::FarEntityList", result.worldFarEntList);
log("World::BulletTable", result.worldBulletList);
log("World::Camera", result.worldCamera);
log("World::ItemList", result.worldItemList);
log("Common::VisualState", result.humanVisualState);
log("Player::IsDead", result.humanIsDead);
log("Inventory::Base", result.playerInventory);
log("Inventory::Hands", result.inventoryHands);
return result;
}
// -----------------------------------------------------------------------
// Apply — write results into RuntimeOffsets, log each change
// -----------------------------------------------------------------------
void SigScanner::Apply(const ScanResult& result)
{
auto applyU64 = [](const char* name, uint64_t& dst, std::optional<uint64_t> src) {
if (!src) return;
if (*src != dst) {
spdlog::info("SigScanner::Apply: {} 0x{:X} -> 0x{:X}", name, dst, *src);
dst = *src;
}
};
auto applyU32 = [](const char* name, uint64_t& dst, std::optional<uint32_t> src) {
if (!src) return;
if (static_cast<uint64_t>(*src) != dst) {
spdlog::info("SigScanner::Apply: {} 0x{:X} -> 0x{:X}", name, dst, *src);
dst = static_cast<uint64_t>(*src);
}
};
applyU64("Base::World", RuntimeOffsets::Base::World, result.baseWorld);
applyU64("Base::NetworkManager", RuntimeOffsets::Base::NetworkManager, result.baseNetworkManager);
applyU32("World::NearEntityList", RuntimeOffsets::World::NearEntityList, result.worldNearEntList);
applyU32("World::FarEntityList", RuntimeOffsets::World::FarEntityList, result.worldFarEntList);
applyU32("World::BulletTable", RuntimeOffsets::World::BulletTable, result.worldBulletList);
applyU32("World::Camera", RuntimeOffsets::World::Camera, result.worldCamera);
applyU32("World::ItemList", RuntimeOffsets::World::ItemList, result.worldItemList);
// Derive companion offsets that always follow the primary by +8
if (result.worldNearEntList) {
RuntimeOffsets::World::NearTableSize = RuntimeOffsets::World::NearEntityList + 8;
}
if (result.worldFarEntList) {
RuntimeOffsets::World::FarTableSize = RuntimeOffsets::World::FarEntityList + 8;
}
if (result.worldBulletList) {
RuntimeOffsets::World::BulletCount = RuntimeOffsets::World::BulletTable + 8;
}
if (result.worldItemList) {
RuntimeOffsets::World::ItemTableAllocCount = RuntimeOffsets::World::ItemList + 8;
RuntimeOffsets::World::ItemTableValidCount = RuntimeOffsets::World::ItemList + 16;
}
applyU32("Common::VisualState", RuntimeOffsets::Common::VisualState, result.humanVisualState);
applyU32("Player::IsDead", RuntimeOffsets::Player::IsDead, result.humanIsDead);
applyU32("Inventory::Base", RuntimeOffsets::Inventory::Base, result.playerInventory);
applyU32("Inventory::Hands", RuntimeOffsets::Inventory::Hands, result.inventoryHands);
}
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "Memory/VmmAccessor.h"
// -----------------------------------------------------------------------
// SigScanner
//
// Reads the game module in chunks via DMA and searches for byte patterns
// to extract offsets that change with each game update.
//
// Supported scan types:
// MovCs — RIP-relative MOV/LEA: read int32 disp from the first wildcard
// position, resolve to a module-relative RVA.
// MovReg — Struct-member access: read uint32 from the first wildcard
// position; the value IS the member offset within the struct.
//
// Usage:
// auto result = SigScanner::Scan(mem, pid, moduleBase);
// SigScanner::Apply(result); // writes into RuntimeOffsets
// -----------------------------------------------------------------------
class SigScanner {
public:
struct ScanResult {
// ------------------------------------------------------------------
// Base:: — module-relative RVAs of global-variable slots
// (MovCs — resolved from RIP-relative displacement)
// ------------------------------------------------------------------
std::optional<uint64_t> baseWorld; // Base::World
std::optional<uint64_t> baseNetworkManager; // Base::NetworkManager
// ------------------------------------------------------------------
// World:: — struct member offsets within the World object
// (MovReg — 4-byte little-endian value at the first wildcard pos)
// ------------------------------------------------------------------
std::optional<uint32_t> worldNearEntList; // World::NearEntityList
std::optional<uint32_t> worldFarEntList; // World::FarEntityList
std::optional<uint32_t> worldBulletList; // World::BulletTable
std::optional<uint32_t> worldCamera; // World::Camera
std::optional<uint32_t> worldItemList; // World::ItemList
// ------------------------------------------------------------------
// Entity struct offsets (MovReg)
// ------------------------------------------------------------------
std::optional<uint32_t> humanVisualState; // Common::VisualState
std::optional<uint32_t> humanIsDead; // Player::IsDead
std::optional<uint32_t> playerInventory; // Inventory::Base
std::optional<uint32_t> inventoryHands; // Inventory::Hands
};
// Scan the game module for all configured patterns.
// moduleBase: runtime VA of the module's first mapped byte.
static ScanResult Scan(VmmAccessor& mem, uint32_t pid, uint64_t moduleBase);
// Write scanned values into RuntimeOffsets, logging each change.
static void Apply(const ScanResult& result);
private:
struct PatternByte { uint8_t value; bool wildcard; };
// Parse a hex pattern string like "4C 8B 05 ? ? ? ? 48" into PatternByte vec.
static std::vector<PatternByte> ParsePattern(const std::string& hex);
// Return the byte-offset of the first match in [haystack], or nullopt.
static std::optional<size_t> FindFirst(const std::vector<uint8_t>& haystack,
const std::vector<PatternByte>& pattern,
size_t startPos = 0);
// Read a 4-byte little-endian uint32 from bytes[offset].
static uint32_t ReadU32(const std::vector<uint8_t>& bytes, size_t offset);
// For MovCs: target = matchRva + instrLen + (int32_t)disp32
static uint64_t ResolveRip(uint64_t matchRva, int instrLen, uint32_t disp32Raw);
// For MovReg: return the uint32 at the first wildcard position in the pattern.
static uint32_t ExtractMovReg(const std::vector<uint8_t>& chunk,
size_t matchPos,
const std::vector<PatternByte>& pattern);
// Read SizeOfImage from the module's PE optional header.
static size_t ReadModuleSize(VmmAccessor& mem, uint32_t pid, uint64_t moduleBase);
};
+144
View File
@@ -0,0 +1,144 @@
#include "Web/BulletTrackCache.h"
#include <algorithm>
#include <cmath>
#include <unordered_set>
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
static float Distance3(const Vector3& a, const Vector3& b) {
float dx = b.x - a.x;
float dy = b.y - a.y;
float dz = b.z - a.z;
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
static float Length3(const Vector3& v) {
return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
// -------------------------------------------------------------------------
// BulletTrackCache::Update
// -------------------------------------------------------------------------
void BulletTrackCache::Update(const std::vector<DayZBulletEntry>& bullets,
int64_t nowMs)
{
std::lock_guard<std::mutex> lk(m_mutex);
// Build set of addresses present in this snapshot.
std::unordered_set<uint64_t> liveSet;
liveSet.reserve(bullets.size());
for (const auto& b : bullets) {
if (b.address != 0)
liveSet.insert(b.address);
}
// ----------------------------------------------------------------
// Process each live bullet entry.
// ----------------------------------------------------------------
for (const auto& entry : bullets) {
if (entry.address == 0 || !entry.position.has_value()) continue;
const Vector3& pos = entry.position.value();
Vector3 dir = entry.direction.has_value() ? entry.direction.value() : Vector3{};
auto it = m_tracks.find(entry.address);
if (it == m_tracks.end()) {
// New bullet — create track.
BulletTrack t;
t.address = entry.address;
t.firstSeenMs = nowMs;
t.lastSeenMs = nowMs;
t.currentPos = pos;
t.initialPos = pos;
t.finalPos = pos;
t.initialDir = dir;
t.lastDir = dir;
t.speedMps = 0.0f;
t.isActive = true;
t.isPhantom = false;
t.isCompleted = false;
t.points.push_back({ nowMs, pos });
m_tracks.emplace(entry.address, std::move(t));
} else {
BulletTrack& t = it->second;
// Speed calculation.
float elapsed = static_cast<float>(nowMs - t.lastSeenMs) / 1000.0f;
if (elapsed > 0.0f) {
float dist = Distance3(t.currentPos, pos);
float speed = dist / elapsed;
// Clamp to [0, 900] m/s (well above any DayZ projectile).
t.speedMps = std::max(0.0f, std::min(900.0f, speed));
}
t.lastSeenMs = nowMs;
t.currentPos = pos;
t.finalPos = pos;
if (Length3(dir) > 0.001f)
t.lastDir = dir;
t.isActive = true;
t.isPhantom = false;
t.points.push_back({ nowMs, pos });
// Finalise check: last 2+ points within kStillM over kStillMs.
if (!t.isCompleted && t.points.size() >= 2) {
const auto& prev = t.points[t.points.size() - 2];
int64_t span = nowMs - prev.first;
float dist = Distance3(prev.second, pos);
if (span >= kStillMs && dist <= kStillM) {
t.isCompleted = true;
t.finalizedMs = nowMs;
}
}
}
}
// ----------------------------------------------------------------
// Mark tracks not in this snapshot as phantom.
// ----------------------------------------------------------------
for (auto& [addr, t] : m_tracks) {
if (liveSet.find(addr) == liveSet.end()) {
if (!t.isCompleted) {
t.isActive = false;
t.isPhantom = true;
}
}
}
// ----------------------------------------------------------------
// Erase stale tracks.
// ----------------------------------------------------------------
for (auto it = m_tracks.begin(); it != m_tracks.end();) {
const BulletTrack& t = it->second;
bool erase = false;
if (t.isCompleted) {
// Keep for kRetentionMs after finalisation.
erase = (nowMs - t.finalizedMs) > kRetentionMs;
} else if (t.isPhantom) {
erase = (nowMs - t.lastSeenMs) > kPhantomLifetimeMs;
}
if (erase)
it = m_tracks.erase(it);
else
++it;
}
}
// -------------------------------------------------------------------------
// BulletTrackCache::GetSnapshot
// -------------------------------------------------------------------------
std::vector<BulletTrack> BulletTrackCache::GetSnapshot() const {
std::lock_guard<std::mutex> lk(m_mutex);
std::vector<BulletTrack> out;
out.reserve(m_tracks.size());
for (const auto& [addr, t] : m_tracks)
out.push_back(t);
return out;
}
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "Core/Models.h"
// -------------------------------------------------------------------------
// BulletTrack — full history for one bullet address.
// -------------------------------------------------------------------------
struct BulletTrack {
uint64_t address = 0;
int64_t firstSeenMs = 0;
int64_t lastSeenMs = 0;
int64_t finalizedMs = 0; // 0 = not yet finalised
Vector3 currentPos;
Vector3 initialPos;
Vector3 finalPos;
Vector3 initialDir;
Vector3 lastDir;
float speedMps = 0.0f;
bool isActive = false; // seen in the most-recent update
bool isPhantom = false; // no longer in live list
bool isCompleted = false; // came to rest
// (timestamp_ms, world-position) pairs, oldest first.
std::vector<std::pair<int64_t, Vector3>> points;
};
// -------------------------------------------------------------------------
// BulletTrackCache
// -------------------------------------------------------------------------
class BulletTrackCache {
public:
/// Merge a fresh bullet snapshot into the cache.
void Update(const std::vector<DayZBulletEntry>& bullets, int64_t nowMs);
/// Thread-safe snapshot of all currently tracked bullets.
std::vector<BulletTrack> GetSnapshot() const;
private:
mutable std::mutex m_mutex;
std::unordered_map<uint64_t, BulletTrack> m_tracks;
// A bullet that hasn't moved more than kStillM over kStillMs is
// considered stopped and its track is finalised.
static constexpr int64_t kStillMs = 75;
static constexpr float kStillM = 0.05f;
// How long to retain a completed track after it was finalised.
static constexpr int64_t kRetentionMs = 1500;
// How long to keep a phantom (disappeared but not completed) track.
static constexpr int64_t kPhantomLifetimeMs = 5000;
};
+126
View File
@@ -0,0 +1,126 @@
#include "Web/MapRegistry.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <string>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
#include "EmbeddedMaps.h"
// Returns the directory that contains the running exe.
// Used so that maps/ is always resolved relative to the exe, not the CWD.
static std::filesystem::path ExeDir() {
wchar_t buf[MAX_PATH] = {};
GetModuleFileNameW(nullptr, buf, MAX_PATH);
return std::filesystem::path(buf).parent_path();
}
// -------------------------------------------------------------------------
// Static map table
// -------------------------------------------------------------------------
static const MapInfo kMaps[] = {
{ "chernarusplus", "ChernarusPlus", 15360, 0 },
{ "livonia", "Livonia", 15360, 1 },
{ "namalsk", "Namalsk", 12800, 2 },
{ "banov", "Banov", 15360, 3 },
{ "deadfall", "Deadfall", 10000, 4 },
{ "deerisle", "Deerisle", 16384, 5 },
{ "esseker", "Esseker", 12800, 6 },
{ "lux", "Lux", 15360, 7 },
{ "sakhal", "Sakhal", 15360, 8 },
{ "takistan", "Takistan", 12800, 9 },
{ "alteria", "Alteria", 8192, 10 },
};
static constexpr int kMapCount = static_cast<int>(sizeof(kMaps) / sizeof(kMaps[0]));
// -------------------------------------------------------------------------
// Alias table: normalised-key → canonical id
// -------------------------------------------------------------------------
struct Alias { const char* from; const char* to; };
static const Alias kAliases[] = {
{ "enoch", "livonia" },
{ "chernarus", "chernarusplus" },
};
// -------------------------------------------------------------------------
// Normalise: lowercase, keep only alphanumeric chars.
// -------------------------------------------------------------------------
static std::string Normalise(std::string_view sv) {
std::string out;
out.reserve(sv.size());
for (unsigned char c : sv) {
if (std::isalnum(c))
out += static_cast<char>(std::tolower(c));
}
return out;
}
// -------------------------------------------------------------------------
// MapRegistry implementation
// -------------------------------------------------------------------------
const MapInfo* MapRegistry::Resolve(std::string_view serverMapName) {
std::string key = Normalise(serverMapName);
if (key.empty()) return nullptr;
// Check alias table first.
for (const auto& alias : kAliases) {
if (key == alias.from) {
key = alias.to;
break;
}
}
// Exact match against canonical id.
for (const auto& m : kMaps) {
if (key == m.id)
return &m;
}
// Partial prefix match (e.g. "chernarusplus_2022" → "chernarusplus").
for (const auto& m : kMaps) {
if (key.rfind(m.id, 0) == 0)
return &m;
}
// Partial suffix / contains match (e.g. "dayz_namalsk").
for (const auto& m : kMaps) {
if (key.find(m.id) != std::string::npos)
return &m;
}
return nullptr;
}
void MapRegistry::Translate(const MapInfo& m, float worldX, float worldZ,
float& outX, float& outY) {
if (m.id == "livonia") {
outX = worldX * 1.2f;
outY = (12800.0f - worldZ) * 1.2f;
} else {
outX = worldX;
outY = static_cast<float>(m.worldSize) - worldZ;
}
}
bool MapRegistry::HasMapImage(const MapInfo& m) {
std::filesystem::path p = ExeDir() / "maps" / (m.id + ".png");
if (std::filesystem::exists(p)) return true;
auto [ptr, sz] = GetEmbeddedMap(m.id);
return ptr != nullptr;
}
const MapInfo* MapRegistry::AllMaps(int& count) {
count = kMapCount;
return kMaps;
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <string>
#include <string_view>
// -------------------------------------------------------------------------
// MapInfo — metadata for a single supported DayZ map.
// -------------------------------------------------------------------------
struct MapInfo {
std::string id; // canonical lowercase key (e.g. "chernarusplus")
std::string name; // human-readable display name
int worldSize; // world-space units that span the full map
int index; // stable ordering index
};
// -------------------------------------------------------------------------
// MapRegistry — static map database + helpers.
// -------------------------------------------------------------------------
class MapRegistry {
public:
MapRegistry() = delete;
/// Return a pointer to the best-matching MapInfo for the given
/// server map name, or nullptr if no match is found.
static const MapInfo* Resolve(std::string_view serverMapName);
/// Convert a world-space position (worldX, worldZ) to 2-D map-image
/// pixel-space coordinates (outX, outY). The transform differs per map.
static void Translate(const MapInfo& m, float worldX, float worldZ,
float& outX, float& outY);
/// Return true if maps/<id>.png exists relative to the working directory.
static bool HasMapImage(const MapInfo& m);
/// Return the full list of known maps (used for bootstrap JSON).
static const MapInfo* AllMaps(int& count);
};
+158
View File
@@ -0,0 +1,158 @@
#include "Web/MapTileService.h"
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <string>
#include <vector>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
#include "EmbeddedMaps.h"
static std::filesystem::path ExeDir() {
wchar_t buf[MAX_PATH] = {};
GetModuleFileNameW(nullptr, buf, MAX_PATH);
return std::filesystem::path(buf).parent_path();
}
// stb_image / stb_image_write are implemented in stb_impl.cpp.
// Include the headers only — no implementation macros here.
#include <stb_image.h>
#include <stb_image_write.h>
static constexpr int kTileSize = 512;
static constexpr int kChannels = 4; // RGBA
// -------------------------------------------------------------------------
// PNG write callback: appends bytes to a vector<uint8_t>*.
// -------------------------------------------------------------------------
static void PngWriteCallback(void* context, void* data, int size) {
auto* buf = static_cast<std::vector<uint8_t>*>(context);
const auto* bytes = static_cast<const uint8_t*>(data);
buf->insert(buf->end(), bytes, bytes + size);
}
// -------------------------------------------------------------------------
// MapTileService::LoadOrGet
// -------------------------------------------------------------------------
const MapTileService::Image* MapTileService::LoadOrGet(const std::string& mapId) {
// Caller must already hold m_mutex.
auto it = m_cache.find(mapId);
if (it != m_cache.end()) {
return &it->second; // cache hit
}
// 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();
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 {
auto [ptr, sz] = GetEmbeddedMap(mapId);
if (ptr)
raw = stbi_load_from_memory(ptr, static_cast<int>(sz),
&w, &h, &channels, kChannels);
}
Image img;
if (raw && w > 0 && h > 0) {
img.w = w;
img.h = h;
img.pixels.assign(raw, raw + static_cast<size_t>(w) * h * kChannels);
stbi_image_free(raw);
} else {
if (raw) stbi_image_free(raw);
// Don't cache the failure — the user may drop the PNG in while running.
return nullptr;
}
auto [inserted_it, ok] = m_cache.emplace(mapId, std::move(img));
(void)ok;
return &inserted_it->second;
}
// -------------------------------------------------------------------------
// MapTileService::GetTile
// -------------------------------------------------------------------------
std::vector<uint8_t> MapTileService::GetTile(const MapInfo& map, int tileX,
int tileY, int& errCode)
{
if (tileX < 0 || tileY < 0) {
errCode = 400;
return {};
}
std::lock_guard<std::mutex> lk(m_mutex);
const Image* img = LoadOrGet(map.id);
if (!img || img->pixels.empty() || img->w == 0 || img->h == 0) {
errCode = 404;
return {};
}
const int pixW = img->w;
const int pixH = img->h;
const float ws = static_cast<float>(map.worldSize);
const float scaleX = static_cast<float>(pixW) / ws;
const float scaleY = static_cast<float>(pixH) / ws;
int srcX = static_cast<int>(static_cast<float>(tileX) * kTileSize * scaleX);
int srcY = static_cast<int>(static_cast<float>(tileY) * kTileSize * scaleY);
int srcW = static_cast<int>(kTileSize * scaleX);
int srcH = static_cast<int>(kTileSize * scaleY);
// Clamp to image extents.
if (srcX >= pixW || srcY >= pixH) {
errCode = 404;
return {};
}
srcW = std::max(1, std::min(srcW, pixW - srcX));
srcH = std::max(1, std::min(srcH, pixH - srcY));
// Build 512×512 RGBA output via nearest-neighbour scaling.
std::vector<uint8_t> tile(static_cast<size_t>(kTileSize) * kTileSize * kChannels, 0);
for (int ty = 0; ty < kTileSize; ++ty) {
// Map output pixel row → source row.
int sy = srcY + static_cast<int>((static_cast<float>(ty) / kTileSize) * srcH);
sy = std::clamp(sy, 0, pixH - 1);
for (int tx = 0; tx < kTileSize; ++tx) {
int sx = srcX + static_cast<int>((static_cast<float>(tx) / kTileSize) * srcW);
sx = std::clamp(sx, 0, pixW - 1);
const size_t srcOff = (static_cast<size_t>(sy) * pixW + sx) * kChannels;
const size_t dstOff = (static_cast<size_t>(ty) * kTileSize + tx) * kChannels;
std::memcpy(tile.data() + dstOff, img->pixels.data() + srcOff, kChannels);
}
}
// Encode to PNG.
std::vector<uint8_t> png;
png.reserve(kTileSize * kTileSize); // rough estimate
int ok = stbi_write_png_to_func(PngWriteCallback, &png,
kTileSize, kTileSize, kChannels,
tile.data(),
kTileSize * kChannels);
if (!ok || png.empty()) {
errCode = 500;
return {};
}
errCode = 200;
return png;
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "Web/MapRegistry.h"
// -------------------------------------------------------------------------
// MapTileService
// Loads a full map PNG on first request, caches it in memory, and serves
// 512×512 tiles on demand (nearest-neighbour resampled).
// -------------------------------------------------------------------------
class MapTileService {
public:
/// 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.
std::vector<uint8_t> GetTile(const MapInfo& map, int tileX, int tileY,
int& errCode);
private:
struct Image {
std::vector<uint8_t> pixels; // RGBA, row-major
int w = 0;
int h = 0;
};
std::mutex m_mutex;
std::unordered_map<std::string, Image> m_cache; // mapId → loaded image
/// Return cached Image or attempt to load it. Returns nullptr on failure.
const Image* LoadOrGet(const std::string& mapId);
};
+463
View File
@@ -0,0 +1,463 @@
// Pull in Windows headers before httplib so WinSock2 is set up correctly.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
// httplib is a single-header library; the full definition is only needed here.
#include <httplib.h>
#include "Web/WebRadarServer.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <system_error>
#include <spdlog/spdlog.h>
// -------------------------------------------------------------------------
// Constructor / Destructor
// -------------------------------------------------------------------------
WebRadarServer::WebRadarServer(WebRadarConfig config)
: m_config(std::move(config))
{}
WebRadarServer::~WebRadarServer() {
Stop();
}
// -------------------------------------------------------------------------
// Start / Stop
// -------------------------------------------------------------------------
void WebRadarServer::Start() {
// Pre-select the first map that has a PNG in maps/ so the browser
// can show tiles immediately, before the game session connects.
if (!m_currentMap) {
int mapCount = 0;
const MapInfo* allMaps = MapRegistry::AllMaps(mapCount);
for (int i = 0; i < mapCount; ++i) {
if (MapRegistry::HasMapImage(allMaps[i])) {
m_currentMap = &allMaps[i];
spdlog::info("Web radar: pre-selected map '{}' from maps/ directory",
allMaps[i].id);
break;
}
}
if (!m_currentMap) {
spdlog::warn("Web radar: no map PNG found in maps/ — tiles will return 404 until "
"a PNG named after a map ID is placed there "
"(e.g. maps/chernarusplus.png, maps/sakhal.png).");
}
}
// Build the initial bootstrap JSON now so /api/bootstrap returns valid JSON
// immediately when the browser loads, before any game session connects.
// Without this, m_bootstrapJson is "" and response.json() throws in the JS,
// leaving the page with a black background and no map tiles.
{
RuntimeUpdate empty{};
m_bootstrapJson = m_snapshotSvc.BuildBootstrapJson(empty, m_currentMap, m_config.port);
m_lastBootstrapMapId = m_currentMap ? m_currentMap->id : "";
}
m_server = std::make_unique<httplib::Server>();
m_server->new_task_queue = [] {
return new httplib::ThreadPool(16);
};
SetupRoutes();
m_thread = std::thread([this] {
spdlog::info("Web radar listening on {}:{}",
m_config.bindAddress, m_config.port);
if (!m_server->listen(m_config.bindAddress, m_config.port)) {
spdlog::error("Web radar failed to bind {}:{}",
m_config.bindAddress, m_config.port);
}
});
}
void WebRadarServer::Stop() {
m_stopping.store(true);
m_cv.notify_all();
if (m_server) m_server->stop();
if (m_thread.joinable()) m_thread.join();
}
// -------------------------------------------------------------------------
// PushSnapshot
// -------------------------------------------------------------------------
void WebRadarServer::PushSnapshot(const RuntimeUpdate& update) {
int64_t now = NowMs();
// Always update bullet tracks — they need sub-100ms freshness for
// trajectory rendering, and the update is cheap (no JSON involved).
m_bullets.Update(update.bullets, now);
if (update.serverMapName.has_value()) {
const MapInfo* resolved = MapRegistry::Resolve(*update.serverMapName);
if (resolved) {
if (m_currentMap != resolved)
spdlog::info("Web radar: map changed to '{}'", resolved->id);
m_currentMap = resolved;
} else {
spdlog::warn("Web radar: unrecognised server map name '{}' — keeping current map",
*update.serverMapName);
}
}
// Rate-limit JSON serialization. At 60 Hz with large entity counts
// (modded server, built bases) BuildStateJson can take several ms and
// produce hundreds of KB per call. 10 Hz is more than enough for a map.
if (now < m_nextSnapshotMs) return;
m_nextSnapshotMs = now + m_config.snapshotIntervalMs;
std::string jsonStr = m_snapshotSvc.BuildStateJson(
update, m_bullets.GetSnapshot(), m_currentMap, now);
{
std::lock_guard<std::mutex> lk(m_cvMutex);
// Bootstrap only changes when the active map changes — don't rebuild
// it every tick.
std::string mapId = m_currentMap ? m_currentMap->id : "";
if (mapId != m_lastBootstrapMapId) {
m_bootstrapJson = m_snapshotSvc.BuildBootstrapJson(
update, m_currentMap, m_config.port);
m_lastBootstrapMapId = std::move(mapId);
}
m_latestJson = std::move(jsonStr);
m_latestPayload = "event: state\ndata: " + m_latestJson + "\n\n";
++m_broadcastSeq;
}
m_cv.notify_all();
}
// -------------------------------------------------------------------------
// SetupRoutes
// -------------------------------------------------------------------------
void WebRadarServer::SetupRoutes() {
// ---- Static files ---------------------------------------------------
m_server->Get("/", [this](const httplib::Request& /*req*/,
httplib::Response& res) {
ServeFile(res, "index.html", "text/html; charset=utf-8");
});
m_server->Get("/app.js", [this](const httplib::Request& /*req*/,
httplib::Response& res) {
ServeFile(res, "app.js", "application/javascript; charset=utf-8");
});
m_server->Get("/style.css", [this](const httplib::Request& /*req*/,
httplib::Response& res) {
ServeFile(res, "style.css", "text/css; charset=utf-8");
});
m_server->Get("/favicon.ico", [this](const httplib::Request& /*req*/,
httplib::Response& res) {
ServeFile(res, "favicon.ico", "image/x-icon");
});
// ---- Bootstrap ------------------------------------------------------
m_server->Get("/api/bootstrap",
[this](const httplib::Request& req, httplib::Response& res) {
if (!Authorise(req.remote_addr,
req.get_param_value("password"))) {
res.status = 401;
return;
}
std::lock_guard<std::mutex> lk(m_cvMutex);
res.set_content(m_bootstrapJson, "application/json");
});
// ---- Polling state --------------------------------------------------
m_server->Get("/api/state",
[this](const httplib::Request& req, httplib::Response& res) {
if (!Authorise(req.remote_addr,
req.get_param_value("password"))) {
res.status = 401;
return;
}
std::lock_guard<std::mutex> lk(m_cvMutex);
res.set_content(m_latestJson, "application/json");
});
// ---- SSE stream -----------------------------------------------------
m_server->Get("/events",
[this](const httplib::Request& req, httplib::Response& res) {
if (!Authorise(req.remote_addr,
req.get_param_value("password"))) {
res.status = 401;
return;
}
res.set_header("Cache-Control", "no-cache");
res.set_header("X-Accel-Buffering", "no");
auto lastSeq = std::make_shared<int64_t>(-1);
res.set_chunked_content_provider(
"text/event-stream",
[this, lastSeq](size_t /*offset*/,
httplib::DataSink& sink) -> bool {
if (m_stopping.load()) return false;
std::string payload;
{
std::unique_lock<std::mutex> lk(m_cvMutex);
m_cv.wait_for(lk, std::chrono::seconds(10),
[this, &lastSeq] {
return m_broadcastSeq != *lastSeq
|| m_stopping.load();
});
if (m_stopping.load()) return false;
if (m_broadcastSeq == *lastSeq) {
// Keepalive comment to prevent proxy timeouts.
payload = ": keepalive\n\n";
} else {
payload = m_latestPayload;
*lastSeq = m_broadcastSeq;
}
}
return sink.write(payload.c_str(), payload.size());
}
);
});
// ---- Map tile -------------------------------------------------------
m_server->Get("/tile",
[this](const httplib::Request& req, httplib::Response& res) {
if (!Authorise(req.remote_addr,
req.get_param_value("password"))) {
res.status = 401;
return;
}
// Prefer the explicit mapId param (sent by app.js); fall back to
// the currently active map resolved from the game session.
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_tiles.GetTile(*tileMap, tx, ty, code);
res.status = code;
if (code == 200) {
// Cache tiles aggressively — they're static PNG slices that only
// change when the map PNG is replaced (different URL via mapId param).
// Cloudflare tunnel and browsers will both benefit from this.
res.set_header("Cache-Control", "public, max-age=86400");
res.set_header("Vary", "Accept-Encoding");
res.set_content(
std::string(bytes.begin(), bytes.end()),
"image/png");
}
});
// ---- Full map image -------------------------------------------------
m_server->Get("/map-image",
[this](const httplib::Request& req, httplib::Response& res) {
if (!Authorise(req.remote_addr,
req.get_param_value("password"))) {
res.status = 401;
return;
}
const MapInfo* imgMap = nullptr;
std::string mapIdParam = req.get_param_value("mapId");
if (!mapIdParam.empty())
imgMap = MapRegistry::Resolve(mapIdParam);
if (!imgMap)
imgMap = m_currentMap;
if (!imgMap) {
res.status = 404;
return;
}
wchar_t mapPathBuf[MAX_PATH] = {};
GetModuleFileNameW(nullptr, mapPathBuf, MAX_PATH);
std::string path = (std::filesystem::path(mapPathBuf).parent_path()
/ "maps" / (imgMap->id + ".png")).string();
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), "image/png");
});
// ---- 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
// the bootstrap JSON currently contains.
m_server->Get("/api/debug",
[this](const httplib::Request& /*req*/, httplib::Response& res) {
namespace fs = std::filesystem;
wchar_t exeBuf[MAX_PATH] = {};
GetModuleFileNameW(nullptr, exeBuf, MAX_PATH);
fs::path exeDir = fs::path(exeBuf).parent_path();
fs::path mapsDir = exeDir / "maps";
// Escape a string for inline JSON (handles backslashes in Windows paths).
auto jsStr = [](const std::string& s) {
std::string r;
r.reserve(s.size() + 4);
for (char c : s) {
if (c == '\\') r += "\\\\";
else if (c == '"') r += "\\\"";
else r += c;
}
return r;
};
std::ostringstream j;
j << "{\n";
j << " \"exeDir\": \"" << jsStr(exeDir.string()) << "\",\n";
j << " \"mapsDir\": \"" << jsStr(mapsDir.string()) << "\",\n";
j << " \"mapsDirExists\": " << (fs::exists(mapsDir) ? "true" : "false") << ",\n";
j << " \"currentMapId\": \"" << (m_currentMap ? m_currentMap->id : "") << "\",\n";
// PNGs actually present in maps/.
j << " \"pngsFound\": [";
{
std::error_code ec;
bool first = true;
for (const auto& e : fs::directory_iterator(mapsDir, ec)) {
if (!ec && e.path().extension() == ".png") {
if (!first) j << ", ";
j << "\"" << jsStr(e.path().filename().string()) << "\"";
first = false;
}
}
}
j << "],\n";
// All known map IDs with whether their PNG was found.
j << " \"knownMaps\": [";
{
int mapCount = 0;
const MapInfo* allMaps = MapRegistry::AllMaps(mapCount);
for (int i = 0; i < mapCount; ++i) {
if (i > 0) j << ", ";
j << "{\"id\": \"" << allMaps[i].id << "\", \"hasImage\": "
<< (MapRegistry::HasMapImage(allMaps[i]) ? "true" : "false") << "}";
}
}
j << "]\n}";
res.set_content(j.str(), "application/json");
});
}
// -------------------------------------------------------------------------
// ServeFile
// -------------------------------------------------------------------------
void WebRadarServer::ServeFile(httplib::Response& res,
const std::string& filename,
const std::string& contentType)
{
std::string path = WebrootPath(filename);
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);
}
// -------------------------------------------------------------------------
// Authorise
// -------------------------------------------------------------------------
bool WebRadarServer::Authorise(const std::string& addr,
const std::string& pwd) const
{
// Always allow loopback connections.
if (addr == "127.0.0.1" || addr == "::1" || addr == "localhost")
return true;
// If no password configured, allow everyone.
if (m_config.password.empty())
return true;
return pwd == m_config.password;
}
// -------------------------------------------------------------------------
// WebrootPath
// -------------------------------------------------------------------------
std::string WebRadarServer::WebrootPath(const std::string& file) const {
wchar_t buf[MAX_PATH] = {};
GetModuleFileNameW(nullptr, buf, MAX_PATH);
// Convert wide string to narrow.
std::wstring wpath(buf);
// Strip the filename component to get the exe directory.
auto lastSlash = wpath.find_last_of(L"\\/");
if (lastSlash != std::wstring::npos)
wpath = wpath.substr(0, lastSlash + 1);
// Convert to std::string (ASCII/UTF-8 — path characters only).
std::string exeDir(wpath.begin(), wpath.end());
return exeDir + "webroot\\" + file;
}
// -------------------------------------------------------------------------
// NowMs
// -------------------------------------------------------------------------
int64_t WebRadarServer::NowMs() {
using namespace std::chrono;
return duration_cast<milliseconds>(
system_clock::now().time_since_epoch()).count();
}
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include "Web/BulletTrackCache.h"
#include "Web/MapRegistry.h"
#include "Web/MapTileService.h"
#include "Web/WebSnapshotService.h"
// Forward-declare httplib types to avoid pulling the whole header (which
// includes <Winsock2.h>) into every translation unit that includes this header.
namespace httplib { class Server; struct Response; }
// -------------------------------------------------------------------------
// WebRadarConfig — tuneable startup parameters.
// -------------------------------------------------------------------------
struct WebRadarConfig {
std::string bindAddress = "0.0.0.0";
int port = 7777;
std::string password = "";
int snapshotIntervalMs = 100; // max web snapshot rate (default 10 Hz)
};
// -------------------------------------------------------------------------
// WebRadarServer
// -------------------------------------------------------------------------
class WebRadarServer {
public:
explicit WebRadarServer(WebRadarConfig config = {});
~WebRadarServer();
WebRadarServer(const WebRadarServer&) = delete;
WebRadarServer& operator=(const WebRadarServer&) = delete;
/// Start the HTTP server on a background thread.
void Start();
/// Stop the HTTP server and join the background thread.
void Stop();
/// Feed a new RuntimeUpdate into the server. Thread-safe.
void PushSnapshot(const RuntimeUpdate& update);
private:
WebRadarConfig m_config;
WebSnapshotService m_snapshotSvc;
BulletTrackCache m_bullets;
MapTileService m_tiles;
const MapInfo* m_currentMap = nullptr;
std::unique_ptr<httplib::Server> m_server;
std::thread m_thread;
// SSE broadcast state.
mutable std::mutex m_cvMutex;
std::condition_variable m_cv;
std::string m_latestPayload; // full "event: state\ndata: ...\n\n"
std::string m_latestJson; // JSON only (for /api/state polling)
std::string m_bootstrapJson; // /api/bootstrap response
int64_t m_broadcastSeq = 0;
std::atomic<bool> m_stopping{false};
int64_t m_nextSnapshotMs = 0; // rate-limit for JSON builds
std::string m_lastBootstrapMapId; // only rebuild bootstrap on map change
void SetupRoutes();
void ServeFile(httplib::Response& res, const std::string& filename,
const std::string& contentType);
/// Returns true if the request is permitted (localhost, no password, or
/// correct password query parameter).
bool Authorise(const std::string& remoteAddr,
const std::string& passwordParam) const;
/// Absolute path to webroot/<file> relative to the executable directory.
std::string WebrootPath(const std::string& file) const;
static int64_t NowMs();
};
+453
View File
@@ -0,0 +1,453 @@
#include "Web/WebSnapshotService.h"
#include <cmath>
#include <format>
#include <string>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
static constexpr float kPi = 3.14159265358979323846f;
/// Convert a world address to a hex-string id (e.g. "1A2B3C4D").
static std::string AddrId(uint64_t address) {
return std::format("{:X}", address);
}
/// Translate world-space (x, z) to map-image space.
/// If map is null, returns the raw coordinates.
static std::pair<float, float> WorldToMap(const MapInfo* map,
float worldX, float worldZ)
{
float ox = worldX, oy = worldZ;
if (map)
MapRegistry::Translate(*map, worldX, worldZ, ox, oy);
return { ox, oy };
}
// -------------------------------------------------------------------------
// Entity JSON builders
// -------------------------------------------------------------------------
static json PlayerEntityJson(const DayZPlayerEntry& p, const MapInfo* map) {
json obj;
obj["id"] = AddrId(p.address);
obj["kind"] = "player";
// Label: prefer nickname, fall back to typeName.
obj["label"] = p.nickname.empty() ? p.typeName : p.nickname;
if (p.position.has_value()) {
auto [mx, my] = WorldToMap(map, p.position->x, p.position->z);
obj["x"] = mx;
obj["y"] = my;
} else {
obj["x"] = 0.0f;
obj["y"] = 0.0f;
}
// headingDegrees is already in degrees from the reader.
obj["rotation"] = p.headingDegrees.value_or(0.0f);
obj["distance"] = -1;
obj["networkId"] = p.networkId;
obj["steamId"] = "";
obj["dead"] = p.isDead;
obj["handItem"] = p.itemInHands;
obj["visibleOnMap"] = true;
return obj;
}
static json PlayerListEntryJson(const DayZPlayerEntry& p) {
json obj;
obj["networkId"] = p.networkId;
obj["label"] = p.nickname.empty() ? p.typeName : p.nickname;
obj["steamId"] = "";
obj["visibleOnMap"] = true;
obj["dead"] = p.isDead;
obj["handItem"] = p.itemInHands;
obj["distance"] = -1;
return obj;
}
static json ZombieEntityJson(const DayZZombieEntry& z, const MapInfo* map) {
json obj;
obj["id"] = AddrId(z.address);
obj["kind"] = "zombie";
obj["label"] = z.entityName.empty() ? z.typeName : z.entityName;
if (z.position.has_value()) {
auto [mx, my] = WorldToMap(map, z.position->x, z.position->z);
obj["x"] = mx;
obj["y"] = my;
} else {
obj["x"] = 0.0f;
obj["y"] = 0.0f;
}
obj["rotation"] = z.headingDegrees.value_or(0.0f);
obj["distance"] = -1;
obj["visibleOnMap"] = true;
return obj;
}
static json AnimalEntityJson(const DayZAnimalEntry& a, const MapInfo* map) {
json obj;
obj["id"] = AddrId(a.address);
obj["kind"] = "animal";
obj["label"] = a.entityName.empty() ? a.typeName : a.entityName;
if (a.position.has_value()) {
auto [mx, my] = WorldToMap(map, a.position->x, a.position->z);
obj["x"] = mx;
obj["y"] = my;
} else {
obj["x"] = 0.0f;
obj["y"] = 0.0f;
}
obj["rotation"] = a.headingDegrees.value_or(0.0f);
obj["distance"] = -1;
obj["visibleOnMap"] = true;
return obj;
}
static json VehicleEntityJson(const DayZCarAndBoatEntry& v, const MapInfo* map) {
json obj;
obj["id"] = AddrId(v.address);
obj["kind"] = "vehicle";
obj["label"] = v.entityName.empty() ? v.typeName : v.entityName;
if (v.position.has_value()) {
auto [mx, my] = WorldToMap(map, v.position->x, v.position->z);
obj["x"] = mx;
obj["y"] = my;
} else {
obj["x"] = 0.0f;
obj["y"] = 0.0f;
}
obj["rotation"] = v.headingDegrees.value_or(0.0f);
obj["distance"] = -1;
obj["visibleOnMap"] = true;
return obj;
}
static json OtherEntityJson(const DayZOtherEntityEntry& e, const MapInfo* map) {
json obj;
obj["id"] = AddrId(e.address);
obj["kind"] = "other-entity";
obj["label"] = e.entityName.empty() ? e.typeName : e.entityName;
if (e.position.has_value()) {
auto [mx, my] = WorldToMap(map, e.position->x, e.position->z);
obj["x"] = mx;
obj["y"] = my;
} else {
obj["x"] = 0.0f;
obj["y"] = 0.0f;
}
obj["rotation"] = e.headingDegrees.value_or(0.0f);
obj["distance"] = -1;
obj["visibleOnMap"] = true;
return obj;
}
static json LootEntityJson(const DayZItemListEntry& item, const MapInfo* map) {
json obj;
obj["id"] = AddrId(item.address);
obj["kind"] = "loot";
obj["label"] = item.cleanName.empty() ? item.entityName : item.cleanName;
if (item.position.has_value()) {
auto [mx, my] = WorldToMap(map, item.position->x, item.position->z);
obj["x"] = mx;
obj["y"] = my;
} else {
obj["x"] = 0.0f;
obj["y"] = 0.0f;
}
obj["rotation"] = 0.0f;
obj["distance"] = -1;
obj["visibleOnMap"] = true;
obj["lootCategory"] = item.filterKey; // filterKey = lowercase composite from item_filters.json
return obj;
}
// -------------------------------------------------------------------------
// Bullet JSON builder
// -------------------------------------------------------------------------
static json BulletJson(const BulletTrack& t, const MapInfo* map) {
json obj;
obj["id"] = AddrId(t.address);
obj["label"] = "Bullet";
obj["kind"] = "bullet";
auto [cx, cy] = WorldToMap(map, t.currentPos.x, t.currentPos.z);
obj["x"] = cx;
obj["y"] = cy;
// Rotation from lastDir: atan2(x, z) gives bearing in radians; convert to degrees.
float bearing = std::atan2(t.lastDir.x, t.lastDir.z) * (180.0f / kPi);
obj["rotation"] = bearing;
obj["distance"] = -1;
obj["visibleOnMap"] = true;
obj["isActive"] = t.isActive;
obj["isPhantom"] = t.isPhantom;
obj["isCompleted"] = t.isCompleted;
obj["firstSeenAtUtcMs"] = t.firstSeenMs;
obj["lastSeenAtUtcMs"] = t.lastSeenMs;
if (t.isCompleted)
obj["finalizedAtUtcMs"] = t.finalizedMs;
else
obj["finalizedAtUtcMs"] = nullptr;
obj["predictionSpeed"] = t.speedMps;
obj["directionSource"] = "velocity";
// initialPosition
if (t.firstSeenMs > 0) {
auto [ix, iy] = WorldToMap(map, t.initialPos.x, t.initialPos.z);
obj["initialPosition"] = { {"x", ix}, {"y", iy} };
} else {
obj["initialPosition"] = nullptr;
}
// finalPosition
if (t.isCompleted) {
auto [fx, fy] = WorldToMap(map, t.finalPos.x, t.finalPos.z);
obj["finalPosition"] = { {"x", fx}, {"y", fy} };
} else {
obj["finalPosition"] = nullptr;
}
// initialDirection (3-D, world space)
obj["initialDirection"] = {
{"x", t.initialDir.x},
{"y", t.initialDir.y},
{"z", t.initialDir.z}
};
// lastDirection
obj["lastDirection"] = {
{"x", t.lastDir.x},
{"y", t.lastDir.y},
{"z", t.lastDir.z}
};
// Path (map-projected 2-D)
json path = json::array();
for (const auto& [ts, pos] : t.points) {
auto [px, py] = WorldToMap(map, pos.x, pos.z);
path.push_back({ {"x", px}, {"y", py} });
}
obj["path"] = std::move(path);
// Predicted path: 20 points at 15 m intervals, up to 300 m.
json predictedPath = json::array();
bool doPredict = !t.isCompleted && t.speedMps >= 1.0f;
if (doPredict) {
Vector3 cur = t.currentPos;
float step = 15.0f;
int nPts = 20;
for (int i = 1; i <= nPts; ++i) {
float dist = step * static_cast<float>(i);
if (dist > 300.0f) break;
float px = cur.x + t.lastDir.x * dist;
float py_val = cur.y + t.lastDir.y * dist;
float pz = cur.z + t.lastDir.z * dist;
(void)py_val;
auto [mx, my] = WorldToMap(map, px, pz);
predictedPath.push_back({ {"x", mx}, {"y", my} });
}
}
obj["predictedPath"] = std::move(predictedPath);
return obj;
}
// -------------------------------------------------------------------------
// WebSnapshotService::BuildStateJson
// -------------------------------------------------------------------------
std::string WebSnapshotService::BuildStateJson(const RuntimeUpdate& update,
const std::vector<BulletTrack>& bullets,
const MapInfo* map,
int64_t nowMs)
{
(void)nowMs;
int64_t seq = m_seq.fetch_add(1, std::memory_order_relaxed);
int mapSize = map ? map->worldSize : 15360;
int tileCount = (mapSize + 511) / 512;
json j;
j["cacheRefreshSequence"] = seq;
j["fastCacheRefreshSequence"] = seq;
j["slowCacheRefreshSequence"] = seq;
j["mapId"] = map ? map->id : "";
j["mapName"] = map ? map->name : "";
j["mapSize"] = mapSize;
j["tileSize"] = 512;
j["tileCountX"] = tileCount;
j["tileCountY"] = tileCount;
j["serverMapName"] = update.serverMapName.value_or("");
j["serverName"] = update.serverName.value_or("");
j["gameVersion"] = update.gameVersion.value_or("");
j["isAttached"] = update.areBaseObjectsReady;
j["isUsingMockData"] = false;
j["status"] = update.status;
bool hasLocal = update.localPlayerPosition.has_value();
j["hasLocalPlayer"] = hasLocal;
if (hasLocal) {
const Vector3& lp = *update.localPlayerPosition;
auto [lx, ly] = WorldToMap(map, lp.x, lp.z);
j["localPlayer"] = {
{ "x", lx },
{ "y", ly },
{ "rotation", update.localPlayerLookDirection.value_or(0.0f) },
{ "cameraRotation", 0.0 }
};
} else {
j["localPlayer"] = nullptr;
}
// playerList
json playerList = json::array();
for (const auto& p : update.players)
playerList.push_back(PlayerListEntryJson(p));
j["playerList"] = std::move(playerList);
// players
json players = json::array();
for (const auto& p : update.players)
players.push_back(PlayerEntityJson(p, map));
j["players"] = std::move(players);
// zombies
json zombies = json::array();
for (const auto& z : update.zombies)
zombies.push_back(ZombieEntityJson(z, map));
j["zombies"] = std::move(zombies);
// animals
json animals = json::array();
for (const auto& a : update.animals)
animals.push_back(AnimalEntityJson(a, map));
j["animals"] = std::move(animals);
// vehicles
json vehicles = json::array();
for (const auto& v : update.carsAndBoats)
vehicles.push_back(VehicleEntityJson(v, map));
j["vehicles"] = std::move(vehicles);
// otherEntities
json others = json::array();
for (const auto& e : update.otherEntities)
others.push_back(OtherEntityJson(e, map));
j["otherEntities"] = std::move(others);
// loot
json loot = json::array();
for (const auto& item : update.items)
loot.push_back(LootEntityJson(item, map));
j["loot"] = std::move(loot);
// bullets
json bulletArr = json::array();
for (const auto& t : bullets)
bulletArr.push_back(BulletJson(t, map));
j["bullets"] = std::move(bulletArr);
return j.dump();
}
// -------------------------------------------------------------------------
// WebSnapshotService::BuildBootstrapJson
// -------------------------------------------------------------------------
std::string WebSnapshotService::BuildBootstrapJson(const RuntimeUpdate& update,
const MapInfo* map,
int port)
{
int mapSize = map ? map->worldSize : 15360;
int tileCount = (mapSize + 511) / 512;
json j;
j["mapId"] = map ? map->id : "";
j["mapName"] = map ? map->name : "";
j["serverMapName"] = update.serverMapName.value_or("");
j["mapSize"] = mapSize;
j["tileSize"] = 512;
j["tileCountX"] = tileCount;
j["tileCountY"] = tileCount;
j["server"] = { { "port", port } };
// All known maps.
int mapCount = 0;
const MapInfo* allMaps = MapRegistry::AllMaps(mapCount);
json mapsArr = json::array();
for (int i = 0; i < mapCount; ++i) {
const MapInfo& m = allMaps[i];
mapsArr.push_back({
{ "id", m.id },
{ "name", m.name },
{ "worldSize", m.worldSize },
{ "index", m.index },
{ "hasImage", MapRegistry::HasMapImage(m) }
});
}
j["maps"] = std::move(mapsArr);
// Entity filter presets (fixed).
json filters = json::array({
{ {"key","player"}, {"label","Players"}, {"kind","player"}, {"color","#ff8800"} },
{ {"key","zombie"}, {"label","Zombies"}, {"kind","zombie"}, {"color","#00cc44"} },
{ {"key","animal"}, {"label","Animals"}, {"kind","animal"}, {"color","#44aaff"} },
{ {"key","vehicle"}, {"label","Vehicles"}, {"kind","vehicle"}, {"color","#aa44ff"} },
{ {"key","bullet"}, {"label","Bullets"}, {"kind","bullet"}, {"color","#ffff00"} },
});
// Per-category loot filters — built from item_filters.json so that
// app.js can map item.lootCategory → the correct colour/label.
for (const auto& def : m_catalog.GetFilters()) {
json f;
f["key"] = "loot_" + def.key; // unique filter key
f["label"] = def.description.empty() ? def.key : def.description;
f["kind"] = "loot";
f["category"] = def.key; // must match item.lootCategory
f["color"] = def.color.value_or("#ffffff");
filters.push_back(std::move(f));
}
// Fallback "other" loot bucket for items that don't match any filter.
filters.push_back({
{"key", "loot_other"},
{"label", "Other"},
{"kind", "loot"},
{"category", ItemFilterCatalog::kFallbackKey},
{"color", "#888888"}
});
j["filters"] = std::move(filters);
return j.dump();
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <string>
#include <vector>
#include "Web/BulletTrackCache.h"
#include "Web/MapRegistry.h"
#include "Readers/ItemFilterCatalog.h"
#include "Runtime/DayZRuntimeService.h"
// -------------------------------------------------------------------------
// WebSnapshotService
// Converts RuntimeUpdate snapshots into JSON strings consumed by the
// browser radar client.
// -------------------------------------------------------------------------
class WebSnapshotService {
public:
/// Build the main state JSON sent on every SSE push / polling request.
std::string BuildStateJson(const RuntimeUpdate& update,
const std::vector<BulletTrack>& bullets,
const MapInfo* map,
int64_t nowMs);
/// Build the one-time bootstrap JSON (map list, filter presets, server info).
std::string BuildBootstrapJson(const RuntimeUpdate& update,
const MapInfo* map,
int port);
private:
std::atomic<int64_t> m_seq{0};
ItemFilterCatalog m_catalog; // loaded from config/item_filters.json
};
+20
View File
@@ -0,0 +1,20 @@
// Single-translation-unit stb implementation.
// All other TUs must include stb_image / stb_image_write WITHOUT defining
// the implementation macros.
#ifdef _MSC_VER
# pragma warning(push)
# pragma warning(disable: 4244 4996 4100)
#endif
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STBI_ONLY_PNG
#define STBI_ONLY_JPEG
#include <stb_image.h>
#include <stb_image_write.h>
#ifdef _MSC_VER
# pragma warning(pop)
#endif
+128
View File
@@ -0,0 +1,128 @@
#include <atomic>
#include <chrono>
#include <filesystem>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
#include <imgui.h>
#include "Config.h"
#include "Logger.h"
#include "Overlay/GameOverlay.h"
#include "Overlay/OverlayWindow.h"
#include "Runtime/DayZRuntimeService.h"
#include "Web/WebRadarServer.h"
// Shared stop flag so the console Ctrl+C / close handler can reach it.
static std::atomic<bool>* g_stopFlag = nullptr;
static BOOL WINAPI ConsoleCtrlHandler(DWORD signal) {
if (signal == CTRL_C_EVENT || signal == CTRL_CLOSE_EVENT ||
signal == CTRL_BREAK_EVENT)
{
if (g_stopFlag) g_stopFlag->store(true);
return TRUE;
}
return FALSE;
}
int main() {
std::filesystem::create_directories("logs");
AppLogger::Init();
auto log = AppLogger::Get();
log->info("DayZ Memory Reader - Starting");
RuntimeConfig rtCfg = RuntimeConfig::Load("config/config.cfg");
DayZRuntimeService service(rtCfg);
// -------------------------------------------------------------------------
// Config — loaded from config/overlay.json (defaults if missing).
// -------------------------------------------------------------------------
static const std::string kCfgPath = "config/overlay.json";
OverlayConfig cfg = OverlayConfig::Load(kCfgPath);
// -------------------------------------------------------------------------
// Web radar server — settings come from config.
// -------------------------------------------------------------------------
WebRadarConfig webCfg;
webCfg.bindAddress = cfg.webBindAddress;
webCfg.port = cfg.webPort;
webCfg.password = cfg.webPassword;
WebRadarServer webRadar(webCfg);
webRadar.Start();
// -------------------------------------------------------------------------
// Runtime service callback: log stats + push snapshot to web radar.
// -------------------------------------------------------------------------
service.SetUpdateCallback([&log, &webRadar](const RuntimeUpdate& update) {
// Always push to the web radar (even when not yet attached) so the
// browser client sees connection/status changes immediately.
webRadar.PushSnapshot(update);
if (!update.areBaseObjectsReady) {
// Status messages are already de-duplicated inside WaitForSession.
return;
}
// Periodic heartbeat at most once every 10 seconds. Kept terse — the
// interesting per-event detail (admins detected, skeletons lost) is logged
// as it happens by the runtime service, not polled here.
using Clock = std::chrono::steady_clock;
static auto nextLog = Clock::time_point{};
auto now = Clock::now();
if (now < nextLog) return;
nextLog = now + std::chrono::seconds(10);
// Skeleton coverage: how many in-range players currently have valid bones.
size_t skelOk = 0, admins = 0;
for (const auto& p : update.players) {
if (p.skeleton.valid) ++skelOk;
if (p.isAdmin) ++admins;
}
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();
// Overlay polls GetLatestUpdate() each frame; no extra callback needed.
std::atomic<bool> stopFlag{false};
g_stopFlag = &stopFlag;
GameOverlay overlay(service, cfg, kCfgPath);
overlay.SetWebRadarPort(webCfg.port);
overlay.SetExitCallback([&stopFlag]() { stopFlag.store(true); });
OverlayWindow window;
window.SetResolutionOverride(cfg.overlayWidth, cfg.overlayHeight);
overlay.SetResizeCallback([&window](int w, int h) { window.ResizeTo(w, h); });
window.SetDrawCallback([&overlay](float w, float h) {
overlay.Draw(w, h);
});
window.SetInputQueryCallback([&overlay]() {
return overlay.IsMenuOpen();
});
window.SetFontReadyCallback([&overlay](ImFont* close, ImFont* lootFar) {
overlay.SetLootFonts(close, lootFar);
});
SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE);
// Blocks until the overlay window is closed or stopFlag is set externally.
window.Run(stopFlag);
webRadar.Stop();
service.Stop();
log->info("Stopped.");
spdlog::shutdown();
return 0;
}