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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue