feat: add runtime guard and world data updates

This commit is contained in:
kedou 2026-08-08 12:42:01 +08:00
parent 390450e8e8
commit 9d09d6e3dc
26 changed files with 1507 additions and 159 deletions

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32"><Configuration>Debug</Configuration><Platform>Win32</Platform></ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32"><Configuration>Release</Configuration><Platform>Win32</Platform></ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64"><Configuration>Debug</Configuration><Platform>x64</Platform></ProjectConfiguration>
<ProjectConfiguration Include="Release|x64"><Configuration>Release</Configuration><Platform>x64</Platform></ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}</ProjectGuid>
<RootNamespace>CFrameworkSelfTest</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>CFramework.SelfTest</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PreprocessorDefinitions>_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(ProjectDir)..\CFramework\Framework\Libs\Include;$(ProjectDir)..\CFramework;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<AdditionalDependencies>User32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="SelfTest.cpp" />
<ClCompile Include="..\CFramework\Cheat\SDK\GameSDK\GameSDK.cpp"><Link>GameSDK.cpp</Link></ClCompile>
<ClCompile Include="..\CFramework\Framework\Memory\Memory.cpp"><Link>Memory.cpp</Link></ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\CFramework\Cheat\SDK\GameSDK\GameSDK.h"><Link>GameSDK.h</Link></ClInclude>
<ClInclude Include="..\CFramework\Cheat\SnapshotHealth.h"><Link>SnapshotHealth.h</Link></ClInclude>
<ClInclude Include="..\CFramework\Framework\Memory\Memory.h"><Link>Memory.h</Link></ClInclude>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files"><UniqueIdentifier>{A3B14D42-97EF-4B22-8F08-C3D2EBB40780}</UniqueIdentifier></Filter>
<Filter Include="SDK"><UniqueIdentifier>{AA584E7B-2691-498B-AB5E-254A42857A93}</UniqueIdentifier></Filter>
<Filter Include="Memory"><UniqueIdentifier>{2B1EC84D-EBA9-42F4-B64B-E1CB68801564}</UniqueIdentifier></Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="SelfTest.cpp"><Filter>Source Files</Filter></ClCompile>
<ClCompile Include="..\CFramework\Cheat\SDK\GameSDK\GameSDK.cpp"><Filter>SDK</Filter></ClCompile>
<ClCompile Include="..\CFramework\Framework\Memory\Memory.cpp"><Filter>Memory</Filter></ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\CFramework\Cheat\SDK\GameSDK\GameSDK.h"><Filter>SDK</Filter></ClInclude>
<ClInclude Include="..\CFramework\Cheat\SnapshotHealth.h"><Filter>SDK</Filter></ClInclude>
<ClInclude Include="..\CFramework\Framework\Memory\Memory.h"><Filter>Memory</Filter></ClInclude>
</ItemGroup>
</Project>

View File

