1130 lines
41 KiB
C#
1130 lines
41 KiB
C#
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 SpatialProjectileCount,
|
|
int CollisionAdmissionCount,
|
|
int CollisionGenerationCount,
|
|
bool OwnsProductionDataCache,
|
|
bool IsDisposed)
|
|
{
|
|
public bool IsConverged =>
|
|
IsDisposed
|
|
&& LandblockCount == 0
|
|
&& RetainedShadowRegistrationCount == 0
|
|
&& SpatialRootCount == 0
|
|
&& SpatialRemoteCount == 0
|
|
&& SpatialProjectileCount == 0
|
|
&& CollisionAdmissionCount == 0
|
|
&& CollisionGenerationCount == 0
|
|
&& OwnsProductionDataCache;
|
|
}
|
|
|
|
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 TimeProvider _timeProvider;
|
|
private readonly Dictionary<RuntimeEntityKey, RuntimeEntityRecord>
|
|
_spatialRoots = new();
|
|
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
|
|
_spatialRemotes = new();
|
|
private readonly Dictionary<RuntimeEntityKey, IRuntimeProjectile>
|
|
_spatialProjectiles = 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,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
DataCache = dataCache ?? PhysicsDataCache.CreateProduction();
|
|
Engine = new PhysicsEngine
|
|
{
|
|
DataCache = DataCache,
|
|
};
|
|
}
|
|
|
|
internal RuntimePhysicsState(
|
|
RuntimeEntityDirectory entities,
|
|
PhysicsEngine engine,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
Engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
|
DataCache = engine.DataCache ?? PhysicsDataCache.CreateProduction();
|
|
Engine.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 int SpatialProjectileCount => _spatialProjectiles.Count;
|
|
internal double UtcNowSeconds =>
|
|
(_timeProvider.GetUtcNow() - DateTimeOffset.UnixEpoch)
|
|
.TotalSeconds;
|
|
|
|
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
|
|
new(
|
|
Engine.LandblockCount,
|
|
Engine.ShadowObjects.RetainedRegistrationCount,
|
|
_spatialRoots.Count,
|
|
_spatialRemotes.Count,
|
|
_spatialProjectiles.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);
|
|
if (record.Projectile is { } projectile)
|
|
_spatialProjectiles[key] = projectile;
|
|
else
|
|
_spatialProjectiles.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 RefreshProjectileComponent(RuntimeEntityRecord record)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
if (record.Key is not { } key
|
|
|| !_spatialRoots.TryGetValue(key, out RuntimeEntityRecord? root)
|
|
|| !ReferenceEquals(root, record)
|
|
|| !Entities.IsCurrent(record))
|
|
{
|
|
RemoveSpatialProjectile(record);
|
|
return;
|
|
}
|
|
|
|
if (record.Projectile is { } projectile)
|
|
_spatialProjectiles[key] = projectile;
|
|
else
|
|
_spatialProjectiles.Remove(key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Installs the one projectile component for an exact canonical
|
|
/// incarnation. App may prepare the DAT collision sphere, but Runtime owns
|
|
/// component identity, prediction invalidation, and workset membership.
|
|
/// </summary>
|
|
public IRuntimeProjectile BindProjectile(
|
|
RuntimeEntityRecord record,
|
|
PhysicsBody body,
|
|
ProjectileCollisionSphere collisionSphere,
|
|
Func<bool>? externalOwnerValid = null)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
ArgumentNullException.ThrowIfNull(body);
|
|
EnsureCurrent(record);
|
|
if (!collisionSphere.IsValid)
|
|
{
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(collisionSphere),
|
|
"A Runtime projectile requires one valid prepared Setup sphere.");
|
|
}
|
|
if (!(externalOwnerValid?.Invoke() ?? true))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed external ownership before projectile binding.");
|
|
}
|
|
if (record.Projectile is { } retained)
|
|
{
|
|
if (!ReferenceEquals(retained.Body, body)
|
|
|| !ReferenceEquals(record.PhysicsBody, body))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} projectile changed its canonical physics body.");
|
|
}
|
|
|
|
retained.Body.State = record.FinalPhysicsState;
|
|
RefreshProjectileComponent(record);
|
|
return retained;
|
|
}
|
|
if (record.ProjectileBindingInProgress)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} projectile binding is already in progress.");
|
|
}
|
|
if (!ReferenceEquals(record.PhysicsBody, body))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} projectile must borrow its canonical physics body.");
|
|
}
|
|
|
|
ulong sessionVersion = Entities.SessionLifetimeVersion;
|
|
Entities.SetProjectileBindingInProgress(record, true);
|
|
try
|
|
{
|
|
var component = new RuntimeProjectile(body, collisionSphere);
|
|
if (Entities.SessionLifetimeVersion != sessionVersion
|
|
|| !Entities.IsCurrent(record)
|
|
|| !ReferenceEquals(record.PhysicsBody, body)
|
|
|| record.Projectile is not null
|
|
|| !(externalOwnerValid?.Invoke() ?? true))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed ownership during projectile binding.");
|
|
}
|
|
|
|
Entities.SetProjectile(record, component);
|
|
body.State = record.FinalPhysicsState;
|
|
SynchronizeBodyActiveState(record);
|
|
RefreshProjectileComponent(record);
|
|
return component;
|
|
}
|
|
finally
|
|
{
|
|
if (Entities.IsCurrent(record))
|
|
Entities.SetProjectileBindingInProgress(record, false);
|
|
else
|
|
record.ProjectileBindingInProgress = false;
|
|
}
|
|
}
|
|
|
|
public bool ClearProjectile(RuntimeEntityRecord record)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
if (record.Projectile is null)
|
|
return false;
|
|
|
|
Entities.SetProjectile(record, null);
|
|
Entities.SetProjectileBindingInProgress(record, false);
|
|
RefreshProjectileComponent(record);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits retail <c>CPhysicsObj::set_velocity</c> (0x005113F0) and,
|
|
/// when supplied, the paired <c>set_omega</c> write to the incarnation's
|
|
/// canonical body. Runtime owns the shared body/object-clock activation
|
|
/// edge; graphical hosts only validate and translate inbound payloads.
|
|
/// </summary>
|
|
internal bool TryCommitAuthoritativeVector(
|
|
RuntimeEntityRecord record,
|
|
PhysicsBody body,
|
|
System.Numerics.Vector3 velocity,
|
|
System.Numerics.Vector3? angularVelocity,
|
|
double currentTime,
|
|
Func<bool>? externalOwnerValid = null)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
ArgumentNullException.ThrowIfNull(body);
|
|
if (!IsFinite(velocity)
|
|
|| (angularVelocity is { } omega && !IsFinite(omega))
|
|
|| !double.IsFinite(currentTime)
|
|
|| !Entities.IsCurrent(record)
|
|
|| !ReferenceEquals(record.PhysicsBody, body)
|
|
|| !(externalOwnerValid?.Invoke() ?? true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool wasBodyActive =
|
|
(body.TransientState & TransientStateFlags.Active) != 0;
|
|
body.set_velocity(velocity);
|
|
if ((record.FinalPhysicsState & PhysicsStateFlags.Static) != 0)
|
|
{
|
|
if (!wasBodyActive)
|
|
body.TransientState &= ~TransientStateFlags.Active;
|
|
}
|
|
else
|
|
{
|
|
bool clockReactivated = record.ObjectClock.Activate();
|
|
if (clockReactivated || !wasBodyActive)
|
|
body.LastUpdateTime = currentTime;
|
|
}
|
|
|
|
if (angularVelocity is { } acceptedOmega)
|
|
body.Omega = acceptedOmega;
|
|
return Entities.IsCurrent(record)
|
|
&& ReferenceEquals(record.PhysicsBody, body)
|
|
&& (externalOwnerValid?.Invoke() ?? true);
|
|
}
|
|
|
|
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);
|
|
if (expectedBody is null)
|
|
InitializeNewPhysicsBody(record, candidateBody);
|
|
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>
|
|
/// Constructs the one built-in remote-motion component for an exact
|
|
/// canonical incarnation. A graphical host may attach animation and pose
|
|
/// sinks afterward, but cannot construct or replace simulation state.
|
|
/// </summary>
|
|
internal RemoteMotion GetOrCreateRemoteMotion(
|
|
RuntimeEntityRecord record,
|
|
Func<bool>? externalOwnerValid = null)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
EnsureCurrent(record);
|
|
if (record.RemoteMotion is { } retained)
|
|
{
|
|
return retained as RemoteMotion
|
|
?? throw new InvalidOperationException(
|
|
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} owns a non-production remote-motion component.");
|
|
}
|
|
|
|
var created = new RemoteMotion(record.PhysicsBody);
|
|
created.Movement.ActivatePhysicsObject = () =>
|
|
TryActivateOrdinaryObject(record, created);
|
|
SetRemoteMotion(record, created, externalOwnerValid);
|
|
return created;
|
|
}
|
|
|
|
internal bool TryActivateOrdinaryObject(
|
|
RuntimeEntityRecord record,
|
|
IRuntimeRemoteMotion runtime,
|
|
Func<bool>? externalOwnerValid = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
ArgumentNullException.ThrowIfNull(runtime);
|
|
if (!Entities.IsCurrent(record)
|
|
|| !ReferenceEquals(record.RemoteMotion, runtime)
|
|
|| (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0
|
|
|| !(externalOwnerValid?.Invoke() ?? true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
record.ObjectClock.Activate();
|
|
runtime.Body.TransientState |= TransientStateFlags.Active;
|
|
return true;
|
|
}
|
|
|
|
/// <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.");
|
|
}
|
|
|
|
InitializeNewPhysicsBody(record, candidate);
|
|
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);
|
|
RemoveSpatialProjectile(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 bool IsSpatialProjectile(
|
|
RuntimeEntityRecord record,
|
|
IRuntimeProjectile projectile) =>
|
|
IsSpatialRoot(record)
|
|
&& ReferenceEquals(record.Projectile, projectile)
|
|
&& record.Key is { } key
|
|
&& _spatialProjectiles.TryGetValue(key, out IRuntimeProjectile? indexed)
|
|
&& ReferenceEquals(indexed, projectile);
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void CopySpatialProjectilesTo(List<RuntimeEntityRecord> destination)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
destination.Clear();
|
|
foreach ((RuntimeEntityKey key, IRuntimeProjectile projectile)
|
|
in _spatialProjectiles)
|
|
{
|
|
if (Entities.TryGetByLocalId(
|
|
key.LocalEntityId,
|
|
out RuntimeEntityRecord record)
|
|
&& record.Key == key
|
|
&& ReferenceEquals(record.Projectile, projectile)
|
|
&& 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);
|
|
}
|
|
|
|
internal bool CommitProjectileCell(
|
|
RuntimeEntityRecord record,
|
|
IRuntimeProjectile projectile,
|
|
ulong predictionAuthorityVersion,
|
|
uint fullCellId,
|
|
Func<bool>? externalOwnerValid)
|
|
{
|
|
EnsureNotDisposed();
|
|
ArgumentNullException.ThrowIfNull(record);
|
|
ArgumentNullException.ThrowIfNull(projectile);
|
|
bool IsExactOwner() =>
|
|
Entities.IsCurrent(record)
|
|
&& ReferenceEquals(record.Projectile, projectile)
|
|
&& ReferenceEquals(record.PhysicsBody, projectile.Body)
|
|
&& projectile.PredictionAuthorityVersion
|
|
== predictionAuthorityVersion
|
|
&& (externalOwnerValid?.Invoke() ?? true);
|
|
return CommitCanonicalCell(
|
|
record,
|
|
fullCellId,
|
|
IsExactOwner);
|
|
}
|
|
|
|
public void ClearSpatialWorksets()
|
|
{
|
|
EnsureNotDisposed();
|
|
_spatialRemotes.Clear();
|
|
_spatialProjectiles.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;
|
|
Engine.Clear();
|
|
_spatialRemotes.Clear();
|
|
_spatialProjectiles.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 RemoveSpatialProjectile(RuntimeEntityRecord record)
|
|
{
|
|
if (record.Key is not { } key)
|
|
return;
|
|
if (_spatialProjectiles.TryGetValue(
|
|
key,
|
|
out IRuntimeProjectile? projectile)
|
|
&& (record.Projectile is null
|
|
|| ReferenceEquals(projectile, record.Projectile)
|
|
|| !Entities.IsCurrent(record)))
|
|
{
|
|
_spatialProjectiles.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 bool IsFinite(System.Numerics.Vector3 value) =>
|
|
float.IsFinite(value.X)
|
|
&& float.IsFinite(value.Y)
|
|
&& float.IsFinite(value.Z);
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>CPhysicsObj::set_description</c> (0x00514F40) installs the
|
|
/// accepted CreateObject velocity and omega once, when the incarnation's
|
|
/// CPhysicsObj is first acquired. Later SetState never replays them.
|
|
/// </summary>
|
|
private static void InitializeNewPhysicsBody(
|
|
RuntimeEntityRecord record,
|
|
PhysicsBody body)
|
|
{
|
|
body.State = record.FinalPhysicsState;
|
|
if (record.Snapshot.Physics is not { } physics)
|
|
return;
|
|
if (physics.Velocity is { } velocity && IsFinite(velocity))
|
|
body.set_velocity(velocity);
|
|
if (physics.AngularVelocity is { } omega && IsFinite(omega))
|
|
body.Omega = omega;
|
|
}
|
|
}
|