THE doorway flap root cause, found via [flap-cam]/[shell]/[cell-transit] (2026-06-03): the player spawned + stood still in the room (cell 0171, NO [cell-transit] after teleport), yet the render rooted at the vestibule (0170) for all 77,951 frames — drawing only 0170's ~8-triangle shell, the rest = GL clear color = the bluish void. CellGraph.CurrCell IS "the player's cell" (the render root), but it was written by SetCurrAndReturn inside the PER-ENTITY ResolveWithTransition + ResolveCellId — so EVERY NPC wrote it. A Holtburg NPC (0x000F4240) jump-looping near the doorway clobbered the player's render root every tick. Standing still (player makes no resolve calls) the NPC's write wins → stuck blue void; moving, player/NPC writes fight → the flap. This is why the membership pick fix (correct, kept) didn't change the visual — the render root was clobbered regardless. Fix: CurrCell is now written ONLY by the player. New PhysicsEngine.UpdatePlayerCurrCell is called from PlayerMovementController.UpdateCellId — the single player-only chokepoint for CellId (teleport / server snap @ SetPosition + per-frame resolver). Removed the CurrCell write from SetCurrAndReturn (inlined the 2 resolve call sites to sp.CurCellId) and the 4 ResolveCellId sites. NPCs no longer touch the render root. Teleport→UpdateCellId also covers spawn/standing-still (CurrCell = the player's spawn cell immediately). CellGraphMembershipTests rewritten to the new contract (3 tests): UpdatePlayerCurrCell writes the render root; ResolveCellId does NOT (the blue-hole guard); stale-beats-null preserved. Full Core suite: 1295 pass / 5 fail = the documented §10 baseline, zero new breakage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
3.1 KiB
C#
77 lines
3.1 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using DatReaderWriter.Types;
|
|
using Xunit;
|
|
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
// 2026-06-03 blue-hole fix: CellGraph.CurrCell IS "the player's cell" (CellGraph.cs:19) — it roots
|
|
// the indoor render (GameWindow.OnRender). It is written ONLY by the player-only
|
|
// PhysicsEngine.UpdatePlayerCurrCell (called from PlayerMovementController.UpdateCellId, the single
|
|
// player chokepoint for CellId), NOT by the per-entity ResolveCellId / ResolveWithTransition. Before
|
|
// the fix, those per-entity paths wrote CurrCell, so a Holtburg NPC jump-looping near the cottage
|
|
// doorway clobbered the player's render root every tick → the render rooted at the NPC's tiny
|
|
// connector cell → the cottage doorway "blue-hole" flap.
|
|
public class CellGraphMembershipTests
|
|
{
|
|
private static PhysicsEngine MakeEngineWithCell(uint cellId)
|
|
{
|
|
var engine = new PhysicsEngine();
|
|
var cache = new PhysicsDataCache();
|
|
engine.DataCache = cache;
|
|
|
|
var cs = new CellStruct
|
|
{
|
|
VertexArray = new VertexArray { Vertices = new Dictionary<ushort, SWVertex>() },
|
|
Polygons = new Dictionary<ushort, Polygon>(),
|
|
PhysicsBSP = null,
|
|
};
|
|
var dat = new DatEnvCell
|
|
{
|
|
Flags = (DatReaderWriter.Enums.EnvCellFlags)0,
|
|
CellPortals = new List<DatReaderWriter.Types.CellPortal>(),
|
|
VisibleCells = new List<ushort>(),
|
|
};
|
|
cache.CacheCellStruct(cellId, dat, cs, Matrix4x4.Identity); // registers in the graph (W1)
|
|
return engine;
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdatePlayerCurrCell_Resolved_WritesCurrCellTrackingTheId()
|
|
{
|
|
var engine = MakeEngineWithCell(0xA9B40174u);
|
|
|
|
engine.UpdatePlayerCurrCell(0xA9B40174u);
|
|
|
|
Assert.NotNull(engine.DataCache!.CellGraph.CurrCell);
|
|
Assert.Equal(0xA9B40174u, engine.DataCache.CellGraph.CurrCell!.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdatePlayerCurrCell_UnresolvableId_LeavesCurrCellUnchanged()
|
|
{
|
|
var engine = MakeEngineWithCell(0xA9B40174u);
|
|
engine.UpdatePlayerCurrCell(0xA9B40174u); // CurrCell = 0174
|
|
|
|
engine.UpdatePlayerCurrCell(0xDEADBEEFu); // not in the graph → stale beats null
|
|
|
|
Assert.NotNull(engine.DataCache!.CellGraph.CurrCell);
|
|
Assert.Equal(0xA9B40174u, engine.DataCache.CellGraph.CurrCell!.Id);
|
|
}
|
|
|
|
// Guards the blue-hole fix: the per-entity ResolveCellId must NOT touch the render root.
|
|
// (Only the player's UpdatePlayerCurrCell does.) Before the fix this wrote CurrCell, so a
|
|
// nearby NPC's ResolveCellId/ResolveWithTransition clobbered the player's render root.
|
|
[Fact]
|
|
public void ResolveCellId_DoesNotWriteTheRenderRoot()
|
|
{
|
|
var engine = MakeEngineWithCell(0xA9B40174u);
|
|
|
|
uint result = engine.ResolveCellId(new Vector3(0, 0, 0), 0.5f, 0xA9B40174u);
|
|
|
|
Assert.Equal(0xA9B40174u, result); // still resolves the cell correctly
|
|
Assert.Null(engine.DataCache!.CellGraph.CurrCell); // but does NOT write the render root
|
|
}
|
|
}
|