Skip to content
Lesson 14.5Versioned Evidence, State, and Integrity

Game State and Invariants

A game's state changes through transitions. Invariants describe which states remain valid, while controls and detectors answer different parts of that promise.

Lesson 14.5
Read or listenPrint chapter or book
About 30 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 14.5Key ideas, reference and questions

A one-page summary of “Game State and Invariants” you can keep beside your notes. Plain Markdown.

Lesson 14.5 — Game State and Invariants: cheatsheet

  • Remember: A control limits a state change before it happens; a detector reports evidence that a condition may already have failed.
  • Also useful: The health bar is a scoreboard copy; the simulation record is the scorekeeper. Changing or checking the display does not necessarily change the deciding value.

Key terms

  • invariant: An invariant is a statement that must be true whenever the system is in a valid state.
  • bypass: A bypass is a path that reaches an outcome without satisfying the control, the code that was supposed to guard it.
  • authoritative: Exactly one copy is authoritative: the one whose value decides what happens next.
  • Server authority: Server authority is the same rule at network scale: the server validates every client request against its own record, because anything a client computed can be altered on the client's machine before it is sent.
  • trace: A trace is a list of events in the order they were logged, each saying what changed, when, and which observer saw it.
  • source: source names the observer that produced the event.

Review

  • Q: A heal command is checked against the health bar's cached value, then applied to the simulation's entity object. Why can an out-of-range heal still get through? A: The check read one copy and the change hit another, so the deciding copy went unchecked
  • Q: Which of these is an invariant? A: An entity ID names at most one live entity in a snapshot
Download cheatsheet (.md)

A Lua script, a controller input, and a network message can all ask a game to change health. Lesson 1.3 used the rule 0 <= health <= max_health to decide whether a player record made sense. Now imagine one of those requests arrives when health is 5 and asks for 20 damage. The game should produce health 0, not -15, and its displayed health bar should eventually agree. This chapter asks where that rule is enforced, which copy of health the game trusts, and how to notice when one route around the rule still changes the game.

The earlier chapters established how to recover an object layout and keep a live hook valid. Advanced game hacking now asks what the observed behavior means: record the decision and effect, then inspect integrity and environment checks and encoded values. The final control cases ask whether each check protects the authoritative state it claims to protect.

Four pieces of evidence keep this question answerable: the value and the address temporarily holding it, the exact game build, a timestamped sequence of state changes, and whether the tool observed state or changed it. The following model connects those pieces before looking for a control gap.

An invariant is a property that must stay true

Section titled “An invariant is a property that must stay true”

An invariant is a statement that must be true whenever the system is in a valid state. Examples include:

  • ammunition never becomes negative;
  • an entity ID refers to at most one live entity in a snapshot;
  • a patch is applied only to the exact build and bytes it was made for;
  • a rendering hook is either fully installed or fully removed;
  • a read-only observer never changes the game state.

An invariant does not live at one address. The health bound 0 <= health <= max_health can be enforced by a clamp instruction, depend on a max_health field, and be reported by a log line, all at the same time. Each of those is a place where the rule is kept or reported, and a bug in one of them leaves the rule itself unchanged: a state with health = 120 and max_health = 100 is invalid whichever of the three failed to catch it.

Predict first

A damage routine guards health with max(new health, 0), and the same routine applies heals as positive changes. Health is 90 and max_health is 100.

A heal of 30 arrives. What health does the game store?

Answer: 120, which breaks the invariant. 90 + 30 = 120, and max(120, 0) = 120 because 120 is not below 0. The rule 0 ≤ health ≤ 100 is now false, and the health bar copies 120 as well.

Apply one health change, then keep 0 ≤ health ≤ max_healthThe game adds the change to health, then its control decides what is stored. Damage is a negative change.

Request, control, accepted state

The request, then the control

health + change
90health now
+ 30heal
= 120before the control
Rule applied
max(120, 0) = 120

Accepted state

Stored health (the deciding copy)
120
0 ≤ 120 ≤ 100: broken
Health bar
shows 120copies the accepted value

90 + 30 = 120. max(120, 0) only raises values below 0, and 120 is not below 0, so it passes unchanged. 120 breaks 0 ≤ health ≤ 100, and the health bar copies the broken value.

State, transitions, controls, and detectors

Section titled “State, transitions, controls, and detectors”
Term Precise meaning Example in a toy game
State Values needed to describe the system at one moment health = 48, mode = Alive
Transition An event and rule that move state to another valid state 5 damage changes health from 48 to 43
Invariant A property required of every valid state or transition 0 <= health <= max_health

A control keeps an invariant true, usually by refusing or limiting a transition before it takes effect. A damage routine that stops health at 0 instead of letting it go negative is a control. A detector reports that an invariant may already have failed, such as a scan that logs any entity whose health exceeds its maximum. The two are separate pieces of code, so they fail separately: a detector can be noisy while the control still works, or quiet while the control is incomplete.

noisy
quiet
Invariant
Control:
tries to preserve it
Detector:
reports possible failure
the control can
still be working
the control can
still be incomplete

What bypass analysis means in this chapter

Section titled “What bypass analysis means in this chapter”

A bypass is a path that reaches an outcome without satisfying the control, the code that was supposed to guard it. In a game, the mismatch is often between a convenient proxy and the real rule:

Proxy that gets checked Property that actually matters
Menu button is enabled Engine policy permits the state change
One field’s checksum matches The complete object relation is current and valid
One environment signal is absent The requested command satisfies game rules
An old object still has a plausible address The identity and generation still match

The analysis pattern is always the same: state the promised rule, trace the branch that claims to enforce it, follow every path to the effect, and look for a path where the proxy and the real property disagree. Later lessons apply that pattern to integrity checks, anti-debug behavior, encoded values, and requests that cross game or system boundaries. The hook and layout contracts remain the foundation that makes those observations trustworthy.

A game keeps several copies of the same fact. The simulation’s entity object holds health, the health bar keeps its own copy for drawing, a script receives one in a snapshot, and in a networked game the server keeps a record while each client keeps a prediction of it. Exactly one copy is authoritative: the one whose value decides what happens next. In a single-player game that is usually the simulation object. In a multiplayer game with an authoritative server it is the server’s record, and each client’s copy is a prediction that the server can overwrite.

copied each frame
copied on request
sent over the network
Damage or heal request
Control:
keep health between
0 and max_health
Authoritative copy:
simulation entity,
or the server's record
Health-bar cache
Script snapshot
Client prediction

A control protects only the copy it runs against. Code that validates the health bar’s copy and then applies damage to the simulation object has checked one copy and changed another; Lesson 14.7 calls this the wrong source of truth. Server authority is the same rule at network scale: the server validates every client request against its own record, because anything a client computed can be altered on the client’s machine before it is sent.

The copies hold the same number most of the time, so reading a matching value does not say which copy was read. Timing does. The authoritative copy changes first and decides the next transition; every other copy catches up afterward, on its own schedule.

Suppose an entity can be alive, downed, or removed. A state diagram lists every allowed transition:

spawn
damage, health remains above 0
health reaches 0
revive
timeout
despawn
Alive
Downed
Removed

The diagram raises a question that one memory sample cannot answer: does the stored health field change before or after the Alive -> Downed transition? A sample shows one moment. A trace of timestamped events shows the order.

A trace is a list of events in the order they were logged, each saying what changed, when, and which observer saw it. Typed fields let a program check the trace:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Alive,
Downed,
Removed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct TraceEvent {
sequence: u64,
tick: u64,
source: &'static str,
entity_id: u32,
event: Event,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Event {
HealthRead { value: i32 },
HealthWrite { before: i32, after: i32 },
ModeChanged { before: Mode, after: Mode },
}

sequence gives log order. tick connects the event to the game clock. source names the observer that produced the event. Because an event carries typed fields rather than a sentence, a test can ask directly whether a HealthWrite ending at 0 comes before the ModeChanged to Downed for the same entity. A short trace for entity 7:

sequence tick source entity event
41 900 health watch 7 HealthWrite { before: 6, after: 0 }
42 900 mode watch 7 ModeChanged { before: Alive, after: Downed }
43 901 health bar 7 HealthRead { value: 0 }

Health reaches 0 at tick 900 and the mode changes in the same tick. The health bar, which draws from a copy, shows 0 only at tick 901, one tick later.

State, transition, and display are different observations. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

entity.health = 6; state = Alive
apply damage without going below zero
if health is zero: enter Downed
choose whether the display has refreshed
copy health to the display when refreshed
report both the state and observed copy

Result: health 0, state Downed, display 0. Similar values do not make these observations the same event.

Final values: damage = 6; refresh = yes; health = 0; state = Downed; display = 0.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.

    damage = 6; refresh = yes.

  2. Step 2, line 2. Damage changes the stored health.

    damage = 6; refresh = yes; health = 0.

  3. Step 3, line 3. The transition tests the new health, rather than the old display.

    damage = 6; refresh = yes; health = 0; state = Downed.

  4. Step 4, line 4. A display can still contain an earlier value.

    damage = 6; refresh = yes; health = 0; state = Downed; display = 6.

  5. Step 5, line 5. Refresh copies the current health.

    damage = 6; refresh = yes; health = 0; state = Downed; display = 0.

  6. Step 6, line 6. Result: health 0, state Downed, display 0. Similar values do not make these observations the same event.

    damage = 6; refresh = yes; health = 0; state = Downed; display = 0.

Watch it happenA changing entity and the later observation of its display copy

Entity 7 health changes from 6 to 0 in event 41 at tick 900. Event 42 changes Alive to Downed in the same tick. Event 43 observes the health-bar copy at 0 at tick 901. The initial display-copy value is left unspecified.

Read the steps
  1. Entity 7 has recorded before-values health 6 and mode Alive. The health-bar copy has not yet been observed in this trace.
  2. Sequence 41 at tick 900 records health changing from 6 to 0. The health value and its six filled units become zero.
  3. Sequence 42 records Alive changing to Downed in the same tick. This is a separate typed event for the same entity.
  4. At tick 901, sequence 43 observes health 0 in the drawing copy. The display observation follows the simulation observations by one tick.
  5. The journal keeps entity identity, event type, sequence and tick. It shows the display delay. Log order alone does not prove that one event caused the next.

The state changes and the journal follow the exact three-event trace. Sequence records order; causation needs additional evidence.

Log order is not cause. Event 41 appearing before event 42 fits the damage causing the transition, but it also fits both events sharing one cause, or two observers that flushed their output at different times. Lesson 14.6 adds correlation IDs, which tie together the events of one action even when log order misleads.

Health is an easy starting example because its valid range is visible. The same questions transfer to a rendering hook, though the fields are different:

Question Health update Hook installation
What is the state? Health 48, maximum 100 Original bytes, trampoline, and callback ownership
What transition is requested? Apply 5 damage Install a detour
What must stay true? Health remains between 0 and 100 A call sees a complete original path or a complete hook path
Where is the control? The damage routine before changing simulation health Version check, instruction preparation, and coordinated publication before calls enter the detour
What can a detector report? A snapshot finds health outside the bound A byte or lifecycle check finds an incomplete installation

The analogy has a limit: health is one value, while a hook changes executable bytes that other threads may be using. Lesson 8.10 handles that concurrency. The transferable method is to name the state, permitted transition, invariant, control, and observable failure before choosing an implementation.

  • Invariant: a property required of every valid state or transition.
  • Control: logic intended to preserve an invariant.
  • Detector: logic that reports a possible invariant failure.
  • Authoritative state: the one copy of a value whose contents decide what happens next.
  • Server authority: a design in which the server’s record is authoritative and every client request is validated against it.
  • Trace: events listed in the order they were logged, each with its time and source.

The useful model is state changing through transitions under an invariant. A control must guard the authoritative copy when a transition takes effect; a detector can report a violation but does not enforce that rule. Lesson 14.7 uses this distinction to examine what an integrity check actually covers.

Knowledge check

Guard the copy that decides

A heal command is checked against the health bar's cached value, then applied to the simulation's entity object. Why can an out-of-range heal still get through?

Finished Lesson 14.5?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 14: 10 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.