using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics; // TerrainSurface
namespace AcDream.Core.World.Cells;
///
/// The unified cell graph: the active, authoritative id->cell resolver and registry.
/// Populated from validated
/// payloads
/// (a physics root is optional; a containment root is required) and consumed across
/// the engine: resolves any cell id, is
/// the player render/lighting root, resolves the
/// 3rd-person camera cell, and supplies the block-local
/// terrain origin for the LandDefs lcoord math. Retail anchor: CObjCell::GetVisible
/// (pseudo_c:308209). Worker-thread populated; reads are concurrency-safe.
///
public sealed class CellGraph
{
private readonly CollisionWorldStateSlot _collisionWorld;
private ConcurrentDictionary _envCells =>
_collisionWorld.Current.EnvCells;
private ConcurrentDictionary _terrain =>
_collisionWorld.Current.Terrain;
private ConcurrentDictionary _outdoorCells =>
_collisionWorld.Current.OutdoorCells;
public CellGraph()
: this(new CollisionWorldStateSlot())
{
}
internal CellGraph(CollisionWorldStateSlot collisionWorld)
{
_collisionWorld = collisionWorld
?? throw new ArgumentNullException(nameof(collisionWorld));
}
/// The player's current cell — the render/lighting root. Written ONLY at the
/// player chokepoint
/// (NPCs never touch it — a per-entity writer was the cottage-doorway "blue-hole"
/// cause); read by the renderer for the player root (GameWindow). Left unchanged when
/// the id isn't yet resolvable in the graph (stale beats null).
public ObjCell? CurrCell { get; internal set; }
public bool Contains(uint envCellId) => _envCells.ContainsKey(envCellId);
public void Add(EnvCell cell) =>
_collisionWorld.Current.TryAddEnvCell(cell.Id, cell);
/// Any id in the cell's landblock; masked to (id & 0xFFFF0000).
public void RegisterTerrain(uint landblockPrefix, TerrainSurface terrain, Vector3 worldOrigin)
{
uint prefix = landblockPrefix & 0xFFFF0000u;
_terrain[prefix] = new CellGraphTerrain(terrain, worldOrigin);
for (uint low = 1u; low <= 0x40u; low++)
{
uint id = prefix | low;
int index = (int)(low - 1u);
_outdoorCells[id] = LandCell.Synthesize(
id,
terrain,
worldOrigin,
index / 8,
index % 8);
}
}
///
/// World origin (SW corner) of the landblock containing ,
/// as registered by . Issue #106: converts the
/// floating-world-frame sphere coords into retail's block-local frame for the
/// lcoord math. False (origin zero) when the landblock
/// has no registered terrain — callers fall back to the legacy anchor-block
/// assumption (world frame == block-local frame).
///
public bool TryGetTerrainOrigin(uint id, out Vector3 origin)
{
if (_terrain.TryGetValue(id & 0xFFFF0000u, out var t))
{
origin = t.Origin;
return true;
}
origin = Vector3.Zero;
return false;
}
public void RemoveLandblock(uint landblockPrefix)
{
uint lb = landblockPrefix & 0xFFFF0000u;
if (CurrCell is { } current
&& (current.Id & 0xFFFF0000u) == lb)
{
CurrCell = null;
}
_terrain.TryRemove(lb, out _);
for (uint low = 1u; low <= 0x40u; low++)
_outdoorCells.TryRemove(lb | low, out _);
RemoveEnvCellPrefixKeys(lb);
}
///
/// O1: retire one prefix's EnvCells through the installed-key ledger —
/// O(prefix keys), never a whole-map scan. Removal tombstones the
/// captured slot list, so index iteration stays exact.
///
private void RemoveEnvCellPrefixKeys(uint prefix)
{
CollisionWorldState world = _collisionWorld.Current;
List? slots = world.EnvCellKeys.SlotsForPrefix(prefix);
if (slots is null)
return;
int limit = slots.Count;
for (int index = 0; index < limit; index++)
{
uint id = slots[index];
if (id != 0u)
world.RemoveEnvCell(id);
}
}
///
/// Remove only a landblock's indoor environment cells while preserving its
/// outdoor terrain registration. Used by Near-to-Far streaming demotion.
///
public void RemoveEnvCellsForLandblock(uint landblockPrefix)
{
uint lb = landblockPrefix & 0xFFFF0000u;
if (CurrCell is { } current
&& (current.Id & 0xFFFF0000u) == lb
&& (current.Id & 0xFFFFu) >= 0x0100u)
{
CurrCell = null;
}
RemoveEnvCellPrefixKeys(lb);
}
/// The universal id->cell resolver (retail CObjCell::GetVisible).
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;
return _outdoorCells.TryGetValue(id, out ObjCell? cell)
? cell
: null;
}
///
/// Resolve the cell on the far side of a portal — retail CCellPortal::GetOtherCell =
/// GetVisible(other_cell_id). documents the traversal source and is
/// reserved for the OtherCellPtr neighbor cache (Stage 3); the lookup keys only on the portal.
///
public ObjCell? Neighbor(ObjCell cell, in CellPortal portal) => GetVisible(portal.OtherCellId);
///
/// Retail CEnvCell::find_visible_child_cell @ 0x0052dc50 (pseudo_c:311397).
/// Walk 's StabList + the root itself, return the first
/// EnvCell whose is true for
/// . Used to resolve the camera cell in 3rd-person
/// from the physics cell graph rather than an independent AABB reclassification —
/// the root is the player cell, never a camera-eye AABB scan.
/// Returns null when no cell in the root's stab list (or the root itself) contains
/// the query point (caller falls back to the player cell as projection root).
///
public EnvCell? FindVisibleChildCell(uint rootId, Vector3 worldPoint)
{
if (!_envCells.TryGetValue(rootId, out var root)) return null;
if (root.PointInCell(worldPoint)) return root;
foreach (var stabId in root.StabList)
if (_envCells.TryGetValue(stabId, out var stab) && stab.PointInCell(worldPoint))
return stab;
return null;
}
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
CellGraph staging,
uint landblockId) => new(this, staging, landblockId);
internal sealed class LandblockReplacementBuilder : IDisposable
{
private readonly CellGraph _active;
private readonly CellGraph _staging;
private readonly uint _prefix;
private readonly List> _envCells = new();
private readonly HashSet _stagingIds = new();
private readonly List _removeIds = new();
private List? _keySlots;
private int _keySlotLimit;
private bool _keySlotsCaptured;
private int _cursor;
private int _phase;
internal LandblockReplacementBuilder(
CellGraph active,
CellGraph staging,
uint landblockId)
{
_active = active;
_staging = staging;
_prefix = landblockId & 0xFFFF0000u;
}
internal bool Advance()
{
if (_phase == 0)
{
// O1: enumerate the staging root's installed target-prefix
// EnvCell keys via the ledger instead of scanning the map.
if (TryTakeNextPrefixKey(
_staging._collisionWorld.Current.EnvCellKeys,
out uint stagingId))
{
if ((stagingId & 0xFFFFu) >= 0x0100u
&& _staging._envCells.TryGetValue(
stagingId,
out EnvCell? cell))
{
_envCells.Add(
new KeyValuePair(stagingId, cell));
_stagingIds.Add(stagingId);
}
return false;
}
_phase = 1;
return false;
}
if (_phase == 1)
{
// O1: active-side removal capture through the ledger — no
// cross-frame live enumerator over the active map.
if (TryTakeNextPrefixKey(
_active._collisionWorld.Current.EnvCellKeys,
out uint activeId))
{
if ((activeId & 0xFFFFu) >= 0x0100u
&& !_stagingIds.Contains(activeId)
&& _active._envCells.ContainsKey(activeId))
{
_removeIds.Add(activeId);
}
return false;
}
bool hasTerrain = _staging._terrain.TryGetValue(
_prefix,
out var terrain);
Prepared = new PreparedCellGraphLandblock(
_prefix,
_removeIds,
_envCells,
hasTerrain,
terrain);
_phase = 2;
}
return true;
}
private bool TryTakeNextPrefixKey(PrefixKeyIndex ledger, out uint key)
{
if (!_keySlotsCaptured)
{
_keySlots = ledger.SlotsForPrefix(_prefix);
_keySlotLimit = _keySlots?.Count ?? 0;
_keySlotsCaptured = true;
_cursor = 0;
}
while (_cursor < _keySlotLimit)
{
uint candidate = _keySlots![_cursor++];
if (candidate != 0u)
{
key = candidate;
return true;
}
}
key = 0u;
_keySlots = null;
_keySlotsCaptured = false;
return false;
}
internal PreparedCellGraphLandblock? Prepared { get; private set; }
public void Dispose()
{
_keySlots = null;
_keySlotsCaptured = false;
}
}
}
internal sealed record PreparedCellGraphLandblock(
uint LandblockPrefix,
IReadOnlyList EnvCellIdsToRemove,
IReadOnlyList> EnvCells,
bool HasTerrain,
CellGraphTerrain? Terrain);
internal sealed record CellGraphTerrain(
TerrainSurface Terrain,
Vector3 Origin);