feat(R3-W4): ground transitions + lifecycle verbatim; K-fix18 DELETED (closes J8, J10, J11-shape, J12, J13, J18, J19)

Core (dedicated agent, independently reviewed): HitGround 0x00528ac0 /
LeaveGround 0x00528b00 verbatim (creature+gravity gates, the
RemoveLinkAnimations seam — K-fix18's retail mechanism — velocity via
GetLeaveGroundVelocity with the autonomous flag, jump-state resets,
apply_current_movement re-sync); enter_default_state 0x00528c80 per A8
(fresh states, InitializeMotionTables seam, sentinel APPENDED without
draining pending_motions — pinned, Initted=1, LeaveGround tail);
Initted gates; the A3 IsThePlayer dual dispatch in
apply_current_movement / ReportExhaustion / SetWeenieObject /
SetPhysicsObject (a remote player routes INTERPRETED — the
ACE-divergence pin); set_hold_run 0x00528b70 + SetHoldKey 0x00528bb0
(XOR guard, None-only-from-Run); adjust_motion creature guard wired
(TS-34 retired); PhysicsBody.LastMoveWasAutonomous +
set_local_velocity(autonomous). Port discovery: retail's
apply_raw_movement 0x005287e0 / apply_interpreted_movement 0x00528600
ARE the already-shipped D6.2a/funnel functions — the dual dispatch
composes them instead of duplicating.

App cutover (orchestrator): the skipTransitionLink flag + both K-fix18
call sites DELETED (AP-74 retired). MotionInterpreter.DefaultSink routes
apply_current_movement's interpreted branch through the REAL funnel
dispatch when a sink is bound — so a remote's LeaveGround engages
Falling via the contact-gated funnel, replacing the forced SetCycle
(J19); the per-remote MotionTableDispatchSink is now PERSISTENT
(EnsureRemoteMotionBindings: DefaultSink + RemoveLinkAnimations +
InitializeMotionTables seams, idempotent from both the UM and
VectorUpdate paths; wire velocity re-applied after LeaveGround so it
stays authoritative). Player: seams bound to the player sequencer; the
controller's grounded→airborne EDGE now fires LeaveGround (jump()
clears OnWalkable and the same frame's transition detection fires it —
retail's order; walk-off-a-ledge gets the momentum fallback + link
strip it never had); the manual jump-block LeaveGround deleted;
LastMoveWasAutonomous set at the controller chokepoint (W6 refines).

Trace S8 re-expressed as the retail mechanism (Falling dispatch +
RemoveAllLinkAnimations = same final state the flag produced). 43 new
lifecycle tests. Registers: TS-34 + AP-74 retired; TS-38, AP-77, AP-78
added. Full suite: 3,665 passed. Live smoke: in-world clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 23:01:31 +02:00
parent af4764443f
commit e214acdf23
8 changed files with 1575 additions and 124 deletions

View file

