2026-09-14 · MITTENS THE KITTENS

The rule was written down wrong

Recompiling mid-play left Mittens standing still for the rest of the session, and three fixes failed because the codebase had the underlying rule wrong in about fourteen comments. Also, the full build now builds every stage.

Recompile a script while the game is running and Mittens stops responding. No error, no exception, nothing in the console except four Input System asserts that looked like the cause and were not. Every control dead, for the rest of the session. It has been eating sessions for weeks.

Three fixes had been aimed at it and none landed, because all three reasoned from the same wrong rule. The project had that rule written down confidently, in about fourteen separate comments: a domain reload nulls every field that is not serialized.

That is wrong, and it is wrong in the direction that hides the bug. During a mid-play reload Unity keeps every field whose type it can serialize, private or not, marked or not.

Field kindWhat a mid-play recompile does
bool, int, float, enum, structSurvives, whether or not it is marked for serialization
Auto-property backing fieldSurvives
Reference to a Unity objectSurvives
Array or list of any of the aboveSurvives, contents intact
Serializable plain classSurvives as a fresh copy, not the original instance
Plain class, not serializableDies, comes back null
Interface, delegate, dictionary, set, queueDies, comes back null
Static fieldResets, which is the safe direction

The first row is the half nobody had written down, and it is the half that caused the bug.

What that did in two places

The motor. It guards its setup with a bool and builds a collision sweeper, a plain class, behind it. After a reload the bool came back true and the sweeper came back null. The setup trusted the bool and skipped the rebuild, so every frame after that the cat read the stick, entered the walk state, carried a velocity of 5.5 units a second, and did not move, because the thing that turns velocity into movement no longer existed. A guard flag outliving the thing it vouches for.

The input source. Its cached action map is a serializable class, so it came back as a copy of the map: same name, same bindings, none of the resolved state. The liveness check could only ask whether the asset is alive and whether this is its map, and the copy answered yes to both. Enabling the copy is what fired the four "Map must be contained in state" asserts. The component then reported a successful rebind while binding nothing.

The fix is small and the diagnosis was the work: mark the guard flags and the cached maps as non-serialized so they die with the things they vouch for, and add a reference check alongside each flag so the two can never disagree again.

It was verified in the engine, not argued on paper. A diagnostic that can be called during play reads the private fields of the input source and the controller by reflection in the same frame, which is the only way to tell "input is dead" from "movement is dead", since they look identical from outside.

  • Before: forced a recompile mid-play, injected the stick, the cat did not move, four asserts.
  • After: forced a recompile mid-play, zero asserts, and the cat moved from x 17.83 to x 21.25 under the same injected stick.

The rest of the codebase was then audited by hand for the same shape, and it is clean. Most guards protect state that is itself serializable and so survives together with its flag. Two files already tested the reference instead of a flag, so someone had hit this before and solved it correctly. One component rebuilds its plain-class field above the guard, which is the pattern worth copying. The comments that stated the rule as a general law have been corrected where they sit, because they were what kept producing wrong fixes.

One bug this fix introduced, and caught

Letting the motor re-initialise created a new hazard. Setup captures the standing collider box once, with a comment saying it is captured before anything can duck her and never recomputed. Allowing it to run twice meant a reload landing while Mittens was ducked, in a crate tunnel say, would capture the ducked box as his standing box. He would never be able to stand up again for the rest of the session. The capture is now gated on the standing size itself, which survives the reload and so is the one field that knows whether it has been set.

The whole game in one build

The full-build menu item had been stopping at the second world, so nine of the fifteen stages were only ever built one at a time, by hand. Those stages were shipping, through a catch-all sweep that appends any scene it finds, but in database order rather than play order, and no full build ever ran their builders. A change to one stage was only compiled into its scene when somebody remembered to press that stage's own menu item.

Both halves are fixed. The build list now carries every scene in the order a player meets them, and one press builds everything: 38 builders in 21.1 seconds, zero failures, zero compiler errors across the session's changes.

Also landed

  • The claw can reach props. It used to sweep only the enemy registry, so it could not touch a watering can hanging on a hook. It now sweeps damageable props too. Crates refuse contact damage by default, so every crate-stack puzzle behaves exactly as before, and opting a prop in is one flag.
  • The map's locked gates are real art. Twenty-three gated nodes were three darkened rectangles each. They are now a drawn gate, checked in the scene at the right size and untinted.
  • The player-select screen is dressed. It was a solid black rectangle between a painted title and a painted map, a raw opaque fill. It now uses the same scrim and plate as every other menu, with a paw cursor per row, and locked cats read as unavailable rather than as equal choices that refuse you. It has been written against the shared menu factory but not yet seen running.