The transport promises less than it delivers
The client sends what you are pressing. The server decides what happened. The client guesses ahead so the game feels immediate, and corrects itself when the server disagrees.
That is the standard architecture, and the two decisions worth writing down are both about refusing conveniences.
Lying to yourself about the network, on purpose
The first implementation of the transport is an in-process channel. Channels deliver everything, in order, exactly once.
The interface in front of it promises none of that. It promises an unreliable datagram: messages may be lost, arrive out of order, arrive twice, and reads never block. There is a fault injector driven by the seeded generator that makes all of that happen in tests.
A protocol written against what the channel actually delivers would work perfectly until the day a real socket appeared, and then break. Not in a test, but on a real connection, for real players, in ways that depend on their route. The interface is narrow so that the code physically cannot depend on guarantees that will evaporate.
Loss recovery is correspondingly blunt: every message carries the last eight input frames, 48 bytes. No acknowledgements, no retransmission timers, no reordering buffer. Redundancy is cheaper than bookkeeping at this size.
Prediction lives in the shared crate
The predicted path and the authoritative path are literally the same function.
Putting prediction in the client would allow the two to drift. Not maliciously, just through a fix applied in one place and forgotten in the other, which is the classic source of "it feels different online". Having one implementation makes divergence require deliberate effort rather than inattention.
It also means the whole thing tests headless, with no window and no graphics.
The bug was in my harness
The end-to-end loss test found one real bug, and it was mine rather than the protocol's.
The server was consuming tick N's input in the same iteration the client sent it. Which made the eight redundant copies completely worthless: by the time any resent copy arrived, the server had already moved past that tick and discarded it.
Redundancy only helps if the client is running ahead of the server. Real clients are, by roughly half the round trip, and I had not modelled that. There is now an explicit client lead, and the loss test fails without it.
The lesson is not subtle but it is easy to miss: a redundancy scheme can look correct in isolation and be a no-op in context. The test that catches it has to drop packets and assert recovery, not just one of the two.
One hazard guarded rather than fixed
Reconciliation replays ticks. If a tick draws from the random generator, its replay draws again, from a different position in the stream, and diverges.
Nothing draws from it inside a predicted tick today. Rather than trusting that to stay true, snapshots carry a count of draws, and a mismatch raises a specific named divergence. The first system that draws inside a predicted tick trips it immediately.
The fix at that point is to serialise the full generator state. The thing not to do is delete the check, and the check exists partly to say so.
The rest of the day was the engine
Most of the commits on this day are not architecture. They are the ordinary tax of building on a fast-moving engine version: a receiver that is sendable but not shareable, event types renamed to messages, a light field moved, vector operators changed, a random-number crate's trait reshuffled.
None of it is interesting and all of it had to happen. It is worth noting only because a devlog that shows nothing but clean architectural decisions is not describing the same day the commit log does.