refactor(runtime): own per-session physics simulation
Move the sole PhysicsEngine, production cache, collision admissions, canonical bodies and hosts, remote components, ordinary/remote worksets, simulation, cell commits, and shadow synchronization under RuntimeEntityObjectLifetime. Keep App as the prepared-asset, animation-input, and render-projection adapter while preserving the named-retail update and collision order. Add exact-incarnation, object-clock, callback-reentrancy, GUID-reuse, two-runtime isolation, source ownership, collision publication, and graphical projection coverage. Release build and the complete 8,588-test solution pass. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
0dc3bfdeff
commit
7e6033d0ad
39 changed files with 3685 additions and 1722 deletions
787
src/AcDream.Runtime/Physics/RuntimePhysicsState.cs
Normal file
787
src/AcDream.Runtime/Physics/RuntimePhysicsState.cs
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.Runtime.Physics;
|
||||
|
||||
public readonly record struct RuntimePhysicsOwnershipSnapshot(
|
||||
int LandblockCount,
|
||||
int RetainedShadowRegistrationCount,
|
||||
int SpatialRootCount,
|
||||
int SpatialRemoteCount,
|
||||
int CollisionAdmissionCount,
|
||||
int CollisionGenerationCount,
|
||||
bool OwnsProductionDataCache,
|
||||
bool IsDisposed);
|
||||
|
||||
public readonly record struct RuntimePhysicsCellCommit(
|
||||
RuntimeEntityRecord Record,
|
||||
uint PreviousFullCellId,
|
||||
uint FullCellId,
|
||||
ulong SpatialAuthorityVersion);
|
||||
|
||||
public sealed record RuntimeLandblockCollisionAssets(
|
||||
uint LandblockId,
|
||||
TerrainSurface Terrain,
|
||||
IReadOnlyList<CellSurface> CellSurfaces,
|
||||
IReadOnlyList<PortalPlane> PortalPlanes,
|
||||
float WorldOffsetX,
|
||||
float WorldOffsetY,
|
||||
uint CurrentCellId);
|
||||
|
||||
public sealed class RuntimeCollisionAdmission
|
||||
{
|
||||
internal RuntimeCollisionAdmission(
|
||||
RuntimePhysicsState owner,
|
||||
uint landblockId,
|
||||
ulong generation)
|
||||
{
|
||||
Owner = owner;
|
||||
LandblockId = landblockId;
|
||||
Generation = generation;
|
||||
}
|
||||
|
||||
internal RuntimePhysicsState Owner { get; }
|
||||
internal bool AssetsAdmitted { get; set; }
|
||||
internal bool Completed { get; set; }
|
||||
public uint LandblockId { get; }
|
||||
public ulong Generation { get; }
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeCollisionAcknowledgement(
|
||||
uint LandblockId,
|
||||
ulong Generation,
|
||||
bool WasResident);
|
||||
|
||||
/// <summary>
|
||||
/// Presentation-free mutable physics world for one Runtime/session owner.
|
||||
/// Immutable prepared collision inputs may be supplied by a graphical or
|
||||
/// no-window host, but the engine, transition scratch, cell graph, and shadow
|
||||
/// registry are never shared between Runtime instances.
|
||||
/// </summary>
|
||||
public sealed class RuntimePhysicsState : IDisposable
|
||||
{
|
||||
private readonly Dictionary<RuntimeEntityKey, RuntimeEntityRecord>
|
||||
_spatialRoots = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
|
||||
_spatialRemotes = new();
|
||||
private readonly Dictionary<uint, ulong> _collisionGenerations = new();
|
||||
private readonly Dictionary<uint, RuntimeCollisionAdmission>
|
||||
_collisionAdmissions = new();
|
||||
private bool _disposed;
|
||||
|
||||
public event Action<RuntimePhysicsCellCommit>? CellCommitted;
|
||||
|
||||
internal RuntimePhysicsState(
|
||||
RuntimeEntityDirectory entities,
|
||||
PhysicsDataCache? dataCache = null)
|
||||
{
|
||||
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||
DataCache = dataCache ?? PhysicsDataCache.CreateProduction();
|
||||
Engine = new PhysicsEngine
|
||||
{
|
||||
DataCache = DataCache,
|
||||
};
|
||||
}
|
||||
|
||||
internal RuntimeEntityDirectory Entities { get; }
|
||||
public PhysicsEngine Engine { get; }
|
||||
public PhysicsDataCache DataCache { get; }
|
||||
public int SpatialRootCount => _spatialRoots.Count;
|
||||
public int SpatialRemoteCount => _spatialRemotes.Count;
|
||||
|
||||
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
|
||||
new(
|
||||
Engine.LandblockCount,
|
||||
Engine.ShadowObjects.RetainedRegistrationCount,
|
||||
_spatialRoots.Count,
|
||||
_spatialRemotes.Count,
|
||||
_collisionAdmissions.Count,
|
||||
_collisionGenerations.Count,
|
||||
ReferenceEquals(Engine.DataCache, DataCache),
|
||||
_disposed);
|
||||
|
||||
public void AcknowledgeSpatialProjection(
|
||||
RuntimeEntityRecord record,
|
||||
bool spatial)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.Key is not { } key)
|
||||
{
|
||||
if (spatial)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} cannot enter the physics workset without a local identity.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (spatial && Entities.IsCurrent(record))
|
||||
{
|
||||
_spatialRoots[key] = record;
|
||||
if (record.RemoteMotion is { } remote)
|
||||
_spatialRemotes[key] = remote;
|
||||
else
|
||||
_spatialRemotes.Remove(key);
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveSpatialProjection(record);
|
||||
}
|
||||
|
||||
public void RefreshRemoteComponent(RuntimeEntityRecord record)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.Key is not { } key
|
||||
|| !_spatialRoots.TryGetValue(key, out RuntimeEntityRecord? root)
|
||||
|| !ReferenceEquals(root, record)
|
||||
|| !Entities.IsCurrent(record))
|
||||
{
|
||||
RemoveSpatialRemote(record);
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.RemoteMotion is { } remote)
|
||||
_spatialRemotes[key] = remote;
|
||||
else
|
||||
_spatialRemotes.Remove(key);
|
||||
}
|
||||
|
||||
public void SetRemoteMotion(
|
||||
RuntimeEntityRecord record,
|
||||
IRuntimeRemoteMotion runtime,
|
||||
Func<bool>? externalOwnerValid = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
EnsureCurrent(record);
|
||||
if (!(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed external ownership before remote-motion binding.");
|
||||
}
|
||||
if (record.RemoteMotionBindingInProgress)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} remote-motion binding is already in progress.");
|
||||
}
|
||||
|
||||
PhysicsBody candidateBody = runtime.Body
|
||||
?? throw new InvalidOperationException(
|
||||
"A remote-motion runtime returned no physics body.");
|
||||
if (ReferenceEquals(record.RemoteMotion, runtime))
|
||||
{
|
||||
if (!ReferenceEquals(record.PhysicsBody, candidateBody))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} remote motion changed its canonical physics body.");
|
||||
}
|
||||
candidateBody.State = record.FinalPhysicsState;
|
||||
SynchronizeBodyActiveState(record);
|
||||
RefreshRemoteComponent(record);
|
||||
return;
|
||||
}
|
||||
if (record.PhysicsBodyAcquisitionInProgress
|
||||
&& record.PhysicsBody is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} cannot bind remote motion during physics-body acquisition.");
|
||||
}
|
||||
if (record.PhysicsBody is { } canonicalBody
|
||||
&& !ReferenceEquals(canonicalBody, candidateBody))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} cannot replace its canonical physics body.");
|
||||
}
|
||||
if (record.RequiresRemotePlacementRuntime
|
||||
&& runtime is not IRuntimeRemotePlacement)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} cannot discard its remote-placement contract.");
|
||||
}
|
||||
|
||||
ulong sessionVersion = Entities.SessionLifetimeVersion;
|
||||
PhysicsBody? expectedBody = record.PhysicsBody;
|
||||
IRuntimeRemoteMotion? expectedRuntime = record.RemoteMotion;
|
||||
bool expectedPlacementContract =
|
||||
record.RequiresRemotePlacementRuntime;
|
||||
Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> readPhysicsHost =
|
||||
() => Entities.IsCurrent(record)
|
||||
&& ReferenceEquals(record.RemoteMotion, runtime)
|
||||
? record.PhysicsHost
|
||||
: null;
|
||||
Func<uint> readCell = () => record.FullCellId;
|
||||
Action<uint> writeCell = cellId =>
|
||||
CommitCanonicalCell(record, runtime, cellId);
|
||||
|
||||
Entities.SetRemoteMotionBindingInProgress(record, true);
|
||||
try
|
||||
{
|
||||
if (runtime is IRuntimeCanonicalPhysicsConsumer canonicalConsumer)
|
||||
{
|
||||
canonicalConsumer.BindCanonicalRuntime(
|
||||
readPhysicsHost,
|
||||
readCell,
|
||||
writeCell);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool consumesHost = runtime is IRuntimePhysicsHostConsumer;
|
||||
bool consumesCell = runtime is IRuntimeCanonicalCellConsumer;
|
||||
if (consumesHost && consumesCell)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A remote runtime that consumes both canonical host and cell identity must bind them atomically.");
|
||||
}
|
||||
if (runtime is IRuntimePhysicsHostConsumer hostConsumer)
|
||||
hostConsumer.BindPhysicsHost(readPhysicsHost);
|
||||
if (runtime is IRuntimeCanonicalCellConsumer cellConsumer)
|
||||
cellConsumer.BindCanonicalCell(readCell, writeCell);
|
||||
}
|
||||
|
||||
if (Entities.SessionLifetimeVersion != sessionVersion
|
||||
|| !Entities.IsCurrent(record)
|
||||
|| !ReferenceEquals(record.PhysicsBody, expectedBody)
|
||||
|| !ReferenceEquals(record.RemoteMotion, expectedRuntime)
|
||||
|| record.RequiresRemotePlacementRuntime
|
||||
!= expectedPlacementContract
|
||||
|| !(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed ownership during remote-motion binding.");
|
||||
}
|
||||
|
||||
Entities.SetRequiresRemotePlacementRuntime(
|
||||
record,
|
||||
expectedPlacementContract
|
||||
|| runtime is IRuntimeRemotePlacement);
|
||||
Entities.SetRemoteMotion(record, runtime);
|
||||
Entities.SetPhysicsBody(record, candidateBody);
|
||||
candidateBody.State = record.FinalPhysicsState;
|
||||
SynchronizeBodyActiveState(record);
|
||||
RefreshRemoteComponent(record);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Entities.IsCurrent(record))
|
||||
{
|
||||
Entities.SetRemoteMotionBindingInProgress(record, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acquires the one retail <c>CPhysicsObj</c> body for an exact accepted
|
||||
/// object incarnation. Factories may cross a host boundary, so ownership
|
||||
/// is revalidated after the callback before the body becomes canonical.
|
||||
/// </summary>
|
||||
public PhysicsBody GetOrCreatePhysicsBody(
|
||||
RuntimeEntityRecord record,
|
||||
Func<RuntimeEntityRecord, PhysicsBody> factory,
|
||||
Func<bool>? externalOwnerValid = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
EnsureCurrent(record);
|
||||
if (!(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed external ownership before physics-body acquisition.");
|
||||
}
|
||||
if (record.PhysicsBody is { } retained)
|
||||
return retained;
|
||||
if (record.PhysicsBodyAcquisitionInProgress)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} physics-body acquisition is already in progress.");
|
||||
}
|
||||
|
||||
Entities.SetPhysicsBodyAcquisitionInProgress(record, true);
|
||||
try
|
||||
{
|
||||
PhysicsBody candidate = factory(record)
|
||||
?? throw new InvalidOperationException(
|
||||
"Physics-body factory returned null.");
|
||||
if (!Entities.IsCurrent(record)
|
||||
|| !(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed ownership during physics-body acquisition.");
|
||||
}
|
||||
if (record.PhysicsBody is { } concurrentlyBound)
|
||||
{
|
||||
if (ReferenceEquals(concurrentlyBound, candidate))
|
||||
return concurrentlyBound;
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} acquired two physics bodies within one incarnation.");
|
||||
}
|
||||
|
||||
Entities.SetPhysicsBody(record, candidate);
|
||||
candidate.State = record.FinalPhysicsState;
|
||||
SynchronizeBodyActiveState(record);
|
||||
return candidate;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// A callback can retire this incarnation. The in-progress bit
|
||||
// belongs to the record itself and must not strand its teardown.
|
||||
if (Entities.IsCurrent(record))
|
||||
Entities.SetPhysicsBodyAcquisitionInProgress(record, false);
|
||||
else
|
||||
record.PhysicsBodyAcquisitionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void InstallPhysicsHost(
|
||||
RuntimeEntityRecord record,
|
||||
AcDream.Core.Physics.Motion.IPhysicsObjHost host,
|
||||
Func<bool>? externalOwnerValid = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
EnsureCurrent(record);
|
||||
if (host.Id != record.ServerGuid)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"A physics host must match its runtime entity GUID.",
|
||||
nameof(host));
|
||||
}
|
||||
if (!(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed external ownership before physics-host installation.");
|
||||
}
|
||||
if (record.PhysicsHost is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} already owns its incarnation-stable physics host.");
|
||||
}
|
||||
|
||||
Entities.SetPhysicsHost(record, host);
|
||||
if (!Entities.IsCurrent(record)
|
||||
|| !(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
if (ReferenceEquals(record.PhysicsHost, host))
|
||||
record.PhysicsHost = null;
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed ownership during physics-host installation.");
|
||||
}
|
||||
}
|
||||
|
||||
public EntityPhysicsHost InstallOrRebindPhysicsHost(
|
||||
RuntimeEntityRecord record,
|
||||
EntityPhysicsHost configuration,
|
||||
Func<bool>? externalOwnerValid = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
EnsureCurrent(record);
|
||||
if (!(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed external ownership before physics-host composition.");
|
||||
}
|
||||
|
||||
if (record.PhysicsHost is null)
|
||||
{
|
||||
InstallPhysicsHost(record, configuration, externalOwnerValid);
|
||||
return configuration;
|
||||
}
|
||||
if (record.PhysicsHost is not EntityPhysicsHost existing)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} owns an incompatible physics-host implementation.");
|
||||
}
|
||||
|
||||
existing.RebindFrom(configuration);
|
||||
if (!Entities.IsCurrent(record)
|
||||
|| !(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed ownership during physics-host composition.");
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
public EntityPhysicsHost SelectStablePhysicsHostWithoutRebind(
|
||||
RuntimeEntityRecord record,
|
||||
EntityPhysicsHost configuration,
|
||||
Func<bool>? externalOwnerValid = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
EnsureCurrent(record);
|
||||
if (!(externalOwnerValid?.Invoke() ?? true))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed external ownership during physics-host preparation.");
|
||||
}
|
||||
|
||||
return record.PhysicsHost switch
|
||||
{
|
||||
null => configuration,
|
||||
EntityPhysicsHost existing => existing,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} owns an incompatible physics-host implementation."),
|
||||
};
|
||||
}
|
||||
|
||||
public bool TryGetPhysicsHost(
|
||||
uint serverGuid,
|
||||
out AcDream.Core.Physics.Motion.IPhysicsObjHost host)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (Entities.TryGetActive(serverGuid, out RuntimeEntityRecord record)
|
||||
&& record.PhysicsHost is { } existing)
|
||||
{
|
||||
host = existing;
|
||||
return true;
|
||||
}
|
||||
|
||||
host = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ClearRemoteMotion(RuntimeEntityRecord record)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (!Entities.IsCurrent(record)
|
||||
|| record.RemoteMotion is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Entities.SetRemoteMotion(record, null);
|
||||
RefreshRemoteComponent(record);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveSpatialProjection(RuntimeEntityRecord record)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (record.Key is not { } key)
|
||||
return;
|
||||
|
||||
if (_spatialRoots.TryGetValue(key, out RuntimeEntityRecord? root)
|
||||
&& ReferenceEquals(root, record))
|
||||
{
|
||||
_spatialRoots.Remove(key);
|
||||
}
|
||||
RemoveSpatialRemote(record);
|
||||
}
|
||||
|
||||
public bool IsSpatialRoot(RuntimeEntityRecord record) =>
|
||||
record.Key is { } key
|
||||
&& Entities.IsCurrent(record)
|
||||
&& _spatialRoots.TryGetValue(key, out RuntimeEntityRecord? indexed)
|
||||
&& ReferenceEquals(indexed, record);
|
||||
|
||||
public bool IsSpatialRemote(
|
||||
RuntimeEntityRecord record,
|
||||
IRuntimeRemoteMotion remote) =>
|
||||
IsSpatialRoot(record)
|
||||
&& ReferenceEquals(record.RemoteMotion, remote)
|
||||
&& record.Key is { } key
|
||||
&& _spatialRemotes.TryGetValue(key, out IRuntimeRemoteMotion? indexed)
|
||||
&& ReferenceEquals(indexed, remote);
|
||||
|
||||
public void CopySpatialRootsTo(List<RuntimeEntityRecord> destination)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
destination.Clear();
|
||||
foreach ((RuntimeEntityKey key, RuntimeEntityRecord record)
|
||||
in _spatialRoots)
|
||||
{
|
||||
if (record.Key == key
|
||||
&& Entities.IsCurrent(record))
|
||||
{
|
||||
destination.Add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CopySpatialRemotesTo(List<RuntimeEntityRecord> destination)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
destination.Clear();
|
||||
foreach ((RuntimeEntityKey key, IRuntimeRemoteMotion remote)
|
||||
in _spatialRemotes)
|
||||
{
|
||||
if (Entities.TryGetByLocalId(
|
||||
key.LocalEntityId,
|
||||
out RuntimeEntityRecord record)
|
||||
&& record.Key == key
|
||||
&& ReferenceEquals(record.RemoteMotion, remote)
|
||||
&& IsSpatialRoot(record))
|
||||
{
|
||||
destination.Add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool CommitOrdinaryCell(
|
||||
RuntimeEntityRecord record,
|
||||
PhysicsBody body,
|
||||
ulong objectClockEpoch,
|
||||
uint fullCellId,
|
||||
Func<bool>? externalOwnerValid)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(body);
|
||||
bool IsExactOwner() =>
|
||||
IsSpatialRoot(record)
|
||||
&& record.ObjectClockEpoch == objectClockEpoch
|
||||
&& ReferenceEquals(record.PhysicsBody, body)
|
||||
&& record.RemoteMotion is null
|
||||
&& (externalOwnerValid?.Invoke() ?? true);
|
||||
return CommitCanonicalCell(
|
||||
record,
|
||||
fullCellId,
|
||||
IsExactOwner);
|
||||
}
|
||||
|
||||
public void ClearSpatialWorksets()
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
_spatialRemotes.Clear();
|
||||
_spatialRoots.Clear();
|
||||
}
|
||||
|
||||
public RuntimeCollisionAdmission BeginCollisionAdmission(
|
||||
uint landblockId)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
uint canonical = CanonicalLandblock(landblockId);
|
||||
ulong generation = _collisionGenerations.TryGetValue(
|
||||
canonical,
|
||||
out ulong current)
|
||||
? checked(current + 1UL)
|
||||
: 1UL;
|
||||
_collisionGenerations[canonical] = generation;
|
||||
var admission = new RuntimeCollisionAdmission(
|
||||
this,
|
||||
canonical,
|
||||
generation);
|
||||
_collisionAdmissions[canonical] = admission;
|
||||
return admission;
|
||||
}
|
||||
|
||||
public void AdmitCollisionAssets(
|
||||
RuntimeCollisionAdmission admission,
|
||||
RuntimeLandblockCollisionAssets assets)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
ArgumentNullException.ThrowIfNull(assets);
|
||||
if (CanonicalLandblock(assets.LandblockId)
|
||||
!= admission.LandblockId)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Collision assets do not match their admission landblock.",
|
||||
nameof(assets));
|
||||
}
|
||||
if (admission.Completed)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A completed collision admission cannot publish more assets.");
|
||||
}
|
||||
if (admission.AssetsAdmitted)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision assets were already admitted by this receipt.");
|
||||
}
|
||||
|
||||
Engine.AddLandblock(
|
||||
admission.LandblockId,
|
||||
assets.Terrain,
|
||||
assets.CellSurfaces,
|
||||
assets.PortalPlanes,
|
||||
assets.WorldOffsetX,
|
||||
assets.WorldOffsetY);
|
||||
if ((assets.CurrentCellId & 0xFFFF0000u)
|
||||
== (admission.LandblockId & 0xFFFF0000u))
|
||||
{
|
||||
Engine.UpdatePlayerCurrCell(assets.CurrentCellId);
|
||||
}
|
||||
admission.AssetsAdmitted = true;
|
||||
}
|
||||
|
||||
public RuntimeCollisionAcknowledgement CompleteCollisionAdmission(
|
||||
RuntimeCollisionAdmission admission)
|
||||
{
|
||||
ValidateAdmission(admission);
|
||||
if (!admission.AssetsAdmitted)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision admission cannot complete before its assets publish.");
|
||||
}
|
||||
if (admission.Completed)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision admission has already completed.");
|
||||
}
|
||||
|
||||
admission.Completed = true;
|
||||
_collisionAdmissions.Remove(admission.LandblockId);
|
||||
return new RuntimeCollisionAcknowledgement(
|
||||
admission.LandblockId,
|
||||
admission.Generation,
|
||||
Engine.IsLandblockTerrainResident(admission.LandblockId));
|
||||
}
|
||||
|
||||
public RuntimeCollisionAcknowledgement DemoteCollisionToTerrain(
|
||||
uint landblockId)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
uint canonical = CanonicalLandblock(landblockId);
|
||||
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
||||
InvalidateCollisionAdmission(canonical);
|
||||
Engine.DemoteLandblockToTerrain(canonical);
|
||||
return new RuntimeCollisionAcknowledgement(
|
||||
canonical,
|
||||
_collisionGenerations[canonical],
|
||||
resident);
|
||||
}
|
||||
|
||||
public RuntimeCollisionAcknowledgement WithdrawCollision(
|
||||
uint landblockId)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
uint canonical = CanonicalLandblock(landblockId);
|
||||
bool resident = Engine.IsLandblockTerrainResident(canonical);
|
||||
InvalidateCollisionAdmission(canonical);
|
||||
Engine.RemoveLandblock(canonical);
|
||||
return new RuntimeCollisionAcknowledgement(
|
||||
canonical,
|
||||
_collisionGenerations[canonical],
|
||||
resident);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_spatialRemotes.Clear();
|
||||
_spatialRoots.Clear();
|
||||
_collisionAdmissions.Clear();
|
||||
_collisionGenerations.Clear();
|
||||
CellCommitted = null;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void CommitCanonicalCell(
|
||||
RuntimeEntityRecord record,
|
||||
IRuntimeRemoteMotion expectedRuntime,
|
||||
uint fullCellId)
|
||||
{
|
||||
_ = CommitCanonicalCell(
|
||||
record,
|
||||
fullCellId,
|
||||
() => Entities.IsCurrent(record)
|
||||
&& ReferenceEquals(record.RemoteMotion, expectedRuntime));
|
||||
}
|
||||
|
||||
private bool CommitCanonicalCell(
|
||||
RuntimeEntityRecord record,
|
||||
uint fullCellId,
|
||||
Func<bool> exactOwnerValid)
|
||||
{
|
||||
if (fullCellId == 0 || !exactOwnerValid())
|
||||
return false;
|
||||
if (fullCellId == record.FullCellId)
|
||||
return true;
|
||||
uint previousCellId = record.FullCellId;
|
||||
Entities.SetFullCell(
|
||||
record,
|
||||
fullCellId,
|
||||
(fullCellId & 0xFFFF0000u) | 0xFFFFu);
|
||||
CellCommitted?.Invoke(
|
||||
new RuntimePhysicsCellCommit(
|
||||
record,
|
||||
previousCellId,
|
||||
fullCellId,
|
||||
record.SpatialAuthorityVersion));
|
||||
return exactOwnerValid()
|
||||
&& record.FullCellId == fullCellId;
|
||||
}
|
||||
|
||||
private void RemoveSpatialRemote(RuntimeEntityRecord record)
|
||||
{
|
||||
if (record.Key is not { } key)
|
||||
return;
|
||||
if (_spatialRemotes.TryGetValue(
|
||||
key,
|
||||
out IRuntimeRemoteMotion? remote)
|
||||
&& (record.RemoteMotion is null
|
||||
|| ReferenceEquals(remote, record.RemoteMotion)
|
||||
|| !Entities.IsCurrent(record)))
|
||||
{
|
||||
_spatialRemotes.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureNotDisposed() =>
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
private void EnsureCurrent(RuntimeEntityRecord record)
|
||||
{
|
||||
if (!Entities.IsCurrent(record))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} is not current.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateAdmission(RuntimeCollisionAdmission admission)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(admission);
|
||||
if (!ReferenceEquals(admission.Owner, this)
|
||||
|| !_collisionAdmissions.TryGetValue(
|
||||
admission.LandblockId,
|
||||
out RuntimeCollisionAdmission? current)
|
||||
|| !ReferenceEquals(current, admission)
|
||||
|| !_collisionGenerations.TryGetValue(
|
||||
admission.LandblockId,
|
||||
out ulong generation)
|
||||
|| generation != admission.Generation)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Collision admission is stale or belongs to another Runtime.");
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateCollisionAdmission(uint landblockId)
|
||||
{
|
||||
ulong generation = _collisionGenerations.TryGetValue(
|
||||
landblockId,
|
||||
out ulong current)
|
||||
? checked(current + 1UL)
|
||||
: 1UL;
|
||||
_collisionGenerations[landblockId] = generation;
|
||||
_collisionAdmissions.Remove(landblockId);
|
||||
}
|
||||
|
||||
private static uint CanonicalLandblock(uint value) =>
|
||||
(value & 0xFFFF0000u) | 0xFFFFu;
|
||||
|
||||
private static void SynchronizeBodyActiveState(RuntimeEntityRecord record)
|
||||
{
|
||||
if (record.PhysicsBody is not { } body)
|
||||
return;
|
||||
if (record.ObjectClock.IsActive)
|
||||
body.TransientState |= TransientStateFlags.Active;
|
||||
else
|
||||
body.TransientState &= ~TransientStateFlags.Active;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue