fix(physics): make collision activation starvation-free
This commit is contained in:
parent
d94145e6b8
commit
6b28ff999c
14 changed files with 4637 additions and 474 deletions
|
|
@ -265,6 +265,13 @@ public sealed class LandblockPhysicsPublisher
|
|||
if (publication.PreparationCommitted)
|
||||
return true;
|
||||
|
||||
if (!_physics.AdvanceCollisionGenerationPreparation(
|
||||
publication.CollisionAdmission,
|
||||
publication.PreparedGeneration).Completed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<WorldEntity> entities =
|
||||
publication.Build.Landblock.Entities;
|
||||
if (publication.Build.Collisions is { } collisions)
|
||||
|
|
@ -548,7 +555,13 @@ public sealed class LandblockPhysicsPublisher
|
|||
publication.RefloodCommitted = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
// Seal reconciliation and the zero-work root transfer are one host
|
||||
// update-thread transaction. Deferring this commit to the next frame
|
||||
// would let continuously moving, unrelated live owners dirty their
|
||||
// already-visited journal slots forever even though each metered replay
|
||||
// had just caught up exactly.
|
||||
if (publication.SealCommitted && !publication.CompletionCommitted)
|
||||
{
|
||||
RuntimeCollisionGenerationCommit commit =
|
||||
_physics.CommitCollisionGeneration(
|
||||
|
|
@ -556,12 +569,9 @@ public sealed class LandblockPhysicsPublisher
|
|||
publication.PreparedGeneration);
|
||||
if (!commit.Committed)
|
||||
{
|
||||
_physics.RestartCollisionRetainedOwnerCapture(
|
||||
publication.CollisionAdmission,
|
||||
publication.PreparedGeneration);
|
||||
publication.RefloodOwnerIds = null;
|
||||
publication.RefloodCursor = 0;
|
||||
publication.RefloodCommitted = false;
|
||||
// Runtime coalesces post-seal arrivals in its owner journal.
|
||||
// Resume that seal tail rather than restarting the
|
||||
// completed generation-wide capture/reflood pass.
|
||||
publication.SealCommitted = false;
|
||||
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||
return false;
|
||||
|
|
|
|||
86
src/AcDream.Core/Physics/CollisionWorldState.cs
Normal file
86
src/AcDream.Core/Physics/CollisionWorldState.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System.Collections.Concurrent;
|
||||
using AcDream.Core.World.Cells;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <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, 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();
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
internal CollisionWorldState Capture() => Current;
|
||||
}
|
||||
|
|
@ -23,18 +23,20 @@ public sealed class PhysicsDataCache
|
|||
{
|
||||
private readonly bool _requirePreparedCollision;
|
||||
private PhysicsDataCache? _readFallback;
|
||||
private readonly CollisionWorldStateSlot _collisionWorld;
|
||||
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
|
||||
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
||||
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
||||
private readonly ConcurrentDictionary<uint, CellPhysics> _cellStruct = new();
|
||||
private ConcurrentDictionary<uint, CellPhysics> _cellStruct =>
|
||||
_collisionWorld.Current.CellStruct;
|
||||
private readonly ConcurrentDictionary<uint, FlatGfxObjCollisionAsset>
|
||||
_flatGfxObj = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatSetupCollision>
|
||||
_flatSetup = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatCellStructureCollisionAsset>
|
||||
_flatCellStruct = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatEnvCellTopology>
|
||||
_flatEnvCell = new();
|
||||
private ConcurrentDictionary<uint, FlatCellStructureCollisionAsset>
|
||||
_flatCellStruct => _collisionWorld.Current.FlatCellStruct;
|
||||
private ConcurrentDictionary<uint, FlatEnvCellTopology>
|
||||
_flatEnvCell => _collisionWorld.Current.FlatEnvCell;
|
||||
|
||||
public PhysicsDataCache()
|
||||
: this(requirePreparedCollision: false)
|
||||
|
|
@ -42,8 +44,18 @@ public sealed class PhysicsDataCache
|
|||
}
|
||||
|
||||
private PhysicsDataCache(bool requirePreparedCollision)
|
||||
: this(requirePreparedCollision, new CollisionWorldStateSlot())
|
||||
{
|
||||
}
|
||||
|
||||
private PhysicsDataCache(
|
||||
bool requirePreparedCollision,
|
||||
CollisionWorldStateSlot collisionWorld)
|
||||
{
|
||||
_requirePreparedCollision = requirePreparedCollision;
|
||||
_collisionWorld = collisionWorld
|
||||
?? throw new ArgumentNullException(nameof(collisionWorld));
|
||||
CellGraph = new UcgCellGraph(_collisionWorld);
|
||||
if (!requirePreparedCollision
|
||||
&& PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||
{
|
||||
|
|
@ -67,6 +79,18 @@ public sealed class PhysicsDataCache
|
|||
return cache;
|
||||
}
|
||||
|
||||
internal static PhysicsDataCache CreateProduction(
|
||||
CollisionWorldStateSlot collisionWorld)
|
||||
{
|
||||
var cache = new PhysicsDataCache(
|
||||
requirePreparedCollision: true,
|
||||
collisionWorld)
|
||||
{
|
||||
CollisionTraversalMode = CollisionTraversalMode.Flat,
|
||||
};
|
||||
return cache;
|
||||
}
|
||||
|
||||
internal CollisionShadowVerifier? CollisionShadow { get; set; }
|
||||
|
||||
public CollisionShadowStats CollisionShadowStats =>
|
||||
|
|
@ -80,7 +104,8 @@ public sealed class PhysicsDataCache
|
|||
CollisionTraversalMode.Graph;
|
||||
|
||||
// ── Phase 2: building portal cache for outdoor→indoor entry ───────────
|
||||
private readonly ConcurrentDictionary<uint, BuildingPhysics> _buildings = new();
|
||||
private ConcurrentDictionary<uint, BuildingPhysics> _buildings =>
|
||||
_collisionWorld.Current.Buildings;
|
||||
|
||||
/// <summary>
|
||||
/// The unified cell graph (UCG): the active id->cell resolver and registry.
|
||||
|
|
@ -94,31 +119,23 @@ public sealed class PhysicsDataCache
|
|||
/// (<c>TryGetTerrainOrigin</c>, read by <c>CellTransit</c>'s pick + transit
|
||||
/// paths). No longer inert.
|
||||
/// </summary>
|
||||
public UcgCellGraph CellGraph { get; private set; } = new();
|
||||
public UcgCellGraph CellGraph { get; }
|
||||
|
||||
internal CollisionWorldStateSlot CollisionWorld => _collisionWorld;
|
||||
|
||||
/// <summary>
|
||||
/// Copies the currently committed immutable collision records into an
|
||||
/// off-side cache. Streaming may replace one landblock in this copy over
|
||||
/// many frames without exposing a partially withdrawn cell graph to live
|
||||
/// physics queries.
|
||||
/// Creates the empty off-side facade used by Runtime's metered root
|
||||
/// materializer. Global immutable catalogs fall through to this cache;
|
||||
/// mutable world topology is installed one leaf per host step.
|
||||
/// </summary>
|
||||
internal PhysicsDataCache CreateCollisionStagingCopy()
|
||||
internal PhysicsDataCache CreateEmptyCollisionStaging(
|
||||
CollisionWorldStateSlot collisionWorld)
|
||||
{
|
||||
var copy = new PhysicsDataCache(_requirePreparedCollision)
|
||||
return new PhysicsDataCache(_requirePreparedCollision, collisionWorld)
|
||||
{
|
||||
CollisionTraversalMode = CollisionTraversalMode,
|
||||
CellGraph = CellGraph.CreateCollisionStagingCopy(),
|
||||
_readFallback = this,
|
||||
};
|
||||
// Global immutable GfxObj/Setup records are not copied wholesale.
|
||||
// The accepted build's exact closure is staged cursor-by-cursor below;
|
||||
// copying the process-retained asset catalog here would turn every
|
||||
// landblock publication into an unbounded frame spike.
|
||||
CopyDictionary(_cellStruct, copy._cellStruct);
|
||||
CopyDictionary(_flatCellStruct, copy._flatCellStruct);
|
||||
CopyDictionary(_flatEnvCell, copy._flatEnvCell);
|
||||
CopyDictionary(_buildings, copy._buildings);
|
||||
return copy;
|
||||
}
|
||||
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
|
|
@ -132,56 +149,6 @@ public sealed class PhysicsDataCache
|
|||
gfxObjectIds,
|
||||
setupIds);
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedPhysicsDataCacheLandblock replacement)
|
||||
{
|
||||
RemoveEntries(_cellStruct, replacement.CellIdsToRemove);
|
||||
RemoveEntries(_flatCellStruct, replacement.FlatCellIdsToRemove);
|
||||
RemoveEntries(_flatEnvCell, replacement.FlatEnvCellIdsToRemove);
|
||||
RemoveEntries(_buildings, replacement.BuildingIdsToRemove);
|
||||
CommitEntries(_gfxObj, replacement.GfxObjects, replace: false);
|
||||
CommitEntries(_visualBounds, replacement.VisualBounds, replace: false);
|
||||
CommitEntries(_flatGfxObj, replacement.FlatGfxObjects, replace: false);
|
||||
CommitEntries(_setup, replacement.Setups, replace: false);
|
||||
CommitEntries(_flatSetup, replacement.FlatSetups, replace: false);
|
||||
CommitEntries(_cellStruct, replacement.Cells, replace: true);
|
||||
CommitEntries(_flatCellStruct, replacement.FlatCells, replace: true);
|
||||
CommitEntries(_flatEnvCell, replacement.FlatEnvCells, replace: true);
|
||||
CommitEntries(_buildings, replacement.Buildings, replace: true);
|
||||
CellGraph.CommitLandblockReplacement(replacement.CellGraph);
|
||||
}
|
||||
|
||||
private static void CopyDictionary<T>(
|
||||
ConcurrentDictionary<uint, T> source,
|
||||
ConcurrentDictionary<uint, T> destination)
|
||||
{
|
||||
foreach ((uint id, T value) in source)
|
||||
destination.TryAdd(id, value);
|
||||
}
|
||||
|
||||
private static void CommitEntries<T>(
|
||||
ConcurrentDictionary<uint, T> destination,
|
||||
IReadOnlyList<KeyValuePair<uint, T>> entries,
|
||||
bool replace)
|
||||
{
|
||||
for (int index = 0; index < entries.Count; index++)
|
||||
{
|
||||
(uint id, T value) = entries[index];
|
||||
if (replace)
|
||||
destination[id] = value;
|
||||
else
|
||||
destination.TryAdd(id, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveEntries<T>(
|
||||
ConcurrentDictionary<uint, T> destination,
|
||||
IReadOnlyList<uint> ids)
|
||||
{
|
||||
for (int index = 0; index < ids.Count; index++)
|
||||
destination.TryRemove(ids[index], out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract and cache the physics BSP + polygon data from a GfxObj,
|
||||
/// PLUS always cache a visual AABB from the vertex data regardless of
|
||||
|
|
@ -1085,6 +1052,9 @@ public sealed class PhysicsDataCache
|
|||
if (_cursor < _gfxIds.Length)
|
||||
{
|
||||
uint id = _gfxIds[_cursor++];
|
||||
Preinstall(_staging._gfxObj, _active._gfxObj, id);
|
||||
Preinstall(_staging._visualBounds, _active._visualBounds, id);
|
||||
Preinstall(_staging._flatGfxObj, _active._flatGfxObj, id);
|
||||
Capture(_staging._gfxObj, id, _gfx);
|
||||
Capture(_staging._visualBounds, id, _bounds);
|
||||
Capture(_staging._flatGfxObj, id, _flatGfx);
|
||||
|
|
@ -1098,6 +1068,8 @@ public sealed class PhysicsDataCache
|
|||
if (_cursor < _setupIds.Length)
|
||||
{
|
||||
uint id = _setupIds[_cursor++];
|
||||
Preinstall(_staging._setup, _active._setup, id);
|
||||
Preinstall(_staging._flatSetup, _active._flatSetup, id);
|
||||
Capture(_staging._setup, id, _setups);
|
||||
Capture(_staging._flatSetup, id, _flatSetups);
|
||||
WorkUnits++;
|
||||
|
|
@ -1229,6 +1201,15 @@ public sealed class PhysicsDataCache
|
|||
destination.Add(new KeyValuePair<uint, T>(id, value));
|
||||
}
|
||||
|
||||
private static void Preinstall<T>(
|
||||
ConcurrentDictionary<uint, T> source,
|
||||
ConcurrentDictionary<uint, T> destination,
|
||||
uint id)
|
||||
{
|
||||
if (source.TryGetValue(id, out T? value))
|
||||
destination.TryAdd(id, value);
|
||||
}
|
||||
|
||||
private static bool CapturePrefixOne<T>(
|
||||
IEnumerator<KeyValuePair<uint, T>> enumerator,
|
||||
uint prefix,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.World.Cells;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
|
|
@ -28,7 +29,15 @@ internal readonly record struct TerrainWalkableSample(
|
|||
/// </summary>
|
||||
public sealed class PhysicsEngine
|
||||
{
|
||||
private readonly Dictionary<uint, LandblockPhysics> _landblocks = new();
|
||||
private CollisionWorldStateSlot _collisionWorld;
|
||||
private Dictionary<uint, LandblockPhysics> _landblocks =>
|
||||
_collisionWorld.Current.Landblocks;
|
||||
private List<uint> _landblockSlots =>
|
||||
_collisionWorld.Current.LandblockSlots;
|
||||
private Dictionary<uint, int> _landblockIndices =>
|
||||
_collisionWorld.Current.LandblockIndices;
|
||||
private Stack<int> _landblockFreeSlots =>
|
||||
_collisionWorld.Current.LandblockFreeSlots;
|
||||
private readonly TransitionScratchArena? _transitionScratch;
|
||||
|
||||
public PhysicsEngine()
|
||||
|
|
@ -43,6 +52,8 @@ public sealed class PhysicsEngine
|
|||
/// </summary>
|
||||
internal PhysicsEngine(bool reuseTransitionScratch)
|
||||
{
|
||||
_collisionWorld = new CollisionWorldStateSlot();
|
||||
ShadowObjects = new ShadowObjectRegistry(_collisionWorld);
|
||||
_transitionScratch = reuseTransitionScratch
|
||||
? new TransitionScratchArena()
|
||||
: null;
|
||||
|
|
@ -57,6 +68,8 @@ public sealed class PhysicsEngine
|
|||
/// <summary>Number of registered landblocks (diagnostic).</summary>
|
||||
public int LandblockCount => _landblocks.Count;
|
||||
|
||||
internal CollisionWorldStateSlot CollisionWorld => _collisionWorld;
|
||||
|
||||
/// <summary>
|
||||
/// Optional high-volume collision trace sink. Production leaves this
|
||||
/// unset; focused diagnostic gates may opt in explicitly.
|
||||
|
|
@ -85,7 +98,7 @@ public sealed class PhysicsEngine
|
|||
public bool IsLandblockTerrainResident(uint cellOrLandblockId)
|
||||
{
|
||||
uint prefix = cellOrLandblockId & 0xFFFF0000u;
|
||||
foreach (var key in _landblocks.Keys)
|
||||
foreach ((uint key, _) in _landblocks)
|
||||
if ((key & 0xFFFF0000u) == prefix) return true;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -103,7 +116,8 @@ public sealed class PhysicsEngine
|
|||
public bool IsNeighborhoodTerrainResident(uint cellOrLandblockId, int radius)
|
||||
{
|
||||
var resident = new HashSet<uint>();
|
||||
foreach (var key in _landblocks.Keys) resident.Add(key & 0xFFFF0000u);
|
||||
foreach ((uint key, _) in _landblocks)
|
||||
resident.Add(key & 0xFFFF0000u);
|
||||
|
||||
int cx = (int)((cellOrLandblockId >> 24) & 0xFFu);
|
||||
int cy = (int)((cellOrLandblockId >> 16) & 0xFFu);
|
||||
|
|
@ -122,7 +136,7 @@ public sealed class PhysicsEngine
|
|||
/// Cell-based spatial index for static object collision.
|
||||
/// Populated during landblock streaming; queried by the Transition system.
|
||||
/// </summary>
|
||||
public ShadowObjectRegistry ShadowObjects { get; } = new();
|
||||
public ShadowObjectRegistry ShadowObjects { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Physics BSP cache shared with the streaming loader. Set once by the
|
||||
|
|
@ -135,10 +149,61 @@ public sealed class PhysicsEngine
|
|||
public PhysicsDataCache? DataCache
|
||||
{
|
||||
get => _dataCache;
|
||||
set { _dataCache = value; ShadowObjects.DataCache = value; }
|
||||
set
|
||||
{
|
||||
if (value is not null
|
||||
&& !ReferenceEquals(_collisionWorld, value.CollisionWorld))
|
||||
{
|
||||
if (ShadowObjects.TotalRegistered != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A populated physics engine cannot change collision roots.");
|
||||
}
|
||||
// Legacy/test construction commonly installs terrain before
|
||||
// attaching its first cache. Preserve that one-time ordering
|
||||
// without permitting a live populated root swap: move only
|
||||
// the engine-owned landblock index into the still-private
|
||||
// cache root, then bind the stable facades.
|
||||
if (_dataCache is null && _landblocks.Count != 0)
|
||||
CopyDetachedLandblocksTo(value.CollisionWorld);
|
||||
else if (_landblocks.Count != 0)
|
||||
throw new InvalidOperationException(
|
||||
"A populated physics engine cannot change collision roots.");
|
||||
_collisionWorld = value.CollisionWorld;
|
||||
ShadowObjects.AttachCollisionWorld(_collisionWorld);
|
||||
}
|
||||
_dataCache = value;
|
||||
ShadowObjects.DataCache = value;
|
||||
}
|
||||
}
|
||||
private PhysicsDataCache? _dataCache;
|
||||
|
||||
private void CopyDetachedLandblocksTo(
|
||||
CollisionWorldStateSlot destinationSlot)
|
||||
{
|
||||
CollisionWorldState source = _collisionWorld.Current;
|
||||
CollisionWorldState destination = destinationSlot.Current;
|
||||
if (destination.Landblocks.Count != 0
|
||||
|| destination.LandblockSlots.Count != 0
|
||||
|| destination.LandblockIndices.Count != 0
|
||||
|| destination.LandblockFreeSlots.Count != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A cache collision root already owns engine landblocks.");
|
||||
}
|
||||
foreach ((uint landblockId, LandblockPhysics landblock) in
|
||||
source.Landblocks)
|
||||
{
|
||||
destination.Landblocks.Add(landblockId, landblock);
|
||||
}
|
||||
destination.LandblockSlots.AddRange(source.LandblockSlots);
|
||||
foreach ((uint landblockId, int slot) in source.LandblockIndices)
|
||||
destination.LandblockIndices.Add(landblockId, slot);
|
||||
int[] freeSlots = source.LandblockFreeSlots.ToArray();
|
||||
for (int index = freeSlots.Length - 1; index >= 0; index--)
|
||||
destination.LandblockFreeSlots.Push(freeSlots[index]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AP-129 (Campaign P Slice P4 review fix, 2026-07-30): optional live
|
||||
/// weenie-object table, consulted ONLY by
|
||||
|
|
@ -165,21 +230,16 @@ public sealed class PhysicsEngine
|
|||
/// Streaming modifies this copy only; the active engine and its borrowed
|
||||
/// cache/registry identities remain stable until Runtime commits.
|
||||
/// </summary>
|
||||
internal PhysicsEngine CreateCollisionStagingCopy(
|
||||
PhysicsDataCache stagingCache)
|
||||
internal CollisionStagingBuilder CreateCollisionStagingBuilder(
|
||||
uint targetLandblockId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stagingCache);
|
||||
var staging = new PhysicsEngine
|
||||
{
|
||||
DataCache = stagingCache,
|
||||
Objects = Objects,
|
||||
};
|
||||
foreach ((uint id, LandblockPhysics landblock) in _landblocks)
|
||||
staging._landblocks[id] = landblock;
|
||||
staging.ShadowObjects.CopyCollisionStateFrom(
|
||||
ShadowObjects,
|
||||
stagingCache);
|
||||
return staging;
|
||||
PhysicsDataCache activeCache = DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Active collision engine has no data cache.");
|
||||
return new CollisionStagingBuilder(
|
||||
this,
|
||||
activeCache,
|
||||
targetLandblockId & 0xFFFF0000u);
|
||||
}
|
||||
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
|
|
@ -187,7 +247,7 @@ public sealed class PhysicsEngine
|
|||
uint landblockId,
|
||||
uint[] gfxObjectIds,
|
||||
uint[] setupIds,
|
||||
IReadOnlyDictionary<uint, ulong> expectedRetainedVersions)
|
||||
IReadOnlyList<uint> expectedRetainedOwners)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(staging);
|
||||
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
|
|
@ -204,6 +264,7 @@ public sealed class PhysicsEngine
|
|||
return new LandblockReplacementBuilder(
|
||||
canonical,
|
||||
landblock,
|
||||
staging,
|
||||
(DataCache ?? throw new InvalidOperationException(
|
||||
"Active collision engine has no data cache."))
|
||||
.CreateLandblockReplacementBuilder(
|
||||
|
|
@ -214,17 +275,721 @@ public sealed class PhysicsEngine
|
|||
ShadowObjects.CreateLandblockReplacementBuilder(
|
||||
staging.ShadowObjects,
|
||||
canonical,
|
||||
expectedRetainedVersions));
|
||||
expectedRetainedOwners));
|
||||
}
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedPhysicsEngineLandblock replacement)
|
||||
{
|
||||
(DataCache ?? throw new InvalidOperationException(
|
||||
"Active collision engine has no data cache."))
|
||||
.CommitLandblockReplacement(replacement.DataCache);
|
||||
_landblocks[replacement.LandblockId] = replacement.Landblock;
|
||||
ShadowObjects.CommitLandblockReplacement(replacement.Shadows);
|
||||
PhysicsDataCache activeCache = DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Active collision engine has no data cache.");
|
||||
PhysicsDataCache stagingCache = replacement.Staging.DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Staging collision engine has no data cache.");
|
||||
uint activeCurrentCellId = activeCache.CellGraph.CurrCell?.Id ?? 0u;
|
||||
stagingCache.CollisionWorld.TransferTo(activeCache.CollisionWorld);
|
||||
if ((activeCurrentCellId & 0xFFFF0000u)
|
||||
== (replacement.LandblockId & 0xFFFF0000u))
|
||||
{
|
||||
activeCache.CellGraph.CurrCell =
|
||||
activeCache.CellGraph.GetVisible(activeCurrentCellId);
|
||||
}
|
||||
}
|
||||
|
||||
internal LandblockReplacementApplyCursor
|
||||
CreateLandblockReplacementApplyCursor(
|
||||
PreparedPhysicsEngineLandblock replacement) =>
|
||||
new(this, replacement);
|
||||
|
||||
internal readonly record struct LandblockReplacementApplyStep(
|
||||
bool Completed,
|
||||
bool Worked,
|
||||
bool HasOwner,
|
||||
uint OwnerId);
|
||||
|
||||
internal LandblockRetirementCursor CreateLandblockRetirementCursor(
|
||||
PhysicsEngine authoritative,
|
||||
uint landblockId,
|
||||
bool withdraw) => new(
|
||||
this,
|
||||
authoritative,
|
||||
landblockId,
|
||||
withdraw);
|
||||
|
||||
/// <summary>
|
||||
/// Applies one demotion/withdrawal to an off-side root without a whole-
|
||||
/// world synchronous scan. Every advance inspects or mutates at most one
|
||||
/// stable owner slot, dictionary leaf, or authored outdoor cell.
|
||||
/// </summary>
|
||||
internal sealed class LandblockRetirementCursor : IDisposable
|
||||
{
|
||||
private readonly PhysicsEngine _destinationEngine;
|
||||
private readonly PhysicsDataCache _destinationCache;
|
||||
private readonly CollisionWorldState _destination;
|
||||
private readonly CollisionWorldState _authoritative;
|
||||
private readonly uint _canonical;
|
||||
private readonly uint _prefix;
|
||||
private readonly bool _withdraw;
|
||||
private readonly List<uint> _ownerSlots;
|
||||
private readonly int _ownerSlotLimit;
|
||||
private readonly LandblockPhysics? _demotedLandblock;
|
||||
private readonly CellGraphTerrain? _demotedTerrain;
|
||||
private IEnumerator<KeyValuePair<uint, CellPhysics>>? _cells;
|
||||
private IEnumerator<KeyValuePair<uint, FlatCellStructureCollisionAsset>>?
|
||||
_flatCells;
|
||||
private IEnumerator<KeyValuePair<uint, FlatEnvCellTopology>>? _flatEnvCells;
|
||||
private IEnumerator<KeyValuePair<uint, BuildingPhysics>>? _buildings;
|
||||
private IEnumerator<KeyValuePair<uint, EnvCell>>? _envCells;
|
||||
private int _ownerIndex;
|
||||
private int _outdoorIndex;
|
||||
private int _phase;
|
||||
|
||||
internal LandblockRetirementCursor(
|
||||
PhysicsEngine destination,
|
||||
PhysicsEngine authoritative,
|
||||
uint landblockId,
|
||||
bool withdraw)
|
||||
{
|
||||
_destinationEngine = destination;
|
||||
_destinationCache = destination.DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Collision engine has no data cache.");
|
||||
_destination = destination._collisionWorld.Capture();
|
||||
_authoritative = authoritative._collisionWorld.Capture();
|
||||
_canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
_prefix = landblockId & 0xFFFF0000u;
|
||||
_withdraw = withdraw;
|
||||
_ownerSlots = _destination.ShadowOwnerSlots;
|
||||
_ownerSlotLimit = _ownerSlots.Count;
|
||||
if (!withdraw)
|
||||
{
|
||||
_authoritative.Landblocks.TryGetValue(
|
||||
_canonical,
|
||||
out _demotedLandblock);
|
||||
_authoritative.Terrain.TryGetValue(
|
||||
_prefix,
|
||||
out _demotedTerrain);
|
||||
}
|
||||
}
|
||||
|
||||
internal uint LandblockId => _canonical;
|
||||
|
||||
internal LandblockRetirementStep Advance()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (_ownerIndex < _ownerSlotLimit)
|
||||
{
|
||||
uint ownerId = _ownerSlots[_ownerIndex++];
|
||||
if (ownerId != 0u)
|
||||
{
|
||||
_destinationEngine.ShadowObjects
|
||||
.RetireOwnerFromLandblock(
|
||||
ownerId,
|
||||
_canonical);
|
||||
}
|
||||
return Worked();
|
||||
}
|
||||
_phase++;
|
||||
continue;
|
||||
case 1:
|
||||
_cells ??= _destination.CellStruct.GetEnumerator();
|
||||
if (RemoveOneInPrefix(_cells, _destination.CellStruct))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _cells);
|
||||
_phase++;
|
||||
continue;
|
||||
case 2:
|
||||
_flatCells ??= _destination.FlatCellStruct.GetEnumerator();
|
||||
if (RemoveOneInPrefix(
|
||||
_flatCells,
|
||||
_destination.FlatCellStruct))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _flatCells);
|
||||
_phase++;
|
||||
continue;
|
||||
case 3:
|
||||
_flatEnvCells ??= _destination.FlatEnvCell.GetEnumerator();
|
||||
if (RemoveOneInPrefix(
|
||||
_flatEnvCells,
|
||||
_destination.FlatEnvCell))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _flatEnvCells);
|
||||
_phase++;
|
||||
continue;
|
||||
case 4:
|
||||
_buildings ??= _destination.Buildings.GetEnumerator();
|
||||
if (RemoveOneInPrefix(
|
||||
_buildings,
|
||||
_destination.Buildings))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _buildings);
|
||||
_phase++;
|
||||
continue;
|
||||
case 5:
|
||||
_envCells ??= _destination.EnvCells.GetEnumerator();
|
||||
if (RemoveOneInPrefix(_envCells, _destination.EnvCells))
|
||||
return Worked();
|
||||
DisposeEnumerator(ref _envCells);
|
||||
_phase++;
|
||||
continue;
|
||||
case 6:
|
||||
if (_outdoorIndex < 0x40)
|
||||
{
|
||||
uint id = _prefix | (uint)++_outdoorIndex;
|
||||
_destination.ShadowCells.Remove(id);
|
||||
if (_withdraw)
|
||||
{
|
||||
_destination.OutdoorCells.TryRemove(id, out _);
|
||||
}
|
||||
else if (_authoritative.OutdoorCells.TryGetValue(
|
||||
id,
|
||||
out ObjCell? outdoor))
|
||||
{
|
||||
_destination.OutdoorCells[id] = outdoor;
|
||||
}
|
||||
return Worked();
|
||||
}
|
||||
_phase++;
|
||||
continue;
|
||||
case 7:
|
||||
if (_withdraw)
|
||||
{
|
||||
_destinationEngine._landblocks.Remove(_canonical);
|
||||
_destinationEngine.RemoveLandblockSlot(_canonical);
|
||||
_destination.Terrain.TryRemove(_prefix, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_demotedLandblock is not null)
|
||||
{
|
||||
_destinationEngine._landblocks[_canonical] =
|
||||
_demotedLandblock;
|
||||
_destinationEngine.EnsureLandblockSlot(_canonical);
|
||||
}
|
||||
if (_demotedTerrain is not null)
|
||||
_destination.Terrain[_prefix] = _demotedTerrain;
|
||||
}
|
||||
_phase++;
|
||||
return Worked();
|
||||
case 8:
|
||||
uint currentCellId =
|
||||
_destinationCache.CellGraph.CurrCell?.Id ?? 0u;
|
||||
if ((currentCellId & 0xFFFF0000u) == _prefix
|
||||
&& (_withdraw
|
||||
|| (currentCellId & 0xFFFFu) >= 0x0100u))
|
||||
{
|
||||
_destinationCache.CellGraph.CurrCell = null;
|
||||
}
|
||||
_phase++;
|
||||
return new LandblockRetirementStep(
|
||||
Completed: true,
|
||||
Worked: false);
|
||||
default:
|
||||
return new LandblockRetirementStep(
|
||||
Completed: true,
|
||||
Worked: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LandblockRetirementStep Worked() => new(
|
||||
Completed: false,
|
||||
Worked: true);
|
||||
|
||||
private bool RemoveOneInPrefix<T>(
|
||||
IEnumerator<KeyValuePair<uint, T>> source,
|
||||
IDictionary<uint, T> destination)
|
||||
{
|
||||
if (!source.MoveNext())
|
||||
return false;
|
||||
uint id = source.Current.Key;
|
||||
if ((id & 0xFFFF0000u) == _prefix)
|
||||
{
|
||||
destination.Remove(id);
|
||||
_destination.ShadowCells.Remove(id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void DisposeEnumerator<T>(
|
||||
ref IEnumerator<KeyValuePair<uint, T>>? enumerator)
|
||||
{
|
||||
enumerator?.Dispose();
|
||||
enumerator = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeEnumerator(ref _cells);
|
||||
DisposeEnumerator(ref _flatCells);
|
||||
DisposeEnumerator(ref _flatEnvCells);
|
||||
DisposeEnumerator(ref _buildings);
|
||||
DisposeEnumerator(ref _envCells);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct LandblockRetirementStep(
|
||||
bool Completed,
|
||||
bool Worked);
|
||||
|
||||
/// <summary>
|
||||
/// Applies one already-committed landblock delta to a later off-side root.
|
||||
/// Each advance mutates at most one dictionary leaf, one synthesized
|
||||
/// outdoor cell, or one logical shadow owner.
|
||||
/// </summary>
|
||||
internal sealed class LandblockReplacementApplyCursor : IDisposable
|
||||
{
|
||||
private readonly PhysicsEngine _destinationEngine;
|
||||
private readonly PhysicsDataCache _destinationCache;
|
||||
private readonly CollisionWorldState _destination;
|
||||
private readonly PreparedPhysicsEngineLandblock _replacement;
|
||||
private int _phase;
|
||||
private int _index;
|
||||
|
||||
internal LandblockReplacementApplyCursor(
|
||||
PhysicsEngine destination,
|
||||
PreparedPhysicsEngineLandblock replacement)
|
||||
{
|
||||
_destinationEngine = destination;
|
||||
_destinationCache = destination.DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Collision engine has no data cache.");
|
||||
_destination = _destinationCache.CollisionWorld.Capture();
|
||||
_replacement = replacement;
|
||||
}
|
||||
|
||||
internal uint LandblockId => _replacement.LandblockId;
|
||||
|
||||
internal LandblockReplacementApplyStep Advance()
|
||||
{
|
||||
PreparedPhysicsDataCacheLandblock data = _replacement.DataCache;
|
||||
PreparedCellGraphLandblock graph = data.CellGraph;
|
||||
while (true)
|
||||
{
|
||||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (RemoveOne(_destination.CellStruct, data.CellIdsToRemove))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 1:
|
||||
if (InstallOne(_destination.CellStruct, data.Cells))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 2:
|
||||
if (RemoveOne(_destination.FlatCellStruct, data.FlatCellIdsToRemove))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 3:
|
||||
if (InstallOne(_destination.FlatCellStruct, data.FlatCells))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 4:
|
||||
if (RemoveOne(_destination.FlatEnvCell, data.FlatEnvCellIdsToRemove))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 5:
|
||||
if (InstallOne(_destination.FlatEnvCell, data.FlatEnvCells))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 6:
|
||||
if (RemoveOne(_destination.Buildings, data.BuildingIdsToRemove))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 7:
|
||||
if (InstallOne(_destination.Buildings, data.Buildings))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 8:
|
||||
if (RemoveOne(_destination.EnvCells, graph.EnvCellIdsToRemove))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 9:
|
||||
if (graph.HasTerrain)
|
||||
{
|
||||
if (_index == 0)
|
||||
{
|
||||
_destination.Terrain[graph.LandblockPrefix] =
|
||||
graph.Terrain!;
|
||||
_index++;
|
||||
return Worked();
|
||||
}
|
||||
if (_index <= 0x40)
|
||||
{
|
||||
uint low = (uint)_index++;
|
||||
CellGraphTerrain terrain = graph.Terrain!;
|
||||
int cellIndex = (int)(low - 1u);
|
||||
uint id = graph.LandblockPrefix | low;
|
||||
_destination.OutdoorCells[id] =
|
||||
LandCell.Synthesize(
|
||||
id,
|
||||
terrain.Terrain,
|
||||
terrain.Origin,
|
||||
cellIndex / 8,
|
||||
cellIndex % 8);
|
||||
return Worked();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_index == 0)
|
||||
{
|
||||
_destination.Terrain.TryRemove(
|
||||
graph.LandblockPrefix,
|
||||
out _);
|
||||
_index++;
|
||||
return Worked();
|
||||
}
|
||||
if (_index <= 0x40)
|
||||
{
|
||||
uint low = (uint)_index++;
|
||||
_destination.OutdoorCells.TryRemove(
|
||||
graph.LandblockPrefix | low,
|
||||
out _);
|
||||
return Worked();
|
||||
}
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 10:
|
||||
if (InstallOne(_destination.EnvCells, graph.EnvCells))
|
||||
return Worked();
|
||||
NextPhase();
|
||||
continue;
|
||||
case 11:
|
||||
_destinationEngine.InstallLandblockClone(
|
||||
_replacement.LandblockId,
|
||||
_replacement.Landblock);
|
||||
NextPhase();
|
||||
return Worked();
|
||||
case 12:
|
||||
if (_index < _replacement.Shadows.OwnerIds.Count)
|
||||
{
|
||||
uint ownerId =
|
||||
_replacement.Shadows.OwnerIds[_index++];
|
||||
return new LandblockReplacementApplyStep(
|
||||
Completed: false,
|
||||
Worked: true,
|
||||
HasOwner: true,
|
||||
ownerId);
|
||||
}
|
||||
NextPhase();
|
||||
continue;
|
||||
case 13:
|
||||
uint currentCellId =
|
||||
_destinationCache.CellGraph.CurrCell?.Id ?? 0u;
|
||||
if ((currentCellId & 0xFFFF0000u)
|
||||
== graph.LandblockPrefix)
|
||||
{
|
||||
_destinationCache.CellGraph.CurrCell =
|
||||
_destinationCache.CellGraph.GetVisible(
|
||||
currentCellId);
|
||||
}
|
||||
_phase++;
|
||||
return new LandblockReplacementApplyStep(
|
||||
Completed: true,
|
||||
Worked: false,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u);
|
||||
default:
|
||||
return new LandblockReplacementApplyStep(
|
||||
Completed: true,
|
||||
Worked: false,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LandblockReplacementApplyStep Worked() => new(
|
||||
Completed: false,
|
||||
Worked: true,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u);
|
||||
|
||||
private void NextPhase()
|
||||
{
|
||||
_phase++;
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
private bool RemoveOne<T>(
|
||||
IDictionary<uint, T> destination,
|
||||
IReadOnlyList<uint> ids)
|
||||
{
|
||||
if (_index >= ids.Count)
|
||||
return false;
|
||||
destination.Remove(ids[_index++]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool InstallOne<T>(
|
||||
IDictionary<uint, T> destination,
|
||||
IReadOnlyList<KeyValuePair<uint, T>> entries)
|
||||
{
|
||||
if (_index >= entries.Count)
|
||||
return false;
|
||||
KeyValuePair<uint, T> pair = entries[_index++];
|
||||
destination[pair.Key] = pair.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retained one-leaf-at-a-time materializer for an off-side collision
|
||||
/// generation. Construction captures the current root reference only; no
|
||||
/// resident dictionary is copied until <see cref="Advance"/>. Runtime's
|
||||
/// owner journal reconciles mutations that occur while this cursor walks.
|
||||
/// </summary>
|
||||
internal sealed class CollisionStagingBuilder : IDisposable
|
||||
{
|
||||
private readonly PhysicsEngine _active;
|
||||
private readonly CollisionWorldState _source;
|
||||
private readonly CollisionWorldState _destination;
|
||||
private readonly ShadowObjectRegistry _sourceShadows;
|
||||
private readonly uint _targetPrefix;
|
||||
private readonly HashSet<uint> _suppressedPrefixes = new();
|
||||
private readonly int _landblockSlotLimit;
|
||||
private readonly int _ownerSlotLimit;
|
||||
private IEnumerator<KeyValuePair<uint, CellPhysics>>? _cells;
|
||||
private IEnumerator<KeyValuePair<uint, FlatCellStructureCollisionAsset>>? _flatCells;
|
||||
private IEnumerator<KeyValuePair<uint, FlatEnvCellTopology>>? _flatEnvCells;
|
||||
private IEnumerator<KeyValuePair<uint, BuildingPhysics>>? _buildings;
|
||||
private IEnumerator<KeyValuePair<uint, EnvCell>>? _envCells;
|
||||
private IEnumerator<KeyValuePair<uint, CellGraphTerrain>>? _terrain;
|
||||
private IEnumerator<KeyValuePair<uint, ObjCell>>? _outdoorCells;
|
||||
private int _landblockIndex;
|
||||
private int _ownerIndex;
|
||||
private int _phase;
|
||||
|
||||
internal CollisionStagingBuilder(
|
||||
PhysicsEngine active,
|
||||
PhysicsDataCache activeCache,
|
||||
uint targetPrefix)
|
||||
{
|
||||
_active = active;
|
||||
_targetPrefix = targetPrefix;
|
||||
_source = active._collisionWorld.Capture();
|
||||
var stagingSlot = new CollisionWorldStateSlot();
|
||||
StagingCache = activeCache.CreateEmptyCollisionStaging(stagingSlot);
|
||||
StagingEngine = new PhysicsEngine
|
||||
{
|
||||
DataCache = StagingCache,
|
||||
Objects = active.Objects,
|
||||
};
|
||||
_destination = stagingSlot.Capture();
|
||||
_sourceShadows = new ShadowObjectRegistry(
|
||||
new CollisionWorldStateSlot(_source));
|
||||
_landblockSlotLimit = _source.LandblockSlots.Count;
|
||||
_ownerSlotLimit = _source.ShadowOwnerSlots.Count;
|
||||
}
|
||||
|
||||
internal PhysicsDataCache StagingCache { get; }
|
||||
internal PhysicsEngine StagingEngine { get; }
|
||||
internal int WorkUnits { get; private set; }
|
||||
internal bool Completed => _phase == 9;
|
||||
|
||||
/// <summary>
|
||||
/// Prevent a landblock retired after this cursor captured its source
|
||||
/// root from being copied back into the draft by a later phase.
|
||||
/// Already-copied leaves are retired by the caller before cloning
|
||||
/// resumes; this tombstone covers every leaf not visited yet.
|
||||
/// </summary>
|
||||
internal void SuppressLandblock(uint landblockId) =>
|
||||
_suppressedPrefixes.Add(landblockId & 0xFFFF0000u);
|
||||
|
||||
internal bool Advance()
|
||||
{
|
||||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (_landblockIndex < _landblockSlotLimit)
|
||||
{
|
||||
uint id = _source.LandblockSlots[_landblockIndex++];
|
||||
if (id != 0u
|
||||
&& (id & 0xFFFF0000u) != _targetPrefix
|
||||
&& !_suppressedPrefixes.Contains(
|
||||
id & 0xFFFF0000u)
|
||||
&& _source.Landblocks.TryGetValue(
|
||||
id,
|
||||
out LandblockPhysics? landblock))
|
||||
{
|
||||
StagingEngine.InstallLandblockClone(id, landblock);
|
||||
}
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_phase++;
|
||||
return false;
|
||||
case 1:
|
||||
_cells ??= _source.CellStruct.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_cells, _destination.CellStruct))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _cells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 2:
|
||||
_flatCells ??= _source.FlatCellStruct.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_flatCells, _destination.FlatCellStruct))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _flatCells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 3:
|
||||
_flatEnvCells ??= _source.FlatEnvCell.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_flatEnvCells, _destination.FlatEnvCell))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _flatEnvCells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 4:
|
||||
_buildings ??= _source.Buildings.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_buildings, _destination.Buildings))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _buildings);
|
||||
_phase++;
|
||||
return false;
|
||||
case 5:
|
||||
_envCells ??= _source.EnvCells.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_envCells, _destination.EnvCells))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _envCells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 6:
|
||||
_terrain ??= _source.Terrain.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_terrain, _destination.Terrain))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _terrain);
|
||||
_phase++;
|
||||
return false;
|
||||
case 7:
|
||||
_outdoorCells ??= _source.OutdoorCells.GetEnumerator();
|
||||
if (CopyOneOutsideTarget(_outdoorCells, _destination.OutdoorCells))
|
||||
return CountOne();
|
||||
DisposeEnumerator(ref _outdoorCells);
|
||||
_phase++;
|
||||
return false;
|
||||
case 8:
|
||||
if (_ownerIndex < _ownerSlotLimit)
|
||||
{
|
||||
uint ownerId = _source.ShadowOwnerSlots[_ownerIndex++];
|
||||
if (ownerId != 0u
|
||||
&& !_sourceShadows.IsStaticOwnerRootedIn(
|
||||
ownerId,
|
||||
_targetPrefix)
|
||||
&& !IsSuppressedStaticOwner(ownerId)
|
||||
&& !StagingEngine.ShadowObjects.HasLogicalOwner(
|
||||
ownerId))
|
||||
{
|
||||
StagingEngine.ShadowObjects.MirrorOwnerFrom(
|
||||
_sourceShadows,
|
||||
ownerId);
|
||||
}
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
uint currentCellId = _active.DataCache?.CellGraph.CurrCell?.Id ?? 0u;
|
||||
StagingCache.CellGraph.CurrCell =
|
||||
StagingCache.CellGraph.GetVisible(currentCellId);
|
||||
_phase++;
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CountOne()
|
||||
{
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CopyOneOutsideTarget<T>(
|
||||
IEnumerator<KeyValuePair<uint, T>> source,
|
||||
IDictionary<uint, T> destination)
|
||||
{
|
||||
if (!source.MoveNext())
|
||||
return false;
|
||||
KeyValuePair<uint, T> pair = source.Current;
|
||||
uint prefix = pair.Key & 0xFFFF0000u;
|
||||
if (prefix != _targetPrefix
|
||||
&& !_suppressedPrefixes.Contains(prefix))
|
||||
destination[pair.Key] = pair.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsSuppressedStaticOwner(uint ownerId)
|
||||
=> _sourceShadows.TryGetStaticOwnerRootPrefix(
|
||||
ownerId,
|
||||
out uint prefix)
|
||||
&& _suppressedPrefixes.Contains(prefix);
|
||||
|
||||
private static void DisposeEnumerator<T>(
|
||||
ref IEnumerator<KeyValuePair<uint, T>>? enumerator)
|
||||
{
|
||||
enumerator?.Dispose();
|
||||
enumerator = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeEnumerator(ref _cells);
|
||||
DisposeEnumerator(ref _flatCells);
|
||||
DisposeEnumerator(ref _flatEnvCells);
|
||||
DisposeEnumerator(ref _buildings);
|
||||
DisposeEnumerator(ref _envCells);
|
||||
DisposeEnumerator(ref _terrain);
|
||||
DisposeEnumerator(ref _outdoorCells);
|
||||
}
|
||||
}
|
||||
|
||||
private void InstallLandblockClone(
|
||||
uint landblockId,
|
||||
LandblockPhysics landblock)
|
||||
{
|
||||
_landblocks[landblockId] = landblock;
|
||||
EnsureLandblockSlot(landblockId);
|
||||
}
|
||||
|
||||
private void EnsureLandblockSlot(uint landblockId)
|
||||
{
|
||||
if (_landblockIndices.ContainsKey(landblockId))
|
||||
return;
|
||||
if (_landblockFreeSlots.TryPop(out int freeIndex))
|
||||
{
|
||||
_landblockSlots[freeIndex] = landblockId;
|
||||
_landblockIndices[landblockId] = freeIndex;
|
||||
return;
|
||||
}
|
||||
_landblockIndices[landblockId] = _landblockSlots.Count;
|
||||
_landblockSlots.Add(landblockId);
|
||||
}
|
||||
|
||||
private void RemoveLandblockSlot(uint landblockId)
|
||||
{
|
||||
if (!_landblockIndices.Remove(landblockId, out int slotIndex))
|
||||
return;
|
||||
_landblockSlots[slotIndex] = 0u;
|
||||
_landblockFreeSlots.Push(slotIndex);
|
||||
}
|
||||
|
||||
internal sealed class PreparedPhysicsEngineLandblock
|
||||
|
|
@ -232,17 +997,20 @@ public sealed class PhysicsEngine
|
|||
internal PreparedPhysicsEngineLandblock(
|
||||
uint landblockId,
|
||||
LandblockPhysics landblock,
|
||||
PhysicsEngine staging,
|
||||
PreparedPhysicsDataCacheLandblock dataCache,
|
||||
ShadowObjectRegistry.PreparedLandblockShadowReplacement shadows)
|
||||
{
|
||||
LandblockId = landblockId;
|
||||
Landblock = landblock;
|
||||
Staging = staging;
|
||||
DataCache = dataCache;
|
||||
Shadows = shadows;
|
||||
}
|
||||
|
||||
internal uint LandblockId { get; }
|
||||
internal LandblockPhysics Landblock { get; }
|
||||
internal PhysicsEngine Staging { get; }
|
||||
internal PreparedPhysicsDataCacheLandblock DataCache { get; }
|
||||
internal ShadowObjectRegistry.PreparedLandblockShadowReplacement Shadows { get; }
|
||||
}
|
||||
|
|
@ -251,6 +1019,7 @@ public sealed class PhysicsEngine
|
|||
{
|
||||
private readonly uint _landblockId;
|
||||
private readonly LandblockPhysics _landblock;
|
||||
private readonly PhysicsEngine _staging;
|
||||
private readonly PhysicsDataCache.LandblockReplacementBuilder _data;
|
||||
private readonly ShadowObjectRegistry.LandblockReplacementBuilder _shadows;
|
||||
private int _phase;
|
||||
|
|
@ -258,19 +1027,23 @@ public sealed class PhysicsEngine
|
|||
internal LandblockReplacementBuilder(
|
||||
uint landblockId,
|
||||
LandblockPhysics landblock,
|
||||
PhysicsEngine staging,
|
||||
PhysicsDataCache.LandblockReplacementBuilder data,
|
||||
ShadowObjectRegistry.LandblockReplacementBuilder shadows)
|
||||
{
|
||||
_landblockId = landblockId;
|
||||
_landblock = landblock;
|
||||
_staging = staging;
|
||||
_data = data;
|
||||
_shadows = shadows;
|
||||
}
|
||||
|
||||
internal int WorkUnits => _data.WorkUnits + _shadows.WorkUnits;
|
||||
internal bool IsStable => _shadows.IsStable;
|
||||
internal PreparedPhysicsEngineLandblock? Prepared { get; private set; }
|
||||
|
||||
internal void RefreshRetainedOwner(uint ownerId) =>
|
||||
_shadows.RefreshOwner(ownerId);
|
||||
|
||||
internal bool Advance()
|
||||
{
|
||||
if (_phase == 0)
|
||||
|
|
@ -284,12 +1057,13 @@ public sealed class PhysicsEngine
|
|||
{
|
||||
if (!_shadows.Advance())
|
||||
return false;
|
||||
if (IsStable && _data.Prepared is not null
|
||||
if (_data.Prepared is not null
|
||||
&& _shadows.Prepared is not null)
|
||||
{
|
||||
Prepared = new PreparedPhysicsEngineLandblock(
|
||||
_landblockId,
|
||||
_landblock,
|
||||
Prepared = new PreparedPhysicsEngineLandblock(
|
||||
_landblockId,
|
||||
_landblock,
|
||||
_staging,
|
||||
_data.Prepared,
|
||||
_shadows.Prepared);
|
||||
}
|
||||
|
|
@ -314,6 +1088,7 @@ public sealed class PhysicsEngine
|
|||
float worldOffsetX, float worldOffsetY)
|
||||
{
|
||||
_landblocks[landblockId] = new LandblockPhysics(terrain, cells, portals, worldOffsetX, worldOffsetY);
|
||||
EnsureLandblockSlot(landblockId);
|
||||
|
||||
// UCG Stage 1: mirror terrain into the unified graph (inert this stage).
|
||||
DataCache?.CellGraph.RegisterTerrain(landblockId, terrain, new Vector3(worldOffsetX, worldOffsetY, 0f));
|
||||
|
|
@ -325,6 +1100,7 @@ public sealed class PhysicsEngine
|
|||
public void RemoveLandblock(uint landblockId)
|
||||
{
|
||||
_landblocks.Remove(landblockId);
|
||||
RemoveLandblockSlot(landblockId);
|
||||
ShadowObjects.DeregisterStaticOwnersForLandblock(landblockId);
|
||||
ShadowObjects.RemoveLandblock(landblockId);
|
||||
DataCache?.RemoveCellsForLandblock(landblockId); // D8: rebase cell BSP transforms on next apply
|
||||
|
|
|
|||
|
|
@ -26,20 +26,26 @@ namespace AcDream.Core.Physics;
|
|||
/// </summary>
|
||||
public sealed class ShadowObjectRegistry
|
||||
{
|
||||
private readonly Dictionary<uint, List<ShadowEntry>> _cells = new();
|
||||
private readonly Dictionary<uint, List<uint>> _entityToCells = new(); // for deregistration
|
||||
private readonly HashSet<uint> _suspendedEntities = new();
|
||||
private CollisionWorldStateSlot _collisionWorld;
|
||||
private Dictionary<uint, List<ShadowEntry>> _cells =>
|
||||
_collisionWorld.Current.ShadowCells;
|
||||
private Dictionary<uint, List<uint>> _entityToCells =>
|
||||
_collisionWorld.Current.ShadowEntityCells; // for deregistration
|
||||
private HashSet<uint> _suspendedEntities =>
|
||||
_collisionWorld.Current.SuspendedShadowEntities;
|
||||
// Rows withdrawn because a touched landblock streamed out. The owner may
|
||||
// be seeded in an adjacent still-resident landblock, so its remaining rows
|
||||
// cannot by themselves tell RefloodLandblock that this prefix needs repair.
|
||||
private readonly Dictionary<uint, HashSet<uint>> _withdrawnPrefixesByOwner = new();
|
||||
private Dictionary<uint, HashSet<uint>> _withdrawnPrefixesByOwner =>
|
||||
_collisionWorld.Current.WithdrawnPrefixesByOwner;
|
||||
|
||||
/// <summary>
|
||||
/// A6.P4 door fix (2026-05-24): per-entity original shape list, used by
|
||||
/// <see cref="UpdatePosition"/> to recompose part world-transforms when
|
||||
/// the entity moves. Cleared by <see cref="Deregister"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<uint, System.Collections.Generic.IReadOnlyList<ShadowShape>> _entityShapes = new();
|
||||
private Dictionary<uint, System.Collections.Generic.IReadOnlyList<ShadowShape>> _entityShapes =>
|
||||
_collisionWorld.Current.ShadowEntityShapes;
|
||||
|
||||
/// <summary>
|
||||
/// BR-7: per-entity registration arguments, kept so a registration can be
|
||||
|
|
@ -50,9 +56,50 @@ public sealed class ShadowObjectRegistry
|
|||
/// gets its cell set recomputed afterwards. <see cref="RefloodLandblock"/>
|
||||
/// is the streaming-side trigger.
|
||||
/// </summary>
|
||||
private readonly Dictionary<uint, RegistrationRecord> _entityReg = new();
|
||||
private readonly Dictionary<uint, ulong> _ownerVersions = new();
|
||||
private ulong _mutationVersion;
|
||||
private Dictionary<uint, RegistrationRecord> _entityReg =>
|
||||
_collisionWorld.Current.ShadowEntityRegistrations;
|
||||
private Dictionary<uint, ulong> _ownerVersions =>
|
||||
_collisionWorld.Current.ShadowOwnerVersions;
|
||||
private Dictionary<uint, HashSet<uint>> _ownerPrefixes =>
|
||||
_collisionWorld.Current.ShadowOwnerPrefixes;
|
||||
private Dictionary<uint, List<uint>> _prefixOwnerSlots =>
|
||||
_collisionWorld.Current.ShadowPrefixOwnerSlots;
|
||||
private Dictionary<uint, Dictionary<uint, int>> _prefixOwnerIndices =>
|
||||
_collisionWorld.Current.ShadowPrefixOwnerIndices;
|
||||
private Dictionary<uint, Stack<int>> _prefixFreeSlots =>
|
||||
_collisionWorld.Current.ShadowPrefixFreeSlots;
|
||||
private List<uint> _ownerSlots =>
|
||||
_collisionWorld.Current.ShadowOwnerSlots;
|
||||
private Dictionary<uint, int> _ownerIndices =>
|
||||
_collisionWorld.Current.ShadowOwnerIndices;
|
||||
private Stack<int> _ownerFreeSlots =>
|
||||
_collisionWorld.Current.ShadowOwnerFreeSlots;
|
||||
private readonly HashSet<uint> _prefixScratch = new();
|
||||
private readonly List<uint> _removedPrefixScratch = new();
|
||||
internal event Action<uint, ulong>? OwnerMutated;
|
||||
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
|
||||
|
||||
public ShadowObjectRegistry()
|
||||
: this(new CollisionWorldStateSlot())
|
||||
{
|
||||
}
|
||||
|
||||
internal ShadowObjectRegistry(CollisionWorldStateSlot collisionWorld)
|
||||
{
|
||||
_collisionWorld = collisionWorld
|
||||
?? throw new ArgumentNullException(nameof(collisionWorld));
|
||||
}
|
||||
|
||||
internal void AttachCollisionWorld(CollisionWorldStateSlot collisionWorld)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(collisionWorld);
|
||||
if (_cells.Count != 0 || _entityReg.Count != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A populated shadow registry cannot change collision roots.");
|
||||
}
|
||||
_collisionWorld = collisionWorld;
|
||||
}
|
||||
|
||||
internal sealed record RegistrationRecord(
|
||||
uint SeedCellId,
|
||||
|
|
@ -76,31 +123,38 @@ public sealed class ShadowObjectRegistry
|
|||
|
||||
private void BumpOwnerVersion(uint entityId)
|
||||
{
|
||||
_ownerVersions[entityId] = checked(GetOwnerVersion(entityId) + 1UL);
|
||||
_mutationVersion = checked(_mutationVersion + 1UL);
|
||||
ulong version = checked(GetOwnerVersion(entityId) + 1UL);
|
||||
_ownerVersions[entityId] = version;
|
||||
RefreshOwnerPrefixIndex(entityId);
|
||||
OwnerMutated?.Invoke(entityId, version);
|
||||
}
|
||||
|
||||
internal ulong MutationVersion => _mutationVersion;
|
||||
|
||||
internal RetainedRefloodOwnerScan CreateRetainedRefloodOwnerScan(
|
||||
uint landblockId) => new(this, landblockId & 0xFFFF0000u);
|
||||
uint landblockId)
|
||||
{
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
_prefixOwnerSlots.TryGetValue(prefix, out List<uint>? slots);
|
||||
return new RetainedRefloodOwnerScan(this, prefix, slots);
|
||||
}
|
||||
|
||||
internal sealed class RetainedRefloodOwnerScan : IDisposable
|
||||
{
|
||||
private readonly ShadowObjectRegistry _owner;
|
||||
private readonly uint _prefix;
|
||||
private readonly ulong _sourceMutationVersion;
|
||||
private Dictionary<uint, RegistrationRecord>.Enumerator _enumerator;
|
||||
private readonly List<uint>? _slots;
|
||||
private readonly int _limit;
|
||||
private int _index;
|
||||
private bool _completed;
|
||||
|
||||
internal RetainedRefloodOwnerScan(
|
||||
ShadowObjectRegistry owner,
|
||||
uint prefix)
|
||||
uint prefix,
|
||||
List<uint>? slots)
|
||||
{
|
||||
_owner = owner;
|
||||
_prefix = prefix;
|
||||
_sourceMutationVersion = owner.MutationVersion;
|
||||
_enumerator = owner._entityReg.GetEnumerator();
|
||||
_slots = slots;
|
||||
_limit = slots?.Count ?? 0;
|
||||
}
|
||||
|
||||
internal RetainedRefloodOwnerScanStep Advance()
|
||||
|
|
@ -109,55 +163,169 @@ public sealed class ShadowObjectRegistry
|
|||
{
|
||||
return new RetainedRefloodOwnerScanStep(
|
||||
Completed: true,
|
||||
Stable: _owner.MutationVersion == _sourceMutationVersion,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u,
|
||||
SourceMutationVersion: _sourceMutationVersion);
|
||||
OwnerId: 0u);
|
||||
}
|
||||
if (_owner.MutationVersion != _sourceMutationVersion)
|
||||
if (_slots is null || _index >= _limit)
|
||||
{
|
||||
_completed = true;
|
||||
return new RetainedRefloodOwnerScanStep(
|
||||
Completed: true,
|
||||
Stable: false,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u,
|
||||
SourceMutationVersion: _sourceMutationVersion);
|
||||
}
|
||||
if (!_enumerator.MoveNext())
|
||||
{
|
||||
_completed = true;
|
||||
return new RetainedRefloodOwnerScanStep(
|
||||
Completed: true,
|
||||
Stable: true,
|
||||
HasOwner: false,
|
||||
OwnerId: 0u,
|
||||
SourceMutationVersion: _sourceMutationVersion);
|
||||
OwnerId: 0u);
|
||||
}
|
||||
|
||||
(uint ownerId, RegistrationRecord registration) =
|
||||
_enumerator.Current;
|
||||
bool retained = !_owner._suspendedEntities.Contains(ownerId)
|
||||
&& (!registration.IsStatic
|
||||
|| (registration.SeedCellId & 0xFFFF0000u) != _prefix)
|
||||
&& _owner.OwnerTouchesLandblock(ownerId, _prefix);
|
||||
uint ownerId = _slots[_index++];
|
||||
bool retained = _owner.IsRetainedRefloodOwner(ownerId, _prefix);
|
||||
return new RetainedRefloodOwnerScanStep(
|
||||
Completed: false,
|
||||
Stable: true,
|
||||
HasOwner: retained,
|
||||
OwnerId: retained ? ownerId : 0u,
|
||||
SourceMutationVersion: _sourceMutationVersion);
|
||||
OwnerId: retained ? ownerId : 0u);
|
||||
}
|
||||
|
||||
public void Dispose() => _enumerator.Dispose();
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
private void RefreshOwnerPrefixIndex(uint entityId)
|
||||
{
|
||||
if (!_entityReg.ContainsKey(entityId))
|
||||
{
|
||||
RemoveOwnerPrefixMembership(entityId);
|
||||
return;
|
||||
}
|
||||
EnsureOwnerSlot(entityId);
|
||||
_prefixScratch.Clear();
|
||||
if (_entityReg.TryGetValue(entityId, out RegistrationRecord? registration))
|
||||
_prefixScratch.Add(registration.SeedCellId & 0xFFFF0000u);
|
||||
if (_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
||||
{
|
||||
for (int index = 0; index < cells.Count; index++)
|
||||
_prefixScratch.Add(cells[index] & 0xFFFF0000u);
|
||||
}
|
||||
if (_withdrawnPrefixesByOwner.TryGetValue(
|
||||
entityId,
|
||||
out HashSet<uint>? withdrawn))
|
||||
{
|
||||
foreach (uint prefix in withdrawn)
|
||||
_prefixScratch.Add(prefix & 0xFFFF0000u);
|
||||
}
|
||||
|
||||
if (!_ownerPrefixes.TryGetValue(entityId, out HashSet<uint>? current))
|
||||
{
|
||||
current = new HashSet<uint>();
|
||||
_ownerPrefixes[entityId] = current;
|
||||
}
|
||||
|
||||
_removedPrefixScratch.Clear();
|
||||
foreach (uint prefix in current)
|
||||
{
|
||||
if (!_prefixScratch.Contains(prefix))
|
||||
_removedPrefixScratch.Add(prefix);
|
||||
}
|
||||
for (int index = 0; index < _removedPrefixScratch.Count; index++)
|
||||
{
|
||||
uint prefix = _removedPrefixScratch[index];
|
||||
current.Remove(prefix);
|
||||
if (_prefixOwnerIndices.TryGetValue(
|
||||
prefix,
|
||||
out Dictionary<uint, int>? indices)
|
||||
&& indices.Remove(entityId, out int slotIndex))
|
||||
{
|
||||
_prefixOwnerSlots[prefix][slotIndex] = 0u;
|
||||
_prefixFreeSlots[prefix].Push(slotIndex);
|
||||
ReleaseEmptyPrefixContainer(prefix, indices);
|
||||
}
|
||||
OwnerPrefixMembershipChanged?.Invoke(entityId, prefix);
|
||||
}
|
||||
|
||||
foreach (uint prefix in _prefixScratch)
|
||||
{
|
||||
if (!current.Add(prefix))
|
||||
continue;
|
||||
if (!_prefixOwnerSlots.TryGetValue(prefix, out List<uint>? slots))
|
||||
{
|
||||
slots = new List<uint>();
|
||||
_prefixOwnerSlots[prefix] = slots;
|
||||
_prefixOwnerIndices[prefix] = new Dictionary<uint, int>();
|
||||
_prefixFreeSlots[prefix] = new Stack<int>();
|
||||
}
|
||||
Dictionary<uint, int> indices = _prefixOwnerIndices[prefix];
|
||||
if (indices.ContainsKey(entityId))
|
||||
continue;
|
||||
Stack<int> free = _prefixFreeSlots[prefix];
|
||||
if (free.TryPop(out int freeIndex))
|
||||
{
|
||||
slots[freeIndex] = entityId;
|
||||
indices[entityId] = freeIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
indices[entityId] = slots.Count;
|
||||
slots.Add(entityId);
|
||||
}
|
||||
OwnerPrefixMembershipChanged?.Invoke(entityId, prefix);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void RemoveOwnerPrefixMembership(uint entityId)
|
||||
{
|
||||
if (_ownerPrefixes.Remove(entityId, out HashSet<uint>? prefixes))
|
||||
{
|
||||
foreach (uint prefix in prefixes)
|
||||
{
|
||||
if (!_prefixOwnerIndices.TryGetValue(
|
||||
prefix,
|
||||
out Dictionary<uint, int>? indices)
|
||||
|| !indices.Remove(entityId, out int slotIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_prefixOwnerSlots[prefix][slotIndex] = 0u;
|
||||
_prefixFreeSlots[prefix].Push(slotIndex);
|
||||
ReleaseEmptyPrefixContainer(prefix, indices);
|
||||
OwnerPrefixMembershipChanged?.Invoke(entityId, prefix);
|
||||
}
|
||||
}
|
||||
if (_ownerIndices.Remove(entityId, out int ownerSlot))
|
||||
{
|
||||
_ownerSlots[ownerSlot] = 0u;
|
||||
_ownerFreeSlots.Push(ownerSlot);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureOwnerSlot(uint entityId)
|
||||
{
|
||||
if (_ownerIndices.ContainsKey(entityId))
|
||||
return;
|
||||
if (_ownerFreeSlots.TryPop(out int freeIndex))
|
||||
{
|
||||
_ownerSlots[freeIndex] = entityId;
|
||||
_ownerIndices[entityId] = freeIndex;
|
||||
return;
|
||||
}
|
||||
_ownerIndices[entityId] = _ownerSlots.Count;
|
||||
_ownerSlots.Add(entityId);
|
||||
}
|
||||
|
||||
private void ReleaseEmptyPrefixContainer(
|
||||
uint prefix,
|
||||
Dictionary<uint, int> indices)
|
||||
{
|
||||
if (indices.Count != 0)
|
||||
return;
|
||||
// An in-flight scan retains its captured List reference and observes
|
||||
// only tombstones. A future owner gets a fresh compact container.
|
||||
_prefixOwnerSlots.Remove(prefix);
|
||||
_prefixOwnerIndices.Remove(prefix);
|
||||
_prefixFreeSlots.Remove(prefix);
|
||||
}
|
||||
|
||||
internal readonly record struct RetainedRefloodOwnerScanStep(
|
||||
bool Completed,
|
||||
bool Stable,
|
||||
bool HasOwner,
|
||||
uint OwnerId,
|
||||
ulong SourceMutationVersion);
|
||||
uint OwnerId);
|
||||
|
||||
/// <summary>
|
||||
/// The flood's data source (cells, buildings, terrain origins). Wired by
|
||||
|
|
@ -193,7 +361,8 @@ public sealed class ShadowObjectRegistry
|
|||
uint state = 0u,
|
||||
EntityCollisionFlags flags = EntityCollisionFlags.None,
|
||||
uint seedCellId = 0u,
|
||||
bool isStatic = true)
|
||||
bool isStatic = true,
|
||||
bool publishMutation = true)
|
||||
{
|
||||
// Flood FIRST: retail keeps the previous shadows when the new cell
|
||||
// array would be empty (SetPositionInternal num_cells gate,
|
||||
|
|
@ -211,7 +380,7 @@ public sealed class ShadowObjectRegistry
|
|||
FloodCache, seed, spheres, spheres.Length, isStatic);
|
||||
if (cellSet.Count == 0) return;
|
||||
|
||||
Deregister(entityId);
|
||||
DeregisterCore(entityId, publishMutation: false);
|
||||
|
||||
var entry = new ShadowEntry(entityId, gfxObjId, worldPos, rotation, radius,
|
||||
collisionType, cylHeight, scale, state, flags);
|
||||
|
|
@ -227,7 +396,10 @@ public sealed class ShadowObjectRegistry
|
|||
_entityReg[entityId] = new RegistrationRecord(
|
||||
seed, worldPos, rotation, state, flags, isStatic,
|
||||
IsMultiPart: false, gfxObjId, radius, collisionType, cylHeight, scale);
|
||||
BumpOwnerVersion(entityId);
|
||||
if (publishMutation)
|
||||
BumpOwnerVersion(entityId);
|
||||
else
|
||||
RefreshOwnerPrefixIndex(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -256,7 +428,8 @@ public sealed class ShadowObjectRegistry
|
|||
EntityCollisionFlags flags,
|
||||
float worldOffsetX, float worldOffsetY, uint landblockId,
|
||||
uint seedCellId = 0u,
|
||||
bool isStatic = false)
|
||||
bool isStatic = false,
|
||||
bool publishMutation = true)
|
||||
{
|
||||
if (shapes.Count == 0) { Deregister(entityId); return; }
|
||||
|
||||
|
|
@ -271,7 +444,7 @@ public sealed class ShadowObjectRegistry
|
|||
FloodCache, seed, floodSpheres, floodSpheres.Count, isStatic);
|
||||
if (cellSet.Count == 0) return;
|
||||
|
||||
Deregister(entityId);
|
||||
DeregisterCore(entityId, publishMutation: false);
|
||||
_entityShapes[entityId] = shapes;
|
||||
var allCells = new List<uint>(cellSet.Count);
|
||||
|
||||
|
|
@ -307,7 +480,10 @@ public sealed class ShadowObjectRegistry
|
|||
seed, entityWorldPos, entityWorldRot, state, flags, isStatic,
|
||||
IsMultiPart: true, GfxObjId: 0u, Radius: 0f,
|
||||
CollisionType: ShadowCollisionType.BSP, CylHeight: 0f, Scale: 1f);
|
||||
BumpOwnerVersion(entityId);
|
||||
if (publishMutation)
|
||||
BumpOwnerVersion(entityId);
|
||||
else
|
||||
RefreshOwnerPrefixIndex(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -635,7 +811,8 @@ public sealed class ShadowObjectRegistry
|
|||
0f,
|
||||
lbPrefix,
|
||||
reg.SeedCellId,
|
||||
reg.IsStatic);
|
||||
reg.IsStatic,
|
||||
publishMutation: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -654,7 +831,8 @@ public sealed class ShadowObjectRegistry
|
|||
reg.State,
|
||||
reg.Flags,
|
||||
reg.SeedCellId,
|
||||
reg.IsStatic);
|
||||
reg.IsStatic,
|
||||
publishMutation: false);
|
||||
}
|
||||
|
||||
// Register is also the authoritative movement/replacement API and
|
||||
|
|
@ -674,6 +852,7 @@ public sealed class ShadowObjectRegistry
|
|||
if (withdrawn.Count == 0)
|
||||
_withdrawnPrefixesByOwner.Remove(entityId);
|
||||
}
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -711,12 +890,16 @@ public sealed class ShadowObjectRegistry
|
|||
if (retained)
|
||||
{
|
||||
_entityReg[entityId] = retainedRegistration! with { State = newState };
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
if (!_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||
{
|
||||
if (retained)
|
||||
BumpOwnerVersion(entityId);
|
||||
return; // not registered — no-op
|
||||
|
||||
}
|
||||
|
||||
foreach (var cellId in cellIds)
|
||||
{
|
||||
if (!_cells.TryGetValue(cellId, out var list)) continue;
|
||||
|
|
@ -727,10 +910,16 @@ public sealed class ShadowObjectRegistry
|
|||
}
|
||||
}
|
||||
|
||||
if (retained)
|
||||
BumpOwnerVersion(entityId);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Remove an entity from all cells it was registered in.</summary>
|
||||
public void Deregister(uint entityId)
|
||||
=> DeregisterCore(entityId, publishMutation: true);
|
||||
|
||||
private void DeregisterCore(uint entityId, bool publishMutation)
|
||||
{
|
||||
bool existed = _entityReg.ContainsKey(entityId)
|
||||
|| _entityToCells.ContainsKey(entityId)
|
||||
|
|
@ -749,8 +938,12 @@ public sealed class ShadowObjectRegistry
|
|||
_entityReg.Remove(entityId);
|
||||
_suspendedEntities.Remove(entityId);
|
||||
_withdrawnPrefixesByOwner.Remove(entityId);
|
||||
if (existed)
|
||||
if (existed && publishMutation)
|
||||
{
|
||||
BumpOwnerVersion(entityId);
|
||||
RemoveOwnerPrefixMembership(entityId);
|
||||
_ownerVersions.Remove(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveOwnerRows(
|
||||
|
|
@ -879,6 +1072,59 @@ public sealed class ShadowObjectRegistry
|
|||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires one logical owner's rows from a streamed-out prefix. This is
|
||||
/// the owner-granular form used by the collision-generation retirement
|
||||
/// cursor; it preserves the same static/dynamic lifetime rules as
|
||||
/// <see cref="RemoveLandblock"/> without scanning the complete registry.
|
||||
/// </summary>
|
||||
internal void RetireOwnerFromLandblock(uint entityId, uint landblockId)
|
||||
{
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
if (_entityReg.TryGetValue(
|
||||
entityId,
|
||||
out RegistrationRecord? registration)
|
||||
&& registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u) == prefix)
|
||||
{
|
||||
DeregisterCore(entityId, publishMutation: false);
|
||||
RemoveOwnerPrefixMembership(entityId);
|
||||
_ownerVersions.Remove(entityId);
|
||||
return;
|
||||
}
|
||||
if (!_entityToCells.TryGetValue(entityId, out List<uint>? cells))
|
||||
return;
|
||||
|
||||
bool touched = false;
|
||||
for (int index = cells.Count - 1; index >= 0; index--)
|
||||
{
|
||||
uint cellId = cells[index];
|
||||
if ((cellId & 0xFFFF0000u) != prefix)
|
||||
continue;
|
||||
touched = true;
|
||||
cells.RemoveAt(index);
|
||||
if (_cells.TryGetValue(cellId, out List<ShadowEntry>? entries))
|
||||
{
|
||||
RemoveOwnerRows(entries, entityId);
|
||||
if (entries.Count == 0)
|
||||
_cells.Remove(cellId);
|
||||
}
|
||||
}
|
||||
if (!touched)
|
||||
return;
|
||||
if (!_withdrawnPrefixesByOwner.TryGetValue(
|
||||
entityId,
|
||||
out HashSet<uint>? withdrawn))
|
||||
{
|
||||
withdrawn = new HashSet<uint>();
|
||||
_withdrawnPrefixesByOwner[entityId] = withdrawn;
|
||||
}
|
||||
withdrawn.Add(prefix);
|
||||
if (cells.Count == 0)
|
||||
_entityToCells.Remove(entityId);
|
||||
BumpOwnerVersion(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All objects registered in a specific cell — retail
|
||||
/// <c>CObjCell::find_obj_collisions</c> iterating only
|
||||
|
|
@ -925,44 +1171,35 @@ public sealed class ShadowObjectRegistry
|
|||
(cell & 0xFFFF0000u) == (landblockId & 0xFFFF0000u));
|
||||
|
||||
/// <summary>
|
||||
/// Copies the committed registry into an off-side collision generation.
|
||||
/// All mutable lists and sets are cloned; immutable registration and shape
|
||||
/// payloads may be shared.
|
||||
/// Mirrors one ordinary active-world mutation into an off-side generation.
|
||||
/// Target-prefix owners may subsequently be reflooded against the staged
|
||||
/// topology; unrelated owners retain these exact active rows.
|
||||
/// </summary>
|
||||
internal void CopyCollisionStateFrom(
|
||||
internal void MirrorOwnerFrom(
|
||||
ShadowObjectRegistry source,
|
||||
PhysicsDataCache stagingCache)
|
||||
uint entityId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(stagingCache);
|
||||
Clear();
|
||||
DataCache = stagingCache;
|
||||
foreach ((uint cellId, List<ShadowEntry> entries) in source._cells)
|
||||
_cells[cellId] = new List<ShadowEntry>(entries);
|
||||
foreach ((uint ownerId, List<uint> cells) in source._entityToCells)
|
||||
_entityToCells[ownerId] = new List<uint>(cells);
|
||||
foreach (uint ownerId in source._suspendedEntities)
|
||||
_suspendedEntities.Add(ownerId);
|
||||
foreach ((uint ownerId, HashSet<uint> prefixes) in
|
||||
source._withdrawnPrefixesByOwner)
|
||||
DeregisterCore(entityId, publishMutation: false);
|
||||
if (source.TryCaptureOwnerState(
|
||||
entityId,
|
||||
out PreparedShadowOwnerState? state)
|
||||
&& state is not null)
|
||||
{
|
||||
_withdrawnPrefixesByOwner[ownerId] = new HashSet<uint>(prefixes);
|
||||
InstallOwnerState(state);
|
||||
_ownerVersions[entityId] = source.GetOwnerVersion(entityId);
|
||||
}
|
||||
foreach ((uint ownerId, IReadOnlyList<ShadowShape> shapes) in
|
||||
source._entityShapes)
|
||||
else
|
||||
{
|
||||
_entityShapes[ownerId] = shapes;
|
||||
RemoveOwnerPrefixMembership(entityId);
|
||||
_ownerVersions.Remove(entityId);
|
||||
}
|
||||
foreach ((uint ownerId, RegistrationRecord registration) in
|
||||
source._entityReg)
|
||||
{
|
||||
_entityReg[ownerId] = registration;
|
||||
}
|
||||
foreach ((uint ownerId, ulong version) in source._ownerVersions)
|
||||
_ownerVersions[ownerId] = version;
|
||||
_mutationVersion = source._mutationVersion;
|
||||
}
|
||||
|
||||
internal int CaptureOwnerSlotLimit() => _ownerSlots.Count;
|
||||
|
||||
internal uint GetOwnerSlot(int index) => _ownerSlots[index];
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes one staging owner from the exact active payload, then floods
|
||||
/// it against the staging generation's complete cell graph. The returned
|
||||
|
|
@ -975,19 +1212,27 @@ public sealed class ShadowObjectRegistry
|
|||
out ulong sourceVersion)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
Deregister(entityId);
|
||||
sourceVersion = source.GetOwnerVersion(entityId);
|
||||
if (!source._entityReg.TryGetValue(
|
||||
entityId,
|
||||
out RegistrationRecord? registration)
|
||||
|| source._suspendedEntities.Contains(entityId)
|
||||
|| (registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u)
|
||||
== (landblockId & 0xFFFF0000u))
|
||||
|| !source.OwnerTouchesLandblock(entityId, landblockId))
|
||||
{
|
||||
// A target-local refresh is not a global owner deletion. Preserve
|
||||
// the exact active rows when the live owner has moved elsewhere.
|
||||
MirrorOwnerFrom(source, entityId);
|
||||
return false;
|
||||
}
|
||||
if (registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u)
|
||||
== (landblockId & 0xFFFF0000u))
|
||||
{
|
||||
// Target statics come from the staged landblock itself.
|
||||
return false;
|
||||
}
|
||||
|
||||
DeregisterCore(entityId, publishMutation: false);
|
||||
|
||||
if (registration.IsMultiPart
|
||||
&& source._entityShapes.TryGetValue(
|
||||
|
|
@ -1005,7 +1250,8 @@ public sealed class ShadowObjectRegistry
|
|||
0f,
|
||||
landblockId,
|
||||
registration.SeedCellId,
|
||||
isStatic: registration.IsStatic);
|
||||
isStatic: registration.IsStatic,
|
||||
publishMutation: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1024,7 +1270,8 @@ public sealed class ShadowObjectRegistry
|
|||
registration.State,
|
||||
registration.Flags,
|
||||
registration.SeedCellId,
|
||||
isStatic: registration.IsStatic);
|
||||
isStatic: registration.IsStatic,
|
||||
publishMutation: false);
|
||||
}
|
||||
|
||||
if (source._withdrawnPrefixesByOwner.TryGetValue(
|
||||
|
|
@ -1041,28 +1288,21 @@ public sealed class ShadowObjectRegistry
|
|||
if (retainedWithdrawn.Count != 0)
|
||||
_withdrawnPrefixesByOwner[entityId] = retainedWithdrawn;
|
||||
}
|
||||
RefreshOwnerPrefixIndex(entityId);
|
||||
_ownerVersions[entityId] = sourceVersion;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
ShadowObjectRegistry staging,
|
||||
uint landblockId,
|
||||
IReadOnlyDictionary<uint, ulong> expectedRetainedVersions) => new(
|
||||
IReadOnlyList<uint> expectedRetainedOwners) => new(
|
||||
this,
|
||||
staging,
|
||||
landblockId,
|
||||
expectedRetainedVersions);
|
||||
expectedRetainedOwners);
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedLandblockShadowReplacement replacement)
|
||||
{
|
||||
for (int index = 0; index < replacement.OwnerIds.Count; index++)
|
||||
Deregister(replacement.OwnerIds[index]);
|
||||
for (int index = 0; index < replacement.OwnerStates.Count; index++)
|
||||
InstallOwnerState(replacement.OwnerStates[index]);
|
||||
}
|
||||
|
||||
private bool OwnerTouchesLandblock(uint entityId, uint landblockId)
|
||||
internal bool OwnerTouchesLandblock(uint entityId, uint landblockId)
|
||||
{
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
if (!_entityReg.TryGetValue(entityId, out RegistrationRecord? record))
|
||||
|
|
@ -1080,6 +1320,43 @@ public sealed class ShadowObjectRegistry
|
|||
&& withdrawn.Contains(prefix);
|
||||
}
|
||||
|
||||
internal bool IsStaticOwnerRootedIn(uint entityId, uint landblockId) =>
|
||||
_entityReg.TryGetValue(entityId, out RegistrationRecord? registration)
|
||||
&& registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u)
|
||||
== (landblockId & 0xFFFF0000u);
|
||||
|
||||
internal bool TryGetStaticOwnerRootPrefix(
|
||||
uint entityId,
|
||||
out uint landblockPrefix)
|
||||
{
|
||||
if (_entityReg.TryGetValue(
|
||||
entityId,
|
||||
out RegistrationRecord? registration)
|
||||
&& registration.IsStatic)
|
||||
{
|
||||
landblockPrefix = registration.SeedCellId & 0xFFFF0000u;
|
||||
return true;
|
||||
}
|
||||
landblockPrefix = 0u;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool HasLogicalOwner(uint entityId) =>
|
||||
_entityReg.ContainsKey(entityId);
|
||||
|
||||
public int PrefixOwnerSlotCapacityForDiagnostics(uint landblockId) =>
|
||||
_prefixOwnerSlots.TryGetValue(
|
||||
landblockId & 0xFFFF0000u,
|
||||
out List<uint>? slots)
|
||||
? slots.Count
|
||||
: 0;
|
||||
|
||||
public int OwnerVersionCountForDiagnostics => _ownerVersions.Count;
|
||||
|
||||
public int PrefixOwnerContainerCountForDiagnostics =>
|
||||
_prefixOwnerSlots.Count;
|
||||
|
||||
private bool TryCaptureOwnerState(
|
||||
uint entityId,
|
||||
out PreparedShadowOwnerState? state)
|
||||
|
|
@ -1148,63 +1425,65 @@ public sealed class ShadowObjectRegistry
|
|||
private readonly ShadowObjectRegistry _active;
|
||||
private readonly ShadowObjectRegistry _staging;
|
||||
private readonly uint _prefix;
|
||||
private readonly ulong _sourceMutationVersion;
|
||||
private readonly IReadOnlyList<uint> _expected;
|
||||
private readonly List<uint>? _activeSlots;
|
||||
private readonly List<uint>? _stagingSlots;
|
||||
private readonly int _activeSlotLimit;
|
||||
private readonly int _stagingSlotLimit;
|
||||
private readonly HashSet<uint> _owners = new();
|
||||
private readonly List<uint> _ownerIds = new();
|
||||
private readonly List<PreparedShadowOwnerState> _states = new();
|
||||
private IEnumerator<KeyValuePair<uint, ulong>>? _expectedEnumerator;
|
||||
private Dictionary<uint, RegistrationRecord>.Enumerator _registrationEnumerator;
|
||||
private HashSet<uint>.Enumerator _ownerEnumerator;
|
||||
private readonly List<PreparedShadowOwnerSlot> _states = new();
|
||||
private readonly Dictionary<uint, int> _stateIndex = new();
|
||||
private int _expectedIndex;
|
||||
private int _activeSlotIndex;
|
||||
private int _stagingSlotIndex;
|
||||
private int _ownerIndex;
|
||||
private int _phase;
|
||||
|
||||
internal LandblockReplacementBuilder(
|
||||
ShadowObjectRegistry active,
|
||||
ShadowObjectRegistry staging,
|
||||
uint landblockId,
|
||||
IReadOnlyDictionary<uint, ulong> expected)
|
||||
IReadOnlyList<uint> expected)
|
||||
{
|
||||
_active = active;
|
||||
_staging = staging;
|
||||
_prefix = landblockId & 0xFFFF0000u;
|
||||
_sourceMutationVersion = active.MutationVersion;
|
||||
_expectedEnumerator = expected.GetEnumerator();
|
||||
_expected = expected;
|
||||
active._prefixOwnerSlots.TryGetValue(
|
||||
_prefix,
|
||||
out _activeSlots);
|
||||
staging._prefixOwnerSlots.TryGetValue(
|
||||
_prefix,
|
||||
out _stagingSlots);
|
||||
_activeSlotLimit = _activeSlots?.Count ?? 0;
|
||||
_stagingSlotLimit = _stagingSlots?.Count ?? 0;
|
||||
}
|
||||
|
||||
internal int WorkUnits { get; private set; }
|
||||
internal bool IsStable =>
|
||||
_active.MutationVersion == _sourceMutationVersion;
|
||||
internal PreparedLandblockShadowReplacement? Prepared { get; private set; }
|
||||
|
||||
internal bool Advance()
|
||||
{
|
||||
if (!IsStable)
|
||||
return true;
|
||||
switch (_phase)
|
||||
{
|
||||
case 0:
|
||||
if (_expectedEnumerator!.MoveNext())
|
||||
if (_expectedIndex < _expected.Count)
|
||||
{
|
||||
(uint ownerId, ulong version) = _expectedEnumerator.Current;
|
||||
if (_active.GetOwnerVersion(ownerId) != version
|
||||
|| !_active.IsRetainedRefloodOwner(ownerId, _prefix))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
AddOwner(ownerId);
|
||||
AddOwner(_expected[_expectedIndex++]);
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_expectedEnumerator.Dispose();
|
||||
_expectedEnumerator = null;
|
||||
_registrationEnumerator = _active._entityReg.GetEnumerator();
|
||||
_phase++;
|
||||
return false;
|
||||
case 1:
|
||||
if (_registrationEnumerator.MoveNext())
|
||||
if (_activeSlotIndex < _activeSlotLimit)
|
||||
{
|
||||
(uint ownerId, RegistrationRecord registration) =
|
||||
_registrationEnumerator.Current;
|
||||
if (registration.IsStatic
|
||||
uint ownerId = _activeSlots![_activeSlotIndex++];
|
||||
if (_active._entityReg.TryGetValue(
|
||||
ownerId,
|
||||
out RegistrationRecord? registration)
|
||||
&& registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
|
||||
{
|
||||
AddOwner(ownerId);
|
||||
|
|
@ -1212,16 +1491,16 @@ public sealed class ShadowObjectRegistry
|
|||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_registrationEnumerator.Dispose();
|
||||
_registrationEnumerator = _staging._entityReg.GetEnumerator();
|
||||
_phase++;
|
||||
return false;
|
||||
case 2:
|
||||
if (_registrationEnumerator.MoveNext())
|
||||
if (_stagingSlotIndex < _stagingSlotLimit)
|
||||
{
|
||||
(uint ownerId, RegistrationRecord registration) =
|
||||
_registrationEnumerator.Current;
|
||||
if (registration.IsStatic
|
||||
uint ownerId = _stagingSlots![_stagingSlotIndex++];
|
||||
if (_staging._entityReg.TryGetValue(
|
||||
ownerId,
|
||||
out RegistrationRecord? registration)
|
||||
&& registration.IsStatic
|
||||
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
|
||||
{
|
||||
AddOwner(ownerId);
|
||||
|
|
@ -1229,32 +1508,24 @@ public sealed class ShadowObjectRegistry
|
|||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_registrationEnumerator.Dispose();
|
||||
_ownerEnumerator = _owners.GetEnumerator();
|
||||
_phase++;
|
||||
return false;
|
||||
case 3:
|
||||
if (_ownerEnumerator.MoveNext())
|
||||
if (_ownerIndex < _ownerIds.Count)
|
||||
{
|
||||
uint ownerId = _ownerEnumerator.Current;
|
||||
if (_staging.TryCaptureOwnerState(
|
||||
ownerId,
|
||||
out PreparedShadowOwnerState? state)
|
||||
&& state is not null)
|
||||
{
|
||||
_states.Add(state);
|
||||
}
|
||||
uint ownerId = _ownerIds[_ownerIndex++];
|
||||
_staging.TryCaptureOwnerState(
|
||||
ownerId,
|
||||
out PreparedShadowOwnerState? state);
|
||||
_stateIndex[ownerId] = _states.Count;
|
||||
_states.Add(new PreparedShadowOwnerSlot(ownerId, state));
|
||||
WorkUnits++;
|
||||
return false;
|
||||
}
|
||||
_ownerEnumerator.Dispose();
|
||||
if (IsStable)
|
||||
{
|
||||
Prepared = new PreparedLandblockShadowReplacement(
|
||||
_prefix,
|
||||
_ownerIds,
|
||||
_states);
|
||||
}
|
||||
Prepared = new PreparedLandblockShadowReplacement(
|
||||
_prefix,
|
||||
_ownerIds,
|
||||
_states);
|
||||
_phase++;
|
||||
return true;
|
||||
default:
|
||||
|
|
@ -1262,20 +1533,34 @@ public sealed class ShadowObjectRegistry
|
|||
}
|
||||
}
|
||||
|
||||
private void AddOwner(uint ownerId)
|
||||
internal void AddOwner(uint ownerId)
|
||||
{
|
||||
if (_owners.Add(ownerId))
|
||||
_ownerIds.Add(ownerId);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
internal void RefreshOwner(uint ownerId)
|
||||
{
|
||||
_expectedEnumerator?.Dispose();
|
||||
if (_phase is 1 or 2)
|
||||
_registrationEnumerator.Dispose();
|
||||
if (_phase == 3)
|
||||
_ownerEnumerator.Dispose();
|
||||
AddOwner(ownerId);
|
||||
if (_stateIndex.TryGetValue(ownerId, out int index))
|
||||
{
|
||||
_staging.TryCaptureOwnerState(
|
||||
ownerId,
|
||||
out PreparedShadowOwnerState? state);
|
||||
_states[index].State = state;
|
||||
return;
|
||||
}
|
||||
if (_phase > 3)
|
||||
{
|
||||
_staging.TryCaptureOwnerState(
|
||||
ownerId,
|
||||
out PreparedShadowOwnerState? state);
|
||||
_stateIndex[ownerId] = _states.Count;
|
||||
_states.Add(new PreparedShadowOwnerSlot(ownerId, state));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
private bool IsRetainedRefloodOwner(uint ownerId, uint landblockId)
|
||||
|
|
@ -1296,7 +1581,7 @@ public sealed class ShadowObjectRegistry
|
|||
internal PreparedLandblockShadowReplacement(
|
||||
uint landblockPrefix,
|
||||
IReadOnlyList<uint> ownerIds,
|
||||
IReadOnlyList<PreparedShadowOwnerState> ownerStates)
|
||||
IReadOnlyList<PreparedShadowOwnerSlot> ownerStates)
|
||||
{
|
||||
LandblockPrefix = landblockPrefix;
|
||||
OwnerIds = ownerIds;
|
||||
|
|
@ -1305,7 +1590,21 @@ public sealed class ShadowObjectRegistry
|
|||
|
||||
internal uint LandblockPrefix { get; }
|
||||
internal IReadOnlyList<uint> OwnerIds { get; }
|
||||
internal IReadOnlyList<PreparedShadowOwnerState> OwnerStates { get; }
|
||||
internal IReadOnlyList<PreparedShadowOwnerSlot> OwnerStates { get; }
|
||||
}
|
||||
|
||||
internal sealed class PreparedShadowOwnerSlot
|
||||
{
|
||||
internal PreparedShadowOwnerSlot(
|
||||
uint entityId,
|
||||
PreparedShadowOwnerState? state)
|
||||
{
|
||||
EntityId = entityId;
|
||||
State = state;
|
||||
}
|
||||
|
||||
internal uint EntityId { get; }
|
||||
internal PreparedShadowOwnerState? State { get; set; }
|
||||
}
|
||||
|
||||
internal sealed record PreparedShadowOwnerState(
|
||||
|
|
@ -1334,7 +1633,15 @@ public sealed class ShadowObjectRegistry
|
|||
_entityShapes.Clear();
|
||||
_entityReg.Clear();
|
||||
_ownerVersions.Clear();
|
||||
_mutationVersion = 0UL;
|
||||
_ownerPrefixes.Clear();
|
||||
_prefixOwnerSlots.Clear();
|
||||
_prefixOwnerIndices.Clear();
|
||||
_prefixFreeSlots.Clear();
|
||||
_ownerSlots.Clear();
|
||||
_ownerIndices.Clear();
|
||||
_ownerFreeSlots.Clear();
|
||||
_prefixScratch.Clear();
|
||||
_removedPrefixScratch.Clear();
|
||||
_fallback = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,24 @@ namespace AcDream.Core.World.Cells;
|
|||
/// </summary>
|
||||
public sealed class CellGraph
|
||||
{
|
||||
private readonly ConcurrentDictionary<uint, EnvCell> _envCells = new();
|
||||
private readonly ConcurrentDictionary<uint, CellGraphTerrain> _terrain = new();
|
||||
private readonly CollisionWorldStateSlot _collisionWorld;
|
||||
private ConcurrentDictionary<uint, EnvCell> _envCells =>
|
||||
_collisionWorld.Current.EnvCells;
|
||||
private ConcurrentDictionary<uint, CellGraphTerrain> _terrain =>
|
||||
_collisionWorld.Current.Terrain;
|
||||
private ConcurrentDictionary<uint, ObjCell> _outdoorCells =>
|
||||
_collisionWorld.Current.OutdoorCells;
|
||||
|
||||
public CellGraph()
|
||||
: this(new CollisionWorldStateSlot())
|
||||
{
|
||||
}
|
||||
|
||||
internal CellGraph(CollisionWorldStateSlot collisionWorld)
|
||||
{
|
||||
_collisionWorld = collisionWorld
|
||||
?? throw new ArgumentNullException(nameof(collisionWorld));
|
||||
}
|
||||
|
||||
/// <summary>The player's current cell — the render/lighting root. Written ONLY at the
|
||||
/// player chokepoint <see cref="AcDream.Core.Physics.PhysicsEngine.UpdatePlayerCurrCell"/>
|
||||
|
|
@ -34,8 +50,21 @@ public sealed class CellGraph
|
|||
|
||||
/// <param name="landblockPrefix">Any id in the cell's landblock; masked to (id & 0xFFFF0000).</param>
|
||||
public void RegisterTerrain(uint landblockPrefix, TerrainSurface terrain, Vector3 worldOrigin)
|
||||
=> _terrain[landblockPrefix & 0xFFFF0000u] =
|
||||
new CellGraphTerrain(terrain, 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// World origin (SW corner) of the landblock containing <paramref name="id"/>,
|
||||
|
|
@ -65,6 +94,8 @@ public sealed class CellGraph
|
|||
CurrCell = null;
|
||||
}
|
||||
_terrain.TryRemove(lb, out _);
|
||||
for (uint low = 1u; low <= 0x40u; low++)
|
||||
_outdoorCells.TryRemove(lb | low, out _);
|
||||
foreach (var id in new List<uint>(_envCells.Keys))
|
||||
if ((id & 0xFFFF0000u) == lb) _envCells.TryRemove(id, out _);
|
||||
}
|
||||
|
|
@ -95,9 +126,9 @@ public sealed class CellGraph
|
|||
|
||||
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);
|
||||
return _outdoorCells.TryGetValue(id, out ObjCell? cell)
|
||||
? cell
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -127,63 +158,10 @@ public sealed class CellGraph
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an immutable-reference snapshot for collision-generation
|
||||
/// preparation. EnvCell and TerrainSurface records are immutable after
|
||||
/// publication, so copying the registries is sufficient; the active graph
|
||||
/// remains untouched while the staging graph is rebuilt.
|
||||
/// </summary>
|
||||
internal CellGraph CreateCollisionStagingCopy()
|
||||
{
|
||||
var copy = new CellGraph { CurrCell = CurrCell };
|
||||
foreach ((uint id, EnvCell cell) in _envCells)
|
||||
copy._envCells.TryAdd(id, cell);
|
||||
foreach ((uint id, CellGraphTerrain terrain) in _terrain)
|
||||
{
|
||||
copy._terrain.TryAdd(id, terrain);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
|
||||
CellGraph staging,
|
||||
uint landblockId) => new(this, staging, landblockId);
|
||||
|
||||
internal void CommitLandblockReplacement(
|
||||
PreparedCellGraphLandblock replacement)
|
||||
{
|
||||
uint currentCellId = CurrCell?.Id ?? 0u;
|
||||
for (int index = 0; index < replacement.EnvCellIdsToRemove.Count; index++)
|
||||
_envCells.TryRemove(replacement.EnvCellIdsToRemove[index], out _);
|
||||
if (replacement.HasTerrain)
|
||||
{
|
||||
_terrain[replacement.LandblockPrefix] = replacement.Terrain!;
|
||||
}
|
||||
else
|
||||
{
|
||||
_terrain.TryRemove(replacement.LandblockPrefix, out _);
|
||||
}
|
||||
for (int index = 0; index < replacement.EnvCells.Count; index++)
|
||||
{
|
||||
(uint id, EnvCell cell) = replacement.EnvCells[index];
|
||||
_envCells[id] = cell;
|
||||
}
|
||||
|
||||
uint desiredCurrentCellId =
|
||||
(currentCellId & 0xFFFF0000u) == replacement.LandblockPrefix
|
||||
? currentCellId
|
||||
: currentCellId == 0u
|
||||
&& (replacement.CurrentCellId & 0xFFFF0000u)
|
||||
== replacement.LandblockPrefix
|
||||
? replacement.CurrentCellId
|
||||
: 0u;
|
||||
if (desiredCurrentCellId != 0u)
|
||||
CurrCell = GetVisible(desiredCurrentCellId);
|
||||
else if ((currentCellId & 0xFFFF0000u)
|
||||
== replacement.LandblockPrefix)
|
||||
CurrCell = null;
|
||||
}
|
||||
|
||||
internal sealed class LandblockReplacementBuilder : IDisposable
|
||||
{
|
||||
private readonly CellGraph _active;
|
||||
|
|
@ -249,8 +227,7 @@ public sealed class CellGraph
|
|||
_removeIds,
|
||||
_envCells,
|
||||
hasTerrain,
|
||||
terrain,
|
||||
_staging.CurrCell?.Id ?? 0u);
|
||||
terrain);
|
||||
_phase = 2;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -271,8 +248,7 @@ internal sealed record PreparedCellGraphLandblock(
|
|||
IReadOnlyList<uint> EnvCellIdsToRemove,
|
||||
IReadOnlyList<KeyValuePair<uint, EnvCell>> EnvCells,
|
||||
bool HasTerrain,
|
||||
CellGraphTerrain? Terrain,
|
||||
uint CurrentCellId);
|
||||
CellGraphTerrain? Terrain);
|
||||
|
||||
internal sealed record CellGraphTerrain(
|
||||
TerrainSurface Terrain,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue