fix(#171): R5-V3 — bind sticky melee (StickyManager live) + real arrival radii
Group-melee interpenetration + facing drift: the R5-V1-ported StickyManager/PositionManager were Core-only — the StickTo/Unstick seams were unbound and every arrival radius was 0, so ACE's Sticky|UseSpheres melee chases closed ~one body-radius too deep and froze at stale arrival poses until the next wire re-arm. Retires TS-39. Wiring (anchors re-verified against the named decomp this session): - EntityPhysicsHost owns a PositionManager; HandleUpdateTarget fans MoveToManager then PositionManager (CPhysicsObj::HandleUpdateTarget 0x00512bc0 order). - Seams bound remote + player: MoveToManager.StickTo (BeginNextNode sticky arrival @0x00529d3a), Unstick (PerformMovement head), and MotionInterpreter.UnstickFromObject (UM funnel head, 0x0050eaea). - AdjustOffset at the retail UpdatePositionInternal slot (@0x00512d0e): NPC branch composes pre-sweep (steer is swept by ResolveWithTransition); remote-player branch chains the combiner offset through the shared delta frame (the interp stage) so sticky OVERWRITES when armed (0x00555430 assigns m_fOrigin, not accumulates); player inside the 30 Hz physics quantum before UpdatePhysicsInternal. - UseTime (the 1 s lease watchdog) at the UpdateObjectInternal tail (@0x005159b3): unconditional per remote; player gated on the physics tick (retail's MinQuantum gate skips UseTime too). - Real setup cylsphere radii (CPartArray::GetRadius/GetHeight 0x005180a0/0x005180b0 = setup radius/height x ObjScale from the spawn record): own via EnsureRemoteMotionBindings + player wiring; target via RouteServerMoveTo AND the speculative use-walk install (retail resolves the target PartArray at EVERY MoveToObject site — ACE PhysicsObj.cs:951). - Teardown parity: exit_world (0x00514e60) UnStick + ClearTarget before the ExitWorld notify; player teleport fires teleport_hook's tail (UnStick in SetPosition + EntityPhysicsHost.NotifyTeleported = ClearTarget + NotifyVoyeurOfEvent(Teleported) @0x00514f1b) so mobs stuck to the player drop their sticks on a recall. - SERVERVEL arbitration also yields to a stuck entity (same starvation class as the #170 fix — sticky owns the between-snap translation). - StickyManager.UseTime aligned to retail's strict > deadline (0x00555626; ACE >): two V1 tests had pinned the >= edge — corrected. Register: TS-39 deleted; TS-41 narrowed (stickyArmed gate); TS-43 added (remote teleport_hook gap — self-corrects within the 1 s lease); AP-23 narrowed (real radii at the speculative site; only the use-radius buckets remain invented). Conformance: 2 new full-stack sticky scenarios in RemoteChaseEndToEndHarnessTests (arrive -> stick -> strafing-target gap+facing track -> lease expiry; unstick-on-rearm -> re-stick). Full suite 4038 green. Pre-commit adversarial diff review (3 lenses + per-finding refuters) confirmed and fixed 4 findings: ObjScale-dead radius read, player UseTime order inversion, missing teleport voyeur notify, speculative- site radius asymmetry. Awaiting the user visual gate: pack melee side-by-side vs retail (attackers reshuffle + keep facing; some overlap is ACE-server-side). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d413ac2a29
commit
5bd2b8bc8b
10 changed files with 586 additions and 32 deletions
|
|
@ -286,6 +286,21 @@ public sealed class PlayerMovementController
|
|||
/// </summary>
|
||||
public AcDream.Core.Physics.Motion.MoveToManager? MoveTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// R5-V3 (#171): the player's <c>PositionManager</c> facade (retail
|
||||
/// <c>CPhysicsObj::position_manager</c> — owned by the player's
|
||||
/// <c>EntityPhysicsHost</c>, handed here by <c>EnterPlayerModeNow</c>).
|
||||
/// <see cref="Update"/> drives it at the two retail per-tick points:
|
||||
/// <c>AdjustOffset</c> inside the physics-tick block (retail
|
||||
/// <c>UpdatePositionInternal</c> @0x00512d0e, BEFORE
|
||||
/// <c>UpdatePhysicsInternal</c> so the sticky steer is part of the swept
|
||||
/// motion) and <c>UseTime</c> after the completed-motions sweep (retail
|
||||
/// <c>UpdateObjectInternal</c> tail @0x005159b3 — the sticky 1 s lease
|
||||
/// watchdog). <see cref="SetPosition(Vector3, uint, Vector3)"/> tears any
|
||||
/// stick down (retail <c>teleport_hook</c> @0x00514eee).
|
||||
/// </summary>
|
||||
public AcDream.Core.Physics.Motion.PositionManager? PositionManager { get; set; }
|
||||
|
||||
public PlayerMovementController(PhysicsEngine physics)
|
||||
{
|
||||
_physics = physics;
|
||||
|
|
@ -468,6 +483,12 @@ public sealed class PlayerMovementController
|
|||
// 0x00527e40: resets fwd/sidestep/turn COMMANDS, zeroes velocity,
|
||||
// enqueues the A9 jump-snapshot node) — not a bare DoMotion(Ready).
|
||||
_motion.StopCompletely();
|
||||
// R5-V3 (#171): retail teleport_hook (0x00514ed0) — PositionManager::
|
||||
// UnStick (@0x00514eee) right after the moveto cancel: a teleport
|
||||
// tears down any active stick. (StopInterpolating/UnConstrain have no
|
||||
// armed acdream counterparts — no local-player InterpolationManager,
|
||||
// constraint leash unarmed per #167.)
|
||||
PositionManager?.UnStick();
|
||||
// Reset the edge tracker: the stop wiped the motion state, so keys
|
||||
// still physically held must re-fire as press edges on the next
|
||||
// Update (matches the pre-W6 level-triggered behavior of walking
|
||||
|
|
@ -793,6 +814,29 @@ public sealed class PlayerMovementController
|
|||
// Integrate accumulated dt, clamped to MaxQuantum so a long
|
||||
// pause doesn't produce one giant integration step.
|
||||
float tickDt = MathF.Min(_physicsAccum, PhysicsBody.MaxQuantum);
|
||||
|
||||
// R5-V3 (#171): retail UpdatePositionInternal (0x00512c30) —
|
||||
// PositionManager::adjust_offset (@0x00512d0e) composes the
|
||||
// sticky steer into this quantum's motion BEFORE
|
||||
// UpdatePhysicsInternal, so the transition sweep below resolves
|
||||
// it like any other movement (preIntegratePos was captured
|
||||
// above). The delta's Origin is mover-LOCAL (rotate out by the
|
||||
// body orientation); the heading is RELATIVE and writes Yaw —
|
||||
// the authoritative facing the body quaternion is re-derived
|
||||
// from every frame (a quaternion-only write would be clobbered).
|
||||
// No-op while nothing is stuck (untouched delta frame).
|
||||
if (PositionManager is { } ppm)
|
||||
{
|
||||
var pmDelta = new AcDream.Core.Physics.Motion.MotionDeltaFrame();
|
||||
ppm.AdjustOffset(pmDelta, tickDt);
|
||||
if (pmDelta.Origin != Vector3.Zero)
|
||||
_body.Position += Vector3.Transform(pmDelta.Origin, _body.Orientation);
|
||||
if (!pmDelta.Orientation.IsIdentity)
|
||||
Yaw = AcDream.Core.Physics.Motion.MoveToMath.YawFromHeading(
|
||||
AcDream.Core.Physics.Motion.MoveToMath.HeadingFromYaw(Yaw)
|
||||
+ pmDelta.GetHeading());
|
||||
}
|
||||
|
||||
_body.calc_acceleration();
|
||||
_body.UpdatePhysicsInternal(tickDt);
|
||||
_physicsAccum -= tickDt;
|
||||
|
|
@ -853,6 +897,16 @@ public sealed class PlayerMovementController
|
|||
{
|
||||
_prevPhysicsPos = oldTickEndPos;
|
||||
_currPhysicsPos = _body.Position;
|
||||
|
||||
// R5-V3 (#171): retail UpdateObjectInternal TAIL — PositionManager::
|
||||
// UseTime (@0x005159b3) runs AFTER the quantum's integration
|
||||
// (whose head hosts adjust_offset) and only on frames where a
|
||||
// physics quantum executed (retail's update_object MinQuantum
|
||||
// gate skips the whole UpdateObjectInternal). Diff-review find:
|
||||
// a head-of-frame placement tore an expiring stick down BEFORE
|
||||
// the crossing quantum's final steer/turn — retail applies that
|
||||
// last adjust_offset, THEN the tail watchdog tears down.
|
||||
PositionManager?.UseTime();
|
||||
}
|
||||
|
||||
// L.3a (2026-04-30): retail wall-bounce / velocity reflection.
|
||||
|
|
|
|||
|
|
@ -24,9 +24,15 @@ namespace AcDream.App.Rendering;
|
|||
/// <see cref="HandleUpdateTarget"/> fan-out are injected by GameWindow so this
|
||||
/// class stays free of GameWindow's internals (code-structure rule #1).</para>
|
||||
///
|
||||
/// <para>PositionManager (sticky) is R5-V3 — this host gains a
|
||||
/// <c>PositionManager</c> and <see cref="HandleUpdateTarget"/> fans to it then;
|
||||
/// V2 delivers only to the MoveToManager.</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
|
||||
{
|
||||
|
|
@ -68,6 +74,7 @@ public sealed class EntityPhysicsHost : IPhysicsObjHost
|
|||
_interruptCurrentMovement = interruptCurrentMovement
|
||||
?? throw new ArgumentNullException(nameof(interruptCurrentMovement));
|
||||
_targetManager = new TargetManager(this);
|
||||
PositionManager = new PositionManager(this);
|
||||
}
|
||||
|
||||
// ── IPhysicsObjHost accessors ──────────────────────────────────────────
|
||||
|
|
@ -84,9 +91,24 @@ public sealed class EntityPhysicsHost : IPhysicsObjHost
|
|||
/// <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) => _handleUpdateTarget(info);
|
||||
|
||||
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)
|
||||
|
|
@ -112,4 +134,16 @@ public sealed class EntityPhysicsHost : IPhysicsObjHost
|
|||
/// stick/moveto). Called on despawn before the host is removed from the
|
||||
/// registry.</summary>
|
||||
public void NotifyExitWorld() => _targetManager.NotifyVoyeurOfEvent(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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4284,6 +4284,14 @@ public sealed class GameWindow : IDisposable
|
|||
// distance/heading math matches the harness exactly.
|
||||
var rmT = rm;
|
||||
var mtBody = rm.Body;
|
||||
// R5-V3 (#171): real setup cylsphere radii — retail CPartArray::
|
||||
// GetRadius/GetHeight (0x005180a0/0x005180b0). Lazy per-call resolve
|
||||
// (a dictionary hit) so the read never races the spawn path's
|
||||
// CacheSetup. Feeds GetCurrentDistance's UseSpheres cylinder math
|
||||
// (own side) and StickyManager::adjust_offset's own-radius gap term.
|
||||
// Replaces the R4 `() => 0f` pins ("setup cylsphere radius lands with
|
||||
// R5-V3").
|
||||
var selfEntity = ae.Entity;
|
||||
// R5-V2: forward-declared so the MoveToManager's target seams route
|
||||
// into the entity's TargetManager (retail CPhysicsObj::set_target →
|
||||
// TargetManager::SetTarget). Assigned right after the manager is built.
|
||||
|
|
@ -4298,8 +4306,8 @@ public sealed class GameWindow : IDisposable
|
|||
mtBody.Orientation),
|
||||
setHeading: (h, _) => mtBody.Orientation =
|
||||
AcDream.Core.Physics.Motion.MoveToMath.SetHeading(mtBody.Orientation, h),
|
||||
getOwnRadius: () => 0f, // pin P4 note: setup cylsphere radius lands with R5-V3
|
||||
getOwnHeight: () => 0f,
|
||||
getOwnRadius: () => GetSetupCylinder(serverGuid, selfEntity).Radius,
|
||||
getOwnHeight: () => GetSetupCylinder(serverGuid, selfEntity).Height,
|
||||
contact: () => mtBody.OnWalkable,
|
||||
isInterpolating: () => rmT.Interp.IsActive,
|
||||
getVelocity: () => mtBody.Velocity,
|
||||
|
|
@ -4326,7 +4334,8 @@ public sealed class GameWindow : IDisposable
|
|||
// delivers the identical position for a moveto's quantum-0 case).
|
||||
// HandleTargetting ticks per frame (added in the per-remote loop);
|
||||
// HandleUpdateTarget fans deliveries to this entity's MoveToManager
|
||||
// (PositionManager sticky joins the fan-out in V3).
|
||||
// and (R5-V3, inside the host) its PositionManager — retail's
|
||||
// CPhysicsObj::HandleUpdateTarget order.
|
||||
host = new EntityPhysicsHost(
|
||||
serverGuid,
|
||||
getPosition: () => new AcDream.Core.Physics.Position(
|
||||
|
|
@ -4335,7 +4344,7 @@ public sealed class GameWindow : IDisposable
|
|||
? selfEnt.Position : mtBody.Position,
|
||||
mtBody.Orientation),
|
||||
getVelocity: () => mtBody.Velocity,
|
||||
getRadius: () => 0f, // R5-V3: real setup cylsphere radius
|
||||
getRadius: () => GetSetupCylinder(serverGuid, selfEntity).Radius,
|
||||
inContact: () => mtBody.OnWalkable,
|
||||
minterpMaxSpeed: () => rmT.Motion.GetMaxSpeed(),
|
||||
curTime: NowSeconds,
|
||||
|
|
@ -4346,6 +4355,20 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.Core.Physics.WeenieError.ActionCancelled));
|
||||
rm.Host = host;
|
||||
_physicsHosts[serverGuid] = host;
|
||||
|
||||
// R5-V3 (#171, retires TS-39): bind the sticky seams to the host's
|
||||
// PositionManager —
|
||||
// * BeginNextNode's sticky-arrival handoff: PositionManager::StickTo
|
||||
// (retail MoveToManager::BeginNextNode @0x00529d3a);
|
||||
// * PerformMovement's head unstick: unstick_from_object →
|
||||
// PositionManager::UnStick (MoveToManager.PerformMovement:414);
|
||||
// * the UM funnel head's unstick: CPhysicsObj::unstick_from_object
|
||||
// (0x0050eaea) — invoked at the mt-0 routing sites (~4894) but
|
||||
// unbound until now.
|
||||
rm.MoveTo.StickTo = (tlid, radius, height) =>
|
||||
host.PositionManager.StickTo(tlid, radius, height);
|
||||
rm.MoveTo.Unstick = host.PositionManager.UnStick;
|
||||
rm.Motion.UnstickFromObject = host.PositionManager.UnStick;
|
||||
return rm.Sink;
|
||||
}
|
||||
|
||||
|
|
@ -4393,6 +4416,64 @@ public sealed class GameWindow : IDisposable
|
|||
return minimal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R5-V3 (#171): retail <c>CPartArray::GetRadius</c>/<c>GetHeight</c>
|
||||
/// (0x005180a0/0x005180b0 — <c>setup->radius/height × scale</c>; ACE
|
||||
/// <c>PartArray.cs:189-207</c> reads the same Setup-level fields). Returns
|
||||
/// (0, 0) when the entity has no resolvable Setup (bare-GfxObj props) —
|
||||
/// ACE's <c>PartArray != null ? … : 0</c> fallback shape
|
||||
/// (<c>PhysicsObj.cs:951-952</c>). Live spawns cache their Setup in
|
||||
/// <c>_physicsDataCache</c> at spawn time (CacheSetup, ~3250), so this is
|
||||
/// a dictionary hit for every creature.
|
||||
/// </summary>
|
||||
private (float Radius, float Height) GetSetupCylinder(
|
||||
uint serverGuid, AcDream.Core.World.WorldEntity entity)
|
||||
{
|
||||
var setup = _physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
|
||||
if (setup is null)
|
||||
return (0f, 0f);
|
||||
// Live spawns bake ObjScale into MeshRefs and never populate
|
||||
// WorldEntity.Scale (a scenery-pipeline field, default 1.0) — the
|
||||
// authoritative per-entity scale is the SPAWN RECORD's ObjScale, the
|
||||
// same source the collision registration's entScale uses (~4137).
|
||||
// entity.Scale remains the fallback for non-spawn entities (scenery —
|
||||
// never a chase/sticky participant). Diff-review find: without this,
|
||||
// a server-scaled creature variant read unscaled radii and kept the
|
||||
// #171 interpenetration exactly for scaled bodies.
|
||||
float scale =
|
||||
_lastSpawnByGuid.TryGetValue(serverGuid, out var sp)
|
||||
&& sp.ObjScale is { } objScale && objScale > 0f
|
||||
? objScale
|
||||
: (entity.Scale > 0f ? entity.Scale : 1f);
|
||||
return (setup.Radius * scale, setup.Height * scale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R5-V3 (#171): apply a <see cref="AcDream.Core.Physics.Motion.MotionDeltaFrame"/>
|
||||
/// written by <c>PositionManager.AdjustOffset</c> onto a body — acdream's
|
||||
/// stand-in for retail's <c>Frame::combine</c> in
|
||||
/// <c>CPhysicsObj::UpdatePositionInternal</c> (0x00512c30, combine
|
||||
/// @0x00512d22). The delta's Origin is mover-LOCAL (sticky writes
|
||||
/// <c>globaltolocalvec</c> output — 0x00555430), so combining = rotating it
|
||||
/// out by the body orientation. The rotation carries a RELATIVE heading;
|
||||
/// an untouched (identity) rotation means "no turn" — retail distinguishes
|
||||
/// an unwritten offset rotation from <c>set_heading(0)</c> by the identity
|
||||
/// VALUE, not the angle, and the P5 pin (identity quaternion = heading 0)
|
||||
/// makes compass addition the exact frame-combine here.
|
||||
/// </summary>
|
||||
private static void ApplyPositionManagerDelta(
|
||||
AcDream.Core.Physics.PhysicsBody body,
|
||||
AcDream.Core.Physics.Motion.MotionDeltaFrame delta)
|
||||
{
|
||||
if (delta.Origin != System.Numerics.Vector3.Zero)
|
||||
body.Position += System.Numerics.Vector3.Transform(delta.Origin, body.Orientation);
|
||||
if (!delta.Orientation.IsIdentity)
|
||||
body.Orientation = AcDream.Core.Physics.Motion.MoveToMath.SetHeading(
|
||||
body.Orientation,
|
||||
AcDream.Core.Physics.Motion.MoveToMath.GetHeading(body.Orientation)
|
||||
+ delta.GetHeading());
|
||||
}
|
||||
|
||||
private bool RemoveLiveEntityByServerGuid(uint serverGuid)
|
||||
{
|
||||
if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var existingEntity))
|
||||
|
|
@ -4417,7 +4498,20 @@ public sealed class GameWindow : IDisposable
|
|||
// timeout never fires), so it must run before the host is pruned.
|
||||
if (_physicsHosts.TryGetValue(serverGuid, out var hostGone)
|
||||
&& hostGone is EntityPhysicsHost ephGone)
|
||||
{
|
||||
// R5-V3 (#171): retail exit_world (0x00514e60) order — the
|
||||
// departing entity first tears down its OWN stick
|
||||
// (PositionManager::UnStick @0x00514e88 — drops its voyeur
|
||||
// subscription on whatever it was stuck to) and its own target
|
||||
// subscription (TargetManager::ClearTarget @0x00514e97 — same
|
||||
// for a plain mid-chase moveto), THEN NotifyVoyeurOfEvent
|
||||
// (ExitWorld) tells the entities watching IT. Without the first
|
||||
// two, a despawning attacker leaves a dead voyeur entry on its
|
||||
// target until send-failure pruning.
|
||||
ephGone.PositionManager.UnStick();
|
||||
ephGone.ClearTarget();
|
||||
ephGone.NotifyExitWorld();
|
||||
}
|
||||
_physicsHosts.Remove(serverGuid);
|
||||
_animatedEntities.Remove(existingEntity.Id);
|
||||
_classificationCache.InvalidateEntity(existingEntity.Id);
|
||||
|
|
@ -4494,6 +4588,15 @@ public sealed class GameWindow : IDisposable
|
|||
ms.Type = AcDream.Core.Physics.MovementType.MoveToObject;
|
||||
ms.ObjectId = tgtGuid;
|
||||
ms.TopLevelId = tgtGuid;
|
||||
// R5-V3 (#171): retail resolves the TARGET object's PartArray
|
||||
// radius/height at the MoveToObject call site
|
||||
// (CPhysicsObj::MoveToObject 0x005128e9/0x00512903; ACE
|
||||
// PhysicsObj.cs:951-952) — they become SoughtObjectRadius/
|
||||
// Height, feeding GetCurrentDistance's edge-to-edge arrival
|
||||
// (UseSpheres) and the sticky-arrival handoff's target radius.
|
||||
// Was unset (0): every attacker closed ~one body-radius deeper
|
||||
// than retail before stopping — the #171 dogpile term.
|
||||
(ms.Radius, ms.Height) = GetSetupCylinder(tgtGuid, tgtEnt);
|
||||
ms.Pos = new AcDream.Core.Physics.Position(
|
||||
cellId, tgtEnt.Position,
|
||||
System.Numerics.Quaternion.Identity);
|
||||
|
|
@ -5861,6 +5964,16 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
_playerController.SetPosition(snappedPos, resolved.CellId,
|
||||
CellLocalForSeed(snappedPos, resolved.CellId));
|
||||
// R5-V3 (#171, diff-review find): retail teleport_hook's TAIL
|
||||
// (0x00514ed0 @0x00514f1b-0x00514f28) — after the manager teardown
|
||||
// SetPosition just ran (moveto cancel + own UnStick), the teleporting
|
||||
// object clears its OWN target subscription and tells every entity
|
||||
// WATCHING IT that it teleported (NotifyVoyeurOfEvent(Teleported)).
|
||||
// That notify is what tears down the mobs' sticks/target-tracking ON
|
||||
// the player — without it, a melee pack stuck to the player keeps
|
||||
// steering toward the post-teleport position at the 5× sticky follow
|
||||
// speed for up to the 1 s lease (non-retail lurch on every recall).
|
||||
_playerHost?.NotifyTeleported();
|
||||
// Face the server-specified destination heading (retail drops you facing a fixed
|
||||
// direction). The render entity already got _pendingTeleportRot above; sync the
|
||||
// controller yaw so the camera + movement frame match it instead of the stale
|
||||
|
|
@ -9778,7 +9891,29 @@ public sealed class GameWindow : IDisposable
|
|||
interp: rm.Interp,
|
||||
maxSpeed: maxSpeed,
|
||||
terrainNormal: terrainNormal);
|
||||
rm.Body.Position += offset;
|
||||
// R5-V3 (#171): retail chains Interpolation → Sticky over
|
||||
// ONE shared delta frame (PositionManager::adjust_offset
|
||||
// 0x00555190). The combiner's catch-up IS acdream's
|
||||
// interpolation stage, so its offset SEEDS the frame
|
||||
// (converted to mover-local) and StickyManager::
|
||||
// adjust_offset OVERWRITES it when armed+initialized
|
||||
// (0x00555430 assigns m_fOrigin rather than accumulating).
|
||||
// With no stick armed the frame comes back untouched and
|
||||
// this reduces to the pre-V3 `Position += offset`.
|
||||
if (rm.Host is { } plHost)
|
||||
{
|
||||
var pmDelta = new AcDream.Core.Physics.Motion.MotionDeltaFrame
|
||||
{
|
||||
Origin = AcDream.Core.Physics.Motion.MoveToMath.GlobalToLocalVec(
|
||||
rm.Body.Orientation, offset),
|
||||
};
|
||||
plHost.PositionManager.AdjustOffset(pmDelta, dt);
|
||||
ApplyPositionManagerDelta(rm.Body, pmDelta);
|
||||
}
|
||||
else
|
||||
{
|
||||
rm.Body.Position += offset;
|
||||
}
|
||||
|
||||
// Slope-staircase diagnostic — gated on ACDREAM_SLOPE_DIAG=1.
|
||||
// Prints per-tick body Z trajectory + queue state + projected
|
||||
|
|
@ -9961,7 +10096,20 @@ public sealed class GameWindow : IDisposable
|
|||
// (turn→run→drain sustained when the manager is ticked).
|
||||
bool moveToArmed = rm.MoveTo is
|
||||
{ MovementTypeState: not AcDream.Core.Physics.MovementType.Invalid };
|
||||
if (!IsPlayerGuid(serverGuid) && rm.HasServerVelocity && !moveToArmed)
|
||||
// R5-V3 (#171): a STUCK entity is also client-side-
|
||||
// driven — after the sticky arrival the moveto is
|
||||
// cleaned (Invalid) but StickyManager::adjust_offset
|
||||
// owns the between-snap translation exactly like an
|
||||
// armed moveto. Routing it to SERVERVEL would glide
|
||||
// the body on synthesized wire velocity AGAINST the
|
||||
// sticky steer (the same starvation class as the #170
|
||||
// SERVERVEL fix — register TS-41). Retail has no
|
||||
// SERVERVEL leg at all; it stays the fallback for
|
||||
// entities with NO client-side movement driver.
|
||||
bool stickyArmed =
|
||||
(rm.Host?.PositionManager.GetStickyObjectId() ?? 0u) != 0u;
|
||||
if (!IsPlayerGuid(serverGuid) && rm.HasServerVelocity
|
||||
&& !moveToArmed && !stickyArmed)
|
||||
{
|
||||
double velocityAge = nowSec - rm.LastServerPosTime;
|
||||
if (velocityAge > ServerControlledVelocityStaleSeconds)
|
||||
|
|
@ -10075,6 +10223,20 @@ public sealed class GameWindow : IDisposable
|
|||
// here to prevent UpdatePhysicsInternal's own omega pass from
|
||||
// double-integrating.
|
||||
var preIntegratePos = rm.Body.Position;
|
||||
// R5-V3 (#171): the sticky steer — retail
|
||||
// UpdatePositionInternal's PositionManager::adjust_offset
|
||||
// slot (0x00512c30, call @0x00512d0e): the delta composes
|
||||
// into this tick's motion BEFORE UpdatePhysicsInternal +
|
||||
// the transition sweep below, so collision resolves the
|
||||
// sticky movement like any other motion (preIntegratePos
|
||||
// is captured first — the sweep covers the steer). No-op
|
||||
// when nothing is stuck (untouched delta frame).
|
||||
if (rm.Host is { } npcHost)
|
||||
{
|
||||
var pmDelta = new AcDream.Core.Physics.Motion.MotionDeltaFrame();
|
||||
npcHost.PositionManager.AdjustOffset(pmDelta, dt);
|
||||
ApplyPositionManagerDelta(rm.Body, pmDelta);
|
||||
}
|
||||
rm.Body.calc_acceleration();
|
||||
rm.Body.UpdatePhysicsInternal(dt);
|
||||
var postIntegratePos = rm.Body.Position;
|
||||
|
|
@ -10196,6 +10358,15 @@ public sealed class GameWindow : IDisposable
|
|||
ae.Entity.ParentCellId = rm.CellId;
|
||||
ae.Entity.Rotation = rm.Body.Orientation;
|
||||
}
|
||||
|
||||
// R5-V3 (#171): retail UpdateObjectInternal tail —
|
||||
// PositionManager::UseTime (0x005156b0, call @0x005159b3,
|
||||
// right after CPartArray::HandleMovement, UNCONDITIONAL for
|
||||
// every entity in both grounded and airborne branches): the
|
||||
// sticky 1 s lease watchdog (StickyManager::UseTime
|
||||
// 0x00555610 — a stick not re-issued by a fresh server arm
|
||||
// within 1 s tears itself down). No-op while nothing is stuck.
|
||||
rm.Host?.PositionManager.UseTime();
|
||||
}
|
||||
|
||||
// ── Get per-part (origin, orientation) from either sequencer or legacy ──
|
||||
|
|
@ -12527,6 +12698,15 @@ public sealed class GameWindow : IDisposable
|
|||
? AcDream.Core.Physics.MovementType.TurnToObject
|
||||
: AcDream.Core.Physics.MovementType.MoveToObject,
|
||||
};
|
||||
// R5-V3 (#171, diff-review find): retail resolves the TARGET's
|
||||
// PartArray radius/height at EVERY MoveToObject call site — the
|
||||
// client-initiated use flow included (CPhysicsObj::MoveToObject
|
||||
// 0x005128e9/0x00512903), not just the wire mt-6 route. Without
|
||||
// this, the player's now-real own radius made the UseSpheres
|
||||
// arrival hybrid (centerDist − playerRadius ≤ useRadius) — the
|
||||
// auto-walk stopped ~one player-radius farther out than the AP-23
|
||||
// constants intended, and the two MoveToObject sites disagreed.
|
||||
(ms.Radius, ms.Height) = GetSetupCylinder(targetGuid, entity);
|
||||
// Part of this install's AP-23 adaptation: store the autonomy flag
|
||||
// exactly as the wire mt-6 this install anticipates would (the P1
|
||||
// unpack store, IsAutonomous=false) — without it the per-tick
|
||||
|
|
@ -13091,8 +13271,9 @@ public sealed class GameWindow : IDisposable
|
|||
// remotes force-assert Contact+OnWalkable every grounded tick, so
|
||||
// OnWalkable was equivalent there); (c) isInterpolating is false —
|
||||
// the local player has no InterpolationManager. Own radius/height
|
||||
// stay 0 for parity with the V4 remote bind (P4 note: setup
|
||||
// cylsphere lands with R5's TargetManager port). setHeading's
|
||||
// are the real setup cylsphere values (R5-V3 — lazy reads because
|
||||
// this method caches the player Setup a few paragraphs further
|
||||
// down). setHeading's
|
||||
// `send` flag is currently UNCONSUMED (register TS-33): the AP
|
||||
// heartbeat diffs position/plane/cell but not orientation (retail's
|
||||
// Frame::is_equal compares the full frame), so a stationary heading
|
||||
|
|
@ -13111,8 +13292,8 @@ public sealed class GameWindow : IDisposable
|
|||
getHeading: () => AcDream.Core.Physics.Motion.MoveToMath.HeadingFromYaw(pcMoveTo.Yaw),
|
||||
setHeading: (h, _) => pcMoveTo.Yaw =
|
||||
AcDream.Core.Physics.Motion.MoveToMath.YawFromHeading(h),
|
||||
getOwnRadius: () => 0f,
|
||||
getOwnHeight: () => 0f,
|
||||
getOwnRadius: () => GetSetupCylinder(_playerServerGuid, playerEntity).Radius,
|
||||
getOwnHeight: () => GetSetupCylinder(_playerServerGuid, playerEntity).Height,
|
||||
contact: () => pcMoveTo.BodyInContact,
|
||||
isInterpolating: () => false,
|
||||
getVelocity: () => pcMoveTo.BodyVelocity,
|
||||
|
|
@ -13139,7 +13320,7 @@ public sealed class GameWindow : IDisposable
|
|||
? pSelf.Position : pcMoveTo.Position,
|
||||
pcMoveTo.BodyOrientation),
|
||||
getVelocity: () => pcMoveTo.BodyVelocity,
|
||||
getRadius: () => 0f, // R5-V3: real setup cylsphere radius
|
||||
getRadius: () => GetSetupCylinder(_playerServerGuid, playerEntity).Radius,
|
||||
inContact: () => pcMoveTo.BodyInContact,
|
||||
minterpMaxSpeed: () => pcMoveTo.Motion.GetMaxSpeed(),
|
||||
curTime: () => pcMoveTo.SimTimeSeconds,
|
||||
|
|
@ -13166,6 +13347,19 @@ public sealed class GameWindow : IDisposable
|
|||
};
|
||||
_playerController.MoveTo = playerMoveTo;
|
||||
|
||||
// R5-V3 (#171, retires TS-39 — player side): bind the sticky seams to
|
||||
// the player host's PositionManager (same trio as the remote bind in
|
||||
// EnsureRemoteMotionBindings: BeginNextNode arrival StickTo
|
||||
// @0x00529d3a, PerformMovement-head Unstick, UM-funnel-head
|
||||
// unstick_from_object 0x0050eaea) and hand the facade to the
|
||||
// controller, which drives AdjustOffset/UseTime at the retail
|
||||
// UpdatePositionInternal/UpdateObjectInternal points.
|
||||
playerMoveTo.StickTo = (tlid, radius, height) =>
|
||||
playerHost.PositionManager.StickTo(tlid, radius, height);
|
||||
playerMoveTo.Unstick = playerHost.PositionManager.UnStick;
|
||||
_playerController.Motion.UnstickFromObject = playerHost.PositionManager.UnStick;
|
||||
_playerController.PositionManager = playerHost.PositionManager;
|
||||
|
||||
// TS-36 RETIRED: the interp's interrupt seam is retail's
|
||||
// interrupt_current_movement → MovementManager::CancelMoveTo(0x36)
|
||||
// chain (raw 278189-278200). Every DoMotion/StopMotion/
|
||||
|
|
|
|||
|
|
@ -136,7 +136,9 @@ public sealed class StickyManager
|
|||
if (TargetId == 0)
|
||||
return;
|
||||
|
||||
if (_host.CurTime >= StickyTimeoutTime)
|
||||
// Strictly AFTER the deadline (retail 0x00555626 `test ah,0x41` —
|
||||
// C0|C3 clear = cur_time > timeout; ACE `>` too), not >=.
|
||||
if (_host.CurTime > StickyTimeoutTime)
|
||||
{
|
||||
TargetId = 0;
|
||||
Initialized = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue