fix(physics): seal collision generations before activation

This commit is contained in:
Erik 2026-07-31 15:53:05 +02:00
parent be94bc9b06
commit d94145e6b8
15 changed files with 1556 additions and 410 deletions

View file

@ -52,6 +52,7 @@ public sealed class ShadowObjectRegistry
/// </summary>
private readonly Dictionary<uint, RegistrationRecord> _entityReg = new();
private readonly Dictionary<uint, ulong> _ownerVersions = new();
private ulong _mutationVersion;
internal sealed record RegistrationRecord(
uint SeedCellId,
@ -76,8 +77,88 @@ public sealed class ShadowObjectRegistry
private void BumpOwnerVersion(uint entityId)
{
_ownerVersions[entityId] = checked(GetOwnerVersion(entityId) + 1UL);
_mutationVersion = checked(_mutationVersion + 1UL);
}
internal ulong MutationVersion => _mutationVersion;
internal RetainedRefloodOwnerScan CreateRetainedRefloodOwnerScan(
uint landblockId) => new(this, landblockId & 0xFFFF0000u);
internal sealed class RetainedRefloodOwnerScan : IDisposable
{
private readonly ShadowObjectRegistry _owner;
private readonly uint _prefix;
private readonly ulong _sourceMutationVersion;
private Dictionary<uint, RegistrationRecord>.Enumerator _enumerator;
private bool _completed;
internal RetainedRefloodOwnerScan(
ShadowObjectRegistry owner,
uint prefix)
{
_owner = owner;
_prefix = prefix;
_sourceMutationVersion = owner.MutationVersion;
_enumerator = owner._entityReg.GetEnumerator();
}
internal RetainedRefloodOwnerScanStep Advance()
{
if (_completed)
{
return new RetainedRefloodOwnerScanStep(
Completed: true,
Stable: _owner.MutationVersion == _sourceMutationVersion,
HasOwner: false,
OwnerId: 0u,
SourceMutationVersion: _sourceMutationVersion);
}
if (_owner.MutationVersion != _sourceMutationVersion)
{
_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);
}
(uint ownerId, RegistrationRecord registration) =
_enumerator.Current;
bool retained = !_owner._suspendedEntities.Contains(ownerId)
&& (!registration.IsStatic
|| (registration.SeedCellId & 0xFFFF0000u) != _prefix)
&& _owner.OwnerTouchesLandblock(ownerId, _prefix);
return new RetainedRefloodOwnerScanStep(
Completed: false,
Stable: true,
HasOwner: retained,
OwnerId: retained ? ownerId : 0u,
SourceMutationVersion: _sourceMutationVersion);
}
public void Dispose() => _enumerator.Dispose();
}
internal readonly record struct RetainedRefloodOwnerScanStep(
bool Completed,
bool Stable,
bool HasOwner,
uint OwnerId,
ulong SourceMutationVersion);
/// <summary>
/// The flood's data source (cells, buildings, terrain origins). Wired by
/// <see cref="PhysicsEngine"/> when its own <c>DataCache</c> is set.
@ -624,8 +705,14 @@ public sealed class ShadowObjectRegistry
{
// Suspended dynamic objects have no cell rows, but their retained
// registration must still receive authoritative state changes.
if (_entityReg.TryGetValue(entityId, out var retainedRegistration))
_entityReg[entityId] = retainedRegistration with { State = newState };
bool retained = _entityReg.TryGetValue(
entityId,
out RegistrationRecord? retainedRegistration);
if (retained)
{
_entityReg[entityId] = retainedRegistration! with { State = newState };
BumpOwnerVersion(entityId);
}
if (!_entityToCells.TryGetValue(entityId, out var cellIds))
return; // not registered — no-op
@ -640,9 +727,6 @@ public sealed class ShadowObjectRegistry
}
}
if (_entityReg.TryGetValue(entityId, out var reg))
_entityReg[entityId] = reg with { State = newState };
BumpOwnerVersion(entityId);
}
/// <summary>Remove an entity from all cells it was registered in.</summary>
@ -657,7 +741,7 @@ public sealed class ShadowObjectRegistry
foreach (var cellId in cellIds)
{
if (_cells.TryGetValue(cellId, out var list))
list.RemoveAll(e => e.EntityId == entityId);
RemoveOwnerRows(list, entityId);
}
_entityToCells.Remove(entityId);
}
@ -669,6 +753,17 @@ public sealed class ShadowObjectRegistry
BumpOwnerVersion(entityId);
}
private static void RemoveOwnerRows(
List<ShadowEntry> entries,
uint entityId)
{
for (int index = entries.Count - 1; index >= 0; index--)
{
if (entries[index].EntityId == entityId)
entries.RemoveAt(index);
}
}
/// <summary>
/// Logically tear down every static object owned by a landblock, including
/// shadow rows flooded into adjacent landblocks. Dynamic/server-live owners
@ -824,6 +919,11 @@ public sealed class ShadowObjectRegistry
/// <summary>Suspended logical registrations awaiting spatial re-entry.</summary>
public int SuspendedRegistrationCount => _suspendedEntities.Count;
public bool HasOwnerRowsInLandblock(uint ownerId, uint landblockId) =>
_entityToCells.TryGetValue(ownerId, out List<uint>? cells)
&& cells.Exists(cell =>
(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
@ -860,16 +960,7 @@ public sealed class ShadowObjectRegistry
}
foreach ((uint ownerId, ulong version) in source._ownerVersions)
_ownerVersions[ownerId] = version;
}
internal uint[] CaptureDynamicRefloodOwnersForLandblock(
uint landblockId)
{
uint[] owners = CaptureRefloodOwnersForLandblock(landblockId);
return owners.Where(ownerId =>
_entityReg.TryGetValue(ownerId, out RegistrationRecord? record)
&& !record.IsStatic)
.ToArray();
_mutationVersion = source._mutationVersion;
}
/// <summary>
@ -877,7 +968,7 @@ public sealed class ShadowObjectRegistry
/// it against the staging generation's complete cell graph. The returned
/// source version is the commit-time freshness token.
/// </summary>
internal bool RefreshDynamicOwnerFrom(
internal bool RefreshRetainedOwnerFrom(
ShadowObjectRegistry source,
uint entityId,
uint landblockId,
@ -889,8 +980,10 @@ public sealed class ShadowObjectRegistry
if (!source._entityReg.TryGetValue(
entityId,
out RegistrationRecord? registration)
|| registration.IsStatic
|| source._suspendedEntities.Contains(entityId)
|| (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u)
== (landblockId & 0xFFFF0000u))
|| !source.OwnerTouchesLandblock(entityId, landblockId))
{
return false;
@ -912,7 +1005,7 @@ public sealed class ShadowObjectRegistry
0f,
landblockId,
registration.SeedCellId,
isStatic: false);
isStatic: registration.IsStatic);
}
else
{
@ -931,92 +1024,42 @@ public sealed class ShadowObjectRegistry
registration.State,
registration.Flags,
registration.SeedCellId,
isStatic: false);
isStatic: registration.IsStatic);
}
if (source._withdrawnPrefixesByOwner.TryGetValue(
entityId,
out HashSet<uint>? sourceWithdrawn))
{
var retainedWithdrawn = new HashSet<uint>(sourceWithdrawn);
uint prefix = landblockId & 0xFFFF0000u;
if (_entityToCells.TryGetValue(entityId, out List<uint>? cells)
&& cells.Exists(cell => (cell & 0xFFFF0000u) == prefix))
{
retainedWithdrawn.Remove(prefix);
}
if (retainedWithdrawn.Count != 0)
_withdrawnPrefixesByOwner[entityId] = retainedWithdrawn;
}
return true;
}
internal uint[] FindDirtyDynamicOwners(
uint landblockId,
IReadOnlyDictionary<uint, ulong> expectedVersions)
{
var dirty = new HashSet<uint>(
CaptureDynamicRefloodOwnersForLandblock(landblockId));
dirty.UnionWith(expectedVersions.Keys);
dirty.RemoveWhere(ownerId =>
expectedVersions.TryGetValue(ownerId, out ulong expected)
&& OwnerTouchesLandblock(ownerId, landblockId)
&& GetOwnerVersion(ownerId) == expected);
uint[] result = dirty.ToArray();
Array.Sort(result);
return result;
}
internal PreparedLandblockShadowReplacement PrepareLandblockReplacement(
internal LandblockReplacementBuilder CreateLandblockReplacementBuilder(
ShadowObjectRegistry staging,
uint landblockId,
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
{
ArgumentNullException.ThrowIfNull(staging);
uint[] dirty = FindDirtyDynamicOwners(
IReadOnlyDictionary<uint, ulong> expectedRetainedVersions) => new(
this,
staging,
landblockId,
expectedDynamicVersions);
if (dirty.Length != 0)
{
throw new InvalidOperationException(
"Dynamic shadow owners changed before collision generation sealing.");
}
var owners = new HashSet<uint>(CaptureStaticOwnersForLandblock(landblockId));
owners.UnionWith(staging.CaptureStaticOwnersForLandblock(landblockId));
owners.UnionWith(expectedDynamicVersions.Keys);
uint[] ownerIds = owners.ToArray();
Array.Sort(ownerIds);
var states = new List<PreparedShadowOwnerState>(ownerIds.Length);
foreach (uint ownerId in ownerIds)
{
if (staging.TryCaptureOwnerState(ownerId, out PreparedShadowOwnerState? state)
&& state is not null)
states.Add(state);
}
return new PreparedLandblockShadowReplacement(
landblockId & 0xFFFF0000u,
ownerIds,
states.ToArray(),
expectedDynamicVersions.ToDictionary(
static pair => pair.Key,
static pair => pair.Value));
}
internal bool ValidateLandblockReplacement(
PreparedLandblockShadowReplacement replacement)
{
foreach ((uint ownerId, ulong version) in replacement.DynamicVersions)
{
if (GetOwnerVersion(ownerId) != version
|| !OwnerTouchesLandblock(ownerId, replacement.LandblockPrefix))
{
return false;
}
}
return CaptureDynamicRefloodOwnersForLandblock(
replacement.LandblockPrefix)
.SequenceEqual(replacement.DynamicVersions.Keys.Order());
}
expectedRetainedVersions);
internal void CommitLandblockReplacement(
PreparedLandblockShadowReplacement replacement)
{
if (!ValidateLandblockReplacement(replacement))
{
throw new InvalidOperationException(
"Dynamic shadow owners changed before collision generation commit.");
}
foreach (uint ownerId in replacement.OwnerIds)
Deregister(ownerId);
foreach (PreparedShadowOwnerState state in replacement.OwnerStates)
InstallOwnerState(state);
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)
@ -1071,10 +1114,10 @@ public sealed class ShadowObjectRegistry
entityId,
registration,
shapes,
cells?.ToArray() ?? Array.Empty<uint>(),
rows.ToArray(),
cells is null ? null : new List<uint>(cells),
rows,
_suspendedEntities.Contains(entityId),
withdrawn?.ToArray() ?? Array.Empty<uint>());
withdrawn is null ? null : new HashSet<uint>(withdrawn));
return true;
}
@ -1085,49 +1128,194 @@ public sealed class ShadowObjectRegistry
_entityShapes[state.EntityId] = state.Shapes;
if (state.Suspended)
_suspendedEntities.Add(state.EntityId);
if (state.WithdrawnPrefixes.Length != 0)
if (state.WithdrawnPrefixes is not null)
{
_withdrawnPrefixesByOwner[state.EntityId] =
new HashSet<uint>(state.WithdrawnPrefixes);
_withdrawnPrefixesByOwner[state.EntityId] = state.WithdrawnPrefixes;
}
if (state.CellIds.Length != 0)
_entityToCells[state.EntityId] = new List<uint>(state.CellIds);
foreach (PreparedShadowCellRows row in state.Rows)
if (state.CellIds is not null)
_entityToCells[state.EntityId] = state.CellIds;
for (int rowIndex = 0; rowIndex < state.Rows.Count; rowIndex++)
{
foreach (ShadowEntry entry in row.Entries)
AddEntryToCell(entry, row.CellId);
PreparedShadowCellRows row = state.Rows[rowIndex];
for (int entryIndex = 0; entryIndex < row.Entries.Length; entryIndex++)
AddEntryToCell(row.Entries[entryIndex], row.CellId);
}
BumpOwnerVersion(state.EntityId);
}
internal sealed class LandblockReplacementBuilder : IDisposable
{
private readonly ShadowObjectRegistry _active;
private readonly ShadowObjectRegistry _staging;
private readonly uint _prefix;
private readonly ulong _sourceMutationVersion;
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 int _phase;
internal LandblockReplacementBuilder(
ShadowObjectRegistry active,
ShadowObjectRegistry staging,
uint landblockId,
IReadOnlyDictionary<uint, ulong> expected)
{
_active = active;
_staging = staging;
_prefix = landblockId & 0xFFFF0000u;
_sourceMutationVersion = active.MutationVersion;
_expectedEnumerator = expected.GetEnumerator();
}
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())
{
(uint ownerId, ulong version) = _expectedEnumerator.Current;
if (_active.GetOwnerVersion(ownerId) != version
|| !_active.IsRetainedRefloodOwner(ownerId, _prefix))
{
return true;
}
AddOwner(ownerId);
WorkUnits++;
return false;
}
_expectedEnumerator.Dispose();
_expectedEnumerator = null;
_registrationEnumerator = _active._entityReg.GetEnumerator();
_phase++;
return false;
case 1:
if (_registrationEnumerator.MoveNext())
{
(uint ownerId, RegistrationRecord registration) =
_registrationEnumerator.Current;
if (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
{
AddOwner(ownerId);
}
WorkUnits++;
return false;
}
_registrationEnumerator.Dispose();
_registrationEnumerator = _staging._entityReg.GetEnumerator();
_phase++;
return false;
case 2:
if (_registrationEnumerator.MoveNext())
{
(uint ownerId, RegistrationRecord registration) =
_registrationEnumerator.Current;
if (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == _prefix)
{
AddOwner(ownerId);
}
WorkUnits++;
return false;
}
_registrationEnumerator.Dispose();
_ownerEnumerator = _owners.GetEnumerator();
_phase++;
return false;
case 3:
if (_ownerEnumerator.MoveNext())
{
uint ownerId = _ownerEnumerator.Current;
if (_staging.TryCaptureOwnerState(
ownerId,
out PreparedShadowOwnerState? state)
&& state is not null)
{
_states.Add(state);
}
WorkUnits++;
return false;
}
_ownerEnumerator.Dispose();
if (IsStable)
{
Prepared = new PreparedLandblockShadowReplacement(
_prefix,
_ownerIds,
_states);
}
_phase++;
return true;
default:
return true;
}
}
private void AddOwner(uint ownerId)
{
if (_owners.Add(ownerId))
_ownerIds.Add(ownerId);
}
public void Dispose()
{
_expectedEnumerator?.Dispose();
if (_phase is 1 or 2)
_registrationEnumerator.Dispose();
if (_phase == 3)
_ownerEnumerator.Dispose();
}
}
private bool IsRetainedRefloodOwner(uint ownerId, uint landblockId)
{
if (!_entityReg.TryGetValue(ownerId, out RegistrationRecord? registration)
|| _suspendedEntities.Contains(ownerId)
|| (registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u)
== (landblockId & 0xFFFF0000u)))
{
return false;
}
return OwnerTouchesLandblock(ownerId, landblockId);
}
internal sealed class PreparedLandblockShadowReplacement
{
internal PreparedLandblockShadowReplacement(
uint landblockPrefix,
uint[] ownerIds,
PreparedShadowOwnerState[] ownerStates,
Dictionary<uint, ulong> dynamicVersions)
IReadOnlyList<uint> ownerIds,
IReadOnlyList<PreparedShadowOwnerState> ownerStates)
{
LandblockPrefix = landblockPrefix;
OwnerIds = ownerIds;
OwnerStates = ownerStates;
DynamicVersions = dynamicVersions;
}
internal uint LandblockPrefix { get; }
internal uint[] OwnerIds { get; }
internal PreparedShadowOwnerState[] OwnerStates { get; }
internal Dictionary<uint, ulong> DynamicVersions { get; }
internal IReadOnlyList<uint> OwnerIds { get; }
internal IReadOnlyList<PreparedShadowOwnerState> OwnerStates { get; }
}
internal sealed record PreparedShadowOwnerState(
uint EntityId,
RegistrationRecord Registration,
IReadOnlyList<ShadowShape>? Shapes,
uint[] CellIds,
PreparedShadowCellRows[] Rows,
List<uint>? CellIds,
IReadOnlyList<PreparedShadowCellRows> Rows,
bool Suspended,
uint[] WithdrawnPrefixes);
HashSet<uint>? WithdrawnPrefixes);
internal sealed record PreparedShadowCellRows(
uint CellId,
@ -1146,6 +1334,7 @@ public sealed class ShadowObjectRegistry
_entityShapes.Clear();
_entityReg.Clear();
_ownerVersions.Clear();
_mutationVersion = 0UL;
_fallback = null;
}