fix(physics): make collision activation starvation-free

This commit is contained in:
Erik 2026-07-31 18:34:46 +02:00
parent d94145e6b8
commit 6b28ff999c
14 changed files with 4637 additions and 474 deletions

View file

@ -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