The "sustain the run" residual. The handoff's "Ready stop-node backlog drains a beat slower than retail" framing was DISPROVEN: a new full-stack offline harness (RemoteChaseEndToEndHarnessTests — real MoveToManager + MotionInterpreter + AnimationSequencer + MotionTableDispatchSink + the manual omega integration, wired field-for-field like EnsureRemoteMotionBindings and ticked in TickAnimations' exact phase order) proves the Core turn/run/drain pipeline healthy: the chase turn completes in <1 s both directions, BeginMoveForward installs per arm, the run sustains across re-arms and attack swings, and pending_motions fully empties (retail cdb invariant add_to_queue == MotionDone). The real mechanism (launch-drainq.log, corrected per-guid attribution — the previous session's timeline mis-attributed [mvto] lines that fire in the network phase): funnel per chasing scamp was 16 mt-6 arms -> 11 dispatched turns -> ONE BeginMoveForward. Any NPC receiving UpdatePositions gets HasServerVelocity=true (synthesized from position deltas even when the wire carries no velocity), and the grounded per-tick branch routed those to the SERVERVEL leg, which SKIPS MoveToManager.UseTime — [npc-tick] literally logged "branch=SERVERVEL (skips UseTime) mtState=MoveToObject". The armed moveto was starved for exactly the duration of the server-side chase: legs stayed in Ready while the body glided on synthesized velocity (the #170 slide); the manager only woke in UP-silent gaps (creature stopped server-side) and its stale-heading turn was interrupted by the next UM before reaching BeginMoveForward. Retail runs MovementManager::UseTime UNCONDITIONALLY every tick (CPhysicsObj::UpdateObjectInternal 0x005156b0, call @0x00515998) and has no wire-velocity leg-driver anywhere; between UPs a moveto-driven body translates from the motion state (get_state_velocity) with UP hard-snaps correcting drift. Fix: an armed moveto (MovementTypeState != Invalid) always takes the MOVETO leg; SERVERVEL remains only as the legacy fallback for entities without a moveto (scripted paths / missiles). Register: TS-41 (the narrowed SERVERVEL stopgap), TS-42 (drain-order divergence also pinned this session: acdream drains AnimDone->MotionDone AFTER HandleTargetting/MoveTo.UseTime; retail's process_hooks @0x00512d3d runs BEFORE TargetManager/MovementManager in UpdateObjectInternal — one frame of extra latency, R6 scope). New conformance: RemoteChaseEndToEndHarnessTests (3 scenarios + theory) + RemoteChaseDrainBisectTests (the drain-chain pin; its first run also demonstrated the TS-40 InWorld=false link-strip wedge shape — harness bodies must replicate the live RemoteMotion construction). ISSUES #170 updated (awaiting user visual gate; probes stay until then); handoff doc superseded-note added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
105 lines
4.6 KiB
C#
105 lines
4.6 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using AcDream.Core.Physics;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace AcDream.Core.Tests.Physics.Motion;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// #170 drain-chain conformance: a stance-change UM's completion must flow
|
|
// CSequence link-anim completion → AnimDone hook → ConsumePendingHooks →
|
|
// MotionTableManager.AnimationDone countdown → MotionDoneTarget →
|
|
// CMotionInterp.MotionDone pop, fully emptying BOTH queues (retail cdb
|
|
// invariant: add_to_queue == MotionDone).
|
|
//
|
|
// History: the first harness run wedged here EXACTLY like the live #170
|
|
// signature ([drainq] q=[0x8000003C ...] stuck) — because the harness body
|
|
// was unseeded (InWorld=false), so the TS-40 detached-object guard stripped
|
|
// every dispatched transition link while the manager kept counting its ticks.
|
|
// With the live-faithful RemoteMotion construction the chain drains in ~1 s.
|
|
// Kept as the regression pin for that whole chain (and as the canonical
|
|
// demonstration of what a link-strip-without-tick-zeroing wedge looks like).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
public sealed class RemoteChaseDrainBisectTests
|
|
{
|
|
private readonly ITestOutputHelper _out;
|
|
public RemoteChaseDrainBisectTests(ITestOutputHelper output) => _out = output;
|
|
|
|
[Fact]
|
|
public void StanceChange_DrainChain_TickByTick()
|
|
{
|
|
var h = new RemoteChaseHarness();
|
|
|
|
// settle spawn
|
|
for (int i = 0; i < 30; i++) h.Tick();
|
|
DumpState(h, "pre-stance");
|
|
|
|
h.UmInterpreted(RemoteChaseHarness.Combat, RemoteChaseHarness.Ready);
|
|
DumpState(h, "post-UM (t=0)");
|
|
|
|
for (int i = 1; i <= 90; i++)
|
|
{
|
|
TickWithHookTrace(h, i);
|
|
if (i % 6 == 0 || i <= 3)
|
|
DumpState(h, $"tick {i} (t={i / 60f:F2})");
|
|
if (!h.Interp.MotionsPending() && h.Seq.Manager.PendingAnimations.Any() == false)
|
|
{
|
|
DumpState(h, $"DRAINED at tick {i}");
|
|
return; // chain healthy
|
|
}
|
|
}
|
|
|
|
DumpState(h, "END — still wedged");
|
|
Assert.Fail("drain chain wedged after stance UM — see output for where");
|
|
}
|
|
|
|
private void TickWithHookTrace(RemoteChaseHarness h, int i)
|
|
{
|
|
// Replicate RemoteChaseHarness.Tick but with hook visibility: we call
|
|
// the same phases, intercepting the hook list.
|
|
h.Now += RemoteChaseHarness.Dt;
|
|
h.Mgr.UseTime();
|
|
if (h.Body.OnWalkable)
|
|
h.Body.set_local_velocity(h.Interp.get_state_velocity(), false);
|
|
|
|
h.Seq.Advance(RemoteChaseHarness.Dt);
|
|
var hooks = h.Seq.ConsumePendingHooks();
|
|
if (hooks.Count > 0)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append(FormattableString.Invariant($"tick {i}: hooks=["));
|
|
for (int k = 0; k < hooks.Count; k++)
|
|
{
|
|
if (k > 0) sb.Append(", ");
|
|
sb.Append(hooks[k]?.GetType().Name ?? "null");
|
|
}
|
|
sb.Append(']');
|
|
_out.WriteLine(sb.ToString());
|
|
}
|
|
for (int k = 0; k < hooks.Count; k++)
|
|
{
|
|
if (hooks[k] is DatReaderWriter.Types.AnimationDoneHook)
|
|
h.Seq.Manager.AnimationDone(success: true);
|
|
}
|
|
h.Seq.Manager.UseTime();
|
|
}
|
|
|
|
private void DumpState(RemoteChaseHarness h, string tag)
|
|
{
|
|
var mgrQ = string.Join(" ", h.Seq.Manager.PendingAnimations
|
|
.Select(p => FormattableString.Invariant($"(0x{p.Motion:X8},t={p.NumAnims})")));
|
|
var core = h.Seq.Core;
|
|
var seqList = new StringBuilder();
|
|
for (var n = core.AnimList.First; n is not null; n = n.Next)
|
|
{
|
|
if (seqList.Length > 0) seqList.Append(',');
|
|
seqList.Append(FormattableString.Invariant($"fr={n.Value.Framerate:F0}"));
|
|
if (ReferenceEquals(n, core.FirstCyclicNode)) seqList.Append('*');
|
|
if (ReferenceEquals(n, core.CurrAnimNode)) seqList.Append('^');
|
|
}
|
|
_out.WriteLine(FormattableString.Invariant(
|
|
$"[{tag}] interpPending={h.Interp.MotionsPending()} mgrQ=[{mgrQ}] counter={h.Seq.Manager.AnimationCounter} seq=[{seqList}] frame={core.FrameNumber:F1} substate=0x{h.Seq.Manager.State.Substate:X8} style=0x{h.Seq.Manager.State.Style:X8}"));
|
|
}
|
|
}
|