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);