refactor(world): canonicalize live physics host ownership
This commit is contained in:
parent
5882b308c1
commit
fcb66198fc
21 changed files with 1546 additions and 323 deletions
|
|
@ -24,6 +24,29 @@ public interface ILiveEntityRemoteMotionRuntime
|
|||
PhysicsBody Body { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional seam for a remote-motion component that reads the exact
|
||||
/// incarnation's canonical movement-manager host from
|
||||
/// <see cref="LiveEntityRecord"/>.
|
||||
/// </summary>
|
||||
public interface ILiveEntityPhysicsHostConsumer
|
||||
{
|
||||
void BindPhysicsHost(Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> read);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Atomic composition seam for a component that consumes both canonical host
|
||||
/// and cell identity. Implementations validate every delegate before
|
||||
/// publishing any of them, so a failed bind leaves the component reusable.
|
||||
/// </summary>
|
||||
public interface ILiveEntityCanonicalRuntimeConsumer
|
||||
{
|
||||
void BindCanonicalRuntime(
|
||||
Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> readPhysicsHost,
|
||||
Func<uint> readCell,
|
||||
Action<uint> writeCell);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional seam for a remote-motion component that consumes the canonical
|
||||
/// full-cell identity owned by <see cref="LiveEntityRecord"/>. The runtime
|
||||
|
|
@ -216,6 +239,8 @@ public sealed class LiveEntityRecord
|
|||
internal bool PhysicsBodyAcquisitionInProgress { get; set; }
|
||||
public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; }
|
||||
public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime { get; internal set; }
|
||||
internal bool RemoteMotionBindingInProgress { get; set; }
|
||||
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost { get; internal set; }
|
||||
internal bool RequiresRemotePlacementRuntime { get; set; }
|
||||
public ILiveEntityProjectileRuntime? ProjectileRuntime { get; internal set; }
|
||||
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
||||
|
|
@ -1201,10 +1226,30 @@ public sealed class LiveEntityRuntime
|
|||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record))
|
||||
throw new InvalidOperationException($"Cannot bind remote motion before live entity 0x{serverGuid:X8} exists.");
|
||||
if (record.RemoteMotionBindingInProgress)
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} 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.RemoteMotionRuntime, runtime))
|
||||
{
|
||||
if (!ReferenceEquals(record.PhysicsBody, candidateBody))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} remote motion changed its canonical physics body.");
|
||||
}
|
||||
// Idempotent publication is important when motion and vector
|
||||
// packets converge on the same runtime in one update turn. The
|
||||
// component seams are incarnation-bound and must never be rebound.
|
||||
candidateBody.State = record.FinalPhysicsState;
|
||||
SynchronizePhysicsBodyActiveState(record);
|
||||
_remoteMotionByGuid[serverGuid] = runtime;
|
||||
RefreshSpatialRuntimeIndexes(record);
|
||||
return;
|
||||
}
|
||||
if (record.PhysicsBodyAcquisitionInProgress && record.PhysicsBody is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} cannot bind remote motion during physics-body acquisition.");
|
||||
PhysicsBody candidateBody = runtime.Body;
|
||||
if (record.PhysicsBody is { } canonicalBody
|
||||
&& !ReferenceEquals(canonicalBody, candidateBody))
|
||||
{
|
||||
|
|
@ -1217,38 +1262,134 @@ public sealed class LiveEntityRuntime
|
|||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} cannot discard its remote placement contract within one incarnation.");
|
||||
}
|
||||
record.RequiresRemotePlacementRuntime |=
|
||||
runtime is ILiveEntityRemotePlacementRuntime;
|
||||
record.RemoteMotionRuntime = runtime;
|
||||
record.PhysicsBody = candidateBody;
|
||||
candidateBody.State = record.FinalPhysicsState;
|
||||
SynchronizePhysicsBodyActiveState(record);
|
||||
if (runtime is ILiveEntityCanonicalCellConsumer cellConsumer)
|
||||
// Bind every possibly-throwing exact-incarnation seam before
|
||||
// publishing any canonical record/index state. An already-bound
|
||||
// component from an older GUID generation must fail without poisoning
|
||||
// the replacement record. The callback is arbitrary App code, so the
|
||||
// exact record, authority epoch, and expected owners are revalidated
|
||||
// before commit just like resource/physics-body acquisition.
|
||||
LiveEntityRecord incarnation = record;
|
||||
ulong sessionVersion = _sessionLifetimeVersion;
|
||||
ulong lifetimeVersion = CurrentLifetimeMutation(serverGuid);
|
||||
PhysicsBody? expectedBody = record.PhysicsBody;
|
||||
ILiveEntityRemoteMotionRuntime? expectedRuntime = record.RemoteMotionRuntime;
|
||||
bool expectedPlacementContract = record.RequiresRemotePlacementRuntime;
|
||||
bool expectedIndexPresent = _remoteMotionByGuid.TryGetValue(
|
||||
serverGuid,
|
||||
out ILiveEntityRemoteMotionRuntime? expectedIndexedRuntime);
|
||||
Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> readPhysicsHost = () =>
|
||||
_recordsByGuid.TryGetValue(serverGuid, out var current)
|
||||
&& ReferenceEquals(current, incarnation)
|
||||
&& ReferenceEquals(current.RemoteMotionRuntime, runtime)
|
||||
? current.PhysicsHost
|
||||
: null;
|
||||
// Reads remain bound to the exact CPhysicsObj incarnation through its
|
||||
// exit_world notifications. Only writes require active ownership.
|
||||
Func<uint> readCell = () => incarnation.FullCellId;
|
||||
Action<uint> writeCell = cellId =>
|
||||
{
|
||||
// Capture the incarnation, not only its GUID. A callback retained
|
||||
// by an old MovementManager must never mutate a replacement that
|
||||
// later reuses the same server GUID.
|
||||
LiveEntityRecord incarnation = record;
|
||||
cellConsumer.BindCanonicalCell(
|
||||
read: () =>
|
||||
_recordsByGuid.TryGetValue(serverGuid, out var current)
|
||||
&& ReferenceEquals(current, incarnation)
|
||||
&& ReferenceEquals(current.RemoteMotionRuntime, runtime)
|
||||
? current.FullCellId
|
||||
: 0u,
|
||||
write: cellId =>
|
||||
if (cellId != 0
|
||||
&& _recordsByGuid.TryGetValue(serverGuid, out var current)
|
||||
&& ReferenceEquals(current, incarnation)
|
||||
&& ReferenceEquals(current.RemoteMotionRuntime, runtime))
|
||||
{
|
||||
RebucketLiveEntity(serverGuid, cellId);
|
||||
}
|
||||
};
|
||||
|
||||
record.RemoteMotionBindingInProgress = true;
|
||||
try
|
||||
{
|
||||
if (runtime is ILiveEntityCanonicalRuntimeConsumer canonicalConsumer)
|
||||
{
|
||||
canonicalConsumer.BindCanonicalRuntime(
|
||||
readPhysicsHost,
|
||||
readCell,
|
||||
writeCell);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool consumesHost = runtime is ILiveEntityPhysicsHostConsumer;
|
||||
bool consumesCell = runtime is ILiveEntityCanonicalCellConsumer;
|
||||
if (consumesHost && consumesCell)
|
||||
{
|
||||
if (cellId != 0
|
||||
&& _recordsByGuid.TryGetValue(serverGuid, out var current)
|
||||
&& ReferenceEquals(current, incarnation)
|
||||
&& ReferenceEquals(current.RemoteMotionRuntime, runtime))
|
||||
{
|
||||
RebucketLiveEntity(serverGuid, cellId);
|
||||
}
|
||||
});
|
||||
throw new InvalidOperationException(
|
||||
"A remote runtime that consumes both canonical host and cell identity must bind them atomically.");
|
||||
}
|
||||
if (runtime is ILiveEntityPhysicsHostConsumer hostConsumer)
|
||||
hostConsumer.BindPhysicsHost(readPhysicsHost);
|
||||
if (runtime is ILiveEntityCanonicalCellConsumer cellConsumer)
|
||||
cellConsumer.BindCanonicalCell(readCell, writeCell);
|
||||
}
|
||||
|
||||
bool indexUnchanged = _remoteMotionByGuid.TryGetValue(
|
||||
serverGuid,
|
||||
out ILiveEntityRemoteMotionRuntime? currentIndexedRuntime)
|
||||
== expectedIndexPresent
|
||||
&& (!expectedIndexPresent
|
||||
|| ReferenceEquals(currentIndexedRuntime, expectedIndexedRuntime));
|
||||
if (_sessionLifetimeVersion != sessionVersion
|
||||
|| CurrentLifetimeMutation(serverGuid) != lifetimeVersion
|
||||
|| !_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? current)
|
||||
|| !ReferenceEquals(current, incarnation)
|
||||
|| !ReferenceEquals(current.PhysicsBody, expectedBody)
|
||||
|| !ReferenceEquals(current.RemoteMotionRuntime, expectedRuntime)
|
||||
|| current.RequiresRemotePlacementRuntime != expectedPlacementContract
|
||||
|| !indexUnchanged)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} changed incarnation or component ownership during remote-motion binding.");
|
||||
}
|
||||
|
||||
record.RequiresRemotePlacementRuntime |=
|
||||
runtime is ILiveEntityRemotePlacementRuntime;
|
||||
record.RemoteMotionRuntime = runtime;
|
||||
record.PhysicsBody = candidateBody;
|
||||
candidateBody.State = record.FinalPhysicsState;
|
||||
SynchronizePhysicsBodyActiveState(record);
|
||||
_remoteMotionByGuid[serverGuid] = runtime;
|
||||
RefreshSpatialRuntimeIndexes(record);
|
||||
}
|
||||
_remoteMotionByGuid[serverGuid] = runtime;
|
||||
RefreshSpatialRuntimeIndexes(record);
|
||||
finally
|
||||
{
|
||||
record.RemoteMotionBindingInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void InstallPhysicsHost(
|
||||
LiveEntityRecord expectedRecord,
|
||||
AcDream.Core.Physics.Motion.IPhysicsObjHost host)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
uint serverGuid = expectedRecord.ServerGuid;
|
||||
if (host.Id != serverGuid)
|
||||
throw new ArgumentException("A physics host must match its live entity GUID.", nameof(host));
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| !ReferenceEquals(record, expectedRecord))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} changed incarnation during physics-host installation.");
|
||||
}
|
||||
if (record.PhysicsHost is not null)
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} already owns its incarnation-stable physics host.");
|
||||
record.PhysicsHost = host;
|
||||
}
|
||||
|
||||
public bool TryGetPhysicsHost(
|
||||
uint serverGuid,
|
||||
out AcDream.Core.Physics.Motion.IPhysicsObjHost host)
|
||||
{
|
||||
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
&& record.PhysicsHost is { } existing)
|
||||
{
|
||||
host = existing;
|
||||
return true;
|
||||
}
|
||||
|
||||
host = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetRemoteMotionRuntime(uint serverGuid, out ILiveEntityRemoteMotionRuntime runtime) =>
|
||||
|
|
@ -2374,6 +2515,7 @@ public sealed class LiveEntityRuntime
|
|||
}
|
||||
record.AnimationRuntime = null;
|
||||
record.RemoteMotionRuntime = null;
|
||||
record.PhysicsHost = null;
|
||||
record.RequiresRemotePlacementRuntime = false;
|
||||
record.PhysicsBody = null;
|
||||
record.ProjectileRuntime = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue