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