
Overview
ArkDMA is a Windows program in C++20 that reads the memory of ARK: Survival Ascended, an Unreal Engine 5 game, from a second PC. Reads go through an FPGA card plugged over PCIe into the gaming machine, driven by DMALibrary (on top of vmm and leechcore), and the data is shown in a Dear ImGui interface on DirectX 11. It is a cheating tool: using it online breaks the game's rules, and I present it for its engineering. The project is at the reading stage. Entities are classified and listed in tables, but nothing is drawn over the game yet, and the modules that would write into its memory are not wired up: ESP.hpp and Aimbot.hpp are only headers.
Reading another PC without freezing the interface
Every DMA read is a PCIe round trip. Called from the render loop, the entity reading pass dropped the display to a few frames per second. So the render thread keeps only short reads, such as the key state. The data thread, started by ArkSDK::StartDataThread, targets 20 Hz with a minimum rest.
// ~20 Hz visé, avec un repos minimum garanti.
constexpr auto kPeriod = std::chrono::milliseconds(50);
constexpr auto kMinIdle = std::chrono::milliseconds(15);
const auto rest = (elapsed < kPeriod) ? (kPeriod - elapsed)
: std::chrono::steady_clock::duration(kMinIdle);
std::this_thread::sleep_for(rest < kMinIdle ? kMinIdle : rest);Without that floor, a pass that runs too long would immediately start the next one and occupy the link continuously. Lists pass from one thread to the other as shared_ptr<const vector> snapshots owned by the caller, no longer as a reference to a vector the data thread could empty mid-iteration.
One batched read per pass, not per entity
The previous version opened one batched read per entity, then chained up to seven reads one at a time, which came to about 2600 round trips and 538 ms for 561 entities. ReadClassifiedEntities now reads the whole frame with one batched read per pass, through my ScatterBatch class: the base fields, what the previous pass reveals (position, level), the string headers, then their contents. The commit that introduces it measures 39 ms on the same entities.
An actor's class never changes. So ClassifyActors reads it only once per actor and resolves the name only once per class: 177 classes for 15,492 actors on one capture. For Blueprint classes, whose name does not reveal their C++ ancestor, ClassifyByHierarchy walks up the parent classes, one scatter per level for all pending classes. A transient failure is no longer frozen: each class gets 60 attempts, about three seconds.
Reads that prove what they read
A failed DMA read returns zero, just like a legitimate null value. So ArkSDK::Init distinguishes an unreadable module, an address outside the image, a world still loading and a stale offset.
ResolveFName turns the engine's name indices into text, with a stride between entries that varies with the Unreal version: I try 2, 4 and 8 bytes and keep a stride only after sixteen readable names, where a single lucky byte used to be enough.
Hotkeys on the gaming PC first read the keyboard from kernel memory, a path that every major Windows update broke. GameInput.cpp now reads the key state table the game keeps itself. Absent from the dump, it is recognised at runtime by its hash table shape, its stride is measured rather than assumed, and its layout checked before anything is published.
Offsets live in Offsets.hpp, dated and sourced, and the engine globals are first looked up by signature (PatternScan::Resolve), falling back to the recorded value. At the last game update, I compared more than 1060 fields across 19 classes between two dumps: only the module globals had moved.
An interface that reports its own faults
Two invisible ImGui faults cost me a session each: a clickable zone submitted before another one it covers steals its hover, and a zone computed from a zero origin draws in the right place but responds elsewhere. Every zone goes through UI::Hit, which recognises these signatures and the zero size and reports them once in the log.
Visit() lists the configuration fields only once, and two visitors, Writer and Reader, write them as JSON and read them back. A single list of 182 colours in Colors.hpp produces the enumeration, the defaults, the serialisation and the picker's tree, and one line in the ACT_FLAGS catalogue is enough to make a setting bindable to a key. In the Explorers, a column with no data shows a dash rather than an invented value.
Counters at zero and silent bugs
In Advanced Settings, the stride settings are saved without the reading layer consulting them, and some counters in Telemetry.hpp are never written: they show zero rather than an invented number. The team field, already read, is still marked unconfirmed. No automated tests: everything is validated by hand, in game and in the log. Several fixed bugs were silent: a key that never fires, a key name skipped for good. What I take from it: make failure visible from the first version, and write the batched reading from the start rather than retrofitting it.