acdream/src/AcDream.Core/Physics/CollisionWorldState.cs
Erik 75ea269d35 feat(physics): S2 chunk 4 - movement publishes from the transition's cells; children inherit at registration
Movement: CommitSetPosition's RefreshPositionRows/ReplacePositionRows and
the staged apply publish the retail render product from the exact cell
list collision just used, the transition's cell_array retail feeds
add_shadows_to_cells in CPhysicsObj::SetPositionInternal @0x00515330
(pseudo-C 283526-283539); the separate move-path bbox recompute is deleted.
calc_cross_cells @0x00515230 stays the distinct full-recompute path
(PhysicsShadowCommitAction.Recalculate).

Children (Contract B recursion): ShadowObjectRegistry.AttachChild/DetachChild
give an attached object the root's current cells as part entries only,
republished whenever the root's array changes, detached at withdrawal and
cascaded from the root's Deregister; nested attachment resolves to the root
with a bounded, cycle-safe chain. EquippedChildRenderController attaches at
realization (FromSetupRenderParts over the child's Setup) and detaches at
its single removal funnel. WalkProductionWorldData's dynamic sweep reads
TryGetRetailCellArray directly; the 64-hop parent-chain walk and its
FindParentLocalId plumbing are deleted. CollisionWorldState.Clear now
also clears the retail products.

Gates (implementer's isolated worktree at identical content): Release
build 0/0; Core 4,970/4,970; App hermetic 6,761/6,761; targeted
walk/child/live-entity/placement/comparator 166/166; Runtime 1,884/1,884.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:37:34 +02:00

321 lines
12 KiB
C#

using System.Collections.Concurrent;
using AcDream.Core.World.Cells;
namespace AcDream.Core.Physics;
/// <summary>
/// Per-landblock installed-key ledger for one collision-world map. Slot lists
/// mirror the shadow registry's prefix-owner-slot idiom: removal tombstones a
/// slot (key 0) so an in-flight metered cursor retains its captured list
/// reference and observes only tombstones, and an emptied container is
/// reclaimed so a future install gets a fresh compact list. Maintained by the
/// <see cref="CollisionWorldState"/> typed install/remove helpers; consumed by
/// the landblock-replacement seal so capturing one prefix's keys never scans
/// the whole resident world (O1 of the 2026-08-02 collision
/// publication-throughput fix).
/// </summary>
internal sealed class PrefixKeyIndex
{
private readonly Dictionary<uint, List<uint>> _slots = new();
private readonly Dictionary<uint, Dictionary<uint, int>> _indices = new();
private readonly Dictionary<uint, Stack<int>> _freeSlots = new();
internal void Add(uint key)
{
uint prefix = key & 0xFFFF0000u;
if (!_slots.TryGetValue(prefix, out List<uint>? slots))
{
slots = new List<uint>();
_slots[prefix] = slots;
_indices[prefix] = new Dictionary<uint, int>();
_freeSlots[prefix] = new Stack<int>();
}
Dictionary<uint, int> indices = _indices[prefix];
if (indices.ContainsKey(key))
return;
if (_freeSlots[prefix].TryPop(out int freeIndex))
{
slots[freeIndex] = key;
indices[key] = freeIndex;
return;
}
indices[key] = slots.Count;
slots.Add(key);
}
internal void Remove(uint key)
{
uint prefix = key & 0xFFFF0000u;
if (!_indices.TryGetValue(prefix, out Dictionary<uint, int>? indices)
|| !indices.Remove(key, out int slotIndex))
{
return;
}
_slots[prefix][slotIndex] = 0u;
_freeSlots[prefix].Push(slotIndex);
if (indices.Count != 0)
return;
// An in-flight seal cursor retains its captured List reference and
// observes only tombstones. A future install gets a fresh container.
_slots.Remove(prefix);
_indices.Remove(prefix);
_freeSlots.Remove(prefix);
}
/// <summary>
/// The live slot list for one landblock prefix, or null when no key is
/// installed. Callers capture the reference plus <c>Count</c> once and
/// iterate by index, skipping tombstone slots (key 0).
/// </summary>
internal List<uint>? SlotsForPrefix(uint prefix) =>
_slots.TryGetValue(prefix & 0xFFFF0000u, out List<uint>? slots)
? slots
: null;
internal int InstalledKeyCountForPrefix(uint prefix) =>
_indices.TryGetValue(prefix & 0xFFFF0000u, out var indices)
? indices.Count
: 0;
}
/// <summary>
/// One exclusive-by-ownership collision-world root. A preparation mutates only
/// its private root; activation transfers the complete root through one volatile
/// reference publication shared by every collision facade.
/// </summary>
internal sealed class CollisionWorldState
{
internal Dictionary<uint, PhysicsEngine.LandblockPhysics> Landblocks { get; } = new();
internal List<uint> LandblockSlots { get; } = new();
internal Dictionary<uint, int> LandblockIndices { get; } = new();
internal Stack<int> LandblockFreeSlots { get; } = new();
internal ConcurrentDictionary<uint, CellPhysics> CellStruct { get; } = new();
internal ConcurrentDictionary<uint, FlatCellStructureCollisionAsset>
FlatCellStruct { get; } = new();
internal ConcurrentDictionary<uint, FlatEnvCellTopology> FlatEnvCell { get; } = new();
internal ConcurrentDictionary<uint, BuildingPhysics> Buildings { get; } = new();
internal ConcurrentDictionary<uint, EnvCell> EnvCells { get; } = new();
internal ConcurrentDictionary<uint, CellGraphTerrain> Terrain { get; } = new();
internal ConcurrentDictionary<uint, ObjCell> OutdoorCells { get; } = new();
internal Dictionary<uint, List<ShadowEntry>> ShadowCells { get; } = new();
internal Dictionary<uint, List<uint>> ShadowEntityCells { get; } = new();
internal HashSet<uint> SuspendedShadowEntities { get; } = new();
internal Dictionary<uint, List<uint>> SuspendedShadowEntityCells { get; } = new();
internal Dictionary<uint, HashSet<uint>> WithdrawnPrefixesByOwner { get; } = new();
internal Dictionary<uint, IReadOnlyList<ShadowShape>> ShadowEntityShapes { get; } = new();
internal Dictionary<uint, ShadowObjectRegistry.RegistrationRecord>
ShadowEntityRegistrations { get; } = new();
internal Dictionary<uint, ulong> ShadowOwnerVersions { get; } = new();
internal Dictionary<uint, HashSet<uint>> ShadowOwnerPrefixes { get; } = new();
internal Dictionary<uint, List<uint>> ShadowPrefixOwnerSlots { get; } = new();
internal Dictionary<uint, Dictionary<uint, int>> ShadowPrefixOwnerIndices { get; } = new();
internal Dictionary<uint, Stack<int>> ShadowPrefixFreeSlots { get; } = new();
internal List<uint> ShadowOwnerSlots { get; } = new();
internal Dictionary<uint, int> ShadowOwnerIndices { get; } = new();
internal Stack<int> ShadowOwnerFreeSlots { get; } = new();
// ── Campaign OVERHAUL S2 chunk 1 ────────────────────────────────────────
// Retail's whole-part-array CELLARRAY (CPhysicsObj::calc_cross_cells_static
// 0x00515160) and its sibling per-cell part-entry product
// (CPartArray::AddPartsShadow 0x00517e40), retained beside the existing
// ShadowEntity* fields. A NEW product only: nothing above touches it,
// and nothing in production reads it yet (see ShadowObjectRegistry).
internal Dictionary<uint, IReadOnlyList<ShadowShape>>
ShadowEntityRetailPartArrays { get; } = new();
internal Dictionary<uint, List<uint>> ShadowEntityRetailCellArrays { get; } = new();
internal Dictionary<uint, RetailCellArrayRoute>
ShadowEntityRetailCellArrayRoutes { get; } = new();
internal Dictionary<uint, List<RetailPartEntry>> RetailPartEntriesByCell { get; } = new();
// ── Campaign OVERHAUL S2 chunk 4 ────────────────────────────────────────
// Retail's add_shadows_to_cells (0x00514ae0) child-inheritance recursion
// (oh1-construction-landscape-contract.md Contract B): an attached
// projection (equipped weapon/shield/ammunition) owns no independent
// collision shapes and inherits its root's retail CELLARRAY instead of
// flooding its own. See ShadowObjectRegistry.AttachChild/DetachChild.
internal Dictionary<uint, uint> ShadowChildParent { get; } = new();
internal Dictionary<uint, List<uint>> ShadowParentChildren { get; } = new();
internal Dictionary<uint, IReadOnlyList<ShadowShape>>
ShadowChildPartArrays { get; } = new();
// ── O1 per-prefix installed-key ledgers ────────────────────────────────
// Every mutation of the five landblock-scoped world maps goes through the
// typed helpers below so these ledgers stay exact. The seal's landblock-
// replacement builders enumerate one prefix's keys instead of scanning the
// whole resident map, and the retirement/removal paths retire one prefix
// in O(prefix keys).
internal PrefixKeyIndex CellStructKeys { get; } = new();
internal PrefixKeyIndex FlatCellStructKeys { get; } = new();
internal PrefixKeyIndex FlatEnvCellKeys { get; } = new();
internal PrefixKeyIndex BuildingKeys { get; } = new();
internal PrefixKeyIndex EnvCellKeys { get; } = new();
internal void SetCellStruct(uint id, CellPhysics value)
{
CellStruct[id] = value;
CellStructKeys.Add(id);
}
internal bool TryAddCellStruct(uint id, CellPhysics value)
{
if (!CellStruct.TryAdd(id, value))
return false;
CellStructKeys.Add(id);
return true;
}
internal bool RemoveCellStruct(uint id)
{
if (!CellStruct.TryRemove(id, out _))
return false;
CellStructKeys.Remove(id);
return true;
}
internal void SetFlatCellStruct(uint id, FlatCellStructureCollisionAsset value)
{
FlatCellStruct[id] = value;
FlatCellStructKeys.Add(id);
}
internal bool TryAddFlatCellStruct(uint id, FlatCellStructureCollisionAsset value)
{
if (!FlatCellStruct.TryAdd(id, value))
return false;
FlatCellStructKeys.Add(id);
return true;
}
internal bool RemoveFlatCellStruct(uint id)
{
if (!FlatCellStruct.TryRemove(id, out _))
return false;
FlatCellStructKeys.Remove(id);
return true;
}
internal void SetFlatEnvCell(uint id, FlatEnvCellTopology value)
{
FlatEnvCell[id] = value;
FlatEnvCellKeys.Add(id);
}
internal bool TryAddFlatEnvCell(uint id, FlatEnvCellTopology value)
{
if (!FlatEnvCell.TryAdd(id, value))
return false;
FlatEnvCellKeys.Add(id);
return true;
}
internal bool RemoveFlatEnvCell(uint id)
{
if (!FlatEnvCell.TryRemove(id, out _))
return false;
FlatEnvCellKeys.Remove(id);
return true;
}
internal void SetBuilding(uint id, BuildingPhysics value)
{
Buildings[id] = value;
BuildingKeys.Add(id);
}
internal bool TryAddBuilding(uint id, BuildingPhysics value)
{
if (!Buildings.TryAdd(id, value))
return false;
BuildingKeys.Add(id);
return true;
}
internal bool RemoveBuilding(uint id)
{
if (!Buildings.TryRemove(id, out _))
return false;
BuildingKeys.Remove(id);
return true;
}
internal void SetEnvCell(uint id, EnvCell value)
{
EnvCells[id] = value;
EnvCellKeys.Add(id);
}
internal bool TryAddEnvCell(uint id, EnvCell value)
{
if (!EnvCells.TryAdd(id, value))
return false;
EnvCellKeys.Add(id);
return true;
}
internal bool RemoveEnvCell(uint id)
{
if (!EnvCells.TryRemove(id, out _))
return false;
EnvCellKeys.Remove(id);
return true;
}
}
/// <summary>
/// Stable indirection shared by PhysicsEngine, PhysicsDataCache, CellGraph,
/// and ShadowObjectRegistry. Readers observe either complete root, never a
/// mixture assembled by several facade assignments.
/// </summary>
internal sealed class CollisionWorldStateSlot
{
private CollisionWorldState? _current = new();
private bool _revoked;
internal CollisionWorldStateSlot()
{
}
internal CollisionWorldStateSlot(CollisionWorldState current)
{
_current = current ?? throw new ArgumentNullException(nameof(current));
}
internal CollisionWorldState Current
{
get
{
if (_revoked)
throw new ObjectDisposedException("Transferred collision generation");
return Volatile.Read(ref _current)
?? throw new ObjectDisposedException("Transferred collision generation");
}
}
internal CollisionWorldState TransferTo(CollisionWorldStateSlot destination)
{
ArgumentNullException.ThrowIfNull(destination);
if (_revoked)
throw new ObjectDisposedException("Transferred collision generation");
CollisionWorldState transferred = _current
?? throw new ObjectDisposedException("Transferred collision generation");
_revoked = true;
Volatile.Write(ref destination._current, transferred);
_current = null;
return transferred;
}
/// <summary>
/// O2 (2026-08-02): terminally revokes a consumed staging root. The
/// per-landblock delta commit installs the staged content into the active
/// root instead of swapping roots, so the staging root no longer becomes
/// the active root — but a committed preparation must still lose access to
/// its private world exactly as the old transfer revoked it.
/// </summary>
internal void Revoke()
{
_revoked = true;
_current = null;
}
internal CollisionWorldState Capture() => Current;
}