Telemetry as Evidence of Game Behavior
Bounded event traces connect inputs, decisions, and effects so a suspected control gap becomes a testable claim.
Cheatsheet for Lesson 14.6
A one-page summary of “Telemetry as Evidence of Game Behavior” you can keep beside your notes. Plain Markdown.
Lesson 14.6 — Telemetry as Evidence of Game Behavior: cheatsheet
- Remember: Useful telemetry connects the input, decision, reason, and resulting effect of one action.
- Also useful: A correlation ID tracks one parcel through several stations; a sequence number orders all recorded arrivals. They answer different questions in the same trace.
Key terms
GameEvent:GameEventstores the fields common to every observation;EventKindcarries the data specific to a snapshot, decision, effect, or dropped-event report.
Formulas and patterns
seq corr event;41 7 Decision allowed=false reason=ValueOutOfRange;42 8 Decision allowed=true reason=Allowed;43 7 EffectFinished changed=true generation=13;44 8 EffectFinished changed=true generation=14
Review
- Q: Which event pair directly contradicts a guarded-operation invariant? A: A denied decision followed by a changed effect with the same correlation ID
- Q: A log says only that a write was refused. Which of the four recorded things is missing? A: The reason: which specific check produced the refusal
An invariant names what a game must keep true. To know whether a control actually enforced it, we need evidence of the input, decision, and effect in the same run. A useful trace follows that path without recording more state than the question requires.
Suppose the game rejects an action just before a health value changes. Those two events are close in time, but proximity does not prove the rejection caused the change. Structured events, a monotonic clock, thread IDs, and a bounded queue let us reconstruct the state-machine path without confusing correlation with causation.
Telemetry should explain a decision path
Section titled “Telemetry should explain a decision path”Most logs turn out to be useless at exactly the moment you need them. They
record that something happened without recording why it was allowed or refused,
so you end up staring at a line that says write failed and guessing.
Useful telemetry lets you follow one game action the whole way through: what came in, what the code decided, why it decided that, and what actually changed as a result.
The rule to design against:
Every guarded operation leaves behind enough evidence to reconstruct four things — which target it touched, what it decided, why, and what happened — without dumping secrets or unbounded blobs into the log.
Those four are worth naming separately, because a log missing any one of them cannot answer the question you will eventually put to it:
| What it records | The question it answers later |
|---|---|
| identity | which process, build, and object was this? |
| decision | was it allowed or denied? |
| reason | which specific check produced that answer? |
| outcome | did the effect actually happen? |
Drop the reason and you can see that something was refused but never why. Drop the outcome and you cannot tell an allowed operation from a completed one.
Design events around the decision pipeline:
The same correlation ID connects the stages without pretending they happened on one thread.
That field earns its place the moment two actions overlap, which on a render thread is most of the time. Here are four events from one run, in the order they reached the log:
seq corr event 41 7 Decision allowed=false reason=ValueOutOfRange 42 8 Decision allowed=true reason=Allowed 43 7 EffectFinished changed=true generation=13 44 8 EffectFinished changed=true generation=14Read that stream the obvious way — pair each effect with the decision just before it — and everything looks correct. The effect at sequence 43 sits right after an allowed decision, so nothing stands out, and the log appears to show two successful operations.
Now pair by correlation ID instead. The effect at 43 belongs to action 7, and action 7 was denied. Something changed state after a refusal, which is the exact contradiction this lesson exists to catch, and the naive reading walked straight past it.
Nothing was missing from the log. The evidence was complete, and ordering alone was still enough to reach the wrong conclusion, because two actions interleaved and the reader assumed they had not. This is why the join in a detection rule is written on the correlation ID rather than on adjacency.
Give each event a stable schema
Section titled “Give each event a stable schema”The diagram becomes two Rust types below. GameEvent stores the fields common
to every observation; EventKind carries the data specific to a snapshot,
decision, effect, or dropped-event report. A consumer can order events by
sequence and join one request’s path by correlation_id.
Optional: practise the named operations and their inputs with a few whole-token blanks.
#[derive(Debug, Clone, PartialEq, Eq)]struct GameEvent { schema_version: u16, sequence: u64, monotonic_micros: u64, correlation_id: u64, build_id: u64, entity_id: Option<u32>, kind: EventKind,}
#[derive(Debug, Clone, PartialEq, Eq)]enum EventKind { SnapshotBuilt { generation: u32 }, Decision { allowed: bool, reason: ReasonCode }, EffectFinished { changed: bool, generation: u32 }, EventsDropped { count: u64 },}Use a monotonic clock for duration and ordering inside one run. Wall-clock time
is useful for humans but can jump. sequence detects missing or reordered
events. A schema version allows the reader to reject fields it cannot
interpret.
Record decisions on every branch
Section titled “Record decisions on every branch”Logging only successful operations creates a blind spot. At minimum, record:
| Field | Why it matters |
|---|---|
| Operation category | Which boundary made the decision |
| Target identity | Which build and entity were involved |
| Snapshot generation | Whether evidence was fresh |
| Allow/deny result | The decision itself |
| Bounded reason code | Why the branch was taken |
| Postcondition | Whether the intended effect occurred |
| Drop counter | Whether evidence was lost |
Avoid raw pointers as durable identity: allocations move and addresses can be reused. Prefer build ID plus entity ID and generation.
Derive detections from invariants
Section titled “Derive detections from invariants”A useful detection is a query over explicit events, not a mysterious score. Examples for a toy game:
- an effect reports
changed = trueafter a denied decision with the same correlation ID; - one entity’s generation decreases;
health > max_healthappears in a validated snapshot;- an installation enters
Installedwithout a preceding verifiedReady; - events are dropped during the exact interval being analyzed.
The first rule can be expressed as a join:
finding = EffectFinished.changed && matching Decision.allowed == false && same correlation_idThis does not infer intent. It identifies a concrete contradiction between a control decision and an observed effect.
Compare traces by structure
Section titled “Compare traces by structure”When two runs differ, line-by-line text comparison is often noisy. Normalize unstable values such as addresses and timestamps, then compare:
- state-transition sequence;
- event kinds and reason codes;
- entity/generation relationships;
- call or hook identity;
- relative durations and outliers;
- final postconditions.
Keep raw evidence available for inspection, but use normalized summaries to find the first meaningful divergence.
Measure detector quality
Section titled “Measure detector quality”Build a labeled fixture table:
| Fixture | Invariant actually fails? | Rule fires? | Classification |
|---|---|---|---|
| Valid command | No | No | True negative |
| Denied command, no effect | No | No | True negative |
| Denied command, effect changes | Yes | Yes | True positive |
| Allowed command, delayed event | No | Yes | False positive |
| Effect changes but decision event was dropped | Yes | No | False negative / insufficient evidence |
Report counts and the exact fixture set. “Detected 100%” is meaningless without knowing what was tested.
Design overload behavior deliberately
Section titled “Design overload behavior deliberately”A render hook cannot block forever to preserve logs. Use a bounded queue and choose a policy:
- drop newest and increment a counter;
- drop oldest and retain the recent window;
- sample low-priority events while preserving decisions and failures;
- write a compact overflow marker once per interval.
Never hide loss. An analysis interval containing dropped decision events has lower evidentiary confidence.
A render hook cannot block forever to preserve logs, so use a bounded queue with a deliberate overflow policy, and never hide how many events were dropped. The interactive version needs JavaScript; the lesson text around it explains the same idea.
Turn a finding into a regression trace
Section titled “Turn a finding into a regression trace”Save the smallest event fixture that demonstrates the failure:
SnapshotBuilt generation=12Decision allowed=false reason=ValueOutOfRangeEffectFinished changed=true generation=13The repair test passes only when the effect remains unchanged after the denied decision. Keep a separate test confirming that a valid command still changes state; a repair that disables everything is not correct.
Glossary terms introduced here
Section titled “Glossary terms introduced here”- Telemetry: structured observations emitted by a running system.
- Correlation ID: an identifier connecting events from one logical action.
- Reason code: a stable bounded explanation for a decision.
- Normalization: replacing unstable details so trace structure can be compared.
- Detection rule: a precise query identifying evidence of an invariant failure.
- Regression trace: a saved minimal event sequence used to test a repair.
Versioned events and correlation IDs connect a decision to its eventual effect. Contradictory pairs reveal a broken invariant; false-positive and false-negative counts show whether the rule works in ordinary use. The smallest failing trace becomes a regression test, while valid behavior must continue to pass.
Detect a control contradiction
Which event pair directly contradicts a guarded-operation invariant?
A shared correlation ID connects the refusal to the effect. If state changes anyway, the decision and effect boundary disagree.
Finished Lesson 14.6?
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.
Plain Markdown, saved in this browser.
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.