acdream/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs

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;
}
}