@ -412,6 +412,11 @@ public sealed class GameWindow : IDisposable
{
public AcDream.Core.Physics.PhysicsBody Body;
public AcDream.Core.Physics.MotionInterpreter Motion;
/// <summary>R3-W4: the persistent per-remote dispatch sink (created
/// once by EnsureRemoteMotionBindings; also bound as
/// Motion.DefaultSink so LeaveGround/HitGround re-applies dispatch
/// cycles through the motion-table backend).</summary>
public AcDream.Core.Physics.Motion.MotionTableDispatchSink? Sink;
/// <summary>Last UpdatePosition timestamp — drives body.update_object sub-stepping.</summary>
public double LastServerPosTime;
/// <summary>Last known server position — kept for diagnostics / HUD.</summary>
@ -4176,6 +4181,45 @@ public sealed class GameWindow : IDisposable
}
}
/// <summary>
/// R3-W4: one-time per-remote wiring of the animation-dispatch stack —
/// the persistent <see cref="RemoteMotion.Sink"/> (ObservedOmega turn
/// callbacks), <c>Motion.DefaultSink</c> (so
/// <c>apply_current_movement</c>'s interpreted branch dispatches cycles
/// — the retail mechanism behind the deleted K-fix18 forced-Falling),
/// and the <c>RemoveLinkAnimations</c>/<c>InitializeMotionTables</c>
/// seams (retail CPhysicsObj::RemoveLinkAnimations 0x0050fe20 /
/// InitializeMotionTables). Idempotent; safe from both the UM path and
/// the VectorUpdate path regardless of arrival order.
/// </summary>
private AcDream.Core.Physics.Motion.MotionTableDispatchSink? EnsureRemoteMotionBindings(
RemoteMotion rm, AnimatedEntity ae)
{
if (ae.Sequencer is null)
return rm.Sink;
if (rm.Sink is not null)
return rm.Sink;
var rmForSink = rm;
rm.Sink = new AcDream.Core.Physics.Motion.MotionTableDispatchSink(ae.Sequencer)
{
TurnApplied = (turnMotion, turnSpeed) =>
{
float signed = (turnMotion & 0xFFu) == 0x0E
? -System.MathF.Abs(turnSpeed)
: turnSpeed;
rmForSink.ObservedOmega = new System.Numerics.Vector3(
0f, 0f, -(System.MathF.PI / 2f) * signed);
},
TurnStopped = () =>
rmForSink.ObservedOmega = System.Numerics.Vector3.Zero,
};
rm.Motion.DefaultSink = rm.Sink;
rm.Motion.RemoveLinkAnimations = ae.Sequencer.RemoveAllLinkAnimations;
rm.Motion.InitializeMotionTables = () => ae.Sequencer.Manager.InitializeState();
return rm.Sink;
}
private bool RemoveLiveEntityByServerGuid(uint serverGuid)
{
if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var existingEntity))
@ -4623,31 +4667,13 @@ public sealed class GameWindow : IDisposable
ims.Actions = actionList;
}
// R2-Q5: funnel dispatches go STRAIGHT into the motion-
// table stack (GetObjectSequence + is_allowed decide) —
// RemoteMotionSink's single-cycle pick + fallback chains
// are deleted (AP-73 retired). The callbacks are the
// ObservedOmega seam: seed remote rotation from the wire
// turn this tick (retail rotates from sequence omega in
// the per-tick apply_physics chain — R6; register row).
AcDream.Core.Physics.Motion.MotionTableDispatchSink? sink = null;
if (ae.Sequencer is not null)
{
var rmForSink = remoteMot;
sink = new AcDream.Core.Physics.Motion.MotionTableDispatchSink(ae.Sequencer)
{
TurnApplied = (turnMotion, turnSpeed) =>
{
float signed = (turnMotion & 0xFFu) == 0x0E
? -System.MathF.Abs(turnSpeed)
: turnSpeed;
rmForSink.ObservedOmega = new System.Numerics.Vector3(
0f, 0f, -(System.MathF.PI / 2f) * signed);
},
TurnStopped = () =>
rmForSink.ObservedOmega = System.Numerics.Vector3.Zero,
};
}
// R2-Q5/R3-W4: funnel dispatches go STRAIGHT into the
// motion-table stack (GetObjectSequence + is_allowed
// decide). The sink is PERSISTENT per remote and also
// bound as Motion.DefaultSink + the RemoveLink/InitTables
// seams (EnsureRemoteMotionBindings) so LeaveGround/
// HitGround dispatch through the same backend.
var sink = EnsureRemoteMotionBindings(remoteMot, ae);
remoteMot.Motion.MoveToInterpretedState(ims, sink);
}
}
@ -4813,26 +4839,24 @@ public sealed class GameWindow : IDisposable
| AcDream.Core.Physics.TransientStateFlags.OnWalkable);
rm.Body.State |= AcDream.Core.Physics.PhysicsStateFlags.Gravity;
// K-fix10 (2026-04-26): force the Falling animation cycle on
// the remote so the legs match the arc. Mirrors the local
// player's UpdatePlayerAnimation path which sets
// animCommand = Falling whenever !IsOnGround.
//
// K-fix18 (2026-04-26): pass skipTransitionLink:true so the
// RunForward→Falling transition frames don't play first.
// Without that flag the remote stood still for ~100 ms at
// the start of the jump while the link drained, then
// folded into Falling. Skipping the link makes the pose
// engage immediately on jump start.
// R3-W4 (J19 — K-fix10/K-fix18 DELETED): the retail mechanism.
// The remote's ground departure fires LeaveGround (0x00528b00):
// strips pending transition links (the RemoveLinkAnimations
// seam) + re-applies movement through DefaultSink, whose
// contact-gated funnel dispatch engages Falling — no forced
// SetCycle, no skip flag. The wire velocity/omega are re-applied
// AFTER so they stay authoritative over LeaveGround's
// state-derived velocity write (adaptation note: retail's
// equivalence comes from the per-tick transition-sweep order —
// R6 scope).
if (_entitiesByServerGuid.TryGetValue(update.Guid, out var ent)
&& _animatedEntities.TryGetValue(ent.Id, out var ae)
&& ae.Sequencer is not null)
{
uint style = ae.Sequencer.CurrentStyle != 0
? ae.Sequencer.CurrentStyle
: 0x8000003Du; // NonCombat default
ae.Sequencer.SetCycle(style, AcDream.Core.Physics.MotionCommand.Falling, 1.0f,
skipTransitionLink: true);
EnsureRemoteMotionBindings(rm, ae);
rm.Motion.LeaveGround();
rm.Body.Velocity = update.Velocity;
rm.Body.Omega = update.Omega;
}
}
@ -10241,14 +10265,11 @@ public sealed class GameWindow : IDisposable
{
animScale = s;
}
// K-fix18 (2026-04-26): when transitioning into Falling
// (jump start), skip the link so the legs engage Falling
// immediately. Without this the local player visibly
// stood still for ~100 ms at the start of every jump
// while the RunForward→Falling transition link drained.
// For everything else (Walk → Run, Run → Ready, etc.) we
// keep the link so transitions stay smooth.
bool skipLink = animCommand == AcDream.Core.Physics.MotionCommand.Falling;
// R3-W4: the K-fix18 skipLink flag is DELETED — the instant
// Falling engage comes from the controller's grounded→airborne
// edge firing MotionInterpreter.LeaveGround, whose
// RemoveLinkAnimations seam (bound at sequencer creation)
// strips pending links exactly where retail does.
// #45 (2026-05-06): scale sidestep speedMod to match ACE's
// wire formula. PlayerMovementController hands us a raw
@ -10270,8 +10291,7 @@ public sealed class GameWindow : IDisposable
* 0.5f;
}
ae.Sequencer.SetCycle(fullStyle, animCommand, animSpeed * animScale,
skipTransitionLink: skipLink);
ae.Sequencer.SetCycle(fullStyle, animCommand, animSpeed * animScale);
}
// Legacy path: update the manual slerp fields (for entities without sequencer)
@ -12965,6 +12985,13 @@ public sealed class GameWindow : IDisposable
&& playerAE.Sequencer is { } playerSeq)
{
_playerController.AttachCycleVelocityAccessor(() => playerSeq.CurrentVelocity);
// R3-W4: bind the player interp's retail seams to the player
// sequencer — LeaveGround/HitGround strip transition links here
// (the K-fix18 replacement); DefaultSink stays null until R3-W6
// (UpdatePlayerAnimation still drives the player's cycles).
_playerController.Motion.RemoveLinkAnimations = playerSeq.RemoveAllLinkAnimations;
_playerController.Motion.InitializeMotionTables =
() => playerSeq.Manager.InitializeState();
}
var q = playerEntity.Rotation;