feat(R2-Q5): RemoteMotionSink DELETED — funnel dispatches straight into PerformMovement; AP-73 retired
The funnel's retail-ordered dispatches now go directly into the entity's motion-table stack via Motion/MotionTableDispatchSink (Core): ApplyMotion → PerformMovement(InterpretedCommand), StopMotion → PerformMovement(StopInterpretedCommand). No axis collection, no single-cycle priority pick, no Commit pass, no HasCycle probe, no Run→Walk→Ready fallback chain — GetObjectSequence 0x00522860 + is_allowed decide (closes H4, H5-callers, H11-callers, H15, H17-carry). Run-while-turning now blends for real: the turn is a Branch-4 modifier combined over the untouched run substate (AP-73 DELETED from the register). AnimationSequencer gains the public PerformMovement passthrough (lazy initialize_state + locomotion velocity synthesis on success — AP-75; remote body translation via PositionManager.ComputeOffset depends on CurrentVelocity) and InitializeState(); HasCycle DELETED (the miss hazard is structurally gone — GetObjectSequence checks the cycle before any surgery; new conformance test pins sequence+state untouched on a miss). GameWindow: sink swap with the TurnApplied/TurnStopped ObservedOmega callbacks (H17 carried verbatim — register AP-76, retire R6); spawn + door sites run retail's enter-world order (initialize_state installs the table default, the wire's initial motion dispatches unguarded — the L.1c fallback chains deleted); despawn drains the pending queue via HandleExitWorld (MotionDone success:false per entry, 0x0051bda0). 5 new MotionTableDispatchSink conformance tests (lazy init, Branch-4 turn blend + callback, Case-B stop unwind, Case-A stop re-drive, miss no-op). Full suite green: 3,462 passed. Live smoke vs ACE: in-world, NPC emote/Ready UMs dispatching through the new path, [MOTIONDONE] completions firing across entities — first live proof of the Q3→Q4→Q5 chain. Zero exceptions. Registers: AP-73 deleted; AP-76 added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c072b73686
commit
d82f07d4e5
8 changed files with 399 additions and 395 deletions
|
|
@ -408,7 +408,7 @@ public sealed class GameWindow : IDisposable
|
|||
/// remote gets the same treatment as the local player.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal sealed class RemoteMotion // internal: RemoteMotionSink (L.2g S2b) consumes it
|
||||
internal sealed class RemoteMotion // internal: the R2-Q5 sink callbacks (ObservedOmega seam) capture it
|
||||
{
|
||||
public AcDream.Core.Physics.PhysicsBody Body;
|
||||
public AcDream.Core.Physics.MotionInterpreter Motion;
|
||||
|
|
@ -3708,47 +3708,17 @@ public sealed class GameWindow : IDisposable
|
|||
seqMotion = AcDream.Core.Physics.MotionCommand.Ready;
|
||||
}
|
||||
|
||||
// Phase L.1c followup (2026-04-28): apply the same
|
||||
// missing-cycle fallback the OnLiveMotionUpdated path
|
||||
// uses. Without this, a monster spawned in combat
|
||||
// stance with the wire's seqMotion absent from its
|
||||
// MotionTable hits ClearCyclicTail() with no
|
||||
// replacement enqueue, every body part snaps to its
|
||||
// setup-default offset, and the visual collapses to
|
||||
// "torso on the ground" — visible to acdream
|
||||
// observers when another client is in combat with a
|
||||
// monster, until the first OnLiveMotionUpdated UM
|
||||
// applies the same fallback there.
|
||||
uint spawnCycle = seqMotion;
|
||||
if (!sequencer.HasCycle(seqStyle, spawnCycle))
|
||||
{
|
||||
uint origCycle = spawnCycle;
|
||||
// RunForward → WalkForward → Ready
|
||||
if ((spawnCycle & 0xFFu) == 0x07
|
||||
&& sequencer.HasCycle(seqStyle, 0x45000005u))
|
||||
{
|
||||
spawnCycle = 0x45000005u;
|
||||
}
|
||||
else if (sequencer.HasCycle(seqStyle, 0x41000003u))
|
||||
{
|
||||
spawnCycle = 0x41000003u;
|
||||
}
|
||||
else
|
||||
{
|
||||
spawnCycle = 0;
|
||||
}
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"spawn cycle missing for guid=0x{spawn.Guid:X8} mtable=0x{mtableId:X8} " +
|
||||
$"style=0x{seqStyle:X8} requested=0x{origCycle:X8} " +
|
||||
$"→ fallback=0x{spawnCycle:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnCycle != 0)
|
||||
sequencer.SetCycle(seqStyle, spawnCycle);
|
||||
// R2-Q5: retail's enter-world sequence — initialize_
|
||||
// state installs the table default (so the entity is
|
||||
// NEVER without a cycle: the L.1c "torso on the
|
||||
// ground" hazard is structurally gone), then the
|
||||
// wire's initial motion dispatches through the
|
||||
// verbatim GetObjectSequence (a missing cycle leaves
|
||||
// the default playing — retail's own miss behavior;
|
||||
// the Run→Walk→Ready fallback chain is deleted with
|
||||
// RemoteMotionSink).
|
||||
sequencer.InitializeState();
|
||||
sequencer.SetCycle(seqStyle, seqMotion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3821,8 +3791,11 @@ public sealed class GameWindow : IDisposable
|
|||
: NonCombatStyle;
|
||||
uint spawnState = spawn.PhysicsState ?? 0u;
|
||||
uint initialCycle = (spawnState & EtherealPs) != 0 ? MotionOn : MotionOff;
|
||||
if (sequencer.HasCycle(initialStyle, initialCycle))
|
||||
sequencer.SetCycle(initialStyle, initialCycle);
|
||||
// R2-Q5: initialize_state guarantees a playing default;
|
||||
// the On/Off dispatch no-ops harmlessly if the door's
|
||||
// table lacks the cycle (HasCycle guard deleted).
|
||||
sequencer.InitializeState();
|
||||
sequencer.SetCycle(initialStyle, initialCycle);
|
||||
|
||||
var template = new (uint, IReadOnlyDictionary<uint, uint>?)[meshRefs.Count];
|
||||
for (int i = 0; i < meshRefs.Count; i++)
|
||||
|
|
@ -4210,6 +4183,12 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
_worldState.RemoveEntityByServerGuid(serverGuid);
|
||||
_worldGameState.RemoveById(existingEntity.Id);
|
||||
// R2-Q5: retail's exit-world drain — every pending queue entry fires
|
||||
// MotionDone(success:false) before the object goes away
|
||||
// (MotionTableManager::HandleExitWorld 0x0051bda0; R3 adds the
|
||||
// CMotionInterp-side drain alongside per r3-port-plan §4).
|
||||
if (_animatedEntities.TryGetValue(existingEntity.Id, out var aeGone))
|
||||
aeGone.Sequencer?.Manager.HandleExitWorld();
|
||||
_animatedEntities.Remove(existingEntity.Id);
|
||||
_classificationCache.InvalidateEntity(existingEntity.Id);
|
||||
_physicsEngine.ShadowObjects.Deregister(existingEntity.Id);
|
||||
|
|
@ -4528,12 +4507,13 @@ public sealed class GameWindow : IDisposable
|
|||
// MotionInterpreter.MoveToInterpretedState applies it with
|
||||
// retail's exact dispatch order + the 15-bit action-stamp
|
||||
// gate (conformance: RetailObserverTraceConformanceTests,
|
||||
// 183/183 vs the live retail-observer cdb trace), and
|
||||
// RemoteMotionSink maps the gate-passed dispatches onto the
|
||||
// sequencer (axis-priority pick + missing-cycle fallback
|
||||
// moved verbatim from the pre-S2 block; airborne handling is
|
||||
// the funnel's contact_allows_move gate — the retail
|
||||
// mechanism behind the old K-fix17 guard).
|
||||
// 183/183 vs the live retail-observer cdb trace), and the
|
||||
// gate-passed dispatches go straight into the motion-table
|
||||
// stack via MotionTableDispatchSink -> PerformMovement
|
||||
// (R2-Q5; GetObjectSequence + is_allowed decide — no sink-
|
||||
// side pick; airborne handling is the funnel's
|
||||
// contact_allows_move gate, the retail mechanism behind the
|
||||
// old K-fix17 guard).
|
||||
//
|
||||
// MoveTo packets (mt 6/7) reuse the PlanMoveToStart seed as
|
||||
// the funnel's forward command (fullMotion/speedMod computed
|
||||
|
|
@ -4640,11 +4620,32 @@ public sealed class GameWindow : IDisposable
|
|||
ims.Actions = actionList;
|
||||
}
|
||||
|
||||
RemoteMotionSink? sink = ae.Sequencer is not null
|
||||
? new RemoteMotionSink(ae.Sequencer, remoteMot, update.Guid)
|
||||
: null;
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
remoteMot.Motion.MoveToInterpretedState(ims, sink);
|
||||
sink?.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue