fix(G.3): hysteresis on the dungeon streaming gate — stop collapse↔expand thrash (#133)

The first cut of the dungeon gate keyed expand on the per-frame insideDungeon
signal (CurrCell is a sealed EnvCell). Live, CurrCell momentarily resolves to
null mid-frame while the player stays put in the dungeon landblock, so the gate
flipped collapse→expand→collapse every few frames. Each expand re-streamed the
full 25×25 window; the unloads couldn't keep up (MaxCompletionsPerFrame=4), so
registered lights leaked to 212k and FPS spiked to single digits between the
~199 fps collapsed frames.

Fix: once collapsed, key the gate on the STABLE observer landblock, not CurrCell.
Stay collapsed while the player remains in the dungeon landblock (_collapsedCenter);
expand only when the observer actually moves to a different landblock (portal/
teleport out). CurrCell flicker no longer thrashes.

Regression test added (Collapsed_CurrCellFlickersToNull_SameLandblock_DoesNotExpand).
Build green; 60 streaming tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-13 22:43:18 +02:00
parent 56860501b6
commit d9e7dd65e9
2 changed files with 39 additions and 7 deletions

View file

@ -109,6 +109,25 @@ public class StreamingControllerDungeonGateTests
Assert.Empty(h.Loads); // no spurious center reloads
}
[Fact]
public void Collapsed_CurrCellFlickersToNull_SameLandblock_DoesNotExpand()
{
// Regression: the live run thrashed collapse↔expand because CurrCell
// momentarily resolved to null (insideDungeon=false) while the player
// stayed in the dungeon landblock — leaking lights via reload storms.
// The landblock-hysteresis must hold the collapse.
var h = Make();
h.State.AddLandblock(MakeLb(0, 7));
h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse
h.Loads.Clear();
h.Unloads.Clear();
h.Ctrl.Tick(0, 7, insideDungeon: false); // CurrCell flicker, same landblock
Assert.Empty(h.Loads); // NO full-window reload
Assert.Empty(h.Unloads); // only the center is resident → nothing to sweep
}
[Fact]
public void ExitsDungeon_RebuildsFullWindow_UnloadsStaleDungeonLandblock()
{