refactor(world): canonicalize live physics host ownership
This commit is contained in:
parent
5882b308c1
commit
fcb66198fc
21 changed files with 1546 additions and 323 deletions
|
|
@ -1,162 +0,0 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <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 registered in <c>GameWindow._physicsHosts</c> (guid → host), 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 readonly Func<Position> _getPosition;
|
||||
private readonly Func<Vector3> _getVelocity;
|
||||
private readonly Func<float> _getRadius;
|
||||
private readonly Func<bool> _inContact;
|
||||
private readonly Func<float?> _minterpMaxSpeed;
|
||||
private readonly Func<double> _curTime;
|
||||
private readonly Func<double> _physicsTimerTime;
|
||||
private readonly Func<uint, IPhysicsObjHost?> _getObjectA;
|
||||
private readonly Action<TargetInfo> _handleUpdateTarget;
|
||||
private readonly 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 = getPosition ?? throw new ArgumentNullException(nameof(getPosition));
|
||||
_getVelocity = getVelocity ?? throw new ArgumentNullException(nameof(getVelocity));
|
||||
_getRadius = getRadius ?? throw new ArgumentNullException(nameof(getRadius));
|
||||
_inContact = inContact ?? throw new ArgumentNullException(nameof(inContact));
|
||||
_minterpMaxSpeed = minterpMaxSpeed ?? throw new ArgumentNullException(nameof(minterpMaxSpeed));
|
||||
_curTime = curTime ?? throw new ArgumentNullException(nameof(curTime));
|
||||
_physicsTimerTime = physicsTimerTime ?? throw new ArgumentNullException(nameof(physicsTimerTime));
|
||||
_getObjectA = getObjectA ?? throw new ArgumentNullException(nameof(getObjectA));
|
||||
_handleUpdateTarget = handleUpdateTarget ?? throw new ArgumentNullException(nameof(handleUpdateTarget));
|
||||
_interruptCurrentMovement = interruptCurrentMovement
|
||||
?? throw new ArgumentNullException(nameof(interruptCurrentMovement));
|
||||
_targetManager = new TargetManager(this);
|
||||
PositionManager = new PositionManager(this);
|
||||
}
|
||||
|
||||
// ── 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 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) => _targetManager.ReceiveUpdate(info);
|
||||
public void AddVoyeur(uint watcherId, float radius, double quantum)
|
||||
=> _targetManager.AddVoyeur(watcherId, radius, quantum);
|
||||
public void RemoveVoyeur(uint watcherId) => _targetManager.RemoveVoyeur(watcherId);
|
||||
|
||||
// ── 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue