feat: add runtime guard and world data updates
This commit is contained in:
parent
390450e8e8
commit
9d09d6e3dc
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@ VisualStudioVersion = 17.11.35219.272
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CFramework", "CFramework\CFramework.vcxproj", "{E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CFramework", "CFramework\CFramework.vcxproj", "{E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CFramework.SelfTest", "CFramework.SelfTest\CFramework.SelfTest.vcxproj", "{4F524CAB-6A95-4A98-95FC-31B8A7E19C3D}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|x64 = Debug|x64
|
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|x64.Build.0 = Release|x64
|
||||||
{E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}.Release|x86.ActiveCfg = Release|Win32
|
{E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}.Release|x86.ActiveCfg = Release|Win32
|
||||||
{E70BCF62-457C-4E1E-9CF0-35E8988F1B0A}.Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@
|
||||||
<ClCompile Include="Cheat\Render.cpp" />
|
<ClCompile Include="Cheat\Render.cpp" />
|
||||||
<ClCompile Include="Cheat\SDK\CExfil\CExfil.cpp" />
|
<ClCompile Include="Cheat\SDK\CExfil\CExfil.cpp" />
|
||||||
<ClCompile Include="Cheat\SDK\CItem\CItem.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\CPlayer\CPlayer.cpp" />
|
||||||
<ClCompile Include="Cheat\SDK\GameSDK\GameSDK.cpp" />
|
<ClCompile Include="Cheat\SDK\GameSDK\GameSDK.cpp" />
|
||||||
<ClCompile Include="Cheat\UpdateList.cpp" />
|
<ClCompile Include="Cheat\UpdateList.cpp" />
|
||||||
|
|
@ -168,8 +169,10 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="Cheat\Framework.h" />
|
<ClInclude Include="Cheat\Framework.h" />
|
||||||
|
<ClInclude Include="Cheat\SnapshotHealth.h" />
|
||||||
<ClInclude Include="Cheat\SDK\CExfil\CExfil.h" />
|
<ClInclude Include="Cheat\SDK\CExfil\CExfil.h" />
|
||||||
<ClInclude Include="Cheat\SDK\CItem\CItem.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\CPlayer\CPlayer.h" />
|
||||||
<ClInclude Include="Cheat\SDK\GameSDK\GameSDK.h" />
|
<ClInclude Include="Cheat\SDK\GameSDK\GameSDK.h" />
|
||||||
<ClInclude Include="Cheat\SDK\GameSDK\Offset.h" />
|
<ClInclude Include="Cheat\SDK\GameSDK\Offset.h" />
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@
|
||||||
<Filter Include="Cheat\SDK\CItem">
|
<Filter Include="Cheat\SDK\CItem">
|
||||||
<UniqueIdentifier>{90d19cbc-e194-4714-b59b-34cf9d2bf64f}</UniqueIdentifier>
|
<UniqueIdentifier>{90d19cbc-e194-4714-b59b-34cf9d2bf64f}</UniqueIdentifier>
|
||||||
</Filter>
|
</Filter>
|
||||||
|
<Filter Include="Cheat\SDK\CContainer">
|
||||||
|
<UniqueIdentifier>{d3c6c6ae-8cf3-4c09-a0df-3a2a0f1b31c4}</UniqueIdentifier>
|
||||||
|
</Filter>
|
||||||
<Filter Include="Cheat\SDK\GameSDK">
|
<Filter Include="Cheat\SDK\GameSDK">
|
||||||
<UniqueIdentifier>{c2a6f7bc-fc36-46f5-8697-b1e2b32653ee}</UniqueIdentifier>
|
<UniqueIdentifier>{c2a6f7bc-fc36-46f5-8697-b1e2b32653ee}</UniqueIdentifier>
|
||||||
</Filter>
|
</Filter>
|
||||||
|
|
@ -72,12 +75,12 @@
|
||||||
<ClCompile Include="Cheat\SDK\CExfil\CExfil.cpp">
|
<ClCompile Include="Cheat\SDK\CExfil\CExfil.cpp">
|
||||||
<Filter>Cheat\SDK\CExfil</Filter>
|
<Filter>Cheat\SDK\CExfil</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="Cheat\SDK\GameSDK\GameSDK.cpp">
|
|
||||||
<Filter>Cheat\SDK\GameSDK</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="Cheat\SDK\CItem\CItem.cpp">
|
<ClCompile Include="Cheat\SDK\CItem\CItem.cpp">
|
||||||
<Filter>Cheat\SDK\CItem</Filter>
|
<Filter>Cheat\SDK\CItem</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="Cheat\SDK\CContainer\CContainer.cpp">
|
||||||
|
<Filter>Cheat\SDK\CContainer</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="Framework\ImGui\Custom.cpp">
|
<ClCompile Include="Framework\ImGui\Custom.cpp">
|
||||||
<Filter>Framework\ImGui</Filter>
|
<Filter>Framework\ImGui</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
|
@ -131,6 +134,9 @@
|
||||||
<ClInclude Include="Cheat\Framework.h">
|
<ClInclude Include="Cheat\Framework.h">
|
||||||
<Filter>Cheat</Filter>
|
<Filter>Cheat</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="Cheat\SnapshotHealth.h">
|
||||||
|
<Filter>Cheat</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include="Cheat\SDK\CPlayer\CPlayer.h">
|
<ClInclude Include="Cheat\SDK\CPlayer\CPlayer.h">
|
||||||
<Filter>Cheat\SDK\CPlayer</Filter>
|
<Filter>Cheat\SDK\CPlayer</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
|
@ -146,6 +152,9 @@
|
||||||
<ClInclude Include="Cheat\SDK\CItem\CItem.h">
|
<ClInclude Include="Cheat\SDK\CItem\CItem.h">
|
||||||
<Filter>Cheat\SDK\CItem</Filter>
|
<Filter>Cheat\SDK\CItem</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="Cheat\SDK\CContainer\CContainer.h">
|
||||||
|
<Filter>Cheat\SDK\CContainer</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include="Framework\ImGui\Fonts\fa.h">
|
<ClInclude Include="Framework\ImGui\Fonts\fa.h">
|
||||||
<Filter>Framework\ImGui\Fonts</Filter>
|
<Filter>Framework\ImGui\Fonts</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,8 @@ void CFramework::AimAll()
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const auto snapshot = GetDynamicSnapshot();
|
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;
|
return;
|
||||||
|
|
||||||
const CPlayer& localPlayer = snapshot->local;
|
const CPlayer& localPlayer = snapshot->local;
|
||||||
|
|
@ -447,7 +448,8 @@ void CFramework::ScanRotationOffset()
|
||||||
|
|
||||||
const auto snapshot = GetDynamicSnapshot();
|
const auto snapshot = GetDynamicSnapshot();
|
||||||
const uintptr_t localAddress = snapshot ? snapshot->local.m_address : 0;
|
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 再点)");
|
strcpy_s(m_scanResult, "仅限局内扫描 (先进 raid 再点)");
|
||||||
m_scanPhase = 0;
|
m_scanPhase = 0;
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,13 @@ namespace
|
||||||
void CFramework::UpdateFeatures()
|
void CFramework::UpdateFeatures()
|
||||||
{
|
{
|
||||||
const auto snapshot = GetDynamicSnapshot();
|
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 = {};
|
local = {};
|
||||||
ResetFeatureOverrides();
|
ResetFeatureOverrides();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
#include "SDK/CPlayer/CPlayer.h"
|
#include "SDK/CPlayer/CPlayer.h"
|
||||||
#include "SDK/CExfil/CExfil.h"
|
#include "SDK/CExfil/CExfil.h"
|
||||||
#include "SDK/CItem/CItem.h"
|
#include "SDK/CItem/CItem.h"
|
||||||
|
#include "SDK/CContainer/CContainer.h"
|
||||||
|
#include "SnapshotHealth.h"
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
@ -25,9 +27,17 @@ struct GrenadeSnapshot
|
||||||
struct DynamicWorldSnapshot
|
struct DynamicWorldSnapshot
|
||||||
{
|
{
|
||||||
uint64_t generation{ 0 };
|
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 initialized{ false };
|
||||||
bool inRaid{ false };
|
bool inRaid{ false };
|
||||||
bool cameraValid{ 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 gameWorld{ 0 };
|
||||||
uintptr_t localGameWorld{ 0 };
|
uintptr_t localGameWorld{ 0 };
|
||||||
uintptr_t fpsCamera{ 0 };
|
uintptr_t fpsCamera{ 0 };
|
||||||
|
|
@ -47,6 +57,7 @@ struct WorkerConfigSnapshot
|
||||||
bool wantGrenades{ false };
|
bool wantGrenades{ false };
|
||||||
bool wantExfils{ false };
|
bool wantExfils{ false };
|
||||||
bool wantItems{ false };
|
bool wantItems{ false };
|
||||||
|
bool wantContainers{ false };
|
||||||
float remoteMaxDistance{ 0.f };
|
float remoteMaxDistance{ 0.f };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -55,6 +66,7 @@ struct StaticWorldSnapshot
|
||||||
uint64_t generation{ 0 };
|
uint64_t generation{ 0 };
|
||||||
std::vector<CExfil> exfils{};
|
std::vector<CExfil> exfils{};
|
||||||
std::vector<CItem> items{};
|
std::vector<CItem> items{};
|
||||||
|
std::vector<CContainer> containers{};
|
||||||
};
|
};
|
||||||
|
|
||||||
class CFramework
|
class CFramework
|
||||||
|
|
@ -189,7 +201,7 @@ private:
|
||||||
std::vector<CPlayer> GetEntityList() const
|
std::vector<CPlayer> GetEntityList() const
|
||||||
{
|
{
|
||||||
auto snapshot = GetDynamicSnapshot();
|
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
|
std::vector<CExfil> GetExfilList() const
|
||||||
{
|
{
|
||||||
|
|
@ -201,6 +213,11 @@ private:
|
||||||
auto snapshot = GetStaticSnapshot();
|
auto snapshot = GetStaticSnapshot();
|
||||||
return snapshot ? snapshot->items : std::vector<CItem>{};
|
return snapshot ? snapshot->items : std::vector<CItem>{};
|
||||||
}
|
}
|
||||||
|
std::vector<CContainer> GetContainerList() const
|
||||||
|
{
|
||||||
|
auto snapshot = GetStaticSnapshot();
|
||||||
|
return snapshot ? snapshot->containers : std::vector<CContainer>{};
|
||||||
|
}
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
ImColor Col_ESP_PMC = { 1.f, 0.f, 0.85f, 1.f };
|
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_ExfilOpen = { 0.f, 1.f, 0.f, 0.5f };
|
||||||
ImColor Col_ESP_ExfilClose = { 1.f, 0.3f, 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_Corpse = { 1.f, 1.f, 1.f, 0.9f };
|
||||||
ImColor Col_ESP_RareItem = { 1.f, 0.f, 0.f, 0.9f };
|
ImColor Col_ESP_RareItem = { 1.f, 0.f, 0.f, 0.9f };
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,10 @@ void CFramework::RenderMenu()
|
||||||
// Setup
|
// Setup
|
||||||
static int Index = 0;
|
static int Index = 0;
|
||||||
const auto runtimeSnapshot = GetDynamicSnapshot();
|
const auto runtimeSnapshot = GetDynamicSnapshot();
|
||||||
const bool runtimeInRaid = runtimeSnapshot && runtimeSnapshot->inRaid;
|
const bool runtimeFresh = runtimeSnapshot && IsSnapshotFresh(runtimeSnapshot->capturedAt);
|
||||||
const uintptr_t runtimeLocalAddress = runtimeSnapshot ? runtimeSnapshot->local.m_address : 0;
|
const bool runtimeInRaid = runtimeFresh && runtimeSnapshot->inRaid;
|
||||||
const std::size_t runtimeEntityCount = runtimeSnapshot ? runtimeSnapshot->players.size() : 0;
|
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::SetNextWindowBgAlpha(0.975f);
|
||||||
ImGui::SetNextWindowSize(ImVec2(725.f, 450.f));
|
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);
|
||||||
ImGui::Checkbox("物资ESP", &g.g_ESP_Item);
|
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_Exfil);
|
||||||
ImGui::Checkbox("手雷ESP", &g.g_ESP_Grenade);
|
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_Distance);
|
||||||
ImGui::Checkbox("名称", &g.g_ESP_Name);
|
ImGui::Checkbox("名称", &g.g_ESP_Name);
|
||||||
ImGui::Checkbox("血条", &g.g_ESP_HealthBar);
|
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();
|
ImGui::EndChild();
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,68 @@
|
||||||
#include "CExfil.h"
|
#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)
|
bool CExfil::GetAddress(uintptr_t& address)
|
||||||
{
|
{
|
||||||
|
|
@ -7,10 +70,11 @@ bool CExfil::GetAddress(uintptr_t& address)
|
||||||
return Memory::IsUserAddress(ptr);
|
return Memory::IsUserAddress(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
int CExfil::GetStatus()
|
uint8_t CExfil::GetStatus()
|
||||||
{
|
{
|
||||||
int status = CLOSE;
|
uint8_t status = 0;
|
||||||
m.TryRead(ptr + offset::Exfil::Status, status);
|
if (!Memory::IsUserAddress(ptr) || !m.TryRead(ptr + offset::Exfil::Status, status))
|
||||||
|
return 0;
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,34 +82,23 @@ std::string CExfil::GetName()
|
||||||
{
|
{
|
||||||
uintptr_t settings = 0;
|
uintptr_t settings = 0;
|
||||||
uintptr_t namePtr = 0;
|
uintptr_t namePtr = 0;
|
||||||
int length = 0;
|
if (!Memory::IsUserAddress(ptr) ||
|
||||||
if (!m.TryRead(ptr + offset::Exfil::Settings, settings) || !Memory::IsUserAddress(settings) ||
|
!m.TryRead(ptr + offset::Exfil::Settings, settings) || !Memory::IsUserAddress(settings) ||
|
||||||
!m.TryRead(settings + offset::Exfil::Name, namePtr) || !Memory::IsUserAddress(namePtr) ||
|
!m.TryRead(settings + offset::Exfil::Name, namePtr) || !Memory::IsUserAddress(namePtr))
|
||||||
!m.TryRead(namePtr + offset::ManagedString::Length, length) || length <= 0)
|
|
||||||
{
|
{
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
length = (std::min)(length, 127);
|
return ReadManagedName(namePtr);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CExfil::Update()
|
bool CExfil::Update()
|
||||||
{
|
{
|
||||||
|
// Status is diagnostic data; a status read failure must not discard a valid position.
|
||||||
m_pExfilStatus = GetStatus();
|
m_pExfilStatus = GetStatus();
|
||||||
if (m_pExfilStatus == CLOSE)
|
m_pVecLocation = {};
|
||||||
|
|
||||||
|
if (!Memory::IsUserAddress(ptr))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const uintptr_t transform = m.ReadChain(ptr,
|
const uintptr_t transform = m.ReadChain(ptr,
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "../GameSDK/GameSDK.h"
|
#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
|
class CExfil
|
||||||
{
|
{
|
||||||
private:
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
uintptr_t ptr;
|
uintptr_t ptr{ 0 };
|
||||||
|
|
||||||
// データ
|
uint8_t m_pExfilStatus{ 0 };
|
||||||
int m_pExfilStatus;
|
Vector3 m_pVecLocation{};
|
||||||
Vector3 m_pVecLocation;
|
std::string m_pExfilName{};
|
||||||
std::string m_pExfilName;
|
|
||||||
|
|
||||||
bool GetAddress(uintptr_t& address);
|
bool GetAddress(uintptr_t& address);
|
||||||
int GetStatus();
|
uint8_t GetStatus();
|
||||||
std::string GetName();
|
std::string GetName();
|
||||||
bool Update();
|
bool Update();
|
||||||
};
|
};
|
||||||
|
|
@ -1,23 +1,136 @@
|
||||||
#include "CItem.h"
|
#include "CItem.h"
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
struct C_EFT_ItemData
|
namespace
|
||||||
{
|
{
|
||||||
int item_price;
|
constexpr int kMaxManagedStringLength = 128;
|
||||||
std::string item_name;
|
|
||||||
std::string item_id;
|
|
||||||
};
|
|
||||||
|
|
||||||
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" },
|
out = {};
|
||||||
{ 20000000, "Labs keycard (Red)", "5c1d0efb86f7744baf2e7b7b" },
|
if (!Memory::IsUserAddress(itemAddress))
|
||||||
{ 20000000, "Labs keycard (Blue)", "1d0c5f86f7744bb2683cf0" },
|
return false;
|
||||||
{ 20000000, "Labs keycard (Green)", "1d0dc586f7744baf2e7b79" },
|
|
||||||
{ 20000000, "Labs keycard (Violet)", "1e495a86f7743109743dfb" },
|
uintptr_t itemIdString = 0;
|
||||||
{ 20000000, "Labs keycard (Black)", "1d0f4986f7744bb01837fa" },
|
if (m.TryRead(itemAddress + offset::InventoryItem::Id, itemIdString) &&
|
||||||
{ 20000000, "Labs keycard (Yellow)", "1d0d6d86f7744bb2683e1f" },
|
Memory::IsUserAddress(itemIdString))
|
||||||
{ 65000, "0.2 BTC", "faff1d86f7746c51718c9c" }
|
{
|
||||||
};
|
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)
|
bool CItem::GetAddress(uintptr_t& ptr)
|
||||||
{
|
{
|
||||||
|
|
@ -28,8 +141,17 @@ bool CItem::GetAddress(uintptr_t& ptr)
|
||||||
bool CItem::Update()
|
bool CItem::Update()
|
||||||
{
|
{
|
||||||
m_bIsCorpse = false;
|
m_bIsCorpse = false;
|
||||||
|
m_bMetadataValid = false;
|
||||||
|
m_bIsQuestItem = false;
|
||||||
|
m_iRarity = 0;
|
||||||
|
m_iMarketPrice = 0;
|
||||||
|
m_CName.clear();
|
||||||
|
m_CItemId.clear();
|
||||||
m_vecOrigin = {};
|
m_vecOrigin = {};
|
||||||
|
|
||||||
|
if (!Memory::IsUserAddress(address))
|
||||||
|
return false;
|
||||||
|
|
||||||
const uintptr_t transform = m.ReadChain(address,
|
const uintptr_t transform = m.ReadChain(address,
|
||||||
{ offset::UnityComponent::Step0, offset::UnityComponent::Step1, offset::UnityComponent::Step2,
|
{ offset::UnityComponent::Step0, offset::UnityComponent::Step1, offset::UnityComponent::Step2,
|
||||||
offset::UnityComponent::Step3, offset::UnityComponent::Step4, offset::UnityComponent::Transform });
|
offset::UnityComponent::Step3, offset::UnityComponent::Step4, offset::UnityComponent::Transform });
|
||||||
|
|
@ -37,7 +159,40 @@ bool CItem::Update()
|
||||||
if (Vec3_Empty(m_vecOrigin))
|
if (Vec3_Empty(m_vecOrigin))
|
||||||
return false;
|
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 uintptr_t nameAddr = m.ReadChain(address, { 0x0, 0x0, 0x48 });
|
||||||
const std::string className = m.ReadString(nameAddr, 64);
|
const std::string className = m.ReadString(nameAddr, 64);
|
||||||
if (className == "ObservedCorpse" || className == "Corpse")
|
if (className == "ObservedCorpse" || className == "Corpse")
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "../GameSDK/GameSDK.h"
|
#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
|
class CItem
|
||||||
{
|
{
|
||||||
private:
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
uintptr_t address;
|
uintptr_t address{ 0 };
|
||||||
|
|
||||||
bool m_bIsCorpse{ false };
|
bool m_bIsCorpse{ false };
|
||||||
|
bool m_bMetadataValid{ false };
|
||||||
|
bool m_bIsQuestItem{ false };
|
||||||
|
int m_iRarity{ 0 };
|
||||||
int m_iMarketPrice{ 0 };
|
int m_iMarketPrice{ 0 };
|
||||||
std::string m_CName{};
|
std::string m_CName{};
|
||||||
|
std::string m_CItemId{};
|
||||||
Vector3 m_vecOrigin{};
|
Vector3 m_vecOrigin{};
|
||||||
|
|
||||||
bool GetAddress(uintptr_t& ptr);
|
bool GetAddress(uintptr_t& ptr);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,36 @@
|
||||||
#include "GameSDK.h"
|
#include "GameSDK.h"
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#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)
|
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);
|
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;
|
return 0;
|
||||||
|
|
||||||
|
BaseObject lastObject{};
|
||||||
|
const bool hasLastObject = Memory::IsUserAddress(lastObjectPtr) && memory.TryRead(lastObjectPtr, lastObject);
|
||||||
|
uintptr_t currentNode = listPtr;
|
||||||
|
char objectNameBuffer[128]{};
|
||||||
|
std::unordered_set<uintptr_t> visitedNodes;
|
||||||
|
visitedNodes.reserve(64);
|
||||||
|
|
||||||
|
for (int safe = 0; safe < 4096 && Memory::IsUserAddress(currentNode); ++safe)
|
||||||
|
{
|
||||||
|
if (!visitedNodes.insert(currentNode).second)
|
||||||
|
break;
|
||||||
|
|
||||||
BaseObject activeObject{};
|
BaseObject activeObject{};
|
||||||
BaseObject lastObject{};
|
if (!memory.TryRead(currentNode, activeObject))
|
||||||
if (!m.TryRead(listPtr, activeObject) || !m.TryRead(lastObjectPtr, lastObject))
|
break;
|
||||||
return 0;
|
|
||||||
|
|
||||||
char objectNameBuffer[128]{};
|
if (Memory::IsUserAddress(activeObject.object))
|
||||||
int safe = 0;
|
|
||||||
while (activeObject.object != 0 && activeObject.object != lastObject.object && safe++ < 4096)
|
|
||||||
{
|
{
|
||||||
uintptr_t classNamePtr = 0;
|
uintptr_t classNamePtr = 0;
|
||||||
if (m.TryRead(activeObject.object + offset::UnityCamera::ObjectName, classNamePtr) &&
|
std::memset(objectNameBuffer, 0, sizeof(objectNameBuffer));
|
||||||
|
if (memory.TryRead(activeObject.object + offset::UnityCamera::ObjectName, classNamePtr) &&
|
||||||
Memory::IsUserAddress(classNamePtr) &&
|
Memory::IsUserAddress(classNamePtr) &&
|
||||||
m.ReadString(classNamePtr, objectNameBuffer, sizeof(objectNameBuffer)) &&
|
memory.ReadString(classNamePtr, objectNameBuffer, sizeof(objectNameBuffer)) &&
|
||||||
std::strcmp(objectNameBuffer, objectName) == 0)
|
std::strcmp(objectNameBuffer, objectName) == 0)
|
||||||
{
|
{
|
||||||
return activeObject.object;
|
return activeObject.object;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!Memory::IsUserAddress(activeObject.nextObjectLink) || !m.TryRead(activeObject.nextObjectLink, activeObject))
|
if (hasLastObject && activeObject.object != 0 && activeObject.object == lastObject.object)
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
if (lastObject.object != 0)
|
const uintptr_t nextNode = activeObject.nextObjectLink;
|
||||||
{
|
if (!Memory::IsUserAddress(nextNode))
|
||||||
uintptr_t classNamePtr = 0;
|
break;
|
||||||
if (m.TryRead(lastObject.object + offset::UnityCamera::ObjectName, classNamePtr) &&
|
currentNode = nextNode;
|
||||||
Memory::IsUserAddress(classNamePtr) &&
|
|
||||||
m.ReadString(classNamePtr, objectNameBuffer, sizeof(objectNameBuffer)) &&
|
|
||||||
std::strcmp(objectNameBuffer, objectName) == 0)
|
|
||||||
{
|
|
||||||
return lastObject.object;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
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()
|
bool CGame::InitAddressUnlocked()
|
||||||
{
|
{
|
||||||
|
m_pGameObjectManager = 0;
|
||||||
|
m_activeNodeList = 0;
|
||||||
|
m_lastActiveNode = 0;
|
||||||
|
|
||||||
|
const uintptr_t baseAddress = m_memory.GetBaseAddress();
|
||||||
uintptr_t pGom = 0;
|
uintptr_t pGom = 0;
|
||||||
if (!m.TryRead(m.m_gBaseAddress + offset::dwGameObjectManager, pGom) || !Memory::IsUserAddress(pGom) ||
|
if (!Memory::IsUserAddress(baseAddress) ||
|
||||||
!m.TryRead(pGom, m_objGameObjectManager))
|
!m_memory.TryRead(baseAddress + offset::dwGameObjectManager, pGom) ||
|
||||||
|
!Memory::IsUserAddress(pGom) ||
|
||||||
|
!m_memory.TryRead(pGom, m_objGameObjectManager))
|
||||||
{
|
{
|
||||||
m_objGameObjectManager = {};
|
m_objGameObjectManager = {};
|
||||||
m_initialized = false;
|
m_initialized = false;
|
||||||
|
m_refreshStatus = GameRefreshStatus::GomUnavailable;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_initialized = m_objGameObjectManager.lastActiveObject != 0 && m_objGameObjectManager.ActiveNodes != 0;
|
m_pGameObjectManager = pGom;
|
||||||
return m_initialized;
|
m_initialized = true;
|
||||||
|
m_refreshStatus = GameRefreshStatus::NodeListUnavailable;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CGame::UpdateWorldUnlocked()
|
bool CGame::UpdateWorldUnlocked()
|
||||||
{
|
{
|
||||||
|
m_activeNodeList = 0;
|
||||||
|
m_lastActiveNode = 0;
|
||||||
m_pGameWorld = 0;
|
m_pGameWorld = 0;
|
||||||
m_pLocalGameWorld = 0;
|
m_pLocalGameWorld = 0;
|
||||||
|
|
||||||
if (!m_initialized && !InitAddressUnlocked())
|
if (!m_initialized && !InitAddressUnlocked())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
uintptr_t activeNodes = 0;
|
struct NodePair
|
||||||
uintptr_t lastActiveNode = 0;
|
|
||||||
if (!m.TryRead(m_objGameObjectManager.ActiveNodes, activeNodes) ||
|
|
||||||
!m.TryRead(m_objGameObjectManager.LastActiveNode, lastActiveNode))
|
|
||||||
{
|
{
|
||||||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_pGameWorld = GetObjectFromList(activeNodes, lastActiveNode, "GameWorld");
|
m_pLocalGameWorld = m_memory.ReadChain(m_pGameWorld,
|
||||||
if (!Memory::IsUserAddress(m_pGameWorld))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
m_pLocalGameWorld = m.ReadChain(m_pGameWorld,
|
|
||||||
{ offset::UnityGameWorld::Component0, offset::UnityGameWorld::Component1, offset::UnityGameWorld::LocalGameWorld });
|
{ 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()
|
bool CGame::DiscoverCameraUnlocked()
|
||||||
|
|
@ -206,12 +326,17 @@ bool CGame::DiscoverCameraUnlocked()
|
||||||
m_pFpsCamera = 0;
|
m_pFpsCamera = 0;
|
||||||
m_cameraValid = false;
|
m_cameraValid = false;
|
||||||
|
|
||||||
|
const uintptr_t baseAddress = m_memory.GetBaseAddress();
|
||||||
uintptr_t allCameraPtr = 0;
|
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;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Camera allCameras{};
|
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;
|
return false;
|
||||||
|
|
||||||
for (uint64_t i = 0; i < allCameras.count; ++i)
|
for (uint64_t i = 0; i < allCameras.count; ++i)
|
||||||
|
|
@ -219,15 +344,15 @@ bool CGame::DiscoverCameraUnlocked()
|
||||||
uintptr_t camera = 0;
|
uintptr_t camera = 0;
|
||||||
uintptr_t cameraObject = 0;
|
uintptr_t cameraObject = 0;
|
||||||
uintptr_t cameraNamePtr = 0;
|
uintptr_t cameraNamePtr = 0;
|
||||||
if (!m.TryRead(allCameras.list + static_cast<uintptr_t>(i * sizeof(uintptr_t)), camera) || !Memory::IsUserAddress(camera) ||
|
if (!m_memory.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_memory.TryRead(camera + offset::UnityCamera::NativeObject, cameraObject) || !Memory::IsUserAddress(cameraObject) ||
|
||||||
!m.TryRead(cameraObject + offset::UnityCamera::ObjectName, cameraNamePtr) || !Memory::IsUserAddress(cameraNamePtr))
|
!m_memory.TryRead(cameraObject + offset::UnityCamera::ObjectName, cameraNamePtr) || !Memory::IsUserAddress(cameraNamePtr))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
char name[64]{};
|
char name[64]{};
|
||||||
if (!m.ReadString(cameraNamePtr, name, sizeof(name)))
|
if (!m_memory.ReadString(cameraNamePtr, name, sizeof(name)))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (std::strcmp(name, "FPS Camera") == 0)
|
if (std::strcmp(name, "FPS Camera") == 0)
|
||||||
|
|
@ -250,9 +375,9 @@ bool CGame::UpdateViewMatrixUnlocked()
|
||||||
uintptr_t viewObject = 0;
|
uintptr_t viewObject = 0;
|
||||||
uintptr_t matrixHolder = 0;
|
uintptr_t matrixHolder = 0;
|
||||||
Matrix matrix{};
|
Matrix matrix{};
|
||||||
if (!m.TryRead(m_pFpsCamera + offset::UnityCamera::ViewObject, viewObject) || !Memory::IsUserAddress(viewObject) ||
|
if (!m_memory.TryRead(m_pFpsCamera + offset::UnityCamera::ViewObject, viewObject) || !Memory::IsUserAddress(viewObject) ||
|
||||||
!m.TryRead(viewObject + offset::UnityCamera::ViewMatrixHolder, matrixHolder) || !Memory::IsUserAddress(matrixHolder) ||
|
!m_memory.TryRead(viewObject + offset::UnityCamera::ViewMatrixHolder, matrixHolder) || !Memory::IsUserAddress(matrixHolder) ||
|
||||||
!m.TryRead(matrixHolder + offset::UnityCamera::ViewMatrix, matrix))
|
!m_memory.TryRead(matrixHolder + offset::UnityCamera::ViewMatrix, matrix))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -285,9 +410,13 @@ bool CGame::UpdateCamera()
|
||||||
bool CGame::Refresh()
|
bool CGame::Refresh()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_stateMutex);
|
std::lock_guard<std::mutex> lock(m_stateMutex);
|
||||||
|
++m_refreshAttempts;
|
||||||
|
|
||||||
if (!m_initialized && !InitAddressUnlocked())
|
if (!m_initialized && !InitAddressUnlocked())
|
||||||
|
{
|
||||||
|
++m_consecutiveRefreshFailures;
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!UpdateWorldUnlocked())
|
if (!UpdateWorldUnlocked())
|
||||||
{
|
{
|
||||||
|
|
@ -296,10 +425,13 @@ bool CGame::Refresh()
|
||||||
{
|
{
|
||||||
m_pGameWorld = 0;
|
m_pGameWorld = 0;
|
||||||
m_pLocalGameWorld = 0;
|
m_pLocalGameWorld = 0;
|
||||||
|
++m_consecutiveRefreshFailures;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_consecutiveRefreshFailures = 0;
|
||||||
|
|
||||||
if (!UpdateViewMatrixUnlocked())
|
if (!UpdateViewMatrixUnlocked())
|
||||||
{
|
{
|
||||||
if (!DiscoverCameraUnlocked() || !UpdateViewMatrixUnlocked())
|
if (!DiscoverCameraUnlocked() || !UpdateViewMatrixUnlocked())
|
||||||
|
|
@ -325,9 +457,16 @@ GameStateSnapshot CGame::GetState() const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_stateMutex);
|
std::lock_guard<std::mutex> lock(m_stateMutex);
|
||||||
GameStateSnapshot state{};
|
GameStateSnapshot state{};
|
||||||
|
state.refreshStatus = m_refreshStatus;
|
||||||
|
state.refreshAttempts = m_refreshAttempts;
|
||||||
|
state.consecutiveRefreshFailures = m_consecutiveRefreshFailures;
|
||||||
state.initialized = m_initialized;
|
state.initialized = m_initialized;
|
||||||
state.inRaid = Memory::IsUserAddress(m_pGameWorld) && Memory::IsUserAddress(m_pLocalGameWorld);
|
state.inRaid = Memory::IsUserAddress(m_pGameWorld) && Memory::IsUserAddress(m_pLocalGameWorld);
|
||||||
state.cameraValid = m_cameraValid;
|
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.gameWorld = m_pGameWorld;
|
||||||
state.localGameWorld = m_pLocalGameWorld;
|
state.localGameWorld = m_pLocalGameWorld;
|
||||||
state.fpsCamera = m_pFpsCamera;
|
state.fpsCamera = m_pFpsCamera;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,69 @@
|
||||||
#include "Struct.h"
|
#include "Struct.h"
|
||||||
#include "../../../Framework/Memory/Memory.h"
|
#include "../../../Framework/Memory/Memory.h"
|
||||||
#include "../../../Framework/Config/Config.h"
|
#include "../../../Framework/Config/Config.h"
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
#include <mutex>
|
#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
|
struct GameStateSnapshot
|
||||||
{
|
{
|
||||||
|
GameRefreshStatus refreshStatus{ GameRefreshStatus::Waiting };
|
||||||
|
uint64_t refreshAttempts{ 0 };
|
||||||
|
uint64_t consecutiveRefreshFailures{ 0 };
|
||||||
bool initialized{ false };
|
bool initialized{ false };
|
||||||
bool inRaid{ false };
|
bool inRaid{ false };
|
||||||
bool cameraValid{ 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 gameWorld{ 0 };
|
||||||
uintptr_t localGameWorld{ 0 };
|
uintptr_t localGameWorld{ 0 };
|
||||||
uintptr_t fpsCamera{ 0 };
|
uintptr_t fpsCamera{ 0 };
|
||||||
|
|
@ -19,13 +75,20 @@ struct GameStateSnapshot
|
||||||
class CGame
|
class CGame
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
IGameMemoryReader& m_memory;
|
||||||
mutable std::mutex m_stateMutex;
|
mutable std::mutex m_stateMutex;
|
||||||
GameObjectManager m_objGameObjectManager{};
|
GameObjectManager m_objGameObjectManager{};
|
||||||
Camera m_pAllCamera{};
|
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_pGameWorld{ 0 };
|
||||||
uintptr_t m_pLocalGameWorld{ 0 };
|
uintptr_t m_pLocalGameWorld{ 0 };
|
||||||
uintptr_t m_pFpsCamera{ 0 };
|
uintptr_t m_pFpsCamera{ 0 };
|
||||||
Matrix m_mxViewMatrix{};
|
Matrix m_mxViewMatrix{};
|
||||||
|
GameRefreshStatus m_refreshStatus{ GameRefreshStatus::Waiting };
|
||||||
|
uint64_t m_refreshAttempts{ 0 };
|
||||||
|
uint64_t m_consecutiveRefreshFailures{ 0 };
|
||||||
bool m_initialized{ false };
|
bool m_initialized{ false };
|
||||||
bool m_cameraValid{ false };
|
bool m_cameraValid{ false };
|
||||||
|
|
||||||
|
|
@ -35,6 +98,9 @@ private:
|
||||||
bool UpdateViewMatrixUnlocked();
|
bool UpdateViewMatrixUnlocked();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
CGame();
|
||||||
|
explicit CGame(IGameMemoryReader& memory) noexcept;
|
||||||
|
|
||||||
bool InitAddress();
|
bool InitAddress();
|
||||||
bool Update();
|
bool Update();
|
||||||
bool UpdateCamera();
|
bool UpdateCamera();
|
||||||
|
|
@ -53,4 +119,5 @@ extern bool Vec3_Empty(const Vector3& value);
|
||||||
extern float GetDistance(const Vector3& value1, const Vector3& value2);
|
extern float GetDistance(const Vector3& value1, const Vector3& value2);
|
||||||
extern Vector3 GetTransformPosition(uintptr_t transform);
|
extern Vector3 GetTransformPosition(uintptr_t transform);
|
||||||
extern bool WorldToScreen(const Matrix& ViewMatrix, const Vector2& GameSize, const Vector3& vIn, Vector2& vOut);
|
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);
|
extern uintptr_t GetObjectFromList(uintptr_t listPtr, uintptr_t lastObjectPtr, const char* objectName);
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,41 @@ namespace offset
|
||||||
constexpr auto Status = 0xC8;
|
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
|
// EFT.GameWorld / EFT.ClientLocalGameWorld
|
||||||
namespace GameWorld
|
namespace GameWorld
|
||||||
{
|
{
|
||||||
constexpr auto ExfilController = 0x30;
|
constexpr auto ExfilController = 0x30;
|
||||||
constexpr auto LootList = 0x118;
|
constexpr auto LootList = 0x118;
|
||||||
|
constexpr auto AllLoot = 0x138;
|
||||||
constexpr auto RegisteredPlayers = 0x140;
|
constexpr auto RegisteredPlayers = 0x140;
|
||||||
constexpr auto AllAlivePlayersList = 0x168;
|
constexpr auto AllAlivePlayersList = 0x168;
|
||||||
constexpr auto ObservedPlayersById = 0x188;
|
constexpr auto ObservedPlayersById = 0x188;
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ namespace
|
||||||
constexpr int kMaxItems = 8192;
|
constexpr int kMaxItems = 8192;
|
||||||
constexpr int kMaxGrenades = 256;
|
constexpr int kMaxGrenades = 256;
|
||||||
constexpr std::size_t kMaxExfils = 32;
|
constexpr std::size_t kMaxExfils = 32;
|
||||||
|
constexpr std::size_t kMaxContainers = 4096;
|
||||||
|
|
||||||
bool ReadManagedPointerList(uintptr_t listObject, std::vector<uintptr_t>& out, int hardLimit)
|
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.wantGrenades = g.g_ESP && g.g_ESP_Grenade;
|
||||||
next.wantExfils = g.g_ESP && g.g_ESP_Exfil;
|
next.wantExfils = g.g_ESP && g.g_ESP_Exfil;
|
||||||
next.wantItems = g.g_ESP && g.g_ESP_Item;
|
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)
|
if (g.g_ESP && g.g_Aim)
|
||||||
next.remoteMaxDistance = (std::max)(g.g_ESP_MaxDistance, g.g_AimMaxDist);
|
next.remoteMaxDistance = (std::max)(g.g_ESP_MaxDistance, g.g_AimMaxDist);
|
||||||
|
|
@ -54,22 +56,29 @@ void CFramework::UpdateWorkerConfigFromUi()
|
||||||
|
|
||||||
void CFramework::UpdateList()
|
void CFramework::UpdateList()
|
||||||
{
|
{
|
||||||
// CGame is owned/refreshed by this thread only. Consumers receive immutable snapshots.
|
// Always publish a snapshot, even before GOM/GameWorld becomes available. Otherwise a failed
|
||||||
while (g.process_active && !tarkov->Refresh())
|
// first refresh leaves consumers stuck forever on the default generation-0 snapshot.
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
|
||||||
|
|
||||||
while (g.process_active)
|
while (g.process_active)
|
||||||
{
|
{
|
||||||
const auto tickStart = std::chrono::steady_clock::now();
|
const auto tickStart = std::chrono::steady_clock::now();
|
||||||
const WorkerConfigSnapshot config = GetWorkerConfig();
|
const WorkerConfigSnapshot config = GetWorkerConfig();
|
||||||
auto snapshot = std::make_shared<DynamicWorldSnapshot>();
|
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();
|
const GameStateSnapshot game = tarkov->GetState();
|
||||||
|
snapshot->refreshStatus = game.refreshStatus;
|
||||||
|
snapshot->refreshAttempts = game.refreshAttempts;
|
||||||
|
snapshot->consecutiveRefreshFailures = game.consecutiveRefreshFailures;
|
||||||
snapshot->initialized = game.initialized;
|
snapshot->initialized = game.initialized;
|
||||||
snapshot->inRaid = refreshed && game.inRaid;
|
snapshot->inRaid = game.inRaid;
|
||||||
snapshot->cameraValid = game.cameraValid;
|
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->gameWorld = game.gameWorld;
|
||||||
snapshot->localGameWorld = game.localGameWorld;
|
snapshot->localGameWorld = game.localGameWorld;
|
||||||
snapshot->fpsCamera = game.fpsCamera;
|
snapshot->fpsCamera = game.fpsCamera;
|
||||||
|
|
@ -174,7 +183,8 @@ void CFramework::UpdateStaticList()
|
||||||
staticSnapshot->generation = ++m_staticGeneration;
|
staticSnapshot->generation = ++m_staticGeneration;
|
||||||
|
|
||||||
const auto dynamicSnapshot = GetDynamicSnapshot();
|
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);
|
PublishStaticSnapshot(staticSnapshot);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -202,6 +212,8 @@ void CFramework::UpdateStaticList()
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
exfil.m_pExfilName = exfil.GetName();
|
exfil.m_pExfilName = exfil.GetName();
|
||||||
|
if (exfil.m_pExfilName.empty())
|
||||||
|
exfil.m_pExfilName = "Exfil";
|
||||||
staticSnapshot->exfils.push_back(std::move(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);
|
PublishStaticSnapshot(staticSnapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,62 @@ bool InScreen(const Vector2& vIn)
|
||||||
vIn.y < g.GameRect.top || vIn.y > g.GameRect.bottom);
|
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()
|
void CFramework::RenderInfo()
|
||||||
{
|
{
|
||||||
const auto snapshot = GetDynamicSnapshot();
|
const auto snapshot = GetDynamicSnapshot();
|
||||||
|
const bool snapshotFresh = snapshot && IsSnapshotFresh(snapshot->capturedAt);
|
||||||
|
|
||||||
std::string infoText{ "Tarkov PvE | " + std::to_string((int)ImGui::GetIO().Framerate) + "FPS" };
|
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());
|
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 bool inRaid = snapshot && snapshot->inRaid;
|
||||||
const uintptr_t localAddress = snapshot ? snapshot->local.m_address : 0;
|
const uintptr_t localAddress = snapshot ? snapshot->local.m_address : 0;
|
||||||
const std::size_t entityCount = snapshot ? snapshot->players.size() : 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),
|
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",
|
initialized ? "OK" : "FAIL",
|
||||||
inRaid ? "Y" : "N",
|
inRaid ? "Y" : "N",
|
||||||
(snapshot && snapshot->cameraValid) ? "OK" : "FAIL",
|
(snapshot && snapshot->cameraValid) ? "OK" : "FAIL",
|
||||||
(unsigned long long)localAddress,
|
snapshotFresh ? "Y" : "N",
|
||||||
entityCount,
|
ageText,
|
||||||
|
(unsigned long long)(snapshot ? snapshot->consecutiveRefreshFailures : 0),
|
||||||
|
(unsigned long long)(snapshot ? snapshot->refreshAttempts : 0),
|
||||||
(unsigned long long)(snapshot ? snapshot->generation : 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);
|
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]{};
|
char aim[192]{};
|
||||||
snprintf(aim, sizeof(aim),
|
snprintf(aim, sizeof(aim),
|
||||||
"Aim:%s M:%s F:%s Tgt:%s d=%.0fpx",
|
"Aim:%s M:%s F:%s Tgt:%s d=%.0fpx",
|
||||||
|
|
@ -39,9 +114,9 @@ void CFramework::RenderInfo()
|
||||||
g.g_AimForce ? "FORCE" : "KEY",
|
g.g_AimForce ? "FORCE" : "KEY",
|
||||||
m_diagBestAddr ? "LOCK" : "NONE",
|
m_diagBestAddr ? "LOCK" : "NONE",
|
||||||
m_diagBestDist);
|
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)
|
switch (g.g_CrosshairType)
|
||||||
{
|
{
|
||||||
|
|
@ -64,7 +139,8 @@ void CFramework::RenderInfo()
|
||||||
void CFramework::RenderESP()
|
void CFramework::RenderESP()
|
||||||
{
|
{
|
||||||
const auto snapshot = GetDynamicSnapshot();
|
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;
|
return;
|
||||||
|
|
||||||
const CPlayer& localPlayer = snapshot->local;
|
const CPlayer& localPlayer = snapshot->local;
|
||||||
|
|
@ -224,7 +300,7 @@ void CFramework::RenderESP()
|
||||||
{
|
{
|
||||||
for (const auto& item : staticSnapshot->items)
|
for (const auto& item : staticSnapshot->items)
|
||||||
{
|
{
|
||||||
if (!item.m_bIsCorpse && item.m_CName.empty())
|
if (item.m_CName.empty())
|
||||||
continue;
|
continue;
|
||||||
if (!g.g_ESP_Corpse && item.m_bIsCorpse)
|
if (!g.g_ESP_Corpse && item.m_bIsCorpse)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -238,13 +314,32 @@ void CFramework::RenderESP()
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
std::string text = item.m_bIsCorpse ? "Corpse" : item.m_CName;
|
std::string text = item.m_bIsCorpse ? "Corpse" : item.m_CName;
|
||||||
text += "[" + std::to_string((int)distance) + "m]";
|
text += " [" + std::to_string((int)distance) + "m]";
|
||||||
const ImColor itemColor = item.m_bIsCorpse ? Col_ESP_Corpse : Col_ESP_RareItem;
|
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);
|
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());
|
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)
|
if (g.g_ESP_Exfil)
|
||||||
{
|
{
|
||||||
for (const auto& exfil : staticSnapshot->exfils)
|
for (const auto& exfil : staticSnapshot->exfils)
|
||||||
|
|
@ -253,8 +348,12 @@ void CFramework::RenderESP()
|
||||||
if (!WorldToScreen(viewMatrix, gameSize, exfil.m_pVecLocation, screen) || !InScreen(screen))
|
if (!WorldToScreen(viewMatrix, gameSize, exfil.m_pVecLocation, screen) || !InScreen(screen))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const ImColor exfilColor = exfil.m_pExfilStatus == NOTREADY ? Col_ESP_ExfilClose : Col_ESP_ExfilOpen;
|
const ExfilDisplayCategory category = ClassifyExfilStatus(exfil.m_pExfilStatus);
|
||||||
const std::string text = exfil.m_pExfilName + " [" + std::to_string((int)GetDistance(localPlayer.m_vecOrigin, exfil.m_pVecLocation)) + "m]";
|
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);
|
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());
|
String(ImVec2(screen.x - (ImGui::CalcTextSize(text.c_str()).x / 2.f), screen.y), exfilColor, text.c_str());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ struct Globals
|
||||||
bool g_ESP = true;
|
bool g_ESP = true;
|
||||||
bool g_ESP_Item = true;
|
bool g_ESP_Item = true;
|
||||||
bool g_ESP_Corpse = true;
|
bool g_ESP_Corpse = true;
|
||||||
|
bool g_ESP_Container = true;
|
||||||
bool g_ESP_Exfil = true;
|
bool g_ESP_Exfil = true;
|
||||||
bool g_ESP_Grenade = true;
|
bool g_ESP_Grenade = true;
|
||||||
bool g_ESP_Line = false;
|
bool g_ESP_Line = false;
|
||||||
|
|
@ -29,6 +30,7 @@ struct Globals
|
||||||
int g_ESP_BoxStyle = 1;
|
int g_ESP_BoxStyle = 1;
|
||||||
float g_ESP_MaxDistance = 500.f;
|
float g_ESP_MaxDistance = 500.f;
|
||||||
float g_ESP_MaxItemDistance = 50.f;
|
float g_ESP_MaxItemDistance = 50.f;
|
||||||
|
float g_ESP_MaxContainerDistance = 100.f;
|
||||||
|
|
||||||
// Misc (仅使用 dump 已确认的对象字段)
|
// Misc (仅使用 dump 已确认的对象字段)
|
||||||
bool g_InfStamina = false;
|
bool g_InfStamina = false;
|
||||||
|
|
|
||||||
29
README.md
29
README.md
|
|
@ -64,9 +64,21 @@
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
- 环境:**Visual Studio 2022**(MSVC v143),x64。
|
- 环境:**Visual Studio 2022**(MSVC v143),x64。
|
||||||
- 所有第三方库(ImGui、DirectXTK SimpleMath、FreeType、nlohmann/json 等)已包含在仓库内,**无需额外安装**,直接打开 `CFramework.sln` 选择 **Release x64** 构建即可。
|
- 所有第三方库(ImGui、DirectXTK SimpleMath、FreeType、nlohmann/json 等)已包含在仓库内,无需额外安装。
|
||||||
- 首次构建参考:[Visual Studio C++ 安装指南](https://learn.microsoft.com/ja-jp/cpp/build/vscpp-step-0-installation?view=msvc-170)
|
- 日常开发不要只执行裸构建;先运行统一回归门禁:
|
||||||
输出:`x64\Release\CFramework.exe`
|
|
||||||
|
```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`。
|
运行前提:先启动 SPT-AKI 3.11.3 客户端(进程名为 `EscapeFromTarkov`),再以管理员身份运行 `CFramework.exe`。
|
||||||
程序通过窗口标题附加进程并创建覆盖层;若附加失败返回码 `1`,Overlay 初始化失败返回码 `2`。
|
程序通过窗口标题附加进程并创建覆盖层;若附加失败返回码 `1`,Overlay 初始化失败返回码 `2`。
|
||||||
|
|
@ -78,6 +90,8 @@
|
||||||
- ESP 与 Aim 目标选择只消费 Snapshot,不在渲染阶段直接 RPM。真正的 Memory Aim / 功能 override 仍通过独立写入路径执行。
|
- ESP 与 Aim 目标选择只消费 Snapshot,不在渲染阶段直接 RPM。真正的 Memory Aim / 功能 override 仍通过独立写入路径执行。
|
||||||
- `RegisteredPlayers` 按 `IPlayer` 处理,并区分完整 `EFT.Player` 与 `ObservedPlayerView` 两种布局。ObservedPlayer 无法从当前 dump 可靠确认的 Profile/Health/Role 字段不会强行套用。
|
- `RegisteredPlayers` 按 `IPlayer` 处理,并区分完整 `EFT.Player` 与 `ObservedPlayerView` 两种布局。ObservedPlayer 无法从当前 dump 可靠确认的 Profile/Health/Role 字段不会强行套用。
|
||||||
- 后台线程不直接读取 ImGui 绑定的普通 `g_*` 变量;渲染线程通过 `WorkerConfigSnapshot` 发布所需配置。
|
- 后台线程不直接读取 ImGui 绑定的普通 `g_*` 变量;渲染线程通过 `WorkerConfigSnapshot` 发布所需配置。
|
||||||
|
- 动态 Snapshot 带采集时间、刷新次数和连续失败数;超过 250ms 的 stale 数据不会用于 ESP、Aim 或功能写入。
|
||||||
|
- `CGame` 的读取边界支持注入 synthetic memory reader;`CFramework.SelfTest` 可在不启动游戏的情况下复现 GOM/节点链和失败恢复路径。
|
||||||
- 玩家、物资、手雷列表使用运行时长度 + 上限校验的批量读取,不再使用固定 `address[64]` / `address[2048]` 缓冲。
|
- 玩家、物资、手雷列表使用运行时长度 + 上限校验的批量读取,不再使用固定 `address[64]` / `address[2048]` 缓冲。
|
||||||
- 工作线程可 join;退出时先停止并等待后台线程,再恢复功能写入和关闭进程句柄。
|
- 工作线程可 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 中仍是混淆字段;本次实现采取保守降级显示,不猜测未知字段语义。
|
- `ObservedPlayerView` 的昵称、Role、实时 Health 等字段在当前 dump 中仍是混淆字段;本次实现采取保守降级显示,不猜测未知字段语义。
|
||||||
- 主 Overlay 仍跟随 VSync/显示器刷新率;`UpdateFeatures()` 是渲染线程写功能路径,后续如果需要进一步降低 CPU/RPM,可以单独给写功能增加节流/脏标记。
|
- 主 Overlay 仍跟随 VSync/显示器刷新率;`UpdateFeatures()` 是渲染线程写功能路径,后续如果需要进一步降低 CPU/RPM,可以单独给写功能增加节流/脏标记。
|
||||||
|
|
||||||
|
|
@ -93,10 +107,15 @@
|
||||||
|
|
||||||
```
|
```
|
||||||
CFramework/
|
CFramework/
|
||||||
├── CFramework.sln / .vcxproj # VS 解决方案
|
├── CFramework.sln # VS 解决方案(主程序 + SelfTest)
|
||||||
|
├── verify_runtime.ps1 # Runtime 回归门禁:测试通过后才构建 Overlay
|
||||||
|
├── CFramework.SelfTest/ # synthetic memory 离线测试项目
|
||||||
|
│ └── SelfTest.cpp
|
||||||
├── CFramework/
|
├── CFramework/
|
||||||
|
│ ├── CFramework.vcxproj
|
||||||
│ ├── main.cpp # 入口:附加进程、初始化 Overlay、字体、后台线程
|
│ ├── main.cpp # 入口:附加进程、初始化 Overlay、字体、后台线程
|
||||||
│ ├── Cheat/ # 功能实现
|
│ ├── Cheat/ # 功能实现
|
||||||
|
│ │ ├── SnapshotHealth.h # generation、时间戳和 250ms freshness 契约
|
||||||
│ │ ├── Aim.cpp # 自瞄 + 压枪(Mouse / Memory)
|
│ │ ├── Aim.cpp # 自瞄 + 压枪(Mouse / Memory)
|
||||||
│ │ ├── Visual.cpp # ESP 渲染(玩家/物资/撤离点/手雷)
|
│ │ ├── Visual.cpp # ESP 渲染(玩家/物资/撤离点/手雷)
|
||||||
│ │ ├── Features.cpp # 耐力/跌落/摇摆/后座/故障 override
|
│ │ ├── Features.cpp # 耐力/跌落/摇摆/后座/故障 override
|
||||||
|
|
|
||||||
|
|
@ -51,22 +51,30 @@
|
||||||
- `min` 调用改为显式 `std::min`,降低 Windows 宏依赖。
|
- `min` 调用改为显式 `std::min`,降低 Windows 宏依赖。
|
||||||
- README 移除源码中不存在的“子弹追踪(只读)已完成”声明。
|
- 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`
|
```powershell
|
||||||
- `Cheat/Visual.cpp`
|
powershell -ExecutionPolicy Bypass -File .\verify_runtime.ps1
|
||||||
- `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` 警告)
|
|
||||||
|
|
||||||
以上文件均通过语法检查。
|
验证结果:
|
||||||
|
|
||||||
当前环境没有 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 实际效果。
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue