feat(R3-W6): LOCAL PLAYER UNIFICATION — edge-driven retail input; UpdatePlayerAnimation + the synthesis layer DELETED (closes J15)
The local player now drives the SAME pipeline remotes use. The controller's per-frame RawMotionState rebuild (level-triggered D6.2) is replaced by retail's CommandInterpreter altitude: key EDGES fire DoMotion/StopMotion (0x00528d20/0x00528530) and the Shift edge fires set_hold_run (0x00528b70, caller-0x006b33ca shape) — the interpreter's own RawState mutates via ApplyMotion/RemoveMotion and every dispatch routes the funnel + DefaultSink into the sequencer. GameWindow's UpdatePlayerAnimation (169 lines) is DELETED with both LocalAnimationCommand/LocalAnimationSpeed MovementResult fields and all three synthesis sites (K-fix5 pacing, Walk→Run cycle selection, the auto-walk overrides): run pacing now comes from apply_run_to_command's my_run_rate promotion — the same source remotes use; airborne-Falling falls out of contact_allows_move (the funnel's second dispatch). Auto-walk's #69 turn-phase legs re-expressed as DoMotion(Turn*) edges; teleport idle = StopCompletely (retail's full stop) + an edge-tracker reset so held keys walk straight out of a portal. Map discovery R1 fixed: ChargeJump() now fires at charge START (the 0x0056afac input-boundary site) — StandingLongJump had NEVER armed in production despite the W3 port. TWO root-cause fixes the cutover surfaced (each would have been invisible under the old synthesis layer): - CurrentHoldKey was a shadow FIELD synced only on the raw dispatch branch; set_hold_run writes RawState.CurrentHoldKey, so an edge-driven Shift toggle followed by DoMotion read a stale None and lost the run promotion. Now an alias of RawState.CurrentHoldKey (retail's adjust_motion reads raw_state.current_holdkey directly). - The controller's grounded velocity + jump-launch writes used set_local_velocity's default autonomous:false, silently clearing LastMoveWasAutonomous and flipping the A3 dual dispatch to the interpreted branch for the local player. Both marked autonomous. EXPECTED-DIFFS (map §6, for the R3 visual pass): #45's 1.248x sidestep factor + ACDREAM_ANIM_SPEED_SCALE died with UpdatePlayerAnimation — local sidestep pacing now matches how remotes have always played; auto-walk-at-run plays walk-pace legs until R4; ApplyServerRunRate's apply_current_movement goes live (Branch-2 fast re-speed absorbs the echo — retail's own mid-run speed-change mechanism). Wire: outbound values stay input-derived (the L.2b-verified byte stream untouched; RawState-sourcing needs the map §2b cdb TODO and R7 owns outbound). MotionStateChanged = the old comparison minus the dead localAnimCmd leg, OR any edge fired. Full suite green: 3,731 passed. Live smoke: in-world, user-driven input through the new path (walk/turn/strafe), 12k entities, zero exceptions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fc5a2cda28
commit
fb7beb706b
4 changed files with 177 additions and 287 deletions
|
|
@ -52,14 +52,12 @@ public readonly record struct MovementResult(
|
||||||
float? SidestepSpeed,
|
float? SidestepSpeed,
|
||||||
float? TurnSpeed,
|
float? TurnSpeed,
|
||||||
bool IsRunning = false,
|
bool IsRunning = false,
|
||||||
uint? LocalAnimationCommand = null, // which cycle to play on the local player (RunForward when running)
|
|
||||||
// K-fix5 (2026-04-26): cycle-pace multiplier for the LOCAL animation
|
// K-fix5 (2026-04-26): cycle-pace multiplier for the LOCAL animation
|
||||||
// sequencer. Decoupled from ForwardSpeed so the wire can keep sending
|
// sequencer. Decoupled from ForwardSpeed so the wire can keep sending
|
||||||
// 1.0 for WalkBackward (ACE-compatible) while the animation plays at
|
// 1.0 for WalkBackward (ACE-compatible) while the animation plays at
|
||||||
// runRate × so the cycle visually matches the run-speed velocity.
|
// runRate × so the cycle visually matches the run-speed velocity.
|
||||||
// Forward+Run = runRate (same as ForwardSpeed); Backward+Run, Strafe+Run
|
// Forward+Run = runRate (same as ForwardSpeed); Backward+Run, Strafe+Run
|
||||||
// = runRate (where ForwardSpeed is 1.0 / null); everything else = 1.0.
|
// = runRate (where ForwardSpeed is 1.0 / null); everything else = 1.0.
|
||||||
float LocalAnimationSpeed = 1f,
|
|
||||||
bool JustLanded = false, // true on the single frame we transitioned airborne → grounded
|
bool JustLanded = false, // true on the single frame we transitioned airborne → grounded
|
||||||
float? JumpExtent = null, // non-null when a jump was triggered this frame
|
float? JumpExtent = null, // non-null when a jump was triggered this frame
|
||||||
Vector3? JumpVelocity = null); // BODY-LOCAL launch velocity (forward/right/up relative to facing) — see PlayerMovementController jump path for the inverse-yaw conversion. Server rotates body→world on broadcast.
|
Vector3? JumpVelocity = null); // BODY-LOCAL launch velocity (forward/right/up relative to facing) — see PlayerMovementController jump path for the inverse-yaw conversion. Server rotates body→world on broadcast.
|
||||||
|
|
@ -182,13 +180,25 @@ public sealed class PlayerMovementController
|
||||||
// to drive the landing animation cycle.
|
// to drive the landing animation cycle.
|
||||||
private bool _wasAirborneLastFrame;
|
private bool _wasAirborneLastFrame;
|
||||||
|
|
||||||
// Previous frame's motion commands for change detection.
|
// Previous frame's motion commands for change detection (wire cadence).
|
||||||
private uint? _prevForwardCmd;
|
private uint? _prevForwardCmd;
|
||||||
private uint? _prevSidestepCmd;
|
private uint? _prevSidestepCmd;
|
||||||
private uint? _prevTurnCmd;
|
private uint? _prevTurnCmd;
|
||||||
private float? _prevForwardSpeed;
|
private float? _prevForwardSpeed;
|
||||||
private bool _prevRunHold;
|
private bool _prevRunHold;
|
||||||
private uint? _prevLocalAnimCmd;
|
|
||||||
|
// R3-W6: previous frame's HELD-KEY state — the edge detector feeding
|
||||||
|
// retail's DoMotion/StopMotion/set_hold_run calls (retail's
|
||||||
|
// CommandInterpreter altitude: motion dispatch happens on key EDGES,
|
||||||
|
// never level-triggered per-frame).
|
||||||
|
private bool _prevForwardHeld;
|
||||||
|
private bool _prevBackwardHeld;
|
||||||
|
private bool _prevStrafeLeftHeld;
|
||||||
|
private bool _prevStrafeRightHeld;
|
||||||
|
private bool _prevTurnLeftHeld;
|
||||||
|
private bool _prevTurnRightHeld;
|
||||||
|
private bool _prevRunHeld;
|
||||||
|
private int _prevAutoWalkTurnDir;
|
||||||
|
|
||||||
// Heartbeat timer.
|
// Heartbeat timer.
|
||||||
// Cadence is 1.0 sec to match holtburger's
|
// Cadence is 1.0 sec to match holtburger's
|
||||||
|
|
@ -302,7 +312,7 @@ public sealed class PlayerMovementController
|
||||||
// turn-first phase (rotating in place toward target) and after
|
// turn-first phase (rotating in place toward target) and after
|
||||||
// arrival. Drives the animation cycle override: walking animation
|
// arrival. Drives the animation cycle override: walking animation
|
||||||
// only plays when the body is actually moving forward.
|
// only plays when the body is actually moving forward.
|
||||||
private bool _autoWalkMovingForwardThisFrame;
|
// R3-W6: _autoWalkMovingForwardThisFrame deleted with the LocalAnimationCommand override (forward legs come from DriveServerAutoWalk's own DoMotion).
|
||||||
|
|
||||||
// 2026-05-16 (issue #69 fix) — turn direction this frame.
|
// 2026-05-16 (issue #69 fix) — turn direction this frame.
|
||||||
// +1 = rotating counter-clockwise (Yaw increasing) → TurnLeft cycle
|
// +1 = rotating counter-clockwise (Yaw increasing) → TurnLeft cycle
|
||||||
|
|
@ -556,7 +566,6 @@ public sealed class PlayerMovementController
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool DriveServerAutoWalk(float dt, MovementInput input)
|
private bool DriveServerAutoWalk(float dt, MovementInput input)
|
||||||
{
|
{
|
||||||
_autoWalkMovingForwardThisFrame = false;
|
|
||||||
_autoWalkTurnDirectionThisFrame = 0;
|
_autoWalkTurnDirectionThisFrame = 0;
|
||||||
if (!_autoWalkActive) return false;
|
if (!_autoWalkActive) return false;
|
||||||
|
|
||||||
|
|
@ -729,6 +738,8 @@ public sealed class PlayerMovementController
|
||||||
// The motion-interpreter state also has to step out of
|
// The motion-interpreter state also has to step out of
|
||||||
// WalkForward so get_state_velocity (used downstream) reports
|
// WalkForward so get_state_velocity (used downstream) reports
|
||||||
// standing-velocity, not the prior frame's run-speed.
|
// standing-velocity, not the prior frame's run-speed.
|
||||||
|
// R3-W6 note: deliberately NOT StopCompletely — auto-walk is
|
||||||
|
// KEEP-LIST until R4 (w6-cutover-map.md R4 risk note).
|
||||||
_motion.DoMotion(MotionCommand.Ready, 1.0f);
|
_motion.DoMotion(MotionCommand.Ready, 1.0f);
|
||||||
if (_body.OnWalkable)
|
if (_body.OnWalkable)
|
||||||
{
|
{
|
||||||
|
|
@ -759,7 +770,6 @@ public sealed class PlayerMovementController
|
||||||
forwardCmdSpeed = 1.0f;
|
forwardCmdSpeed = 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
_autoWalkMovingForwardThisFrame = true;
|
|
||||||
|
|
||||||
// Update interpreted motion state — drives the animation cycle
|
// Update interpreted motion state — drives the animation cycle
|
||||||
// via UpdatePlayerAnimation downstream + the MotionInterpreter's
|
// via UpdatePlayerAnimation downstream + the MotionInterpreter's
|
||||||
|
|
@ -836,7 +846,22 @@ public sealed class PlayerMovementController
|
||||||
// reconstruct that run vector via get_state_velocity and the player would sprint
|
// reconstruct that run vector via get_state_velocity and the player would sprint
|
||||||
// off in the old direction the instant input resumes. Resetting the forward
|
// off in the old direction the instant input resumes. Resetting the forward
|
||||||
// command to Ready makes the player arrive at rest.
|
// command to Ready makes the player arrive at rest.
|
||||||
_motion.DoMotion(MotionCommand.Ready, 1.0f);
|
// R3-W6: retail's teleport idle is a FULL stop (StopCompletely
|
||||||
|
// 0x00527e40: resets fwd/sidestep/turn COMMANDS, zeroes velocity,
|
||||||
|
// enqueues the A9 jump-snapshot node) — not a bare DoMotion(Ready).
|
||||||
|
_motion.StopCompletely();
|
||||||
|
// Reset the edge tracker: the stop wiped the motion state, so keys
|
||||||
|
// still physically held must re-fire as press edges on the next
|
||||||
|
// Update (matches the pre-W6 level-triggered behavior of walking
|
||||||
|
// straight out of a teleport while W stays held).
|
||||||
|
_prevForwardHeld = false;
|
||||||
|
_prevBackwardHeld = false;
|
||||||
|
_prevStrafeLeftHeld = false;
|
||||||
|
_prevStrafeRightHeld = false;
|
||||||
|
_prevTurnLeftHeld = false;
|
||||||
|
_prevTurnRightHeld = false;
|
||||||
|
_prevRunHeld = false;
|
||||||
|
_prevAutoWalkTurnDir = 0;
|
||||||
|
|
||||||
// Reset physics clock so any subsequent update_object calls start fresh.
|
// Reset physics clock so any subsequent update_object calls start fresh.
|
||||||
_body.LastUpdateTime = 0.0;
|
_body.LastUpdateTime = 0.0;
|
||||||
|
|
@ -889,40 +914,80 @@ public sealed class PlayerMovementController
|
||||||
TurnSpeed: null);
|
TurnSpeed: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── D6.2: normalize input into the interpreted motion state ───────────
|
// ── R3-W6: EDGE-DRIVEN retail input (replaces the D6.2 per-frame
|
||||||
// ONE RawMotionState from MovementInput drives the local velocity + turn
|
// RawMotionState rebuild — the level-triggered substitute for
|
||||||
// (retail CMotionInterp::apply_raw_movement 0x005287e0). RAW speeds are
|
// retail's edge-triggered CommandInterpreter). Each key EDGE fires
|
||||||
// 1.0 — apply_run_to_command applies the run rate, so passing a pre-scaled
|
// DoMotion/StopMotion (0x00528d20/0x00528530) which mutate the
|
||||||
// speed would double-scale. adjust_motion normalizes backward →
|
// interpreter's OWN RawState via ApplyMotion/RemoveMotion and
|
||||||
// WalkForward (×-0.65) and strafe-left → SideStepRight (×-1×1.248), so
|
// dispatch through the funnel + DefaultSink — the SAME pipeline
|
||||||
// get_state_velocity below is correct for ALL directions (retires the
|
// remotes use. The Shift edge is retail's set_hold_run
|
||||||
// hand-mirrored formulas; register TS-22). Skipped during server
|
// (0x00528b70, caller 0x006b33ca shape: interrupt=true). RAW speeds
|
||||||
// auto-walk, which drives the body itself.
|
// stay 1.0 (apply_run_to_command applies the run rate — pre-scaling
|
||||||
|
// would double-scale; TS-22 unchanged). Skipped during server
|
||||||
|
// auto-walk, which drives the body itself (edge state still
|
||||||
|
// tracked so release edges fire correctly when auto-walk ends).
|
||||||
|
bool motionEdgeFired = false;
|
||||||
if (!autoWalkConsumedMotion)
|
if (!autoWalkConsumedMotion)
|
||||||
{
|
{
|
||||||
var axisHold = input.Run ? HoldKey.Run : HoldKey.None;
|
var p = new AcDream.Core.Physics.Motion.MovementParameters();
|
||||||
var raw = new RawMotionState
|
|
||||||
|
// Shift/run edge FIRST — retail's set_hold_run re-applies
|
||||||
|
// movement, so the hold-key state is current before any
|
||||||
|
// same-frame directional dispatch.
|
||||||
|
if (input.Run != _prevRunHeld)
|
||||||
{
|
{
|
||||||
CurrentHoldKey = axisHold,
|
_motion.set_hold_run(input.Run, interrupt: true);
|
||||||
ForwardCommand = input.Forward ? MotionCommand.WalkForward
|
motionEdgeFired = true;
|
||||||
: input.Backward ? MotionCommand.WalkBackward
|
}
|
||||||
: MotionCommand.Ready,
|
|
||||||
ForwardHoldKey = (input.Forward || input.Backward) ? axisHold : HoldKey.Invalid,
|
// Forward channel (W / S share one raw channel — retail
|
||||||
ForwardSpeed = 1.0f,
|
// RawMotionState.ApplyMotion's forward-class switch). W wins on
|
||||||
SidestepCommand = input.StrafeRight ? MotionCommand.SideStepRight
|
// a same-frame double-press; releasing one key while the
|
||||||
: input.StrafeLeft ? MotionCommand.SideStepLeft
|
// opposite is still held re-issues the survivor (equivalent to
|
||||||
: 0u,
|
// the old level-triggered build, which always reflected the
|
||||||
SidestepHoldKey = (input.StrafeRight || input.StrafeLeft) ? axisHold : HoldKey.Invalid,
|
// currently-held set).
|
||||||
SidestepSpeed = 1.0f,
|
if (input.Forward && !_prevForwardHeld)
|
||||||
TurnCommand = input.TurnRight ? MotionCommand.TurnRight
|
{ _motion.DoMotion(MotionCommand.WalkForward, p); motionEdgeFired = true; }
|
||||||
: input.TurnLeft ? MotionCommand.TurnLeft
|
else if (input.Backward && !_prevBackwardHeld && !input.Forward)
|
||||||
: 0u,
|
{ _motion.DoMotion(MotionCommand.WalkBackward, p); motionEdgeFired = true; }
|
||||||
TurnHoldKey = (input.TurnRight || input.TurnLeft) ? axisHold : HoldKey.Invalid,
|
if (!input.Forward && _prevForwardHeld)
|
||||||
TurnSpeed = 1.0f,
|
{
|
||||||
};
|
if (input.Backward)
|
||||||
_motion.apply_raw_movement(raw);
|
_motion.DoMotion(MotionCommand.WalkBackward, p);
|
||||||
|
else
|
||||||
|
_motion.StopMotion(MotionCommand.WalkForward, p);
|
||||||
|
motionEdgeFired = true;
|
||||||
|
}
|
||||||
|
else if (!input.Backward && _prevBackwardHeld && !input.Forward)
|
||||||
|
{ _motion.StopMotion(MotionCommand.WalkBackward, p); motionEdgeFired = true; }
|
||||||
|
|
||||||
|
// Sidestep channel.
|
||||||
|
if (input.StrafeRight && !_prevStrafeRightHeld)
|
||||||
|
{ _motion.DoMotion(MotionCommand.SideStepRight, p); motionEdgeFired = true; }
|
||||||
|
else if (input.StrafeLeft && !_prevStrafeLeftHeld)
|
||||||
|
{ _motion.DoMotion(MotionCommand.SideStepLeft, p); motionEdgeFired = true; }
|
||||||
|
else if (!input.StrafeRight && !input.StrafeLeft
|
||||||
|
&& (_prevStrafeRightHeld || _prevStrafeLeftHeld))
|
||||||
|
{ _motion.StopMotion(MotionCommand.SideStepRight, p); motionEdgeFired = true; }
|
||||||
|
|
||||||
|
// Turn channel.
|
||||||
|
if (input.TurnRight && !_prevTurnRightHeld)
|
||||||
|
{ _motion.DoMotion(MotionCommand.TurnRight, p); motionEdgeFired = true; }
|
||||||
|
else if (input.TurnLeft && !_prevTurnLeftHeld)
|
||||||
|
{ _motion.DoMotion(MotionCommand.TurnLeft, p); motionEdgeFired = true; }
|
||||||
|
else if (!input.TurnRight && !input.TurnLeft
|
||||||
|
&& (_prevTurnRightHeld || _prevTurnLeftHeld))
|
||||||
|
{ _motion.StopMotion(MotionCommand.TurnRight, p); motionEdgeFired = true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_prevForwardHeld = input.Forward;
|
||||||
|
_prevBackwardHeld = input.Backward;
|
||||||
|
_prevStrafeLeftHeld = input.StrafeLeft;
|
||||||
|
_prevStrafeRightHeld = input.StrafeRight;
|
||||||
|
_prevTurnLeftHeld = input.TurnLeft;
|
||||||
|
_prevTurnRightHeld = input.TurnRight;
|
||||||
|
_prevRunHeld = input.Run;
|
||||||
|
|
||||||
// ── 1. Apply turning from keyboard + mouse ────────────────────────────
|
// ── 1. Apply turning from keyboard + mouse ────────────────────────────
|
||||||
// 2026-05-16 — retail-faithful turn rate.
|
// 2026-05-16 — retail-faithful turn rate.
|
||||||
// Anchor: docs/research/named-retail/acclient_2013_pseudo_c.txt
|
// Anchor: docs/research/named-retail/acclient_2013_pseudo_c.txt
|
||||||
|
|
@ -986,7 +1051,10 @@ public sealed class PlayerMovementController
|
||||||
// that adjust_motion ran above. The hand-mirrored formulas are gone
|
// that adjust_motion ran above. The hand-mirrored formulas are gone
|
||||||
// (register TS-22 retired).
|
// (register TS-22 retired).
|
||||||
var stateVel = _motion.get_state_velocity();
|
var stateVel = _motion.get_state_velocity();
|
||||||
_body.set_local_velocity(new Vector3(stateVel.X, stateVel.Y, savedWorldVz));
|
// R3-W6: autonomous — this is local input-driven motion; the
|
||||||
|
// default (false) silently cleared LastMoveWasAutonomous and
|
||||||
|
// flipped the A3 dual dispatch to the interpreted branch.
|
||||||
|
_body.set_local_velocity(new Vector3(stateVel.X, stateVel.Y, savedWorldVz), autonomous: true);
|
||||||
}
|
}
|
||||||
} // end of `if (!autoWalkConsumedMotion)` — section 2
|
} // end of `if (!autoWalkConsumedMotion)` — section 2
|
||||||
|
|
||||||
|
|
@ -1004,6 +1072,12 @@ public sealed class PlayerMovementController
|
||||||
{
|
{
|
||||||
_jumpCharging = true;
|
_jumpCharging = true;
|
||||||
_jumpExtent = 0f;
|
_jumpExtent = 0f;
|
||||||
|
// R3-W6 (map R1): retail's charge_jump fires at charge
|
||||||
|
// START (SmartBox/input boundary 0x0056afac) — the ONLY
|
||||||
|
// place StandingLongJump arms (grounded + Ready + no
|
||||||
|
// sidestep/turn). Never called by production code before
|
||||||
|
// this line despite the W3 port.
|
||||||
|
_motion.ChargeJump();
|
||||||
}
|
}
|
||||||
_jumpExtent = MathF.Min(_jumpExtent + dt * JumpChargeRate, 1.0f);
|
_jumpExtent = MathF.Min(_jumpExtent + dt * JumpChargeRate, 1.0f);
|
||||||
}
|
}
|
||||||
|
|
@ -1036,7 +1110,7 @@ public sealed class PlayerMovementController
|
||||||
// client renders the jump in the same world direction the
|
// client renders the jump in the same world direction the
|
||||||
// server is broadcasting to observers. Same vector we just
|
// server is broadcasting to observers. Same vector we just
|
||||||
// sent in JumpAction — local + remote stay in sync.
|
// sent in JumpAction — local + remote stay in sync.
|
||||||
_body.set_local_velocity(outJumpVelocity.Value);
|
_body.set_local_velocity(outJumpVelocity.Value, autonomous: true);
|
||||||
}
|
}
|
||||||
_jumpCharging = false;
|
_jumpCharging = false;
|
||||||
_jumpExtent = 0f;
|
_jumpExtent = 0f;
|
||||||
|
|
@ -1301,24 +1375,25 @@ public sealed class PlayerMovementController
|
||||||
// from the character's run skill and auto-upgrades WalkForward+HoldKey.Run
|
// from the character's run skill and auto-upgrades WalkForward+HoldKey.Run
|
||||||
// → RunForward for observers. Sending the raw forward_speed=1.0 (omitted by
|
// → RunForward for observers. Sending the raw forward_speed=1.0 (omitted by
|
||||||
// default-difference packing) still broadcasts RunForward @ runRate — a
|
// default-difference packing) still broadcasts RunForward @ runRate — a
|
||||||
// retail observer saw +Acdream run at full pace. The earlier "wire must
|
// retail observer saw +Acdream run at full pace.
|
||||||
// send run_rate because ACE relays it" comment (citing MovementData.cs)
|
// R3-W6: the LOCAL animation no longer needs a separate
|
||||||
// was WRONG. Our own local animation still wants the actual RunForward
|
// LocalAnimationCommand — the walk→run promotion happens inside the
|
||||||
// cycle — carried separately in LocalAnimationCommand below.
|
// ported machinery (apply_raw_movement → apply_run_to_command
|
||||||
uint? localAnimCmd = null;
|
// promotes WalkForward+HoldKey.Run → RunForward @ my_run_rate on the
|
||||||
|
// interpreted side, which the DefaultSink dispatch plays).
|
||||||
|
// NOTE (R7 scope): the wire values below stay derived from input —
|
||||||
|
// the L.2b-verified byte stream is untouched; sourcing them from
|
||||||
|
// _motion.RawState needs the cdb CommandInterpreter-boundary capture
|
||||||
|
// the W6 map's §2b TODO flags, and R7 owns outbound anyway.
|
||||||
if (input.Forward)
|
if (input.Forward)
|
||||||
{
|
{
|
||||||
outForwardCmd = MotionCommand.WalkForward;
|
outForwardCmd = MotionCommand.WalkForward;
|
||||||
outForwardSpeed = 1.0f; // RAW — ACE recomputes the broadcast speed
|
outForwardSpeed = 1.0f; // RAW — ACE recomputes the broadcast speed
|
||||||
localAnimCmd = (input.Run && _weenie.InqRunRate(out _))
|
|
||||||
? MotionCommand.RunForward // local cycle is RunForward
|
|
||||||
: MotionCommand.WalkForward;
|
|
||||||
}
|
}
|
||||||
else if (input.Backward)
|
else if (input.Backward)
|
||||||
{
|
{
|
||||||
outForwardCmd = MotionCommand.WalkBackward;
|
outForwardCmd = MotionCommand.WalkBackward;
|
||||||
outForwardSpeed = 1.0f;
|
outForwardSpeed = 1.0f;
|
||||||
localAnimCmd = MotionCommand.WalkBackward;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strafe: retail uses speed=1.0 for SideStep (see holtburger
|
// Strafe: retail uses speed=1.0 for SideStep (see holtburger
|
||||||
|
|
@ -1363,18 +1438,22 @@ public sealed class PlayerMovementController
|
||||||
// observers. The forward_speed comparison below is retained (harmless — it
|
// observers. The forward_speed comparison below is retained (harmless — it
|
||||||
// never fires now that forward_speed is constant) for defensiveness.
|
// never fires now that forward_speed is constant) for defensiveness.
|
||||||
bool runHold = input.Run;
|
bool runHold = input.Run;
|
||||||
|
// R3-W6: the localAnimCmd leg is deleted with the synthesis layer;
|
||||||
|
// motionEdgeFired (any DoMotion/StopMotion/set_hold_run edge this
|
||||||
|
// frame) is OR-ed in — by construction an edge IS a state change
|
||||||
|
// (retail dispatches only on edges), keeping the wire cadence
|
||||||
|
// identical to the old output-comparison.
|
||||||
bool changed = outForwardCmd != _prevForwardCmd
|
bool changed = outForwardCmd != _prevForwardCmd
|
||||||
|| outSidestepCmd != _prevSidestepCmd
|
|| outSidestepCmd != _prevSidestepCmd
|
||||||
|| outTurnCmd != _prevTurnCmd
|
|| outTurnCmd != _prevTurnCmd
|
||||||
|| !FloatsEqual(outForwardSpeed, _prevForwardSpeed)
|
|| !FloatsEqual(outForwardSpeed, _prevForwardSpeed)
|
||||||
|| runHold != _prevRunHold
|
|| runHold != _prevRunHold
|
||||||
|| localAnimCmd != _prevLocalAnimCmd;
|
|| motionEdgeFired;
|
||||||
_prevForwardCmd = outForwardCmd;
|
_prevForwardCmd = outForwardCmd;
|
||||||
_prevSidestepCmd = outSidestepCmd;
|
_prevSidestepCmd = outSidestepCmd;
|
||||||
_prevTurnCmd = outTurnCmd;
|
_prevTurnCmd = outTurnCmd;
|
||||||
_prevForwardSpeed = outForwardSpeed;
|
_prevForwardSpeed = outForwardSpeed;
|
||||||
_prevRunHold = runHold;
|
_prevRunHold = runHold;
|
||||||
_prevLocalAnimCmd = localAnimCmd;
|
|
||||||
|
|
||||||
static bool FloatsEqual(float? a, float? b)
|
static bool FloatsEqual(float? a, float? b)
|
||||||
{
|
{
|
||||||
|
|
@ -1434,50 +1513,31 @@ public sealed class PlayerMovementController
|
||||||
|
|
||||||
HeartbeatDue = groundedOnWalkable && sendThisFrame;
|
HeartbeatDue = groundedOnWalkable && sendThisFrame;
|
||||||
|
|
||||||
// K-fix5 (2026-04-26): local-animation-cycle pacing. Visual rate
|
// R3-W6: the K-fix5 LocalAnimationSpeed synthesis is DELETED — the
|
||||||
// should match the actual movement speed. For Forward+Run this is
|
// run pacing now comes from the ported machinery itself
|
||||||
// already runRate (it equals ForwardSpeed). For Backward+Run and
|
// (apply_run_to_command scales the interpreted speed by my_run_rate;
|
||||||
// Strafe+Run it must be runRate too even though the wire keeps
|
// the DefaultSink dispatch plays the cycle at that speed — the same
|
||||||
// those at 1.0. Picking runMul (already computed above) keeps the
|
// source remotes use).
|
||||||
// math in one place.
|
|
||||||
bool anyDirectional = input.Forward || input.Backward
|
bool anyDirectional = input.Forward || input.Backward
|
||||||
|| input.StrafeLeft || input.StrafeRight;
|
|| input.StrafeLeft || input.StrafeRight;
|
||||||
float localAnimSpeed = (input.Run && anyDirectional)
|
|
||||||
? (_weenie.InqRunRate(out float vrrAnim) ? vrrAnim : 1f)
|
|
||||||
: 1f;
|
|
||||||
|
|
||||||
// 2026-05-16 (issue #75) — server-initiated auto-walk drives
|
// R3-W6 (#69 kept alive through the retail pipeline): auto-walk's
|
||||||
// the local animation cycle directly:
|
// turn-first phase now dispatches the turn cycle as an EDGE through
|
||||||
// - moving forward → WalkForward / RunForward (legs animate)
|
// DoMotion/StopMotion instead of the deleted LocalAnimationCommand
|
||||||
// - turn-first phase → TurnLeft / TurnRight (issue #69 fix)
|
// override. Forward legs during auto-walk come from
|
||||||
// - aligned but pre-step / arrival → no override, falls to
|
// DriveServerAutoWalk's own DoMotion(WalkForward) (KEEP-LIST, R4
|
||||||
// the user-input section's default (idle)
|
// replaces). Expected-diff: auto-walk-at-run plays walk-pace legs
|
||||||
// UpdatePlayerAnimation reads LocalAnimationCommand +
|
// until R4's MoveToManager drives CanCharge walk/run selection.
|
||||||
// LocalAnimationSpeed; without these overrides the body
|
if (_autoWalkTurnDirectionThisFrame != _prevAutoWalkTurnDir)
|
||||||
// translates/rotates without leg/arm animation. The motion
|
|
||||||
// cycle commands here flow into the animation sequencer
|
|
||||||
// ONLY — the wire-layer guard at GameWindow.cs:6419 prevents
|
|
||||||
// them from leaking to a user-MoveToState packet during
|
|
||||||
// auto-walk.
|
|
||||||
if (_autoWalkMovingForwardThisFrame)
|
|
||||||
{
|
{
|
||||||
if (_autoWalkInitiallyRunning && _weenie.InqRunRate(out float autoWalkRunRate))
|
var pTurn = new AcDream.Core.Physics.Motion.MovementParameters();
|
||||||
{
|
if (_autoWalkTurnDirectionThisFrame > 0)
|
||||||
localAnimCmd = MotionCommand.RunForward;
|
_motion.DoMotion(MotionCommand.TurnLeft, pTurn);
|
||||||
localAnimSpeed = autoWalkRunRate;
|
else if (_autoWalkTurnDirectionThisFrame < 0)
|
||||||
}
|
_motion.DoMotion(MotionCommand.TurnRight, pTurn);
|
||||||
else
|
else
|
||||||
{
|
_motion.StopMotion(MotionCommand.TurnRight, pTurn);
|
||||||
localAnimCmd = MotionCommand.WalkForward;
|
_prevAutoWalkTurnDir = _autoWalkTurnDirectionThisFrame;
|
||||||
localAnimSpeed = 1f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (_autoWalkTurnDirectionThisFrame != 0)
|
|
||||||
{
|
|
||||||
localAnimCmd = _autoWalkTurnDirectionThisFrame > 0
|
|
||||||
? MotionCommand.TurnLeft
|
|
||||||
: MotionCommand.TurnRight;
|
|
||||||
localAnimSpeed = 1f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MovementResult(
|
return new MovementResult(
|
||||||
|
|
@ -1500,8 +1560,6 @@ public sealed class PlayerMovementController
|
||||||
// who then animated walk + dead-reckoned at walk speed while the
|
// who then animated walk + dead-reckoned at walk speed while the
|
||||||
// server position moved at run speed — visible as observer lag.
|
// server position moved at run speed — visible as observer lag.
|
||||||
IsRunning: input.Run && anyDirectional,
|
IsRunning: input.Run && anyDirectional,
|
||||||
LocalAnimationCommand: localAnimCmd,
|
|
||||||
LocalAnimationSpeed: localAnimSpeed,
|
|
||||||
JustLanded: justLanded,
|
JustLanded: justLanded,
|
||||||
JumpExtent: outJumpExtent,
|
JumpExtent: outJumpExtent,
|
||||||
JumpVelocity: outJumpVelocity);
|
JumpVelocity: outJumpVelocity);
|
||||||
|
|
|
||||||
|
|
@ -739,8 +739,6 @@ public sealed class GameWindow : IDisposable
|
||||||
private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera;
|
private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera;
|
||||||
private bool _playerMode;
|
private bool _playerMode;
|
||||||
private uint _playerServerGuid;
|
private uint _playerServerGuid;
|
||||||
private uint? _playerCurrentAnimCommand;
|
|
||||||
private float _playerCurrentAnimSpeed = 1f;
|
|
||||||
private uint? _playerMotionTableId; // server-sent MotionTable override for the player's character
|
private uint? _playerMotionTableId; // server-sent MotionTable override for the player's character
|
||||||
private MovementTruthOutbound? _lastMovementTruthOutbound;
|
private MovementTruthOutbound? _lastMovementTruthOutbound;
|
||||||
|
|
||||||
|
|
@ -8003,7 +8001,6 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the player entity's animation cycle to match current motion.
|
// Update the player entity's animation cycle to match current motion.
|
||||||
UpdatePlayerAnimation(result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10122,189 +10119,14 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// R3-W6: UpdatePlayerAnimation DELETED — the player's sequencer is
|
||||||
/// Phase B.2: switch the locally-controlled player entity's animation cycle
|
// driven through the SAME MotionTableDispatchSink/DefaultSink funnel
|
||||||
/// to match the current motion command. Only re-resolves when the command
|
// remotes use (edge-driven DoMotion/StopMotion/set_hold_run in
|
||||||
/// actually changes (forward → run, idle → walk, etc.) to avoid re-building
|
// PlayerMovementController; airborne-Falling falls out of
|
||||||
/// the animation entry every frame.
|
// contact_allows_move + apply_current_movement). The #45 sidestep
|
||||||
///
|
// 1.248x factor + ACDREAM_ANIM_SPEED_SCALE died with it —
|
||||||
/// <para>
|
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have
|
||||||
/// Action motions (Jump, FallDown, emotes, attacks) are routed through
|
// always played (w6-cutover-map.md R3).
|
||||||
/// <see cref="AcDream.Core.Physics.AnimationSequencer.PlayAction"/> — they
|
|
||||||
/// live in the motion table's Modifiers dict, not the Cycles dict, and
|
|
||||||
/// are inserted into the queue on top of the current cycle instead of
|
|
||||||
/// replacing it.
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
|
||||||
private void UpdatePlayerAnimation(AcDream.App.Input.MovementResult result)
|
|
||||||
{
|
|
||||||
if (_dats is null) return;
|
|
||||||
|
|
||||||
// ── Airborne SubState (Falling) ────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Retail models the jump-animation as a SubState swap to
|
|
||||||
// MotionCommand.Falling (0x40000015) while airborne, NOT as an
|
|
||||||
// Action overlay. Empirically verified: Links[(NonCombat,RunForward)]
|
|
||||||
// has 3 transitions including 0x40000015 Falling. The SubState cycle
|
|
||||||
// for Falling lives in Cycles[(style, Falling)] and loops while
|
|
||||||
// airborne. On land, we transition back to whatever SubState the
|
|
||||||
// motion input implies (Ready / WalkForward / RunForward).
|
|
||||||
//
|
|
||||||
// Implementation: force animCommand = Falling when airborne; the
|
|
||||||
// existing SetCycle pathway resolves the link + cycle correctly and
|
|
||||||
// the transition back happens naturally when airborne becomes false.
|
|
||||||
|
|
||||||
// Determine the animation command: airborne takes priority (Falling
|
|
||||||
// SubState), then forward, sidestep, turn, then idle (Ready 0x41000003).
|
|
||||||
//
|
|
||||||
// Airborne → Falling (retail behavior; see airborne note above).
|
|
||||||
// Otherwise: LocalAnimationCommand (RunForward when running) preferred,
|
|
||||||
// falling back to wire ForwardCommand (WalkForward / WalkBackward).
|
|
||||||
uint animCommand;
|
|
||||||
if (!result.IsOnGround)
|
|
||||||
animCommand = AcDream.Core.Physics.MotionCommand.Falling;
|
|
||||||
else if (result.LocalAnimationCommand is { } localCmd)
|
|
||||||
animCommand = localCmd;
|
|
||||||
else if (result.ForwardCommand is { } fwd)
|
|
||||||
animCommand = fwd;
|
|
||||||
else if (result.SidestepCommand is { } ss)
|
|
||||||
animCommand = ss;
|
|
||||||
else if (result.TurnCommand is { } tc)
|
|
||||||
animCommand = tc;
|
|
||||||
else
|
|
||||||
animCommand = 0x41000003u; // Ready (idle)
|
|
||||||
|
|
||||||
// Fast path: no command change AND speed delta is negligible. If
|
|
||||||
// command is unchanged but speed changed, we must still propagate
|
|
||||||
// so the sequencer can MultiplyCyclicFramerate — keeping the run
|
|
||||||
// loop smooth without restart.
|
|
||||||
// K-fix5 (2026-04-26): use LocalAnimationSpeed (cycle pace) NOT
|
|
||||||
// ForwardSpeed (wire field) — backward+run + strafe+run keep
|
|
||||||
// ForwardSpeed/SidestepSpeed at 1.0 for ACE compatibility but
|
|
||||||
// need the local cycle to play at runRate × so the animation
|
|
||||||
// matches the actual movement velocity.
|
|
||||||
float newSpeed = result.LocalAnimationSpeed;
|
|
||||||
bool sameCmd = animCommand == _playerCurrentAnimCommand;
|
|
||||||
bool sameSpeed = MathF.Abs(newSpeed - _playerCurrentAnimSpeed) < 1e-3f;
|
|
||||||
if (sameCmd && sameSpeed) return;
|
|
||||||
_playerCurrentAnimCommand = animCommand;
|
|
||||||
_playerCurrentAnimSpeed = newSpeed;
|
|
||||||
|
|
||||||
if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe)) return;
|
|
||||||
|
|
||||||
// The player entity may not be in _animatedEntities if a post-spawn
|
|
||||||
// UpdateMotion removed it (Phase 6.8 pattern). In that case, load
|
|
||||||
// the Setup and re-register. This is the player's own character so
|
|
||||||
// we always want it animated in player mode.
|
|
||||||
if (!_animatedEntities.TryGetValue(pe.Id, out var ae))
|
|
||||||
{
|
|
||||||
// A.5 T10: lock around _dats.Get — worker thread may be
|
|
||||||
// building a landblock mesh concurrently. DatBinReader's
|
|
||||||
// shared buffer position would corrupt without serialization.
|
|
||||||
DatReaderWriter.DBObjs.Setup? setup;
|
|
||||||
lock (_datLock) { setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(pe.SourceGfxObjOrSetupId); }
|
|
||||||
if (setup is null) return;
|
|
||||||
_physicsDataCache.CacheSetup(pe.SourceGfxObjOrSetupId, setup);
|
|
||||||
|
|
||||||
// Build a minimal part template from the entity's current MeshRefs.
|
|
||||||
var template = new (uint, IReadOnlyDictionary<uint, uint>?)[pe.MeshRefs.Count];
|
|
||||||
for (int i = 0; i < pe.MeshRefs.Count; i++)
|
|
||||||
template[i] = (pe.MeshRefs[i].GfxObjId, pe.MeshRefs[i].SurfaceOverrides);
|
|
||||||
|
|
||||||
ae = new AnimatedEntity
|
|
||||||
{
|
|
||||||
Entity = pe,
|
|
||||||
Setup = setup,
|
|
||||||
Animation = null!, // filled below
|
|
||||||
LowFrame = 0,
|
|
||||||
HighFrame = 0,
|
|
||||||
Framerate = 30f,
|
|
||||||
Scale = 1f,
|
|
||||||
PartTemplate = template,
|
|
||||||
CurrFrame = 0f,
|
|
||||||
};
|
|
||||||
_animatedEntities[pe.Id] = ae;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The motion table cycle key is (style << 16) | (command & 0xFFFFFF).
|
|
||||||
// Without a stance override, the resolver uses the table default
|
|
||||||
// (which always maps to the idle/Ready cycle regardless of command).
|
|
||||||
// Pass the NonCombat stance (0x003D) so the resolver builds the
|
|
||||||
// correct cycle key for walk/run/turn commands.
|
|
||||||
ushort cmdOverride = (ushort)(animCommand & 0xFFFFu);
|
|
||||||
const ushort NonCombatStance = 0x003D;
|
|
||||||
var cycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle(
|
|
||||||
ae.Setup, _dats,
|
|
||||||
motionTableIdOverride: _playerMotionTableId,
|
|
||||||
stanceOverride: NonCombatStance,
|
|
||||||
commandOverride: cmdOverride);
|
|
||||||
|
|
||||||
// Sequencer path: SetCycle handles adjust_motion internally
|
|
||||||
// (TurnLeft→TurnRight with negative speed, etc.)
|
|
||||||
//
|
|
||||||
// Speed scaling: K-fix5 (2026-04-26) — use LocalAnimationSpeed
|
|
||||||
// (the PlayerMovementController-computed cycle pace) instead of
|
|
||||||
// the wire ForwardSpeed. Forward+Run = runRate; Backward+Run =
|
|
||||||
// runRate (where ForwardSpeed is the ACE-compatible 1.0);
|
|
||||||
// Strafe+Run = runRate (where SidestepSpeed is 1.0). Anything
|
|
||||||
// not in run = 1.0. The animation cycle now visually matches
|
|
||||||
// the movement velocity in every direction.
|
|
||||||
if (ae.Sequencer is not null)
|
|
||||||
{
|
|
||||||
uint fullStyle = 0x80000000u | (uint)NonCombatStance;
|
|
||||||
float animSpeed = result.LocalAnimationSpeed > 0f
|
|
||||||
? result.LocalAnimationSpeed
|
|
||||||
: 1f;
|
|
||||||
// ACDREAM_ANIM_SPEED_SCALE: optional visual-pacing knob. Retail's
|
|
||||||
// animation framerate scales linearly with speedMod (r03 §8.3),
|
|
||||||
// and our speedMod = runRate. If the visual feel doesn't match
|
|
||||||
// retail, override via env var (default 1.0 = no change).
|
|
||||||
float animScale = 1.0f;
|
|
||||||
if (float.TryParse(
|
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_ANIM_SPEED_SCALE"),
|
|
||||||
System.Globalization.NumberStyles.Float,
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
|
||||||
out var s) && s > 0f)
|
|
||||||
{
|
|
||||||
animScale = s;
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
// localAnimSpeed (1.0 slow / runRate fast), but ACE's
|
|
||||||
// BroadcastMovement converts SidestepSpeed via
|
|
||||||
// `speed × 3.12 / 1.25 × 0.5`
|
|
||||||
// (Network/Motion/MovementData.cs:124-131). Without the
|
|
||||||
// matching multiplier here, the local sidestep cycle plays
|
|
||||||
// at speedMod = 1.0 while the observer-side cycle plays at
|
|
||||||
// ~1.248 — local strafe visibly slower than retail (user
|
|
||||||
// report at #45 close-out of #39).
|
|
||||||
// Factor = WalkAnimSpeed / SidestepAnimSpeed × 0.5
|
|
||||||
// = 3.12 / 1.25 × 0.5 = 1.248.
|
|
||||||
uint cmdLow = animCommand & 0xFFu;
|
|
||||||
if (cmdLow == 0x0Fu /* SideStepRight */ || cmdLow == 0x10u /* SideStepLeft */)
|
|
||||||
{
|
|
||||||
animSpeed *= AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed
|
|
||||||
/ AcDream.Core.Physics.MotionInterpreter.SidestepAnimSpeed
|
|
||||||
* 0.5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
ae.Sequencer.SetCycle(fullStyle, animCommand, animSpeed * animScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy path: update the manual slerp fields (for entities without sequencer)
|
|
||||||
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
|
|
||||||
ae.Animation = cycle.Animation;
|
|
||||||
ae.LowFrame = Math.Max(0, cycle.LowFrame);
|
|
||||||
ae.HighFrame = Math.Min(cycle.HighFrame, cycle.Animation.PartFrames.Count - 1);
|
|
||||||
ae.Framerate = cycle.Framerate;
|
|
||||||
ae.CurrFrame = ae.LowFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase 3a — re-roll the active DayGroup whenever the current
|
/// Phase 3a — re-roll the active DayGroup whenever the current
|
||||||
|
|
@ -11845,7 +11667,6 @@ public sealed class GameWindow : IDisposable
|
||||||
_playerController = null;
|
_playerController = null;
|
||||||
_chaseCamera = null;
|
_chaseCamera = null;
|
||||||
_retailChaseCamera = null;
|
_retailChaseCamera = null;
|
||||||
_playerCurrentAnimCommand = null;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
_window!.Close();
|
_window!.Close();
|
||||||
|
|
@ -12743,7 +12564,6 @@ public sealed class GameWindow : IDisposable
|
||||||
_playerController = null;
|
_playerController = null;
|
||||||
_chaseCamera = null;
|
_chaseCamera = null;
|
||||||
_retailChaseCamera = null;
|
_retailChaseCamera = null;
|
||||||
_playerCurrentAnimCommand = null;
|
|
||||||
_playerMouseDeltaX = 0f;
|
_playerMouseDeltaX = 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12997,6 +12817,12 @@ public sealed class GameWindow : IDisposable
|
||||||
() => playerSeq.Manager.InitializeState();
|
() => playerSeq.Manager.InitializeState();
|
||||||
_playerController.Motion.CheckForCompletedMotions =
|
_playerController.Motion.CheckForCompletedMotions =
|
||||||
playerSeq.Manager.CheckForCompletedMotions;
|
playerSeq.Manager.CheckForCompletedMotions;
|
||||||
|
// R3-W6: the player's cycles are now driven through the SAME
|
||||||
|
// dispatch sink remotes use (TurnApplied/TurnStopped omitted —
|
||||||
|
// ObservedOmega is a remote-DR-only concept; local rotation is
|
||||||
|
// the controller's Yaw integration).
|
||||||
|
_playerController.Motion.DefaultSink =
|
||||||
|
new AcDream.Core.Physics.Motion.MotionTableDispatchSink(playerSeq);
|
||||||
}
|
}
|
||||||
|
|
||||||
var q = playerEntity.Rotation;
|
var q = playerEntity.Rotation;
|
||||||
|
|
|
||||||
|
|
@ -587,12 +587,15 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The physics object's currently-held movement key (retail
|
/// The physics object's currently-held movement key (retail
|
||||||
/// <c>this->raw_state.current_holdkey</c>). <see cref="adjust_motion"/>
|
/// <c>this->raw_state.current_holdkey</c>). <see cref="adjust_motion"/>
|
||||||
/// falls back to this field when the per-channel holdKey argument passed
|
/// falls back to this property when the per-channel holdKey argument
|
||||||
/// in is <see cref="HoldKey.Invalid"/>. D6.2 (the raw-state orchestrator)
|
/// passed in is <see cref="HoldKey.Invalid"/>. R3-W6: an ALIAS of
|
||||||
/// is responsible for keeping this in sync with the raw motion state;
|
/// <see cref="RawMotionState.CurrentHoldKey"/> — retail's adjust_motion
|
||||||
/// default <see cref="HoldKey.None"/> is a safe no-op until then.
|
/// reads <c>raw_state.current_holdkey</c> directly; the former shadow
|
||||||
|
/// field only synced on the raw dispatch branch, so an edge-driven
|
||||||
|
/// set_hold_run followed by DoMotion read a stale None and lost the
|
||||||
|
/// run promotion (the W6 cutover surfaced this).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HoldKey CurrentHoldKey = HoldKey.None;
|
public HoldKey CurrentHoldKey => RawState.CurrentHoldKey;
|
||||||
|
|
||||||
/// <summary>True when crouching-in-place for a standing long jump (offset +0x70).</summary>
|
/// <summary>True when crouching-in-place for a standing long jump (offset +0x70).</summary>
|
||||||
public bool StandingLongJump;
|
public bool StandingLongJump;
|
||||||
|
|
@ -1345,7 +1348,10 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void apply_raw_movement(RawMotionState raw)
|
public void apply_raw_movement(RawMotionState raw)
|
||||||
{
|
{
|
||||||
CurrentHoldKey = raw.CurrentHoldKey;
|
// R3-W6: CurrentHoldKey is now an alias of RawState.CurrentHoldKey;
|
||||||
|
// keep the interpreter's raw state authoritative when a snapshot
|
||||||
|
// arrives through this legacy overload.
|
||||||
|
RawState.CurrentHoldKey = raw.CurrentHoldKey;
|
||||||
|
|
||||||
// Copy raw -> interpreted. Retail copies 7 fields; our
|
// Copy raw -> interpreted. Retail copies 7 fields; our
|
||||||
// InterpretedMotionState omits current_style (velocity doesn't use it).
|
// InterpretedMotionState omits current_style (velocity doesn't use it).
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ public sealed class MotionNormalizationTests
|
||||||
public void AdjustMotion_HoldKeyInvalid_FallsBackToCurrentHoldKey_PromotesWalkForwardWhenRun()
|
public void AdjustMotion_HoldKeyInvalid_FallsBackToCurrentHoldKey_PromotesWalkForwardWhenRun()
|
||||||
{
|
{
|
||||||
var interp = MakeInterp();
|
var interp = MakeInterp();
|
||||||
interp.CurrentHoldKey = HoldKey.Run;
|
interp.RawState.CurrentHoldKey = HoldKey.Run; // R3-W6: CurrentHoldKey aliases RawState
|
||||||
uint motion = MotionCommand.WalkForward;
|
uint motion = MotionCommand.WalkForward;
|
||||||
float speed = 1.0f;
|
float speed = 1.0f;
|
||||||
|
|
||||||
|
|
@ -164,7 +164,7 @@ public sealed class MotionNormalizationTests
|
||||||
public void AdjustMotion_HoldKeyNone_DoesNotPromote()
|
public void AdjustMotion_HoldKeyNone_DoesNotPromote()
|
||||||
{
|
{
|
||||||
var interp = MakeInterp();
|
var interp = MakeInterp();
|
||||||
interp.CurrentHoldKey = HoldKey.Run;
|
interp.RawState.CurrentHoldKey = HoldKey.Run; // R3-W6: CurrentHoldKey aliases RawState
|
||||||
uint motion = MotionCommand.WalkForward;
|
uint motion = MotionCommand.WalkForward;
|
||||||
float speed = 1.0f;
|
float speed = 1.0f;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue