# Not Every Byte Gets a Vote

> Source: <https://mitander.xyz/posts/not-every-byte-gets-a-vote/>
> Published: 2026-04-28 00:00:00+00:00

Not Every Byte Gets a Vote
When I started wiring replay for a deterministic simulation, my first instinct was simple:
Easy. Hash everything.
It sounds safe. No byte left behind. If anything changes, the checksum changes.
The catch is that "everything" is not a design boundary. It includes real gameplay state, but it can also include caches, debug traces, render helpers, padding, ordering accidents, and things that are useful without being authoritative.
So the question I ended up needing was narrower:
Which state is allowed to change what happens next?
Player health? Yes.
Projectile position? Yes.
The RNG stream? Yes.
Debug events? Probably not. They are observations.
Render interpolation? No. Useful, but not gameplay truth.
Pathfinding cache? Maybe worth saving, maybe worth rebuilding, but it needs to be named as one of those. It shouldn't become authoritative by accident.
This post is a walkthrough of that split: truth, cache, observation, and presentation. The examples are excerpts from a Zig ARPG game engine. The categories are local to this project: a working vocabulary that has helped me find bugs and argue with the code more clearly.
The tick is the unit replay can trust
The sim advances in fixed ticks. Not frames. Not vibes. Ticks.
The outer tick function mostly schedules phases:
I like this function because it is plain, not clever.
idle
-> ingress
-> control
-> derive
-> plan
-> apply // movement, physics, combat
-> cleanup
-> idle
Replay needs that kind of boring order. Every tick starts from idle, drains the queues it expects to drain, runs systems in a fixed order, increments time once, and returns to idle.
If a queue leaks between phases, the next phase can read a command that belonged to the previous one. If a system mutates state in the wrong phase, it becomes harder to explain which tick caused which result. If the world does not return to idle, the next tick starts with unfinished work already loaded.
The tick matters because it says where work is allowed to happen.
Replay stores inputs, not outcomes
For this replay setup, the file says what went in, not what supposedly happened.
If the replay file stores "the fireball hit for 18," replay is no longer checking the sim. It is just checking whether I can read yesterday's answer back correctly.
The contract is:
seed + input tape -> ticks -> same authoritative result
The recorder is deliberately small:
Seed. Inputs. Tick count. Final checksum.
That's the shape.
The harness then runs once while recording, runs again while replaying, and compares the final checksum:
The checksum is where this becomes easy to fool yourself. Hash too little and replay can miss real bugs. Hash too much and replay starts failing because some non-authoritative helper changed shape.
The checksum also doesn't make the sim deterministic by itself. The usual suspects still matter: fixed tick size, explicit RNG, stable iteration order, initialized state, and no hidden dependency on render timing or local machine state. The checksum just tells you when those rules failed.
So the checksum needs a boundary, not just appetite.
The checksum is a chosen surface
The checksum entrypoint names what counts as authoritative gameplay state:
The list is the point. It says these things can affect future gameplay:
- tick count
- current phase
- RNG state
- entity state
- projectile state
- item, ground-loot, and trinket runtime stores
- tilemap and encounter interaction markers
- runtime modifiers, behavior emissions, scope rewires, and rules
- phase queues
- encounter/node state
This is not "all memory." It is not "whatever fields happen to exist." It is a decision, and it only works if the list contains every piece of state that can affect future gameplay.
One wrinkle: cached stats are in this checksum because later systems read those stats as runtime facts. If the cache is wrong, future gameplay can change. A helper cache can stay out only when it is rebuilt deterministically from authoritative state before anything reads it, or when the authoritative inputs to the cache are what the sim actually branches on.
The list also leaves things out. Debug events are not checksum authority. AI traces are not checksum authority. Render state is not checksum authority.
Those things still matter. Good debug output and good presentation are part of making the game work. They shouldn't make replay look broken when a debug label changes or a render helper gets nicer.
The checksum should catch gameplay divergence. It shouldn't punish better breadcrumbs.
Snapshots ask a different question
Replay and save/load look related, but they are checking different properties.
Replay asks:
If I start over from the same seed and inputs, do I arrive at the same authoritative state?
Snapshot asks:
Can I freeze this world, write it to bytes, restore it, and keep going?
Those overlap, but they are not the same test.
A snapshot may include things that are not checksum authority. A cache might be cheap to rebuild, or it might be worth restoring because rebuilding it at that boundary is awkward. That doesn't automatically make the cache replay truth.
The snapshot encoder uses the same path for measuring and writing:
If buffer
is null, the encoder walks the protocol and counts bytes. If buffer
exists, it writes.
Using the same path keeps measuring and writing from drifting apart.
The field encoder is generic, but the protocol is still explicit:
The failure mode is the useful part. If a field type is not part of the snapshot protocol, the build stops. It doesn't silently invent a format.
Boring in the useful way.
Caches are allowed to be caches
A deterministic engine can still have derived state. Recomputing everything all the time is not magically more correct.
The trick is naming derived state as derived.
The flowfield stores pathing data so AI can ask which way to move from a tile:
AI uses this, so it matters. The cache needs a contract.
If the flowfield is rebuilt in the derive phase from authoritative inputs before AI reads it, the
checksum can care about those inputs instead of the helper arrays. If a later tick can branch on a
persisted valid
flag or stale direction field without rebuilding, that field has joined the
authority path and should be treated like it gets a vote.
The distinction to preserve:
Is this source of truth, or a cached expression of truth with a deterministic rebuild contract?
With that contract, the cache layout can change. In the current code, snapshots do serialize the flowfield so save/load resumes from the exact cached shape, while replay still doesn't treat the helper arrays as checksum authority.
Events are receipts, not steering wheels
The sim tick can optionally emit events:
That ?
is the useful bit. The event queue may not exist.
Turning events on or off should produce the same gameplay state. Events are receipts. They describe what committed: a skill started, a hit landed, a thing died, something interesting happened for VFX or tests.
They don't steer the sim.
This keeps observation from quietly becoming gameplay. If the renderer has to be present for the sim to behave, the replay boundary has probably leaked. If a test changes the outcome by asking for events, it is not only observing anymore.
Input goes in. Gameplay state changes inside the tick. Events come out.
Keep the arrows clean.
Render does not get a vote either
Render is allowed to be smart about presentation. It can interpolate, draw telegraphs, sort sprites, play VFX, and make the game readable.
It doesn't get to decide gameplay facts.
If something is dangerous, the sim should expose that fact. Render can color it red, pulse it, or make it dramatic. But render should not infer danger from pixels or animation timing.
The deadline version of this is tempting:
Just check the animation frame.
That stays out of the authority path.
In this codebase, the same rule shows up elsewhere: app routes input, game owns session meaning, sim owns encounter truth, render presents committed truth.
The boundary can move as the project changes. If it moves, it should move intentionally.
The working taxonomy
When new state appears, I use this checklist:
Can this state change future gameplay?
yes -> checksum/replay authority
no -> keep asking
Is it needed to restore and continue correctly?
yes -> snapshot surface
no -> keep asking
Is it an observation of committed gameplay?
yes -> event/debug/inspect surface
no -> keep asking
Is it only presentation?
yes -> render/app state
The categories are not about importance. Render is important. Debugging is important. Snapshots are important.
Authority is just a different question.
Authoritative state is state with the right to change the future.
Everything else can be useful without getting a vote.
Why bother?
Because bugs get easier to sort.
If replay fails, I can start by assuming gameplay truth diverged, not that a debug label changed.
If save/load fails, I inspect the snapshot protocol.
If a visual is wrong, I ask whether the sim emitted the right fact or render drew it wrong.
If an event is wrong, I can fix the observer without treating it as a replay-authority change.
That's the payoff. Not purity. Fewer places for bugs to hide.
Most of the code here is phase order, bounded storage, explicit hashing, narrow codecs, optional event queues, and assertions. The hard part is deciding what each piece of state is allowed to mean.
Not every byte gets a vote. That's the line this design is trying to hold.
