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
|
|
@ -463,7 +463,7 @@ internal sealed class LivePresentationCompositionPhase
|
|||
setupId => content.Dats.Get<Setup>(setupId)),
|
||||
static value => value.Dispose());
|
||||
var ordinaryPhysicsUpdater = new LiveEntityOrdinaryPhysicsUpdater(
|
||||
d.PhysicsEngine,
|
||||
d.EntityObjects.Physics,
|
||||
d.MotionBindings.GetSetupCylinder);
|
||||
var animationScheduler = new LiveEntityAnimationScheduler(
|
||||
liveEntities,
|
||||
|
|
@ -858,7 +858,7 @@ internal sealed class LivePresentationCompositionPhase
|
|||
removeEnvCells: envCellLease.Resource.RemoveLandblock,
|
||||
envCellPublisher: envCellLease.Resource);
|
||||
var landblockPhysicsPublisher = new LandblockPhysicsPublisher(
|
||||
d.PhysicsEngine,
|
||||
d.EntityObjects.Physics,
|
||||
world.TerrainBuild.HeightTable);
|
||||
var landblockStaticPublisher =
|
||||
new LandblockStaticPresentationPublisher(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
global using AcDream.Runtime.Gameplay;
|
||||
global using AcDream.Runtime.Physics;
|
||||
global using ILiveEntityRemoteMotionRuntime =
|
||||
AcDream.Runtime.Physics.IRuntimeRemoteMotion;
|
||||
global using ILiveEntityPhysicsHostConsumer =
|
||||
AcDream.Runtime.Physics.IRuntimePhysicsHostConsumer;
|
||||
global using ILiveEntityCanonicalRuntimeConsumer =
|
||||
AcDream.Runtime.Physics.IRuntimeCanonicalPhysicsConsumer;
|
||||
global using ILiveEntityCanonicalCellConsumer =
|
||||
AcDream.Runtime.Physics.IRuntimeCanonicalCellConsumer;
|
||||
global using ILiveEntityRemotePlacementRuntime =
|
||||
AcDream.Runtime.Physics.IRuntimeRemotePlacement;
|
||||
global using LocalPlayerControllerSlot =
|
||||
AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState;
|
||||
global using ILocalPlayerControllerSource =
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ internal sealed class PlayerModeController :
|
|||
},
|
||||
interruptCurrentMovement: () => exactMovement.CancelMoveTo(
|
||||
WeenieError.ActionCancelled));
|
||||
playerHost = EntityPhysicsHost.SelectStableHostWithoutRebind(
|
||||
playerHost = EntityPhysicsHostComposition.SelectStableHostWithoutRebind(
|
||||
_liveEntities,
|
||||
playerRecord,
|
||||
configuredHost);
|
||||
|
|
@ -443,7 +443,7 @@ internal sealed class PlayerModeController :
|
|||
_camera.EnterChaseMode(legacyCamera, retailCamera);
|
||||
|
||||
EntityPhysicsHost stableAfterCamera =
|
||||
EntityPhysicsHost.SelectStableHostWithoutRebind(
|
||||
EntityPhysicsHostComposition.SelectStableHostWithoutRebind(
|
||||
_liveEntities,
|
||||
playerRecord,
|
||||
configuredHost);
|
||||
|
|
@ -465,7 +465,7 @@ internal sealed class PlayerModeController :
|
|||
// DAT, placement, shadow, and camera preparation has succeeded. A
|
||||
// late preparation failure therefore cannot expose an abandoned
|
||||
// controller through LiveEntityRecord.PhysicsHost.
|
||||
EntityPhysicsHost publishedHost = EntityPhysicsHost.InstallOrRebind(
|
||||
EntityPhysicsHost publishedHost = EntityPhysicsHostComposition.InstallOrRebind(
|
||||
_liveEntities,
|
||||
playerRecord,
|
||||
configuredHost);
|
||||
|
|
|
|||
|
|
@ -1,344 +0,0 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.App.World;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// R5-V2 — the App-side <see cref="IPhysicsObjHost"/> per entity: acdream's
|
||||
/// stand-in for retail's <c>CPhysicsObj</c> as the movement managers see it.
|
||||
/// One is built per entity (a remote <c>RemoteMotion</c> or the local player)
|
||||
/// in <c>GameWindow.EnsureRemoteMotionBindings</c> / <c>EnterPlayerModeNow</c>
|
||||
/// and stored on the exact <c>LiveEntityRecord</c>, so
|
||||
/// <see cref="GetObjectA"/> can resolve OTHER entities' hosts — the
|
||||
/// cross-entity delivery path the <see cref="TargetManager"/> voyeur system
|
||||
/// needs.
|
||||
///
|
||||
/// <para>Owns a <see cref="TargetManager"/> (retail
|
||||
/// <c>CPhysicsObj::target_manager</c>). Its <c>set_target</c>/<c>clear_target</c>/
|
||||
/// <c>add_voyeur</c>/<c>remove_voyeur</c>/<c>receive_target_update</c> seams
|
||||
/// forward to it exactly as retail's CPhysicsObj does; the movement managers'
|
||||
/// target seams are repointed here, replacing the AP-79 poll adapter. The
|
||||
/// per-entity accessors (position/velocity/radius/contact/clocks) and the
|
||||
/// <see cref="HandleUpdateTarget"/> fan-out are injected by GameWindow so this
|
||||
/// class stays free of GameWindow's internals (code-structure rule #1).</para>
|
||||
///
|
||||
/// <para>R5-V3: owns a <see cref="PositionManager"/> too (retail
|
||||
/// <c>CPhysicsObj::position_manager</c> — retail creates it lazily via
|
||||
/// <c>get_position_manager</c>; acdream constructs it eagerly, which is
|
||||
/// behaviorally identical because the empty facade no-ops until its first
|
||||
/// <c>StickTo</c>/<c>ConstrainTo</c>). <see cref="HandleUpdateTarget"/> fans
|
||||
/// deliveries to the injected MoveToManager fan FIRST, then the
|
||||
/// PositionManager — retail <c>CPhysicsObj::HandleUpdateTarget</c> order
|
||||
/// (0x00512bc0: MovementManager @0x00512bf0, PositionManager
|
||||
/// @0x00512c1a).</para>
|
||||
/// </summary>
|
||||
public sealed class EntityPhysicsHost : IPhysicsObjHost
|
||||
{
|
||||
private Func<Position> _getPosition;
|
||||
private Func<Vector3> _getVelocity;
|
||||
private Func<float> _getRadius;
|
||||
private Func<bool> _inContact;
|
||||
private Func<float?> _minterpMaxSpeed;
|
||||
private Func<double> _curTime;
|
||||
private Func<double> _physicsTimerTime;
|
||||
private Func<uint, IPhysicsObjHost?> _getObjectA;
|
||||
private Action<TargetInfo> _handleUpdateTarget;
|
||||
private Action _interruptCurrentMovement;
|
||||
private readonly TargetManager _targetManager;
|
||||
|
||||
public EntityPhysicsHost(
|
||||
uint id,
|
||||
Func<Position> getPosition,
|
||||
Func<Vector3> getVelocity,
|
||||
Func<float> getRadius,
|
||||
Func<bool> inContact,
|
||||
Func<float?> minterpMaxSpeed,
|
||||
Func<double> curTime,
|
||||
Func<double> physicsTimerTime,
|
||||
Func<uint, IPhysicsObjHost?> getObjectA,
|
||||
Action<TargetInfo> handleUpdateTarget,
|
||||
Action interruptCurrentMovement)
|
||||
{
|
||||
Id = id;
|
||||
_getPosition = null!;
|
||||
_getVelocity = null!;
|
||||
_getRadius = null!;
|
||||
_inContact = null!;
|
||||
_minterpMaxSpeed = null!;
|
||||
_curTime = null!;
|
||||
_physicsTimerTime = null!;
|
||||
_getObjectA = null!;
|
||||
_handleUpdateTarget = null!;
|
||||
_interruptCurrentMovement = null!;
|
||||
Rebind(
|
||||
getPosition,
|
||||
getVelocity,
|
||||
getRadius,
|
||||
inContact,
|
||||
minterpMaxSpeed,
|
||||
curTime,
|
||||
physicsTimerTime,
|
||||
getObjectA,
|
||||
handleUpdateTarget,
|
||||
interruptCurrentMovement);
|
||||
_targetManager = new TargetManager(this);
|
||||
PositionManager = new PositionManager(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebinds the changing CPhysicsObj access seams without replacing this
|
||||
/// object's retail-lifetime managers. A static/minimal live object can gain
|
||||
/// a MovementManager later, and the local player controller can be rebuilt,
|
||||
/// but retail keeps the same CPhysicsObj, TargetManager, and PositionManager
|
||||
/// for the accepted INSTANCE_TS incarnation. Existing voyeur, sticky, and
|
||||
/// constraint state therefore survives the upgrade.
|
||||
/// </summary>
|
||||
private void Rebind(
|
||||
Func<Position> getPosition,
|
||||
Func<Vector3> getVelocity,
|
||||
Func<float> getRadius,
|
||||
Func<bool> inContact,
|
||||
Func<float?> minterpMaxSpeed,
|
||||
Func<double> curTime,
|
||||
Func<double> physicsTimerTime,
|
||||
Func<uint, IPhysicsObjHost?> getObjectA,
|
||||
Action<TargetInfo> handleUpdateTarget,
|
||||
Action interruptCurrentMovement)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getPosition);
|
||||
ArgumentNullException.ThrowIfNull(getVelocity);
|
||||
ArgumentNullException.ThrowIfNull(getRadius);
|
||||
ArgumentNullException.ThrowIfNull(inContact);
|
||||
ArgumentNullException.ThrowIfNull(minterpMaxSpeed);
|
||||
ArgumentNullException.ThrowIfNull(curTime);
|
||||
ArgumentNullException.ThrowIfNull(physicsTimerTime);
|
||||
ArgumentNullException.ThrowIfNull(getObjectA);
|
||||
ArgumentNullException.ThrowIfNull(handleUpdateTarget);
|
||||
ArgumentNullException.ThrowIfNull(interruptCurrentMovement);
|
||||
|
||||
_getPosition = getPosition;
|
||||
_getVelocity = getVelocity;
|
||||
_getRadius = getRadius;
|
||||
_inContact = inContact;
|
||||
_minterpMaxSpeed = minterpMaxSpeed;
|
||||
_curTime = curTime;
|
||||
_physicsTimerTime = physicsTimerTime;
|
||||
_getObjectA = getObjectA;
|
||||
_handleUpdateTarget = handleUpdateTarget;
|
||||
_interruptCurrentMovement = interruptCurrentMovement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a freshly composed delegate configuration while retaining this
|
||||
/// host's exact identity and manager instances.
|
||||
/// </summary>
|
||||
private void RebindFrom(EntityPhysicsHost configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (configuration.Id != Id)
|
||||
throw new ArgumentException(
|
||||
"A physics-host configuration must match the existing host GUID.",
|
||||
nameof(configuration));
|
||||
|
||||
Rebind(
|
||||
configuration._getPosition,
|
||||
configuration._getVelocity,
|
||||
configuration._getRadius,
|
||||
configuration._inContact,
|
||||
configuration._minterpMaxSpeed,
|
||||
configuration._curTime,
|
||||
configuration._physicsTimerTime,
|
||||
configuration._getObjectA,
|
||||
configuration._handleUpdateTarget,
|
||||
configuration._interruptCurrentMovement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the first host for an exact live-object incarnation, or
|
||||
/// enriches that incarnation's existing minimal host in place. This is the
|
||||
/// shared remote/player composition seam; it deliberately never replaces
|
||||
/// TargetManager or PositionManager ownership.
|
||||
/// </summary>
|
||||
internal static EntityPhysicsHost InstallOrRebind(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord expectedRecord,
|
||||
EntityPhysicsHost configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (!runtime.TryGetRecord(expectedRecord.ServerGuid, out LiveEntityRecord current)
|
||||
|| !ReferenceEquals(current, expectedRecord))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{expectedRecord.ServerGuid:X8} changed incarnation during physics-host composition.");
|
||||
}
|
||||
|
||||
if (current.PhysicsHost is null)
|
||||
{
|
||||
runtime.InstallPhysicsHost(current, configuration);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
if (current.PhysicsHost is not EntityPhysicsHost existing)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{expectedRecord.ServerGuid:X8} owns an incompatible physics-host implementation.");
|
||||
}
|
||||
|
||||
existing.RebindFrom(configuration);
|
||||
return existing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an exact incarnation and returns the manager-stable host that
|
||||
/// a later <see cref="InstallOrRebind"/> will publish, without changing any
|
||||
/// live delegates. Player-mode construction uses this preparation edge so
|
||||
/// DAT/physics/camera failures cannot expose a half-rebound host.
|
||||
/// </summary>
|
||||
internal static EntityPhysicsHost SelectStableHostWithoutRebind(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord expectedRecord,
|
||||
EntityPhysicsHost configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (!runtime.TryGetRecord(expectedRecord.ServerGuid, out LiveEntityRecord current)
|
||||
|| !ReferenceEquals(current, expectedRecord))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{expectedRecord.ServerGuid:X8} changed incarnation during physics-host preparation.");
|
||||
}
|
||||
|
||||
return current.PhysicsHost switch
|
||||
{
|
||||
null => configuration,
|
||||
EntityPhysicsHost existing => existing,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Live entity 0x{expectedRecord.ServerGuid:X8} owns an incompatible physics-host implementation."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the position-only CPhysicsObj facade used before an animated or
|
||||
/// player movement owner exists. The delegates capture the exact live
|
||||
/// record rather than the visible-world projection, so exit_world
|
||||
/// notifications retain the object's last cell and frame after the picker
|
||||
/// view has already withdrawn it.
|
||||
/// </summary>
|
||||
internal static EntityPhysicsHost CreateMinimal(
|
||||
LiveEntityRecord record,
|
||||
Func<uint, IPhysicsObjHost?> resolve,
|
||||
Func<double> now)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(resolve);
|
||||
ArgumentNullException.ThrowIfNull(now);
|
||||
return new EntityPhysicsHost(
|
||||
record.ServerGuid,
|
||||
getPosition: () => new Position(
|
||||
record.FullCellId,
|
||||
record.WorldEntity?.Position ?? Vector3.Zero,
|
||||
record.WorldEntity?.Rotation ?? Quaternion.Identity),
|
||||
getVelocity: () => Vector3.Zero,
|
||||
getRadius: () => 0f,
|
||||
inContact: () => true,
|
||||
minterpMaxSpeed: () => null,
|
||||
curTime: now,
|
||||
physicsTimerTime: now,
|
||||
getObjectA: resolve,
|
||||
handleUpdateTarget: _ => { },
|
||||
interruptCurrentMovement: () => { });
|
||||
}
|
||||
|
||||
// ── IPhysicsObjHost accessors ──────────────────────────────────────────
|
||||
public uint Id { get; }
|
||||
public Position Position => _getPosition();
|
||||
public Vector3 Velocity => _getVelocity();
|
||||
public float Radius => _getRadius();
|
||||
public bool InContact => _inContact();
|
||||
public float? MinterpMaxSpeed => _minterpMaxSpeed();
|
||||
public double CurTime => _curTime();
|
||||
public double PhysicsTimerTime => _physicsTimerTime();
|
||||
|
||||
/// <summary>The owned voyeur manager (retail
|
||||
/// <c>CPhysicsObj::target_manager</c>).</summary>
|
||||
public TargetManager TargetManager => _targetManager;
|
||||
|
||||
/// <summary>R5-V3 — the owned <see cref="PositionManager"/> facade (retail
|
||||
/// <c>CPhysicsObj::position_manager</c>): sticky follow + (unarmed)
|
||||
/// constraint leash. Seam targets: <c>MoveToManager.StickTo/Unstick</c>,
|
||||
/// <c>MotionInterpreter.UnstickFromObject</c>, the per-tick
|
||||
/// <c>AdjustOffset</c>/<c>UseTime</c> drivers.</summary>
|
||||
public PositionManager PositionManager { get; }
|
||||
|
||||
// ── IPhysicsObjHost fan-out / target-tracking seams ────────────────────
|
||||
public IPhysicsObjHost? GetObjectA(uint id) => _getObjectA(id);
|
||||
public IPhysicsObjHost? GetRelationshipTarget(uint objectId) =>
|
||||
_targetManager.GetRelationshipTarget(objectId);
|
||||
|
||||
public void HandleUpdateTarget(TargetInfo info)
|
||||
{
|
||||
// Retail CPhysicsObj::HandleUpdateTarget (0x00512bc0) fan order:
|
||||
// MovementManager (the injected MoveToManager fan) first, then
|
||||
// PositionManager (@0x00512c1a — the R5-V3 sticky consumer).
|
||||
_handleUpdateTarget(info);
|
||||
PositionManager.HandleUpdateTarget(info);
|
||||
}
|
||||
public void InterruptCurrentMovement() => _interruptCurrentMovement();
|
||||
|
||||
public void SetTarget(uint contextId, uint objectId, float radius, double quantum)
|
||||
=> _targetManager.SetTarget(contextId, objectId, radius, quantum);
|
||||
|
||||
public void ClearTarget() => _targetManager.ClearTarget();
|
||||
public void ReceiveTargetUpdate(TargetInfo info, IPhysicsObjHost sender) =>
|
||||
_targetManager.ReceiveUpdate(info, sender);
|
||||
public void AddVoyeur(IPhysicsObjHost watcher, float radius, double quantum)
|
||||
=> _targetManager.AddVoyeur(watcher, radius, quantum);
|
||||
public void RemoveVoyeur(uint watcherId, IPhysicsObjHost expectedWatcher) =>
|
||||
_targetManager.RemoveVoyeur(watcherId, expectedWatcher);
|
||||
|
||||
// ── per-tick driver + lifecycle (called by GameWindow) ─────────────────
|
||||
|
||||
/// <summary>Retail <c>TargetManager::HandleTargetting</c> — the per-tick
|
||||
/// voyeur sweep (self-throttled to 0.5 s). Retail runs it unconditionally
|
||||
/// for every entity in <c>UpdateObjectInternal</c>, BEFORE the movement
|
||||
/// managers' <c>UseTime</c>.</summary>
|
||||
public void HandleTargetting() => _targetManager.HandleTargetting();
|
||||
|
||||
/// <summary>Retail <c>CPhysicsObj::exit_world</c>'s
|
||||
/// <c>TargetManager::NotifyVoyeurOfEvent(ExitWorld)</c> — tell every
|
||||
/// watcher of this entity that it left the world (they drop the
|
||||
/// stick/moveto). Called on despawn before the host is removed from the
|
||||
/// registry.</summary>
|
||||
public void NotifyExitWorld() => _targetManager.NotifyVoyeurOfEvent(TargetStatus.ExitWorld);
|
||||
|
||||
/// <summary>
|
||||
/// A Hidden object remains logically alive but is removed from the cell
|
||||
/// lookup/interaction surface. Retail's DetectionManager sends
|
||||
/// <c>LeftDetection</c> from <c>CObjCell::hide_object</c>; that manager is
|
||||
/// not ported yet, so the App bridges the same unavailability edge through
|
||||
/// the existing target fan-out. The non-Ok update tears down watcher
|
||||
/// MoveTo/Sticky consumers, and the watched-role table is cleared so an
|
||||
/// UnHide re-subscription receives a fresh initial snapshot. The object's
|
||||
/// own watcher role is deliberately preserved.
|
||||
/// </summary>
|
||||
public void NotifyHidden() =>
|
||||
_targetManager.NotifyVoyeurOfEventAndClear(TargetStatus.ExitWorld);
|
||||
|
||||
/// <summary>R5-V3 (#171): retail <c>CPhysicsObj::teleport_hook</c>'s tail
|
||||
/// (0x00514ed0 @0x00514f1b-0x00514f28) — <c>TargetManager::ClearTarget</c>
|
||||
/// (drop this entity's OWN subscription) then
|
||||
/// <c>NotifyVoyeurOfEvent(Teleported)</c> (every entity watching THIS one
|
||||
/// drops its stick/moveto — <c>StickyManager::HandleUpdateTarget</c>'s
|
||||
/// non-Ok teardown path). Called after a teleport placement.</summary>
|
||||
public void NotifyTeleported()
|
||||
{
|
||||
_targetManager.ClearTarget();
|
||||
_targetManager.NotifyVoyeurOfEvent(TargetStatus.Teleported);
|
||||
}
|
||||
}
|
||||
82
src/AcDream.App/Physics/EntityPhysicsHostComposition.cs
Normal file
82
src/AcDream.App/Physics/EntityPhysicsHostComposition.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Graphical configuration factory for Runtime's presentation-free
|
||||
/// <see cref="EntityPhysicsHost"/>. Runtime owns exact-incarnation
|
||||
/// installation, stable identity, manager lifetime, and lookup.
|
||||
/// </summary>
|
||||
internal static class EntityPhysicsHostComposition
|
||||
{
|
||||
internal static EntityPhysicsHost InstallOrRebind(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord expectedRecord,
|
||||
EntityPhysicsHost configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
bool ProjectionIsCurrent() =>
|
||||
runtime.TryGetRecord(
|
||||
expectedRecord.ServerGuid,
|
||||
out LiveEntityRecord current)
|
||||
&& ReferenceEquals(current, expectedRecord);
|
||||
return runtime.Physics.InstallOrRebindPhysicsHost(
|
||||
expectedRecord.Canonical,
|
||||
configuration,
|
||||
ProjectionIsCurrent);
|
||||
}
|
||||
|
||||
internal static EntityPhysicsHost SelectStableHostWithoutRebind(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord expectedRecord,
|
||||
EntityPhysicsHost configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
bool ProjectionIsCurrent() =>
|
||||
runtime.TryGetRecord(
|
||||
expectedRecord.ServerGuid,
|
||||
out LiveEntityRecord current)
|
||||
&& ReferenceEquals(current, expectedRecord);
|
||||
return runtime.Physics.SelectStablePhysicsHostWithoutRebind(
|
||||
expectedRecord.Canonical,
|
||||
configuration,
|
||||
ProjectionIsCurrent);
|
||||
}
|
||||
|
||||
internal static EntityPhysicsHost CreateMinimal(
|
||||
LiveEntityRecord record,
|
||||
Func<uint, IPhysicsObjHost?> resolve,
|
||||
Func<double> now)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(resolve);
|
||||
ArgumentNullException.ThrowIfNull(now);
|
||||
return new EntityPhysicsHost(
|
||||
record.ServerGuid,
|
||||
getPosition: () => new Position(
|
||||
record.FullCellId,
|
||||
record.WorldEntity?.Position
|
||||
?? record.PhysicsBody?.Position
|
||||
?? Vector3.Zero,
|
||||
record.WorldEntity?.Rotation
|
||||
?? record.PhysicsBody?.Orientation
|
||||
?? Quaternion.Identity),
|
||||
getVelocity: () => record.PhysicsBody?.Velocity ?? Vector3.Zero,
|
||||
getRadius: () => 0f,
|
||||
inContact: () => record.PhysicsBody?.InContact ?? true,
|
||||
minterpMaxSpeed: () => null,
|
||||
curTime: now,
|
||||
physicsTimerTime: now,
|
||||
getObjectA: resolve,
|
||||
handleUpdateTarget: _ => { },
|
||||
interruptCurrentMovement: () => { });
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +181,7 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
handleUpdateTarget: info => rmT.Movement.HandleUpdateTarget(info),
|
||||
interruptCurrentMovement: () => rmT.Movement.CancelMoveTo(
|
||||
AcDream.Core.Physics.WeenieError.ActionCancelled));
|
||||
host = EntityPhysicsHost.InstallOrRebind(
|
||||
host = EntityPhysicsHostComposition.InstallOrRebind(
|
||||
liveEntities,
|
||||
hostRecord,
|
||||
configuredHost);
|
||||
|
|
@ -242,7 +242,7 @@ internal sealed class LiveEntityMotionRuntimeController
|
|||
return existing;
|
||||
|
||||
double NowSeconds() => (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
var minimal = EntityPhysicsHost.CreateMinimal(
|
||||
var minimal = EntityPhysicsHostComposition.CreateMinimal(
|
||||
activeRecord,
|
||||
ResolvePhysicsHost,
|
||||
NowSeconds);
|
||||
|
|
|
|||
|
|
@ -1,30 +1,29 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the body-backed, manager-less branch of retail
|
||||
/// <c>CPhysicsObj::UpdateObjectInternal</c> (0x005156B0). A retained canonical
|
||||
/// body can temporarily outlive its RemoteMotion or projectile component; it
|
||||
/// must still compose the complete PartArray Frame, integrate physics, sweep
|
||||
/// through Transition, commit its cell, and move its collision shadow.
|
||||
/// Projects the presentation-free Runtime result of retail
|
||||
/// <c>CPhysicsObj::UpdateObjectInternal</c> (0x005156B0) into App's
|
||||
/// <see cref="WorldEntity"/> and spatial buckets.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityOrdinaryPhysicsUpdater
|
||||
{
|
||||
private readonly PhysicsEngine _physics;
|
||||
private readonly RuntimeOrdinaryPhysicsUpdater _runtime;
|
||||
private readonly Func<uint, WorldEntity, (float Radius, float Height)>
|
||||
_getSetupCylinder;
|
||||
|
||||
public LiveEntityOrdinaryPhysicsUpdater(
|
||||
PhysicsEngine physics,
|
||||
RuntimePhysicsState physics,
|
||||
Func<uint, WorldEntity, (float Radius, float Height)> getSetupCylinder)
|
||||
{
|
||||
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
||||
_runtime = new RuntimeOrdinaryPhysicsUpdater(
|
||||
physics ?? throw new ArgumentNullException(nameof(physics)));
|
||||
_getSetupCylinder = getSetupCylinder
|
||||
?? throw new ArgumentNullException(nameof(getSetupCylinder));
|
||||
}
|
||||
|
|
@ -47,165 +46,47 @@ internal sealed class LiveEntityOrdinaryPhysicsUpdater
|
|||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(rootFrame);
|
||||
ArgumentNullException.ThrowIfNull(captureAnimationHooks);
|
||||
if (record.PhysicsBody is not { } body
|
||||
|| !IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch))
|
||||
{
|
||||
if (record.PhysicsBody is not { } body)
|
||||
return false;
|
||||
}
|
||||
|
||||
body.State = record.FinalPhysicsState;
|
||||
Vector3 priorPosition = body.Position;
|
||||
Quaternion priorOrientation = body.Orientation;
|
||||
bool previousContact = body.InContact;
|
||||
bool previousOnWalkable = body.OnWalkable;
|
||||
|
||||
// UpdatePositionInternal 0x00512CA1: PartArray root translation is
|
||||
// scaled only while transient OnWalkable is set. Its orientation stays
|
||||
// in the complete Frame and composes regardless of that translation
|
||||
// gate.
|
||||
Vector3 candidatePosition = priorPosition;
|
||||
if (body.OnWalkable && rootFrame.Origin != Vector3.Zero)
|
||||
{
|
||||
candidatePosition += Vector3.Transform(
|
||||
rootFrame.Origin * objectScale,
|
||||
priorOrientation);
|
||||
}
|
||||
|
||||
Quaternion candidateOrientation = priorOrientation;
|
||||
if (!rootFrame.Orientation.IsIdentity)
|
||||
{
|
||||
candidateOrientation = FrameOps.SetRotate(
|
||||
candidatePosition,
|
||||
priorOrientation,
|
||||
priorOrientation * rootFrame.Orientation);
|
||||
}
|
||||
|
||||
body.SetFrameInCurrentCell(candidatePosition, candidateOrientation);
|
||||
body.calc_acceleration();
|
||||
body.UpdatePhysicsInternal(quantum);
|
||||
// Omega integration writes Orientation directly; mirror it into the
|
||||
// retained Position frame before Transition consumes the candidate.
|
||||
body.SetFrameInCurrentCell(body.Position, body.Orientation);
|
||||
|
||||
// UpdatePositionInternal processes PartArray hooks after physics but
|
||||
// before UpdateObjectInternal performs its transition sweep.
|
||||
if (sequencer is not null)
|
||||
captureAnimationHooks(entity.Id, sequencer);
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch))
|
||||
return false;
|
||||
|
||||
Vector3 integratedPosition = body.Position;
|
||||
uint sourceCellId = record.FullCellId;
|
||||
uint resolvedCellId = sourceCellId;
|
||||
bool frameChanged = integratedPosition != priorPosition
|
||||
|| body.Orientation != priorOrientation;
|
||||
var (radius, height) = _getSetupCylinder(record.ServerGuid, entity);
|
||||
bool ExternalOwnerValid() =>
|
||||
IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch);
|
||||
|
||||
if (integratedPosition != priorPosition
|
||||
&& sourceCellId != 0
|
||||
&& radius >= 0.05f
|
||||
&& _physics.LandblockCount > 0)
|
||||
{
|
||||
ResolveResult resolved = _physics.ResolveWithTransition(
|
||||
priorPosition,
|
||||
integratedPosition,
|
||||
sourceCellId,
|
||||
if (!_runtime.TryBegin(
|
||||
record.Canonical,
|
||||
rootFrame,
|
||||
objectScale,
|
||||
quantum,
|
||||
radius,
|
||||
height,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
isOnGround: previousOnWalkable,
|
||||
body: body,
|
||||
moverFlags: IsPlayerGuid(record.ServerGuid)
|
||||
? ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide
|
||||
: ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: entity.Id);
|
||||
objectClockEpoch,
|
||||
sequencer,
|
||||
captureAnimationHooks,
|
||||
ExternalOwnerValid,
|
||||
out RuntimeOrdinaryPhysicsCommit commit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resolved.Ok)
|
||||
return _runtime.Complete(
|
||||
commit,
|
||||
liveCenterX,
|
||||
liveCenterY,
|
||||
snapshot =>
|
||||
{
|
||||
resolvedCellId = resolved.CellId != 0
|
||||
? resolved.CellId
|
||||
: sourceCellId;
|
||||
body.CommitTransitionPosition(resolvedCellId, resolved.Position);
|
||||
PhysicsObjUpdate.CommitSetPositionTransition(
|
||||
body,
|
||||
resolved.InContact,
|
||||
resolved.OnWalkable,
|
||||
resolved.CollisionNormalValid,
|
||||
resolved.CollisionNormal,
|
||||
previousContact,
|
||||
previousOnWalkable);
|
||||
body.CachedVelocity = quantum > 0f
|
||||
? (body.Position - priorPosition) / quantum
|
||||
: Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
// transition() returned null: UpdateObjectInternal keeps the
|
||||
// integrated frame in the current cell and reports no realized
|
||||
// transition velocity.
|
||||
body.CachedVelocity = Vector3.Zero;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The no-PartArray-sphere arm calls set_frame and writes zero
|
||||
// cached_velocity rather than fabricating a collision shape.
|
||||
body.CachedVelocity = Vector3.Zero;
|
||||
}
|
||||
if (!ExternalOwnerValid())
|
||||
return false;
|
||||
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch))
|
||||
return false;
|
||||
|
||||
entity.SetPosition(body.Position);
|
||||
entity.Rotation = body.Orientation;
|
||||
entity.ParentCellId = resolvedCellId;
|
||||
if (resolvedCellId != sourceCellId
|
||||
&& !runtime.RebucketLiveEntity(record.ServerGuid, resolvedCellId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch))
|
||||
return false;
|
||||
|
||||
if (frameChanged && record.IsSpatiallyVisible && record.FullCellId != 0)
|
||||
{
|
||||
ShadowPositionSynchronizer.Sync(
|
||||
_physics.ShadowObjects,
|
||||
entity.Id,
|
||||
body.Position,
|
||||
body.Orientation,
|
||||
record.FullCellId,
|
||||
liveCenterX,
|
||||
liveCenterY);
|
||||
}
|
||||
|
||||
return IsCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
body,
|
||||
objectClockEpoch);
|
||||
entity.SetPosition(snapshot.Position);
|
||||
entity.Rotation = snapshot.Orientation;
|
||||
entity.ParentCellId = snapshot.FullCellId;
|
||||
return ExternalOwnerValid();
|
||||
});
|
||||
}
|
||||
|
||||
private static bool IsCurrent(
|
||||
|
|
@ -220,7 +101,4 @@ internal sealed class LiveEntityOrdinaryPhysicsUpdater
|
|||
&& ReferenceEquals(record.PhysicsBody, body)
|
||||
&& record.RemoteMotionRuntime is null
|
||||
&& record.ProjectileRuntime is null;
|
||||
|
||||
private static bool IsPlayerGuid(uint guid) =>
|
||||
(guid & 0xFF000000u) == 0x50000000u;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using AcDream.Core.Net.Messages;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
|
||||
using RemoteMotion = AcDream.Runtime.Physics.RemoteMotion;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,345 +0,0 @@
|
|||
using AcDream.App.World;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Per-remote-entity physics + motion stack — verbatim application of
|
||||
/// retail's client-side motion pipeline to every remote. Mirrors retail
|
||||
/// <c>CPhysicsObj::update_object</c> (0x00515D10) →
|
||||
/// <c>CPhysicsObj::UpdateObjectInternal</c> (0x005156B0) →
|
||||
/// <c>CPhysicsObj::UpdatePositionInternal</c> (0x00512C30) →
|
||||
/// <c>CPhysicsObj::UpdatePhysicsInternal</c> (0x00510700), and ACE's
|
||||
/// <c>PhysicsObj.cs</c> port.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail has NO special "interpolator" for remote entities — it runs
|
||||
/// the full motion state machine on every entity, local or remote,
|
||||
/// and reconciles via hard-snap on UpdatePosition. This class simply
|
||||
/// pairs a <see cref="AcDream.Core.Physics.PhysicsBody"/> with its
|
||||
/// <see cref="AcDream.Core.Physics.MotionInterpreter"/> so each
|
||||
/// remote gets the same treatment as the local player.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal sealed class RemoteMotion :
|
||||
AcDream.App.World.ILiveEntityRemotePlacementRuntime,
|
||||
AcDream.App.World.ILiveEntityCanonicalRuntimeConsumer
|
||||
{
|
||||
public AcDream.Core.Physics.PhysicsBody Body { get; }
|
||||
AcDream.Core.Physics.PhysicsBody AcDream.App.World.ILiveEntityRemoteMotionRuntime.Body => Body;
|
||||
|
||||
/// <summary>R5-V5: retail <c>CPhysicsObj::movement_manager</c> — the
|
||||
/// ONE per-entity owner of the interp + moveto pair (acclient.h
|
||||
/// <c>/* 3463 */</c>). Constructed here with the interp; the
|
||||
/// MoveToManager side arrives via <c>MoveToFactory</c> +
|
||||
/// <c>MakeMoveToManager()</c> in EnsureRemoteMotionBindings.
|
||||
/// <see cref="Motion"/>/<see cref="MoveTo"/> below are views of its
|
||||
/// children (retail <c>get_minterp</c>-style access), kept so the
|
||||
/// dozens of existing call sites read unchanged.</summary>
|
||||
public AcDream.Core.Physics.Motion.MovementManager Movement;
|
||||
|
||||
public AcDream.Core.Physics.MotionInterpreter Motion => Movement.Minterp;
|
||||
/// <summary>R3-W4: the persistent per-remote dispatch sink (created
|
||||
/// once by EnsureRemoteMotionBindings; also bound as
|
||||
/// Motion.DefaultSink so LeaveGround/HitGround re-applies dispatch
|
||||
/// cycles through the motion-table backend).</summary>
|
||||
public AcDream.Core.Physics.Motion.MotionTableDispatchSink? Sink;
|
||||
/// <summary>Last UpdatePosition timestamp — drives body.update_object sub-stepping.</summary>
|
||||
public double LastServerPosTime;
|
||||
/// <summary>Last known server position — kept for diagnostics / HUD.</summary>
|
||||
public System.Numerics.Vector3 LastServerPos;
|
||||
/// <summary>
|
||||
/// #184: the body position at which this remote's collision SHADOW was
|
||||
/// last (re-)registered. The per-tick shadow-follows-resolved sync
|
||||
/// (<c>SyncRemoteShadowToBody</c>) skips re-flooding when the body has
|
||||
/// not moved > ε since this — a resting/idle-town crowd doesn't churn
|
||||
/// the registry, while a moving/de-overlapping crowd re-registers every
|
||||
/// tick. Sentinel <c>Zero</c> forces the first sync.
|
||||
/// </summary>
|
||||
public System.Numerics.Vector3 LastShadowSyncPos;
|
||||
/// <summary>
|
||||
/// Complete root orientation paired with <see cref="LastShadowSyncPos"/>.
|
||||
/// Multipart and off-centre Setup collision geometry must be re-flooded
|
||||
/// when the object turns in place, not only when its origin translates.
|
||||
/// The default zero quaternion is the first-sync sentinel.
|
||||
/// </summary>
|
||||
public System.Numerics.Quaternion LastShadowSyncOrientation;
|
||||
/// <summary>
|
||||
/// Latest server-authoritative velocity for NPC/monster smoothing.
|
||||
/// Prefer the HasVelocity vector from UpdatePosition; when ACE omits
|
||||
/// it for a server-controlled creature, derive it from consecutive
|
||||
/// authoritative positions instead of guessing from player RUM state.
|
||||
/// </summary>
|
||||
public System.Numerics.Vector3 ServerVelocity;
|
||||
public bool HasServerVelocity;
|
||||
|
||||
/// <summary>R4-V4: the entity's verbatim retail MoveToManager
|
||||
/// (server-directed movement), constructed + seam-bound by
|
||||
/// EnsureRemoteMotionBindings beside the R2-Q5/R3 binds. R5-V5:
|
||||
/// owned by <see cref="Movement"/>; this is the child view.</summary>
|
||||
public AcDream.Core.Physics.Motion.MoveToManager? MoveTo => Movement.MoveTo;
|
||||
|
||||
// R5-V2: the entity's CPhysicsObj stand-in — owns the TargetManager
|
||||
// voyeur system (retail CPhysicsObj::target_manager). Replaces the
|
||||
// AP-79 TrackedTarget* poll fields: the MoveToManager's set_target/
|
||||
// clear_target/quantum seams route here, HandleTargetting ticks per
|
||||
// frame, and OTHER entities resolve this one through LiveEntityRuntime's
|
||||
// exact-incarnation PhysicsHost slot.
|
||||
private Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?>? _physicsHostReader;
|
||||
private bool _fullPhysicsHostBound;
|
||||
public EntityPhysicsHost? Host => _fullPhysicsHostBound
|
||||
? _physicsHostReader?.Invoke() as EntityPhysicsHost
|
||||
: null;
|
||||
/// <summary>
|
||||
/// True while a server MoveToObject/MoveToPosition packet is the
|
||||
/// active locomotion source. Retail runs these through MoveToManager
|
||||
/// and CMotionInterp; the per-tick remote driver consults this to
|
||||
/// decide whether to feed body steering through
|
||||
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> instead of
|
||||
/// the InterpretedMotionState path.
|
||||
/// </summary>
|
||||
public bool ServerMoveToActive;
|
||||
|
||||
/// <summary>
|
||||
/// True once a MoveTo packet's full path payload (Origin + thresholds)
|
||||
/// has been parsed and the world-converted destination is stored on
|
||||
/// <see cref="MoveToDestinationWorld"/>. Cleared on arrival or when
|
||||
/// the next non-MoveTo UpdateMotion replaces the locomotion source.
|
||||
/// Phase L.1c (2026-04-28).
|
||||
/// </summary>
|
||||
public bool HasMoveToDestination;
|
||||
|
||||
/// <summary>
|
||||
/// World-space destination from the most recent MoveTo packet's
|
||||
/// <c>Origin</c> field, converted via the same landblock-grid
|
||||
/// arithmetic <c>OnLivePositionUpdated</c> uses.
|
||||
/// </summary>
|
||||
public System.Numerics.Vector3 MoveToDestinationWorld;
|
||||
|
||||
/// <summary>
|
||||
/// <c>min_distance</c> from the MoveTo packet's MovementParameters.
|
||||
/// Used by <see cref="AcDream.Core.Physics.RemoteMoveToDriver"/> as
|
||||
/// the chase-arrival threshold per retail
|
||||
/// <c>MoveToManager::HandleMoveToPosition</c>.
|
||||
/// </summary>
|
||||
public float MoveToMinDistance;
|
||||
|
||||
/// <summary>
|
||||
/// <c>distance_to_object</c> from the MoveTo packet. Reserved for
|
||||
/// the flee branch (<c>move_away</c>); chase uses
|
||||
/// <see cref="MoveToMinDistance"/>.
|
||||
/// </summary>
|
||||
public float MoveToDistanceToObject;
|
||||
|
||||
/// <summary>
|
||||
/// True if MovementParameters bit 9 (<c>move_towards</c>, mask
|
||||
/// <c>0x200</c>) is set on the active packet — i.e. this is a
|
||||
/// chase. False = flee (<c>move_away</c>) or static target.
|
||||
/// </summary>
|
||||
public bool MoveToMoveTowards;
|
||||
|
||||
/// <summary>
|
||||
/// Seconds-since-epoch timestamp of the most recent MoveTo packet
|
||||
/// for this entity. Used by the per-tick driver to give up
|
||||
/// steering when no refresh has arrived for
|
||||
/// <see cref="AcDream.Core.Physics.RemoteMoveToDriver.StaleDestinationSeconds"/>
|
||||
/// — typically because the entity left our streaming view and
|
||||
/// the server stopped broadcasting its MoveTo updates.
|
||||
/// </summary>
|
||||
public double LastMoveToPacketTime;
|
||||
/// <summary>
|
||||
/// Full 32-bit cell ID from the latest UpdatePosition. High 16 bits
|
||||
/// = landblock (LBx,LBy), low 16 bits = cell index (outdoor 0x0001-
|
||||
/// 0x0040, indoor 0x0100+). Fed into
|
||||
/// <see cref="AcDream.Core.Physics.PhysicsEngine.ResolveWithTransition"/>
|
||||
/// so the retail sphere-sweep can look up the right terrain/EnvCell
|
||||
/// polygons for each remote's per-frame motion. Zero until the first
|
||||
/// UP lands, which disables the transition step for that frame
|
||||
/// (Euler alone, matching pre-wire behavior).
|
||||
/// </summary>
|
||||
private uint _cellId;
|
||||
private Func<uint>? _canonicalCellReader;
|
||||
private Action<uint>? _canonicalCellWriter;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical full cell for this one live CPhysicsObj. Ordinary remotes
|
||||
/// retain the local backing value. A MovementManager attached to an
|
||||
/// existing projectile delegates reads and writes to
|
||||
/// <see cref="LiveEntityRuntime"/>, so projectile prediction,
|
||||
/// Movement packets, and spatial rebucketing cannot develop competing
|
||||
/// cell identities.
|
||||
/// </summary>
|
||||
public uint CellId
|
||||
{
|
||||
get => _canonicalCellReader?.Invoke() ?? _cellId;
|
||||
set
|
||||
{
|
||||
_cellId = value;
|
||||
_canonicalCellWriter?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPosition
|
||||
{
|
||||
get => LastServerPos;
|
||||
set => LastServerPos = value;
|
||||
}
|
||||
|
||||
double AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastServerPositionTime
|
||||
{
|
||||
get => LastServerPosTime;
|
||||
set => LastServerPosTime = value;
|
||||
}
|
||||
|
||||
System.Numerics.Vector3 AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncPosition
|
||||
{
|
||||
get => LastShadowSyncPos;
|
||||
set => LastShadowSyncPos = value;
|
||||
}
|
||||
|
||||
System.Numerics.Quaternion AcDream.App.World.ILiveEntityRemotePlacementRuntime.LastShadowSyncOrientation
|
||||
{
|
||||
get => LastShadowSyncOrientation;
|
||||
set => LastShadowSyncOrientation = value;
|
||||
}
|
||||
|
||||
bool AcDream.App.World.ILiveEntityRemotePlacementRuntime.Airborne
|
||||
{
|
||||
get => Airborne;
|
||||
set => Airborne = value;
|
||||
}
|
||||
|
||||
void AcDream.App.World.ILiveEntityRemotePlacementRuntime.HitGround() =>
|
||||
Movement.HitGround();
|
||||
|
||||
void AcDream.App.World.ILiveEntityRemotePlacementRuntime.LeaveGround() =>
|
||||
Motion.LeaveGround();
|
||||
|
||||
/// <summary>
|
||||
/// K-fix9 (2026-04-26): true while the remote is airborne (jump
|
||||
/// arc in flight). Set when a 0xF74E VectorUpdate arrives with
|
||||
/// non-trivial +Z velocity; cleared when the next UpdatePosition
|
||||
/// snaps to a new ground location. While true, the per-tick
|
||||
/// remote update SKIPS the "force OnWalkable + apply_current_movement"
|
||||
/// step that would otherwise stomp the body's Z velocity each
|
||||
/// frame, AND enables gravity so the parabolic arc actually plays
|
||||
/// out between server snaps.
|
||||
/// </summary>
|
||||
public bool Airborne;
|
||||
|
||||
/// <summary>
|
||||
/// Per-remote position-waypoint queue + catch-up math (retail's
|
||||
/// CPhysicsObj::InterpolateTo + InterpolationManager::adjust_offset).
|
||||
/// Drives per-tick body translation for grounded player remotes
|
||||
/// via <see cref="Position"/>.
|
||||
/// </summary>
|
||||
public AcDream.Core.Physics.InterpolationManager Interp { get; } =
|
||||
new AcDream.Core.Physics.InterpolationManager();
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame combiner for animation root motion + InterpolationManager
|
||||
/// correction. Mirrors retail UpdatePositionInternal @ 0x00512c30:
|
||||
/// queue catch-up REPLACES anim when active; anim stands when queue
|
||||
/// is idle.
|
||||
/// </summary>
|
||||
public AcDream.Core.Physics.RemoteMotionCombiner Position { get; } =
|
||||
new AcDream.Core.Physics.RemoteMotionCombiner();
|
||||
|
||||
/// <summary>Reusable complete delta frame shared by interpolation,
|
||||
/// Sticky/Constraint, and the final body composition.</summary>
|
||||
public AcDream.Core.Physics.Motion.MotionDeltaFrame PositionManagerDeltaScratch { get; } =
|
||||
new AcDream.Core.Physics.Motion.MotionDeltaFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): the
|
||||
/// previous UpdatePosition's world position + timestamp. The per-tick
|
||||
/// path compares the server's broadcast pace with the literal root
|
||||
/// displacement emitted by <c>CSequence::update</c>. The old inferred
|
||||
/// walk/run velocity was the source of the periodic overshoot this
|
||||
/// diagnostic was introduced to find.
|
||||
/// </summary>
|
||||
public System.Numerics.Vector3 PrevServerPos;
|
||||
public double PrevServerPosTime;
|
||||
public double LastOmegaDiagLogTime;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic-only: maximum scaled CSequence root-motion speed
|
||||
/// observed since the last UpdatePosition arrival. The next UP
|
||||
/// compares it with the server's observed pace. Reset on each UP.
|
||||
/// </summary>
|
||||
public float MaxRootMotionSpeedSinceLastUP;
|
||||
|
||||
public RemoteMotion(AcDream.Core.Physics.PhysicsBody? sharedBody = null)
|
||||
{
|
||||
Body = sharedBody ?? new AcDream.Core.Physics.PhysicsBody
|
||||
{
|
||||
// Remotes don't simulate gravity — server owns Z. Force
|
||||
// Contact + OnWalkable + Active so apply_current_movement
|
||||
// writes velocity through every tick (the gate in
|
||||
// MotionInterpreter.apply_current_movement is
|
||||
// PhysicsObj.OnWalkable).
|
||||
State = AcDream.Core.Physics.PhysicsStateFlags.ReportCollisions,
|
||||
TransientState = AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
|
||||
| AcDream.Core.Physics.TransientStateFlags.Active,
|
||||
// R4-V5 door-swing fix: a RemoteMotion exists only for a
|
||||
// WORLD entity — retail's physics_obj->cell is non-null the
|
||||
// moment the object is placed. Without this, the interp's
|
||||
// detached-object guard stripped every dispatched
|
||||
// transition link (see PhysicsBody.InWorld).
|
||||
InWorld = true,
|
||||
};
|
||||
// R5-V5: the interp is owned by the MovementManager facade
|
||||
// (retail CPhysicsObj::movement_manager -> motion_interpreter).
|
||||
// acdream constructs it eagerly here — retail's lazy-create
|
||||
// window is never observable (see MovementManager's class doc).
|
||||
Movement = new AcDream.Core.Physics.Motion.MovementManager(
|
||||
new AcDream.Core.Physics.MotionInterpreter(Body)
|
||||
{
|
||||
// R4-V5 #160 fix: retail remotes carry a weenie whose
|
||||
// InqRunRate FAILS, landing apply_run_to_command on
|
||||
// my_run_rate (the M13 wire feed). A null weenie took the
|
||||
// degenerate 1.0 branch — observer-side run movetos played
|
||||
// and moved in slow motion. See RemoteWeenie.
|
||||
WeenieObj = new AcDream.Core.Physics.RemoteWeenie(),
|
||||
});
|
||||
}
|
||||
|
||||
public void BindCanonicalRuntime(
|
||||
Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> readPhysicsHost,
|
||||
Func<uint> readCell,
|
||||
Action<uint> writeCell)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(readPhysicsHost);
|
||||
ArgumentNullException.ThrowIfNull(readCell);
|
||||
ArgumentNullException.ThrowIfNull(writeCell);
|
||||
if (_physicsHostReader is not null
|
||||
|| _canonicalCellReader is not null
|
||||
|| _canonicalCellWriter is not null)
|
||||
{
|
||||
throw new InvalidOperationException("The remote canonical runtime context is already bound.");
|
||||
}
|
||||
|
||||
// All validation precedes publication. Nothing below can throw, so a
|
||||
// rejected context never leaves a half-bound reusable component.
|
||||
_physicsHostReader = readPhysicsHost;
|
||||
_canonicalCellReader = readCell;
|
||||
_canonicalCellWriter = writeCell;
|
||||
}
|
||||
|
||||
public void BindCanonicalCell(Func<uint> read, Action<uint> write)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(read);
|
||||
ArgumentNullException.ThrowIfNull(write);
|
||||
if (_canonicalCellReader is not null || _canonicalCellWriter is not null)
|
||||
throw new InvalidOperationException("The remote canonical cell source is already bound.");
|
||||
_canonicalCellReader = read;
|
||||
_canonicalCellWriter = write;
|
||||
}
|
||||
|
||||
public void MarkFullPhysicsHostBound()
|
||||
{
|
||||
if (_physicsHostReader is null)
|
||||
throw new InvalidOperationException("The canonical physics-host source is not bound.");
|
||||
_fullPhysicsHostBound = true;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,39 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Shared host adapter for retail's moved-object shadow re-registration
|
||||
/// (<c>CPhysicsObj::SetPositionInternal</c> 0x00515330). Both the local body
|
||||
/// and remote dead-reckoning bodies publish their resolved position through
|
||||
/// this one cell-offset calculation.
|
||||
/// </summary>
|
||||
internal static class ShadowPositionSynchronizer
|
||||
{
|
||||
public static void Sync(
|
||||
ShadowObjectRegistry registry,
|
||||
uint entityId,
|
||||
Vector3 position,
|
||||
Quaternion orientation,
|
||||
uint cellId,
|
||||
int liveCenterX,
|
||||
int liveCenterY)
|
||||
{
|
||||
if (cellId == 0)
|
||||
return;
|
||||
|
||||
int landblockX = (int)((cellId >> 24) & 0xFFu);
|
||||
int landblockY = (int)((cellId >> 16) & 0xFFu);
|
||||
float worldOffsetX = (landblockX - liveCenterX) * 192f;
|
||||
float worldOffsetY = (landblockY - liveCenterY) * 192f;
|
||||
registry.UpdatePosition(
|
||||
entityId,
|
||||
position,
|
||||
orientation,
|
||||
worldOffsetX,
|
||||
worldOffsetY,
|
||||
cellId,
|
||||
seedCellId: cellId);
|
||||
}
|
||||
}
|
||||
|
|
@ -141,13 +141,14 @@ public sealed class GameWindow :
|
|||
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
|
||||
new(nearRadius: 4, farRadius: 12);
|
||||
// Phase B.3: physics engine — populated from the streaming pipeline.
|
||||
private readonly AcDream.Core.Physics.PhysicsEngine _physicsEngine = new();
|
||||
private AcDream.Core.Physics.PhysicsEngine _physicsEngine =>
|
||||
_runtimeEntityObjects.Physics.Engine;
|
||||
|
||||
// Task 4: physics data cache — BSP trees + collision shapes extracted from
|
||||
// GfxObj/Setup dats during streaming. Populated on the worker thread;
|
||||
// ConcurrentDictionary inside makes cross-thread access safe.
|
||||
private readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache =
|
||||
AcDream.Core.Physics.PhysicsDataCache.CreateProduction();
|
||||
private AcDream.Core.Physics.PhysicsDataCache _physicsDataCache =>
|
||||
_runtimeEntityObjects.Physics.DataCache;
|
||||
|
||||
// #184 Slice 2a: the per-remote dead-reckoning tick, extracted out of the
|
||||
// >10k-line TickAnimations (Code Structure Rule 1). Reusable setup/motion
|
||||
|
|
@ -607,7 +608,7 @@ public sealed class GameWindow :
|
|||
// setup/motion dependency crosses the fail-fast construction bridge;
|
||||
// the server-velocity animation cycle is stateless shared policy.
|
||||
_remotePhysicsUpdater = new AcDream.App.Physics.RemotePhysicsUpdater(
|
||||
_physicsEngine,
|
||||
_runtimeEntityObjects.Physics,
|
||||
_liveEntityMotionBindings.GetSetupCylinder,
|
||||
AcDream.App.Physics.RemoteServerControlledVelocityCycle.Apply);
|
||||
_remoteInboundMotion = new AcDream.App.Physics.RemoteInboundMotionDispatcher(
|
||||
|
|
@ -1120,7 +1121,6 @@ public sealed class GameWindow :
|
|||
{
|
||||
// Task 7: wire the physics data cache into the engine so Transition can
|
||||
// run narrow-phase BSP tests during FindObjCollisions.
|
||||
_physicsEngine.DataCache = _physicsDataCache;
|
||||
|
||||
GameWindowPlatformResult<GL, IInputContext> platform = AcquirePlatform();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using AcDream.Core.Physics.Motion;
|
|||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using DatReaderWriter.Types;
|
||||
using RemoteMotion = AcDream.App.Physics.RemoteMotion;
|
||||
using RemoteMotion = AcDream.Runtime.Physics.RemoteMotion;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Diagnostics;
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
|
@ -19,7 +20,8 @@ public sealed class LandblockPhysicsPublication
|
|||
Vector3 origin,
|
||||
uint currentCellId,
|
||||
BuildingInfo[] buildings,
|
||||
uint[] priorStaticOwnerIds)
|
||||
uint[] priorStaticOwnerIds,
|
||||
RuntimeCollisionAdmission collisionAdmission)
|
||||
{
|
||||
Owner = owner;
|
||||
Build = build;
|
||||
|
|
@ -27,6 +29,7 @@ public sealed class LandblockPhysicsPublication
|
|||
CurrentCellId = currentCellId;
|
||||
Buildings = buildings;
|
||||
PriorStaticOwnerIds = priorStaticOwnerIds;
|
||||
CollisionAdmission = collisionAdmission;
|
||||
}
|
||||
|
||||
internal object Owner { get; }
|
||||
|
|
@ -34,6 +37,7 @@ public sealed class LandblockPhysicsPublication
|
|||
internal uint CurrentCellId { get; }
|
||||
internal BuildingInfo[] Buildings { get; }
|
||||
internal uint[] PriorStaticOwnerIds { get; }
|
||||
internal RuntimeCollisionAdmission CollisionAdmission { get; }
|
||||
internal SortedSet<uint> GfxObjectIdSet { get; } = new();
|
||||
internal uint[] GfxObjectIds { get; set; } = Array.Empty<uint>();
|
||||
internal int PreparationCursor { get; set; }
|
||||
|
|
@ -102,6 +106,7 @@ public readonly record struct LandblockPhysicsPublisherDiagnostics(
|
|||
public sealed class LandblockPhysicsPublisher
|
||||
{
|
||||
private readonly object _receiptOwner = new();
|
||||
private readonly RuntimePhysicsState _physics;
|
||||
private readonly PhysicsEngine _physicsEngine;
|
||||
private readonly PhysicsDataCache _physicsDataCache;
|
||||
private readonly float[] _heightTable;
|
||||
|
|
@ -121,21 +126,19 @@ public sealed class LandblockPhysicsPublisher
|
|||
private long _fullRemovalCount;
|
||||
|
||||
public LandblockPhysicsPublisher(
|
||||
PhysicsEngine physicsEngine,
|
||||
RuntimePhysicsState physics,
|
||||
float[] heightTable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(physicsEngine);
|
||||
ArgumentNullException.ThrowIfNull(physics);
|
||||
ArgumentNullException.ThrowIfNull(heightTable);
|
||||
if (heightTable.Length < 256)
|
||||
throw new ArgumentException(
|
||||
"The retail terrain height table must contain at least 256 entries.",
|
||||
nameof(heightTable));
|
||||
|
||||
_physicsEngine = physicsEngine;
|
||||
_physicsDataCache = physicsEngine.DataCache
|
||||
?? throw new ArgumentException(
|
||||
"The physics engine must own its canonical data cache before publication wiring.",
|
||||
nameof(physicsEngine));
|
||||
_physics = physics;
|
||||
_physicsEngine = physics.Engine;
|
||||
_physicsDataCache = physics.DataCache;
|
||||
_heightTable = (float[])heightTable.Clone();
|
||||
}
|
||||
|
||||
|
|
@ -208,6 +211,8 @@ public sealed class LandblockPhysicsPublisher
|
|||
_physicsDataCache.CellGraph.CurrCell?.Id ?? 0u,
|
||||
buildings,
|
||||
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||
build.Landblock.LandblockId),
|
||||
_physics.BeginCollisionAdmission(
|
||||
build.Landblock.LandblockId));
|
||||
publication.SetupObjectIds = build.Collisions is { } collisions
|
||||
? [.. collisions.SetupIds]
|
||||
|
|
@ -332,18 +337,16 @@ public sealed class LandblockPhysicsPublisher
|
|||
}
|
||||
else if (!publication.BaseCommitted)
|
||||
{
|
||||
_physicsEngine.AddLandblock(
|
||||
landblock.LandblockId,
|
||||
publication.TerrainSurface,
|
||||
publication.CellSurfaces,
|
||||
publication.PortalPlanes,
|
||||
origin.X,
|
||||
origin.Y);
|
||||
if ((publication.CurrentCellId & 0xFFFF0000u)
|
||||
== (landblock.LandblockId & 0xFFFF0000u))
|
||||
{
|
||||
_physicsEngine.UpdatePlayerCurrCell(publication.CurrentCellId);
|
||||
}
|
||||
_physics.AdmitCollisionAssets(
|
||||
publication.CollisionAdmission,
|
||||
new RuntimeLandblockCollisionAssets(
|
||||
landblock.LandblockId,
|
||||
publication.TerrainSurface,
|
||||
publication.CellSurfaces.ToArray(),
|
||||
publication.PortalPlanes.ToArray(),
|
||||
origin.X,
|
||||
origin.Y,
|
||||
publication.CurrentCellId));
|
||||
publication.BaseCommitted = true;
|
||||
}
|
||||
else
|
||||
|
|
@ -481,6 +484,8 @@ public sealed class LandblockPhysicsPublisher
|
|||
}
|
||||
else
|
||||
{
|
||||
_physics.CompleteCollisionAdmission(
|
||||
publication.CollisionAdmission);
|
||||
_staticBspOwnerCount += publication.BspOwnerCount;
|
||||
_staticCylinderOwnerCount += publication.CylinderOwnerCount;
|
||||
publication.CompletionCommitted = true;
|
||||
|
|
@ -493,13 +498,13 @@ public sealed class LandblockPhysicsPublisher
|
|||
|
||||
public void DemoteToTerrain(uint landblockId)
|
||||
{
|
||||
_physicsEngine.DemoteLandblockToTerrain(landblockId);
|
||||
_physics.DemoteCollisionToTerrain(landblockId);
|
||||
_demotionCount++;
|
||||
}
|
||||
|
||||
public void RemoveLandblock(uint landblockId)
|
||||
{
|
||||
_physicsEngine.RemoveLandblock(landblockId);
|
||||
_physics.WithdrawCollision(landblockId);
|
||||
_fullRemovalCount++;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,65 +20,6 @@ public interface ILiveEntityAnimationRuntime
|
|||
uint CurrentMotion { get; }
|
||||
}
|
||||
|
||||
/// <summary>Remote motion state owned by a live object.</summary>
|
||||
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
|
||||
/// binds this for every production remote, regardless of whether projectile
|
||||
/// classification arrives before or after the MovementManager.
|
||||
/// </summary>
|
||||
public interface ILiveEntityCanonicalCellConsumer
|
||||
{
|
||||
void BindCanonicalCell(Func<uint> read, Action<uint> write);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remote CPhysicsObj state required to finish a retail MoveOrTeleport
|
||||
/// placement. A same-incarnation runtime replacement may wrap the same body,
|
||||
/// but it must preserve this complete placement contract.
|
||||
/// </summary>
|
||||
public interface ILiveEntityRemotePlacementRuntime :
|
||||
ILiveEntityRemoteMotionRuntime,
|
||||
ILiveEntityCanonicalCellConsumer
|
||||
{
|
||||
uint CellId { get; set; }
|
||||
bool Airborne { get; set; }
|
||||
Vector3 LastServerPosition { get; set; }
|
||||
double LastServerPositionTime { get; set; }
|
||||
Vector3 LastShadowSyncPosition { get; set; }
|
||||
Quaternion LastShadowSyncOrientation { get; set; }
|
||||
void HitGround();
|
||||
void LeaveGround();
|
||||
}
|
||||
|
||||
/// <summary>Projectile physics state owned by a live object.</summary>
|
||||
public interface ILiveEntityProjectileRuntime
|
||||
{
|
||||
|
|
@ -296,14 +237,26 @@ public sealed class LiveEntityRecord
|
|||
set => _directory.SetPhysicsBodyAcquisitionInProgress(Canonical, value);
|
||||
}
|
||||
public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; }
|
||||
public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime { get; internal set; }
|
||||
internal bool RemoteMotionBindingInProgress { get; set; }
|
||||
public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime
|
||||
{
|
||||
get => Canonical.RemoteMotion;
|
||||
internal set => _directory.SetRemoteMotion(Canonical, value);
|
||||
}
|
||||
internal bool RemoteMotionBindingInProgress
|
||||
{
|
||||
get => Canonical.RemoteMotionBindingInProgress;
|
||||
set => _directory.SetRemoteMotionBindingInProgress(Canonical, value);
|
||||
}
|
||||
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost
|
||||
{
|
||||
get => Canonical.PhysicsHost;
|
||||
internal set => _directory.SetPhysicsHost(Canonical, value);
|
||||
}
|
||||
internal bool RequiresRemotePlacementRuntime { get; set; }
|
||||
internal bool RequiresRemotePlacementRuntime
|
||||
{
|
||||
get => Canonical.RequiresRemotePlacementRuntime;
|
||||
set => _directory.SetRequiresRemotePlacementRuntime(Canonical, value);
|
||||
}
|
||||
public ILiveEntityProjectileRuntime? ProjectileRuntime { get; internal set; }
|
||||
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
||||
public bool ResourcesRegistered { get; internal set; }
|
||||
|
|
@ -435,6 +388,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
private readonly ILiveEntityRuntimeComponentLifecycle _runtimeComponentLifecycle;
|
||||
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
||||
private readonly RuntimeEntityDirectory _directory;
|
||||
private readonly RuntimePhysicsState _physics;
|
||||
private readonly LiveEntityProjectionStore _projections;
|
||||
// Logical components survive retail's 25-second leave-visibility lifetime.
|
||||
// Frame loops must not scan those retained owners. These component-specific
|
||||
|
|
@ -442,12 +396,9 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
// projection. Hidden and Frozen remain members: their state controls what
|
||||
// each retail update path advances after the O(active) lookup.
|
||||
private readonly Dictionary<RuntimeEntityKey, ILiveEntityAnimationRuntime> _spatialAnimations = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, ILiveEntityRemoteMotionRuntime> _spatialRemoteMotion = new();
|
||||
private readonly Dictionary<RuntimeEntityKey, ILiveEntityProjectileRuntime> _spatialProjectiles = new();
|
||||
// Retail CPhysics::UseTime walks the ordinary object table, not a render-
|
||||
// animation component table. This index is the loaded/cell-backed root
|
||||
// workset; component-specific indexes remain lookup accelerators only.
|
||||
private readonly Dictionary<RuntimeEntityKey, LiveEntityRecord> _spatialRootObjects = new();
|
||||
private readonly List<RuntimeEntityRecord> _spatialRootCanonicalScratch = new();
|
||||
private readonly List<RuntimeEntityRecord> _spatialRemoteCanonicalScratch = new();
|
||||
private bool _isClearing;
|
||||
private bool _sessionClearPendingFinalization;
|
||||
private bool _isRegisteringResources;
|
||||
|
|
@ -492,8 +443,10 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
_entityObjects = entityObjects
|
||||
?? throw new ArgumentNullException(nameof(entityObjects));
|
||||
_directory = _entityObjects.Entities;
|
||||
_physics = _entityObjects.Physics;
|
||||
_projections = new LiveEntityProjectionStore(_directory);
|
||||
_spatial.LiveProjectionVisibilityChanged += OnSpatialVisibilityChanged;
|
||||
_physics.CellCommitted += OnRuntimePhysicsCellCommitted;
|
||||
}
|
||||
|
||||
public int Count => _directory.Count;
|
||||
|
|
@ -506,6 +459,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
_projections.VisibleRecords;
|
||||
internal IReadOnlyCollection<RuntimeEntityRecord> CanonicalRecords =>
|
||||
_directory.ActiveRecords;
|
||||
internal RuntimePhysicsState Physics => _physics;
|
||||
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _directory.Snapshots;
|
||||
internal int AnimationRuntimeCount
|
||||
{
|
||||
|
|
@ -522,9 +476,9 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
}
|
||||
}
|
||||
internal int SpatialAnimationRuntimeCount => _spatialAnimations.Count;
|
||||
internal int SpatialRemoteMotionRuntimeCount => _spatialRemoteMotion.Count;
|
||||
internal int SpatialRemoteMotionRuntimeCount => _physics.SpatialRemoteCount;
|
||||
internal int SpatialProjectileRuntimeCount => _spatialProjectiles.Count;
|
||||
internal int SpatialRootObjectCount => _spatialRootObjects.Count;
|
||||
internal int SpatialRootObjectCount => _physics.SpatialRootCount;
|
||||
public ParentAttachmentState ParentAttachments => _directory.ParentAttachments;
|
||||
|
||||
internal bool TryGetCanonical(
|
||||
|
|
@ -798,10 +752,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
bool wasProjected = record.IsSpatiallyProjected;
|
||||
bool wasVisible = record.IsSpatiallyVisible;
|
||||
bool wasOrdinaryRoot = _spatialRootObjects.TryGetValue(
|
||||
key,
|
||||
out LiveEntityRecord? indexedRoot)
|
||||
&& ReferenceEquals(indexedRoot, record);
|
||||
bool wasOrdinaryRoot = _physics.IsSpatialRoot(record.Canonical);
|
||||
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
||||
// GpuWorldState reports an intermediate false/true pair while moving
|
||||
// between two loaded buckets. Suppress those implementation details
|
||||
|
|
@ -936,10 +887,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
return false;
|
||||
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
bool wasOrdinaryRoot = _spatialRootObjects.TryGetValue(
|
||||
key,
|
||||
out LiveEntityRecord? rootRecord)
|
||||
&& ReferenceEquals(rootRecord, record);
|
||||
bool wasOrdinaryRoot = _physics.IsSpatialRoot(record.Canonical);
|
||||
ulong objectClockEpoch = record.ObjectClockEpoch;
|
||||
ulong projectionOperation = ++record.ProjectionMutationVersion;
|
||||
Exception? spatialNotificationFailure = null;
|
||||
|
|
@ -1436,43 +1384,16 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
throw new InvalidOperationException(
|
||||
$"Cannot acquire physics body before live entity 0x{serverGuid:X8} is materialized.");
|
||||
}
|
||||
if (record.PhysicsBody is { } retained)
|
||||
return retained;
|
||||
if (record.PhysicsBodyAcquisitionInProgress)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} physics-body acquisition is already in progress.");
|
||||
}
|
||||
|
||||
record.PhysicsBodyAcquisitionInProgress = true;
|
||||
try
|
||||
{
|
||||
PhysicsBody candidate = factory(record)
|
||||
?? throw new InvalidOperationException("Physics-body factory returned null.");
|
||||
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? current)
|
||||
|| !ReferenceEquals(current, record)
|
||||
|| current.WorldEntity is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} changed incarnation during physics-body acquisition.");
|
||||
}
|
||||
if (record.PhysicsBody is { } concurrentlyBound)
|
||||
{
|
||||
if (ReferenceEquals(concurrentlyBound, candidate))
|
||||
return concurrentlyBound;
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} acquired two physics bodies within one incarnation.");
|
||||
}
|
||||
|
||||
record.PhysicsBody = candidate;
|
||||
candidate.State = record.FinalPhysicsState;
|
||||
SynchronizePhysicsBodyActiveState(record);
|
||||
return candidate;
|
||||
}
|
||||
finally
|
||||
{
|
||||
record.PhysicsBodyAcquisitionInProgress = false;
|
||||
}
|
||||
bool ProjectionIsCurrent() =>
|
||||
_projections.TryGetCurrent(
|
||||
serverGuid,
|
||||
out LiveEntityRecord? current)
|
||||
&& ReferenceEquals(current, record)
|
||||
&& current.WorldEntity is not null;
|
||||
return _physics.GetOrCreatePhysicsBody(
|
||||
record.Canonical,
|
||||
_ => factory(record),
|
||||
ProjectionIsCurrent);
|
||||
}
|
||||
|
||||
public void SetRemoteMotionRuntime(uint serverGuid, ILiveEntityRemoteMotionRuntime runtime)
|
||||
|
|
@ -1480,122 +1401,17 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
if (!_projections.TryGetCurrent(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);
|
||||
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.");
|
||||
if (record.PhysicsBody is { } canonicalBody
|
||||
&& !ReferenceEquals(canonicalBody, candidateBody))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} cannot replace its canonical physics body within one incarnation.");
|
||||
}
|
||||
if (record.RequiresRemotePlacementRuntime
|
||||
&& runtime is not ILiveEntityRemotePlacementRuntime)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8} cannot discard its remote placement contract within one incarnation.");
|
||||
}
|
||||
// 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 = _directory.SessionLifetimeVersion;
|
||||
ulong lifetimeVersion = CurrentLifetimeMutation(serverGuid);
|
||||
PhysicsBody? expectedBody = record.PhysicsBody;
|
||||
ILiveEntityRemoteMotionRuntime? expectedRuntime = record.RemoteMotionRuntime;
|
||||
bool expectedPlacementContract = record.RequiresRemotePlacementRuntime;
|
||||
Func<AcDream.Core.Physics.Motion.IPhysicsObjHost?> readPhysicsHost = () =>
|
||||
_projections.TryGetCurrent(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 =>
|
||||
{
|
||||
if (cellId != 0
|
||||
&& _projections.TryGetCurrent(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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
if (_directory.SessionLifetimeVersion != sessionVersion
|
||||
|| CurrentLifetimeMutation(serverGuid) != lifetimeVersion
|
||||
|| !_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? current)
|
||||
|| !ReferenceEquals(current, incarnation)
|
||||
|| !ReferenceEquals(current.PhysicsBody, expectedBody)
|
||||
|| !ReferenceEquals(current.RemoteMotionRuntime, expectedRuntime)
|
||||
|| current.RequiresRemotePlacementRuntime != expectedPlacementContract)
|
||||
{
|
||||
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);
|
||||
RefreshSpatialRuntimeIndexes(record);
|
||||
}
|
||||
finally
|
||||
{
|
||||
record.RemoteMotionBindingInProgress = false;
|
||||
}
|
||||
_physics.SetRemoteMotion(
|
||||
record.Canonical,
|
||||
runtime,
|
||||
() => CurrentLifetimeMutation(serverGuid) == lifetimeVersion
|
||||
&& _projections.TryGetCurrent(
|
||||
serverGuid,
|
||||
out LiveEntityRecord? current)
|
||||
&& ReferenceEquals(current, record));
|
||||
SynchronizePhysicsBodyActiveState(record);
|
||||
RefreshSpatialRuntimeIndexes(record);
|
||||
}
|
||||
|
||||
public void InstallPhysicsHost(
|
||||
|
|
@ -1605,26 +1421,23 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
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 (!_projections.TryGetCurrent(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;
|
||||
bool ProjectionIsCurrent() =>
|
||||
_projections.TryGetCurrent(
|
||||
serverGuid,
|
||||
out LiveEntityRecord? current)
|
||||
&& ReferenceEquals(current, expectedRecord);
|
||||
_physics.InstallPhysicsHost(
|
||||
expectedRecord.Canonical,
|
||||
host,
|
||||
ProjectionIsCurrent);
|
||||
}
|
||||
|
||||
public bool TryGetPhysicsHost(
|
||||
uint serverGuid,
|
||||
out AcDream.Core.Physics.Motion.IPhysicsObjHost host)
|
||||
{
|
||||
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||||
&& record.PhysicsHost is { } existing)
|
||||
if (_projections.TryGetCurrent(serverGuid, out _)
|
||||
&& _physics.TryGetPhysicsHost(serverGuid, out var existing))
|
||||
{
|
||||
host = existing;
|
||||
return true;
|
||||
|
|
@ -1656,7 +1469,8 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.RemoteMotionRuntime is null)
|
||||
return false;
|
||||
record.RemoteMotionRuntime = null;
|
||||
if (!_physics.ClearRemoteMotion(record.Canonical))
|
||||
return false;
|
||||
RefreshSpatialRuntimeIndexes(record);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1977,10 +1791,16 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
destination.Clear();
|
||||
foreach ((RuntimeEntityKey key, LiveEntityRecord indexed) in _spatialRootObjects)
|
||||
_physics.CopySpatialRootsTo(_spatialRootCanonicalScratch);
|
||||
for (int index = 0;
|
||||
index < _spatialRootCanonicalScratch.Count;
|
||||
index++)
|
||||
{
|
||||
if (_projections.TryGet(key, out LiveEntityRecord? current)
|
||||
&& ReferenceEquals(current, indexed)
|
||||
RuntimeEntityRecord canonical =
|
||||
_spatialRootCanonicalScratch[index];
|
||||
if (_projections.TryGet(
|
||||
canonical,
|
||||
out LiveEntityRecord? current)
|
||||
&& HasSpatialRuntimeProjection(current))
|
||||
{
|
||||
destination.Add(current);
|
||||
|
|
@ -1990,9 +1810,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
|
||||
internal bool IsCurrentSpatialRootObject(LiveEntityRecord record) =>
|
||||
IsCurrentRecord(record)
|
||||
&& record.ProjectionKey is { } key
|
||||
&& _spatialRootObjects.TryGetValue(key, out var indexed)
|
||||
&& ReferenceEquals(indexed, record)
|
||||
&& _physics.IsSpatialRoot(record.Canonical)
|
||||
&& HasSpatialRuntimeProjection(record);
|
||||
|
||||
internal bool IsCurrentSpatialAnimation(
|
||||
|
|
@ -2169,12 +1987,16 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(destination);
|
||||
destination.Clear();
|
||||
|
||||
foreach ((RuntimeEntityKey key, ILiveEntityRemoteMotionRuntime runtime)
|
||||
in _spatialRemoteMotion)
|
||||
_physics.CopySpatialRemotesTo(_spatialRemoteCanonicalScratch);
|
||||
for (int index = 0;
|
||||
index < _spatialRemoteCanonicalScratch.Count;
|
||||
index++)
|
||||
{
|
||||
if (_projections.TryGet(key, out LiveEntityRecord? record)
|
||||
&& ReferenceEquals(record.RemoteMotionRuntime, runtime)
|
||||
RuntimeEntityRecord canonical =
|
||||
_spatialRemoteCanonicalScratch[index];
|
||||
if (_projections.TryGet(
|
||||
canonical,
|
||||
out LiveEntityRecord? record)
|
||||
&& HasSpatialRuntimeProjection(record))
|
||||
{
|
||||
destination.Add(record);
|
||||
|
|
@ -2186,10 +2008,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
LiveEntityRecord record,
|
||||
ILiveEntityRemoteMotionRuntime runtime) =>
|
||||
IsCurrentRecord(record)
|
||||
&& ReferenceEquals(record.RemoteMotionRuntime, runtime)
|
||||
&& record.ProjectionKey is { } key
|
||||
&& _spatialRemoteMotion.TryGetValue(key, out var indexed)
|
||||
&& ReferenceEquals(indexed, runtime)
|
||||
&& _physics.IsSpatialRemote(record.Canonical, runtime)
|
||||
&& HasSpatialRuntimeProjection(record);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2623,16 +2442,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
return;
|
||||
}
|
||||
|
||||
if (spatial)
|
||||
{
|
||||
_spatialRootObjects[key] = record;
|
||||
}
|
||||
else if (current
|
||||
|| (_spatialRootObjects.TryGetValue(key, out var indexedRoot)
|
||||
&& ReferenceEquals(indexedRoot, record)))
|
||||
{
|
||||
_spatialRootObjects.Remove(key);
|
||||
}
|
||||
_physics.AcknowledgeSpatialProjection(record.Canonical, spatial);
|
||||
|
||||
if (record.WorldEntity is not null)
|
||||
{
|
||||
|
|
@ -2649,18 +2459,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
}
|
||||
}
|
||||
|
||||
if (spatial && record.RemoteMotionRuntime is { } remote)
|
||||
{
|
||||
_spatialRemoteMotion[key] = remote;
|
||||
}
|
||||
else if (current
|
||||
|| (record.RemoteMotionRuntime is { } retainedRemote
|
||||
&& _spatialRemoteMotion.TryGetValue(key, out var indexedRemote)
|
||||
&& ReferenceEquals(indexedRemote, retainedRemote)))
|
||||
{
|
||||
_spatialRemoteMotion.Remove(key);
|
||||
}
|
||||
|
||||
if (spatial && record.ProjectileRuntime is { } projectile)
|
||||
{
|
||||
_spatialProjectiles[key] = projectile;
|
||||
|
|
@ -2679,11 +2477,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
if (record.ProjectionKey is not { } key)
|
||||
return;
|
||||
|
||||
if (_spatialRootObjects.TryGetValue(key, out var indexedRoot)
|
||||
&& ReferenceEquals(indexedRoot, record))
|
||||
{
|
||||
_spatialRootObjects.Remove(key);
|
||||
}
|
||||
_physics.RemoveSpatialProjection(record.Canonical);
|
||||
|
||||
if (record.WorldEntity is not null
|
||||
&& _spatialAnimations.TryGetValue(key, out var indexedAnimation)
|
||||
|
|
@ -2692,12 +2486,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
_spatialAnimations.Remove(key);
|
||||
}
|
||||
|
||||
if (_spatialRemoteMotion.TryGetValue(key, out var indexedRemote)
|
||||
&& ReferenceEquals(indexedRemote, record.RemoteMotionRuntime))
|
||||
{
|
||||
_spatialRemoteMotion.Remove(key);
|
||||
}
|
||||
|
||||
if (_spatialProjectiles.TryGetValue(key, out var indexedProjectile)
|
||||
&& ReferenceEquals(indexedProjectile, record.ProjectileRuntime))
|
||||
{
|
||||
|
|
@ -2725,10 +2513,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
bool wasVisible = record.IsSpatiallyVisible;
|
||||
if (RequireProjectionKey(record) != key)
|
||||
return;
|
||||
bool wasOrdinaryRoot = _spatialRootObjects.TryGetValue(
|
||||
key,
|
||||
out LiveEntityRecord? indexedRoot)
|
||||
&& ReferenceEquals(indexedRoot, record);
|
||||
bool wasOrdinaryRoot = _physics.IsSpatialRoot(record.Canonical);
|
||||
record.IsSpatiallyVisible = visible;
|
||||
bool isOrdinaryRoot = record.ProjectionKind is LiveEntityProjectionKind.World
|
||||
&& record.IsSpatiallyProjected
|
||||
|
|
@ -2749,6 +2534,25 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
PublishProjectionVisibilityChanged(record, visible);
|
||||
}
|
||||
|
||||
private void OnRuntimePhysicsCellCommitted(
|
||||
RuntimePhysicsCellCommit commit)
|
||||
{
|
||||
if (commit.Record.SpatialAuthorityVersion
|
||||
!= commit.SpatialAuthorityVersion
|
||||
|| !_directory.IsCurrent(commit.Record)
|
||||
|| !_projections.TryGet(
|
||||
commit.Record,
|
||||
out LiveEntityRecord? projection)
|
||||
|| projection.WorldEntity is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RebucketLiveEntity(
|
||||
commit.Record.ServerGuid,
|
||||
commit.FullCellId);
|
||||
}
|
||||
|
||||
private static void SynchronizePhysicsBodyActiveState(
|
||||
LiveEntityRecord record)
|
||||
{
|
||||
|
|
@ -2877,9 +2681,8 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
}
|
||||
|
||||
_spatialAnimations.Clear();
|
||||
_spatialRemoteMotion.Clear();
|
||||
_spatialProjectiles.Clear();
|
||||
_spatialRootObjects.Clear();
|
||||
_physics.ClearSpatialWorksets();
|
||||
_projections.ClearConverged();
|
||||
_spatial.ClearLiveEntityLifetimeState();
|
||||
_sessionClearPendingFinalization = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue