Everything you need to iterate on ZeusMod without restarting Icarus every time: the Python inspector, the DLL's debug pipe, and the validated UE4 offsets discovered so far.
Updated 2026-04-21.
┌───────────────────────┐
│ Icarus-Win64-Shipping │
│ + IcarusInternal.dll │◄──── pipe \\.\pipe\ZeusModPipe ◄──── scripts/inspect.py
│ │ (message-mode, single-instance) (Python)
│ - Trainer (cheats) │
│ - Hooks (MinHook) │◄──── ImGui/DX11 overlay
│ - UObjectLookup │ toggles: GodMode, FreeCraft,
│ - Pipe server thread │ InfiniteItems, StackBooster, …
└───────────────────────┘
▲
│ injected by native/injector/IcarusInjector.exe
│ (also controlled by app/ Electron launcher)
- DLL:
native/internal/— everything that runs inside Icarus. Compiles toIcarusInternal.dll. Post-build copies tonative/injector/bin/Release/so the injector always ships the latest build. - Injector:
native/injector/— classic LoadLibrary injector. - Launcher:
app/— Electron UI that talks to the DLL over the same pipe and wraps the injection. - Inspector:
scripts/inspect.py— live memory explorer; the primary autonomous-iteration tool described in this doc.
The DLL opens \\.\pipe\ZeusModPipe as PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, single-instance, 64 KB buffers. A client connects,
sends one request, reads one response, closes.
<command>:<value> — cheat toggle / value (existing commands)
dbg:<subcmd>[:<args...>] — debug / introspection (new, read-only)
Responses always start with OK or ERR .
| Prefix | Owner | Example |
|---|---|---|
dbg: |
inspection/reflection | dbg:props:Inventory |
| (bare) | cheats / tuning | godmode:1, speed_mult:4.0 |
The single-instance pipe means clients must race-retry on
ERROR_FILE_NOT_FOUND (2) and ERROR_PIPE_BUSY (231). The Python client
does this transparently.
Implemented in native/internal/src/cheats/Trainer.cpp, split between
HandleDbgCommand (uses std::string freely) and HandleDbgRaw
(SEH-only: must not use C++ objects because __try can't coexist with
C++ unwind — error C2712).
| Command | Args | Description |
|---|---|---|
classof |
<addr> |
UClass name of the UObject at addr |
nameof |
<addr> |
FName of the UObject at addr (reads FName at addr+0x18) |
findcls |
<name> |
Find a UClass by exact name → OK 0x… |
findstruct |
<name> |
Find a UClass or UScriptStruct by name |
findobj |
<className> |
First non-CDO live instance of a class |
listobj |
<className>:<maxN> |
List up to N live instances |
| Command | Args | Description |
|---|---|---|
read8 |
<addr> |
1-byte read (SEH-guarded) |
read32 |
<addr> |
4-byte read as u32 (OK 0xHH (dec)) |
read64 |
<addr> |
8-byte read as u64 / pointer |
dump |
<addr>:<size> |
Hex dump, size ≤ 0xC000 |
scan |
<addr>:<range> |
Find {ptr, num, max} patterns in range bytes |
| Command | Args | Description |
|---|---|---|
propoff |
<class>:<prop> |
Offset of a named property (walks Super chain, 16 hops max) |
props |
<className> |
List all own properties: +0xNNN Name Type |
propsall |
<className> |
Same but walks Super chain |
props/propsall accept UClass or UScriptStruct names (they share
UStruct layout). Example:
dbg:props:Inventory
OK props Inventory (cls=0x150A9B62380)
+0x0C8 OnInventoryItemChanged MulticastInlineDelegateProperty
+0x0E8 CurrentWeight FloatProperty
+0x0F0 Slots StructProperty
+0x1F8 Items (inside Slots struct — not shown here, see propswalk)
…
| Command | Args | Description |
|---|---|---|
character |
— | Player pawn ptr + Off::Player_InventoryComp |
playerinv |
— | Walk the InventoryComponent, list child UInventory* candidates |
Three modes:
# REPL
python scripts/inspect.py
# One-shot
python scripts/inspect.py "props Inventory"
python scripts/inspect.py "read64 \$char+0x758"
# Batch (semicolon-separated, variables persist)
python scripts/inspect.py -c "character = c; read64 \$c+0x758 = comp; dump \$comp 0x100"Every hex argument goes through VarStore.resolve() which understands:
- bare hex (
132515DAAC0) — auto-prefixed to0x132515DAAC0 - explicit hex (
0xDEADBEEF) - decimal (
4096) - variables (
$char,$comp,$prev) - arithmetic (
$char + 0x758,$bag - 0x10)
Save a command's primary hex result with = name:
character = char
read64 $char+0x758 = comp
scan $comp+0xE8 0x30
Same name as the DLL command. The inspector handles hex-encoding, variable substitution, and response parsing.
DBG_PRIMITIVES = {
"findcls", "findstruct", "findobj", "listobj", "classof", "nameof",
"read8", "read32", "read64", "dump", "scan", "character", "playerinv",
"props", "propsall", "propoff",
}Cheat toggles are available too:
CHEAT_COMMANDS = {
"godmode", "stamina", "armor", "oxygen", "food", "water",
"craft", "items", "stacks", "weight", "speed", "speed_mult",
"time", "time_val", "temp", "temp_val", "megaexp",
"talent", "tech", "solo", "give",
}Located in scripts/inspect.py, registered in COMPOSITES.
| Composite | Purpose |
|---|---|
follow <addr> [+offN...] |
Chase a pointer chain, report final class |
obj <addr> |
Class, name, and 5-level outer chain |
finduobj <className> |
findobj + obj one-liner |
invtest <addr> |
Full UInventory candidate analysis: class, outer chain, all nested TArrays in 0x30..0x300, first slot dump |
findplayerinv |
Walk character → InvComp → list UInventory candidates, flag the one whose outer reaches the character as [OWNS PLAYER] |
propswalk <addr> |
Raw ChildProperties walker (works even when the current DLL doesn't have props). Walks UStruct.ChildProperties (+0x50 → FField chain, +0x20 Next, +0x28 NamePrivate, +0x4C Offset_Internal) |
def composite_foo(args: list[str], vars_: VarStore) -> str:
if not args:
return "ERR usage: foo <addr>"
addr = vars_.resolve(args[0])
# Call primitives via primitive_send(), read results via extract_hex()
r = primitive_send("read64", [f"{addr + 0x10:X}"], vars_)
cls = extract_hex(r)
vars_.prev = addr # so the result can be chained
return f"OK foo 0x{addr:X} cls=0x{cls:X}"
COMPOSITES["foo"] = composite_fooComposites should stay composable: they call primitive_send() just
like a user would, so every operation is still logged in the pipe and
can be reproduced manually.
All offsets are resolved dynamically via reflection in the DLL, but the values below are stable for this game build and have been observed in live memory (slots 0..29 of the real Backpack, 30/56 items in use).
UObject:
+0x00 vtable
+0x08 ObjectFlags | ObjectIndex
+0x10 ClassPrivate (UClass*)
+0x18 NamePrivate (FName) ← what ReadFNameAt uses
+0x20 OuterPrivate (UObject*)
UStruct (base of UClass and UScriptStruct):
+0x40 Super (UStruct*)
+0x48 Children (UField*) — functions chain
+0x50 ChildProperties (FField*) — properties chain
FField (base of FProperty):
+0x00 ClassPrivate (FFieldClass*) — first field = FName at +0x00
+0x20 Next (FField*)
+0x28 NamePrivate (FName)
FProperty:
+0x4C Offset_Internal (int32) ← the byte offset we want
UFunction (at vtable[68] = ProcessEvent in Icarus):
+0xB0 FunctionFlags
+0xB4 NumParms
+0xB6 ParmsSize
+0xB8 ReturnValueOff
+0xD8 Native Func (C++ thunk)
BP_IcarusPlayerCharacterSurvival_C
+0x758 InventoryComponent* (Off::Player_InventoryComp)
UInventoryComponent
+0x0E8 Inventories : TArray<Entry> (Off::InvComp_Inventories)
Entry = 0x20 bytes = {
+0x00 vtable (constant for this build)
+0x08 FName name ← "Quickbar" / "Backpack" / "Equipment" / "Suit" / "Upgrade"
+0x10 UInventory* bag ← the actual container
+0x18 weakObjectPtr
}
+0x138 ManuallyAddedItems
UInventory (class "Inventory")
+0x0E8 CurrentWeight : float
+0x0F0 Slots : FInventorySlotsFastArray (0x160 bytes)
+0x1F8 ↳ Items : TArray<FInventorySlot> (Off::FastArray_Slots)
+0x250 OverflowSpawnTransform
+0x288 InitialItems
+0x2A8 CurrentlyEquippedModifiers
+0x308 ReplicatedModifierStackMultipliers
+0x318 InventoryInfoRowHandle
+0x3A0 ParentInventory
FInventorySlot (stride 0x240)
+0x010 ItemData : FItemData (Off::Slot_ItemData)
+0x200 Query
+0x218 Locked
+0x21C LastItem
+0x234 Slotable
+0x238 Index (int32) ← set to slot index when appending
FItemData (total 0x1F0)
+0x018 ItemStaticData : FItemTemplateRowHandle (Off::Item_StaticData)
+0x00 DataTablePtr (FWeakObjectPtr, 8 bytes)
+0x08 RowName (FName)
+0x10 DataTableName (FName)
+0x030 ItemDynamicData : TArray<FItemDynamicData> (Off::Item_DynamicData)
FItemDynamicData = 8 bytes = {
+0x00 uint8 type ← e.g. 7 = ItemableStack
+0x04 int32 value ← stack count when type == ItemableStack
}
+0x040 ItemCustomStats
+0x050 CustomProperties
+0x0A0 CachedStats
+0x1B0 bIsItemInstance
+0x1B8 DatabaseGUID (Off::Item_DatabaseGUID)
+0x1C8 ItemOwnerLookupId
+0x1D0 RuntimeTags
These live in native/internal/src/cheats/Trainer.h inside
namespace Off so every module shares them.
A stack of N items is one FInventorySlot containing one
FItemData whose ItemDynamicData TArray has an entry with
type == 7 (ItemableStack) and value == N.
So the Stack Booster walks every slot and sets value = 9999 on each
ItemableStack dynamic property — it does not duplicate meta items.
All items live in a global IcarusDataTable singleton resolved on init:
g_dItemTemplate→ pointer toD_ItemTemplate(3054 rows)BuildItemLibrary_Internal()iterates rows viaIntToStructand caches theirFNamerow names for the UI
When placing an item we:
- Call
MakeItemTemplate(rowName)viaCallUFunctionto build anFItemTemplateRowHandlewith a correctDataTablePtr. - Call
CreateItemwith that handle to build anFItemDatatemplate. - Copy the resulting 0x1F0 bytes into
Slots[foundSlot].ItemData(direct memory write — the server-authoritativeManuallyForcePlaceItemrejects client-side placements). - Increment
Slots.numif we appended past the current tail. - Optionally invoke
MarkSlotIndexDirty(foundSlot)to nudge replication/UI.
python scripts/inspect.py
zm> props ClassName # list reflected properties
zm> propoff ClassName FieldName # one-off offset
zm> findstruct MyStruct = st
zm> propswalk $st # fallback when DLL lacks struct fallback
zm> finduobj SomeClass # first live non-CDO
zm> listobj SomeClass 20 # list 20 instances
zm> invtest 0x… # heavy analysis on a candidate
zm> character = c
zm> read64 $c+0x758 = comp
zm> read64 $comp+0xE8 = invArr # TArray data ptr
zm> dump $invArr 0xC0 # all 6 inventory entries
zm> findplayerinv # composite: does all of the above
zm> nameof 0x153DF930020+0x18 # resolve RowName of slot 0
OK Stone
zm> nameof 0x153DF930020+0x240+0x18 # slot 1
OK Sulfur
zm> nameof 0x153DF930020+29*0x240+0x18 # slot 29
OK Refined_Metal
The nameof X trick reads an FName at X+0x18 (it's really designed
for UObjects but works for any FName-shaped memory: pass
FNameAddr - 0x18).
-
Open
native/internal/src/cheats/Trainer.cpp. -
Decide: does your command need
std::string/std::vector?- Yes → add it in
HandleDbgCommand, before thereturn HandleDbgRaw(...)line. - No (raw memory only) → add it in
HandleDbgRaw(SEH-only, no C++ objects allowed in the same function).
- Yes → add it in
-
Keep the response format
OK ...orERR .... The Python client parses the first hex token as$prev. -
If it takes typed UE data, use
UObjectLookup::*— the FName pool and GObjects resolver is already set up. -
Update the error fallthrough at the bottom of
HandleDbgRaw:return _snprintf_s(out, outCap, _TRUNCATE, "ERR unknown cmd '%s'. Available: … <your-cmd>", cmd);
-
Add the name to
DBG_PRIMITIVESinscripts/inspect.pyso the client routes the command through thedbg:prefix automatically.
Windows structured exceptions (__try/__except) can't coexist with C++
unwind in the same function. If you need both, split the function:
static bool SafeRawRead(void* p, int* out) {
__try { *out = *reinterpret_cast<int*>(p); return true; }
__except (1) { return false; }
}
int OuterFunction() {
std::string s; // unwindable — __try would fail here
int v = 0;
if (!SafeRawRead(somePtr, &v)) return 0;
s = std::to_string(v);
// …
}This pattern is used throughout ResolvePlayerInventoryByName and
HandleDbgCommand / HandleDbgRaw.
- Field in
Trainer.h:bool MyCheat = false; - Pipe dispatch in
PipeServerThread(Trainer.cpp):else if (strcmp(cmd, "mycheat") == 0) self->MyCheat = (v != 0);
- Tick logic in
Trainer::Tick(guard everything with__try/ bounds checks — this runs on a worker thread every frame). - Electron UI toggle in
app/src/renderer/index.html+ handler. - ImGui toggle in
native/internal/src/hooks/Render.cpp(inside the cheat panel draw). - Legacy overlay toggle (optional) in
native/internal/src/ui/Overlay.cpp.
Use ResolvePlayerBackpack() / ResolvePlayerQuickbar() rather than
Player_InventoryComp directly — the former returns a real UInventory
whose FastArray_Slots offset is valid.
# From repo root, with VS Build Tools installed:
"C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe" \
native/internal/IcarusInternal.vcxproj \
/p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 /m /v:mOutput:
native/internal/bin/Release/IcarusInternal.dll— canonicalnative/injector/bin/Release/IcarusInternal.dll— copied by post-build step; locked while Icarus is running (expect a post-build copy failure, that's OK — the DLL itself still compiled)
If the copy step fails because Icarus is running, close the game and
either re-run MSBuild or cp the DLL manually:
cp native/internal/bin/Release/IcarusInternal.dll \
native/injector/bin/Release/IcarusInternal.dll- FWeakObjectPtr — 8 bytes,
{int32 ObjectIndex, int32 ObjectSerialNumber}. Both values change per session; never hardcode. - FName — 8 bytes,
{int32 ComparisonIndex, int32 Number}. The comparison index is a pool offset; resolve viaReadFNameAtor thenameoftrick. - UInventoryComponent is not a UInventory. It holds a TArray of
named UInventory children (Quickbar, Backpack, …). Direct-writing
slot data into the component's memory works coincidentally because
Off::FastArray_Slots = 0x1F8happens to fall in a reasonable spot, but it doesn't replicate and silently fails validation. Always resolve the right child first. ManuallyForcePlaceItemis server-authoritative. Client-side calls returnfalsewith no other signal. Use direct memory write into the Slots TArray instead, thenMarkSlotIndexDirtyto signal replication.- Single-instance pipe.
scripts/inspect.pyretries on err 2 / 231 for 400 ms — don't remove the retry loop, composites fire hundreds of requests in a row and will hit the connect/recreate window. - Post-build copy failure ≠ build failure. MSBuild reports
error MSB3073when Icarus holds the injected DLL. The.dllinnative/internal/bin/Release/is still fresh. - FProperty size/dim offsets beyond
Offset_Internalhave not been validated for this build. Don't trustpropsoutput for those fields — it's elided in the current code. - Slots.num vs Slots.max.
num= currently-used;max= pre-allocated capacity. A backpack withnum=30 max=56has 26 free slots — iterate tomaxwhen looking for insertion space, bumpnumwhen you append past the current tail.
# Character & inventory
python scripts/inspect.py character
python scripts/inspect.py findplayerinv
# Structure introspection
python scripts/inspect.py "props Inventory"
python scripts/inspect.py "propoff Inventory Slots"
python scripts/inspect.py "findstruct ItemData"
python scripts/inspect.py "propswalk 0x…"
# Memory probing
python scripts/inspect.py "read64 \$char+0x758"
python scripts/inspect.py "dump \$comp 0x100"
python scripts/inspect.py "scan \$bag 0x400"
# Combined discovery session
python scripts/inspect.py -c \
"character = c; \
read64 \$c+0x758 = comp; \
read64 \$comp+0xE8 = invArr; \
read64 \$invArr+0x10 = bag; \
classof \$bag; \
scan \$bag+0xF0 0x160"
# Toggle a cheat directly over pipe (smoke test after a rebuild)
python scripts/inspect.py "stacks 1"
python scripts/inspect.py "items 1"
python scripts/inspect.py "give wood,10"