@ -0,0 +1,401 @@
#include "../CFramework/Cheat/SDK/GameSDK/GameSDK.h"
#include "../CFramework/Cheat/SnapshotHealth.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <exception>
#include <iostream>
#include <string>
#include <type_traits>
#include <unordered_map>
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<std::uint8_t*>(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 <typename T>
void Write(const uintptr_t address, const T& value)
{
static_assert(std::is_trivially_copyable_v<T>, "synthetic memory values must be trivially copyable");
const auto* bytes = reinterpret_cast<const std::uint8_t*>(&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<uintptr_t, std::uint8_t> m_bytes;
std::unordered_map<uintptr_t, std::string> m_strings;
mutable std::unordered_map<uintptr_t, std::size_t> m_failuresRemaining;
mutable std::unordered_map<uintptr_t, std::size_t> 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 <typename Test>
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();
}

View File

@ -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

View File

@ -146,6 +146,7 @@
<ClCompile Include="Cheat\Render.cpp" />
<ClCompile Include="Cheat\SDK\CExfil\CExfil.cpp" />
<ClCompile Include="Cheat\SDK\CItem\CItem.cpp" />
<ClCompile Include="Cheat\SDK\CContainer\CContainer.cpp" />
<ClCompile Include="Cheat\SDK\CPlayer\CPlayer.cpp" />
<ClCompile Include="Cheat\SDK\GameSDK\GameSDK.cpp" />
<ClCompile Include="Cheat\UpdateList.cpp" />
@ -168,8 +169,10 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="Cheat\Framework.h" />
<ClInclude Include="Cheat\SnapshotHealth.h" />
<ClInclude Include="Cheat\SDK\CExfil\CExfil.h" />
<ClInclude Include="Cheat\SDK\CItem\CItem.h" />
<ClInclude Include="Cheat\SDK\CContainer\CContainer.h" />
<ClInclude Include="Cheat\SDK\CPlayer\CPlayer.h" />
<ClInclude Include="Cheat\SDK\GameSDK\GameSDK.h" />
<ClInclude Include="Cheat\SDK\GameSDK\Offset.h" />

View File

@ -31,6 +31,9 @@
<Filter Include="Cheat\SDK\CItem">
<UniqueIdentifier>{90d19cbc-e194-4714-b59b-34cf9d2bf64f}</UniqueIdentifier>
</Filter>
<Filter Include="Cheat\SDK\CContainer">
<UniqueIdentifier>{d3c6c6ae-8cf3-4c09-a0df-3a2a0f1b31c4}</UniqueIdentifier>
</Filter>
<Filter Include="Cheat\SDK\GameSDK">
<UniqueIdentifier>{c2a6f7bc-fc36-46f5-8697-b1e2b32653ee}</UniqueIdentifier>
</Filter>
@ -72,12 +75,12 @@
<ClCompile Include="Cheat\SDK\CExfil\CExfil.cpp">
<Filter>Cheat\SDK\CExfil</Filter>
</ClCompile>
<ClCompile Include="Cheat\SDK\GameSDK\GameSDK.cpp">
<Filter>Cheat\SDK\GameSDK</Filter>
</ClCompile>
<ClCompile Include="Cheat\SDK\CItem\CItem.cpp">
<Filter>Cheat\SDK\CItem</Filter>
</ClCompile>
<ClCompile Include="Cheat\SDK\CContainer\CContainer.cpp">
<Filter>Cheat\SDK\CContainer</Filter>
</ClCompile>
<ClCompile Include="Framework\ImGui\Custom.cpp">
<Filter>Framework\ImGui</Filter>
</ClCompile>
@ -131,6 +134,9 @@
<ClInclude Include="Cheat\Framework.h">
<Filter>Cheat</Filter>
</ClInclude>
<ClInclude Include="Cheat\SnapshotHealth.h">
<Filter>Cheat</Filter>
</ClInclude>
<ClInclude Include="Cheat\SDK\CPlayer\CPlayer.h">
<Filter>Cheat\SDK\CPlayer</Filter>
</ClInclude>
@ -146,6 +152,9 @@
<ClInclude Include="Cheat\SDK\CItem\CItem.h">
<Filter>Cheat\SDK\CItem</Filter>
</ClInclude>
<ClInclude Include="Cheat\SDK\CContainer\CContainer.h">
<Filter>Cheat\SDK\CContainer</Filter>
</ClInclude>
<ClInclude Include="Framework\ImGui\Fonts\fa.h">
<Filter>Framework\ImGui\Fonts</Filter>
</ClInclude>

View File

@ -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;

View File

@ -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();

View File

@ -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 <mutex>
#include <memory>
#include <atomic>
@ -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<CExfil> exfils{};
std::vector<CItem> items{};
std::vector<CContainer> containers{};
};
class CFramework
@ -189,7 +201,7 @@ private:
std::vector<CPlayer> GetEntityList() const
{
auto snapshot = GetDynamicSnapshot();
return snapshot ? snapshot->players : std::vector<CPlayer>{};
return snapshot && IsSnapshotFresh(snapshot->capturedAt) ? snapshot->players : std::vector<CPlayer>{};
}
std::vector<CExfil> GetExfilList() const
{
@ -201,6 +213,11 @@ private:
auto snapshot = GetStaticSnapshot();
return snapshot ? snapshot->items : std::vector<CItem>{};
}
std::vector<CContainer> GetContainerList() const
{
auto snapshot = GetStaticSnapshot();
return snapshot ? snapshot->containers : std::vector<CContainer>{};
}
// 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 };

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,17 @@
#pragma once
#include "../CItem/CItem.h"
#include <cstdint>
#include <string>
// 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();
};

View File

