feat(L.2g-S2b): wire remote entities onto the CMotionInterp funnel (DEV-1 integration)
OnLiveMotionUpdated's remote branch (368 lines of bulk-copy + cycle picker + per-axis DoInterpretedMotion + command-list router) collapses to: build InboundInterpretedState from the wire (retail UnPack defaults; MoveTo packets feed the PlanMoveToStart seed as the forward command) -> MotionInterpreter.MoveToInterpretedState(ims, RemoteMotionSink) -> sink.Commit(). RemoteMotionSink (new, App): receives the funnel's gate-passed dispatches in retail order; the axis-priority pick (fwd > side > turn), Run->Walk->Ready missing-cycle fallback, overlay routing, ObservedOmega seeding, and diag lines are MOVED VERBATIM from the pre-S2 block. Register row AP-73 documents the single-cycle composition approximation (retail blends modifiers via re_modify — DEV-9, retires with S3/S6). Retail-verbatim behavior changes: - Absent stance now defaults to NonCombat 0x8000003D (retail UnPack default, S0-trace-verified) instead of keep-current. - Remote command lists flow through the funnel's 15-bit action-stamp gate — retail's actual mechanism for ACE's re-bundled stale entries; the old skip-SubState router workaround is now local-player-only. - Airborne cycle preservation is the funnel's contact_allows_move gate (K-fix17's guard semantics, now from the retail mechanism). - Stops ride the same path: empty UM -> flat copy -> Ready dispatch -> get_state_velocity 0 (DEV-3 core; the 300ms stop-detection fallback stays until S6 verifies NPC unification). Full suite green (3290). Live smoke next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7b0cbbda2c
commit
67506ce988
3 changed files with 289 additions and 315 deletions
|
|
@ -408,7 +408,7 @@ public sealed class GameWindow : IDisposable
|
|||
/// remote gets the same treatment as the local player.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private sealed class RemoteMotion
|
||||
internal sealed class RemoteMotion // internal: RemoteMotionSink (L.2g S2b) consumes it
|
||||
{
|
||||
public AcDream.Core.Physics.PhysicsBody Body;
|
||||
public AcDream.Core.Physics.MotionInterpreter Motion;
|
||||
|
|
@ -4521,83 +4521,32 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
var forwardRoute = AcDream.Core.Physics.AnimationCommandRouter.Classify(fullMotion);
|
||||
bool forwardIsOverlay = forwardRoute is AcDream.Core.Physics.AnimationCommandRouteKind.Action
|
||||
or AcDream.Core.Physics.AnimationCommandRouteKind.Modifier
|
||||
or AcDream.Core.Physics.AnimationCommandRouteKind.ChatEmote;
|
||||
bool remoteIsAirborne = _remoteDeadReckon.TryGetValue(update.Guid, out var rmCheck)
|
||||
&& rmCheck.Airborne;
|
||||
|
||||
// Retail MotionTable::GetObjectSequence routes action-class
|
||||
// ForwardCommand values (creature attacks, chat-emotes) through
|
||||
// the Action branch, where the swing is appended before the
|
||||
// current cyclic tail and currState.Substate remains Ready.
|
||||
// Treating 0x10000051/52/53 as SetCycle commands made the
|
||||
// immediate follow-up Ready packet abort the swing.
|
||||
// Phase L.1c followup (2026-04-28): the next two state-update
|
||||
// blocks are LIFTED out of the substate-only `else` branch so
|
||||
// they run for BOTH overlay (Action/Modifier/ChatEmote) and
|
||||
// substate (Walk/Run/Ready/etc) packets. Two separate research
|
||||
// agents converged on the same root cause for the user-
|
||||
// observed "creature just runs instead of attacking" symptom:
|
||||
// ── L.2g S2b (2026-07-02): remote entities flow through the
|
||||
// verbatim CMotionInterp funnel. The wire state becomes an
|
||||
// InboundInterpretedState (retail UnPack defaults for absent
|
||||
// fields — a flags=0 UM is a wholesale stop, S0-verified),
|
||||
// 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).
|
||||
//
|
||||
// 1. Attack swings arrive as mt=0 with
|
||||
// ForwardCommand=AttackHigh1 (Action class). Retail's
|
||||
// CMotionInterp::move_to_interpreted_state
|
||||
// (acclient_2013_pseudo_c.txt:305936-305992) bulk-copies
|
||||
// forward_command from the wire into the body's
|
||||
// InterpretedState UNCONDITIONALLY. With
|
||||
// forward_command=AttackHigh1, get_state_velocity
|
||||
// returns 0 because its gate is RunForward||WalkForward
|
||||
// — body stops moving forward.
|
||||
//
|
||||
// 2. The acdream overlay branch was routing through
|
||||
// PlayAction (animation overlay) but skipping ALL of:
|
||||
// - ServerMoveToActive flag update
|
||||
// - MoveToPath capture
|
||||
// - InterpretedState.ForwardCommand assignment
|
||||
// So during a swing UM, the body's InterpretedState
|
||||
// stayed at RunForward from the prior MoveTo packet,
|
||||
// ServerMoveToActive stayed true, and the per-tick
|
||||
// remote driver kept steering + applying RunForward
|
||||
// velocity through every frame.
|
||||
//
|
||||
// Note: we bypass DoInterpretedMotion / ApplyMotionToInterpretedState
|
||||
// here because the latter is a heuristic that ONLY handles
|
||||
// WalkForward / RunForward / WalkBackward / SideStep / Turn
|
||||
// / Ready (MotionInterpreter.cs:941-970). For an Action
|
||||
// command (e.g. AttackHigh1 = 0x10000062) the switch falls
|
||||
// through and InterpretedState is silently NOT updated —
|
||||
// exactly the bug we are fixing. Direct field assignment
|
||||
// matches retail's <c>copy_movement_from</c> bulk-copy
|
||||
// (acclient_2013_pseudo_c.txt:293301-293311).
|
||||
// MoveTo packets (mt 6/7) reuse the PlanMoveToStart seed as
|
||||
// the funnel's forward command (fullMotion/speedMod computed
|
||||
// above) and additionally capture the path for the per-tick
|
||||
// remote driver.
|
||||
if (_remoteDeadReckon.TryGetValue(update.Guid, out var remoteMot))
|
||||
{
|
||||
remoteMot.ServerMoveToActive = update.MotionState.IsServerControlledMoveTo;
|
||||
|
||||
// Bulk-copy the wire's resolved ForwardCommand + speed
|
||||
// into InterpretedState UNCONDITIONALLY (overlay,
|
||||
// substate, AND MoveTo packets). Matches retail's
|
||||
// copy_movement_from semantics
|
||||
// (acclient_2013_pseudo_c.txt:293301-293311) which does
|
||||
// not filter by MovementType.
|
||||
//
|
||||
// For MoveTo packets, fullMotion is the RunForward seed
|
||||
// from PlanMoveToStart, so this populates
|
||||
// ForwardCommand=RunForward + ForwardSpeed=speed*runRate
|
||||
// — what the OLD substate-only DoInterpretedMotion call
|
||||
// (commit f794832 removed) used to set. Without it,
|
||||
// apply_current_movement reads the default
|
||||
// ForwardCommand=Ready and produces zero velocity, so
|
||||
// chasing creatures only translate via UpdatePosition
|
||||
// hard-snaps and at spawn appear posed at default
|
||||
// (visible as "torso on the ground" until the first UP
|
||||
// snap hits).
|
||||
//
|
||||
// For overlay (Action) packets this sets ForwardCommand
|
||||
// to the Attack/Twitch/etc command, and
|
||||
// get_state_velocity returns 0 because the gate is
|
||||
// RunForward||WalkForward — body stops moving forward.
|
||||
// [FWD_WIRE] + observed-velocity history invalidation on
|
||||
// a forward-command change (pre-S2 behavior, unchanged:
|
||||
// the per-tick scaling must not reuse a stale ratio
|
||||
// derived from the OLD motion).
|
||||
if (remoteMot.Motion.InterpretedState.ForwardCommand != fullMotion)
|
||||
{
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
|
|
@ -4608,35 +4557,16 @@ public sealed class GameWindow : IDisposable
|
|||
+ $"newCmd=0x{fullMotion:X8} "
|
||||
+ $"newLow=0x{fullMotion & 0xFFu:X2} speed={speedMod:F3}");
|
||||
}
|
||||
// Motion command changed — invalidate observed-velocity
|
||||
// history so the per-tick scaling in TickAnimations
|
||||
// doesn't reuse a stale ratio derived from the OLD
|
||||
// motion (e.g. carrying run-pace serverSpeed into the
|
||||
// first walk frame, which would briefly accelerate
|
||||
// walk to run pace before settling).
|
||||
remoteMot.PrevServerPosTime = 0.0;
|
||||
}
|
||||
remoteMot.Motion.InterpretedState.ForwardCommand = fullMotion;
|
||||
// Pass speedMod through verbatim — preserve sign so retail's
|
||||
// adjust_motion'd backward walk (cmd=WalkForward, spd<0)
|
||||
// produces backward velocity in get_state_velocity, NOT
|
||||
// forward. Pre-fix used `<=0 ? 1 : speedMod` which clamped
|
||||
// negative to 1.0 and made the dead-reckoned body translate
|
||||
// forward despite the reverse-playback animation — visually
|
||||
// "still walking forward" from the observer's POV.
|
||||
remoteMot.Motion.InterpretedState.ForwardSpeed = speedMod;
|
||||
|
||||
if (update.MotionState.IsServerControlledMoveTo
|
||||
&& update.MotionState.MoveToPath is { } path)
|
||||
{
|
||||
remoteMot.MoveToDestinationWorld = AcDream.Core.Physics.RemoteMoveToDriver
|
||||
.OriginToWorld(
|
||||
path.OriginCellId,
|
||||
path.OriginX,
|
||||
path.OriginY,
|
||||
path.OriginZ,
|
||||
_liveCenterX,
|
||||
_liveCenterY);
|
||||
path.OriginCellId, path.OriginX, path.OriginY, path.OriginZ,
|
||||
_liveCenterX, _liveCenterY);
|
||||
remoteMot.MoveToMinDistance = path.MinDistance;
|
||||
remoteMot.MoveToDistanceToObject = path.DistanceToObject;
|
||||
remoteMot.MoveToMoveTowards = update.MotionState.MoveTowards;
|
||||
|
|
@ -4646,247 +4576,77 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
else if (!update.MotionState.IsServerControlledMoveTo)
|
||||
{
|
||||
// Off MoveTo — clear stale destination so the per-tick
|
||||
// driver doesn't keep steering.
|
||||
// Off MoveTo — clear stale destination so the
|
||||
// per-tick driver doesn't keep steering.
|
||||
remoteMot.HasMoveToDestination = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (forwardIsOverlay)
|
||||
{
|
||||
if (!remoteIsAirborne)
|
||||
// Build the funnel input. fullMotion/speedMod already
|
||||
// encode retail's absent-field forward defaults (null/0
|
||||
// → Ready; MoveTo → PlanMoveToStart seed). Style:
|
||||
// retail's InterpretedMotionState::UnPack (0x0051f400)
|
||||
// defaults an absent stance to NonCombat 0x8000003D —
|
||||
// NOT keep-current (S0 trace: every empty UM applied
|
||||
// 0x8000003d on the retail observer).
|
||||
var ims = new AcDream.Core.Physics.InboundInterpretedState
|
||||
{
|
||||
AcDream.Core.Physics.AnimationCommandRouter.RouteFullCommand(
|
||||
ae.Sequencer,
|
||||
fullStyle,
|
||||
fullMotion,
|
||||
speedMod <= 0f ? 1f : speedMod);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pick which cycle to play on the sequencer. Priority:
|
||||
// 1. Forward cmd if active (RunForward / WalkForward) — legs run/walk.
|
||||
// 2. Else sidestep cmd if active — legs strafe.
|
||||
// 3. Else turn cmd if active — legs pivot.
|
||||
// 4. Else Ready — idle.
|
||||
//
|
||||
// For forward+sidestep or forward+turn, the forward cycle
|
||||
// wins at the anim layer; the sidestep/turn contribute via
|
||||
// MotionInterpreter velocity/omega writes.
|
||||
uint animCycle = fullMotion;
|
||||
float animSpeed = speedMod;
|
||||
uint fwdLow = fullMotion & 0xFFu;
|
||||
bool fwdIsRunWalk = fwdLow == 0x05 /* Walk */ || fwdLow == 0x06 /* WalkBack */
|
||||
|| fwdLow == 0x07 /* Run */;
|
||||
if (!fwdIsRunWalk)
|
||||
{
|
||||
// Forward is Ready (or absent). Prefer sidestep cycle if present,
|
||||
// else turn cycle, else Ready.
|
||||
if (update.MotionState.SideStepCommand is { } sideForAnim && sideForAnim != 0)
|
||||
{
|
||||
uint sideFullForAnim = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(sideForAnim);
|
||||
if (sideFullForAnim == 0) sideFullForAnim = 0x65000000u | sideForAnim;
|
||||
animCycle = sideFullForAnim;
|
||||
animSpeed = update.MotionState.SideStepSpeed ?? 1f;
|
||||
}
|
||||
else if (update.MotionState.TurnCommand is { } turnForAnim && turnForAnim != 0)
|
||||
{
|
||||
uint turnFullForAnim = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(turnForAnim);
|
||||
if (turnFullForAnim == 0) turnFullForAnim = 0x65000000u | turnForAnim;
|
||||
animCycle = turnFullForAnim;
|
||||
// SIGNED — do NOT MathF.Abs. ACE encodes TurnLeft on the
|
||||
// wire as (TurnCommand=TurnRight, TurnSpeed=NEGATIVE),
|
||||
// mirroring retail's adjust_motion convention. The
|
||||
// sequencer's negative-speed path (EnqueueMotionData
|
||||
// multiplies MotionData.Omega by speedMod, the
|
||||
// synthesize-omega fallback flips zomega via
|
||||
// -(pi/2)*adjustedSpeed) only produces the correct
|
||||
// CCW rotation when the sign is preserved here.
|
||||
// Confirmed by live wire trace 2026-05-03: TurnLeft
|
||||
// input arrives as turnCmd16=0x000D, speed=-1.500.
|
||||
animSpeed = update.MotionState.TurnSpeed ?? 1f;
|
||||
}
|
||||
}
|
||||
// K-fix17 (2026-04-26): preserve the Falling cycle while
|
||||
// the remote is airborne. ACE broadcasts UpdateMotion
|
||||
// mid-arc when the player turns / runs — the previous
|
||||
// SetCycle here swapped Falling → RunForward, breaking
|
||||
// the visible jump animation. The arc still played out
|
||||
// physics-wise (body went up/down), but the legs ran
|
||||
// instead of folded. Skip the cycle swap when airborne;
|
||||
// the InterpretedState updates below still fire so the
|
||||
// body's velocity matches the new motion command, AND
|
||||
// the post-resolve landing path restores the cycle to
|
||||
// whatever the interpreted state says when the body
|
||||
// lands.
|
||||
if (!remoteIsAirborne)
|
||||
{
|
||||
// Fallback chain for missing cycles in the MotionTable.
|
||||
// SetCycle unconditionally calls ClearCyclicTail() before
|
||||
// looking up the cycle; if the cycle is absent, the body
|
||||
// ends up with no cyclic tail at all and every part snaps
|
||||
// to its setup-default offset — visible as "torso on the
|
||||
// ground" because most creatures' setup-default puts all
|
||||
// limbs at the torso origin.
|
||||
//
|
||||
// This is specifically a regression from commit 186a584
|
||||
// (Phase L.1c port): pre-fix, MoveTo packets fell through
|
||||
// to fullMotion=Ready (which always exists in every
|
||||
// MotionTable). Post-fix, MoveTo packets seed
|
||||
// fullMotion=RunForward, but some creatures (especially
|
||||
// when stance=HandCombat) lack a (combat, RunForward)
|
||||
// cycle. Fall through RunForward → WalkForward → Ready
|
||||
// until we find one the table actually contains.
|
||||
//
|
||||
// Note: this fallback is for the SEQUENCER (visible
|
||||
// animation) only. InterpretedState.ForwardCommand still
|
||||
// gets the wire's (or seeded) ForwardCommand verbatim
|
||||
// so apply_current_movement produces correct velocity.
|
||||
uint cycleToPlay = animCycle;
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
|
||||
&& (animCycle & 0xFFu) is 0x05u or 0x07u)
|
||||
{
|
||||
bool hc = ae.Sequencer.HasCycle(fullStyle, cycleToPlay);
|
||||
System.Console.WriteLine(
|
||||
$"[HASCYCLE] guid={update.Guid:X8} style=0x{fullStyle:X8} "
|
||||
+ $"requestedCycle=0x{cycleToPlay:X8} HasCycle={hc}");
|
||||
}
|
||||
if (!ae.Sequencer.HasCycle(fullStyle, cycleToPlay))
|
||||
{
|
||||
uint requested = cycleToPlay;
|
||||
// RunForward (0x44000007) → WalkForward (0x45000005)
|
||||
if ((cycleToPlay & 0xFFu) == 0x07
|
||||
&& ae.Sequencer.HasCycle(fullStyle, 0x45000005u))
|
||||
{
|
||||
cycleToPlay = 0x45000005u;
|
||||
}
|
||||
// WalkForward → Ready (0x41000003)
|
||||
else if (ae.Sequencer.HasCycle(fullStyle, 0x41000003u))
|
||||
{
|
||||
cycleToPlay = 0x41000003u;
|
||||
}
|
||||
// Ready missing too — leave the existing cycle alone
|
||||
// by not calling SetCycle at all (avoids the
|
||||
// ClearCyclicTail wipe).
|
||||
else
|
||||
{
|
||||
cycleToPlay = 0;
|
||||
}
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"UM cycle missing for guid=0x{update.Guid:X8} " +
|
||||
$"style=0x{fullStyle:X8} requested=0x{requested:X8} " +
|
||||
$"→ fallback=0x{cycleToPlay:X8}");
|
||||
}
|
||||
}
|
||||
if (cycleToPlay != 0)
|
||||
{
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
|
||||
&& (ae.Sequencer.CurrentMotion != cycleToPlay
|
||||
|| MathF.Abs(ae.Sequencer.CurrentSpeedMod - animSpeed) > 1e-3f))
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SETCYCLE] guid={update.Guid:X8} "
|
||||
+ $"old=(motion=0x{ae.Sequencer.CurrentMotion:X8} speed={ae.Sequencer.CurrentSpeedMod:F3}) "
|
||||
+ $"new=(motion=0x{cycleToPlay:X8} speed={animSpeed:F3})");
|
||||
}
|
||||
ae.Sequencer.SetCycle(fullStyle, cycleToPlay, animSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// Retail runs the full MotionInterp state machine on every
|
||||
// remote. Route each wire command (forward, sidestep, turn)
|
||||
// through DoInterpretedMotion so apply_current_movement →
|
||||
// get_state_velocity → PhysicsBody.set_local_velocity fires
|
||||
// on a subsequent tick exactly as retail's FUN_00529210
|
||||
// (apply_current_movement) does.
|
||||
//
|
||||
// Decompile refs:
|
||||
// FUN_00529930 DoMotion
|
||||
// FUN_00528f70 DoInterpretedMotion
|
||||
// FUN_00528960 get_state_velocity
|
||||
// FUN_00529210 apply_current_movement
|
||||
// ServerMoveToActive flag, MoveToPath capture, and the
|
||||
// InterpretedState.ForwardCommand bulk-copy are already
|
||||
// handled by the LIFTED block above (so overlay-class swings
|
||||
// also clear stale MoveTo state and update the body's
|
||||
// forward command). This branch only handles sidestep /
|
||||
// turn axes plus the ObservedOmega seed — none of which
|
||||
// appear on overlay packets, so the existing logic is
|
||||
// correct unchanged. (`remoteMot` is the same dictionary
|
||||
// entry obtained at the top of the lifted block.)
|
||||
if (remoteMot is not null)
|
||||
{
|
||||
// Sidestep axis.
|
||||
CurrentStyle = stance != 0 ? (0x80000000u | (uint)stance) : 0x8000003Du,
|
||||
ForwardCommand = fullMotion,
|
||||
ForwardSpeed = speedMod,
|
||||
SideStepCommand = 0u,
|
||||
SideStepSpeed = update.MotionState.SideStepSpeed ?? 1f,
|
||||
TurnCommand = 0u,
|
||||
TurnSpeed = update.MotionState.TurnSpeed ?? 1f,
|
||||
};
|
||||
if (update.MotionState.SideStepCommand is { } sideCmd16 && sideCmd16 != 0)
|
||||
{
|
||||
uint sideFull = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(sideCmd16);
|
||||
if (sideFull == 0) sideFull = 0x65000000u | sideCmd16;
|
||||
float sideSpd = update.MotionState.SideStepSpeed ?? 1f;
|
||||
remoteMot.Motion.DoInterpretedMotion(
|
||||
sideFull, sideSpd, modifyInterpretedState: true);
|
||||
ims.SideStepCommand = sideFull != 0 ? sideFull : (0x65000000u | sideCmd16);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No sidestep — clear any leftover strafing motion.
|
||||
remoteMot.Motion.StopInterpretedMotion(
|
||||
AcDream.Core.Physics.MotionCommand.SideStepRight, modifyInterpretedState: true);
|
||||
remoteMot.Motion.StopInterpretedMotion(
|
||||
AcDream.Core.Physics.MotionCommand.SideStepLeft, modifyInterpretedState: true);
|
||||
}
|
||||
|
||||
// Turn axis — and use as the on/off switch for ObservedOmega.
|
||||
// On turn start: seed ObservedOmega from the formula
|
||||
// (π/2 × turnSpeed) so rotation begins THIS tick without
|
||||
// waiting for the next UP to observe a delta.
|
||||
// On turn end: zero ObservedOmega so rotation stops
|
||||
// immediately instead of coasting at the last observed
|
||||
// rate until the next UP shows zero delta.
|
||||
// UpdatePosition still REFINES the rate from actual
|
||||
// server deltas (more accurate than the formula), but
|
||||
// this ensures instant on/off response.
|
||||
if (update.MotionState.TurnCommand is { } turnCmd16 && turnCmd16 != 0)
|
||||
{
|
||||
uint turnFull = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(turnCmd16);
|
||||
if (turnFull == 0) turnFull = 0x65000000u | turnCmd16;
|
||||
float turnSpd = update.MotionState.TurnSpeed ?? 1f;
|
||||
ims.TurnCommand = turnFull != 0 ? turnFull : (0x65000000u | turnCmd16);
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[TURN_WIRE] guid={update.Guid:X8} turnCmd16=0x{turnCmd16:X4} "
|
||||
+ $"turnFull=0x{turnFull:X8} low=0x{turnFull & 0xFFu:X2} "
|
||||
+ $"({(((turnFull & 0xFFu) == 0x0D) ? "TurnRight" : ((turnFull & 0xFFu) == 0x0E) ? "TurnLeft" : "OTHER")}) "
|
||||
+ $"speed={turnSpd:F3}");
|
||||
+ $"turnFull=0x{ims.TurnCommand:X8} speed={ims.TurnSpeed:F3}");
|
||||
}
|
||||
remoteMot.Motion.DoInterpretedMotion(
|
||||
turnFull, turnSpd, modifyInterpretedState: true);
|
||||
// Seed ObservedOmega with formula so rotation starts
|
||||
// immediately; UP deltas will refine the rate.
|
||||
uint turnLow = turnFull & 0xFFu;
|
||||
if (turnLow == 0x0D /* TurnRight */)
|
||||
remoteMot.ObservedOmega = new System.Numerics.Vector3(0, 0, -(MathF.PI / 2f) * turnSpd);
|
||||
else if (turnLow == 0x0E /* TurnLeft */)
|
||||
remoteMot.ObservedOmega = new System.Numerics.Vector3(0, 0, (MathF.PI / 2f) * turnSpd);
|
||||
}
|
||||
else
|
||||
|
||||
// Command list → the funnel's action list. Retail applies
|
||||
// EVERY entry via DoInterpretedMotion under the 15-bit
|
||||
// server_action_stamp gate — the gate is what makes ACE's
|
||||
// re-bundled stale entries (e.g. the Ready bundled into a
|
||||
// RunForward UM, 2026-05-03 finding) inert, replacing the
|
||||
// old skip-SubState-class workaround.
|
||||
if (update.MotionState.Commands is { Count: > 0 } cmdList)
|
||||
{
|
||||
remoteMot.Motion.StopInterpretedMotion(
|
||||
AcDream.Core.Physics.MotionCommand.TurnRight, modifyInterpretedState: true);
|
||||
remoteMot.Motion.StopInterpretedMotion(
|
||||
AcDream.Core.Physics.MotionCommand.TurnLeft, modifyInterpretedState: true);
|
||||
// Zero ObservedOmega immediately — don't coast.
|
||||
remoteMot.ObservedOmega = System.Numerics.Vector3.Zero;
|
||||
var actionList = new System.Collections.Generic.List<AcDream.Core.Physics.InboundMotionAction>(cmdList.Count);
|
||||
foreach (var item in cmdList)
|
||||
{
|
||||
uint full = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(item.Command);
|
||||
if (full == 0) full = 0x10000000u | item.Command;
|
||||
actionList.Add(new AcDream.Core.Physics.InboundMotionAction(
|
||||
full,
|
||||
Stamp: item.PackedSequence & 0x7FFF,
|
||||
Autonomous: (item.PackedSequence & 0x8000) != 0,
|
||||
Speed: item.Speed));
|
||||
}
|
||||
ims.Actions = actionList;
|
||||
}
|
||||
|
||||
RemoteMotionSink? sink = ae.Sequencer is not null
|
||||
? new RemoteMotionSink(ae.Sequencer, remoteMot, update.Guid)
|
||||
: null;
|
||||
remoteMot.Motion.MoveToInterpretedState(ims, sink);
|
||||
sink?.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: when we enter a locomotion cycle (Walk/Run/etc),
|
||||
// stamp the _remoteLastMove timestamp to "now". Without this,
|
||||
|
|
@ -4938,7 +4698,11 @@ public sealed class GameWindow : IDisposable
|
|||
// a second SetCycle call wiped the first within the same UM
|
||||
// packet processing. Only Actions/Modifiers/ChatEmotes (overlays
|
||||
// that interleave with the cycle) belong in the list iteration.
|
||||
if (update.MotionState.Commands is { Count: > 0 } cmds)
|
||||
if (update.Guid == _playerServerGuid // L.2g S2b: LOCAL ONLY — remote command
|
||||
// lists flow through the funnel's
|
||||
// action-stamp gate (retail's actual
|
||||
// mechanism for bundled stale entries)
|
||||
&& update.MotionState.Commands is { Count: > 0 } cmds)
|
||||
{
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
|
|
@ -4964,7 +4728,7 @@ public sealed class GameWindow : IDisposable
|
|||
if (itemRoute == AcDream.Core.Physics.AnimationCommandRouteKind.SubState)
|
||||
continue;
|
||||
AcDream.Core.Physics.AnimationCommandRouter.RouteWireCommand(
|
||||
ae.Sequencer,
|
||||
ae.Sequencer!, // guarded by the enclosing `if (ae.Sequencer is not null)`
|
||||
fullStyle,
|
||||
item.Command,
|
||||
item.Speed);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue