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

Telemetry as Evidence of Game Behavior

Bounded event traces connect inputs, decisions, and effects so a suspected control gap becomes a testable claim.

Lesson 14.6
Read or listenPrint chapter or book
About 47 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.6Key ideas, reference and questions

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: GameEvent stores the fields common to every observation; EventKind carries 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
Download cheatsheet (.md)

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.

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:

Game effectValidatorSnapshot builderObserverGame effectValidatorSnapshot builderObserveralt[allowed][denied]input event + correlation IDversioned snapshotallow/deny + reasonvalidated commandpostcondition + generationrefusal recorded

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=14

Read 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.

Matching correlation IDs across log rowsFour rows, two correlation IDs. Following one ID through the log shows what happened to that request.
  1. 1 Four rows in the order they were logged

    41
    denied
    corr 7
    42
    allowed
    corr 8
    43
    state changed
    corr 7
    gen 13
    44
    state changed
    corr 8
    gen 14

    Each row has a sequence number, an event, a correlation ID and sometimes a generation.

  2. 2 Correlation 7 changed state after being denied

    41
    denied
    corr 7
    43
    state changed
    corr 7
    gen 13

    Rows 41 and 43 share ID 7, so they are one request: denied, yet changed at generation 13. Correlation 8 shows the expected order, allowed then changed.

Four rows, two correlation IDs. Following one ID through the log shows what happened to that request.

same corr_id
same corr_id
Decision seq 41, corr 7:
denied, ValueOutOfRange
EffectFinished seq 43, corr 7:
changed = true
Decision seq 42, corr 8:
allowed
EffectFinished seq 44, corr 8:
changed = true

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.

GameEvent
schema_version
sequence
monotonic_micros
correlation_id
build_id
entity_id (optional u32)
kind: EventKind
SnapshotBuilt { generation }
Decision { allowed, reason }
EffectFinished { changed, generation }
EventsDropped { count }

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.

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.

A useful detection is a query over explicit events, not a mysterious score. Examples for a toy game:

  • an effect reports changed = true after a denied decision with the same correlation ID;
  • one entity’s generation decreases;
  • health > max_health appears in a validated snapshot;
  • an installation enters Installed without a preceding verified Ready;
  • 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_id

This does not infer intent. It identifies a concrete contradiction between a control decision and an observed effect.

When two runs differ, line-by-line text comparison is often noisy. Normalize unstable values such as addresses and timestamps, then compare:

  1. state-transition sequence;
  2. event kinds and reason codes;
  3. entity/generation relationships;
  4. call or hook identity;
  5. relative durations and outliers;
  6. final postconditions.

Keep raw evidence available for inspection, but use normalized summaries to find the first meaningful divergence.

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.

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.

Save the smallest event fixture that demonstrates the failure:

SnapshotBuilt generation=12
Decision allowed=false reason=ValueOutOfRange
EffectFinished changed=true generation=13

The 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.

  • 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.

Knowledge check

Detect a control contradiction

Which event pair directly contradicts a guarded-operation invariant?

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.

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.