1 Commits

Author SHA1 Message Date
mleo2003 25073d9ebe fix(checkpoint): keep poisonSteps across a restore (#1971)
`Checkpoint.restore()` rejected most valid checkpoints:

    Checkpoint restoration failed: restored state differed at $.save.poisonSteps

poisonSteps is a plain step counter -- (poisonSteps + 1) % 4 on EVERY step,
not only while a mon is poisoned (OverworldController:applyFieldPoison) -- so
it is non-zero three steps out of four in ordinary play.

Installing the restored world re-enters the map, and the map-enter path zeroes
the counter, correctly mirroring ClearVariablesOnEnterMap. Checkpoint.restore
then re-captures the applied state and compares it against the checkpoint, so
the field it had just discarded failed the comparison and the whole restore
rolled back.

A restore is not a map entry from the player's point of view: the counter
belongs to the state being restored. Carry it across the push.

Why this stayed hidden: the two autosave triggers a checkpoint consumer
naturally uses, map.entered and player.warped, are emitted from inside the
very paths that zero the counter (OverworldController 377/572 and 4664/4783),
so those captures hold 0 and restore cleanly. Only a capture taken at an
arbitrary step -- a manual quicksave, or one tied to the ordinary SAVE --
carries a non-zero value. The T4 title-checkpoint tier misses it for the same
reason: its save is fresh, so the counter is already 0.

The comment above the push claimed Checkpoint.resume was this method's only
caller. Both callers arrive through Checkpoint.apply, which serves resume from
the title session and restore from a settled runtime; that is exactly why the
map-entry side effects matter here. Corrected.

tests/engine/restore_poison_steps_bug1971.lua covers 1..3 and 0 across a
restore, and pins the two behaviours that must NOT change: a plain map entry
still zeroes the counter, and a seamless connection crossing still carries it.
It fails on main (3/6, "got 0, want 3") and passes with this change.
2026-08-31 09:46:45 -07:00