From 9d09d6e3dcf8e62dce3c07089062f9f2f4924b10 Mon Sep 17 00:00:00 2001 From: kedou Date: Sat, 8 Aug 2026 12:42:01 +0800 Subject: [PATCH] feat: add runtime guard and world data updates --- .../CFramework.SelfTest.vcxproj | 65 +++ .../CFramework.SelfTest.vcxproj.filters | 18 + CFramework.SelfTest/SelfTest.cpp | 401 ++++++++++++++++++ CFramework.sln | 10 + CFramework/CFramework.vcxproj | 3 + CFramework/CFramework.vcxproj.filters | 15 +- CFramework/Cheat/Aim.cpp | 6 +- CFramework/Cheat/Features.cpp | 8 +- CFramework/Cheat/Framework.h | 27 +- CFramework/Cheat/Menu.cpp | 12 +- .../Cheat/SDK/CContainer/CContainer.cpp | 37 ++ CFramework/Cheat/SDK/CContainer/CContainer.h | 17 + CFramework/Cheat/SDK/CExfil/CExfil.cpp | 99 ++++- CFramework/Cheat/SDK/CExfil/CExfil.h | 34 +- CFramework/Cheat/SDK/CItem/CItem.cpp | 187 +++++++- CFramework/Cheat/SDK/CItem/CItem.h | 35 +- CFramework/Cheat/SDK/GameSDK/GameSDK.cpp | 239 ++++++++--- CFramework/Cheat/SDK/GameSDK/GameSDK.h | 67 +++ CFramework/Cheat/SDK/GameSDK/Offset.h | 30 ++ CFramework/Cheat/SnapshotHealth.h | 43 ++ CFramework/Cheat/UpdateList.cpp | 46 +- CFramework/Cheat/Visual.cpp | 123 +++++- CFramework/Framework/Config/Config.h | 2 + README.md | 29 +- REFACTOR_NOTES.md | 36 +- verify_runtime.ps1 | 77 ++++ 26 files changed, 1507 insertions(+), 159 deletions(-) create mode 100644 CFramework.SelfTest/CFramework.SelfTest.vcxproj create mode 100644 CFramework.SelfTest/CFramework.SelfTest.vcxproj.filters create mode 100644 CFramework.SelfTest/SelfTest.cpp create mode 100644 CFramework/Cheat/SDK/CContainer/CContainer.cpp create mode 100644 CFramework/Cheat/SDK/CContainer/CContainer.h create mode 100644 CFramework/Cheat/SnapshotHealth.h create mode 100644 verify_runtime.ps1 diff --git a/CFramework.SelfTest/CFramework.SelfTest.vcxproj b/CFramework.SelfTest/CFramework.SelfTest.vcxproj new file mode 100644 index 0000000..2c0753e --- /dev/null +++ b/CFramework.SelfTest/CFramework.SelfTest.vcxproj @@ -0,0 +1,65 @@ + + + + DebugWin32 + ReleaseWin32 + Debugx64 + Releasex64 + + + 17.0 + Win32Proj + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D} + CFrameworkSelfTest + 10.0 + CFramework.SelfTest + + + + Application + true + v143 + MultiByte + + + Application + false + v143 + true + MultiByte + + + + + + + + + + + Level3 + true + true + stdcpp17 + _CONSOLE;%(PreprocessorDefinitions) + $(ProjectDir)..\CFramework\Framework\Libs\Include;$(ProjectDir)..\CFramework;%(AdditionalIncludeDirectories) + /utf-8 %(AdditionalOptions) + + + Console + User32.lib;%(AdditionalDependencies) + + + + + GameSDK.cpp + Memory.cpp + + + GameSDK.h + SnapshotHealth.h + Memory.h + + + + diff --git a/CFramework.SelfTest/CFramework.SelfTest.vcxproj.filters b/CFramework.SelfTest/CFramework.SelfTest.vcxproj.filters new file mode 100644 index 0000000..410124e --- /dev/null +++ b/CFramework.SelfTest/CFramework.SelfTest.vcxproj.filters @@ -0,0 +1,18 @@ + + + + {A3B14D42-97EF-4B22-8F08-C3D2EBB40780} + {AA584E7B-2691-498B-AB5E-254A42857A93} + {2B1EC84D-EBA9-42F4-B64B-E1CB68801564} + + + Source Files + SDK + Memory + + + SDK + SDK + Memory + + diff --git a/CFramework.SelfTest/SelfTest.cpp b/CFramework.SelfTest/SelfTest.cpp new file mode 100644 index 0000000..e13d230 --- /dev/null +++ b/CFramework.SelfTest/SelfTest.cpp @@ -0,0 +1,401 @@ +#include "../CFramework/Cheat/SDK/GameSDK/GameSDK.h" +#include "../CFramework/Cheat/SnapshotHealth.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class SparseMemoryReader final : public IGameMemoryReader +{ +public: + explicit SparseMemoryReader(const uintptr_t baseAddress = 0x10000000ULL) + : m_baseAddress(baseAddress) + { + } + + bool TryReadBuffer(const uintptr_t address, void* buffer, const std::size_t size) const noexcept override + { + ++m_readCounts[address]; + auto failure = m_failuresRemaining.find(address); + if (failure != m_failuresRemaining.end() && failure->second > 0) + { + --failure->second; + return false; + } + + if (!Memory::IsUserAddress(address) || !buffer || size == 0) + return false; + + for (std::size_t i = 0; i < size; ++i) + { + if (m_bytes.find(address + i) == m_bytes.end()) + return false; + } + + auto* output = static_cast(buffer); + for (std::size_t i = 0; i < size; ++i) + output[i] = m_bytes.at(address + i); + return true; + } + + bool ReadString(const uintptr_t address, void* buffer, const std::size_t size) const override + { + ++m_readCounts[address]; + if (!Memory::IsUserAddress(address) || !buffer || size == 0) + return false; + + const auto value = m_strings.find(address); + if (value == m_strings.end()) + return false; + + std::memset(buffer, 0, size); + const std::size_t copyLength = (std::min)(size - 1, value->second.size()); + std::memcpy(buffer, value->second.data(), copyLength); + return true; + } + + uintptr_t GetBaseAddress() const noexcept override + { + return m_baseAddress; + } + + template + void Write(const uintptr_t address, const T& value) + { + static_assert(std::is_trivially_copyable_v, "synthetic memory values must be trivially copyable"); + const auto* bytes = reinterpret_cast(&value); + for (std::size_t i = 0; i < sizeof(T); ++i) + m_bytes[address + i] = bytes[i]; + } + + void WriteString(const uintptr_t address, std::string value) + { + m_strings[address] = std::move(value); + } + + void FailNextReads(const uintptr_t address, const std::size_t count) + { + m_failuresRemaining[address] = count; + } + + std::size_t ReadCount(const uintptr_t address) const + { + const auto count = m_readCounts.find(address); + return count == m_readCounts.end() ? 0 : count->second; + } + +private: + uintptr_t m_baseAddress; + std::unordered_map m_bytes; + std::unordered_map m_strings; + mutable std::unordered_map m_failuresRemaining; + mutable std::unordered_map m_readCounts; +}; + +namespace +{ + constexpr uintptr_t kNodeA = 0x30000000ULL; + constexpr uintptr_t kNodeB = 0x30010000ULL; + constexpr uintptr_t kObjectA = 0x31000000ULL; + constexpr uintptr_t kObjectB = 0x31010000ULL; + constexpr uintptr_t kUnreadableNode = 0x32000000ULL; + + void WriteNode(SparseMemoryReader& memory, const uintptr_t node, const uintptr_t next, + const uintptr_t object, const char* name) + { + BaseObject value{}; + value.nextObjectLink = next; + value.object = object; + memory.Write(node, value); + + const uintptr_t nameAddress = object + 0x800; + memory.Write(object + offset::UnityCamera::ObjectName, nameAddress); + if (name) + memory.WriteString(nameAddress, name); + } + + enum class GomLayout + { + ActiveNodesIndirect, + ActiveNodesDirect, + ActiveObjectsDirect, + ActiveObjectsIndirect + }; + + struct ReadyWorldFixture + { + uintptr_t gom{ 0 }; + uintptr_t activeNode{ 0 }; + uintptr_t gameWorld{ 0 }; + uintptr_t localGameWorld{ 0 }; + }; + + ReadyWorldFixture ConfigureReadyWorld(SparseMemoryReader& memory, const GomLayout layout) + { + const uintptr_t base = memory.GetBaseAddress(); + const uintptr_t gomAddress = 0x40000000ULL; + const uintptr_t activeNode = 0x41000000ULL; + const uintptr_t activeSlot = 0x42000000ULL; + const uintptr_t lastSlot = 0x42010000ULL; + const uintptr_t gameWorld = 0x43000000ULL; + const uintptr_t component0 = 0x44000000ULL; + const uintptr_t component1 = 0x44010000ULL; + const uintptr_t localGameWorld = 0x45000000ULL; + + memory.Write(base + offset::dwGameObjectManager, gomAddress); + + GameObjectManager gom{}; + switch (layout) + { + case GomLayout::ActiveNodesIndirect: + gom.ActiveNodes = activeSlot; + gom.LastActiveNode = lastSlot; + memory.Write(activeSlot, activeNode); + memory.Write(lastSlot, activeNode); + break; + case GomLayout::ActiveNodesDirect: + gom.ActiveNodes = activeNode; + gom.LastActiveNode = activeNode; + break; + case GomLayout::ActiveObjectsDirect: + gom.activeObjects = activeNode; + gom.lastActiveObject = activeNode; + break; + case GomLayout::ActiveObjectsIndirect: + gom.activeObjects = activeSlot; + gom.lastActiveObject = lastSlot; + memory.Write(activeSlot, activeNode); + memory.Write(lastSlot, activeNode); + break; + } + memory.Write(gomAddress, gom); + + WriteNode(memory, activeNode, 0, gameWorld, "GameWorld"); + memory.Write(gameWorld + offset::UnityGameWorld::Component0, component0); + memory.Write(component0 + offset::UnityGameWorld::Component1, component1); + memory.Write(component1 + offset::UnityGameWorld::LocalGameWorld, localGameWorld); + + return { gomAddress, activeNode, gameWorld, localGameWorld }; + } + + bool VerifyReadyLayout(const GomLayout layout) + { + SparseMemoryReader memory; + const ReadyWorldFixture fixture = ConfigureReadyWorld(memory, layout); + CGame game(memory); + if (!game.Refresh()) + return false; + + const GameStateSnapshot state = game.GetState(); + return state.refreshStatus == GameRefreshStatus::Ready && + state.initialized && state.inRaid && !state.cameraValid && + state.refreshAttempts == 1 && state.consecutiveRefreshFailures == 0 && + state.gameObjectManager == fixture.gom && + state.activeNodeList == fixture.activeNode && + state.gameWorld == fixture.gameWorld && + state.localGameWorld == fixture.localGameWorld; + } + + bool Near(const float lhs, const float rhs) + { + return std::fabs(lhs - rhs) < 0.001f; + } + + class TestRunner + { + public: + template + void Run(const char* name, Test&& test) + { + bool passed = false; + try + { + passed = test(); + } + catch (const std::exception& error) + { + std::cout << "[FAIL] " << name << " (exception: " << error.what() << ")\n"; + ++m_failed; + return; + } + catch (...) + { + std::cout << "[FAIL] " << name << " (unknown exception)\n"; + ++m_failed; + return; + } + + std::cout << (passed ? "[PASS] " : "[FAIL] ") << name << '\n'; + passed ? ++m_passed : ++m_failed; + } + + int Finish() const + { + std::cout << "\nSummary: " << m_passed << " PASS, " << m_failed << " FAIL\n"; + return m_failed == 0 ? 0 : 1; + } + + private: + int m_passed{ 0 }; + int m_failed{ 0 }; + }; +} + +int main() +{ + TestRunner tests; + + tests.Run("ObjectList target at first node", [] + { + SparseMemoryReader memory; + WriteNode(memory, kNodeA, 0, kObjectA, "GameWorld"); + return GetObjectFromList(memory, kNodeA, kNodeA, "GameWorld") == kObjectA; + }); + + tests.Run("ObjectList accepts last=0", [] + { + SparseMemoryReader memory; + WriteNode(memory, kNodeA, kNodeB, kObjectA, "Other"); + WriteNode(memory, kNodeB, 0, kObjectB, "GameWorld"); + return GetObjectFromList(memory, kNodeA, 0, "GameWorld") == kObjectB; + }); + + tests.Run("ObjectList accepts unreadable last sentinel", [] + { + SparseMemoryReader memory; + WriteNode(memory, kNodeA, kNodeB, kObjectA, "Other"); + WriteNode(memory, kNodeB, 0, kObjectB, "GameWorld"); + return GetObjectFromList(memory, kNodeA, kUnreadableNode, "GameWorld") == kObjectB; + }); + + tests.Run("ObjectList continues after name read failure", [] + { + SparseMemoryReader memory; + WriteNode(memory, kNodeA, kNodeB, kObjectA, nullptr); + WriteNode(memory, kNodeB, 0, kObjectB, "GameWorld"); + return GetObjectFromList(memory, kNodeA, 0, "GameWorld") == kObjectB; + }); + + tests.Run("ObjectList stops self-loop", [] + { + SparseMemoryReader memory; + WriteNode(memory, kNodeA, kNodeA, kObjectA, "Other"); + return GetObjectFromList(memory, kNodeA, 0, "GameWorld") == 0 && + memory.ReadCount(kNodeA) == 1; + }); + + tests.Run("ObjectList stops two-node cycle", [] + { + SparseMemoryReader memory; + WriteNode(memory, kNodeA, kNodeB, kObjectA, "OtherA"); + WriteNode(memory, kNodeB, kNodeA, kObjectB, "OtherB"); + return GetObjectFromList(memory, kNodeA, 0, "GameWorld") == 0 && + memory.ReadCount(kNodeA) + memory.ReadCount(kNodeB) == 2; + }); + + tests.Run("ObjectList target absent", [] + { + SparseMemoryReader memory; + WriteNode(memory, kNodeA, kNodeB, kObjectA, "OtherA"); + WriteNode(memory, kNodeB, 0, kObjectB, "OtherB"); + return GetObjectFromList(memory, kNodeA, 0, "GameWorld") == 0; + }); + + tests.Run("GOM ActiveNodes indirect", [] { return VerifyReadyLayout(GomLayout::ActiveNodesIndirect); }); + tests.Run("GOM ActiveNodes direct", [] { return VerifyReadyLayout(GomLayout::ActiveNodesDirect); }); + tests.Run("GOM activeObjects direct", [] { return VerifyReadyLayout(GomLayout::ActiveObjectsDirect); }); + tests.Run("GOM activeObjects indirect", [] { return VerifyReadyLayout(GomLayout::ActiveObjectsIndirect); }); + + tests.Run("CGame recovers after initial GOM failures", [] + { + SparseMemoryReader memory; + ConfigureReadyWorld(memory, GomLayout::ActiveNodesIndirect); + const uintptr_t gomRoot = memory.GetBaseAddress() + offset::dwGameObjectManager; + memory.FailNextReads(gomRoot, 3); + + CGame game(memory); + for (uint64_t attempt = 1; attempt <= 3; ++attempt) + { + if (game.Refresh()) + return false; + const GameStateSnapshot failed = game.GetState(); + if (failed.refreshStatus != GameRefreshStatus::GomUnavailable || + failed.refreshAttempts != attempt || + failed.consecutiveRefreshFailures != attempt) + { + return false; + } + } + + if (!game.Refresh()) + return false; + const GameStateSnapshot recovered = game.GetState(); + return recovered.refreshStatus == GameRefreshStatus::Ready && recovered.inRaid && + recovered.refreshAttempts == 4 && recovered.consecutiveRefreshFailures == 0; + }); + + tests.Run("Snapshot generation and freshness liveness", [] + { + using namespace std::chrono; + const auto start = steady_clock::time_point(milliseconds(1000)); + const bool worldReady[] = { false, false, true, false }; + SnapshotLiveness liveness{}; + for (std::size_t i = 0; i < std::size(worldReady); ++i) + { + (void)worldReady[i]; + liveness = AdvanceSnapshotLiveness(liveness.generation, start + milliseconds(33 * i)); + } + + return liveness.generation == 4 && + IsSnapshotFresh(liveness.capturedAt, liveness.capturedAt + milliseconds(250)) && + !IsSnapshotFresh(liveness.capturedAt, liveness.capturedAt + milliseconds(251)) && + GetSnapshotAgeMs(liveness.capturedAt, liveness.capturedAt + milliseconds(251)) == 251 && + !IsSnapshotFresh(steady_clock::time_point{}, start); + }); + + tests.Run("Camera unavailable keeps world Ready", [] + { + SparseMemoryReader memory; + ConfigureReadyWorld(memory, GomLayout::ActiveNodesDirect); + CGame game(memory); + if (!game.Refresh() || !game.Refresh()) + return false; + + const GameStateSnapshot state = game.GetState(); + return state.refreshStatus == GameRefreshStatus::Ready && state.inRaid && + !state.cameraValid && state.refreshAttempts == 2 && + state.consecutiveRefreshFailures == 0; + }); + + tests.Run("W2S projects visible point", [] + { + const Matrix projection( + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 0.f, 1.f, + 0.f, 0.f, 0.f, 0.f); + Vector2 output{}; + return WorldToScreen(projection, Vector2(1920.f, 1080.f), Vector3(0.f, 0.f, 1.f), output) && + Near(output.x, 960.f) && Near(output.y, 540.f); + }); + + tests.Run("W2S rejects point behind camera", [] + { + const Matrix projection( + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 0.f, 1.f, + 0.f, 0.f, 0.f, 0.f); + Vector2 output{}; + return !WorldToScreen(projection, Vector2(1920.f, 1080.f), Vector3(0.f, 0.f, -1.f), output); + }); + + return tests.Finish(); +} diff --git a/CFramework.sln b/CFramework.sln index 4c6b1db..3973022 100644 --- a/CFramework.sln +++ b/CFramework.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.11.35219.272 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CFramework", "CFramework\CFramework.vcxproj", "{E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CFramework.SelfTest", "CFramework.SelfTest\CFramework.SelfTest.vcxproj", "{4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -21,6 +23,14 @@ Global {E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}.Release|x64.Build.0 = Release|x64 {E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}.Release|x86.ActiveCfg = Release|Win32 {E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}.Release|x86.Build.0 = Release|Win32 + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}.Debug|x64.ActiveCfg = Debug|x64 + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}.Debug|x64.Build.0 = Debug|x64 + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}.Debug|x86.ActiveCfg = Debug|Win32 + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}.Debug|x86.Build.0 = Debug|Win32 + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}.Release|x64.ActiveCfg = Release|x64 + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}.Release|x64.Build.0 = Release|x64 + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}.Release|x86.ActiveCfg = Release|Win32 + {4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CFramework/CFramework.vcxproj b/CFramework/CFramework.vcxproj index 4dbd883..37e7dd6 100644 --- a/CFramework/CFramework.vcxproj +++ b/CFramework/CFramework.vcxproj @@ -146,6 +146,7 @@ + @@ -168,8 +169,10 @@ + + diff --git a/CFramework/CFramework.vcxproj.filters b/CFramework/CFramework.vcxproj.filters index 16ef923..26be211 100644 --- a/CFramework/CFramework.vcxproj.filters +++ b/CFramework/CFramework.vcxproj.filters @@ -31,6 +31,9 @@ {90d19cbc-e194-4714-b59b-34cf9d2bf64f} + + {d3c6c6ae-8cf3-4c09-a0df-3a2a0f1b31c4} + {c2a6f7bc-fc36-46f5-8697-b1e2b32653ee} @@ -72,12 +75,12 @@ Cheat\SDK\CExfil - - Cheat\SDK\GameSDK - Cheat\SDK\CItem + + Cheat\SDK\CContainer + Framework\ImGui @@ -131,6 +134,9 @@ Cheat + + Cheat + Cheat\SDK\CPlayer @@ -146,6 +152,9 @@ Cheat\SDK\CItem + + Cheat\SDK\CContainer + Framework\ImGui\Fonts diff --git a/CFramework/Cheat/Aim.cpp b/CFramework/Cheat/Aim.cpp index 665126a..96468fa 100644 --- a/CFramework/Cheat/Aim.cpp +++ b/CFramework/Cheat/Aim.cpp @@ -99,7 +99,8 @@ void CFramework::AimAll() return; const auto snapshot = GetDynamicSnapshot(); - if (!snapshot || !snapshot->inRaid || !snapshot->cameraValid || !snapshot->local.IsValid()) + if (!snapshot || !IsSnapshotFresh(snapshot->capturedAt) || + !snapshot->inRaid || !snapshot->cameraValid || !snapshot->local.IsValid()) return; const CPlayer& localPlayer = snapshot->local; @@ -447,7 +448,8 @@ void CFramework::ScanRotationOffset() const auto snapshot = GetDynamicSnapshot(); const uintptr_t localAddress = snapshot ? snapshot->local.m_address : 0; - if (!snapshot || !snapshot->inRaid || !Memory::IsUserAddress(localAddress)) + if (!snapshot || !IsSnapshotFresh(snapshot->capturedAt) || + !snapshot->inRaid || !Memory::IsUserAddress(localAddress)) { strcpy_s(m_scanResult, "仅限局内扫描 (先进 raid 再点)"); m_scanPhase = 0; diff --git a/CFramework/Cheat/Features.cpp b/CFramework/Cheat/Features.cpp index c400ce1..1ae042c 100644 --- a/CFramework/Cheat/Features.cpp +++ b/CFramework/Cheat/Features.cpp @@ -197,7 +197,13 @@ namespace void CFramework::UpdateFeatures() { const auto snapshot = GetDynamicSnapshot(); - if (!snapshot || !snapshot->inRaid || !snapshot->local.IsValid()) + if (!snapshot || !IsSnapshotFresh(snapshot->capturedAt)) + { + local = {}; + return; + } + + if (!snapshot->inRaid || !snapshot->local.IsValid()) { local = {}; ResetFeatureOverrides(); diff --git a/CFramework/Cheat/Framework.h b/CFramework/Cheat/Framework.h index c586727..a286a9e 100644 --- a/CFramework/Cheat/Framework.h +++ b/CFramework/Cheat/Framework.h @@ -10,6 +10,8 @@ #include "SDK/CPlayer/CPlayer.h" #include "SDK/CExfil/CExfil.h" #include "SDK/CItem/CItem.h" +#include "SDK/CContainer/CContainer.h" +#include "SnapshotHealth.h" #include #include #include @@ -25,9 +27,17 @@ struct GrenadeSnapshot struct DynamicWorldSnapshot { uint64_t generation{ 0 }; + std::chrono::steady_clock::time_point capturedAt{}; + uint64_t refreshAttempts{ 0 }; + uint64_t consecutiveRefreshFailures{ 0 }; + GameRefreshStatus refreshStatus{ GameRefreshStatus::Waiting }; bool initialized{ false }; bool inRaid{ false }; bool cameraValid{ false }; + uintptr_t moduleBase{ 0 }; + uintptr_t gameObjectManager{ 0 }; + uintptr_t activeNodeList{ 0 }; + uintptr_t lastActiveNode{ 0 }; uintptr_t gameWorld{ 0 }; uintptr_t localGameWorld{ 0 }; uintptr_t fpsCamera{ 0 }; @@ -47,6 +57,7 @@ struct WorkerConfigSnapshot bool wantGrenades{ false }; bool wantExfils{ false }; bool wantItems{ false }; + bool wantContainers{ false }; float remoteMaxDistance{ 0.f }; }; @@ -55,6 +66,7 @@ struct StaticWorldSnapshot uint64_t generation{ 0 }; std::vector exfils{}; std::vector items{}; + std::vector containers{}; }; class CFramework @@ -189,7 +201,7 @@ private: std::vector GetEntityList() const { auto snapshot = GetDynamicSnapshot(); - return snapshot ? snapshot->players : std::vector{}; + return snapshot && IsSnapshotFresh(snapshot->capturedAt) ? snapshot->players : std::vector{}; } std::vector GetExfilList() const { @@ -201,6 +213,11 @@ private: auto snapshot = GetStaticSnapshot(); return snapshot ? snapshot->items : std::vector{}; } + std::vector GetContainerList() const + { + auto snapshot = GetStaticSnapshot(); + return snapshot ? snapshot->containers : std::vector{}; + } // Colors ImColor Col_ESP_PMC = { 1.f, 0.f, 0.85f, 1.f }; @@ -213,6 +230,14 @@ private: ImColor Col_ESP_ExfilOpen = { 0.f, 1.f, 0.f, 0.5f }; ImColor Col_ESP_ExfilClose = { 1.f, 0.3f, 0.f, 0.5f }; + ImColor Col_ESP_ExfilPending = { 1.f, 1.f, 0.f, 0.9f }; + ImColor Col_ESP_ExfilUnknown = { 1.f, 0.65f, 0.f, 0.9f }; + + ImColor Col_ESP_ItemQuest = { 1.f, 1.f, 0.f, 0.9f }; + ImColor Col_ESP_ItemCommon = { 1.f, 1.f, 1.f, 0.9f }; + ImColor Col_ESP_ItemRare = { 0.38f, 0.43f, 1.f, 0.9f }; + ImColor Col_ESP_ItemSuperRare = { 1.f, 0.29f, 0.36f, 0.9f }; + ImColor Col_ESP_Container = { 1.f, 0.2f, 0.09f, 1.f }; ImColor Col_ESP_Corpse = { 1.f, 1.f, 1.f, 0.9f }; ImColor Col_ESP_RareItem = { 1.f, 0.f, 0.f, 0.9f }; diff --git a/CFramework/Cheat/Menu.cpp b/CFramework/Cheat/Menu.cpp index 5c21bc7..f6d729f 100644 --- a/CFramework/Cheat/Menu.cpp +++ b/CFramework/Cheat/Menu.cpp @@ -14,9 +14,10 @@ void CFramework::RenderMenu() // Setup static int Index = 0; const auto runtimeSnapshot = GetDynamicSnapshot(); - const bool runtimeInRaid = runtimeSnapshot && runtimeSnapshot->inRaid; - const uintptr_t runtimeLocalAddress = runtimeSnapshot ? runtimeSnapshot->local.m_address : 0; - const std::size_t runtimeEntityCount = runtimeSnapshot ? runtimeSnapshot->players.size() : 0; + const bool runtimeFresh = runtimeSnapshot && IsSnapshotFresh(runtimeSnapshot->capturedAt); + const bool runtimeInRaid = runtimeFresh && runtimeSnapshot->inRaid; + const uintptr_t runtimeLocalAddress = runtimeFresh ? runtimeSnapshot->local.m_address : 0; + const std::size_t runtimeEntityCount = runtimeFresh ? runtimeSnapshot->players.size() : 0; ImGui::SetNextWindowBgAlpha(0.975f); ImGui::SetNextWindowSize(ImVec2(725.f, 450.f)); @@ -96,7 +97,8 @@ void CFramework::RenderMenu() ImGui::Checkbox("ESP", &g.g_ESP); ImGui::Checkbox("物资ESP", &g.g_ESP_Item); - ImGui::Checkbox("尸体ESP", &g.g_ESP_Item); + ImGui::Checkbox("尸体ESP", &g.g_ESP_Corpse); + ImGui::Checkbox("容器ESP", &g.g_ESP_Container); ImGui::Checkbox("撤离点ESP", &g.g_ESP_Exfil); ImGui::Checkbox("手雷ESP", &g.g_ESP_Grenade); @@ -114,6 +116,8 @@ void CFramework::RenderMenu() ImGui::Checkbox("距离", &g.g_ESP_Distance); ImGui::Checkbox("名称", &g.g_ESP_Name); ImGui::Checkbox("血条", &g.g_ESP_HealthBar); + ImGui::CustomSliderFloat("物资最大距离", "##ItemDist", &g.g_ESP_MaxItemDistance, 0.f, 1000.f); + ImGui::CustomSliderFloat("容器最大距离", "##ContainerDist", &g.g_ESP_MaxContainerDistance, 0.f, 1000.f); ImGui::EndChild(); break; diff --git a/CFramework/Cheat/SDK/CContainer/CContainer.cpp b/CFramework/Cheat/SDK/CContainer/CContainer.cpp new file mode 100644 index 0000000..8fe0e0c --- /dev/null +++ b/CFramework/Cheat/SDK/CContainer/CContainer.cpp @@ -0,0 +1,37 @@ +#include "CContainer.h" + +bool CContainer::GetAddress(uintptr_t& ptr) +{ + address = ptr; + return Memory::IsUserAddress(address); +} + +bool CContainer::Update() +{ + m_CName.clear(); + m_vecOrigin = {}; + + if (!Memory::IsUserAddress(address)) + return false; + + uint8_t isContainer = 0; + if (!m.TryRead(address + offset::LootItemPosition::IsContainer, isContainer) || isContainer == 0) + return false; + + if (!m.TryRead(address + offset::LootItemPosition::Position, m_vecOrigin) || Vec3_Empty(m_vecOrigin)) + return false; + + uintptr_t itemAddress = 0; + LootItemMetadata metadata{}; + if (m.TryRead(address + offset::LootItemPosition::Item, itemAddress) && + Memory::IsUserAddress(itemAddress) && + ReadInventoryItemMetadata(itemAddress, metadata)) + { + m_CName = !metadata.shortName.empty() ? metadata.shortName : metadata.name; + } + + if (m_CName.empty()) + m_CName = "Container"; + + return true; +} diff --git a/CFramework/Cheat/SDK/CContainer/CContainer.h b/CFramework/Cheat/SDK/CContainer/CContainer.h new file mode 100644 index 0000000..8a6af32 --- /dev/null +++ b/CFramework/Cheat/SDK/CContainer/CContainer.h @@ -0,0 +1,17 @@ +#pragma once +#include "../CItem/CItem.h" +#include +#include + +// This address is the LootItemPositionClass entry from GameWorld::AllLoot, +// not necessarily the LootableContainer MonoBehaviour itself. +class CContainer +{ +public: + uintptr_t address{ 0 }; + std::string m_CName{}; + Vector3 m_vecOrigin{}; + + bool GetAddress(uintptr_t& ptr); + bool Update(); +}; diff --git a/CFramework/Cheat/SDK/CExfil/CExfil.cpp b/CFramework/Cheat/SDK/CExfil/CExfil.cpp index d577d7d..9f7ad88 100644 --- a/CFramework/Cheat/SDK/CExfil/CExfil.cpp +++ b/CFramework/Cheat/SDK/CExfil/CExfil.cpp @@ -1,5 +1,68 @@ #include "CExfil.h" -#include +#include +#include +#include + +namespace +{ + std::string ReadManagedName(uintptr_t stringObject) + { + if (!Memory::IsUserAddress(stringObject)) + return {}; + + int32_t length = 0; + if (!m.TryRead(stringObject + offset::ManagedString::Length, length) || + length <= 0 || length > 127) + { + return {}; + } + + std::vector chars; + if (!m.TryReadVector(stringObject + offset::ManagedString::Data, + static_cast(length), chars, 127)) + { + return {}; + } + + std::string name; + name.reserve(chars.size()); + for (const char16_t ch : chars) + { + if (ch == 0) + break; + name.push_back(ch >= 0x20 && ch <= 0x7E ? static_cast(ch) : '?'); + } + return name; + } +} + +ExfilDisplayCategory ClassifyExfilStatus(const uint8_t status) noexcept +{ + switch (status) + { + case static_cast(CLOSE): + return ExfilDisplayCategory::Closed; + case static_cast(NOTREADY): + return ExfilDisplayCategory::Pending; + case static_cast(OPEN): + case static_cast(READY): + return ExfilDisplayCategory::Open; + default: + return ExfilDisplayCategory::Unknown; + } +} + +const char* GetExfilStatusText(const uint8_t status) noexcept +{ + switch (status) + { + case static_cast(CLOSE): return "Closed"; + case static_cast(NOTREADY): return "Not ready"; + case static_cast(OPEN): return "Open"; + case static_cast(READY): return "Ready"; + default: return "Unknown"; + } +} bool CExfil::GetAddress(uintptr_t& address) { @@ -7,10 +70,11 @@ bool CExfil::GetAddress(uintptr_t& address) return Memory::IsUserAddress(ptr); } -int CExfil::GetStatus() +uint8_t CExfil::GetStatus() { - int status = CLOSE; - m.TryRead(ptr + offset::Exfil::Status, status); + uint8_t status = 0; + if (!Memory::IsUserAddress(ptr) || !m.TryRead(ptr + offset::Exfil::Status, status)) + return 0; return status; } @@ -18,34 +82,23 @@ std::string CExfil::GetName() { uintptr_t settings = 0; uintptr_t namePtr = 0; - int length = 0; - if (!m.TryRead(ptr + offset::Exfil::Settings, settings) || !Memory::IsUserAddress(settings) || - !m.TryRead(settings + offset::Exfil::Name, namePtr) || !Memory::IsUserAddress(namePtr) || - !m.TryRead(namePtr + offset::ManagedString::Length, length) || length <= 0) + if (!Memory::IsUserAddress(ptr) || + !m.TryRead(ptr + offset::Exfil::Settings, settings) || !Memory::IsUserAddress(settings) || + !m.TryRead(settings + offset::Exfil::Name, namePtr) || !Memory::IsUserAddress(namePtr)) { return {}; } - length = (std::min)(length, 127); - std::string name; - name.reserve(static_cast(length)); - for (int i = 0; i < length; ++i) - { - wchar_t ch = 0; - if (!m.TryRead(namePtr + offset::ManagedString::Data + static_cast(i) * offset::ManagedString::CharStride, ch)) - break; - if (ch >= 0x20 && ch <= 0x7E) - name.push_back(static_cast(ch)); - else if (ch != 0) - name.push_back('?'); - } - return name; + return ReadManagedName(namePtr); } bool CExfil::Update() { + // Status is diagnostic data; a status read failure must not discard a valid position. m_pExfilStatus = GetStatus(); - if (m_pExfilStatus == CLOSE) + m_pVecLocation = {}; + + if (!Memory::IsUserAddress(ptr)) return false; const uintptr_t transform = m.ReadChain(ptr, diff --git a/CFramework/Cheat/SDK/CExfil/CExfil.h b/CFramework/Cheat/SDK/CExfil/CExfil.h index 1003c81..40140a0 100644 --- a/CFramework/Cheat/SDK/CExfil/CExfil.h +++ b/CFramework/Cheat/SDK/CExfil/CExfil.h @@ -1,20 +1,32 @@ #pragma once #include "../GameSDK/GameSDK.h" +#include +#include + +enum class ExfilDisplayCategory : uint8_t +{ + Closed, + Open, + Pending, + Unknown +}; + +// Only the legacy numeric values already used by this reader are classified. +// The dump lists EExfiltrationStatus names but does not preserve explicit values. +ExfilDisplayCategory ClassifyExfilStatus(uint8_t status) noexcept; +const char* GetExfilStatusText(uint8_t status) noexcept; class CExfil { -private: - public: - uintptr_t ptr; + uintptr_t ptr{ 0 }; - // データ - int m_pExfilStatus; - Vector3 m_pVecLocation; - std::string m_pExfilName; + uint8_t m_pExfilStatus{ 0 }; + Vector3 m_pVecLocation{}; + std::string m_pExfilName{}; - bool GetAddress(uintptr_t& address); - int GetStatus(); - std::string GetName(); - bool Update(); + bool GetAddress(uintptr_t& address); + uint8_t GetStatus(); + std::string GetName(); + bool Update(); }; \ No newline at end of file diff --git a/CFramework/Cheat/SDK/CItem/CItem.cpp b/CFramework/Cheat/SDK/CItem/CItem.cpp index 3209086..c1b2530 100644 --- a/CFramework/Cheat/SDK/CItem/CItem.cpp +++ b/CFramework/Cheat/SDK/CItem/CItem.cpp @@ -1,23 +1,136 @@ #include "CItem.h" +#include +#include +#include -struct C_EFT_ItemData +namespace { - int item_price; - std::string item_name; - std::string item_id; -}; + constexpr int kMaxManagedStringLength = 128; -std::vector C_LootList = + void AppendUtf8(std::string& output, uint32_t codePoint) + { + if (codePoint <= 0x7F) + { + output.push_back(static_cast(codePoint)); + } + else if (codePoint <= 0x7FF) + { + output.push_back(static_cast(0xC0 | (codePoint >> 6))); + output.push_back(static_cast(0x80 | (codePoint & 0x3F))); + } + else if (codePoint <= 0xFFFF) + { + output.push_back(static_cast(0xE0 | (codePoint >> 12))); + output.push_back(static_cast(0x80 | ((codePoint >> 6) & 0x3F))); + output.push_back(static_cast(0x80 | (codePoint & 0x3F))); + } + else + { + output.push_back(static_cast(0xF0 | (codePoint >> 18))); + output.push_back(static_cast(0x80 | ((codePoint >> 12) & 0x3F))); + output.push_back(static_cast(0x80 | ((codePoint >> 6) & 0x3F))); + output.push_back(static_cast(0x80 | (codePoint & 0x3F))); + } + } + + std::string ReadManagedUtf16String(uintptr_t stringObject) + { + if (!Memory::IsUserAddress(stringObject)) + return {}; + + int32_t length = 0; + if (!m.TryRead(stringObject + offset::ManagedString::Length, length) || + length <= 0 || length > kMaxManagedStringLength) + { + return {}; + } + + std::vector chars; + if (!m.TryReadVector(stringObject + offset::ManagedString::Data, + static_cast(length), chars, kMaxManagedStringLength)) + { + return {}; + } + + std::string output; + output.reserve(chars.size()); + for (std::size_t index = 0; index < chars.size(); ++index) + { + const uint32_t first = chars[index]; + if (first == 0) + break; + + uint32_t codePoint = first; + if (first >= 0xD800 && first <= 0xDBFF) + { + if (index + 1 < chars.size() && chars[index + 1] >= 0xDC00 && chars[index + 1] <= 0xDFFF) + { + codePoint = 0x10000u + ((first - 0xD800u) << 10) + (chars[++index] - 0xDC00u); + } + else + { + codePoint = 0xFFFDu; + } + } + else if (first >= 0xDC00 && first <= 0xDFFF) + { + codePoint = 0xFFFDu; + } + + if (codePoint < 0x20u && codePoint != '\t') + codePoint = 0xFFFDu; + AppendUtf8(output, codePoint); + } + return output; + } +} + +bool ReadInventoryItemMetadata(const uintptr_t itemAddress, LootItemMetadata& out) { - { 2500000, "Red Rebel", "5c0126f40db834002a125382" }, - { 20000000, "Labs keycard (Red)", "5c1d0efb86f7744baf2e7b7b" }, - { 20000000, "Labs keycard (Blue)", "1d0c5f86f7744bb2683cf0" }, - { 20000000, "Labs keycard (Green)", "1d0dc586f7744baf2e7b79" }, - { 20000000, "Labs keycard (Violet)", "1e495a86f7743109743dfb" }, - { 20000000, "Labs keycard (Black)", "1d0f4986f7744bb01837fa" }, - { 20000000, "Labs keycard (Yellow)", "1d0d6d86f7744bb2683e1f" }, - { 65000, "0.2 BTC", "faff1d86f7746c51718c9c" } -}; + out = {}; + if (!Memory::IsUserAddress(itemAddress)) + return false; + + uintptr_t itemIdString = 0; + if (m.TryRead(itemAddress + offset::InventoryItem::Id, itemIdString) && + Memory::IsUserAddress(itemIdString)) + { + out.itemId = ReadManagedUtf16String(itemIdString); + } + + uintptr_t templateAddress = 0; + if (!m.TryRead(itemAddress + offset::InventoryItem::Template, templateAddress) || + !Memory::IsUserAddress(templateAddress)) + { + return false; + } + + out.valid = true; + + uintptr_t nameString = 0; + if (m.TryRead(templateAddress + offset::ItemTemplate::Name, nameString) && + Memory::IsUserAddress(nameString)) + { + out.name = ReadManagedUtf16String(nameString); + } + + uintptr_t shortNameString = 0; + if (m.TryRead(templateAddress + offset::ItemTemplate::ShortName, shortNameString) && + Memory::IsUserAddress(shortNameString)) + { + out.shortName = ReadManagedUtf16String(shortNameString); + } + + uint8_t questItem = 0; + if (m.TryRead(templateAddress + offset::ItemTemplate::QuestItem, questItem)) + out.isQuestItem = questItem != 0; + + int32_t rarity = 0; + if (m.TryRead(templateAddress + offset::ItemTemplate::Rarity, rarity)) + out.rarity = rarity; + + return true; +} bool CItem::GetAddress(uintptr_t& ptr) { @@ -28,8 +141,17 @@ bool CItem::GetAddress(uintptr_t& ptr) bool CItem::Update() { m_bIsCorpse = false; + m_bMetadataValid = false; + m_bIsQuestItem = false; + m_iRarity = 0; + m_iMarketPrice = 0; + m_CName.clear(); + m_CItemId.clear(); m_vecOrigin = {}; + if (!Memory::IsUserAddress(address)) + return false; + const uintptr_t transform = m.ReadChain(address, { offset::UnityComponent::Step0, offset::UnityComponent::Step1, offset::UnityComponent::Step2, offset::UnityComponent::Step3, offset::UnityComponent::Step4, offset::UnityComponent::Transform }); @@ -37,7 +159,40 @@ bool CItem::Update() if (Vec3_Empty(m_vecOrigin)) return false; - // Keep the legacy class-name chain from the original project, but validate every link. + uintptr_t inventoryItem = 0; + LootItemMetadata metadata{}; + if (m.TryRead(address + offset::LootItem::Item, inventoryItem) && + Memory::IsUserAddress(inventoryItem)) + { + m_bMetadataValid = ReadInventoryItemMetadata(inventoryItem, metadata); + if (m_bMetadataValid) + { + m_bIsQuestItem = metadata.isQuestItem; + m_iRarity = metadata.rarity; + m_CItemId = metadata.itemId; + m_CName = !metadata.shortName.empty() ? metadata.shortName : metadata.name; + } + } + + uintptr_t outerItemId = 0; + if (m_CItemId.empty() && m.TryRead(address + offset::LootItem::ItemId, outerItemId) && + Memory::IsUserAddress(outerItemId)) + { + m_CItemId = ReadManagedUtf16String(outerItemId); + } + + if (m_CName.empty()) + { + uintptr_t outerName = 0; + if (m.TryRead(address + offset::LootItem::Name, outerName) && Memory::IsUserAddress(outerName)) + m_CName = ReadManagedUtf16String(outerName); + } + + // Keep a stable label even when a partially initialized loot object has no template metadata. + if (m_CName.empty()) + m_CName = "Item"; + + // Keep the legacy corpse check, but validate the chain through ReadChain's read-only checks. const uintptr_t nameAddr = m.ReadChain(address, { 0x0, 0x0, 0x48 }); const std::string className = m.ReadString(nameAddr, 64); if (className == "ObservedCorpse" || className == "Corpse") diff --git a/CFramework/Cheat/SDK/CItem/CItem.h b/CFramework/Cheat/SDK/CItem/CItem.h index fa3368e..8e206da 100644 --- a/CFramework/Cheat/SDK/CItem/CItem.h +++ b/CFramework/Cheat/SDK/CItem/CItem.h @@ -1,18 +1,35 @@ #pragma once #include "../GameSDK/GameSDK.h" +#include +#include + +struct LootItemMetadata +{ + bool valid{ false }; + bool isQuestItem{ false }; + int rarity{ 0 }; + std::string itemId{}; + std::string name{}; + std::string shortName{}; +}; + +// Reads only the display metadata exposed by EFT.InventoryLogic.Item/ItemTemplate. +bool ReadInventoryItemMetadata(uintptr_t itemAddress, LootItemMetadata& out); class CItem { -private: - public: - uintptr_t address; + uintptr_t address{ 0 }; - bool m_bIsCorpse{ false }; - int m_iMarketPrice{ 0 }; - std::string m_CName{}; - Vector3 m_vecOrigin{}; + bool m_bIsCorpse{ false }; + bool m_bMetadataValid{ false }; + bool m_bIsQuestItem{ false }; + int m_iRarity{ 0 }; + int m_iMarketPrice{ 0 }; + std::string m_CName{}; + std::string m_CItemId{}; + Vector3 m_vecOrigin{}; - bool GetAddress(uintptr_t& ptr); - bool Update(); + bool GetAddress(uintptr_t& ptr); + bool Update(); }; \ No newline at end of file diff --git a/CFramework/Cheat/SDK/GameSDK/GameSDK.cpp b/CFramework/Cheat/SDK/GameSDK/GameSDK.cpp index 5f13778..85ebbbe 100644 --- a/CFramework/Cheat/SDK/GameSDK/GameSDK.cpp +++ b/CFramework/Cheat/SDK/GameSDK/GameSDK.cpp @@ -1,6 +1,36 @@ #include "GameSDK.h" +#include #include #include +#include + +namespace +{ + class ProcessGameMemoryReader final : public IGameMemoryReader + { + public: + bool TryReadBuffer(const uintptr_t address, void* buffer, const std::size_t size) const noexcept override + { + return m.TryReadBuffer(address, buffer, static_cast(size)); + } + + bool ReadString(const uintptr_t address, void* buffer, const std::size_t size) const override + { + return m.ReadString(address, buffer, static_cast(size)); + } + + uintptr_t GetBaseAddress() const noexcept override + { + return m.m_gBaseAddress; + } + }; + + IGameMemoryReader& GetProcessGameMemoryReader() + { + static ProcessGameMemoryReader reader; + return reader; + } +} bool Vec2_Empty(const Vector2& value) { @@ -118,87 +148,177 @@ bool WorldToScreen(const Matrix& ViewMatrix, const Vector2& GameSize, const Vect return std::isfinite(vOut.x) && std::isfinite(vOut.y); } -uintptr_t GetObjectFromList(uintptr_t listPtr, uintptr_t lastObjectPtr, const char* objectName) +uintptr_t GetObjectFromList(IGameMemoryReader& memory, uintptr_t listPtr, uintptr_t lastObjectPtr, const char* objectName) { - if (!Memory::IsUserAddress(listPtr) || !Memory::IsUserAddress(lastObjectPtr) || !objectName) + if (!Memory::IsUserAddress(listPtr) || !objectName) return 0; - BaseObject activeObject{}; BaseObject lastObject{}; - if (!m.TryRead(listPtr, activeObject) || !m.TryRead(lastObjectPtr, lastObject)) - return 0; - + const bool hasLastObject = Memory::IsUserAddress(lastObjectPtr) && memory.TryRead(lastObjectPtr, lastObject); + uintptr_t currentNode = listPtr; char objectNameBuffer[128]{}; - int safe = 0; - while (activeObject.object != 0 && activeObject.object != lastObject.object && safe++ < 4096) - { - uintptr_t classNamePtr = 0; - if (m.TryRead(activeObject.object + offset::UnityCamera::ObjectName, classNamePtr) && - Memory::IsUserAddress(classNamePtr) && - m.ReadString(classNamePtr, objectNameBuffer, sizeof(objectNameBuffer)) && - std::strcmp(objectNameBuffer, objectName) == 0) - { - return activeObject.object; - } + std::unordered_set visitedNodes; + visitedNodes.reserve(64); - if (!Memory::IsUserAddress(activeObject.nextObjectLink) || !m.TryRead(activeObject.nextObjectLink, activeObject)) + for (int safe = 0; safe < 4096 && Memory::IsUserAddress(currentNode); ++safe) + { + if (!visitedNodes.insert(currentNode).second) break; - } - if (lastObject.object != 0) - { - uintptr_t classNamePtr = 0; - if (m.TryRead(lastObject.object + offset::UnityCamera::ObjectName, classNamePtr) && - Memory::IsUserAddress(classNamePtr) && - m.ReadString(classNamePtr, objectNameBuffer, sizeof(objectNameBuffer)) && - std::strcmp(objectNameBuffer, objectName) == 0) + BaseObject activeObject{}; + if (!memory.TryRead(currentNode, activeObject)) + break; + + if (Memory::IsUserAddress(activeObject.object)) { - return lastObject.object; + uintptr_t classNamePtr = 0; + std::memset(objectNameBuffer, 0, sizeof(objectNameBuffer)); + if (memory.TryRead(activeObject.object + offset::UnityCamera::ObjectName, classNamePtr) && + Memory::IsUserAddress(classNamePtr) && + memory.ReadString(classNamePtr, objectNameBuffer, sizeof(objectNameBuffer)) && + std::strcmp(objectNameBuffer, objectName) == 0) + { + return activeObject.object; + } } + + if (hasLastObject && activeObject.object != 0 && activeObject.object == lastObject.object) + break; + + const uintptr_t nextNode = activeObject.nextObjectLink; + if (!Memory::IsUserAddress(nextNode)) + break; + currentNode = nextNode; } return 0; } +uintptr_t GetObjectFromList(uintptr_t listPtr, uintptr_t lastObjectPtr, const char* objectName) +{ + return GetObjectFromList(GetProcessGameMemoryReader(), listPtr, lastObjectPtr, objectName); +} + +CGame::CGame() + : CGame(GetProcessGameMemoryReader()) +{ +} + +CGame::CGame(IGameMemoryReader& memory) noexcept + : m_memory(memory) +{ +} + bool CGame::InitAddressUnlocked() { + m_pGameObjectManager = 0; + m_activeNodeList = 0; + m_lastActiveNode = 0; + + const uintptr_t baseAddress = m_memory.GetBaseAddress(); uintptr_t pGom = 0; - if (!m.TryRead(m.m_gBaseAddress + offset::dwGameObjectManager, pGom) || !Memory::IsUserAddress(pGom) || - !m.TryRead(pGom, m_objGameObjectManager)) + if (!Memory::IsUserAddress(baseAddress) || + !m_memory.TryRead(baseAddress + offset::dwGameObjectManager, pGom) || + !Memory::IsUserAddress(pGom) || + !m_memory.TryRead(pGom, m_objGameObjectManager)) { m_objGameObjectManager = {}; m_initialized = false; + m_refreshStatus = GameRefreshStatus::GomUnavailable; return false; } - m_initialized = m_objGameObjectManager.lastActiveObject != 0 && m_objGameObjectManager.ActiveNodes != 0; - return m_initialized; + m_pGameObjectManager = pGom; + m_initialized = true; + m_refreshStatus = GameRefreshStatus::NodeListUnavailable; + return true; } bool CGame::UpdateWorldUnlocked() { + m_activeNodeList = 0; + m_lastActiveNode = 0; m_pGameWorld = 0; m_pLocalGameWorld = 0; if (!m_initialized && !InitAddressUnlocked()) return false; - uintptr_t activeNodes = 0; - uintptr_t lastActiveNode = 0; - if (!m.TryRead(m_objGameObjectManager.ActiveNodes, activeNodes) || - !m.TryRead(m_objGameObjectManager.LastActiveNode, lastActiveNode)) + struct NodePair { + uintptr_t active{ 0 }; + uintptr_t last{ 0 }; + }; + + std::array candidates{}; + std::size_t candidateCount = 0; + const auto addCandidate = [&](uintptr_t active, uintptr_t last) + { + if (!Memory::IsUserAddress(active)) + return; + for (std::size_t i = 0; i < candidateCount; ++i) + { + if (candidates[i].active == active && candidates[i].last == last) + return; + } + if (candidateCount < candidates.size()) + candidates[candidateCount++] = { active, last }; + }; + + // Keep the legacy pointer-to-pointer path first, then tolerate Unity builds where the GOM + // stores list heads directly or still uses the older activeObjects pair. + uintptr_t active = 0; + uintptr_t last = 0; + if (Memory::IsUserAddress(m_objGameObjectManager.ActiveNodes) && + m_memory.TryRead(m_objGameObjectManager.ActiveNodes, active)) + { + if (Memory::IsUserAddress(m_objGameObjectManager.LastActiveNode)) + m_memory.TryRead(m_objGameObjectManager.LastActiveNode, last); + addCandidate(active, last); + } + addCandidate(m_objGameObjectManager.ActiveNodes, m_objGameObjectManager.LastActiveNode); + addCandidate(m_objGameObjectManager.activeObjects, m_objGameObjectManager.lastActiveObject); + + active = 0; + last = 0; + if (Memory::IsUserAddress(m_objGameObjectManager.activeObjects) && + m_memory.TryRead(m_objGameObjectManager.activeObjects, active)) + { + if (Memory::IsUserAddress(m_objGameObjectManager.lastActiveObject)) + m_memory.TryRead(m_objGameObjectManager.lastActiveObject, last); + addCandidate(active, last); + } + + for (std::size_t i = 0; i < candidateCount; ++i) + { + const uintptr_t gameWorld = GetObjectFromList(m_memory, candidates[i].active, candidates[i].last, "GameWorld"); + if (!Memory::IsUserAddress(gameWorld)) + continue; + + m_activeNodeList = candidates[i].active; + m_lastActiveNode = candidates[i].last; + m_pGameWorld = gameWorld; + break; + } + + if (!Memory::IsUserAddress(m_pGameWorld)) + { + m_refreshStatus = candidateCount == 0 + ? GameRefreshStatus::NodeListUnavailable + : GameRefreshStatus::GameWorldUnavailable; return false; } - m_pGameWorld = GetObjectFromList(activeNodes, lastActiveNode, "GameWorld"); - if (!Memory::IsUserAddress(m_pGameWorld)) - return false; - - m_pLocalGameWorld = m.ReadChain(m_pGameWorld, + m_pLocalGameWorld = m_memory.ReadChain(m_pGameWorld, { offset::UnityGameWorld::Component0, offset::UnityGameWorld::Component1, offset::UnityGameWorld::LocalGameWorld }); + if (!Memory::IsUserAddress(m_pLocalGameWorld)) + { + m_refreshStatus = GameRefreshStatus::LocalWorldUnavailable; + return false; + } - return Memory::IsUserAddress(m_pLocalGameWorld); + m_refreshStatus = GameRefreshStatus::Ready; + return true; } bool CGame::DiscoverCameraUnlocked() @@ -206,12 +326,17 @@ bool CGame::DiscoverCameraUnlocked() m_pFpsCamera = 0; m_cameraValid = false; + const uintptr_t baseAddress = m_memory.GetBaseAddress(); uintptr_t allCameraPtr = 0; - if (!m.TryRead(m.m_gBaseAddress + offset::dwAllCamera, allCameraPtr) || !Memory::IsUserAddress(allCameraPtr)) + if (!Memory::IsUserAddress(baseAddress) || + !m_memory.TryRead(baseAddress + offset::dwAllCamera, allCameraPtr) || + !Memory::IsUserAddress(allCameraPtr)) + { return false; + } Camera allCameras{}; - if (!m.TryRead(allCameraPtr, allCameras) || !Memory::IsUserAddress(allCameras.list) || allCameras.count == 0 || allCameras.count > 512) + if (!m_memory.TryRead(allCameraPtr, allCameras) || !Memory::IsUserAddress(allCameras.list) || allCameras.count == 0 || allCameras.count > 512) return false; for (uint64_t i = 0; i < allCameras.count; ++i) @@ -219,15 +344,15 @@ bool CGame::DiscoverCameraUnlocked() uintptr_t camera = 0; uintptr_t cameraObject = 0; uintptr_t cameraNamePtr = 0; - if (!m.TryRead(allCameras.list + static_cast(i * sizeof(uintptr_t)), camera) || !Memory::IsUserAddress(camera) || - !m.TryRead(camera + offset::UnityCamera::NativeObject, cameraObject) || !Memory::IsUserAddress(cameraObject) || - !m.TryRead(cameraObject + offset::UnityCamera::ObjectName, cameraNamePtr) || !Memory::IsUserAddress(cameraNamePtr)) + if (!m_memory.TryRead(allCameras.list + static_cast(i * sizeof(uintptr_t)), camera) || !Memory::IsUserAddress(camera) || + !m_memory.TryRead(camera + offset::UnityCamera::NativeObject, cameraObject) || !Memory::IsUserAddress(cameraObject) || + !m_memory.TryRead(cameraObject + offset::UnityCamera::ObjectName, cameraNamePtr) || !Memory::IsUserAddress(cameraNamePtr)) { continue; } char name[64]{}; - if (!m.ReadString(cameraNamePtr, name, sizeof(name))) + if (!m_memory.ReadString(cameraNamePtr, name, sizeof(name))) continue; if (std::strcmp(name, "FPS Camera") == 0) @@ -250,9 +375,9 @@ bool CGame::UpdateViewMatrixUnlocked() uintptr_t viewObject = 0; uintptr_t matrixHolder = 0; Matrix matrix{}; - if (!m.TryRead(m_pFpsCamera + offset::UnityCamera::ViewObject, viewObject) || !Memory::IsUserAddress(viewObject) || - !m.TryRead(viewObject + offset::UnityCamera::ViewMatrixHolder, matrixHolder) || !Memory::IsUserAddress(matrixHolder) || - !m.TryRead(matrixHolder + offset::UnityCamera::ViewMatrix, matrix)) + if (!m_memory.TryRead(m_pFpsCamera + offset::UnityCamera::ViewObject, viewObject) || !Memory::IsUserAddress(viewObject) || + !m_memory.TryRead(viewObject + offset::UnityCamera::ViewMatrixHolder, matrixHolder) || !Memory::IsUserAddress(matrixHolder) || + !m_memory.TryRead(matrixHolder + offset::UnityCamera::ViewMatrix, matrix)) { return false; } @@ -285,9 +410,13 @@ bool CGame::UpdateCamera() bool CGame::Refresh() { std::lock_guard lock(m_stateMutex); + ++m_refreshAttempts; if (!m_initialized && !InitAddressUnlocked()) + { + ++m_consecutiveRefreshFailures; return false; + } if (!UpdateWorldUnlocked()) { @@ -296,10 +425,13 @@ bool CGame::Refresh() { m_pGameWorld = 0; m_pLocalGameWorld = 0; + ++m_consecutiveRefreshFailures; return false; } } + m_consecutiveRefreshFailures = 0; + if (!UpdateViewMatrixUnlocked()) { if (!DiscoverCameraUnlocked() || !UpdateViewMatrixUnlocked()) @@ -325,9 +457,16 @@ GameStateSnapshot CGame::GetState() const { std::lock_guard lock(m_stateMutex); GameStateSnapshot state{}; + state.refreshStatus = m_refreshStatus; + state.refreshAttempts = m_refreshAttempts; + state.consecutiveRefreshFailures = m_consecutiveRefreshFailures; state.initialized = m_initialized; state.inRaid = Memory::IsUserAddress(m_pGameWorld) && Memory::IsUserAddress(m_pLocalGameWorld); state.cameraValid = m_cameraValid; + state.moduleBase = m_memory.GetBaseAddress(); + state.gameObjectManager = m_pGameObjectManager; + state.activeNodeList = m_activeNodeList; + state.lastActiveNode = m_lastActiveNode; state.gameWorld = m_pGameWorld; state.localGameWorld = m_pLocalGameWorld; state.fpsCamera = m_pFpsCamera; diff --git a/CFramework/Cheat/SDK/GameSDK/GameSDK.h b/CFramework/Cheat/SDK/GameSDK/GameSDK.h index f505b6c..a785ccd 100644 --- a/CFramework/Cheat/SDK/GameSDK/GameSDK.h +++ b/CFramework/Cheat/SDK/GameSDK/GameSDK.h @@ -3,13 +3,69 @@ #include "Struct.h" #include "../../../Framework/Memory/Memory.h" #include "../../../Framework/Config/Config.h" +#include +#include #include +#include + +class IGameMemoryReader +{ +public: + virtual ~IGameMemoryReader() = default; + + virtual bool TryReadBuffer(uintptr_t address, void* buffer, std::size_t size) const noexcept = 0; + virtual bool ReadString(uintptr_t address, void* buffer, std::size_t size) const = 0; + virtual uintptr_t GetBaseAddress() const noexcept = 0; + + template + bool TryRead(const uintptr_t address, T& value) const noexcept + { + value = T{}; + return Memory::IsUserAddress(address) && TryReadBuffer(address, &value, sizeof(T)); + } + + uintptr_t ReadChain(uintptr_t address, const std::vector& offsets) const noexcept + { + if (!Memory::IsUserAddress(address) || offsets.empty()) + return 0; + + uintptr_t result = 0; + if (!TryRead(address + offsets.front(), result) || !Memory::IsUserAddress(result)) + return 0; + + for (std::size_t i = 1; i < offsets.size(); ++i) + { + uintptr_t next = 0; + if (!TryRead(result + offsets[i], next) || !Memory::IsUserAddress(next)) + return 0; + result = next; + } + return result; + } +}; + +enum class GameRefreshStatus : uint8_t +{ + Waiting = 0, + GomUnavailable, + NodeListUnavailable, + GameWorldUnavailable, + LocalWorldUnavailable, + Ready +}; struct GameStateSnapshot { + GameRefreshStatus refreshStatus{ GameRefreshStatus::Waiting }; + uint64_t refreshAttempts{ 0 }; + uint64_t consecutiveRefreshFailures{ 0 }; bool initialized{ false }; bool inRaid{ false }; bool cameraValid{ false }; + uintptr_t moduleBase{ 0 }; + uintptr_t gameObjectManager{ 0 }; + uintptr_t activeNodeList{ 0 }; + uintptr_t lastActiveNode{ 0 }; uintptr_t gameWorld{ 0 }; uintptr_t localGameWorld{ 0 }; uintptr_t fpsCamera{ 0 }; @@ -19,13 +75,20 @@ struct GameStateSnapshot class CGame { private: + IGameMemoryReader& m_memory; mutable std::mutex m_stateMutex; GameObjectManager m_objGameObjectManager{}; Camera m_pAllCamera{}; + uintptr_t m_pGameObjectManager{ 0 }; + uintptr_t m_activeNodeList{ 0 }; + uintptr_t m_lastActiveNode{ 0 }; uintptr_t m_pGameWorld{ 0 }; uintptr_t m_pLocalGameWorld{ 0 }; uintptr_t m_pFpsCamera{ 0 }; Matrix m_mxViewMatrix{}; + GameRefreshStatus m_refreshStatus{ GameRefreshStatus::Waiting }; + uint64_t m_refreshAttempts{ 0 }; + uint64_t m_consecutiveRefreshFailures{ 0 }; bool m_initialized{ false }; bool m_cameraValid{ false }; @@ -35,6 +98,9 @@ private: bool UpdateViewMatrixUnlocked(); public: + CGame(); + explicit CGame(IGameMemoryReader& memory) noexcept; + bool InitAddress(); bool Update(); bool UpdateCamera(); @@ -53,4 +119,5 @@ extern bool Vec3_Empty(const Vector3& value); extern float GetDistance(const Vector3& value1, const Vector3& value2); extern Vector3 GetTransformPosition(uintptr_t transform); extern bool WorldToScreen(const Matrix& ViewMatrix, const Vector2& GameSize, const Vector3& vIn, Vector2& vOut); +extern uintptr_t GetObjectFromList(IGameMemoryReader& memory, uintptr_t listPtr, uintptr_t lastObjectPtr, const char* objectName); extern uintptr_t GetObjectFromList(uintptr_t listPtr, uintptr_t lastObjectPtr, const char* objectName); diff --git a/CFramework/Cheat/SDK/GameSDK/Offset.h b/CFramework/Cheat/SDK/GameSDK/Offset.h index d467339..4bfe421 100644 --- a/CFramework/Cheat/SDK/GameSDK/Offset.h +++ b/CFramework/Cheat/SDK/GameSDK/Offset.h @@ -78,11 +78,41 @@ namespace offset constexpr auto Status = 0xC8; } + namespace LootItem + { + constexpr auto Name = 0x58; + constexpr auto ItemId = 0x68; + constexpr auto TemplateId = 0x70; + constexpr auto Item = 0xB8; + } + + namespace InventoryItem + { + constexpr auto Id = 0x10; + constexpr auto Template = 0x40; + } + + namespace ItemTemplate + { + constexpr auto Name = 0x10; + constexpr auto ShortName = 0x18; + constexpr auto QuestItem = 0xBC; + constexpr auto Rarity = 0xEC; + } + + namespace LootItemPosition + { + constexpr auto Item = 0x18; + constexpr auto Position = 0x28; + constexpr auto IsContainer = 0x40; + } + // EFT.GameWorld / EFT.ClientLocalGameWorld namespace GameWorld { constexpr auto ExfilController = 0x30; constexpr auto LootList = 0x118; + constexpr auto AllLoot = 0x138; constexpr auto RegisteredPlayers = 0x140; constexpr auto AllAlivePlayersList = 0x168; constexpr auto ObservedPlayersById = 0x188; diff --git a/CFramework/Cheat/SnapshotHealth.h b/CFramework/Cheat/SnapshotHealth.h new file mode 100644 index 0000000..781618c --- /dev/null +++ b/CFramework/Cheat/SnapshotHealth.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +constexpr auto kDynamicSnapshotFreshnessLimit = std::chrono::milliseconds(250); +constexpr std::uint64_t kInvalidSnapshotAgeMs = (std::numeric_limits::max)(); + +struct SnapshotLiveness +{ + std::uint64_t generation{ 0 }; + std::chrono::steady_clock::time_point capturedAt{}; +}; + +inline SnapshotLiveness AdvanceSnapshotLiveness( + const std::uint64_t previousGeneration, + const std::chrono::steady_clock::time_point capturedAt) noexcept +{ + return { previousGeneration + 1, capturedAt }; +} + +inline std::uint64_t GetSnapshotAgeMs( + const std::chrono::steady_clock::time_point capturedAt, + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) noexcept +{ + if (capturedAt == std::chrono::steady_clock::time_point{}) + return kInvalidSnapshotAgeMs; + if (now <= capturedAt) + return 0; + + return static_cast( + std::chrono::duration_cast(now - capturedAt).count()); +} + +inline bool IsSnapshotFresh( + const std::chrono::steady_clock::time_point capturedAt, + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) noexcept +{ + const std::uint64_t ageMs = GetSnapshotAgeMs(capturedAt, now); + return ageMs != kInvalidSnapshotAgeMs && + ageMs <= static_cast(kDynamicSnapshotFreshnessLimit.count()); +} diff --git a/CFramework/Cheat/UpdateList.cpp b/CFramework/Cheat/UpdateList.cpp index be1c1e7..dfd5031 100644 --- a/CFramework/Cheat/UpdateList.cpp +++ b/CFramework/Cheat/UpdateList.cpp @@ -9,6 +9,7 @@ namespace constexpr int kMaxItems = 8192; constexpr int kMaxGrenades = 256; constexpr std::size_t kMaxExfils = 32; + constexpr std::size_t kMaxContainers = 4096; bool ReadManagedPointerList(uintptr_t listObject, std::vector& out, int hardLimit) { @@ -40,6 +41,7 @@ void CFramework::UpdateWorkerConfigFromUi() next.wantGrenades = g.g_ESP && g.g_ESP_Grenade; next.wantExfils = g.g_ESP && g.g_ESP_Exfil; next.wantItems = g.g_ESP && g.g_ESP_Item; + next.wantContainers = g.g_ESP && g.g_ESP_Container; if (g.g_ESP && g.g_Aim) next.remoteMaxDistance = (std::max)(g.g_ESP_MaxDistance, g.g_AimMaxDist); @@ -54,22 +56,29 @@ void CFramework::UpdateWorkerConfigFromUi() void CFramework::UpdateList() { - // CGame is owned/refreshed by this thread only. Consumers receive immutable snapshots. - while (g.process_active && !tarkov->Refresh()) - std::this_thread::sleep_for(std::chrono::milliseconds(250)); - + // Always publish a snapshot, even before GOM/GameWorld becomes available. Otherwise a failed + // first refresh leaves consumers stuck forever on the default generation-0 snapshot. while (g.process_active) { const auto tickStart = std::chrono::steady_clock::now(); const WorkerConfigSnapshot config = GetWorkerConfig(); auto snapshot = std::make_shared(); - snapshot->generation = ++m_dynamicGeneration; + const SnapshotLiveness liveness = AdvanceSnapshotLiveness(m_dynamicGeneration.fetch_add(1), tickStart); + snapshot->generation = liveness.generation; + snapshot->capturedAt = liveness.capturedAt; - const bool refreshed = tarkov->Refresh(); + tarkov->Refresh(); const GameStateSnapshot game = tarkov->GetState(); + snapshot->refreshStatus = game.refreshStatus; + snapshot->refreshAttempts = game.refreshAttempts; + snapshot->consecutiveRefreshFailures = game.consecutiveRefreshFailures; snapshot->initialized = game.initialized; - snapshot->inRaid = refreshed && game.inRaid; + snapshot->inRaid = game.inRaid; snapshot->cameraValid = game.cameraValid; + snapshot->moduleBase = game.moduleBase; + snapshot->gameObjectManager = game.gameObjectManager; + snapshot->activeNodeList = game.activeNodeList; + snapshot->lastActiveNode = game.lastActiveNode; snapshot->gameWorld = game.gameWorld; snapshot->localGameWorld = game.localGameWorld; snapshot->fpsCamera = game.fpsCamera; @@ -174,7 +183,8 @@ void CFramework::UpdateStaticList() staticSnapshot->generation = ++m_staticGeneration; const auto dynamicSnapshot = GetDynamicSnapshot(); - if (!dynamicSnapshot || !dynamicSnapshot->inRaid || !Memory::IsUserAddress(dynamicSnapshot->localGameWorld)) + if (!dynamicSnapshot || !IsSnapshotFresh(dynamicSnapshot->capturedAt) || + !dynamicSnapshot->inRaid || !Memory::IsUserAddress(dynamicSnapshot->localGameWorld)) { PublishStaticSnapshot(staticSnapshot); continue; @@ -202,6 +212,8 @@ void CFramework::UpdateStaticList() continue; exfil.m_pExfilName = exfil.GetName(); + if (exfil.m_pExfilName.empty()) + exfil.m_pExfilName = "Exfil"; staticSnapshot->exfils.push_back(std::move(exfil)); } } @@ -226,6 +238,24 @@ void CFramework::UpdateStaticList() } } + if (config.wantContainers) + { + uintptr_t allLoot = 0; + std::vector lootEntries; + if (m.TryRead(gameWorld + offset::GameWorld::AllLoot, allLoot) && + ReadManagedPointerList(allLoot, lootEntries, static_cast(kMaxContainers))) + { + staticSnapshot->containers.reserve(lootEntries.size()); + for (uintptr_t address : lootEntries) + { + CContainer container{}; + if (!Memory::IsUserAddress(address) || !container.GetAddress(address) || !container.Update()) + continue; + staticSnapshot->containers.push_back(std::move(container)); + } + } + } + PublishStaticSnapshot(staticSnapshot); } } diff --git a/CFramework/Cheat/Visual.cpp b/CFramework/Cheat/Visual.cpp index 1af4e22..bbd9902 100644 --- a/CFramework/Cheat/Visual.cpp +++ b/CFramework/Cheat/Visual.cpp @@ -8,9 +8,62 @@ bool InScreen(const Vector2& vIn) vIn.y < g.GameRect.top || vIn.y > g.GameRect.bottom); } +namespace +{ + const char* GetRefreshStatusName(GameRefreshStatus status) + { + switch (status) + { + case GameRefreshStatus::Waiting: return "WAIT"; + case GameRefreshStatus::GomUnavailable: return "GOM"; + case GameRefreshStatus::NodeListUnavailable: return "NODES"; + case GameRefreshStatus::GameWorldUnavailable: return "GW"; + case GameRefreshStatus::LocalWorldUnavailable: return "LGW"; + case GameRefreshStatus::Ready: return "READY"; + default: return "UNKNOWN"; + } + } + + ImColor GetItemDisplayColor(const CItem& item, + const ImColor& questColor, const ImColor& commonColor, + const ImColor& rareColor, const ImColor& superRareColor, + const ImColor& corpseColor) + { + if (item.m_bIsCorpse) + return corpseColor; + if (item.m_bIsQuestItem) + return questColor; + + // JsonType.ELootRarity exposes member order only in the dump. These values + // assume the C# enum uses its default sequential numbering: Common=1, + // Rare=2, Superrare=3. Unknown values intentionally fall back to common. + switch (item.m_iRarity) + { + case 2: return rareColor; + case 3: return superRareColor; + case 1: + default: return commonColor; + } + } + + ImColor GetExfilDisplayColor(const ExfilDisplayCategory category, + const ImColor& closedColor, const ImColor& openColor, + const ImColor& pendingColor, const ImColor& unknownColor) + { + switch (category) + { + case ExfilDisplayCategory::Closed: return closedColor; + case ExfilDisplayCategory::Open: return openColor; + case ExfilDisplayCategory::Pending: return pendingColor; + default: return unknownColor; + } + } +} + void CFramework::RenderInfo() { const auto snapshot = GetDynamicSnapshot(); + const bool snapshotFresh = snapshot && IsSnapshotFresh(snapshot->capturedAt); std::string infoText{ "Tarkov PvE | " + std::to_string((int)ImGui::GetIO().Framerate) + "FPS" }; StringEx(ImVec2(8.f, 8.f), ImColor(1.f, 1.f, 1.f, 1.f), ImGui::GetFontSize(), infoText.c_str()); @@ -19,18 +72,40 @@ void CFramework::RenderInfo() const bool inRaid = snapshot && snapshot->inRaid; const uintptr_t localAddress = snapshot ? snapshot->local.m_address : 0; const std::size_t entityCount = snapshot ? snapshot->players.size() : 0; + const uint64_t ageMs = snapshot ? GetSnapshotAgeMs(snapshot->capturedAt) : kInvalidSnapshotAgeMs; - char diag[224]{}; + char ageText[32]{}; + if (ageMs == kInvalidSnapshotAgeMs) + snprintf(ageText, sizeof(ageText), "N/A"); + else + snprintf(ageText, sizeof(ageText), "%llums", (unsigned long long)ageMs); + + char diag[320]{}; snprintf(diag, sizeof(diag), - "Init:%s Raid:%s Cam:%s Local:0x%llX Ent:%zu Gen:%llu", + "St:%s Init:%s Raid:%s Cam:%s Fresh:%s Age:%s Fail:%llu Try:%llu Gen:%llu", + snapshot ? GetRefreshStatusName(snapshot->refreshStatus) : "NONE", initialized ? "OK" : "FAIL", inRaid ? "Y" : "N", (snapshot && snapshot->cameraValid) ? "OK" : "FAIL", - (unsigned long long)localAddress, - entityCount, + snapshotFresh ? "Y" : "N", + ageText, + (unsigned long long)(snapshot ? snapshot->consecutiveRefreshFailures : 0), + (unsigned long long)(snapshot ? snapshot->refreshAttempts : 0), (unsigned long long)(snapshot ? snapshot->generation : 0)); StringEx(ImVec2(8.f, 24.f), ImColor(1.f, 1.f, 0.f, 1.f), ImGui::GetFontSize(), diag); + char pointers[320]{}; + snprintf(pointers, sizeof(pointers), + "B:%llX GOM:%llX N:%llX GW:%llX LGW:%llX L:%llX E:%zu", + (unsigned long long)(snapshot ? snapshot->moduleBase : 0), + (unsigned long long)(snapshot ? snapshot->gameObjectManager : 0), + (unsigned long long)(snapshot ? snapshot->activeNodeList : 0), + (unsigned long long)(snapshot ? snapshot->gameWorld : 0), + (unsigned long long)(snapshot ? snapshot->localGameWorld : 0), + (unsigned long long)localAddress, + entityCount); + StringEx(ImVec2(8.f, 40.f), ImColor(0.3f, 0.9f, 1.f, 1.f), ImGui::GetFontSize(), pointers); + char aim[192]{}; snprintf(aim, sizeof(aim), "Aim:%s M:%s F:%s Tgt:%s d=%.0fpx", @@ -39,9 +114,9 @@ void CFramework::RenderInfo() g.g_AimForce ? "FORCE" : "KEY", m_diagBestAddr ? "LOCK" : "NONE", m_diagBestDist); - StringEx(ImVec2(8.f, 42.f), ImColor(1.f, 0.f, 0.f, 1.f), ImGui::GetFontSize(), aim); + StringEx(ImVec2(8.f, 58.f), ImColor(1.f, 0.f, 0.f, 1.f), ImGui::GetFontSize(), aim); - if (g.g_Crosshair && snapshot && snapshot->local.IsValid() && !snapshot->local.IsAiming()) + if (g.g_Crosshair && snapshotFresh && snapshot->local.IsValid() && !snapshot->local.IsAiming()) { switch (g.g_CrosshairType) { @@ -64,7 +139,8 @@ void CFramework::RenderInfo() void CFramework::RenderESP() { const auto snapshot = GetDynamicSnapshot(); - if (!snapshot || !snapshot->inRaid || !snapshot->cameraValid || !snapshot->local.IsValid()) + if (!snapshot || !IsSnapshotFresh(snapshot->capturedAt) || + !snapshot->inRaid || !snapshot->cameraValid || !snapshot->local.IsValid()) return; const CPlayer& localPlayer = snapshot->local; @@ -224,7 +300,7 @@ void CFramework::RenderESP() { for (const auto& item : staticSnapshot->items) { - if (!item.m_bIsCorpse && item.m_CName.empty()) + if (item.m_CName.empty()) continue; if (!g.g_ESP_Corpse && item.m_bIsCorpse) continue; @@ -238,13 +314,32 @@ void CFramework::RenderESP() continue; std::string text = item.m_bIsCorpse ? "Corpse" : item.m_CName; - text += "[" + std::to_string((int)distance) + "m]"; - const ImColor itemColor = item.m_bIsCorpse ? Col_ESP_Corpse : Col_ESP_RareItem; + text += " [" + std::to_string((int)distance) + "m]"; + const ImColor itemColor = GetItemDisplayColor(item, Col_ESP_ItemQuest, Col_ESP_ItemCommon, + Col_ESP_ItemRare, Col_ESP_ItemSuperRare, Col_ESP_Corpse); ImGui::GetBackgroundDrawList()->AddCircleFilled(ImVec2(screen.x, screen.y - 2.f), 2.f, itemColor, 0); String(ImVec2(screen.x - (ImGui::CalcTextSize(text.c_str()).x / 2.f), screen.y), itemColor, text.c_str()); } } + if (g.g_ESP_Container) + { + for (const auto& container : staticSnapshot->containers) + { + const float distance = GetDistance(localPlayer.m_vecOrigin, container.m_vecOrigin); + if (distance > g.g_ESP_MaxContainerDistance) + continue; + + Vector2 screen{}; + if (!WorldToScreen(viewMatrix, gameSize, container.m_vecOrigin, screen) || !InScreen(screen)) + continue; + + const std::string text = container.m_CName + " [" + std::to_string((int)distance) + "m]"; + ImGui::GetBackgroundDrawList()->AddCircleFilled(ImVec2(screen.x, screen.y - 2.f), 2.f, Col_ESP_Container, 0); + String(ImVec2(screen.x - (ImGui::CalcTextSize(text.c_str()).x / 2.f), screen.y), Col_ESP_Container, text.c_str()); + } + } + if (g.g_ESP_Exfil) { for (const auto& exfil : staticSnapshot->exfils) @@ -253,8 +348,12 @@ void CFramework::RenderESP() if (!WorldToScreen(viewMatrix, gameSize, exfil.m_pVecLocation, screen) || !InScreen(screen)) continue; - const ImColor exfilColor = exfil.m_pExfilStatus == NOTREADY ? Col_ESP_ExfilClose : Col_ESP_ExfilOpen; - const std::string text = exfil.m_pExfilName + " [" + std::to_string((int)GetDistance(localPlayer.m_vecOrigin, exfil.m_pVecLocation)) + "m]"; + const ExfilDisplayCategory category = ClassifyExfilStatus(exfil.m_pExfilStatus); + const ImColor exfilColor = GetExfilDisplayColor(category, Col_ESP_ExfilClose, + Col_ESP_ExfilOpen, Col_ESP_ExfilPending, Col_ESP_ExfilUnknown); + const float distance = GetDistance(localPlayer.m_vecOrigin, exfil.m_pVecLocation); + const std::string text = exfil.m_pExfilName + " [" + GetExfilStatusText(exfil.m_pExfilStatus) + "] [" + + std::to_string((int)distance) + "m]"; ImGui::GetBackgroundDrawList()->AddCircleFilled(ImVec2(screen.x, screen.y - 2.f), 2.f, exfilColor, 0); String(ImVec2(screen.x - (ImGui::CalcTextSize(text.c_str()).x / 2.f), screen.y), exfilColor, text.c_str()); } diff --git a/CFramework/Framework/Config/Config.h b/CFramework/Framework/Config/Config.h index 8892e75..1d55e52 100644 --- a/CFramework/Framework/Config/Config.h +++ b/CFramework/Framework/Config/Config.h @@ -17,6 +17,7 @@ struct Globals bool g_ESP = true; bool g_ESP_Item = true; bool g_ESP_Corpse = true; + bool g_ESP_Container = true; bool g_ESP_Exfil = true; bool g_ESP_Grenade = true; bool g_ESP_Line = false; @@ -29,6 +30,7 @@ struct Globals int g_ESP_BoxStyle = 1; float g_ESP_MaxDistance = 500.f; float g_ESP_MaxItemDistance = 50.f; + float g_ESP_MaxContainerDistance = 100.f; // Misc (仅使用 dump 已确认的对象字段) bool g_InfStamina = false; diff --git a/README.md b/README.md index 56c178d..c401069 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,21 @@ ## 构建 - 环境:**Visual Studio 2022**(MSVC v143),x64。 -- 所有第三方库(ImGui、DirectXTK SimpleMath、FreeType、nlohmann/json 等)已包含在仓库内,**无需额外安装**,直接打开 `CFramework.sln` 选择 **Release x64** 构建即可。 -- 首次构建参考:[Visual Studio C++ 安装指南](https://learn.microsoft.com/ja-jp/cpp/build/vscpp-step-0-installation?view=msvc-170) -输出:`x64\Release\CFramework.exe` +- 所有第三方库(ImGui、DirectXTK SimpleMath、FreeType、nlohmann/json 等)已包含在仓库内,无需额外安装。 +- 日常开发不要只执行裸构建;先运行统一回归门禁: + +```powershell +powershell -ExecutionPolicy Bypass -File .\verify_runtime.ps1 +``` + +门禁会依次: + +1. 从当前源码重建 `CFramework.SelfTest`; +2. 运行对象链、四种 GOM 布局、首次失败恢复、Snapshot freshness、Camera 和 W2S 回归测试; +3. 仅在全部测试通过后重建 Release x64 Overlay; +4. 将隔离产物和 SHA256 输出到 `x64\RuntimeGuard\`,不会覆盖正在使用的 `x64\Release\CFramework.exe`。 + +也可以直接打开 `CFramework.sln` 选择 **Release x64** 构建;正式发布前仍必须运行上述门禁。 运行前提:先启动 SPT-AKI 3.11.3 客户端(进程名为 `EscapeFromTarkov`),再以管理员身份运行 `CFramework.exe`。 程序通过窗口标题附加进程并创建覆盖层;若附加失败返回码 `1`,Overlay 初始化失败返回码 `2`。 @@ -78,6 +90,8 @@ - ESP 与 Aim 目标选择只消费 Snapshot,不在渲染阶段直接 RPM。真正的 Memory Aim / 功能 override 仍通过独立写入路径执行。 - `RegisteredPlayers` 按 `IPlayer` 处理,并区分完整 `EFT.Player` 与 `ObservedPlayerView` 两种布局。ObservedPlayer 无法从当前 dump 可靠确认的 Profile/Health/Role 字段不会强行套用。 - 后台线程不直接读取 ImGui 绑定的普通 `g_*` 变量;渲染线程通过 `WorkerConfigSnapshot` 发布所需配置。 +- 动态 Snapshot 带采集时间、刷新次数和连续失败数;超过 250ms 的 stale 数据不会用于 ESP、Aim 或功能写入。 +- `CGame` 的读取边界支持注入 synthetic memory reader;`CFramework.SelfTest` 可在不启动游戏的情况下复现 GOM/节点链和失败恢复路径。 - 玩家、物资、手雷列表使用运行时长度 + 上限校验的批量读取,不再使用固定 `address[64]` / `address[2048]` 缓冲。 - 工作线程可 join;退出时先停止并等待后台线程,再恢复功能写入和关闭进程句柄。 @@ -85,7 +99,7 @@ ## 已知问题 -- 当前包是在 Linux 容器中做源码级重构,环境没有 Visual Studio 2022 / Windows SDK,因此没有执行真正的 MSVC Release x64 链接构建。核心重构源文件已用 Clang + Windows API 测试桩做 `-fsyntax-only` 验证;最终仍建议在 VS2022 中执行一次 `Release x64` 全量构建。 +- 当前 Windows 环境已完成 MSVC v143 `Release x64` 全量重建,并通过 16 项离线 Runtime SelfTest;仍需在每次 offset、线程或读取逻辑变更后执行 `verify_runtime.ps1` 和实机进出局烟雾测试。 - `ObservedPlayerView` 的昵称、Role、实时 Health 等字段在当前 dump 中仍是混淆字段;本次实现采取保守降级显示,不猜测未知字段语义。 - 主 Overlay 仍跟随 VSync/显示器刷新率;`UpdateFeatures()` 是渲染线程写功能路径,后续如果需要进一步降低 CPU/RPM,可以单独给写功能增加节流/脏标记。 @@ -93,10 +107,15 @@ ``` CFramework/ -├── CFramework.sln / .vcxproj # VS 解决方案 +├── CFramework.sln # VS 解决方案(主程序 + SelfTest) +├── verify_runtime.ps1 # Runtime 回归门禁:测试通过后才构建 Overlay +├── CFramework.SelfTest/ # synthetic memory 离线测试项目 +│ └── SelfTest.cpp ├── CFramework/ +│ ├── CFramework.vcxproj │ ├── main.cpp # 入口:附加进程、初始化 Overlay、字体、后台线程 │ ├── Cheat/ # 功能实现 +│ │ ├── SnapshotHealth.h # generation、时间戳和 250ms freshness 契约 │ │ ├── Aim.cpp # 自瞄 + 压枪(Mouse / Memory) │ │ ├── Visual.cpp # ESP 渲染(玩家/物资/撤离点/手雷) │ │ ├── Features.cpp # 耐力/跌落/摇摆/后座/故障 override diff --git a/REFACTOR_NOTES.md b/REFACTOR_NOTES.md index 02da663..9830295 100644 --- a/REFACTOR_NOTES.md +++ b/REFACTOR_NOTES.md @@ -51,22 +51,30 @@ - `min` 调用改为显式 `std::min`,降低 Windows 宏依赖。 - README 移除源码中不存在的“子弹追踪(只读)已完成”声明。 +## Runtime 防回归护栏 + +为避免后续 pointer validation、GOM 布局或线程优化再次让 ESP 静默失效,新增以下强制契约: + +- 动态 Snapshot 无论刷新成功与否都持续发布,`generation` 单调增加; +- Snapshot 带 `capturedAt`、刷新总次数和连续失败数;超过 250ms 的数据不会用于 ESP、Aim 或功能写入; +- `CGame` 通过 `IGameMemoryReader` 支持 synthetic memory 注入,生产默认适配器仍使用全局 `Memory m`; +- `GetObjectFromList` 保留可选 last sentinel,并对 self-loop 和多节点环进行检测; +- 新增独立 `CFramework.SelfTest` Console 项目,覆盖对象链、四种 GOM 布局、首次失败恢复、Camera 降级和 W2S; +- 根目录 `verify_runtime.ps1` 会先重建并运行 SelfTest,测试失败则阻止 Overlay 构建。 + ## 验证 -在当前容器里对以下核心文件执行了 Clang C++17 `-fsyntax-only` 静态语法验证(Windows API 使用最小测试桩;内置 UTF-16 字体头仅在临时测试副本中转换编码): +当前 Windows 环境已执行: -- `Cheat/UpdateList.cpp` -- `Cheat/Visual.cpp` -- `Cheat/Aim.cpp` -- `Cheat/Features.cpp` -- `Cheat/Menu.cpp` -- `Cheat/SDK/GameSDK/GameSDK.cpp` -- `Cheat/SDK/CPlayer/CPlayer.cpp` -- `Cheat/SDK/CExfil/CExfil.cpp` -- `Cheat/SDK/CItem/CItem.cpp` -- `Cheat/Render.cpp` -- `Framework/Memory/Memory.cpp`(仅有测试桩下的 `NULL -> DWORD` 警告) +```powershell +powershell -ExecutionPolicy Bypass -File .\verify_runtime.ps1 +``` -以上文件均通过语法检查。 +验证结果: -当前环境没有 Visual Studio 2022、MSVC v143、Windows SDK 和 DX11/DWM 链接环境,因此**没有声称已经完成真实 Windows Release 链接/运行验证**。建议拿到包后第一步在 VS2022 中执行 `Release | x64` 全量 Rebuild,并根据实际 SPT/EFT build 对照最新 dump 校验根指针和版本敏感 offset。 +- Offline Runtime SelfTest:16 PASS,0 FAIL; +- 主程序:MSVC v143 `Release | x64` 全量重建成功; +- 新产物写入隔离目录 `x64\RuntimeGuard\`,不会覆盖正在运行的 known-good `x64\Release\CFramework.exe`; +- 仅保留项目既存的 `C4828`、`C4244`、`LNK4098` 和 FreeType `LNK4099` warning,无新增编译或链接错误。 + +离线门禁不能替代最终实机烟雾测试。涉及 offset、Camera、玩家结构或线程生命周期的修改,仍必须验证启动顺序、连续进出局、`Gen/Fresh/Age/Fail/Try` 和 ESP/Aim 实际效果。 diff --git a/verify_runtime.ps1 b/verify_runtime.ps1 new file mode 100644 index 0000000..82ab9e4 --- /dev/null +++ b/verify_runtime.ps1 @@ -0,0 +1,77 @@ +[CmdletBinding()] +param( + [string]$OutputDirectory = '' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$root = (Resolve-Path -LiteralPath $PSScriptRoot).Path +if ([string]::IsNullOrWhiteSpace($OutputDirectory)) { + $OutputDirectory = Join-Path $root 'x64\RuntimeGuard' +} + +$output = [System.IO.Path]::GetFullPath($OutputDirectory).TrimEnd('\') + '\' +$intermediateRoot = $output.TrimEnd('\') + '.int' +$vswhere = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' +if (-not (Test-Path -LiteralPath $vswhere)) { + throw "vswhere not found: $vswhere" +} + +$msbuild = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild ` + -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 +if ([string]::IsNullOrWhiteSpace($msbuild)) { + throw 'MSBuild was not found by vswhere.' +} + +New-Item -ItemType Directory -Force -Path $output | Out-Null +New-Item -ItemType Directory -Force -Path $intermediateRoot | Out-Null + +function Invoke-ProjectRebuild { + param( + [Parameter(Mandatory)] [string]$Project, + [Parameter(Mandatory)] [string]$IntermediateName + ) + + $intermediate = (Join-Path $intermediateRoot $IntermediateName).TrimEnd('\') + '\' + $arguments = @( + $Project, + '/m', + '/t:Rebuild', + '/p:Configuration=Release', + '/p:Platform=x64', + "/p:OutDir=$output", + "/p:IntDir=$intermediate", + '/v:minimal' + ) + + & $msbuild @arguments + if ($LASTEXITCODE -ne 0) { + throw "MSBuild failed for $Project with exit code $LASTEXITCODE." + } +} + +$selfTestProject = Join-Path $root 'CFramework.SelfTest\CFramework.SelfTest.vcxproj' +$overlayProject = Join-Path $root 'CFramework\CFramework.vcxproj' +$selfTestExe = Join-Path $output 'CFramework.SelfTest.exe' +$overlayExe = Join-Path $output 'CFramework.exe' + +Write-Host '[guard] Rebuilding offline runtime tests...' +Invoke-ProjectRebuild -Project $selfTestProject -IntermediateName 'SelfTest' + +Write-Host '[guard] Running offline runtime tests...' +& $selfTestExe +if ($LASTEXITCODE -ne 0) { + throw "Runtime self-test failed with exit code $LASTEXITCODE. Overlay build blocked." +} + +Write-Host '[guard] Tests passed; rebuilding Release x64 overlay...' +Invoke-ProjectRebuild -Project $overlayProject -IntermediateName 'CFramework' + +$selfTestHash = (Get-FileHash -LiteralPath $selfTestExe -Algorithm SHA256).Hash +$overlayHash = (Get-FileHash -LiteralPath $overlayExe -Algorithm SHA256).Hash +Write-Host "[guard] PASS" +Write-Host "[guard] SelfTest: $selfTestExe" +Write-Host "[guard] SelfTest SHA256: $selfTestHash" +Write-Host "[guard] Overlay: $overlayExe" +Write-Host "[guard] Overlay SHA256: $overlayHash"