feat(R4-V5): LOCAL PLAYER cutover - B.6 auto-walk DELETED; P1 autonomous gate ported; TS-36 bound (closes M1-local, M9, M10, M17; retires TS-36, AD-26; re-anchors AD-27, AP-23)
The local player now runs server MoveTos through the same verbatim MoveToManager remotes got in V4. One commit, GameWindow + controller, per the no-fan-out rule for coupled slices. P1 gate (V0-pins.md P1, ported verbatim): CPhysics::SetObjectMovement (0x00509690 @0050972e) drops any movement event whose wire autonomous byte is set when the addressed object IsThePlayer - ACE's self-addressed MoveToState reflection (MovementData.cs:162 IsAutonomous=1) never reaches unpack_movement, which is what makes the retail unconditional unpack-head interrupt safe. Gate placed AFTER the sequence gates (retail order). The stale "ACE follows every mt=0x06 with an mt=0x00" comment block dies with the code it excused (its causal story was pre-#75; refuted in V0-pins P1). Run-rate re-anchor (P1 contingency NOT needed - no AD row): the echo tap ApplyServerRunRate is deleted outright. Both retail feeds already exist: PlayerDescription skills via SetCharacterSkills (K-fix7) into InqRunRate (preferred by apply_run_to_command/get_state_velocity), and the mt-6/7 my_run_rate wire write (M13) now performed for the player by the shared RouteServerMoveTo. The tap's InterpretedState.ForwardSpeed overwrite was a pre-R3 mechanism that fought the ported machinery. B.6 auto-walk deleted wholesale (~330 lines): fields, Begin/End/ DriveServerAutoWalk, IsServerAutoWalking, AutoWalkArrived, the autoWalkConsumedMotion gates, the #69 turn-dir edge synthesizer, and the relocated AutoWalkArrivalEpsilon/AutoWalkTurnRateFor constants (AD-26 retired - the invented 5/30-degree bands are gone; arrival is retail's distance predicate; turn-first is the TurnToHeading node). TS-36 retired: Motion.InterruptCurrentMovement binds to MoveTo.CancelMoveTo(ActionCancelled). Movement-key edges (ctor-default params carry the 0x8000 CancelMoveTo bit), Shift (set_hold_run interrupt:true), jump(), StopCompletely, and teleport all cancel a running moveto through the retail chain - verified by controller-level tests, not assumed. MoveToComplete seam WIDENED to natural completion (Core): retail's BeginNextNode empty-queue completion is inline CleanUp+StopCompletely (raw @00529d47) and notifies nothing - the client-addition seam had to fire there (both sticky and non-sticky exits) or AD-27's deferred close-range Use/PickUp re-send never fires. Never fires on CancelMoveTo. AD-27 re-anchored from the deleted AutoWalkArrived event. InstallSpeculativeTurnToTarget rewired through the player's manager (retail 9a/9b client-initiated shape): close-range -> TurnToObject, far -> MoveToObject; AP-23 radius buckets + the #77 CanCharge prediction survive as the params source (row re-anchored). Per-tick: MoveTo.UseTime() at the old DriveServerAutoWalk slot (provisional until R6, per the plan's placement decision); the P4 player-side TargetTracker twin (fields + pre-Update feed) mirrors the remote adapter (AP-79 row widened). HitGround dual-call added on the player landing edge AND the remote landing site - the latter closes a V4 wiring-contract gap the adversarial review caught (retail 2d order: minterp then moveto; without it a landing NPC's moveto never re-arms). Also from the adversarial review: the mt-8 unresolvable-target degrade now performs retail's params.desired_heading = wire_heading substitution (decomp 2f case 8; invisible against ACE per P6, required for the verbatim degrade); the V4 remote MoveToManager binding gets a real curTime clock (the ctor stub advanced 1/30s per READ, skewing the progress/fail-distance windows - note: the pending V4 NPC smoke ran on the skewed clock); TS-33 row extended with the orientation-diff gap (ApproxPositionEqual vs retail Frame::is_equal full-frame compare - a stationary heading snap does not reach the wire; masked against ACE, R7 outbound scope owns the fix). MoveToMath gains HeadingFromYaw/YawFromHeading (the P5 scalar bridge for yaw-authoritative bodies - the player's heading snap must write Yaw, not the quaternion the controller re-derives every frame). Tests: 3,956 green (+8). New: MoveToManagerCompletionSeamTests (arrival fires once with None, no refire, cancel never fires, sticky handoff order) + PlayerMoveToCutoverTests (EnterPlayerModeNow-shape rig: walks to arrival with zero user input and zero MotionStateChanged frames - the #75 invariant by construction; TurnToHeading rotates Yaw and snaps exact; W-edge and jump cancel without firing complete). W6 edge suite retargeted from the deleted echo tap to a direct apply pass (the regression lives in ApplyInterpretedMovement, not the wire trigger). Spec: docs/research/2026-07-03-r4-moveto/r4-port-plan.md section 3 V5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e4553a0262
commit
b3decdfac6
9 changed files with 947 additions and 688 deletions
|
|
@ -0,0 +1,123 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R4-V5 — the <c>MoveToComplete</c> CLIENT-ADDITION seam contract (see the
|
||||
/// seam's doc on <see cref="MoveToManager.MoveToComplete"/>): fires with
|
||||
/// <see cref="WeenieError.None"/> on NATURAL COMPLETION (the
|
||||
/// <see cref="MoveToManager.BeginNextNode"/> empty-queue exits, both sticky
|
||||
/// and non-sticky, plus <c>CleanUpAndCallWeenie</c>'s instant-success path)
|
||||
/// and NEVER on <see cref="MoveToManager.CancelMoveTo"/>. The App layer's
|
||||
/// AD-27 re-anchor (deferred Use/PickUp re-send on arrival) depends on
|
||||
/// exactly this split: arrival fires the deferred action once; a user-input
|
||||
/// cancel must not.
|
||||
/// </summary>
|
||||
public sealed class MoveToManagerCompletionSeamTests
|
||||
{
|
||||
/// <summary>Arms a position move 5 m dead ahead (heading 90 = +X,
|
||||
/// facing it) and drives the scripted body to arrival via UseTime.</summary>
|
||||
private static MoveToManagerHarness DriveToArrival()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
h.Manager.MoveToPosition(
|
||||
new Position(1u, new Vector3(5f, 0f, 0f), Quaternion.Identity),
|
||||
new MovementParameters { UseSpheres = false, DistanceToObject = 0.6f });
|
||||
h.DrainPendingMotions();
|
||||
|
||||
for (int i = 0; i < 200 && h.Manager.IsMovingTo(); i++)
|
||||
{
|
||||
h.Tick();
|
||||
var cur = h.WorldPosition.Frame.Origin;
|
||||
h.WorldPosition = new Position(
|
||||
1u, cur + new Vector3(0.2f, 0f, 0f), Quaternion.Identity);
|
||||
h.Manager.UseTime();
|
||||
h.DrainPendingMotions();
|
||||
}
|
||||
|
||||
Assert.False(h.Manager.IsMovingTo(),
|
||||
"scripted drive must reach arrival within the tick budget");
|
||||
return h;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaturalArrival_FiresMoveToComplete_OnceWithNone()
|
||||
{
|
||||
var h = DriveToArrival();
|
||||
|
||||
Assert.Single(h.MoveToCompleteCalls);
|
||||
Assert.Equal(WeenieError.None, h.MoveToCompleteCalls[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AfterArrival_ExtraUseTimeTicks_DoNotRefire()
|
||||
{
|
||||
var h = DriveToArrival();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
h.Tick();
|
||||
h.Manager.UseTime();
|
||||
}
|
||||
|
||||
Assert.Single(h.MoveToCompleteCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelMoveTo_DoesNotFireMoveToComplete()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
h.Manager.MoveToPosition(
|
||||
new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity),
|
||||
new MovementParameters { UseSpheres = false });
|
||||
Assert.True(h.Manager.IsMovingTo());
|
||||
|
||||
h.Manager.CancelMoveTo(WeenieError.ActionCancelled);
|
||||
|
||||
Assert.False(h.Manager.IsMovingTo());
|
||||
Assert.Empty(h.MoveToCompleteCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StickyCompletion_FiresMoveToComplete_AfterStickToHandoff()
|
||||
{
|
||||
// In-range sticky object move: MoveToObject arms the tracker; the
|
||||
// first Ok callback finds the target already inside
|
||||
// DistanceToObject, so no motion nodes are needed and BeginNextNode
|
||||
// lands straight on the empty-queue STICKY exit — StickTo gets the
|
||||
// pre-CleanUp tlid/radius/height, then the completion seam fires.
|
||||
var h = new MoveToManagerHarness();
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
h.Manager.MoveToObject(
|
||||
objectId: 0x1000u, topLevelId: 0x1000u, radius: 0.5f, height: 1f,
|
||||
new MovementParameters { Sticky = true, DistanceToObject = 5f });
|
||||
h.DrainPendingMotions();
|
||||
|
||||
var target = new Position(1u, new Vector3(1f, 0f, 0f), Quaternion.Identity);
|
||||
h.Manager.HandleUpdateTarget(
|
||||
new TargetInfo(0x1000u, TargetStatus.Ok, target, target));
|
||||
h.DrainPendingMotions();
|
||||
|
||||
// Some builds need the turn/settle nodes ticked through first.
|
||||
for (int i = 0; i < 50 && h.Manager.IsMovingTo(); i++)
|
||||
{
|
||||
h.Tick();
|
||||
h.Manager.UseTime();
|
||||
h.DrainPendingMotions();
|
||||
}
|
||||
|
||||
Assert.False(h.Manager.IsMovingTo());
|
||||
Assert.Single(h.StickToCalls);
|
||||
Assert.Equal((0x1000u, 0.5f, 1f), h.StickToCalls[0]);
|
||||
Assert.Single(h.MoveToCompleteCalls);
|
||||
Assert.Equal(WeenieError.None, h.MoveToCompleteCalls[0]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue