CFramework/CFramework.SelfTest/SelfTest.cpp

402 lines
14 KiB
C++

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