feat(core): UCG Stage 1 — CellGraph resolver + registry + inert CurrCell

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-02 09:21:12 +02:00
parent b4c4318c8b
commit cf5d60d8fb
2 changed files with 125 additions and 0 deletions

View file

@ -0,0 +1,53 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics; // TerrainSurface
namespace AcDream.Core.World.Cells;
/// <summary>
/// The unified cell graph: the authoritative id-&gt;cell resolver and registry.
/// Built alongside the legacy render/physics cell systems in Stage 1 and consumed
/// by nobody (zero behavior change). Retail anchor: CObjCell::GetVisible (pseudo_c:308209).
/// Worker-thread populated; reads are concurrency-safe.
/// </summary>
public sealed class CellGraph
{
private readonly ConcurrentDictionary<uint, EnvCell> _envCells = new();
private readonly ConcurrentDictionary<uint, (TerrainSurface Terrain, Vector3 Origin)> _terrain = new();
/// <summary>Player's current cell. Defined for Stage 2; INERT in Stage 1 (no writer).</summary>
public ObjCell? CurrCell { get; internal set; }
public bool Contains(uint envCellId) => _envCells.ContainsKey(envCellId);
public void Add(EnvCell cell) => _envCells.TryAdd(cell.Id, cell);
/// <param name="landblockPrefix">Any id in the cell's landblock; masked to (id &amp; 0xFFFF0000).</param>
public void RegisterTerrain(uint landblockPrefix, TerrainSurface terrain, Vector3 worldOrigin)
=> _terrain[landblockPrefix & 0xFFFF0000u] = (terrain, worldOrigin);
public void RemoveLandblock(uint landblockPrefix)
{
uint lb = landblockPrefix & 0xFFFF0000u;
_terrain.TryRemove(lb, out _);
foreach (var id in new List<uint>(_envCells.Keys))
if ((id & 0xFFFF0000u) == lb) _envCells.TryRemove(id, out _);
}
/// <summary>The universal id-&gt;cell resolver (retail CObjCell::GetVisible).</summary>
public ObjCell? GetVisible(uint id)
{
if (id == 0u) return null;
if ((id & 0xFFFFu) >= 0x100u)
return _envCells.TryGetValue(id, out var env) ? env : null;
uint low = id & 0xFFFFu;
if (low < 1u || low > 0x40u) return null;
if (!_terrain.TryGetValue(id & 0xFFFF0000u, out var t)) return null;
int idx = (int)(low - 1u);
return LandCell.Synthesize(id, t.Terrain, t.Origin, idx / 8, idx % 8);
}
public ObjCell? Neighbor(ObjCell cell, in CellPortal portal) => GetVisible(portal.OtherCellId);
}

View file

@ -0,0 +1,72 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.World.Cells;
using Xunit;
namespace AcDream.Core.Tests.World.Cells;
public class CellGraphTests
{
private static TerrainSurface FlatTerrain() => new TerrainSurface(new byte[81], new float[256]);
private static EnvCell Env(uint id) => new EnvCell(id, Matrix4x4.Identity, Matrix4x4.Identity,
Vector3.Zero, new Vector3(10,10,10), System.Array.Empty<CellPortal>(),
System.Array.Empty<uint>(), false, null);
[Fact]
public void GetVisible_ZeroId_ReturnsNull()
=> Assert.Null(new CellGraph().GetVisible(0u));
[Fact]
public void GetVisible_EnvId_ReturnsAddedEnvCell()
{
var g = new CellGraph();
var env = Env(0xA9B40174u);
g.Add(env);
Assert.Same(env, g.GetVisible(0xA9B40174u));
}
[Fact]
public void GetVisible_UnknownEnvId_ReturnsNull()
=> Assert.Null(new CellGraph().GetVisible(0xA9B40174u));
[Fact]
public void GetVisible_LandId_SynthesizesFromRegisteredTerrain()
{
var g = new CellGraph();
g.RegisterTerrain(0xA9B40000u, FlatTerrain(), new Vector3(1000,2000,0));
var cell = g.GetVisible(0xA9B40014u);
var land = Assert.IsType<LandCell>(cell);
Assert.Equal(2, land.Cx);
Assert.Equal(3, land.Cy);
}
[Fact]
public void GetVisible_LandId_NoTerrain_ReturnsNull()
=> Assert.Null(new CellGraph().GetVisible(0xA9B40014u));
[Fact]
public void RemoveLandblock_EvictsEnvAndTerrain()
{
var g = new CellGraph();
g.Add(Env(0xA9B40174u));
g.RegisterTerrain(0xA9B40000u, FlatTerrain(), Vector3.Zero);
g.RemoveLandblock(0xA9B40000u);
Assert.Null(g.GetVisible(0xA9B40174u));
Assert.Null(g.GetVisible(0xA9B40014u));
}
[Fact]
public void Neighbor_ResolvesPortalOtherCellId()
{
var g = new CellGraph();
var target = Env(0xA9B40175u);
g.Add(target);
var portal = new CellPortal(0xA9B40175u, 0, 0, 0);
Assert.Same(target, g.Neighbor(Env(0xA9B40174u), portal));
}
[Fact]
public void CurrCell_IsNull_InStage1()
=> Assert.Null(new CellGraph().CurrCell);
}