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:
@@ -9,3 +9,7 @@ out/
|
||||
*.exe
|
||||
*.pdb
|
||||
*.ilk
|
||||
*.md
|
||||
|
||||
# Keep these specific files tracked
|
||||
!README.md
|
||||
|
||||
@@ -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
|
||||
Vendored
+2
-2
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user