diff --git a/.gitignore b/.gitignore index 85a3aa4..626d4a7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ out/ *.exe *.pdb *.ilk +*.md + +# Keep these specific files tracked +!README.md diff --git a/TODO.md b/TODO.md index 6d68bcd..5fa5263 100644 --- a/TODO.md +++ b/TODO.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 \ No newline at end of file diff --git a/external/lumin/framework/gui.cpp b/external/lumin/framework/gui.cpp index 42933b8..c499f90 100644 --- a/external/lumin/framework/gui.cpp +++ b/external/lumin/framework/gui.cpp @@ -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(); diff --git a/src/Core/Models.h b/src/Core/Models.h index 04a804d..59a4e95 100644 --- a/src/Core/Models.h +++ b/src/Core/Models.h @@ -1,7 +1,8 @@ #pragma once #include -#include #include +#include +#include // ------------------------------------------------------------------------- // 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 wornItems; std::string typeName; std::string configName; std::string modelName; diff --git a/src/Offsets.h b/src/Offsets.h index b811c23..dc21fb3 100644 --- a/src/Offsets.h +++ b/src/Offsets.h @@ -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 { diff --git a/src/Readers/EntityCategoryProjector.cpp b/src/Readers/EntityCategoryProjector.cpp index c604e35..3159403 100644 --- a/src/Readers/EntityCategoryProjector.cpp +++ b/src/Readers/EntityCategoryProjector.cpp @@ -126,10 +126,11 @@ std::vector EntityCategoryProjector::BuildPlayers( const std::vector& far, const std::vector& slow, const std::unordered_map& scoreboardNames, - std::function deadResolver, - std::function heldItemResolver, - std::function healthResolver, - std::function adminResolver) + std::function deadResolver, + std::function heldItemResolver, + std::function healthResolver, + std::function adminResolver, + std::function(uint64_t)> wornClothesResolver) { auto all = EnumerateEntities(near, far, slow); @@ -159,10 +160,11 @@ std::vector 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)); } diff --git a/src/Readers/EntityCategoryProjector.h b/src/Readers/EntityCategoryProjector.h index 67456bc..fd4132d 100644 --- a/src/Readers/EntityCategoryProjector.h +++ b/src/Readers/EntityCategoryProjector.h @@ -24,10 +24,11 @@ public: const std::vector& far, const std::vector& slow, const std::unordered_map& scoreboardNames, - std::function deadResolver, - std::function heldItemResolver, - std::function healthResolver, - std::function adminResolver = nullptr); + std::function deadResolver, + std::function heldItemResolver, + std::function healthResolver, + std::function adminResolver = nullptr, + std::function(uint64_t)> wornClothesResolver = nullptr); static std::vector BuildAnimals( const std::vector& near, diff --git a/src/Readers/FarEntityListReader.cpp b/src/Readers/FarEntityListReader.cpp index 3dfa8ab..19bf64e 100644 --- a/src/Readers/FarEntityListReader.cpp +++ b/src/Readers/FarEntityListReader.cpp @@ -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; } } diff --git a/src/Runtime/DayZRuntimeService.cpp b/src/Runtime/DayZRuntimeService.cpp index a0321b1..7262fdd 100644 --- a/src/Runtime/DayZRuntimeService.cpp +++ b/src/Runtime/DayZRuntimeService.cpp @@ -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::vector 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(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. diff --git a/src/Web/WebSnapshotService.cpp b/src/Web/WebSnapshotService.cpp index a444f6c..3676bd7 100644 --- a/src/Web/WebSnapshotService.cpp +++ b/src/Web/WebSnapshotService.cpp @@ -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; }