fix(physics): activate collision generations atomically

This commit is contained in:
Erik 2026-07-31 15:19:25 +02:00
parent 3e0f3b6206
commit be94bc9b06
18 changed files with 1402 additions and 80 deletions

View file

@ -153,13 +153,106 @@ public sealed class PhysicsEngine
/// </summary>
public ClientObjectTable? Objects { get; set; }
private sealed record LandblockPhysics(
internal sealed record LandblockPhysics(
TerrainSurface Terrain,
IReadOnlyList<CellSurface> Cells,
IReadOnlyList<PortalPlane> Portals,
float WorldOffsetX,
float WorldOffsetY);
/// <summary>
/// Creates an off-side collision world from the last complete generation.
/// 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)
{
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;
}
internal PreparedPhysicsEngineLandblock PrepareLandblockReplacement(
PhysicsEngine staging,
uint landblockId,
ReadOnlySpan<uint> gfxObjectIds,
ReadOnlySpan<uint> setupIds,
IReadOnlyDictionary<uint, ulong> expectedDynamicVersions)
{
ArgumentNullException.ThrowIfNull(staging);
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
if (!staging._landblocks.TryGetValue(
canonical,
out LandblockPhysics? landblock))
{
throw new InvalidOperationException(
$"Staging collision generation has no landblock 0x{canonical:X8}.");
}
PhysicsDataCache stagingCache = staging.DataCache
?? throw new InvalidOperationException(
"Staging collision engine has no data cache.");
return new PreparedPhysicsEngineLandblock(
canonical,
landblock,
stagingCache.PrepareLandblockReplacement(
canonical,
gfxObjectIds,
setupIds),
ShadowObjects.PrepareLandblockReplacement(
staging.ShadowObjects,
canonical,
expectedDynamicVersions));
}
internal bool ValidateLandblockReplacement(
PreparedPhysicsEngineLandblock replacement) =>
ShadowObjects.ValidateLandblockReplacement(replacement.Shadows);
internal void CommitLandblockReplacement(
PreparedPhysicsEngineLandblock replacement)
{
if (!ValidateLandblockReplacement(replacement))
{
throw new InvalidOperationException(
"Collision generation changed after it was sealed.");
}
(DataCache ?? throw new InvalidOperationException(
"Active collision engine has no data cache."))
.CommitLandblockReplacement(replacement.DataCache);
_landblocks[replacement.LandblockId] = replacement.Landblock;
ShadowObjects.CommitLandblockReplacement(replacement.Shadows);
}
internal sealed class PreparedPhysicsEngineLandblock
{
internal PreparedPhysicsEngineLandblock(
uint landblockId,
LandblockPhysics landblock,
PreparedPhysicsDataCacheLandblock dataCache,
ShadowObjectRegistry.PreparedLandblockShadowReplacement shadows)
{
LandblockId = landblockId;
Landblock = landblock;
DataCache = dataCache;
Shadows = shadows;
}
internal uint LandblockId { get; }
internal LandblockPhysics Landblock { get; }
internal PreparedPhysicsDataCacheLandblock DataCache { get; }
internal ShadowObjectRegistry.PreparedLandblockShadowReplacement Shadows { get; }
}
/// <summary>
/// Register a landblock with its terrain surface, indoor cells, portal
/// planes, and world-space origin offset.