-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
This page describes how ZeusMod's four components fit together, what runs in which process, and how data flows between them.
┌──────────────────────────────────────────┐
│ Operator's PC │
└──────────────────────────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ ZeusMod.exe │ │ inspect.py │ │ Icarus-Win64- │
│ (Electron) │ │ (Python client) │ │ Shipping.exe │
│ │ │ │ │ │
│ • main.js │ │ • REPL │ │ ┌───────────┐ │
│ • preload.js │ │ • Batch / watch │ │ │IcarusInt- │ │
│ • renderer/ │ │ • JSON mode │ │ │ernal.dll │ │
│ • injector.js │ │ │ │ └─────┬─────┘ │
│ (koffi FFI) │ │ │ │ │ │
└────────┬─────────┘ └──────────┬────────┘ │ │ │
│ │ │ ┌─────▼─────┐ │
│ IPC (IPCMain/Render) │ named pipe │ │ MinHook │ │
│ │ │ │ detours │ │
│ ┌───────────────────────────┼────────────────┼─► │ on UFunc │ │
│ │ named pipe │ │ │ thunks │ │
│ │ \\.\pipe\ZeusModPipe │ │ └─────┬─────┘ │
│ │ \\.\pipe\ZeusModDbg │ │ │ │
│ ▼ ▼ │ ┌─────▼─────┐ │
└──► pipe client (Electron) ◄──── pipe client ──┼───►│ UE4 │ │
│ │reflection │ │
│ │ (FField, │ │
│ │ UFunc…) │ │
│ └───────────┘ │
└──────────────────┘
Two pipes, two client roles:
| Pipe | Server | Clients | Purpose |
|---|---|---|---|
\\.\pipe\ZeusModPipe |
DLL (PipeServer thread) | Electron launcher | Cheat on/off, multiplier values, attach heartbeat |
\\.\pipe\ZeusModDbg |
DLL (DbgServer thread) | scripts/inspect.py |
Reflection + memory access (read, write, scan, …) |
The two pipes share the same wire format but different command vocabularies — see Pipe Protocol.
| Process | Lifetime |
|---|---|
ZeusMod.exe |
From user double-click to user close. Can be relaunched without reinjecting. |
Icarus-Win64-Shipping.exe |
The game. Completely unmodified on disk. |
IcarusInternal.dll |
Loaded into the game via LoadLibraryW. Unloads on F10 or on game exit. |
| Trainer thread (inside the DLL) | Spawned in DllMain(DLL_PROCESS_ATTACH). Runs the main trainer loop until F10. |
| Pipe-server thread | Spawned alongside the trainer thread. Blocks on ConnectNamedPipe; per-client dispatcher. |
| Debug-pipe server thread | Same pattern as the cheat pipe, separate pipe name. |
IcarusInjector.exe |
Optional headless path. Lives only long enough to fire CreateRemoteThread(LoadLibraryW) and report an exit code. |
The Electron app and Icarus are independent processes. Killing one does not affect the other; the DLL keeps running inside Icarus whether or not ZeusMod is open.
- User clicks the checkbox in the Inventory panel.
-
renderer/js/app.js→handleToggleChange("weight", true). -
window.zeusmod.toggleCheat("weight", "1")(preload bridge). -
ipcMain.handle("cheat:toggle")picks it up inmain.js. -
main.jswrites the lineweight:1\nto\\.\pipe\ZeusModPipevia the standard pipe client. - Inside the game,
PipeServer::HandleLine("weight:1")dispatches toTrainer::OnCheatChange("weight", "1"). - The trainer sets
Trainer::Get().NoWeight = trueand, if not already installed, callsInstallWeightHook():UObjectLookup::FindNativeFunction("IcarusFunctionLibrary", "AddModifierState")MinHook → CreateHook(target, &HookAddModifierState, &g_origAddModifierState)MH_EnableHook(target)
- From the next tick onward, whenever the game internally calls
AddModifierState, our detour inspects theinModifierstruct. If the row is"Overburdened"andNoWeightis true, the detour swallows the call and returnsfalse— the modifier is never applied.
Every cheat follows this exact pattern. Some additionally run
per-tick clamps on UPROPERTYs (Stamina, Health, Oxygen, Food,
Water, Temperature).
native/
├── shared/ ← libZeusMod shared types + pipe helpers
│ ├── include/
│ │ ├── PipeProtocol.h
│ │ └── SharedTypes.h
│ └── src/PipeProtocol.cpp
├── injector/ ← IcarusInjector.exe (standalone CLI + minimal GUI)
│ ├── src/
│ │ ├── core/
│ │ │ ├── Injector.cpp / .h ← CreateRemoteThread path
│ │ │ └── ProcessUtils.cpp / .h ← PID lookup, privilege elevation
│ │ ├── ui/GUI.cpp / .h ← Optional WinForms window
│ │ └── main.cpp ← CLI entry point
│ └── resources/ ← App icon + .rc
└── internal/ ← IcarusInternal.dll (the payload)
├── src/
│ ├── core/dllmain.cpp ← DllMain, trainer thread boot
│ ├── cheats/
│ │ ├── Trainer.cpp / .h ← Main loop, per-cheat toggles
│ │ ├── TrainerResolve.cpp ← Runtime offset resolution
│ │ ├── TrainerFreeCraft.cpp ← Crafting chain hooks
│ │ ├── TrainerGiveItem.cpp ← MakeItemTemplate → AddItem
│ │ └── TrainerDiagnostics.cpp ← Debug pipe handler surface
│ ├── game/
│ │ ├── UE4.h ← Minimal UE 4.27 primitives
│ │ ├── UObjectLookup.cpp/.h ← FField walker, FName resolver
│ │ └── SDK.h ← Small hand-written SDK slice
│ ├── hooks/Render.cpp/.h ← Swapchain Present hook for overlay
│ ├── ui/Overlay.cpp/.h ← ImGui menu wiring
│ └── util/Logger.cpp/.h ← Ring-buffered in-game log
└── IcarusInternal.vcxproj
The Electron app mirrors this split:
app/
├── src/
│ ├── main/
│ │ ├── main.js ← ipcMain handlers, window lifecycle
│ │ ├── injector.js ← koffi FFI binding (LoadLibraryW injection)
│ │ └── preload.js ← contextBridge window.zeusmod.*
│ ├── renderer/
│ │ ├── index.html ← Sidebar, cards, update modal
│ │ ├── js/app.js ← Renderer-side logic
│ │ └── styles/main.css ← Cyan/purple theme
│ └── assets/icon.ico
├── bin/ ← Bundled DLL + CLI injector
└── package.json
Every single byte-AOB pattern we've tried against Icarus has broken within two patches. Reflection-driven resolution is the difference between "works for six weeks then needs a full reverse-engineering session" and "works across content patches for months".
Concretely, for every UFunction we hook, we do:
uint8_t* target = UObjectLookup::FindNativeFunction(
/* className */ "SurvivalCharacter",
/* funcName */ "SetHealth");Under the hood that looks up the class in GObjects, walks
UStruct::Children to find the UFunction, reads its
Func pointer (the generated thunk), and walks the thunk's
instructions to the C++ exec body. Result: a stable pointer that
survives re-layout of the function within the compiled binary.
See Reflection Internals.
A handful of UE 4.27 layout assumptions remain. They are stable within UE 4.27 but would need re-validation if Icarus ever moves to UE5:
| Assumption | Lives in |
|---|---|
FUObjectItem serial-number offset (+0x14) |
UObjectLookup::GetObjectByIndex |
UStruct::Children offset (+0x40) |
UObjectLookup::WalkFields |
FName::ComparisonIndex layout |
UObjectLookup::ResolveFNameByIndex |
DeployableTickSubsystem +0x60 active-processor TArray
|
TrainerFreeCraft::InjectProcessorEntry |
UMG_EncumbranceBar_C widget offsets (PlayerWeight +0x304) |
TrainerResolve::LocateEncumbranceWidgetOffsets |
| Windows x64 calling convention + MinHook allocation strategy | Everywhere we install a detour |
Anything not in this table is resolved by name or by type.
- Pipe Protocol — the exact wire format for both pipes.
- Hook Catalog — one entry per MinHook detour.
-
Reflection Internals — how
UObjectLookupfinds things. - Memory Layout — every resolved offset we rely on.