refactor(world): canonicalize live physics host ownership

This commit is contained in:
Erik 2026-07-21 14:05:34 +02:00
parent 5882b308c1
commit fcb66198fc
21 changed files with 1546 additions and 323 deletions

View file

@ -777,7 +777,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
private IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> LastSpawns =>
_liveEntities?.Snapshots ?? EmptyLiveSpawnMap;
// R5-V2: the local player's CPhysicsObj stand-in — owns the player's
// TargetManager voyeur system, registered in _physicsHosts so remote
// TargetManager voyeur system, stored on the exact live record so remote
// entities chasing the player resolve it. Replaces the AP-79
// _playerMoveToTarget* poll fields.
private EntityPhysicsHost? _playerHost;
@ -787,7 +787,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
// giving the TargetManager voyeur round-trip its cross-entity delivery
// path. Populated in EnsureRemoteMotionBindings (remotes) + EnterPlayerModeNow
// (player); pruned only by logical LiveEntityRuntime teardown.
private readonly Dictionary<uint, AcDream.Core.Physics.Motion.IPhysicsObjHost> _physicsHosts = new();
private static bool IsPlayerGuid(uint guid) => (guid & 0xFF000000u) == 0x50000000u;
// ServerControlledVelocityStaleSeconds moved to RemotePhysicsUpdater (#184
@ -2644,7 +2643,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
AnimationHookFrames = () => _animationHookFrames?.Clear(),
LivePresentation = () => _liveEntityPresentation?.Clear(),
RemoteMovementDiagnostics = _remoteLastMove.Clear,
PhysicsHostIndex = _physicsHosts.Clear,
});
private void ResetSessionMouseCapture()
@ -4714,7 +4712,12 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
// live object has no PartArray sink, so build the manager once anyway.
if (rm.Host is not null)
return rm.Sink;
if (_liveEntities is not { } liveEntities
|| !liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord hostRecord)
|| !ReferenceEquals(hostRecord.RemoteMotionRuntime, rm))
{
return rm.Sink;
}
// R4-V4: the verbatim MoveToManager, seam-bound per the V2 harness
// wiring (the conformance-tested reference). Positions are WORLD
// space on both sides (getPosition + the MovementStruct positions
@ -4799,12 +4802,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
// HandleUpdateTarget fans deliveries to this entity's MoveToManager
// and (R5-V3, inside the host) its PositionManager — retail's
// CPhysicsObj::HandleUpdateTarget order.
host = new EntityPhysicsHost(
var configuredHost = new EntityPhysicsHost(
serverGuid,
getPosition: () => new AcDream.Core.Physics.Position(
rmT.CellId,
_entitiesByServerGuid.TryGetValue(serverGuid, out var selfEnt)
? selfEnt.Position : mtBody.Position,
hostRecord.FullCellId,
hostRecord.WorldEntity?.Position ?? mtBody.Position,
mtBody.Orientation),
getVelocity: () => mtBody.Velocity,
getRadius: () => GetSetupCylinder(serverGuid, selfEntity).Radius,
@ -4820,8 +4822,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
handleUpdateTarget: info => rmT.Movement.HandleUpdateTarget(info),
interruptCurrentMovement: () => rmT.Movement.CancelMoveTo(
AcDream.Core.Physics.WeenieError.ActionCancelled));
rm.Host = host;
_physicsHosts[serverGuid] = host;
host = EntityPhysicsHost.InstallOrRebind(
liveEntities,
hostRecord,
configuredHost);
rm.MarkFullPhysicsHostBound();
// R5-V5: retail MakeMoveToManager (0x00524000) — constructs the
// MoveToManager via the factory above (host now exists for the
@ -4851,42 +4856,38 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
/// </summary>
private AcDream.Core.Physics.Motion.IPhysicsObjHost? ResolvePhysicsHost(uint id)
{
// CObjCell::hide_object removes Hidden objects from the lookup surface
// used to establish new target/voyeur relationships. Existing
// subscriptions are cleared by LiveEntityPresentationController.
if (!_visibleEntitiesByServerGuid.ContainsKey(id))
if (_liveEntities is not { } liveEntities)
return null;
bool isActive = liveEntities.TryGetRecord(id, out LiveEntityRecord activeRecord);
if (!isActive)
return null;
// TS-49: CObjCell::hide_object removes Hidden objects from the
// relationship surface until DetectionManager is ported. Pending,
// attached, and otherwise non-render-visible active objects remain in
// CObjectMaint's object table and must still resolve here.
if (liveEntities.IsHidden(id))
{
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
{
Console.WriteLine(
$"[autowalk-host-miss] object=0x{id:X8} "
+ $"materialized={_entitiesByServerGuid.ContainsKey(id)} "
+ $"registered={_physicsHosts.ContainsKey(id)} "
+ $"hidden={_liveEntities?.IsHidden(id) == true}");
+ $"registered={liveEntities.TryGetPhysicsHost(id, out _)} "
+ $"hidden={liveEntities.IsHidden(id)}");
}
return null;
}
if (_physicsHosts.TryGetValue(id, out var existing))
if (liveEntities.TryGetPhysicsHost(id, out var existing))
return existing;
double NowSeconds() => (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
var minimal = new EntityPhysicsHost(
id,
getPosition: () => new AcDream.Core.Physics.Position(
0u,
_visibleEntitiesByServerGuid.TryGetValue(id, out var e)
? e.Position : System.Numerics.Vector3.Zero,
System.Numerics.Quaternion.Identity),
getVelocity: () => System.Numerics.Vector3.Zero, // static target
getRadius: () => 0f,
inContact: () => true,
minterpMaxSpeed: () => null,
curTime: NowSeconds,
physicsTimerTime: NowSeconds,
getObjectA: ResolvePhysicsHost,
handleUpdateTarget: _ => { }, // not a watcher
interruptCurrentMovement: () => { });
_physicsHosts[id] = minimal;
var minimal = EntityPhysicsHost.CreateMinimal(
activeRecord,
ResolvePhysicsHost,
NowSeconds);
liveEntities.InstallPhysicsHost(activeRecord, minimal);
return minimal;
}
@ -4973,7 +4974,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
AcDream.Core.Selection.SelectionChangeSource.System,
AcDream.Core.Selection.SelectionChangeReason.Cleared);
if (_physicsHosts.TryGetValue(serverGuid, out var hiddenHost)
if (_liveEntities?.TryGetPhysicsHost(serverGuid, out var hiddenHost) == true
&& hiddenHost is EntityPhysicsHost hiddenEntityHost)
hiddenEntityHost.NotifyHidden();
}
@ -5038,15 +5039,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
// moveto/stick. This is the ONLY clean-up for a watcher whose target
// already sent an Ok (once status != Undefined the 10 s staleness
// timeout never fires), so it must run before the host is pruned.
EntityPhysicsHost? ephGone = rmGone?.Host;
if (ephGone is null)
{
incarnation.RunIfNoReplacement(() =>
{
if (_physicsHosts.TryGetValue(serverGuid, out var hostGone))
ephGone = hostGone as EntityPhysicsHost;
});
}
EntityPhysicsHost? ephGone = record.PhysicsHost as EntityPhysicsHost;
if (ephGone is not null)
{
// R5-V3 (#171): retail exit_world (0x00514e60) order — the
@ -5061,8 +5054,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
cleanups.Add(ephGone.PositionManager.UnStick);
cleanups.Add(ephGone.ClearTarget);
cleanups.Add(ephGone.NotifyExitWorld);
EntityPhysicsHost capturedHost = ephGone;
cleanups.Add(() => incarnation.RemoveCaptured(_physicsHosts, capturedHost));
}
cleanups.Add(() => _animatedEntities.Remove(existingEntity.Id));
cleanups.Add(() => _classificationCache.InvalidateEntity(existingEntity.Id));
@ -5259,7 +5250,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
bool targetVisible = turnPath.TargetGuid is { } visibleGuid
&& _visibleEntitiesByServerGuid.ContainsKey(visibleGuid);
bool targetHost = turnPath.TargetGuid is { } hostGuid
&& _physicsHosts.ContainsKey(hostGuid);
&& _liveEntities?.TryGetPhysicsHost(hostGuid, out _) == true;
var moveTo = movement.MoveTo;
Console.WriteLine(
$"[autowalk-turn-route] wire=0x{update.MotionState.MovementType:X2} "
@ -10922,9 +10913,10 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
Func<bool> isCurrent)
{
_remoteDeadReckon.TryGetValue(serverGuid, out RemoteMotion? remote);
EntityPhysicsHost? host = _physicsHosts.TryGetValue(serverGuid, out var registered)
? registered as EntityPhysicsHost
: remote?.Host;
EntityPhysicsHost? host =
_liveEntities?.TryGetPhysicsHost(serverGuid, out var registered) == true
? registered as EntityPhysicsHost
: null;
return AcDream.App.Physics.RemoteTeleportHook.Execute(
new AcDream.App.Physics.RemoteTeleportHookActions(
CancelMoveTo: error => remote?.Movement.CancelMoveTo(error),
@ -13139,18 +13131,19 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
return false;
}
LiveEntityRecord? playerRecord = null;
if (_liveEntities?.TryGetRecord(
if (_liveEntities is not { } playerLiveEntities
|| !playerLiveEntities.TryGetRecord(
_playerServerGuid,
out LiveEntityRecord foundPlayerRecord) == true)
out LiveEntityRecord playerRecord))
{
playerRecord = foundPlayerRecord;
Console.WriteLine(
$"live: {loggingTag} — player record 0x{_playerServerGuid:X8} not found yet");
return false;
}
_playerController = new AcDream.App.Input.PlayerMovementController(
_physicsEngine,
playerRecord?.ObjectClock);
if (playerRecord is not null)
_playerController.ApplyPhysicsState(playerRecord.FinalPhysicsState);
playerRecord.ObjectClock);
_playerController.ApplyPhysicsState(playerRecord.FinalPhysicsState);
// R4-V5: the local player's verbatim MoveToManager — same seam
// wiring shape as EnsureRemoteMotionBindings, with three
@ -13232,15 +13225,15 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
// R5-V2: the player's CPhysicsObj stand-in + TargetManager. Position is
// the player's WORLD position (WorldEntity.Position — what the AP-79
// poll used), so an NPC watching the player receives the identical
// position. Registered in _physicsHosts so those NPCs' GetObjectA
// position. Stored on the exact live record so those NPCs' GetObjectA
// resolves the player; HandleTargetting is ticked in the player
// pre-Update block.
playerHost = new EntityPhysicsHost(
var exactPlayerMovement = pcMoveTo.Movement;
var configuredPlayerHost = new EntityPhysicsHost(
_playerServerGuid,
getPosition: () => new AcDream.Core.Physics.Position(
pcMoveTo.CellId,
_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pSelf)
? pSelf.Position : pcMoveTo.Position,
playerRecord.FullCellId,
playerRecord.WorldEntity?.Position ?? pcMoveTo.Position,
pcMoveTo.BodyOrientation),
getVelocity: () => pcMoveTo.BodyVelocity,
getRadius: () => GetSetupCylinder(_playerServerGuid, playerEntity).Radius,
@ -13263,12 +13256,15 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
+ $"{info.TargetPosition.Frame.Origin.Y:F2},"
+ $"{info.TargetPosition.Frame.Origin.Z:F2})");
}
_playerController?.Movement.HandleUpdateTarget(info);
exactPlayerMovement.HandleUpdateTarget(info);
},
interruptCurrentMovement: () => _playerController?.Movement.CancelMoveTo(
interruptCurrentMovement: () => exactPlayerMovement.CancelMoveTo(
AcDream.Core.Physics.WeenieError.ActionCancelled));
playerHost = EntityPhysicsHost.InstallOrRebind(
playerLiveEntities,
playerRecord,
configuredPlayerHost);
_playerHost = playerHost;
_physicsHosts[_playerServerGuid] = playerHost;
// R5-V5: retail MakeMoveToManager (0x00524000) — constructs the
// player MoveToManager via the factory above (playerHost now exists