Skip to lesson
Game Hacking Academy
Preparing lesson text…
Exit reader mode
Voice and speed

Preparing read-aloud controls…

Lesson 14.1 Listening edition

Object Layouts Across Game Updates

Object identity, field offsets, and layouts must be recovered for each build through several independent access patterns, starting from a toy guard whose padded layout is known.

About 50 minutes

The build manifest identifies which executable is present. It does not tell us whether an old player layout still describes that executable. That second question needs evidence from the running objects and the code that uses them.

An old field offset can still point into mapped memory after an update, which makes a broken tool look plausible. To recover the layout, compare C++ object shape, vtables, container headers, pointer chains, and x86-64 field accesses against the fingerprint of the exact build. Independent clues matter more than one address that happens to return a reasonable number.

Section: An offset belongs to one exact build.

If one build reads health from [r c x+hexadecimal 1 3 8], the number hexadecimal 1 3 8 is true only for that build. The complete statement is:

In this exact build, a method receiving the player object in r c x reads a four-byte health value at offset hexadecimal 1 3 8.

An update can recompile the class with a field added, removed, or reordered, and the same offset then lands on something else. An offset therefore belongs to one exact build, and it has to be checked again after every update before it is used.

The target invariant is:

A layout is used only on the build and the object it was recovered from, and every field read is bounded and checked for a sensible value.

Unpacked, that is four separate demands. Use an offset only on the exact build you proved it on. Use it only on an object you have confirmed is the one you meant. Never read past the end of that object. And check that the value coming back makes sense before believing it — a four-byte read that succeeds is not the same thing as a four-byte read that returned health.

Section: Recover a layout from several independent checks.

Lesson diagram

One memory scan cannot settle a structure: a four-byte 100 at some offset fits health, ammunition, or a coincidence equally well. Static access patterns, runtime value transitions, object identity, and relationships among fields each rule out different wrong answers, so a layout is accepted only where they agree.

Section: Build an offset table.

Suppose several methods access one candidate object:

Lesson table

The relationship 0 less than or equal to health less than or equal to max_health is stronger than “both numbers look reasonable.” The clamp that stores health at hexadecimal 1 3 8 reads its upper bound from hexadecimal 1 3 C, so one instruction sequence ties the two candidates together and each supports the other.

Health and max health: layout and the relation between them.

Section: Distinguish identity from shape.

Two objects can have similar bytes but different roles. Validate identity with several signals:

vtable pointer falls inside the module’s expected read-only data section, and several entries reached through it fall inside executable code;

entity ID matches the manager entry used to reach the object;

generation changes when the slot is reused;

position is finite and within broad world bounds;

health relationships hold;

repeated methods use the same object pointer.

A shape check says bytes resemble a player. An identity check says this is the player object reached through the expected ownership path in this snapshot.

In brief: Matching an object’s shape is different from proving its identity and current ownership path.

Section: Represent uncertainty in the recovered type.

LayoutV42 is metadata in the analysis tool, not a struct read directly from the game. Its usize fields are candidate byte offsets for one exact build. The separate Confidence value records how much independent evidence supports each candidate.

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Generated metadata stores the confidence level beside each numeric offset, together with the accesses and value transitions that earned it, so a Candidate never becomes a trusted field just because it has a convenient name.

Section: The guard’s structure, byte by byte.

A recovered layout is easier to judge after reading one whose source is known. Lesson 4.10 numbered a toy guard’s states for a C++ layout. Here is the whole structure. The guard’s Rust lab, npc_brain_lab.rs, cannot show one itself, because its Rust types use Rust’s default layout, which lets the compiler order fields however it likes. In C++, the fields of a plain structure sit in memory in the order they are declared:

Code example. The code’s comments explain: hexadecimal 1 5 to hexadecimal 1 7: padding

Every float and uint32_t takes 4 bytes, so the offsets count up in fours: 0, 4, 8, 12 (hexadecimal 0 C), and 16 (hexadecimal 1 0). The state is stored as a single byte at 20, which is hexadecimal 1 4 (1 times 16 + 4). The next field is 4 bytes wide, and its alignment requires it to start at a multiple of 4. Bytes 21, 22, and 23 are not, so the compiler leaves them as padding, and seen_for starts at 24, which is hexadecimal 1 8 (1 times 16 + 8). search_ticks follows at 28, hexadecimal 1 C (1 times 16 + 12), and target_id at 32, hexadecimal 2 0 (2 times 16). The structure ends at 36 bytes, hexadecimal 2 4 (2 times 16 + 4).

Here is the guard at the end of tick 5 of Lesson 4.10’s run, mid-chase. It has seen the player on ticks 1 to 5, so seen_for is 5, and chase steps of 1 on ticks 3, 4, and 5 have taken it to (3, 0), still facing east. Its health is still the 100 that the lab gives every new guard:

Labelled memory cells
Labelled memory cells

The target field is an id rather than a pointer. Some games store a raw pointer to the target object instead, and that pointer goes stale when the target is destroyed and its memory reused, the problem that generation-checked handles in Lesson 3.10 solve.

Narration: An id survives its target being destroyed in a way a raw pointer does not: the lookup simply finds nothing, instead of reading a reused address.

Section: A state value that means nothing.

A byte’s 8 bits give 2 to the power of 8 equals 256 different values, 0 to 255, but only 0 to 4 mean anything to the guard’s code. Write 9 into the state byte of a toy program that uses the lab’s five-state guard, and the result depends on how the compiler built the switch statement that picks a branch for each state. Compilers often turn a switch over small numbers into a table of jump targets, one code address per value. If the compiled code checks the range first, 9 goes to a default branch. If it indexes the table without a check, it reads whatever bytes lie past the table’s end as an address, jumps there, and usually crashes.

The guard’s state is an enum, a type whose values come from a fixed list of names. In the Rust lab, safe code cannot store 9 in a GuardState at all. Rust treats an enum holding a value outside its list as undefined behavior: the language promises nothing about what the program then does, so safe code is never allowed to create one. A tool that reads this byte from another process therefore decodes it as a plain integer first and accepts it only when it is one of the five known values, the kind of check that rejecting impossible layouts applies to every field.

Section: Which claims about the guard a run settles.

Suppose the guard’s 36 bytes came from a C++ build whose source you do not have, and you watched them through the demo run. Each line of the layout above is then a claim, and each needs its own check. The run settles some claims and cannot reach others:

Lesson table

The health line shows the limit of any run: it settles only what it made happen. The demo never damaged the guard, so it shows only that +hexadecimal 1 0 held 100. The Confidence value above sums up exactly this evidence, one field at a time.

Predict first

Through all 16 ticks of the demo, bytes +hexadecimal 1 5 to +hexadecimal 1 7 read 00 00 00.

Does that settle the padding claim?

Answer: No: it needs a check that no instruction touches them, in every state. Padding means bytes that no instruction uses. Access breakpoints, which fire on any read or write, that never fire on hexadecimal 1 5 to hexadecimal 1 7 settle it, but only for the states the run reached.

The demo reached Patrol, Chase, Attack and Search, never Flee. A run where the guard is hurt down to 25 health or less, the point at which it flees, tests the health claim and the padding claim in that last state at the same time.

Section: Migrate after an update by matching behavior.

When a new build moves fields:

fingerprint the new executable and symbols that remain available;

locate the same behavioral function through call relationships or a carefully versioned pattern;

collect member accesses again instead of adding a constant to every offset;

replay the same controlled game transitions;

rebuild the offset table;

compare field relationships, not just individual values;

create a new layout record and keep the old record immutable;

run captured-snapshot regression tests for both builds.

As a flow:

Lesson diagram

Step 3 deserves emphasis, because the tempting shortcut is to diff two known offsets and add that difference to all the others. Layouts do not move like that. A field inserted in the middle shifts everything after it and nothing before it, and alignment can absorb part of the shift:

Alternative: An inserted field resembles an inserted book chapter: later positions may shift, earlier ones may stay put, and padding can absorb part of the change.

Labelled memory cells
Labelled memory cells

Nothing here moved by one constant. Two fields did not move at all, one appeared, and the rest shifted by the size of the insertion. A tool that added +8 everywhere would read position eight bytes into the wrong place and report numbers that still look like perfectly plausible coordinates.

Narration: An old offset can still return plausible coordinates from the wrong field. Preserve the access evidence rather than accepting numbers merely because they look reasonable.

Compiler optimization can split, inline, reorder, or eliminate accesses. A missing old pattern does not prove the underlying concept disappeared.

Section: Reject partial reads and impossible layouts.

A reader should validate the whole requested field range before interpreting bytes. Use checked addition for base + offset, enforce maximum object size, and reject unreadable page crossings. Decode into local values; do not create a reference whose lifetime pretends remote bytes belong to the current process.

Validation should reject:

non-finite vectors;

health greater than max_health when the game model forbids it;

a vtable pointer outside expected read-only module data, or initial entries that do not point into an expected executable code section;

duplicate live entity IDs in one snapshot;

generation changes during a multi-field read;

an unsupported build rather than falling back to the nearest known layout.

Section: Score candidates without hiding the reasons.

A score helps rank candidates but does not replace the underlying facts:

Lesson table

The score is stored with the list of checks that produced it, so a high score can be audited.

Section: Glossary terms introduced here.

Layout: the byte positions and interpretation of fields in an object.

Shape check: validation that bytes have a plausible structure.

Identity check: validation that the object is the intended instance.

Generation: a counter distinguishing reuse of the same storage slot.

Cross-validation: confirming a field with independent checks, such as an access pattern, a value transition, and a range relation, that all agree.

Build fingerprint: stable data used to identify one exact binary version.

An old offset is a hypothesis about a new build, not a migrated layout. Recover the new object’s fields from its access patterns and relationships, check its shape and identity, and keep the evidence beside each confidence score. That record shows which uncertainty remains when the build changes again.

Build identity and a recovered layout tell us what a read would mean. They do not grant permission to perform it. The next lesson follows the access rights on the Windows handle that lets a tool reach another process.