Add worn player item snapshots and refresh DayZ offsets

- Resolve worn clothing from player inventory and expose it in web snapshots
- Keep far entities alive through transient transform/position read misses
- Refresh v1.29 health, third-person, weapon, magazine, ammo, and stat offsets
- Force full skeleton cache re-probe when stale bone data is detected
- Remove status styling from ESP tab toggles
This commit is contained in:
67
2026-06-17 20:08:21 +08:00
parent f04e38b8ae
commit 7945bc6536
10 changed files with 151 additions and 41 deletions
+4
View File
@@ -9,3 +9,7 @@ out/
*.exe
*.pdb
*.ilk
*.md
# Keep these specific files tracked
!README.md
+5
View File
@@ -1,3 +1,8 @@
# TODO
(none)
item esp doesnt render past a certain distance
inventory detection
ballistic prediction
combat mode on web radar
create a grid coord system for web radar
+2 -2
View File
@@ -230,7 +230,7 @@ static void render_esp_tab(const visual_widget_filter& w, float section_height)
gui->begin_group();
{
begin_visual_section("Entities", section_height);
w.checkbox("Players", "Show player ESP", g_menu->showPlayers, title_status_safe);
w.checkbox("Players", "Show player ESP", g_menu->showPlayers);
w.checkbox("Animals", "Show animal ESP", g_menu->showAnimals);
w.checkbox("Zombies", "Show infected ESP", g_menu->showZombies);
w.checkbox("Items", "Show loot ESP", g_menu->showItems);
@@ -241,7 +241,7 @@ static void render_esp_tab(const visual_widget_filter& w, float section_height)
w.checkbox("Weapon In Hand", "Show held weapon name", g_menu->showWeapon);
w.checkbox("Health Bar", "Draw player health bar", g_menu->showHealthBar);
w.checkbox("Health Number", "Draw numeric health", g_menu->showHealthNumber);
w.checkbox("Skeleton Debug", "Label every named bone", g_menu->debugSkeleton, title_status_warning);
w.checkbox("Skeleton Debug", "Label every named bone", g_menu->debugSkeleton);
end_visual_section();
}
gui->end_group();
+3 -1
View File
@@ -1,7 +1,8 @@
#pragma once
#include <cstdint>
#include <string>
#include <optional>
#include <string>
#include <vector>
// -------------------------------------------------------------------------
// Primitive math types
@@ -129,6 +130,7 @@ struct DayZPlayerEntry {
bool isAdmin = false; // model matches a known invisible/admin model path
std::string nickname;
std::string itemInHands;
std::vector<std::string> wornItems;
std::string typeName;
std::string configName;
std::string modelName;
+43 -5
View File
@@ -73,7 +73,9 @@ namespace Offsets {
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]
// 0x9C is the adjacent sibling DWORD — confirmed wrong by UC dumper r15 (2026-06-15).
// Correct value is MissionHeader::is_third_person_disabled at 0x74.
constexpr uint64_t ThirdPerson = 0x74; // v1.29 [UC dumper r15 2026-06-15]
constexpr uint64_t Crosshair = 0xA0; // v1.29 [manual]
}
@@ -107,10 +109,46 @@ namespace Offsets {
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;
// Damage manager — pointer to health/blood zone manager.
// Chain: entity+0x700 → damage manager → zone array → per-zone float health.
constexpr uint64_t DamageManager = 0x700; // v1.29 [UC dumper r15 2026-06-15]
// Player stats hash map container. Chain: entity+0x6F0 → stats container → records.
constexpr uint64_t StatsContainer = 0x6F0; // v1.29 [UC dumper r15 2026-06-15]
// Health is the Quality float at entity+0x194 (same field as Inventory::ItemQuality).
// Values are discrete increments of 0.25f: 0.25 | 0.50 | 0.75 | 1.0
// (maps to critically injured / badly injured / injured / healthy).
// Valid for both players and zombies. Read as float, multiply by 100 for display.
constexpr uint64_t Health = 0x194; // v1.29 [operator-confirmed 2026-06-17]
}
// Player stat record — offset of the float value field within a StatRecord object.
// Confirmed: PlayerStats::AddDelta at VA 0x6ABA30 — `addss xmm1,[rcx+0x2C]; movss [rcx+0x2C],xmm1; ret`
namespace PlayerStats {
constexpr uint64_t RecordValue = 0x2C; // v1.29 [UC dumper r15 2026-06-15]
}
// Weapon entity struct offsets (entity is already an InventoryItem with a type ptr at +0x180).
// Chain to magazine: entity+0x1B0 (ChamberedPtr) → chambered item → magazine.
namespace Weapon {
constexpr uint64_t ChamberedPtr = 0x1B0; // v1.29 [UC dumper r15 2026-06-15]
constexpr uint64_t AmmoMagCount = 0x6AC; // v1.29 [UC dumper r15 2026-06-15] — loaded magazine ammo count
constexpr uint64_t AmmoCapacityA = 0x6B0; // v1.29 [UC dumper r15 2026-06-15]
constexpr uint64_t AmmoCapacityB = 0x6B4; // v1.29 [UC dumper r15 2026-06-15]
}
// Magazine entity struct offsets.
// AmmoTypePtr → ammo type object; AmmoCount = current rounds; MaxAmmo = capacity.
namespace Magazine {
constexpr uint64_t AmmoTypePtr = 0x20; // v1.29 [UC dumper r15 2026-06-15]
constexpr uint64_t AmmoCount = 0x3B0; // v1.29 [UC dumper r15 2026-06-15]
constexpr uint64_t MaxAmmo = 0x3A4; // v1.29 [UC dumper r15 2026-06-15]
}
// AmmoType config object offsets (reached via Magazine::AmmoTypePtr).
namespace AmmoType {
constexpr uint64_t InitSpeed = 0x38C; // v1.29 [UC dumper r15 2026-06-15]
constexpr uint64_t AirFriction = 0x3B4; // v1.29 [UC dumper r15 2026-06-15]
constexpr uint64_t Dispersion = 0x3A4; // v1.29 [UC dumper r15 2026-06-15]
}
namespace Infected {
+10 -8
View File
@@ -126,10 +126,11 @@ std::vector<DayZPlayerEntry> EntityCategoryProjector::BuildPlayers(
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)
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,
std::function<std::vector<std::string>(uint64_t)> wornClothesResolver)
{
auto all = EnumerateEntities(near, far, slow);
@@ -159,10 +160,11 @@ std::vector<DayZPlayerEntry> EntityCategoryProjector::BuildPlayers(
}
}
// 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);
// Dead state, held item, health, and worn clothes — 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);
if (wornClothesResolver) entry.wornItems = wornClothesResolver(ep.address);
result.push_back(std::move(entry));
}
+5 -4
View File
@@ -24,10 +24,11 @@ public:
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);
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,
std::function<std::vector<std::string>(uint64_t)> wornClothesResolver = nullptr);
static std::vector<DayZAnimalEntry> BuildAnimals(
const std::vector<DayZNearEntityEntry>& near,
+18 -14
View File
@@ -212,10 +212,9 @@ void FarEntityListReader::ProcessEntity(VmmAccessor& me
cached.entry.modelName = typeMeta.modelName;
cached.lastSeenGeneration = m_scanGeneration;
cached.failedTypeReads = 0;
if (transformOk) {
cached.lastSuccessfulRead = now;
}
// Entity address was present in the pointer table — it is alive regardless
// of whether the transform read succeeded this tick.
cached.lastSuccessfulRead = now;
m_resultDirty = true;
}
@@ -381,20 +380,25 @@ void FarEntityListReader::RefreshPositions(VmmAccessor& mem,
auto now = std::chrono::steady_clock::now();
for (auto& slot : slots) {
if (slot.vsAddr == 0) continue;
auto it = m_entities.find(slot.entityAddr);
if (it == m_entities.end()) continue;
// VS pointer resolved — entity is provably still in the game world.
// Bump the keep-alive stamp regardless of whether the position scatter
// returned a valid vector; a bad position is a transient DMA miss,
// not evidence the entity was removed from the table.
it->second.lastSuccessfulRead = now;
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;
// Position unreadable this tick — keep last known position and
// force a fresh VS re-read next pass in case the pointer moved.
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;
}
it->second.entry.position = slot.posOut;
m_resultDirty = true;
}
}
+59 -7
View File
@@ -632,6 +632,55 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
return hp;
};
// --- wornClothesResolver ---
// Chain: entity → +0x650 (inventory) → +0x150 (clothes grid ptr)
// grid → +0x8 (items array ptr), +0xC (uint32 count)
// items[i*8] → item entity → +0x180 (type) → cleanName
auto wornClothesResolver = [&](uint64_t addr) -> std::vector<std::string> {
std::vector<std::string> result;
uint64_t inventoryAddr = 0;
if (!m_memory.TryReadPointer(pid, addr + RuntimeOffsets::Inventory::Base, inventoryAddr)
|| !MemoryValidation::IsValidUserAddress(inventoryAddr))
return result;
uint64_t clothesGrid = 0;
if (!m_memory.TryReadPointer(pid, inventoryAddr + Offsets::Inventory::WornClothes, clothesGrid)
|| !MemoryValidation::IsValidUserAddress(clothesGrid))
return result;
uint64_t itemsArray = 0;
if (!m_memory.TryReadPointer(pid, clothesGrid + Offsets::Inventory::ItemPtr, itemsArray)
|| !MemoryValidation::IsValidUserAddress(itemsArray))
return result;
uint32_t count = 0;
if (!m_memory.TryReadValue<uint32_t>(pid, clothesGrid + Offsets::Inventory::CargoGridCount, count)
|| count == 0 || count > 32)
return result;
result.reserve(count);
for (uint32_t i = 0; i < count; ++i) {
uint64_t itemAddr = 0;
if (!m_memory.TryReadPointer(pid, itemsArray + i * sizeof(uint64_t), itemAddr)
|| !MemoryValidation::IsValidUserAddress(itemAddr))
continue;
uint64_t typeAddr = 0;
if (!m_memory.TryReadPointer(pid, itemAddr + Offsets::Common::Type, typeAddr)
|| !MemoryValidation::IsValidUserAddress(typeAddr))
continue;
EntityTypeMetadata meta = ReadEntityTypeMetadata(m_memory, pid, typeAddr);
std::string name;
if (!meta.cleanName.empty()) name = meta.cleanName;
else if (!meta.typeName.empty()) name = FormatEntityName(meta.typeName);
else if (!meta.configName.empty()) name = meta.configName;
if (!name.empty()) result.push_back(std::move(name));
}
return result;
};
RuntimeUpdate update;
update.areBaseObjectsReady = true;
update.status = "Live";
@@ -677,7 +726,8 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
update.players = EntityCategoryProjector::BuildPlayers(
m_state.nearEntities, m_state.farEntities, m_state.slowEntities,
scoreboardNames, deadResolver, heldItemResolver, healthResolver, adminResolver);
scoreboardNames, deadResolver, heldItemResolver, healthResolver,
adminResolver, wornClothesResolver);
// ---- Announce newly-detected admins once each (name + how + range) ----
{
@@ -1305,9 +1355,10 @@ void DayZRuntimeService::RefreshBonesScatter(
const float hy = skel.head.y - refY;
const float hz = skel.head.z - refZ;
if (hx*hx + hy*hy + hz*hz > 9.0f) { // > 3 m from VS origin → stale pointer
skel.valid = false;
ed.cache->valid = false; // force re-resolve next frame
lossReason = "stale pointer (head drift > 3m)";
skel.valid = false;
ed.cache->valid = false;
ed.cache->animClass = 0; // skip fast-path; force full re-probe
lossReason = "stale pointer (head drift > 3m)";
}
}
@@ -1319,9 +1370,10 @@ void DayZRuntimeService::RefreshBonesScatter(
// here causes ensureCached to re-read a fresh matBase on the next tick via
// the fast 2-read path (animClass is still cached).
if (skel.valid && vsOk && (skel.head.y - vsY) < 0.3f) {
skel.valid = false;
ed.cache->valid = false;
lossReason = "collapsed skeleton (stale matBase)";
skel.valid = false;
ed.cache->valid = false;
ed.cache->animClass = 0; // stale matBase implies animClass may also be bad; force full re-probe
lossReason = "collapsed skeleton (stale matBase)";
}
// Record presence / log the valid→invalid edge for diagnostics.
+2
View File
@@ -58,6 +58,7 @@ static json PlayerEntityJson(const DayZPlayerEntry& p, const MapInfo* map) {
obj["steamId"] = "";
obj["dead"] = p.isDead;
obj["handItem"] = p.itemInHands;
obj["wornItems"] = p.wornItems;
obj["visibleOnMap"] = true;
return obj;
}
@@ -70,6 +71,7 @@ static json PlayerListEntryJson(const DayZPlayerEntry& p) {
obj["visibleOnMap"] = true;
obj["dead"] = p.isDead;
obj["handItem"] = p.itemInHands;
obj["wornItems"] = p.wornItems;
obj["distance"] = -1;
return obj;
}