@ -1,5 +1,68 @@
#include "CExfil.h"
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
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<char16_t> chars;
if (!m.TryReadVector<char16_t>(stringObject + offset::ManagedString::Data,
static_cast<std::size_t>(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<char>(ch) : '?');
}
return name;
}
}
ExfilDisplayCategory ClassifyExfilStatus(const uint8_t status) noexcept
{
switch (status)
{
case static_cast<uint8_t>(CLOSE):
return ExfilDisplayCategory::Closed;
case static_cast<uint8_t>(NOTREADY):
return ExfilDisplayCategory::Pending;
case static_cast<uint8_t>(OPEN):
case static_cast<uint8_t>(READY):
return ExfilDisplayCategory::Open;
default:
return ExfilDisplayCategory::Unknown;
}
}
const char* GetExfilStatusText(const uint8_t status) noexcept
{
switch (status)
{
case static_cast<uint8_t>(CLOSE): return "Closed";
case static_cast<uint8_t>(NOTREADY): return "Not ready";
case static_cast<uint8_t>(OPEN): return "Open";
case static_cast<uint8_t>(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<std::size_t>(length));
for (int i = 0; i < length; ++i)
{
wchar_t ch = 0;
if (!m.TryRead(namePtr + offset::ManagedString::Data + static_cast<uintptr_t>(i) * offset::ManagedString::CharStride, ch))
break;
if (ch >= 0x20 && ch <= 0x7E)
name.push_back(static_cast<char>(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,

View File

@ -1,20 +1,32 @@
#pragma once
#include "../GameSDK/GameSDK.h"
#include <cstdint>
#include <string>
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();
};

View File

@ -1,23 +1,136 @@
#include "CItem.h"
#include <cstdint>
#include <string>
#include <vector>
struct C_EFT_ItemData
namespace
{
int item_price;
std::string item_name;
std::string item_id;
};
constexpr int kMaxManagedStringLength = 128;
std::vector<C_EFT_ItemData> C_LootList =
void AppendUtf8(std::string& output, uint32_t codePoint)
{
if (codePoint <= 0x7F)
{
output.push_back(static_cast<char>(codePoint));
}
else if (codePoint <= 0x7FF)
{
output.push_back(static_cast<char>(0xC0 | (codePoint >> 6)));
output.push_back(static_cast<char>(0x80 | (codePoint & 0x3F)));
}
else if (codePoint <= 0xFFFF)
{
output.push_back(static_cast<char>(0xE0 | (codePoint >> 12)));
output.push_back(static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F)));
output.push_back(static_cast<char>(0x80 | (codePoint & 0x3F)));
}
else
{
output.push_back(static_cast<char>(0xF0 | (codePoint >> 18)));
output.push_back(static_cast<char>(0x80 | ((codePoint >> 12) & 0x3F)));
output.push_back(static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F)));
output.push_back(static_cast<char>(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<char16_t> chars;
if (!m.TryReadVector<char16_t>(stringObject + offset::ManagedString::Data,
static_cast<std::size_t>(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")

View File

@ -1,18 +1,35 @@
#pragma once
#include "../GameSDK/GameSDK.h"
#include <cstdint>
#include <string>
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();
};

View File

@ -1,6 +1,36 @@
#include "GameSDK.h"
#include <array>
#include <cmath>
#include <cstring>
#include <unordered_set>
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_T>(size));
}
bool ReadString(const uintptr_t address, void* buffer, const std::size_t size) const override
{
return m.ReadString(address, buffer, static_cast<SIZE_T>(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<uintptr_t> 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<NodePair, 4> 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<uintptr_t>(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<uintptr_t>(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<std::mutex> 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<std::mutex> 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;

View File

@ -3,13 +3,69 @@
#include "Struct.h"
#include "../../../Framework/Memory/Memory.h"
#include "../../../Framework/Config/Config.h"
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <vector>
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 <typename T>
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<uintptr_t>& 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);

View File

@ -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;

View File

@ -0,0 +1,43 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <limits>
constexpr auto kDynamicSnapshotFreshnessLimit = std::chrono::milliseconds(250);
constexpr std::uint64_t kInvalidSnapshotAgeMs = (std::numeric_limits<std::uint64_t>::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::uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(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<std::uint64_t>(kDynamicSnapshotFreshnessLimit.count());
}

View File

@ -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<uintptr_t>& 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<DynamicWorldSnapshot>();
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<uintptr_t> lootEntries;
if (m.TryRead(gameWorld + offset::GameWorld::AllLoot, allLoot) &&
ReadManagedPointerList(allLoot, lootEntries, static_cast<int>(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);
}
}

View File

@ -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());
}

View File

@ -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;

View File

@ -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

View File

@ -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 实际效果。

77
verify_runtime.ps1 Normal file
View File

@ -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"