fix(G.3): pin dungeon collapse to the cell's landblock, not the position-derived one (#133)
"The dungeon is broken" — the collapse was unloading the REAL dungeon. A dungeon's EnvCells sit at arbitrary "ocean" world coords with negative cell-local Y (snap showed pos=(58.9,-69.6) in cell 0x00070133), so the observer landblock _liveCenterY + floor(pp.Y/192) = 7 + floor(-69.6/192) = 7 + (-1) = 6 lands one row off. The collapse pinned to 0x0006 and unloaded 0x0007 — the real dungeon — which nulled CurrCell (the cell no longer existed) and left the player floating in outdoor-lit empty space (lb 1/1 @ ~1585 fps, but the wrong landblock). This is the Bug-A negative-local-coordinate class. Fix: when inside a dungeon, pin the collapse to the cell's OWN landblock (CurrCell.Id >> 16), never the position-derived observer landblock — the cell id is the authoritative landblock for ocean-placed dungeon geometry. Also hardened the hysteresis so a transient CurrCell flicker can't thrash: - Re-collapse when insideDungeon at a DIFFERENT landblock (multi-landblock dungeon). - Expand only on a DISTANT move (Chebyshev > 1) — a real exit teleports far from the ocean-grid block; the off-by-one flicker is always an ADJACENT (±1) landblock, so it now HOLDS the collapse instead of expanding. - SweepCollapsed always preserves _collapsedCenter (the true dungeon landblock), never the per-frame observer landblock. Build green; 59 streaming tests green (flicker regression test updated to the realistic adjacent off-by-one). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d9e7dd65e9
commit
2561918a70
3 changed files with 52 additions and 18 deletions
|
|
@ -6892,10 +6892,23 @@ public sealed class GameWindow : IDisposable
|
||||||
// and keep their surrounding terrain.
|
// and keep their surrounding terrain.
|
||||||
// Mirrors the playerInsideCell computation below (CurrCell → registry
|
// Mirrors the playerInsideCell computation below (CurrCell → registry
|
||||||
// LoadedCell.SeenOutside): true only for a sealed indoor cell.
|
// LoadedCell.SeenOutside): true only for a sealed indoor cell.
|
||||||
bool insideDungeon =
|
bool insideDungeon = false;
|
||||||
_physicsEngine.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell pcEnv
|
if (_physicsEngine.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell pcEnv
|
||||||
&& _cellVisibility.TryGetCell(pcEnv.Id, out var pcReg)
|
&& _cellVisibility.TryGetCell(pcEnv.Id, out var pcReg)
|
||||||
&& pcReg is { SeenOutside: false };
|
&& pcReg is { SeenOutside: false })
|
||||||
|
{
|
||||||
|
insideDungeon = true;
|
||||||
|
// Pin the collapse to the cell's OWN landblock (cell id high 16 bits),
|
||||||
|
// NOT the position-derived observer landblock. A dungeon's EnvCells sit
|
||||||
|
// at arbitrary world coords (the "ocean" placement) with negative local
|
||||||
|
// offsets, so floor(pp.Y/192) lands one landblock off — which collapses
|
||||||
|
// onto the WRONG landblock and unloads the real dungeon, nulling CurrCell
|
||||||
|
// and breaking the render (the Bug-A coordinate class). The cell id is the
|
||||||
|
// authoritative landblock.
|
||||||
|
uint cellLb = pcEnv.Id >> 16;
|
||||||
|
observerCx = (int)((cellLb >> 8) & 0xFFu);
|
||||||
|
observerCy = (int)(cellLb & 0xFFu);
|
||||||
|
}
|
||||||
_streamingController.Tick(observerCx, observerCy, insideDungeon);
|
_streamingController.Tick(observerCx, observerCy, insideDungeon);
|
||||||
|
|
||||||
// Re-inject persistent entities rescued from unloaded landblocks
|
// Re-inject persistent entities rescued from unloaded landblocks
|
||||||
|
|
|
||||||
|
|
@ -120,13 +120,22 @@ public sealed class StreamingController
|
||||||
|
|
||||||
if (_collapsed)
|
if (_collapsed)
|
||||||
{
|
{
|
||||||
// Hysteresis: stay collapsed while the player remains in the dungeon
|
// Hysteresis. Cases:
|
||||||
// landblock, regardless of CurrCell flicker. Expand only on an actual
|
// - Still in the SAME dungeon landblock → hold (sweep stragglers).
|
||||||
// landblock change (the player left through a portal / was teleported).
|
// - In a DIFFERENT dungeon cell (multi-landblock dungeon / new dungeon)
|
||||||
if (centerId != _collapsedCenter)
|
// → re-collapse onto it.
|
||||||
|
// - CurrCell flickered null but the player hasn't gone anywhere: the
|
||||||
|
// observer landblock reverts to the position-derived value, which for a
|
||||||
|
// dungeon is only ever the ADJACENT off-by-one landblock (negative cell-
|
||||||
|
// local Y). Hold — never expand on an adjacent flicker.
|
||||||
|
// - Genuinely left to a DISTANT landblock (portal/teleport out, always far
|
||||||
|
// from the ocean-grid dungeon block) → expand.
|
||||||
|
if (insideDungeon && centerId != _collapsedCenter)
|
||||||
|
EnterDungeonCollapse(observerCx, observerCy, centerId);
|
||||||
|
else if (!insideDungeon && ChebyshevLandblocks(centerId, _collapsedCenter) > 1)
|
||||||
ExitDungeonExpand(observerCx, observerCy);
|
ExitDungeonExpand(observerCx, observerCy);
|
||||||
else
|
else
|
||||||
SweepCollapsed(centerId);
|
SweepCollapsed();
|
||||||
}
|
}
|
||||||
else if (insideDungeon)
|
else if (insideDungeon)
|
||||||
{
|
{
|
||||||
|
|
@ -200,10 +209,20 @@ public sealed class StreamingController
|
||||||
/// effect. At steady state only the dungeon landblock is resident, so this
|
/// effect. At steady state only the dungeon landblock is resident, so this
|
||||||
/// is a no-op.
|
/// is a no-op.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SweepCollapsed(uint centerId)
|
private void SweepCollapsed()
|
||||||
{
|
{
|
||||||
|
// Always preserve the true dungeon landblock (_collapsedCenter), never the
|
||||||
|
// per-frame observer landblock — a CurrCell flicker must not unload the dungeon.
|
||||||
foreach (var id in _state.LoadedLandblockIds)
|
foreach (var id in _state.LoadedLandblockIds)
|
||||||
if (id != centerId) _enqueueUnload(id);
|
if (id != _collapsedCenter) _enqueueUnload(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Chebyshev distance in landblock cells between two landblock ids.</summary>
|
||||||
|
private static int ChebyshevLandblocks(uint a, uint b)
|
||||||
|
{
|
||||||
|
int ax = (int)((a >> 24) & 0xFFu), ay = (int)((a >> 16) & 0xFFu);
|
||||||
|
int bx = (int)((b >> 24) & 0xFFu), by = (int)((b >> 16) & 0xFFu);
|
||||||
|
return Math.Max(Math.Abs(ax - bx), Math.Abs(ay - by));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -110,22 +110,24 @@ public class StreamingControllerDungeonGateTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Collapsed_CurrCellFlickersToNull_SameLandblock_DoesNotExpand()
|
public void Collapsed_CurrCellFlickersToAdjacentOffByOne_DoesNotExpand()
|
||||||
{
|
{
|
||||||
// Regression: the live run thrashed collapse↔expand because CurrCell
|
// Regression: the live run broke because a dungeon cell's negative local-Y
|
||||||
// momentarily resolved to null (insideDungeon=false) while the player
|
// makes the position-derived observer landblock land one row off (0,7→0,6).
|
||||||
// stayed in the dungeon landblock — leaking lights via reload storms.
|
// When CurrCell flickers null mid-frame, GameWindow stops overriding to the
|
||||||
// The landblock-hysteresis must hold the collapse.
|
// cell landblock and passes that adjacent (0,6). The Chebyshev>1 guard must
|
||||||
|
// treat that as a flicker and HOLD — never expand (which would unload the
|
||||||
|
// real dungeon and re-stream the 25×25 neighbor window).
|
||||||
var h = Make();
|
var h = Make();
|
||||||
h.State.AddLandblock(MakeLb(0, 7));
|
h.State.AddLandblock(MakeLb(0, 7));
|
||||||
h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse
|
h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse onto the dungeon (0,7)
|
||||||
h.Loads.Clear();
|
h.Loads.Clear();
|
||||||
h.Unloads.Clear();
|
h.Unloads.Clear();
|
||||||
|
|
||||||
h.Ctrl.Tick(0, 7, insideDungeon: false); // CurrCell flicker, same landblock
|
h.Ctrl.Tick(0, 6, insideDungeon: false); // flicker → adjacent off-by-one
|
||||||
|
|
||||||
Assert.Empty(h.Loads); // NO full-window reload
|
Assert.Empty(h.Loads); // NO full-window reload
|
||||||
Assert.Empty(h.Unloads); // only the center is resident → nothing to sweep
|
Assert.Empty(h.Unloads); // dungeon (0,7) preserved; nothing else resident
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue