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"