Three unpack_movement parity items (facade deferred per the handoff's own optional clause — see r5-wiring-handoff §V4 status): 1. HEAD style-on-change (0x00524440 @00524502-0052452c): both GameWindow routing heads (remote + player) now dispatch DoMotion(style, ctor defaults) when the UM's stance differs from the interp's current style, BEFORE the movement-type routing — for EVERY type. Previously style applied only through the mt-0 funnel copy, so a chase/turn UM (mt 6-9) carrying a stance change started the move in the OLD stance (a monster charged in NonCombat posture until the next mt-0). The RetailObserverTraceConformanceTests exclusion note updated — the trace filter stays (head calls can't appear in a MoveToInterpretedState replay) but the production gap it pointed at is closed. 2. #164 (closes): the action-replay loop threads each action's autonomy into the dispatch params (Autonomous = the 0x1000 splice, raw 305982) — replayed actions enter the interpreted actions list with their real autonomy instead of ctor-default false. 3. mt-0 wire flags consumed (UpdateMotion parsed them since R4-V3): 0x1 StickToObject → CPhysicsObj::stick_to_object port (0x005127e0: resolve target, PartArray radii — 0 when shapeless, guid-as-is for acdream's flat entity table — → PositionManager.StickTo; unresolvable target → no stick), at BOTH case-0 tails in retail order (@00524583-0052458e: funnel apply → stick → longjump flag); 0x2 StandingLongJump → Motion.StandingLongJump, UNCONDITIONAL write (absent flag clears — retail @0052458e). ServerMotionState gains the StandingLongJump field. Conformance: ChaseArm_WithStanceChange_AppliesStanceBeforeTheChase (harness mirrors the routing-head dispatch) + Actions_ReplayCarriesAutonomyIntoTheInterpretedList. Suite 4041 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3306 lines
152 KiB
C#
3306 lines
152 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
using AcDream.Core.Physics.Motion;
|
||
|
||
namespace AcDream.Core.Physics;
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// MotionInterpreter — C# port of CMotionInterp from acclient.exe (chunk_00520000.c).
|
||
//
|
||
// Source addresses (chunk_00520000.c):
|
||
// FUN_00529a90 PerformMovement — top-level dispatcher (switch 1-5)
|
||
// FUN_00529930 DoMotion — process one raw motion command
|
||
// FUN_00528a50 StopCompletely — reset to Ready/idle
|
||
// FUN_00528960 get_state_velocity — compute world-space velocity for current motion
|
||
// FUN_00529210 apply_current_movement — apply interpreted motion as velocity
|
||
// FUN_00529390 jump — initiate jump: validate, record extent, leave ground
|
||
// FUN_005286b0 GetJumpVZ — get vertical jump velocity (R3-W3 rename)
|
||
// FUN_00528cd0 GetLeaveGroundVelocity — compose full 3D launch vector (R3-W3 rename)
|
||
// FUN_00528ec0 jump_is_allowed — can we jump? (R3-W3: full verbatim chain)
|
||
// FUN_00528dd0 contact_allows_move — slope angle / contact state check
|
||
//
|
||
// R3-W3 jump-family addresses (docs/research/2026-07-02-r3-motioninterp/):
|
||
// 0x00527a50 JumpChargeIsAllowed
|
||
// 0x005281c0 ChargeJump — the ONLY place StandingLongJump arms (closes J6)
|
||
//
|
||
// Cross-checked against ACE MotionInterp.cs.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
// ── Motion command constants (from retail dat / wire protocol) ────────────────
|
||
/// <summary>
|
||
/// Raw AC motion command IDs used in CMotionInterp.
|
||
/// Values sourced from the decompiled comparisons in chunk_00520000.c and
|
||
/// confirmed against ACE's MotionCommand enum.
|
||
/// </summary>
|
||
public static class MotionCommand
|
||
{
|
||
/// <summary>0x41000003 — idle/default state.</summary>
|
||
public const uint Ready = 0x41000003u;
|
||
/// <summary>0x45000005 — walk forward.</summary>
|
||
public const uint WalkForward = 0x45000005u;
|
||
/// <summary>0x44000007 — run forward.</summary>
|
||
public const uint RunForward = 0x44000007u;
|
||
/// <summary>0x45000006 — walk backward.</summary>
|
||
public const uint WalkBackward = 0x45000006u;
|
||
/// <summary>0x6500000D — turn right.</summary>
|
||
public const uint TurnRight = 0x6500000Du;
|
||
/// <summary>0x6500000E — turn left.</summary>
|
||
public const uint TurnLeft = 0x6500000Eu;
|
||
/// <summary>0x6500000F — sidestep right.</summary>
|
||
public const uint SideStepRight = 0x6500000Fu;
|
||
/// <summary>0x65000010 — sidestep left.</summary>
|
||
public const uint SideStepLeft = 0x65000010u;
|
||
/// <summary>0x40000008 — Fallen (lying on ground).</summary>
|
||
public const uint Fallen = 0x40000008u;
|
||
/// <summary>
|
||
/// 0x40000015 — Falling (SubState). The airborne cycle. Retail's
|
||
/// MotionTable has Links from RunForward/Ready/WalkForward → Falling,
|
||
/// and a Cycles entry for (style, Falling) that loops while the body
|
||
/// is in the air. Swap via <see cref="AnimationSequencer.SetCycle"/>
|
||
/// when airborne; swap back to Ready/WalkForward/RunForward on land.
|
||
/// </summary>
|
||
public const uint Falling = 0x40000015u;
|
||
/// <summary>
|
||
/// 0x2500003B — Jump (Modifier flag). NOT an animation trigger; retail
|
||
/// uses this as a state flag internally. Kept for future use.
|
||
/// </summary>
|
||
public const uint Jump = 0x2500003Bu;
|
||
/// <summary>
|
||
/// 0x1000004B — Jumpup (Action). Not present in the humanoid player
|
||
/// motion table's Links dict (empirically verified). Retail uses the
|
||
/// Falling SubState for airborne animation instead.
|
||
/// </summary>
|
||
public const uint Jumpup = 0x1000004Bu;
|
||
/// <summary>
|
||
/// 0x10000050 — FallDown (Action). Same story as Jumpup; not in the
|
||
/// humanoid motion table's Links. Landing returns to Ready via the
|
||
/// regular SetCycle transition.
|
||
/// </summary>
|
||
public const uint FallDown = 0x10000050u;
|
||
/// <summary>0x40000011 - persistent dead substate.</summary>
|
||
public const uint Dead = 0x40000011u;
|
||
/// <summary>0x10000057 - Sanctuary death-trigger action.</summary>
|
||
public const uint Sanctuary = 0x10000057u;
|
||
/// <summary>0x41000012 - crouching substate.</summary>
|
||
public const uint Crouch = 0x41000012u;
|
||
/// <summary>0x41000013 - sitting substate.</summary>
|
||
public const uint Sitting = 0x41000013u;
|
||
/// <summary>0x41000014 - sleeping substate.</summary>
|
||
public const uint Sleeping = 0x41000014u;
|
||
/// <summary>0x41000011 — Crouch lower bound for blocked-jump check.</summary>
|
||
public const uint CrouchLowerBound = 0x41000011u;
|
||
/// <summary>0x41000015 - exclusive upper bound of crouch/sit/sleep range.</summary>
|
||
public const uint CrouchUpperExclusive = 0x41000015u;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Movement type passed in PerformMovement's switch statement. Matches
|
||
/// retail's <c>MovementTypes::Type</c> (acclient.h:2856, enum #229) in full.
|
||
///
|
||
/// <para>
|
||
/// <b>R4-V1 widening (closes M11).</b> Values 1-5 dispatch through
|
||
/// <c>CMotionInterp</c> (the 5-case switch at FUN_00529a90, unchanged since
|
||
/// R1-R3); values 6-9 dispatch through <c>MoveToManager</c>
|
||
/// (<c>MovementManager::PerformMovement</c>, r4-moveto-decomp.md §2b:
|
||
/// <c>(type - 1) > 8 → 0x47</c>, case 0-4 → CMotionInterp, case 5-8 →
|
||
/// MoveToManager). <c>Invalid=0</c> and values > 9 both fail with
|
||
/// <see cref="WeenieError.GeneralMovementFailure"/> (0x47) at the
|
||
/// MovementManager level — no consumer wiring changes in this slice
|
||
/// (mechanical, additive-only; MoveToManager itself is R4-V2+).
|
||
/// </para>
|
||
/// </summary>
|
||
public enum MovementType
|
||
{
|
||
/// <summary>
|
||
/// 0 — no movement in progress / uninitialized. R4-V1 addition (M11).
|
||
/// <c>MoveToManager::InitializeLocalVariables</c> resets
|
||
/// <c>movement_type</c> to this value; <c>MovementManager::PerformMovement</c>
|
||
/// rejects it with 0x47 (§2b: <c>(type - 1) > 8</c> underflows to a
|
||
/// huge unsigned value for type 0, which is always > 8).
|
||
/// </summary>
|
||
Invalid = 0,
|
||
/// <summary>case 1 — raw motion command (DoMotion).</summary>
|
||
RawCommand = 1,
|
||
/// <summary>case 2 — interpreted motion command (DoInterpretedMotion).</summary>
|
||
InterpretedCommand = 2,
|
||
/// <summary>case 3 — stop raw motion (StopMotion).</summary>
|
||
StopRawCommand = 3,
|
||
/// <summary>case 4 — stop interpreted motion (StopInterpretedMotion).</summary>
|
||
StopInterpretedCommand = 4,
|
||
/// <summary>case 5 — stop completely (StopCompletely).</summary>
|
||
StopCompletely = 5,
|
||
/// <summary>
|
||
/// 6 — MoveToObject. R4-V1 addition (M11). Dispatches to
|
||
/// <c>MoveToManager::MoveToObject</c> (r4-moveto-decomp.md §3b); uses
|
||
/// <see cref="MovementStruct.ObjectId"/>/<see cref="MovementStruct.TopLevelId"/>/
|
||
/// <see cref="MovementStruct.Radius"/>/<see cref="MovementStruct.Height"/>.
|
||
/// </summary>
|
||
MoveToObject = 6,
|
||
/// <summary>
|
||
/// 7 — MoveToPosition. R4-V1 addition (M11). Dispatches to
|
||
/// <c>MoveToManager::MoveToPosition</c> (§3c); uses
|
||
/// <see cref="MovementStruct.Pos"/>.
|
||
/// </summary>
|
||
MoveToPosition = 7,
|
||
/// <summary>
|
||
/// 8 — TurnToObject. R4-V1 addition (M11). Dispatches to
|
||
/// <c>MoveToManager::TurnToObject</c> (§3d); uses
|
||
/// <see cref="MovementStruct.ObjectId"/>/<see cref="MovementStruct.TopLevelId"/>.
|
||
/// </summary>
|
||
TurnToObject = 8,
|
||
/// <summary>
|
||
/// 9 — TurnToHeading. R4-V1 addition (M11). Dispatches to
|
||
/// <c>MoveToManager::TurnToHeading</c> (§3e); uses
|
||
/// <see cref="MovementStruct.Params"/>'s <c>DesiredHeading</c>.
|
||
/// </summary>
|
||
TurnToHeading = 9,
|
||
}
|
||
|
||
/// <summary>
|
||
/// WeenieError-shaped codes returned by CMotionInterp methods. Values are
|
||
/// the hex constants used directly in the decompiled C code.
|
||
///
|
||
/// <para>
|
||
/// <b>R3-W1 renumber (closes J16-codes/A10).</b> Prior to this slice, the
|
||
/// names were shuffled relative to retail's actual numeric semantics
|
||
/// (<c>GeneralMovementFailure</c> was assigned 0x24 and used at the
|
||
/// airborne-jump gate, but retail's 0x24 means "not grounded / no contact"
|
||
/// and retail's 0x47 is the general-movement-failure code; 0x48 meant
|
||
/// "cannot jump while in the air" here, but retail's 0x48 means "jump
|
||
/// blocked by the CURRENT MOTION OR POSITION" — a blocklist check
|
||
/// (<c>motion_allows_jump</c>), not an airborne check). Renumbered per the
|
||
/// definitive, exhaustively-swept table in
|
||
/// <c>docs/research/2026-07-02-r3-motioninterp/W0-pins.md</c> §A10 (19
|
||
/// return sites + 1 store site over raw 304908-306277 + 300150-300540).
|
||
/// These codes are LOCAL-ONLY — never serialized to the wire — so the
|
||
/// renumber is safe (verified: no consumer outside
|
||
/// <c>MotionInterpreter.cs</c> pattern-matches on the numeric value).
|
||
/// </para>
|
||
/// </summary>
|
||
public enum WeenieError : uint
|
||
{
|
||
/// <summary>0x00 — success.</summary>
|
||
None = 0x00,
|
||
/// <summary>
|
||
/// 0x08 — no <c>physics_obj</c>. Sites (A10): StopCompletely @305214;
|
||
/// DoInterpretedMotion @305579; StopInterpretedMotion @305639;
|
||
/// StopMotion @305680; jump @305798; DoMotion @306165.
|
||
/// </summary>
|
||
NoPhysicsObject = 0x08,
|
||
/// <summary>
|
||
/// 0x0B — NoMotionInterpreter. R4-V1 addition (M12), per
|
||
/// docs/research/2026-07-03-r4-moveto/r4-moveto-decomp.md §12 constants
|
||
/// inventory (<c>8, 0xb, 0x36, 0x37, 0x38, 0x3d, 0x47</c>). ACE name;
|
||
/// the retail sites that store this code were not individually
|
||
/// extracted in the R4 pass (no MoveToManager consumer in this slice —
|
||
/// V1 pins the numeric value only).
|
||
/// </summary>
|
||
NoMotionInterpreter = 0x0B,
|
||
/// <summary>
|
||
/// 0x24 — not grounded / no contact. Sites (A10):
|
||
/// <c>jump_is_allowed</c> @305570 (gravity-active creature without
|
||
/// Contact+OnWalkable; also the <c>physics_obj == null</c> case, which
|
||
/// falls out to this same code per the A10 note — NOT 8);
|
||
/// <c>DoInterpretedMotion</c> @305622-305623 (action-class motion
|
||
/// blocked by <c>contact_allows_move</c>).
|
||
/// </summary>
|
||
NotGrounded = 0x24,
|
||
/// <summary>
|
||
/// 0x3f — Crouch (0x41000012) rejected while in combat stance. Site:
|
||
/// DoMotion @306196.
|
||
/// </summary>
|
||
CrouchInCombatStance = 0x3f,
|
||
/// <summary>
|
||
/// 0x40 — Sitting (0x41000013) rejected while in combat stance. Site:
|
||
/// DoMotion @306199.
|
||
/// </summary>
|
||
SitInCombatStance = 0x40,
|
||
/// <summary>
|
||
/// 0x41 — Sleeping (0x41000014) rejected while in combat stance. Site:
|
||
/// DoMotion @306202.
|
||
/// </summary>
|
||
SleepInCombatStance = 0x41,
|
||
/// <summary>
|
||
/// 0x42 — <c>motion & 0x2000000</c> (the chat-emote bit) rejected
|
||
/// outside NonCombat (0x8000003d). Site: DoMotion @306205.
|
||
/// </summary>
|
||
ChatEmoteOutsideNonCombat = 0x42,
|
||
/// <summary>
|
||
/// 0x36 — ActionCancelled. R4-V1 addition (M12). Site:
|
||
/// <c>MoveToManager::PerformMovement</c> (r4-moveto-decomp.md §3a
|
||
/// @0052a901) — every new moveto cancels the previous one with this
|
||
/// code before dispatching; also <c>CPhysicsObj::interrupt_current_movement</c>
|
||
/// → <c>MovementManager::CancelMoveTo(0x36)</c> (§9e — the TS-36 cancel
|
||
/// entry). Per §7c, <c>MoveToManager::CancelMoveTo</c>'s WeenieError arg
|
||
/// is NEVER READ in this build's body — kept for parity/logging only.
|
||
/// </summary>
|
||
ActionCancelled = 0x36,
|
||
/// <summary>
|
||
/// 0x37 — ObjectGone. R4-V1 addition (M12). Site:
|
||
/// <c>MoveToManager::HandleUpdateTarget</c> (§6d @307866-307867) — a
|
||
/// RETARGET delivery arrives with a non-OK target status (the target
|
||
/// object was already being tracked, then went away).
|
||
/// </summary>
|
||
ObjectGone = 0x37,
|
||
/// <summary>
|
||
/// 0x38 — NoObject. R4-V1 addition (M12). Site:
|
||
/// <c>MoveToManager::HandleUpdateTarget</c> (§6d @307857-307858) — the
|
||
/// FIRST target callback arrives with a non-OK status (the target never
|
||
/// resolved in the first place).
|
||
/// </summary>
|
||
NoObject = 0x38,
|
||
/// <summary>
|
||
/// 0x45 — action-queue depth cap: an action-class motion (bit
|
||
/// 0x10000000) with <c>GetNumActions() >= 6</c> pending. Site:
|
||
/// DoMotion @306209.
|
||
/// </summary>
|
||
ActionDepthExceeded = 0x45,
|
||
/// <summary>
|
||
/// 0x47 — general movement failure. Sites (A10):
|
||
/// <c>jump_is_allowed</c> @305525 (<c>IsFullyConstrained</c>);
|
||
/// @305549+305556 (<c>JumpStaminaCost</c> refusal);
|
||
/// <c>CMotionInterp::PerformMovement</c> @306227 (dispatch type-1 > 4);
|
||
/// <c>MovementManager::PerformMovement</c> @300201 (dispatch type-1 > 8).
|
||
/// </summary>
|
||
GeneralMovementFailure = 0x47,
|
||
/// <summary>
|
||
/// 0x48 — jump BLOCKED by the current motion or position (A1: the
|
||
/// <c>motion_allows_jump</c> literal-range blocklist — NOT an airborne
|
||
/// check; airborne is 0x24). Sites (A10):
|
||
/// <c>motion_allows_jump</c> @304930; <c>jump_charge_is_allowed</c>
|
||
/// @304948 (Fallen or Crouch..Sleeping); <c>charge_jump</c> @305459
|
||
/// (same predicate); STORED (not returned) as the queue node's
|
||
/// <c>jump_error_code</c> in <c>DoInterpretedMotion</c> @305605 when
|
||
/// <c>disable_jump_during_link</c> is set.
|
||
/// </summary>
|
||
YouCantJumpFromThisPosition = 0x48,
|
||
/// <summary>
|
||
/// 0x49 — the weenie's <c>CanJump(jump_extent)</c> virtual refused
|
||
/// (e.g. stamina/burden gate). Sites: <c>jump_charge_is_allowed</c>
|
||
/// @304941; <c>charge_jump</c> @305454.
|
||
/// </summary>
|
||
CantJumpLoadedDown = 0x49,
|
||
/// <summary>
|
||
/// 0x3D — YouChargedTooFar. R4-V1 addition (M12). Site:
|
||
/// <c>MoveToManager::HandleMoveToPosition</c> Phase 2 arrival check
|
||
/// (r4-moveto-decomp.md §6b) — the <c>fail_distance</c> progress gate
|
||
/// exceeded (<c>CheckProgressMade</c> §5b failing for >1s AND the
|
||
/// mover overshot <c>fail_distance</c>). NOTE: numerically out of A10's
|
||
/// increasing order (0x3D < 0x3F/0x40/0x41/0x42/0x45) because it was
|
||
/// not part of the CMotionInterp jump-family sweep this code sits
|
||
/// beside — it belongs to the MoveToManager family instead (§7c, §12).
|
||
/// </summary>
|
||
YouChargedTooFar = 0x3D,
|
||
}
|
||
|
||
// ── Motion state structs ───────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Interpreted motion state, derived from the raw state (retail
|
||
/// <c>InterpretedMotionState</c>, ctor 0x0051e8d0, decomp 293418-293431).
|
||
/// Struct layout: starts at offset +0x44 (ForwardCommand at +0x4C, ForwardSpeed at +0x50).
|
||
///
|
||
/// <para>
|
||
/// <b>R3-W1 (closes J2):</b> gains retail's action FIFO
|
||
/// (<see cref="AddAction"/>/<see cref="RemoveAction"/>/<see cref="GetNumActions"/>/
|
||
/// <see cref="ApplyMotion"/>/<see cref="RemoveMotion"/>) — previously this was
|
||
/// a flat 6-field struct with no action tracking, so
|
||
/// <c>DoMotion</c>'s <c>GetNumActions() >= 6</c> depth cap and
|
||
/// <c>MotionDone</c>'s action-class <c>RemoveAction</c> pop had nothing to
|
||
/// operate on. The action list is backed by a private
|
||
/// <see cref="List{T}"/> lazily created on first mutation (defensive
|
||
/// against a bare <c>default(InterpretedMotionState)</c>, which C# structs
|
||
/// permit even though every constructor in this file routes through
|
||
/// <see cref="Default"/>).
|
||
/// </para>
|
||
/// </summary>
|
||
public struct InterpretedMotionState
|
||
{
|
||
/// <summary>Forward/backward interpreted command (offset +0x4C).</summary>
|
||
public uint ForwardCommand;
|
||
/// <summary>Speed scalar for interpreted forward motion (offset +0x50).</summary>
|
||
public float ForwardSpeed;
|
||
/// <summary>Sidestep interpreted command (offset +0x54).</summary>
|
||
public uint SideStepCommand;
|
||
/// <summary>Speed scalar for interpreted sidestep (offset +0x58).</summary>
|
||
public float SideStepSpeed;
|
||
/// <summary>Turn interpreted command (offset +0x5C).</summary>
|
||
public uint TurnCommand;
|
||
/// <summary>Speed scalar for turn (offset +0x60).</summary>
|
||
public float TurnSpeed;
|
||
/// <summary>Current style / stance (retail current_style). Adopted from
|
||
/// the raw state's style channel; NOT part of retail's
|
||
/// <c>InterpretedMotionState::ApplyMotion</c> dispatch itself (that
|
||
/// writes <c>this->current_style</c> only via the negative-motion
|
||
/// branch — see <see cref="ApplyMotion"/>). Default NonCombat
|
||
/// 0x8000003D.</summary>
|
||
public uint CurrentStyle;
|
||
|
||
private List<RawMotionAction>? _actions;
|
||
|
||
/// <summary>Action FIFO in retail order (oldest first). Empty (never
|
||
/// null) when read — the private field is lazily created.</summary>
|
||
public readonly IReadOnlyList<RawMotionAction> Actions
|
||
=> (IReadOnlyList<RawMotionAction>?)_actions ?? Array.Empty<RawMotionAction>();
|
||
|
||
/// <summary>Initialize to the idle/ready state.</summary>
|
||
public static InterpretedMotionState Default() => new()
|
||
{
|
||
ForwardCommand = MotionCommand.Ready,
|
||
ForwardSpeed = 1.0f,
|
||
SideStepCommand = 0,
|
||
SideStepSpeed = 1.0f,
|
||
TurnCommand = 0,
|
||
TurnSpeed = 1.0f,
|
||
CurrentStyle = 0x8000003Du,
|
||
};
|
||
|
||
/// <summary>
|
||
/// <c>InterpretedMotionState::AddAction</c> (0x0051e9e0, decomp
|
||
/// 293500-293527): unconditional tail-append of
|
||
/// <c>{motion, speed, action_stamp, autonomous}</c>. Identical shape to
|
||
/// <see cref="RawMotionState.AddAction"/> — retail duplicates the
|
||
/// LListData append logic per state type rather than sharing it.
|
||
/// </summary>
|
||
public void AddAction(uint motion, float speed, uint actionStamp, bool autonomous)
|
||
{
|
||
_actions ??= new List<RawMotionAction>();
|
||
_actions.Add(new RawMotionAction(
|
||
Command: (ushort)motion,
|
||
Stamp: (ushort)actionStamp,
|
||
Autonomous: autonomous,
|
||
Speed: speed));
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>InterpretedMotionState::RemoveAction</c> (0x0051ead0, decomp
|
||
/// 293568-293586): pop the FIFO head unconditionally, returning its
|
||
/// <c>motion</c> field (0 when empty).
|
||
/// </summary>
|
||
public uint RemoveAction()
|
||
{
|
||
if (_actions is null || _actions.Count == 0)
|
||
return 0;
|
||
var head = _actions[0];
|
||
_actions.RemoveAt(0);
|
||
return head.Command;
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>InterpretedMotionState::GetNumActions</c> (0x0051eb00, decomp
|
||
/// 293590-293603): count the FIFO by walking it (retail has no O(1)
|
||
/// count field on the LList).
|
||
/// </summary>
|
||
public readonly uint GetNumActions() => (uint)(_actions?.Count ?? 0);
|
||
|
||
/// <summary>
|
||
/// <c>InterpretedMotionState::ApplyMotion</c> (0x0051ea40, decomp
|
||
/// 293531-293564). Verbatim dispatch, quoted from the raw named decomp:
|
||
/// <code>
|
||
/// if (arg2 == 0x6500000d) { turn_command = arg2; turn_speed = params.speed; return; }
|
||
/// if (arg2 == 0x6500000f) { sidestep_command = arg2; sidestep_speed = params.speed; return; }
|
||
/// if ((arg2 & 0x40000000) != 0) { forward_command = arg2; forward_speed = params.speed; return; }
|
||
/// if (arg2 < 0) { forward_command = 0x41000003; current_style = arg2; return; }
|
||
/// if ((arg2 & 0x10000000) != 0)
|
||
/// AddAction(arg2, params.speed, params.action_stamp, (params.bitfield>>0xc)&1);
|
||
/// </code>
|
||
/// Note only TurnRight (0x6500000d) and SideStepRight (0x6500000f) are
|
||
/// tested here — unlike <see cref="RawMotionState.ApplyMotion"/>, the
|
||
/// LEFT variants (TurnLeft/SideStepLeft) are NOT separately cased;
|
||
/// retail's <c>adjust_motion</c> normalizes Left→Right upstream before
|
||
/// this ever runs, so this asymmetry is verbatim, not a gap.
|
||
/// </summary>
|
||
/// <param name="motion">Retail <c>arg2</c> — the motion id AFTER
|
||
/// <c>adjust_motion</c> normalization (the ADJUSTED id, unlike
|
||
/// <see cref="RawMotionState.ApplyMotion"/> which takes the ORIGINAL).</param>
|
||
/// <param name="p">Retail <c>arg3</c> (<c>MovementParameters const*</c>).</param>
|
||
public void ApplyMotion(uint motion, MovementParameters p)
|
||
{
|
||
if (motion == 0x6500000du) // TurnRight
|
||
{
|
||
TurnCommand = motion;
|
||
TurnSpeed = p.Speed;
|
||
return;
|
||
}
|
||
if (motion == 0x6500000fu) // SideStepRight
|
||
{
|
||
SideStepCommand = motion;
|
||
SideStepSpeed = p.Speed;
|
||
return;
|
||
}
|
||
if ((motion & 0x40000000u) != 0)
|
||
{
|
||
ForwardCommand = motion;
|
||
ForwardSpeed = p.Speed;
|
||
return;
|
||
}
|
||
if (motion >= 0x80000000u) // arg2 < 0 as signed int32
|
||
{
|
||
ForwardCommand = 0x41000003u;
|
||
CurrentStyle = motion;
|
||
return;
|
||
}
|
||
if ((motion & 0x10000000u) != 0)
|
||
AddAction(motion, p.Speed, p.ActionStamp, p.Autonomous);
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>InterpretedMotionState::RemoveMotion</c> (0x0051e790, decomp
|
||
/// 293315-293340):
|
||
/// <code>
|
||
/// if (arg2 == 0x6500000d) { turn_command = 0; return; }
|
||
/// if (arg2 == 0x6500000f) { sidestep_command = 0; return; }
|
||
/// if ((arg2 & 0x40000000) == 0) {
|
||
/// if (arg2 < 0 && arg2 == current_style) current_style = 0x8000003d;
|
||
/// } else if (arg2 == forward_command) {
|
||
/// forward_command = 0x41000003; forward_speed = 1f;
|
||
/// }
|
||
/// </code>
|
||
/// Note the asymmetric range test vs <see cref="RawMotionState.RemoveMotion"/>
|
||
/// (which uses <c>(arg2 - 0x6500000d) > 3</c> covering all four
|
||
/// turn/sidestep ids) — this one only special-cases the RIGHT variants
|
||
/// by exact equality; TurnLeft/SideStepLeft fall through to the
|
||
/// style/forward-command branch below. Verbatim, not a gap (mirrors
|
||
/// retail's own asymmetry between the two RemoveMotion bodies).
|
||
/// </summary>
|
||
/// <param name="motion">Retail <c>arg2</c>.</param>
|
||
public void RemoveMotion(uint motion)
|
||
{
|
||
if (motion == 0x6500000du)
|
||
{
|
||
TurnCommand = 0;
|
||
return;
|
||
}
|
||
if (motion == 0x6500000fu)
|
||
{
|
||
SideStepCommand = 0;
|
||
return;
|
||
}
|
||
if ((motion & 0x40000000u) == 0)
|
||
{
|
||
if (motion >= 0x80000000u && motion == CurrentStyle)
|
||
CurrentStyle = 0x8000003du;
|
||
}
|
||
else if (motion == ForwardCommand)
|
||
{
|
||
ForwardCommand = 0x41000003u;
|
||
ForwardSpeed = 1f;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Lightweight struct passed into PerformMovement.
|
||
/// Fields correspond to what the retail dispatcher read from param_1 (the movement packet struct).
|
||
///
|
||
/// <para>
|
||
/// <b>R4-V1 widening (closes M11).</b> Retail's full <c>MovementStruct</c>
|
||
/// (acclient.h:38069, struct #4067):
|
||
/// <code>
|
||
/// struct __cppobj MovementStruct
|
||
/// {
|
||
/// MovementTypes::Type type;
|
||
/// unsigned int motion; // types 1-4 only
|
||
/// unsigned int object_id; // types 6, 8
|
||
/// unsigned int top_level_id; // types 6, 8
|
||
/// Position pos; // type 7
|
||
/// float radius; // type 6
|
||
/// float height; // type 6
|
||
/// MovementParameters *params; // types 1-4, 6-9
|
||
/// };
|
||
/// </code>
|
||
/// <see cref="ObjectId"/>/<see cref="TopLevelId"/>/<see cref="Pos"/>/
|
||
/// <see cref="Radius"/>/<see cref="Height"/>/<see cref="Params"/> are the
|
||
/// R4-V1 additions — additive only, no consumer wiring in this slice
|
||
/// (MoveToManager itself is R4-V2). The pre-R4 fields (<see cref="Type"/>/
|
||
/// <see cref="Motion"/>/<see cref="Speed"/>/<see cref="Autonomous"/>/
|
||
/// <see cref="ModifyInterpretedState"/>/<see cref="ModifyRawState"/>) are
|
||
/// untouched. <see cref="Pos"/> uses acdream's <see cref="Position"/>
|
||
/// (ObjCellId + CellFrame) rather than retail's block-local Position —
|
||
/// V0-pins.md §P5: distances are equivalent after rebase in acdream's
|
||
/// streaming-world space.
|
||
/// </para>
|
||
/// </summary>
|
||
public struct MovementStruct
|
||
{
|
||
/// <summary>Which movement type to dispatch (retail <c>MovementTypes::Type</c>, full 0-9 range).</summary>
|
||
public MovementType Type;
|
||
/// <summary>Motion command ID (e.g. WalkForward). Types 1-4 only.</summary>
|
||
public uint Motion;
|
||
/// <summary>Speed scalar for this motion.</summary>
|
||
public float Speed;
|
||
/// <summary>Autonomous (player-initiated) flag.</summary>
|
||
public bool Autonomous;
|
||
/// <summary>Whether to modify the interpreted state.</summary>
|
||
public bool ModifyInterpretedState;
|
||
/// <summary>Whether to modify the raw state.</summary>
|
||
public bool ModifyRawState;
|
||
|
||
/// <summary>
|
||
/// R4-V1 — retail <c>object_id</c>. Types 6 (MoveToObject), 8
|
||
/// (TurnToObject) only.
|
||
/// </summary>
|
||
public uint ObjectId;
|
||
/// <summary>
|
||
/// R4-V1 — retail <c>top_level_id</c>. Types 6 (MoveToObject), 8
|
||
/// (TurnToObject) only.
|
||
/// </summary>
|
||
public uint TopLevelId;
|
||
/// <summary>
|
||
/// R4-V1 — retail <c>pos</c> (world position + cell). Type 7
|
||
/// (MoveToPosition) only.
|
||
/// </summary>
|
||
public Position Pos;
|
||
/// <summary>
|
||
/// R4-V1 — retail <c>radius</c>. Type 6 (MoveToObject) only.
|
||
/// </summary>
|
||
public float Radius;
|
||
/// <summary>
|
||
/// R4-V1 — retail <c>height</c>. Type 6 (MoveToObject) only.
|
||
/// </summary>
|
||
public float Height;
|
||
/// <summary>
|
||
/// R4-V1 — retail <c>params</c> (a pointer in retail; a reference
|
||
/// here). Types 1-4 and 6-9.
|
||
/// </summary>
|
||
public Motion.MovementParameters? Params;
|
||
}
|
||
|
||
// ── Optional WeenieObject interface ──────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Minimal interface for the server-side WeenieObject callbacks that CMotionInterp
|
||
/// reaches through at vtable offsets +0x30, +0x34, +0x3C.
|
||
/// Allows testing without a real weenie.
|
||
/// </summary>
|
||
public interface IWeenieObject
|
||
{
|
||
/// <summary>vtable +0x30 — InqJumpVelocity. Returns true and sets vz if valid.</summary>
|
||
bool InqJumpVelocity(float extent, out float vz);
|
||
/// <summary>vtable +0x34 — InqRunRate. Returns true and sets rate if valid.</summary>
|
||
bool InqRunRate(out float rate);
|
||
/// <summary>vtable +0x3C — CanJump. Returns true if the weenie can jump at this extent.</summary>
|
||
bool CanJump(float extent);
|
||
|
||
/// <summary>
|
||
/// Retail <c>CWeenieObject::IsCreature</c>. Non-creature weenies bypass
|
||
/// the ground-contact gate in <c>contact_allows_move</c> (0x00528240),
|
||
/// the run remap in <c>adjust_motion</c> (wired R3-W4, closes J18 —
|
||
/// register TS-34 retired), and the <see cref="HitGround"/>/
|
||
/// <see cref="LeaveGround"/> creature gates. Players, NPCs, and
|
||
/// monsters are creatures — default true keeps existing implementers
|
||
/// retail-correct.
|
||
/// </summary>
|
||
bool IsCreature() => true;
|
||
|
||
/// <summary>
|
||
/// Retail <c>ACCWeenieObject::IsThePlayer</c> (vtable +0x14,
|
||
/// 0x0058C3D0): <c>this->id == SmartBox::smartbox->player_id</c>.
|
||
/// PINNED (W0-pins.md A3, adversarially verified) as the dual-dispatch
|
||
/// gate for <c>apply_current_movement</c>/<c>ReportExhaustion</c>/
|
||
/// <c>SetWeenieObject</c>/<c>SetPhysicsObject</c> — NOT <c>IsCreature</c>
|
||
/// (a remote player is a creature but not the player; ACE's server-side
|
||
/// <c>IsCreature</c> gate is a genuine divergence, not a reading to
|
||
/// copy). Default false; only the local player's weenie returns true.
|
||
/// No consumer in R3-W3 — the W4 dual-dispatch port is the first reader.
|
||
/// </summary>
|
||
bool IsThePlayer() => false;
|
||
|
||
/// <summary>
|
||
/// Retail <c>CWeenieObject::JumpStaminaCost</c> (vtable +0x44, referenced
|
||
/// from <c>jump_is_allowed</c> raw 305549-305556): given the charge
|
||
/// <paramref name="extent"/>, returns whether the weenie can afford the
|
||
/// stamina cost of this jump and writes the cost to
|
||
/// <paramref name="cost"/>. <c>jump_is_allowed</c> treats a <c>false</c>
|
||
/// return as <see cref="WeenieError.GeneralMovementFailure"/> (0x47).
|
||
/// Default true / cost 0 — real stamina gating stays TS-5-deferred (the
|
||
/// same register row that already covers <c>CanJump</c> always-true;
|
||
/// this extends it to <c>JumpStaminaCost</c> until stat plumbing lands).
|
||
/// </summary>
|
||
bool JumpStaminaCost(float extent, out int cost)
|
||
{
|
||
cost = 0;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// ── MotionInterpreter ─────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// C# port of CMotionInterp (chunk_00520000.c).
|
||
///
|
||
/// Owns the raw and interpreted motion states for a physics object and
|
||
/// translates network movement commands into PhysicsBody velocity calls.
|
||
///
|
||
/// <para>
|
||
/// <b>R3-W2 (closes J1, J17):</b> implements <see cref="IMotionDoneSink"/> —
|
||
/// the entity's <c>MotionTableManager</c> (R2-Q3) binds its animation-done
|
||
/// callback here, standing in for retail's null-guarded relay chain
|
||
/// <c>CPhysicsObj::MotionDone</c> 0x0050fdb0 → <c>MovementManager::MotionDone</c>
|
||
/// 0x005242d0 → <c>CMotionInterp::MotionDone</c> 0x00527ec0 (r3-port-plan.md
|
||
/// §4). This adds the <c>pending_motions</c> queue (retail
|
||
/// <c>CMotionInterp::pending_motions</c>, a singly-linked <c>LList</c> —
|
||
/// ported as a managed <see cref="LinkedList{T}"/> per the AD-34 managed-list
|
||
/// register wording) that the funnel's dispatch/stop producers populate and
|
||
/// <see cref="MotionDone"/>/<see cref="HandleExitWorld"/> drain.
|
||
/// </para>
|
||
/// </summary>
|
||
public sealed class MotionInterpreter : IMotionDoneSink
|
||
{
|
||
// ── animation speed constants (from ACE / confirmed by decompile globals) ─
|
||
/// <summary>Walk animation base speed. Retail-exact (.rdata 0x007c891c =
|
||
/// 3.11999989f); unified for both <see cref="get_state_velocity"/> and
|
||
/// <see cref="adjust_motion"/>'s sidestep scale in D6.2.</summary>
|
||
public const float WalkAnimSpeed = 3.11999989f;
|
||
/// <summary>Run animation base speed (_DAT_007c96e0 family).</summary>
|
||
public const float RunAnimSpeed = 4.0f;
|
||
/// <summary>Sidestep animation base speed (_DAT_007c96e8 family).</summary>
|
||
public const float SidestepAnimSpeed = 1.25f;
|
||
/// <summary>
|
||
/// Minimum jump extent before get_jump_v_z bothers computing
|
||
/// (_DAT_007c9734). R3-W3 (closes J16-epsilons, A5/A6): retail's exact
|
||
/// literal is <c>0.000199999995f</c> (raw @304959:
|
||
/// <c>((long double)0.000199999995f)</c>), NOT the previous <c>0.001</c>
|
||
/// approximation. Used by both <see cref="GetJumpVZ"/> and
|
||
/// <see cref="GetLeaveGroundVelocity"/> (three times there, one per
|
||
/// axis — A6).
|
||
/// </summary>
|
||
public const float JumpVzEpsilon = 0.000199999995f;
|
||
/// <summary>Fallback vertical jump velocity when WeenieObj is absent (_DAT_0079c6d4).</summary>
|
||
public const float DefaultJumpVz = 10.0f;
|
||
/// <summary>Maximum jump extent clamped by get_jump_v_z (_DAT_007938b0 = 1.0f).</summary>
|
||
public const float MaxJumpExtent = 1.0f;
|
||
|
||
/// <summary>
|
||
/// Backward-speed multiplier applied by <see cref="adjust_motion"/> when
|
||
/// remapping WalkBackward → WalkForward (retail .rdata 0x007c8910).
|
||
/// Retail-exact value; do not round to 0.65f.
|
||
/// </summary>
|
||
public const float BackwardsFactor = 0.649999976f;
|
||
|
||
/// <summary>
|
||
/// Turn speed multiplier applied by <see cref="apply_run_to_command"/> for
|
||
/// TurnRight when the hold key is Run (retail apply_run_to_command, no
|
||
/// run-rate scaling and no clamp — just a flat ×1.5).
|
||
/// </summary>
|
||
public const float RunTurnFactor = 1.5f;
|
||
|
||
/// <summary>
|
||
/// Symmetric clamp on sidestep interpreted speed applied by
|
||
/// <see cref="apply_run_to_command"/> for SideStepRight when running
|
||
/// (retail apply_run_to_command SideStepRight clamp, ±3.0).
|
||
/// </summary>
|
||
public const float MaxSidestepAnimRate = 3.0f;
|
||
|
||
/// <summary>
|
||
/// Retail-exact sidestep scale factor used by <see cref="adjust_motion"/>
|
||
/// when remapping to SideStepRight (retail SidestepFactor = 0.5).
|
||
/// </summary>
|
||
public const float SidestepFactor = 0.5f;
|
||
|
||
|
||
// ── fields (matching struct layout from acclient_function_map.md) ─────────
|
||
|
||
/// <summary>The physics body this interpreter controls (struct offset +0x08).</summary>
|
||
public PhysicsBody? PhysicsObj { get; set; }
|
||
|
||
/// <summary>Optional WeenieObject for stamina / run-rate queries (struct offset +0x04).</summary>
|
||
public IWeenieObject? WeenieObj { get; set; }
|
||
|
||
/// <summary>
|
||
/// Raw (network-derived) motion state (struct offsets +0x14..+0x44).
|
||
///
|
||
/// <para>
|
||
/// R3-W1 fold (closes J2): this now holds the SAME retail-faithful,
|
||
/// full-field <see cref="Physics.RawMotionState"/> type the outbound
|
||
/// wire packer reads from — the former flat <c>LegacyRawMotionState</c>
|
||
/// struct (no action FIFO, no <c>ApplyMotion</c>/<c>RemoveMotion</c>)
|
||
/// is deleted. One raw-state type end to end.
|
||
/// </para>
|
||
/// </summary>
|
||
public RawMotionState RawState;
|
||
|
||
/// <summary>Interpreted motion state derived from raw (struct offsets +0x44..+0x7C).</summary>
|
||
public InterpretedMotionState InterpretedState;
|
||
|
||
/// <summary>Jump charge accumulator — set in jump(), cleared in LeaveGround() (offset +0x74).</summary>
|
||
public float JumpExtent;
|
||
|
||
/// <summary>Stored run rate from last successful InqRunRate call (offset +0x7C).</summary>
|
||
public float MyRunRate = 1.0f;
|
||
|
||
/// <summary>
|
||
/// The physics object's currently-held movement key (retail
|
||
/// <c>this->raw_state.current_holdkey</c>). <see cref="adjust_motion"/>
|
||
/// falls back to this property when the per-channel holdKey argument
|
||
/// passed in is <see cref="HoldKey.Invalid"/>. R3-W6: an ALIAS of
|
||
/// <see cref="RawMotionState.CurrentHoldKey"/> — retail's adjust_motion
|
||
/// 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>
|
||
public HoldKey CurrentHoldKey => RawState.CurrentHoldKey;
|
||
|
||
/// <summary>True when crouching-in-place for a standing long jump (offset +0x70).</summary>
|
||
public bool StandingLongJump;
|
||
|
||
/// <summary>
|
||
/// R3-W2 — retail <c>CMotionInterp::pending_motions</c> (offset last in
|
||
/// the struct, a singly-linked <c>LList<MotionNode></c>). Ported as
|
||
/// a managed <see cref="LinkedList{T}"/> — the AD-34 register wording
|
||
/// ("retail's intrusive DLList/LList is ported as a managed LinkedList of
|
||
/// value nodes; node identity semantics are preserved via
|
||
/// LinkedListNode<T> references rather than raw prev/next
|
||
/// pointers") applies here exactly as it does to
|
||
/// <c>MotionTableManager._pendingAnimations</c> — this is the OTHER,
|
||
/// never-merged queue (movement side, waiting for a MotionDone callback,
|
||
/// vs the manager's animation side, waiting for a tick countdown; see
|
||
/// r3-port-plan.md §4 rule 1). Exposed read-only; mutated only through
|
||
/// <see cref="AddToQueue"/> / <see cref="MotionDone"/> /
|
||
/// <see cref="HandleExitWorld"/>.
|
||
/// </summary>
|
||
private readonly LinkedList<MotionNode> _pendingMotions = new();
|
||
|
||
/// <summary>Read-only inspection surface (tests + future callers): the
|
||
/// pending motion queue in head-to-tail order.</summary>
|
||
public IEnumerable<MotionNode> PendingMotions => _pendingMotions;
|
||
|
||
/// <summary>
|
||
/// R3-W2 no-op seam standing in for retail <c>CPhysicsObj::unstick_from_object</c>
|
||
/// (called from <see cref="MotionDone"/>/<see cref="HandleExitWorld"/> when
|
||
/// the popped queue head is action-class, i.e. <c>Motion & 0x10000000</c>
|
||
/// is set). Register row: releases a "stuck to object" sticky-manager
|
||
/// attachment — R5 wires the real StickyManager; until then this is an
|
||
/// optional callback the App layer may bind, matching the existing
|
||
/// <c>Action?</c> seam convention (see <c>MotionTableDispatchSink.TurnStopped</c>).
|
||
/// </summary>
|
||
public Action? UnstickFromObject { get; set; }
|
||
|
||
/// <summary>
|
||
/// R3-W3 no-op seam standing in for retail <c>CPhysicsObj::interrupt_current_movement</c>
|
||
/// (called unconditionally at the top of <see cref="jump"/>, raw @305800,
|
||
/// and by <see cref="StopCompletely"/> per A9). Register row: retail
|
||
/// cancels any in-flight MoveToManager transition before starting a new
|
||
/// movement action; R4 wires the real <c>cancel_moveto</c> once
|
||
/// MoveToManager exists. Until then this is an optional callback the App
|
||
/// layer may bind, matching the <see cref="UnstickFromObject"/> seam
|
||
/// convention.
|
||
/// </summary>
|
||
public Action? InterruptCurrentMovement { get; set; }
|
||
|
||
/// <summary>
|
||
/// R3-W4 seam standing in for retail <c>CPhysicsObj::RemoveLinkAnimations</c>
|
||
/// (0x0050fe20, decomp §9 summary table) → <c>CPartArray</c> →
|
||
/// <c>CSequence::remove_all_link_animations</c> — called from
|
||
/// <see cref="HitGround"/>/<see cref="LeaveGround"/> (§4a/4b) whenever
|
||
/// the ground-transition body actually runs. This is retail's REAL
|
||
/// mechanism for "leaving the ground strips any pending link
|
||
/// animation so Falling engages instantly" — the K-fix18 App-side
|
||
/// forced-<c>SetCycle(skipTransitionLink:true)</c> hack this seam
|
||
/// replaces is deleted in the orchestrator's App-side half (register
|
||
/// row retirement rides with that deletion, not this commit — Core
|
||
/// has no K-fix18 code to remove). The App layer binds this to the
|
||
/// entity's <c>AnimationSequencer</c>/<c>CSequence</c> core
|
||
/// (<c>RemoveAllLinkAnimations</c>-equivalent) per entity, matching the
|
||
/// existing <see cref="UnstickFromObject"/>/<see cref="InterruptCurrentMovement"/>
|
||
/// <c>Action?</c> seam convention. Optional — a null binding is a
|
||
/// silent no-op (safe default for tests and for any physics body with
|
||
/// no sequencer attached yet).
|
||
/// </summary>
|
||
public Action? RemoveLinkAnimations { get; set; }
|
||
|
||
/// <summary>
|
||
/// R3-W4 no-op seam standing in for retail
|
||
/// <c>CPhysicsObj::InitializeMotionTables</c>, called from
|
||
/// <see cref="EnterDefaultState"/> (§4d, raw @306124:
|
||
/// <c>CPhysicsObj::InitializeMotionTables(this->physics_obj);</c>).
|
||
/// This re-seeds the physics object's <c>MotionTableManager</c>/motion
|
||
/// table stack — a manager-side (App-bound) concern, not something
|
||
/// <c>MotionInterpreter</c> itself owns state for. Register row: no-op
|
||
/// until the App layer binds it to the entity's motion-table
|
||
/// initialization; matches the <see cref="RemoveLinkAnimations"/> seam
|
||
/// convention.
|
||
/// </summary>
|
||
public Action? InitializeMotionTables { get; set; }
|
||
|
||
/// <summary>
|
||
/// R3-W5 seam standing in for retail <c>CPhysicsObj::CheckForCompletedMotions</c>
|
||
/// (0x0050fe30, decomp §7d @277925) → <c>CPartArray::CheckForCompletedMotions</c>
|
||
/// → <c>MotionTableManager::CheckForCompletedMotions</c> (the SECOND call
|
||
/// site of the shared animation-completion driver, §7b) — called by
|
||
/// <see cref="PerformMovement"/> after EVERY dispatched op (raw
|
||
/// 306234/306241/306248/306255/306262, §2/§7d), synchronously draining
|
||
/// any already-elapsed (0-duration) animation-table node right after the
|
||
/// motion is applied, in case the newly-applied motion table entry
|
||
/// completes immediately. The App layer binds this to the entity's
|
||
/// <c>MotionTableManager.CheckForCompletedMotions</c> (R2-Q3 object),
|
||
/// matching the existing <see cref="RemoveLinkAnimations"/>/
|
||
/// <see cref="UnstickFromObject"/> <c>Action?</c> seam convention. A null
|
||
/// binding is a silent no-op (safe default for tests and for any physics
|
||
/// body with no MotionTableManager attached yet).
|
||
/// </summary>
|
||
public Action? CheckForCompletedMotions { get; set; }
|
||
|
||
/// <summary>
|
||
/// R3-W4 — retail <c>CMotionInterp::initted</c> (set by
|
||
/// <see cref="EnterDefaultState"/>, raw @306152: <c>this->initted =
|
||
/// 1;</c>). Gates <see cref="apply_current_movement"/> and
|
||
/// <see cref="ReportExhaustion"/>'s entry (A3: <c>physics_obj != 0
|
||
/// && initted != 0</c>).
|
||
///
|
||
/// <para>
|
||
/// <b>Constructor default is <c>true</c>, NOT retail's <c>false</c>
|
||
/// (see the final-report note on this divergence).</b> Retail's
|
||
/// <c>CMotionInterp</c> is never used standalone — every real
|
||
/// construction path (<c>MovementManager::PerformMovement</c>'s
|
||
/// lazy-create, <c>MovementManager::Create</c>) immediately calls
|
||
/// <see cref="EnterDefaultState"/> (retail's <c>enter_default_state</c>)
|
||
/// before the interpreter is exposed to any caller, so <c>initted</c>
|
||
/// is always <c>1</c> by the time application code can observe it.
|
||
/// acdream's <c>MotionInterpreter</c> constructor is used directly by
|
||
/// ~40 pre-existing tests AND by both App call sites
|
||
/// (<c>PlayerMovementController</c>, <c>GameWindow</c>) as a
|
||
/// complete, immediately-usable object with no separate
|
||
/// "enter default state" step — porting retail's literal
|
||
/// <c>false</c> default would silently no-op every one of those
|
||
/// call sites' <c>apply_current_movement</c>/<c>ReportExhaustion</c>
|
||
/// calls until something remembers to call
|
||
/// <see cref="EnterDefaultState"/> (which nothing currently does).
|
||
/// Defaulting <c>true</c> here is the C# equivalent of "the
|
||
/// constructor already did what <c>enter_default_state</c> would have
|
||
/// done to this flag" — <see cref="EnterDefaultState"/> remains the
|
||
/// verbatim, fully-testable retail method for callers (tests, and a
|
||
/// future App reset-to-idle path) that need the REST of its behavior
|
||
/// (state reset, sentinel enqueue, <see cref="LeaveGround"/> tail).
|
||
/// </para>
|
||
/// </summary>
|
||
public bool Initted { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// Optional accessor for the owning entity's current animation cycle
|
||
/// velocity (AnimationSequencer.CurrentVelocity, i.e. MotionData.Velocity
|
||
/// scaled by speedMod). When wired, <see cref="get_state_velocity"/>
|
||
/// uses it as the primary forward-axis drive instead of the hardcoded
|
||
/// <see cref="RunAnimSpeed"/> / <see cref="WalkAnimSpeed"/> constants.
|
||
///
|
||
/// <para>
|
||
/// <b>Why:</b> the decompiled <c>get_state_velocity</c> (FUN_00528960)
|
||
/// literally computes <c>RunAnimSpeed * ForwardSpeed</c>. That works in
|
||
/// retail because retail's Humanoid MotionTable happens to bake
|
||
/// <c>MotionData.Velocity == RunAnimSpeed (4.0)</c> for the RunForward
|
||
/// cycle — so the constant and the dat data agree. For MotionTables
|
||
/// where they disagree (other creatures; swapped weapon-style cycles;
|
||
/// modded dats), the constant causes the body's world velocity to
|
||
/// drift away from the animation's baked-in root-motion velocity,
|
||
/// producing the classic "legs cycle too slowly for how fast the body
|
||
/// is sliding" visual bug.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Per <c>docs/research/deepdives/r03-motion-animation.md</c> §1.3,
|
||
/// the retail animation pipeline treats <c>MotionData.Velocity *
|
||
/// speedMod</c> as the canonical per-cycle world velocity. The
|
||
/// <see cref="RunAnimSpeed"/> constant survives in our port only as
|
||
/// the max-speed clamp (see below), which matches the decompile's
|
||
/// <c>if (|velocity| > RunAnimSpeed * rate)</c> guard.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Call site: <c>PlayerMovementController.AttachCycleVelocityAccessor</c>
|
||
/// wires this to <c>AnimatedEntity.Sequencer.CurrentVelocity</c> once
|
||
/// the player's sequencer is built. Null = fall back to the decompiled
|
||
/// constant-based path (used by tests and by any physics body with
|
||
/// no sequencer).
|
||
/// </para>
|
||
/// </summary>
|
||
public Func<Vector3>? GetCycleVelocity { get; set; }
|
||
|
||
// ── constructor ────────────────────────────────────────────────────────────
|
||
|
||
public MotionInterpreter()
|
||
{
|
||
RawState = new RawMotionState();
|
||
InterpretedState = InterpretedMotionState.Default();
|
||
}
|
||
|
||
public MotionInterpreter(PhysicsBody physicsObj, IWeenieObject? weenieObj = null)
|
||
{
|
||
PhysicsObj = physicsObj;
|
||
WeenieObj = weenieObj;
|
||
RawState = new RawMotionState();
|
||
InterpretedState = InterpretedMotionState.Default();
|
||
}
|
||
|
||
// ── FUN_00529a90 — PerformMovement ────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Top-level dispatcher for a network movement struct.
|
||
///
|
||
/// Decompiled logic (FUN_00529a90):
|
||
/// switch(*param_1) { ← MovementStruct.Type
|
||
/// case 1: DoMotion(…) → raw command
|
||
/// case 2: DoInterpretedMotion(…)
|
||
/// case 3: StopMotion(…)
|
||
/// case 4: StopInterpretedMotion(…)
|
||
/// case 5: StopCompletely()
|
||
/// default: return 0x47
|
||
/// }
|
||
/// FUN_0050fe30 — CPhysicsObj::CheckForCompletedMotions, called after EVERY
|
||
/// dispatched op (raw 306234/306241/306248/306255/306262, R3-W5 closes J14).
|
||
/// </summary>
|
||
public WeenieError PerformMovement(MovementStruct mvs)
|
||
{
|
||
var p = new MovementParameters
|
||
{
|
||
Speed = mvs.Speed,
|
||
ModifyInterpretedState = mvs.ModifyInterpretedState,
|
||
ModifyRawState = mvs.ModifyRawState,
|
||
};
|
||
|
||
bool dispatched = true;
|
||
WeenieError result = mvs.Type switch
|
||
{
|
||
MovementType.RawCommand => DoMotion(mvs.Motion, p),
|
||
MovementType.InterpretedCommand => DoInterpretedMotion(mvs.Motion, p),
|
||
MovementType.StopRawCommand => StopMotion(mvs.Motion, p),
|
||
MovementType.StopInterpretedCommand => StopInterpretedMotion(mvs.Motion, p),
|
||
MovementType.StopCompletely => StopCompletely(),
|
||
_ => Invalid(out dispatched),
|
||
};
|
||
|
||
// R3-W5 (closes J14): CPhysicsObj::CheckForCompletedMotions fires
|
||
// after EVERY dispatched op — a synchronous zero-tick flush so a
|
||
// newly-applied motion-table entry that completes IMMEDIATELY
|
||
// (0-duration node) gets its MotionDone callback in the same call,
|
||
// not on the next tick. The type-dispatch failure path (bad
|
||
// MovementStruct.type, 0x47) never reaches a dispatch, so it must
|
||
// NOT flush — matches retail's raw 306227 early-return before the
|
||
// switch even begins.
|
||
if (dispatched)
|
||
CheckForCompletedMotions?.Invoke();
|
||
|
||
return result;
|
||
|
||
static WeenieError Invalid(out bool dispatched)
|
||
{
|
||
dispatched = false;
|
||
return WeenieError.GeneralMovementFailure;
|
||
}
|
||
}
|
||
|
||
// ── FUN_00529930 — DoMotion ───────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::DoMotion</c> (0x00528d20, decomp §2 @306159, FULL
|
||
/// BODY, R3-W5, closes J3). App-facing 2-arg compat overload —
|
||
/// PlayerMovementController's call sites pass a raw
|
||
/// <c>(uint motion, float speed)</c> pair; this builds a
|
||
/// default-constructed <see cref="MovementParameters"/> (retail's own
|
||
/// UnPack/CommandInterpreter callers build one the same way for a bare
|
||
/// raw command) with <see cref="MovementParameters.Speed"/> overridden,
|
||
/// then delegates to the verbatim overload below.
|
||
/// </summary>
|
||
public WeenieError DoMotion(uint motion, float speed = 1.0f)
|
||
=> DoMotion(motion, new MovementParameters { Speed = speed });
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::DoMotion</c> (0x00528d20, decomp §2 @306159, FULL
|
||
/// BODY, R3-W5, closes J3).
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 306159-306221):
|
||
/// <code>
|
||
/// if (physics_obj == 0) return 8;
|
||
/// ebp = arg2; // ORIGINAL motion id, unmutated
|
||
/// snapshot every MovementParameters field onto the stack;
|
||
/// var_2c = fresh local MovementParameters(); // re-default
|
||
/// if (bitfield sign bit) interrupt_current_movement(physics_obj);
|
||
/// if (bitfield & 0x800 /*SetHoldKey*/)
|
||
/// SetHoldKey(hold_key_to_apply, (bitfield>>0xf)&1 /*cancel_moveto bit, A4*/);
|
||
/// adjust_motion(&arg2, &speed, hold_key_to_apply); // mutates arg2/speed in place
|
||
/// if (interpreted_state.current_style != 0x8000003d) { // combat/special stance
|
||
/// if (ebp == 0x41000012) return 0x3f;
|
||
/// if (ebp == 0x41000013) return 0x40;
|
||
/// if (ebp == 0x41000014) return 0x41;
|
||
/// if (ebp & 0x2000000) return 0x42;
|
||
/// }
|
||
/// if ((ebp & 0x10000000) && GetNumActions() >= 6) return 0x45;
|
||
/// result = DoInterpretedMotion(arg2 /*ADJUSTED*/, &var_2c /*fresh local params*/);
|
||
/// if (result == 0 && bitfield & 0x2000 /*ModifyRawState*/)
|
||
/// RawMotionState::ApplyMotion(ebp /*ORIGINAL*/, arg3 /*CALLER's params, not var_2c*/);
|
||
/// return result;
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Two mirror-discipline details, both verbatim: (1) the
|
||
/// <see cref="DoInterpretedMotion(uint,MovementParameters)"/> call
|
||
/// receives a FRESH local <see cref="MovementParameters"/> (re-defaulted,
|
||
/// only <c>Speed</c>/<c>HoldKeyToApply</c> carried through
|
||
/// <c>adjust_motion</c>'s mutation) — the caller's own
|
||
/// <paramref name="p"/> distance/heading/fail-distance fields are
|
||
/// discarded for the interpreted call, matching retail's explicit
|
||
/// re-construction of <c>var_2c</c>. (2) the raw-state mirror at the
|
||
/// bottom reads the ORIGINAL <c>arg3</c> (<paramref name="p"/>) — NOT
|
||
/// <c>var_2c</c> — for <see cref="MovementParameters.SetHoldKey"/>/
|
||
/// <see cref="MovementParameters.HoldKeyToApply"/>/<see cref="MovementParameters.Speed"/>,
|
||
/// per <c>RawMotionState::ApplyMotion</c>'s own signature
|
||
/// (<c>const MovementParameters*</c>) receiving the same pointer
|
||
/// <c>DoMotion</c> was called with.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="motion">Retail <c>arg2</c> (the ORIGINAL, pre-adjustment
|
||
/// motion id — <c>ebp</c>).</param>
|
||
/// <param name="p">Retail <c>arg3</c>.</param>
|
||
public WeenieError DoMotion(uint motion, MovementParameters p)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return WeenieError.NoPhysicsObject;
|
||
|
||
uint originalMotion = motion;
|
||
float speed = p.Speed;
|
||
var local = new MovementParameters(); // var_2c — fresh re-default
|
||
|
||
if (p.CancelMoveTo) // bitfield high-byte sign bit
|
||
InterruptCurrentMovement?.Invoke();
|
||
|
||
if (p.SetHoldKey) // bitfield & 0x800
|
||
SetHoldKey(p.HoldKeyToApply, p.CancelMoveTo); // A4: 2nd arg IS the cancel_moveto bit
|
||
|
||
adjust_motion(ref motion, ref speed, p.HoldKeyToApply); // mutates motion/speed in place
|
||
|
||
if (InterpretedState.CurrentStyle != 0x8000003du) // not MotionStance_NonCombat
|
||
{
|
||
if (originalMotion == MotionCommand.Crouch)
|
||
return WeenieError.CrouchInCombatStance; // 0x3f
|
||
if (originalMotion == MotionCommand.Sitting)
|
||
return WeenieError.SitInCombatStance; // 0x40
|
||
if (originalMotion == MotionCommand.Sleeping)
|
||
return WeenieError.SleepInCombatStance; // 0x41
|
||
if ((originalMotion & 0x2000000u) != 0)
|
||
return WeenieError.ChatEmoteOutsideNonCombat; // 0x42
|
||
}
|
||
|
||
if ((originalMotion & 0x10000000u) != 0 && InterpretedState.GetNumActions() >= 6)
|
||
return WeenieError.ActionDepthExceeded; // 0x45
|
||
|
||
local.Speed = speed;
|
||
local.HoldKeyToApply = p.HoldKeyToApply;
|
||
|
||
WeenieError result = DoInterpretedMotion(motion, local); // ADJUSTED id, fresh local params
|
||
|
||
if (result == WeenieError.None && p.ModifyRawState) // bitfield & 0x2000
|
||
RawState.ApplyMotion(originalMotion, p); // ORIGINAL id, CALLER's params (arg3, not var_2c)
|
||
|
||
return result;
|
||
}
|
||
|
||
// ── StopMotion ────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// App-facing compat overload for <c>StopMotion</c> (0x00528530, decomp
|
||
/// §5b @305674). Builds a default-constructed <see cref="MovementParameters"/>
|
||
/// and delegates to the verbatim overload below.
|
||
/// </summary>
|
||
public WeenieError StopMotion(uint motion)
|
||
=> StopMotion(motion, new MovementParameters());
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::StopMotion</c> (0x00528530, decomp §5b @305674, FULL
|
||
/// BODY, R3-W5, closes J3's StopMotion leg). The <c>DoMotion</c> mirror:
|
||
/// same defaulting/reconstruction pattern, same optional
|
||
/// <c>interrupt_current_movement</c>, same <c>adjust_motion</c>
|
||
/// reinterpretation — but NO combat-stance or action-depth gating (those
|
||
/// are <c>DoMotion</c>-only).
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305674-305706): snapshot <paramref name="p"/> fields,
|
||
/// build a fresh local <see cref="MovementParameters"/>, optional
|
||
/// interrupt on the sign bit, <c>adjust_motion(&arg3, &speed,
|
||
/// hold_key_to_apply)</c> (mutates the motion id in place — note retail
|
||
/// reuses the <c>arg3</c> register slot for the motion id here, not a
|
||
/// separate local), delegate to
|
||
/// <c>StopInterpretedMotion(adjusted, &local)</c>; on success AND
|
||
/// bit <c>0x2000</c> (ModifyRawState), mirror via
|
||
/// <c>RawMotionState::RemoveMotion(ORIGINAL motion)</c> — using the
|
||
/// ORIGINAL unmutated id, matching <c>DoMotion</c>'s equivalent
|
||
/// <c>ApplyMotion(ebp, arg3)</c> mirror discipline.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="motion">Retail <c>arg2</c> (the ORIGINAL motion id).</param>
|
||
/// <param name="p">Retail <c>arg3</c>.</param>
|
||
public WeenieError StopMotion(uint motion, MovementParameters p)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return WeenieError.NoPhysicsObject;
|
||
|
||
uint originalMotion = motion;
|
||
float speed = p.Speed;
|
||
var local = new MovementParameters(); // fresh re-default
|
||
|
||
if (p.CancelMoveTo)
|
||
InterruptCurrentMovement?.Invoke();
|
||
|
||
adjust_motion(ref motion, ref speed, p.HoldKeyToApply);
|
||
|
||
local.Speed = speed;
|
||
local.HoldKeyToApply = p.HoldKeyToApply;
|
||
|
||
WeenieError result = StopInterpretedMotion(motion, local); // ADJUSTED id
|
||
|
||
if (result == WeenieError.None && p.ModifyRawState)
|
||
RawState.RemoveMotion(originalMotion); // ORIGINAL id
|
||
|
||
return result;
|
||
}
|
||
|
||
// ── FUN_00527e40 — StopCompletely ─────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::StopCompletely</c> (0x00527e40, decomp §5a @305208,
|
||
/// FULL BODY, R3-W5, closes J9). W0-pins.md A9 — ported INCLUDING the
|
||
/// jump-snapshot quirk.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305208-305236):
|
||
/// <code>
|
||
/// if (physics_obj == 0) return 8;
|
||
/// interrupt_current_movement(physics_obj);
|
||
/// eax_2 = motion_allows_jump(interpreted_state.forward_command); // OLD fwd, BEFORE overwrite
|
||
/// raw_state.forward_command = 0x41000003; raw_state.forward_speed = 1f;
|
||
/// raw_state.sidestep_command = 0; raw_state.turn_command = 0;
|
||
/// interpreted_state.forward_command = 0x41000003; interpreted_state.forward_speed = 1f;
|
||
/// interpreted_state.sidestep_command = 0; interpreted_state.turn_command = 0;
|
||
/// StopCompletely_Internal(physics_obj);
|
||
/// add_to_queue(0, 0x41000003, eax_2); // eax_2 = the PRE-overwrite snapshot
|
||
/// if (physics_obj != 0 && physics_obj->cell == 0) RemoveLinkAnimations(physics_obj);
|
||
/// return 0;
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>A9 quirk</b>: <c>motion_allows_jump</c> is evaluated on the OLD
|
||
/// <c>interpreted_state.forward_command</c> BEFORE it gets overwritten
|
||
/// two lines later — the queued node's <c>JumpErrorCode</c> reflects the
|
||
/// PRE-stop command, not the post-stop Ready command (which would always
|
||
/// pass). <b>J9</b>: retail touches ONLY the forward command/speed plus
|
||
/// the sidestep/turn COMMANDS — it does NOT write <c>sidestep_speed</c>
|
||
/// or <c>turn_speed</c> on either state. The pre-R3 acdream divergence
|
||
/// (unconditional 1.0 speed resets on all four speed fields) is REMOVED
|
||
/// here.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <c>StopCompletely_Internal</c> (0x0050f5a0, CPhysicsObj-side) stands
|
||
/// in as <see cref="PhysicsBody.set_velocity"/>(Zero) — register row,
|
||
/// kept (full port is physics-layer scope, r3-port-plan.md §2 KEEP
|
||
/// LIST). CurCell proxy: <see cref="PhysicsBody.CellPosition"/>.<c>ObjCellId
|
||
/// == 0</c> (unseeded/detached body — matches <c>cell == 0</c>).
|
||
/// </para>
|
||
/// </summary>
|
||
public WeenieError StopCompletely()
|
||
{
|
||
if (PhysicsObj is null)
|
||
return WeenieError.NoPhysicsObject;
|
||
|
||
InterruptCurrentMovement?.Invoke();
|
||
|
||
// A9: snapshot BEFORE the overwrite below.
|
||
WeenieError jumpSnapshot = MotionAllowsJump(InterpretedState.ForwardCommand);
|
||
|
||
// J9: forward cmd/speed + sidestep/turn COMMANDS only — speeds untouched.
|
||
RawState.ForwardCommand = MotionCommand.Ready;
|
||
RawState.ForwardSpeed = 1.0f;
|
||
RawState.SidestepCommand = 0;
|
||
RawState.TurnCommand = 0;
|
||
|
||
InterpretedState.ForwardCommand = MotionCommand.Ready;
|
||
InterpretedState.ForwardSpeed = 1.0f;
|
||
InterpretedState.SideStepCommand = 0;
|
||
InterpretedState.TurnCommand = 0;
|
||
|
||
// StopCompletely_Internal — R4-V5 wedge-fix CORRECTION of the R3
|
||
// misread: retail's body (0x0050ead0, tailcall
|
||
// CPartArray::StopCompletelyInternal 0x00518890) is
|
||
// MotionTableManager::PerformMovement(type 5) through the ANIMATION
|
||
// stack — NOT a physics-side velocity zero (the "0x0050f5a0" the R3
|
||
// note cited is a different function's interior). The manager
|
||
// queues its type-5 pending_animations entry UNCONDITIONALLY, and
|
||
// that entry's AnimationDone → MotionDone is the matched pop for
|
||
// the A9 pending_motions node queued below. Without this dispatch
|
||
// every StopCompletely (login/teleport SetPosition + every
|
||
// MoveToManager PerformMovement head) left an orphan node,
|
||
// MotionsPending never drained at idle, and BeginTurnToHeading's
|
||
// wait-for-anims gate wedged every armed moveto (the 2026-07-03
|
||
// live door bug — server walked the player, the local body never
|
||
// moved, rubber-band on the next input).
|
||
DefaultSink?.StopCompletely();
|
||
|
||
// Immediate velocity zero: kept from the R3 stand-in. Retail
|
||
// reaches zero through the Ready state's next velocity apply;
|
||
// acdream's controller altitude performs that apply in its grounded
|
||
// section-2 write, so the immediate zero preserves the established
|
||
// teleport/stop behavior for same-frame readers.
|
||
PhysicsObj.set_velocity(Vector3.Zero);
|
||
|
||
AddToQueue(contextId: 0, MotionCommand.Ready, (uint)jumpSnapshot);
|
||
|
||
// R4-V5 door-swing fix (2026-07-03): retail's guard is
|
||
// physics_obj->cell == 0 — DETACHED objects only (raw @305627).
|
||
// The old proxy (CellPosition.ObjCellId == 0, #145 machinery seeded
|
||
// only by the local player's SnapToCell) read every REMOTE body as
|
||
// detached, so every dispatch stripped the just-appended transition
|
||
// link (door swings snapped; remote walk↔run links died) — see
|
||
// PhysicsBody.InWorld (register row).
|
||
if (!PhysicsObj.InWorld)
|
||
RemoveLinkAnimations?.Invoke();
|
||
|
||
return WeenieError.None;
|
||
}
|
||
|
||
// ── FUN_00528960 — get_state_velocity ────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Compute the body-local velocity vector for the current interpreted motion.
|
||
///
|
||
/// <para>
|
||
/// <b>Decompiled path (FUN_00528960)</b>:
|
||
/// <code>
|
||
/// velocity = (0, 0, 0)
|
||
/// if InterpretedState.SideStepCommand == 0x6500000F:
|
||
/// velocity.X = _DAT_007c96e8 * InterpretedState.SideStepSpeed
|
||
/// = SidestepAnimSpeed * SideStepSpeed
|
||
/// if InterpretedState.ForwardCommand == 0x45000005 (WalkForward):
|
||
/// velocity.Y = _DAT_007c96e4 * InterpretedState.ForwardSpeed
|
||
/// = WalkAnimSpeed * ForwardSpeed
|
||
/// elif InterpretedState.ForwardCommand == 0x44000007 (RunForward):
|
||
/// velocity.Y = _DAT_007c96e0 * InterpretedState.ForwardSpeed
|
||
/// = RunAnimSpeed * ForwardSpeed
|
||
/// rate = InqRunRate() or MyRunRate
|
||
/// maxSpeed = RunAnimSpeed * rate
|
||
/// if |velocity| > maxSpeed: velocity = normalize(velocity) * maxSpeed
|
||
/// return velocity
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>Option B — MotionData-sourced forward velocity</b>:
|
||
/// when <see cref="GetCycleVelocity"/> is wired (i.e. the owning
|
||
/// entity has an AnimationSequencer), we prefer
|
||
/// <c>MotionData.Velocity.Y * speedMod</c> over the hardcoded
|
||
/// <see cref="WalkAnimSpeed"/> / <see cref="RunAnimSpeed"/> constants.
|
||
/// This keeps the body's world velocity locked to the animation's
|
||
/// baked-in root-motion velocity (<c>r03 §1.3</c>), so the
|
||
/// legs-per-meter ratio is invariant regardless of which motion table
|
||
/// drives the character. The decompiled constant was a
|
||
/// MotionTable-specific value that happens to equal the Humanoid
|
||
/// RunForward MotionData.Velocity — fine as a fallback, but the dat
|
||
/// is the ground truth for any non-humanoid creature.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// The <see cref="RunAnimSpeed"/> constant survives as the max-speed
|
||
/// clamp at the bottom, faithfully matching the decompile's
|
||
/// <c>if (|velocity| > RunAnimSpeed * rate)</c> guard. Sidestep
|
||
/// continues to use <see cref="SidestepAnimSpeed"/> because the
|
||
/// sequencer only tracks the current forward cycle — strafe is
|
||
/// implemented as a separate axis in our controller (see
|
||
/// <c>PlayerMovementController.Update</c>).
|
||
/// </para>
|
||
/// </summary>
|
||
public Vector3 get_state_velocity()
|
||
{
|
||
var velocity = Vector3.Zero;
|
||
|
||
if (InterpretedState.SideStepCommand == MotionCommand.SideStepRight)
|
||
velocity.X = SidestepAnimSpeed * InterpretedState.SideStepSpeed;
|
||
|
||
// Forward axis — prefer sequencer's current cycle velocity when available.
|
||
// Sequencer's CurrentVelocity is already `MotionData.Velocity * speedMod`
|
||
// where speedMod == ForwardSpeed for locomotion cycles, so we use it as-is
|
||
// (no additional ForwardSpeed multiplication). Fall back to the decompiled
|
||
// constant-based path when the accessor is unwired or returns zero Y
|
||
// (e.g. during zero-velocity link transitions — in which case the constant
|
||
// is the safe default to keep physics moving at ForwardSpeed).
|
||
Vector3? cycleVel = GetCycleVelocity?.Invoke();
|
||
bool haveCycleForward = cycleVel.HasValue
|
||
&& MathF.Abs(cycleVel.Value.Y) > float.Epsilon;
|
||
|
||
if (InterpretedState.ForwardCommand == MotionCommand.WalkForward)
|
||
{
|
||
velocity.Y = haveCycleForward
|
||
? cycleVel!.Value.Y
|
||
: WalkAnimSpeed * InterpretedState.ForwardSpeed;
|
||
}
|
||
else if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
|
||
{
|
||
velocity.Y = haveCycleForward
|
||
? cycleVel!.Value.Y
|
||
: RunAnimSpeed * InterpretedState.ForwardSpeed;
|
||
}
|
||
|
||
// Determine the current run rate via WeenieObj or fall back to MyRunRate.
|
||
// Decompile: calls vtable+0x34 (InqRunRate).
|
||
float rate = MyRunRate;
|
||
if (WeenieObj is not null)
|
||
{
|
||
if (WeenieObj.InqRunRate(out float queried))
|
||
rate = queried;
|
||
// else: rate stays MyRunRate
|
||
}
|
||
|
||
float maxSpeed = RunAnimSpeed * rate;
|
||
float len = velocity.Length();
|
||
if (len > maxSpeed && len > 0f)
|
||
{
|
||
velocity = Vector3.Normalize(velocity) * maxSpeed;
|
||
}
|
||
|
||
return velocity;
|
||
}
|
||
|
||
// ── FUN_00528010 — adjust_motion ──────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Normalize one raw motion channel (forward, sidestep, or turn) into its
|
||
/// canonical interpreted form: fold the "negative" commands (WalkBackward,
|
||
/// TurnLeft, SideStepLeft) into their positive counterpart with a negated/
|
||
/// scaled speed, then apply the Run hold-key promotion.
|
||
///
|
||
/// <para>
|
||
/// Decompiled logic (FUN_00528010, <c>docs/research/named-retail/
|
||
/// acclient_2013_pseudo_c.txt:305343-305400</c>):
|
||
/// <code>
|
||
/// if weenie != null and !weenie.IsCreature(): return
|
||
/// switch cmd:
|
||
/// RunForward: return // no scale, NO holdkey path
|
||
/// WalkBackwards: cmd = WalkForward; speed *= -BackwardsFactor
|
||
/// TurnLeft: cmd = TurnRight; speed *= -1
|
||
/// SideStepLeft: cmd = SideStepRight; speed *= -1
|
||
/// if cmd == SideStepRight:
|
||
/// speed *= SidestepFactor * (WalkAnimSpeed / SidestepAnimSpeed) // ≈ 1.24799995
|
||
/// if holdKey == Invalid: holdKey = raw.current_holdkey
|
||
/// if holdKey == Run: apply_run_to_command(ref cmd, ref speed)
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// GOTCHA: RunForward early-returns before the sidestep scale AND before
|
||
/// the hold-key path — a running forward command is already normalized
|
||
/// and must not be touched again. The sidestep negate (SideStepLeft →
|
||
/// SideStepRight, ×-1) happens BEFORE the ×1.248 anim-rate scale, so the
|
||
/// net multiplier for SideStepLeft is -1.248×speed, not -1×(1.248×speed)
|
||
/// applied out of order (same result here, but the ORDER matters once
|
||
/// apply_run_to_command's sign-dependent clamp is layered on top).
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>R3-W4 (closes J18, retires register TS-34): creature guard WIRED.</b>
|
||
/// Retail (raw @305343): <c>if (weenie != 0 && weenie->IsCreature() == 0)
|
||
/// return;</c> — a weenie present whose <c>IsCreature()</c> returns
|
||
/// false short-circuits the ENTIRE function (no normalization, no
|
||
/// hold-key path). <see cref="IWeenieObject.IsCreature"/> has existed
|
||
/// since W3; this was the one caller left un-gated.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="motion">Raw motion command, normalized in place.</param>
|
||
/// <param name="speed">Raw motion speed, normalized in place.</param>
|
||
/// <param name="holdKey">
|
||
/// The hold key for THIS channel (forward/sidestep/turn each carry their
|
||
/// own hold key in the retail raw state). Pass <see cref="HoldKey.Invalid"/>
|
||
/// to fall back to <see cref="CurrentHoldKey"/>.
|
||
/// </param>
|
||
public void adjust_motion(ref uint motion, ref float speed, HoldKey holdKey)
|
||
{
|
||
if (WeenieObj is not null && !WeenieObj.IsCreature())
|
||
return;
|
||
|
||
switch (motion)
|
||
{
|
||
case MotionCommand.RunForward:
|
||
// Already normalized; no scale, no hold-key path.
|
||
return;
|
||
case MotionCommand.WalkBackward:
|
||
motion = MotionCommand.WalkForward;
|
||
speed *= -BackwardsFactor;
|
||
break;
|
||
case MotionCommand.TurnLeft:
|
||
motion = MotionCommand.TurnRight;
|
||
speed *= -1f;
|
||
break;
|
||
case MotionCommand.SideStepLeft:
|
||
motion = MotionCommand.SideStepRight;
|
||
speed *= -1f;
|
||
break;
|
||
}
|
||
|
||
if (motion == MotionCommand.SideStepRight)
|
||
speed *= SidestepFactor * (WalkAnimSpeed / SidestepAnimSpeed);
|
||
|
||
if (holdKey == HoldKey.Invalid)
|
||
holdKey = CurrentHoldKey;
|
||
|
||
if (holdKey == HoldKey.Run)
|
||
apply_run_to_command(ref motion, ref speed);
|
||
}
|
||
|
||
// ── FUN_00527be0 — apply_run_to_command ───────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Apply the Run hold-key promotion/scale to an already-normalized
|
||
/// (positive-form) motion command.
|
||
///
|
||
/// <para>
|
||
/// Decompiled logic (FUN_00527be0, <c>docs/research/named-retail/
|
||
/// acclient_2013_pseudo_c.txt:305062-305123</c>):
|
||
/// <code>
|
||
/// speedMod = 1.0
|
||
/// if weenie != null: speedMod = weenie.InqRunRate(out r) ? r : MyRunRate
|
||
/// switch cmd:
|
||
/// WalkForward:
|
||
/// if speed > 0: cmd = RunForward // promote ONLY when moving forward
|
||
/// speed *= speedMod // UNCONDITIONAL — applies to backward too
|
||
/// TurnRight:
|
||
/// speed *= RunTurnFactor // ×1.5, no run-rate, no clamp
|
||
/// SideStepRight:
|
||
/// speed *= speedMod
|
||
/// if abs(speed) > MaxSidestepAnimRate: speed = speed > 0 ? Max : -Max
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// GOTCHA: the WalkForward <c>speed *= speedMod</c> is unconditional —
|
||
/// it applies even when speed is negative (backward), even though the
|
||
/// command is NOT promoted to RunForward in that case (the promotion is
|
||
/// sign-gated, the scale is not).
|
||
/// </para>
|
||
/// </summary>
|
||
public void apply_run_to_command(ref uint motion, ref float speed)
|
||
{
|
||
float speedMod = 1.0f;
|
||
if (WeenieObj is not null)
|
||
speedMod = WeenieObj.InqRunRate(out float rate) ? rate : MyRunRate;
|
||
|
||
switch (motion)
|
||
{
|
||
case MotionCommand.WalkForward:
|
||
if (speed > 0f)
|
||
motion = MotionCommand.RunForward;
|
||
speed *= speedMod;
|
||
break;
|
||
case MotionCommand.TurnRight:
|
||
speed *= RunTurnFactor;
|
||
break;
|
||
case MotionCommand.SideStepRight:
|
||
speed *= speedMod;
|
||
if (MathF.Abs(speed) > MaxSidestepAnimRate)
|
||
speed = speed > 0f ? MaxSidestepAnimRate : -MaxSidestepAnimRate;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ── 0x005287e0 — apply_raw_movement ───────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Retail <c>CMotionInterp::apply_raw_movement</c> orchestrator
|
||
/// (<c>0x005287e0</c>, <c>docs/research/named-retail/acclient_2013_pseudo_c.txt:305817-305834</c>).
|
||
/// Copies the raw motion state into the interpreted state, then normalizes
|
||
/// each of the three channels (forward / sidestep / turn) IN PLACE via
|
||
/// <see cref="adjust_motion"/> using that channel's own hold key.
|
||
///
|
||
/// <para>
|
||
/// After this call, <see cref="get_state_velocity"/> yields correct velocity
|
||
/// for ALL directions — backward (WalkBackward→WalkForward, ×-0.65) and
|
||
/// strafe-left (SideStepLeft→SideStepRight, ×-1×1.248) are normalized here,
|
||
/// which is the D6 fix that retires the hand-mirrored controller formulas
|
||
/// (register TS-22). Callers must pass RAW speeds (forward_speed = 1.0), NOT
|
||
/// pre-run-scaled speeds — <see cref="apply_run_to_command"/> applies the run
|
||
/// rate, so a pre-scaled input would double-scale.
|
||
/// </para>
|
||
/// </summary>
|
||
public void apply_raw_movement(RawMotionState raw)
|
||
{
|
||
// 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
|
||
// InterpretedMotionState omits current_style (velocity doesn't use it).
|
||
InterpretedState.ForwardCommand = raw.ForwardCommand;
|
||
InterpretedState.ForwardSpeed = raw.ForwardSpeed;
|
||
InterpretedState.SideStepCommand = raw.SidestepCommand;
|
||
InterpretedState.SideStepSpeed = raw.SidestepSpeed;
|
||
InterpretedState.TurnCommand = raw.TurnCommand;
|
||
InterpretedState.TurnSpeed = raw.TurnSpeed;
|
||
|
||
// Normalize each channel with its OWN per-channel hold key.
|
||
uint fCmd = InterpretedState.ForwardCommand; float fSpd = InterpretedState.ForwardSpeed;
|
||
adjust_motion(ref fCmd, ref fSpd, raw.ForwardHoldKey);
|
||
InterpretedState.ForwardCommand = fCmd; InterpretedState.ForwardSpeed = fSpd;
|
||
|
||
uint sCmd = InterpretedState.SideStepCommand; float sSpd = InterpretedState.SideStepSpeed;
|
||
adjust_motion(ref sCmd, ref sSpd, raw.SidestepHoldKey);
|
||
InterpretedState.SideStepCommand = sCmd; InterpretedState.SideStepSpeed = sSpd;
|
||
|
||
uint tCmd = InterpretedState.TurnCommand; float tSpd = InterpretedState.TurnSpeed;
|
||
adjust_motion(ref tCmd, ref tSpd, raw.TurnHoldKey);
|
||
InterpretedState.TurnCommand = tCmd; InterpretedState.TurnSpeed = tSpd;
|
||
}
|
||
|
||
// ── FUN_00528870 — apply_current_movement ─────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::apply_current_movement</c> (0x00528870, decomp §4e,
|
||
/// W0-pins.md A3, FULL BODY dual dispatch — replaces the pre-W4
|
||
/// direct-velocity approximation).
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305838-305857):
|
||
/// <code>
|
||
/// if (physics_obj != 0 && initted != 0) {
|
||
/// if (weenie_obj != 0) eax_2 = weenie_obj->IsThePlayer();
|
||
/// if ((weenie_obj == 0 || eax_2 != 0)
|
||
/// && movement_is_autonomous(physics_obj) != 0) {
|
||
/// apply_raw_movement(this, arg2, arg3);
|
||
/// return;
|
||
/// }
|
||
/// apply_interpreted_movement(this, arg2, arg3);
|
||
/// }
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// A3: the dispatch gate is <c>IsThePlayer</c> (no weenie = treat as
|
||
/// the player), NOT <c>IsCreature</c> — a remote player weenie
|
||
/// (<c>IsThePlayer()==false</c>, <c>IsCreature()==true</c>) routes
|
||
/// INTERPRETED even when <c>movement_is_autonomous</c> is true. This is
|
||
/// the genuine divergence from ACE's server-side <c>IsCreature</c> gate
|
||
/// (ACE MotionInterp.cs:430-438) — do not copy.
|
||
/// </para>
|
||
/// </summary>
|
||
public void apply_current_movement(bool cancelMoveTo, bool allowJump)
|
||
{
|
||
if (PhysicsObj is null || !Initted)
|
||
return;
|
||
|
||
bool isThePlayer = WeenieObj is null || WeenieObj.IsThePlayer();
|
||
if (isThePlayer && PhysicsObj.LastMoveWasAutonomous)
|
||
{
|
||
apply_raw_movement(cancelMoveTo, allowJump);
|
||
return;
|
||
}
|
||
|
||
ApplyCurrentMovementInterpreted(cancelMoveTo, allowJump);
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::apply_raw_movement</c> (0x005287e0, decomp §4e
|
||
/// referenced body, raw 305817-305834), the <c>(cancelMoveTo,
|
||
/// allowJump)</c>-arg overload reached from
|
||
/// <see cref="apply_current_movement"/>'s dual dispatch (A3's
|
||
/// "IsThePlayer + autonomous" branch), <see cref="ReportExhaustion"/>,
|
||
/// <see cref="SetWeenieObject"/>, and <see cref="SetPhysicsObject"/>.
|
||
///
|
||
/// <para>
|
||
/// Verbatim: copy the SEVEN raw-state fields (style + all three
|
||
/// axis command/speed pairs) into <see cref="InterpretedState"/>,
|
||
/// normalize each of the three axes IN PLACE via <see cref="adjust_motion"/>
|
||
/// using THAT axis's own <see cref="RawState"/> hold key, then
|
||
/// tail-call <c>apply_interpreted_movement(this, arg2, arg3)</c> — the
|
||
/// SAME retail function (0x00528600) as <see cref="ApplyInterpretedMovement"/>
|
||
/// (the funnel producer). This is NOT a new function: it is the
|
||
/// existing <see cref="apply_raw_movement(RawMotionState)"/> overload's
|
||
/// body (the D6.2a raw-state-orchestrator port), reached here with
|
||
/// <see cref="RawState"/> itself as the source instead of an externally
|
||
/// supplied snapshot, plus the previously-missing
|
||
/// <c>apply_interpreted_movement(arg2, arg3)</c> tail call this
|
||
/// dispatch site needs.
|
||
/// </para>
|
||
/// </summary>
|
||
public void apply_raw_movement(bool cancelMoveTo, bool allowJump)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return;
|
||
|
||
apply_raw_movement(RawState);
|
||
ApplyCurrentMovementInterpreted(cancelMoveTo, allowJump);
|
||
}
|
||
|
||
/// <summary>
|
||
/// The retail <c>apply_interpreted_movement</c> tail reached from BOTH
|
||
/// dual-dispatch branches of <see cref="apply_current_movement"/> (raw
|
||
/// 305855/305832 — <c>CMotionInterp::apply_interpreted_movement</c>,
|
||
/// 0x00528600, the SAME function as the funnel's
|
||
/// <see cref="ApplyInterpretedMovement"/>, per the raw's identical call
|
||
/// target at every one of its six call sites).
|
||
///
|
||
/// <para>
|
||
/// <b>Physics-only-port body (pre-W4 approximation, SURVIVES per the
|
||
/// plan's "less invasive" choice — see the final-report note):</b> this
|
||
/// port has no <see cref="IInterpretedMotionSink"/> wired at this call
|
||
/// site (retail's real body drives <c>DoInterpretedMotion</c>/
|
||
/// <c>StopInterpretedMotion</c> through the animation-table backend,
|
||
/// exactly like <see cref="ApplyInterpretedMovement"/> already does for
|
||
/// the INBOUND funnel path — but that path always has a sink available
|
||
/// from its caller, while <c>apply_current_movement</c>'s callers
|
||
/// (<see cref="HitGround"/>/<see cref="LeaveGround"/>/
|
||
/// <see cref="ReportExhaustion"/>/hold-key toggles/jump) do not thread
|
||
/// one through). Kept as the pre-existing direct
|
||
/// <c>get_state_velocity</c> → grounded <c>set_local_velocity</c> write
|
||
/// — an acdream adaptation of root-motion-driven velocity, register row
|
||
/// added same commit — until R6 (root motion drives the body directly
|
||
/// and this local-only fallback retires in favor of a real sink here
|
||
/// too).
|
||
/// </para>
|
||
/// </summary>
|
||
/// <summary>
|
||
/// R3-W4 (App-bound): the entity's persistent animation-dispatch sink.
|
||
/// When set, <see cref="ApplyCurrentMovementInterpreted"/> routes the
|
||
/// REAL <see cref="ApplyInterpretedMovement"/> (retail
|
||
/// <c>apply_interpreted_movement</c> 0x00528600 — the same function the
|
||
/// inbound funnel drives), so HitGround/LeaveGround/hold-key re-applies
|
||
/// dispatch cycles (the airborne-Falling engage) through the
|
||
/// motion-table backend exactly as retail does. Null (the local player
|
||
/// until R3-W6, interp-less entities) → the AP-77 physics-only tail.
|
||
/// </summary>
|
||
public IInterpretedMotionSink? DefaultSink { get; set; }
|
||
|
||
private void ApplyCurrentMovementInterpreted(bool cancelMoveTo, bool allowJump)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return;
|
||
|
||
// R3-W4: with a persistent sink bound (remotes), this IS retail's
|
||
// apply_interpreted_movement — full dispatch through the funnel
|
||
// body (style → forward-or-Falling → sidestep → turn), which is
|
||
// what makes LeaveGround engage Falling without the deleted
|
||
// K-fix18 forced-SetCycle. Without a sink, fall through to the
|
||
// AP-77 physics-only adaptation below.
|
||
if (DefaultSink is not null)
|
||
{
|
||
ApplyInterpretedMovement(InterpretedState.CurrentStyle, DefaultSink,
|
||
cancelMoveTo, allowJump);
|
||
return;
|
||
}
|
||
|
||
_ = cancelMoveTo;
|
||
_ = allowJump;
|
||
|
||
// Decompile writes back MyRunRate when in run state (offset +0x7C).
|
||
if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
|
||
MyRunRate = InterpretedState.ForwardSpeed;
|
||
|
||
// Only replace velocity when grounded. While airborne, the physics
|
||
// body's integrated velocity (from LeaveGround) should persist —
|
||
// gravity pulls Z down, horizontal momentum is preserved.
|
||
// The retail client's apply_current_movement also gates on Contact+OnWalkable
|
||
// before writing velocity (the full decompiled flow routes through
|
||
// update_object which checks transient state).
|
||
if (PhysicsObj.OnWalkable)
|
||
{
|
||
var localVelocity = get_state_velocity();
|
||
// AP-77: this adaptation's fallback write has no real opinion on
|
||
// "was this move autonomous" (retail's real set_local_velocity
|
||
// call for this dispatch path lives deep inside the animation
|
||
// backend this stand-in doesn't have — see the seam's doc
|
||
// comment). Preserve whatever LastMoveWasAutonomous already
|
||
// holds rather than resetting it, so a caller like LeaveGround
|
||
// (which just set it true via its OWN direct set_local_velocity
|
||
// call, autonomous=1 per raw @305763-305765) isn't clobbered by
|
||
// this immediately-following re-sync tail call.
|
||
PhysicsObj.set_local_velocity(localVelocity, PhysicsObj.LastMoveWasAutonomous);
|
||
}
|
||
}
|
||
|
||
// ── FUN_005288d0 — ReportExhaustion ────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::ReportExhaustion</c> (0x005288d0, decomp §4c
|
||
/// @305861, FULL BODY, W0-pins.md A3). Identical entry gate and
|
||
/// dual-dispatch predicate to <see cref="apply_current_movement"/>,
|
||
/// with HARDCODED <c>(0, 0)</c> dispatch args (raw 305874/305878) —
|
||
/// the retail caller (<c>CPhysicsObj::report_exhaustion</c> 0x0050fdd0
|
||
/// → <c>MovementManager::ReportExhaustion</c> 0x00524360) passes no
|
||
/// per-call parameters through.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305861-305880):
|
||
/// <code>
|
||
/// if (physics_obj != 0 && initted != 0) {
|
||
/// if (weenie_obj != 0) eax_2 = weenie_obj->IsThePlayer();
|
||
/// if ((weenie_obj == 0 || eax_2 != 0)
|
||
/// && movement_is_autonomous(physics_obj) != 0) {
|
||
/// apply_raw_movement(this, 0, 0);
|
||
/// return;
|
||
/// }
|
||
/// apply_interpreted_movement(this, 0, 0);
|
||
/// }
|
||
/// </code>
|
||
/// </para>
|
||
/// </summary>
|
||
public void ReportExhaustion()
|
||
{
|
||
if (PhysicsObj is null || !Initted)
|
||
return;
|
||
|
||
bool isThePlayer = WeenieObj is null || WeenieObj.IsThePlayer();
|
||
if (isThePlayer && PhysicsObj.LastMoveWasAutonomous)
|
||
{
|
||
// Raw (0, 0): arg3 is DisableJumpDuringLink → allowJump: true
|
||
// (#161 polarity decode; consumed since the apply-pass params
|
||
// became real).
|
||
apply_raw_movement(cancelMoveTo: false, allowJump: true);
|
||
return;
|
||
}
|
||
|
||
ApplyCurrentMovementInterpreted(cancelMoveTo: false, allowJump: true);
|
||
}
|
||
|
||
// ── FUN_00528920 / FUN_00528970 — SetWeenieObject / SetPhysicsObject ───────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::SetWeenieObject</c> (0x00528920, decomp §4f
|
||
/// @305884, FULL BODY, W0-pins.md A3). Assigns <see cref="WeenieObj"/>,
|
||
/// then — if already physics-bound AND <see cref="Initted"/> — re-applies
|
||
/// movement via the SAME dual-dispatch predicate as
|
||
/// <see cref="apply_current_movement"/>, EXCEPT the weenie tested for
|
||
/// <c>IsThePlayer</c> is the INCOMING <paramref name="weenie"/> (not
|
||
/// <see cref="WeenieObj"/> post-assignment — they're the same value
|
||
/// here, but retail's raw makes the incoming-arg read explicit: a null
|
||
/// incoming weenie takes the SAME branch as a non-null-but-IsThePlayer
|
||
/// weenie, both via <c>goto label_528946</c>). Dispatch args are
|
||
/// hardcoded <c>(1, 0)</c>.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305884-305907):
|
||
/// <code>
|
||
/// bool noPhysics = (physics_obj == 0);
|
||
/// weenie_obj = arg2;
|
||
/// if (!noPhysics && initted != 0) {
|
||
/// if (arg2 == 0) {
|
||
/// label_528946:
|
||
/// if (movement_is_autonomous(physics_obj) != 0) {
|
||
/// apply_raw_movement(this, 1, 0);
|
||
/// return;
|
||
/// }
|
||
/// } else if (arg2->IsThePlayer() != 0)
|
||
/// goto label_528946;
|
||
/// apply_interpreted_movement(this, 1, 0);
|
||
/// }
|
||
/// </code>
|
||
/// </para>
|
||
/// </summary>
|
||
public void SetWeenieObject(IWeenieObject? weenie)
|
||
{
|
||
WeenieObj = weenie;
|
||
|
||
if (PhysicsObj is null || !Initted)
|
||
return;
|
||
|
||
bool isThePlayer = weenie is null || weenie.IsThePlayer();
|
||
if (isThePlayer && PhysicsObj.LastMoveWasAutonomous)
|
||
{
|
||
// Raw (1, 0): cancelMoveTo set, DisableJumpDuringLink clear →
|
||
// allowJump: true (#161 polarity decode).
|
||
apply_raw_movement(cancelMoveTo: true, allowJump: true);
|
||
return;
|
||
}
|
||
|
||
ApplyCurrentMovementInterpreted(cancelMoveTo: true, allowJump: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::SetPhysicsObject</c> (0x00528970, decomp §4g
|
||
/// @305911, FULL BODY, W0-pins.md A3). Assigns <see cref="PhysicsObj"/>
|
||
/// unconditionally FIRST, then — if the incoming
|
||
/// <paramref name="physicsObj"/> is non-null AND <see cref="Initted"/> —
|
||
/// re-applies movement via the standard dual-dispatch predicate (this
|
||
/// time reading <see cref="WeenieObj"/>, unchanged by this call).
|
||
/// Dispatch args are hardcoded <c>(1, 0)</c>, matching
|
||
/// <see cref="SetWeenieObject"/>.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305911-305932):
|
||
/// <code>
|
||
/// this->physics_obj = arg2;
|
||
/// if (arg2 != 0 && initted != 0) {
|
||
/// if (weenie_obj != 0) eax_1 = weenie_obj->IsThePlayer();
|
||
/// if ((weenie_obj == 0 || eax_1 != 0)
|
||
/// && movement_is_autonomous(physics_obj) != 0) {
|
||
/// apply_raw_movement(this, 1, 0);
|
||
/// return;
|
||
/// }
|
||
/// apply_interpreted_movement(this, 1, 0);
|
||
/// }
|
||
/// </code>
|
||
/// Note the entry gate is on the INCOMING <c>arg2</c> (not the
|
||
/// just-assigned <c>this->physics_obj</c> — same value, but the raw
|
||
/// tests the parameter register directly), unlike
|
||
/// <see cref="SetWeenieObject"/>'s entry gate which tests the
|
||
/// PRE-assignment <c>physics_obj</c> snapshot (<c>noPhysics</c>).
|
||
/// </para>
|
||
/// </summary>
|
||
public void SetPhysicsObject(PhysicsBody? physicsObj)
|
||
{
|
||
PhysicsObj = physicsObj;
|
||
|
||
if (physicsObj is null || !Initted)
|
||
return;
|
||
|
||
bool isThePlayer = WeenieObj is null || WeenieObj.IsThePlayer();
|
||
if (isThePlayer && physicsObj.LastMoveWasAutonomous)
|
||
{
|
||
// Raw (1, 0) → allowJump: true (#161 polarity decode).
|
||
apply_raw_movement(cancelMoveTo: true, allowJump: true);
|
||
return;
|
||
}
|
||
|
||
ApplyCurrentMovementInterpreted(cancelMoveTo: true, allowJump: true);
|
||
}
|
||
|
||
// ── FUN_00527a50 — jump_charge_is_allowed ─────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::jump_charge_is_allowed</c> (0x00527a50, decomp §3b
|
||
/// @304935, W0-pins.md A1 polarity). Gate consulted while the jump
|
||
/// charge is accumulating (spacebar held) — NOT the same gate as
|
||
/// <see cref="jump_is_allowed"/>'s ground check.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 304940-304948):
|
||
/// <code>
|
||
/// weenie_obj = this->weenie_obj;
|
||
/// if (weenie_obj != 0 && weenie_obj.CanJump(this->jump_extent) == 0)
|
||
/// return 0x49;
|
||
/// forward_command = this->interpreted_state.forward_command;
|
||
/// if (forward_command != 0x40000008
|
||
/// && (forward_command <= 0x41000011 || forward_command > 0x41000014))
|
||
/// return 0;
|
||
/// return 0x48;
|
||
/// </code>
|
||
/// </para>
|
||
/// </summary>
|
||
public WeenieError JumpChargeIsAllowed(float extent)
|
||
{
|
||
if (WeenieObj is not null && !WeenieObj.CanJump(extent))
|
||
return WeenieError.CantJumpLoadedDown; // 0x49
|
||
|
||
uint forward = InterpretedState.ForwardCommand;
|
||
if (forward != MotionCommand.Fallen
|
||
&& (forward <= MotionCommand.CrouchLowerBound || forward > MotionCommand.Sleeping))
|
||
return WeenieError.None;
|
||
|
||
return WeenieError.YouCantJumpFromThisPosition; // 0x48
|
||
}
|
||
|
||
// ── FUN_005281c0 — charge_jump ────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::charge_jump</c> (0x005281c0, decomp §3e @305448,
|
||
/// W0-pins.md A1 polarity). R3-W3 (closes J6): the ONLY place retail
|
||
/// arms <see cref="StandingLongJump"/> — the old
|
||
/// <c>contact_allows_move</c> side effect (:1139-1148 pre-W3 numbering)
|
||
/// is DELETED (see <see cref="contact_allows_move"/>'s doc comment).
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305453-305466):
|
||
/// <code>
|
||
/// weenie_obj = this->weenie_obj;
|
||
/// if (weenie_obj != 0 && weenie_obj.CanJump(this->jump_extent) == 0)
|
||
/// return 0x49;
|
||
/// forward_command = this->interpreted_state.forward_command;
|
||
/// if (forward_command == 0x40000008
|
||
/// || (forward_command > 0x41000011 && forward_command <= 0x41000014))
|
||
/// return 0x48;
|
||
/// transient_state = physics_obj->transient_state;
|
||
/// if ((transient_state & 1) != 0 && (transient_state & 2) != 0
|
||
/// && forward_command == 0x41000003
|
||
/// && interpreted_state.sidestep_command == 0
|
||
/// && interpreted_state.turn_command == 0)
|
||
/// this->standing_longjump = 1;
|
||
/// return 0;
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Note the inverted-but-equivalent posture test vs
|
||
/// <see cref="JumpChargeIsAllowed"/>: that function's PASS condition is
|
||
/// <c>forward <= 0x41000011 || forward > 0x41000014</c>; this
|
||
/// function's BLOCK condition is <c>forward == 0x40000008 ||
|
||
/// (forward > 0x41000011 && forward <= 0x41000014)</c> — the
|
||
/// same Fallen-exact / Crouch..Sleeping-range predicate, just phrased as
|
||
/// its own negation plus the explicit Fallen exact-match up front.
|
||
/// Ported literally rather than reusing <see cref="JumpChargeIsAllowed"/>
|
||
/// to keep each function's control flow traceable to its own raw
|
||
/// address.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Caller (outside CMotionInterp, raw line 376144, 0x0056afac — the
|
||
/// player-input/SmartBox layer, out of R3 scope): drives the charge
|
||
/// accumulation loop while spacebar is held. acdream's charge
|
||
/// accumulation stays controller-side (AP-24 register row survives);
|
||
/// <c>PlayerMovementController</c> may call this once wired (W4/W6 — no
|
||
/// regression today since nothing calls <c>ChargeJump</c> yet, so
|
||
/// remotes/local both continue unaffected).
|
||
/// </para>
|
||
/// </summary>
|
||
public WeenieError ChargeJump()
|
||
{
|
||
if (WeenieObj is not null && !WeenieObj.CanJump(JumpExtent))
|
||
return WeenieError.CantJumpLoadedDown; // 0x49
|
||
|
||
uint forward = InterpretedState.ForwardCommand;
|
||
if (forward == MotionCommand.Fallen
|
||
|| (forward > MotionCommand.CrouchLowerBound && forward <= MotionCommand.Sleeping))
|
||
return WeenieError.YouCantJumpFromThisPosition; // 0x48
|
||
|
||
if (PhysicsObj is not null)
|
||
{
|
||
bool onGround = PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)
|
||
&& PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable);
|
||
if (onGround
|
||
&& forward == MotionCommand.Ready
|
||
&& InterpretedState.SideStepCommand == 0
|
||
&& InterpretedState.TurnCommand == 0)
|
||
{
|
||
StandingLongJump = true;
|
||
}
|
||
}
|
||
|
||
return WeenieError.None;
|
||
}
|
||
|
||
// ── FUN_00528780 — jump ───────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Initiate a jump: validate, store jump extent, leave the ground.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (0x00528780, decomp §3f @305792):
|
||
/// <code>
|
||
/// physics_obj = this->physics_obj;
|
||
/// if (physics_obj == 0) return 8;
|
||
/// interrupt_current_movement(physics_obj);
|
||
/// result = jump_is_allowed(this, arg2, arg3);
|
||
/// if (result != 0) {
|
||
/// this->standing_longjump = 0;
|
||
/// return result;
|
||
/// }
|
||
/// this->jump_extent = arg2;
|
||
/// set_on_walkable(physics_obj, 0);
|
||
/// return result; // == 0
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// R3-W3 (closes J7-interp-side): <c>interrupt_current_movement</c> is
|
||
/// now called UNCONDITIONALLY before <see cref="jump_is_allowed"/>, via
|
||
/// the <see cref="InterruptCurrentMovement"/> no-op seam (register row —
|
||
/// R4 wires the real cancel_moveto). <see cref="StandingLongJump"/> is
|
||
/// cleared ONLY on the failure path — a successful jump leaves it
|
||
/// untouched (the caller/LeaveGround owns clearing it on success).
|
||
/// </para>
|
||
/// </summary>
|
||
public WeenieError jump(float extent, int adjustStamina = 0)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return WeenieError.NoPhysicsObject;
|
||
|
||
InterruptCurrentMovement?.Invoke();
|
||
|
||
var result = jump_is_allowed(extent, out _);
|
||
if (result == WeenieError.None)
|
||
{
|
||
JumpExtent = extent;
|
||
PhysicsObj.set_on_walkable(false);
|
||
return WeenieError.None;
|
||
}
|
||
|
||
StandingLongJump = false;
|
||
return result;
|
||
}
|
||
|
||
// ── FUN_00527aa0 — get_jump_v_z ──────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Get the vertical (Z) component of jump velocity.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (0x00527aa0, decomp §3c @304953, W0-pins.md A5 — the BN text
|
||
/// is x87-flag garbled; ACE's clean-room reading adjudicates):
|
||
/// <code>
|
||
/// extent = this->jump_extent;
|
||
/// if (extent < 0.000199999995f) return 0.0f;
|
||
/// if (extent > 1.0f) extent = 1.0f;
|
||
/// if (this->weenie_obj == 0) return 10.0f;
|
||
/// if (weenie_obj.InqJumpVelocity(extent, &extent) != 0) return extent;
|
||
/// return 0.0f;
|
||
/// </code>
|
||
/// </para>
|
||
/// </summary>
|
||
public float GetJumpVZ()
|
||
{
|
||
float extent = JumpExtent;
|
||
|
||
if (extent < JumpVzEpsilon)
|
||
return 0.0f;
|
||
|
||
if (extent > MaxJumpExtent)
|
||
extent = MaxJumpExtent;
|
||
|
||
if (WeenieObj is null)
|
||
return DefaultJumpVz;
|
||
|
||
if (WeenieObj.InqJumpVelocity(extent, out float vz))
|
||
return vz;
|
||
|
||
return 0.0f;
|
||
}
|
||
|
||
// ── FUN_005280c0 — get_leave_ground_velocity ──────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Compose the full 3D body-local launch velocity when leaving the ground.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (0x005280c0, decomp §3d @305404, W0-pins.md A6): body order
|
||
/// is <c>get_state_velocity(esi)</c> → <c>esi.z = get_jump_v_z()</c> →
|
||
/// fallback fires ONLY when <c>|x| AND |y| AND |z|</c> are ALL
|
||
/// <c>< 0.000199999995f</c> (epsilon tested three times, one per
|
||
/// component), and then OVERWRITES ALL THREE components (including the
|
||
/// z the function just computed) with
|
||
/// <c>globaltolocal(physics_obj->m_velocityVector)</c> — decisively
|
||
/// pinned as GLOBAL→LOCAL by the row-linear match against
|
||
/// <c>Frame::globaltolocalvec</c> (A6). The existing
|
||
/// <c>Vector3.Transform(Velocity, Quaternion.Inverse(Orientation))</c>
|
||
/// transform already IS global→local — kept unchanged.
|
||
/// </para>
|
||
/// </summary>
|
||
public Vector3 GetLeaveGroundVelocity()
|
||
{
|
||
var velocity = get_state_velocity();
|
||
velocity.Z = GetJumpVZ();
|
||
|
||
float eps = JumpVzEpsilon;
|
||
if (MathF.Abs(velocity.X) < eps && MathF.Abs(velocity.Y) < eps && MathF.Abs(velocity.Z) < eps
|
||
&& PhysicsObj is not null)
|
||
{
|
||
var invOrientation = Quaternion.Inverse(PhysicsObj.Orientation);
|
||
velocity = Vector3.Transform(PhysicsObj.Velocity, invOrientation);
|
||
}
|
||
|
||
return velocity;
|
||
}
|
||
|
||
// ── FUN_005282b0 — jump_is_allowed ────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Determine whether a jump is currently permitted. R3-W3 (closes J5):
|
||
/// FULL verbatim chain replacing the pre-W3 15-line approximation —
|
||
/// entry shape, <c>IsFullyConstrained</c>, the pending-head peek (A2),
|
||
/// the charge→motion→stamina chain, all now present.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (0x005282b0, decomp §3h, W0-pins.md A2/A10):
|
||
/// <code>
|
||
/// if (physics_obj != 0) {
|
||
/// if (weenie_obj != 0) eax_2 = weenie_obj.IsCreature();
|
||
/// if (weenie_obj != 0 && eax_2 == 0) goto shared_gate; // non-creature weenie skips ground gate
|
||
/// if (physics_obj == 0 || (state bit 0x400) == 0) goto shared_gate; // gravity-state off skips ground gate
|
||
/// if (Contact && OnWalkable) goto shared_gate; // grounded also reaches shared gate
|
||
/// }
|
||
/// return 0x24; // gravity-bound creature, not grounded
|
||
///
|
||
/// shared_gate:
|
||
/// if (IsFullyConstrained(physics_obj) != 0) return 0x47;
|
||
/// head = pending_motions.head_;
|
||
/// if (head != 0) eax_6 = head.jump_error_code; // +0xc
|
||
/// if (head == 0 || eax_6 == 0) {
|
||
/// eax_6 = jump_charge_is_allowed(this);
|
||
/// if (eax_6 == 0) {
|
||
/// eax_7 = motion_allows_jump(this, interpreted_state.forward_command);
|
||
/// if (eax_7 != 0) return eax_7;
|
||
/// if (weenie_obj_1 == 0) return eax_7; // == 0 (success, no weenie to consult)
|
||
/// eax_6 = 0x47;
|
||
/// if (weenie_obj_1.JumpStaminaCost(arg2, arg3) != 0) return eax_7; // == 0 (afforded)
|
||
/// // JumpStaminaCost returned false -> falls out, eax_6 stays 0x47
|
||
/// }
|
||
/// }
|
||
/// return eax_6;
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// A10 note: <c>physics_obj == null</c> returns <c>0x24</c> (NotGrounded),
|
||
/// NOT <c>8</c> — the "8 = no physics obj" convention that holds
|
||
/// everywhere else in CMotionInterp does not hold here (the
|
||
/// <c>if (physics_obj != 0) {...}</c> wrapper falls out to
|
||
/// <c>return 0x24</c> when physics_obj is null, same as the grounded-check
|
||
/// failure path).
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="extent">Jump charge fraction, forwarded to
|
||
/// <c>JumpStaminaCost</c>.</param>
|
||
/// <param name="staminaCost">Out-param mirroring retail's <c>arg3</c>
|
||
/// (<c>int32_t*</c>) — the stamina cost <c>JumpStaminaCost</c> computed,
|
||
/// 0 when the chain never reaches that call.</param>
|
||
public WeenieError jump_is_allowed(float extent, out int staminaCost)
|
||
{
|
||
staminaCost = 0;
|
||
|
||
if (PhysicsObj is not null)
|
||
{
|
||
bool nonCreatureWeenie = WeenieObj is not null && !WeenieObj.IsCreature();
|
||
bool gravityStateOff = !PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity);
|
||
bool grounded = PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)
|
||
&& PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable);
|
||
|
||
if (nonCreatureWeenie || gravityStateOff || grounded)
|
||
return JumpIsAllowedSharedGate(extent, ref staminaCost);
|
||
}
|
||
|
||
return WeenieError.NotGrounded; // 0x24 — gravity-bound creature, not grounded (also physics_obj == null)
|
||
}
|
||
|
||
/// <summary>
|
||
/// The <c>shared_gate</c> label inside <see cref="jump_is_allowed"/>
|
||
/// (raw 305524-305556) — split out only for C# readability; retail
|
||
/// reaches this point via three different <c>goto</c> sites, all folded
|
||
/// into one function here since C# has no goto-into-shared-tail idiom
|
||
/// as clean as retail's.
|
||
/// </summary>
|
||
private WeenieError JumpIsAllowedSharedGate(float extent, ref int staminaCost)
|
||
{
|
||
if (PhysicsObj is not null && PhysicsObj.IsFullyConstrained)
|
||
return WeenieError.GeneralMovementFailure; // 0x47
|
||
|
||
// A2: peek the pending_motions head WHENEVER non-empty — no Count>1
|
||
// gate. A nonzero head.JumpErrorCode short-circuits the whole
|
||
// charge/motion/stamina chain below.
|
||
var head = _pendingMotions.First;
|
||
uint peeked = head is not null ? head.Value.JumpErrorCode : 0;
|
||
|
||
if (head is null || peeked == 0)
|
||
{
|
||
WeenieError chargeResult = JumpChargeIsAllowed(extent);
|
||
if (chargeResult == WeenieError.None)
|
||
{
|
||
WeenieError motionResult = MotionAllowsJump(InterpretedState.ForwardCommand);
|
||
if (motionResult != WeenieError.None)
|
||
return motionResult;
|
||
|
||
if (WeenieObj is null)
|
||
return motionResult; // == None
|
||
|
||
if (!WeenieObj.JumpStaminaCost(extent, out staminaCost))
|
||
return WeenieError.GeneralMovementFailure; // 0x47 — can't afford
|
||
|
||
return motionResult; // == None (success)
|
||
}
|
||
|
||
return chargeResult;
|
||
}
|
||
|
||
return (WeenieError)peeked;
|
||
}
|
||
|
||
// ── FUN_00528dd0 — contact_allows_move ────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Determine whether the current contact state allows this motion command.
|
||
///
|
||
/// <para>
|
||
/// Decompiled logic (0x00528240, pseudo-C 305471): allowed (1) when —
|
||
/// motion is TurnRight/TurnLeft (0x6500000D/0E), OR motion is Falling
|
||
/// (0x40000015) or Dead-class (0x40000011), OR the weenie exists and is
|
||
/// NOT a creature, OR gravity is off, OR the body has Contact +
|
||
/// OnWalkable. Everything else (a gravity-bound creature without ground
|
||
/// contact) is blocked — this is the real mechanism behind "airborne
|
||
/// remotes keep their cycle" (K-fix17's empirical guard).
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// The return type in the decompile is undefined4 (int), but ACE models
|
||
/// it as bool (0 = allowed, non-zero = blocked). We model it as bool
|
||
/// here for cleaner call sites, treating any non-zero return as
|
||
/// "blocked".
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>R3-W3 (closes J6): the StandingLongJump side effect that used to
|
||
/// live here is DELETED.</b> The S2a port had flagged it explicitly as
|
||
/// "PRE-EXISTING acdream side effect (not part of 0x00528240)" — a
|
||
/// misattribution: retail's <c>contact_allows_move</c> (0x00528240)
|
||
/// never reads or writes <c>standing_longjump</c> at all. The real
|
||
/// arming site is <see cref="ChargeJump"/> (0x005281c0, decomp §3e),
|
||
/// which now owns the identical grounded+idle predicate exclusively.
|
||
/// Consequence of the OLD bug this retires: every grounded idle contact
|
||
/// check (i.e. every frame the player stood still, since
|
||
/// <c>ApplyInterpretedMovement</c> calls this every dispatch) flipped
|
||
/// the flag regardless of whether a jump charge was ever started —
|
||
/// <see cref="ChargeJump"/> is the only path that can arm it now.
|
||
/// </para>
|
||
/// </summary>
|
||
public bool contact_allows_move(uint motion)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return false;
|
||
|
||
if (motion > 0x40000015u)
|
||
{
|
||
if (motion is MotionCommand.TurnRight or MotionCommand.TurnLeft)
|
||
return true;
|
||
}
|
||
else if (motion == MotionCommand.Falling || motion == 0x40000011u)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (WeenieObj is not null && !WeenieObj.IsCreature())
|
||
return true;
|
||
|
||
if (!PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity))
|
||
return true;
|
||
|
||
bool grounded = PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)
|
||
&& PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable);
|
||
|
||
return grounded;
|
||
}
|
||
|
||
// ── R3-W2 — pending_motions lifecycle ─────────────────────────────────
|
||
// docs/research/2026-07-02-r3-motioninterp/r3-motioninterp-decomp.md §1.
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::add_to_queue</c> (0x00527b80, decomp §1a @305032):
|
||
/// allocate a <see cref="MotionNode"/> and append it to the tail of
|
||
/// <see cref="PendingMotions"/> (retail: append at tail; if the queue was
|
||
/// empty, head and tail both point at the new node — the C#
|
||
/// <see cref="LinkedList{T}"/> gives this for free via
|
||
/// <c>AddLast</c>).
|
||
/// </summary>
|
||
/// <param name="contextId">Retail <c>arg2</c> — <c>MotionNode.context_id</c>.</param>
|
||
/// <param name="motion">Retail <c>arg3</c> — <c>MotionNode.motion</c>.</param>
|
||
/// <param name="jumpErrorCode">Retail <c>arg4</c> — <c>MotionNode.jump_error_code</c>.</param>
|
||
public void AddToQueue(uint contextId, uint motion, uint jumpErrorCode)
|
||
{
|
||
_pendingMotions.AddLast(new MotionNode(contextId, motion, jumpErrorCode));
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::motions_pending</c> (0x00527fe0, decomp §1b
|
||
/// @305322): <c>pending_motions.head_ != null</c>.
|
||
/// </summary>
|
||
public bool MotionsPending() => _pendingMotions.First is not null;
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::MotionDone</c> (0x00527ec0, decomp §1c @305238,
|
||
/// FULL BODY). A7 (W0-pins.md): <paramref name="motion"/> and
|
||
/// <paramref name="success"/> are read into locals by the decompiled
|
||
/// relay chain but NEVER actually used by this build's body — the queue
|
||
/// head is popped UNCONDITIONALLY, never matched by motion id. Params
|
||
/// are kept for R5 signature parity with the real
|
||
/// <c>MovementManager::MotionDone</c> relay.
|
||
///
|
||
/// <para>
|
||
/// Body (verbatim): no-op if <see cref="PhysicsObj"/> is null or the
|
||
/// queue is empty. Peek the HEAD: if <c>head.Motion & 0x10000000</c>
|
||
/// (the action-class bit) is set, fire
|
||
/// <see cref="UnstickFromObject"/> then pop the head of BOTH
|
||
/// <see cref="InterpretedState"/>'s and <see cref="RawState"/>'s action
|
||
/// FIFOs. Then unconditionally dequeue the pending_motions head.
|
||
/// </para>
|
||
/// </summary>
|
||
public void MotionDone(uint motion, bool success)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return;
|
||
|
||
var head = _pendingMotions.First;
|
||
if (head is null)
|
||
return;
|
||
|
||
if ((head.Value.Motion & 0x10000000u) != 0)
|
||
{
|
||
UnstickFromObject?.Invoke();
|
||
InterpretedState.RemoveAction();
|
||
RawState.RemoveAction();
|
||
}
|
||
|
||
// Re-peek per retail's structure (head__1 = this->pending_motions.head_,
|
||
// a fresh read after the action-class branch above) — functionally
|
||
// the same node since nothing else can mutate the queue mid-call.
|
||
var head1 = _pendingMotions.First;
|
||
if (head1 is not null)
|
||
{
|
||
_pendingMotions.RemoveFirst();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::HandleExitWorld</c> (0x00527f30, decomp §1d):
|
||
/// identical body to <see cref="MotionDone"/>'s single-pop logic, looped
|
||
/// until <see cref="PendingMotions"/> drains — world exit flushes all
|
||
/// pending motion callbacks unconditionally, since no more
|
||
/// animation-completion events will arrive.
|
||
///
|
||
/// <para>
|
||
/// <b>Ambiguity resolved (not in W0-pins.md — found while porting):</b>
|
||
/// the raw decompile's loop condition is <c>head_ != 0</c>
|
||
/// (re-read every iteration), but the pop only happens inside a NESTED
|
||
/// <c>if (physics_obj != 0 && head_ != 0)</c> guard — a literal
|
||
/// translation would infinite-loop if <see cref="PhysicsObj"/> were null
|
||
/// with a non-empty queue (the loop variable never advances). This is
|
||
/// dead code in retail: a live <c>CMotionInterp::HandleExitWorld</c> call
|
||
/// only ever happens on an interp already bound to a <c>physics_obj</c>
|
||
/// (retail's <c>CMotionInterp</c> lifetime is physics-object-scoped) —
|
||
/// the null check exists defensively, not as a real "still drain, but
|
||
/// skip the pop" branch. Porting a genuine infinite loop is not
|
||
/// "faithful", it's a bug retail never actually exercises; this port
|
||
/// drains unconditionally regardless of <see cref="PhysicsObj"/> (same
|
||
/// choice <see cref="AddToQueue"/>/<see cref="MotionsPending"/> already
|
||
/// make — they too are called before a physics_obj may exist during
|
||
/// interp construction/testing).
|
||
/// </para>
|
||
/// </summary>
|
||
public void HandleExitWorld()
|
||
{
|
||
while (_pendingMotions.First is not null)
|
||
{
|
||
var head = _pendingMotions.First!;
|
||
if ((head.Value.Motion & 0x10000000u) != 0)
|
||
{
|
||
UnstickFromObject?.Invoke();
|
||
InterpretedState.RemoveAction();
|
||
RawState.RemoveAction();
|
||
}
|
||
|
||
_pendingMotions.RemoveFirst();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::is_standing_still</c> (0x00527fa0, decomp @305309):
|
||
/// on-ground (Contact + OnWalkable) AND ForwardCommand == Ready AND
|
||
/// SideStepCommand == 0 AND TurnCommand == 0.
|
||
/// </summary>
|
||
public bool IsStandingStill()
|
||
{
|
||
if (PhysicsObj is null)
|
||
return false;
|
||
|
||
bool grounded = PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)
|
||
&& PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable);
|
||
if (!grounded)
|
||
return false;
|
||
|
||
return InterpretedState.ForwardCommand == MotionCommand.Ready
|
||
&& InterpretedState.SideStepCommand == 0
|
||
&& InterpretedState.TurnCommand == 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::motion_allows_jump</c> (0x005279e0, decomp §3a
|
||
/// @304908, <c>__pure</c>). A1 (W0-pins.md, adversarially verified):
|
||
/// PINNED as a BLOCKLIST — <c>0</c> = jump allowed (pass), <c>0x48</c> =
|
||
/// jump BLOCKED. Ported as literal uint range comparisons mirroring the
|
||
/// decomp's exact branch algebra (NOT enum-ordinal ranges — the whole
|
||
/// point of A1 is that ACE's enum-order dependence is fragile).
|
||
///
|
||
/// <para>
|
||
/// Blocklist (definitive table, W0-pins.md §A1):
|
||
/// <list type="bullet">
|
||
/// <item><c>[0x1000006f, 0x10000078]</c> — MagicPowerUp01..MagicPowerUp10.</item>
|
||
/// <item><c>[0x10000128, 0x10000131]</c> — TripleThrustLow..MagicPowerUp07Purple.</item>
|
||
/// <item><c>0x40000008</c> exact — Fallen (NOT Falling; ACE mis-transcribed this).</item>
|
||
/// <item><c>[0x40000016, 0x40000018]</c> — Reload, Unload, Pickup.</item>
|
||
/// <item><c>[0x4000001e, 0x40000039]</c> — AimLevel..MagicPray.</item>
|
||
/// <item><c>[0x41000012, 0x41000014]</c> — Crouch, Sitting, Sleeping.</item>
|
||
/// </list>
|
||
/// Everything else — including Falling <c>0x40000015</c>, Ready
|
||
/// <c>0x41000003</c>, Dead <c>0x40000011</c>, all turn/sidestep ids — PASSES.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// This is a pure function (no instance state) — retail's <c>this</c>
|
||
/// parameter is unused in the body (the decomp's <c>__pure</c>
|
||
/// attribute confirms it doesn't read <c>this</c>).
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="motion">Retail <c>arg2</c> — the motion id to test.</param>
|
||
public static WeenieError MotionAllowsJump(uint motion)
|
||
{
|
||
// Verbatim branch algebra from raw 304908-304931 (W0-pins.md §A1):
|
||
// if (arg2 > 0x40000018) {
|
||
// if (arg2 > 0x41000014) return 0;
|
||
// if (arg2 < 0x41000012 && (arg2 < 0x4000001e || arg2 > 0x40000039)) return 0;
|
||
// } else if (arg2 < 0x40000016) {
|
||
// if (arg2 > 0x10000131) {
|
||
// if (arg2 != 0x40000008) return 0;
|
||
// } else if (arg2 < 0x10000128 && (arg2 < 0x1000006f || arg2 > 0x10000078)) return 0;
|
||
// }
|
||
// return 0x48;
|
||
//
|
||
// The middle band [0x40000016, 0x40000018] satisfies NEITHER outer
|
||
// branch (not > 0x40000018, not < 0x40000016) and falls straight to
|
||
// `return 0x48` — a genuine "no early return" gap in the middle.
|
||
if (motion > 0x40000018u)
|
||
{
|
||
if (motion > 0x41000014u)
|
||
return WeenieError.None;
|
||
if (motion < 0x41000012u && (motion < 0x4000001eu || motion > 0x40000039u))
|
||
return WeenieError.None;
|
||
}
|
||
else if (motion < 0x40000016u)
|
||
{
|
||
if (motion > 0x10000131u)
|
||
{
|
||
if (motion != 0x40000008u)
|
||
return WeenieError.None;
|
||
}
|
||
else if (motion < 0x10000128u && (motion < 0x1000006fu || motion > 0x10000078u))
|
||
{
|
||
return WeenieError.None;
|
||
}
|
||
}
|
||
|
||
return WeenieError.YouCantJumpFromThisPosition;
|
||
}
|
||
|
||
// ── FUN_00528b00 — LeaveGround ────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::LeaveGround</c> (0x00528b00, decomp §4b @306022,
|
||
/// FULL BODY — replaces the pre-W4 approximation, closes J8's
|
||
/// LeaveGround leg). Called when the body becomes airborne (also
|
||
/// tail-called unconditionally from <see cref="EnterDefaultState"/>,
|
||
/// §4d).
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 306022-306040, quoted in full in the final-report):
|
||
/// creature gate (<c>weenie_obj == 0 || IsCreature() != 0</c>) AND
|
||
/// state-0x400 (Gravity) gate — SAME shape as <see cref="HitGround"/>.
|
||
/// When both pass: compute the launch velocity via
|
||
/// <see cref="GetLeaveGroundVelocity"/>, apply it with
|
||
/// <c>set_local_velocity(&v, autonomous=1)</c> (the literal
|
||
/// <c>1</c> arg — <see cref="PhysicsBody.LastMoveWasAutonomous"/>
|
||
/// becomes true), reset <see cref="StandingLongJump"/> and
|
||
/// <see cref="JumpExtent"/> to their zero/false defaults (the jump
|
||
/// charge is consumed the instant you actually leave the ground), then
|
||
/// <see cref="RemoveLinkAnimations"/> + <c>apply_current_movement(0,
|
||
/// 0)</c> (the same re-sync <see cref="HitGround"/> performs).
|
||
/// </para>
|
||
/// </summary>
|
||
public void LeaveGround()
|
||
{
|
||
if (PhysicsObj is null)
|
||
return;
|
||
|
||
bool isCreature = WeenieObj is null || WeenieObj.IsCreature();
|
||
if (!isCreature)
|
||
return;
|
||
|
||
if (!PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity))
|
||
return;
|
||
|
||
var velocity = GetLeaveGroundVelocity();
|
||
PhysicsObj.set_local_velocity(velocity, autonomous: true);
|
||
|
||
StandingLongJump = false;
|
||
JumpExtent = 0f;
|
||
|
||
RemoveLinkAnimations?.Invoke();
|
||
// Raw (0, 0) → allowJump: true (#161 polarity decode).
|
||
apply_current_movement(cancelMoveTo: false, allowJump: true);
|
||
}
|
||
|
||
// ── FUN_00528ac0 — HitGround ──────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::HitGround</c> (0x00528ac0, decomp §4a @305996,
|
||
/// FULL BODY — replaces the pre-W4 approximation, closes J8's
|
||
/// HitGround leg). Called when the body lands on a walkable surface.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305996-306014):
|
||
/// <code>
|
||
/// if (physics_obj != 0) {
|
||
/// if (weenie_obj != 0) eax_2 = weenie_obj->IsCreature();
|
||
/// if (weenie_obj == 0 || eax_2 != 0) {
|
||
/// if (physics_obj != 0 && (state & 0x400) != 0) {
|
||
/// RemoveLinkAnimations(physics_obj);
|
||
/// apply_current_movement(this, 0, 0);
|
||
/// }
|
||
/// }
|
||
/// }
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// A3 note: this creature gate uses <c>IsCreature</c> (vtable +0x2c),
|
||
/// NOT <c>IsThePlayer</c> (+0x14) — the anti-artifact proof for A3's
|
||
/// pin (BN distinguishes the two slots locally, ~0x250 bytes from the
|
||
/// IsThePlayer-gated functions).
|
||
/// </para>
|
||
/// </summary>
|
||
public void HitGround()
|
||
{
|
||
if (PhysicsObj is null)
|
||
return;
|
||
|
||
bool isCreature = WeenieObj is null || WeenieObj.IsCreature();
|
||
if (!isCreature)
|
||
return;
|
||
|
||
if (!PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity))
|
||
return;
|
||
|
||
RemoveLinkAnimations?.Invoke();
|
||
// Raw (0, 0) → allowJump: true (#161 polarity decode). The
|
||
// re-apply dispatches the PRESERVED interpreted forward command
|
||
// (the apply pass never writes it — ModifyInterpretedState=false)
|
||
// — this IS the falling-pose exit on landing.
|
||
apply_current_movement(cancelMoveTo: false, allowJump: true);
|
||
}
|
||
|
||
// ── FUN_00528c80 — enter_default_state ────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::enter_default_state</c> (0x00528c80, decomp §4d
|
||
/// @306124, FULL BODY, W0-pins.md A8, closes J10). Resets both motion
|
||
/// states to fresh defaults, re-initializes the physics object's
|
||
/// motion tables, APPENDS the canonical <c>{0, Ready, 0}</c> sentinel
|
||
/// to <see cref="PendingMotions"/> (NO drain — pre-existing queued
|
||
/// nodes survive, A8), marks <see cref="Initted"/>, then tail-calls
|
||
/// <see cref="LeaveGround"/> unconditionally.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 306124-306154):
|
||
/// <code>
|
||
/// raw_state = RawMotionState(); // fresh ctor defaults
|
||
/// interpreted_state = InterpretedMotionState(); // fresh ctor defaults
|
||
/// CPhysicsObj::InitializeMotionTables(physics_obj);
|
||
/// node = new MotionNode { context_id=0, motion=0x41000003, jump_error_code=0 };
|
||
/// pending_motions.append(node); // tail-splice, NO clear
|
||
/// initted = 1;
|
||
/// LeaveGround(this);
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Retail's real construction paths (<c>MovementManager::Create</c> +
|
||
/// the lazy-create guard at every <c>MovementManager</c> entry point)
|
||
/// call this exactly once (or twice on the lazy-create double-call —
|
||
/// §6g, genuine retail, not "fixed") before exposing the interpreter to
|
||
/// any caller. acdream's constructors default <see cref="Initted"/> to
|
||
/// <c>true</c> directly (see that property's doc comment) rather than
|
||
/// routing every construction through this method, so this port is the
|
||
/// verbatim, fully-testable "reset to default/idle state" operation —
|
||
/// callers that want retail's FULL reset semantics (state defaults +
|
||
/// sentinel + LeaveGround tail), not just the <c>Initted</c> flag, call
|
||
/// this explicitly.
|
||
/// </para>
|
||
/// </summary>
|
||
public void EnterDefaultState()
|
||
{
|
||
RawState = new RawMotionState();
|
||
InterpretedState = InterpretedMotionState.Default();
|
||
|
||
InitializeMotionTables?.Invoke();
|
||
|
||
AddToQueue(contextId: 0, MotionCommand.Ready, jumpErrorCode: 0);
|
||
|
||
Initted = true;
|
||
|
||
LeaveGround();
|
||
}
|
||
|
||
// ── FUN_00528b70 / FUN_00528bb0 — set_hold_run / SetHoldKey ────────────────
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::set_hold_run</c> (0x00528b70, decomp §5c @306053,
|
||
/// FULL BODY, closes J13's set_hold_run leg). XOR toggle guard: only
|
||
/// acts when <paramref name="holdingRun"/> actually FLIPS the current
|
||
/// effective hold-run state (skip redundant re-application).
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 306053-306070):
|
||
/// <code>
|
||
/// eax = (arg2 == 0); // "run key is up"
|
||
/// edx = (raw_state.current_holdkey != Run);
|
||
/// if (eax != edx) { // XOR: state actually changes
|
||
/// raw_state.current_holdkey = (arg2 != 0) + 1; // false->1(None), true->2(Run)
|
||
/// apply_current_movement(this, arg3, 0);
|
||
/// }
|
||
/// </code>
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="holdingRun">Retail <c>arg2</c> — nonzero = the run key
|
||
/// is currently held down.</param>
|
||
/// <param name="interrupt">Retail <c>arg3</c> — passed through as
|
||
/// <c>apply_current_movement</c>'s <c>cancelMoveTo</c> arg.</param>
|
||
public void set_hold_run(bool holdingRun, bool interrupt)
|
||
{
|
||
bool runKeyUp = !holdingRun;
|
||
bool notCurrentlyRun = RawState.CurrentHoldKey != HoldKey.Run;
|
||
|
||
if (runKeyUp != notCurrentlyRun)
|
||
{
|
||
RawState.CurrentHoldKey = holdingRun ? HoldKey.Run : HoldKey.None;
|
||
// Raw (arg3, 0) → allowJump: true (#161 polarity decode).
|
||
apply_current_movement(cancelMoveTo: interrupt, allowJump: true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::SetHoldKey</c> (0x00528bb0, decomp §5d @306072,
|
||
/// FULL BODY, closes J13's SetHoldKey leg). No-op if
|
||
/// <paramref name="key"/> already equals <see cref="RawMotionState.CurrentHoldKey"/>.
|
||
/// Setting <see cref="HoldKey.None"/> only takes effect (and
|
||
/// re-applies movement) if the CURRENT hold key is
|
||
/// <see cref="HoldKey.Run"/> — requesting None while already something
|
||
/// else (e.g. Invalid) is silently ignored. Setting
|
||
/// <see cref="HoldKey.Run"/> takes effect whenever the current key
|
||
/// isn't already Run.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 306072-306095):
|
||
/// <code>
|
||
/// current = raw_state.current_holdkey;
|
||
/// if (arg2 != current) {
|
||
/// if (arg2 == HoldKey_None) {
|
||
/// if (current == HoldKey_Run) {
|
||
/// raw_state.current_holdkey = HoldKey_None;
|
||
/// apply_current_movement(this, arg3, 0);
|
||
/// }
|
||
/// } else if (arg2 == HoldKey_Run && current != HoldKey_Run) {
|
||
/// raw_state.current_holdkey = HoldKey_Run;
|
||
/// apply_current_movement(this, arg3, 0);
|
||
/// }
|
||
/// }
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// This is the function <c>DoMotion</c> (W5) calls when bit
|
||
/// <c>0x800</c> (SetHoldKey) of the incoming <c>MovementParameters</c>
|
||
/// requests a hold-key change, and what <see cref="adjust_motion"/>
|
||
/// reads back via <see cref="RawMotionState.CurrentHoldKey"/> (mirrored
|
||
/// onto <see cref="CurrentHoldKey"/> — see the field doc) to decide
|
||
/// whether <see cref="apply_run_to_command"/> fires.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="key">Retail <c>arg2</c>.</param>
|
||
/// <param name="cancelMoveTo">Retail <c>arg3</c> — passed through as
|
||
/// <c>apply_current_movement</c>'s <c>cancelMoveTo</c> arg.</param>
|
||
public void SetHoldKey(HoldKey key, bool cancelMoveTo)
|
||
{
|
||
HoldKey current = RawState.CurrentHoldKey;
|
||
if (key == current)
|
||
return;
|
||
|
||
if (key == HoldKey.None)
|
||
{
|
||
if (current == HoldKey.Run)
|
||
{
|
||
RawState.CurrentHoldKey = HoldKey.None;
|
||
// Raw (arg3, 0) → allowJump: true (#161 polarity decode).
|
||
apply_current_movement(cancelMoveTo, allowJump: true);
|
||
}
|
||
}
|
||
else if (key == HoldKey.Run && current != HoldKey.Run)
|
||
{
|
||
RawState.CurrentHoldKey = HoldKey.Run;
|
||
apply_current_movement(cancelMoveTo, allowJump: true);
|
||
}
|
||
}
|
||
|
||
// ── CMotionInterp::get_max_speed (0x00527cb0) ─────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Return the maximum movement speed in m/s: run rate × RunAnimSpeed (4.0).
|
||
/// Mirrors retail <c>CMotionInterp::get_max_speed</c> at <c>0x00527cb0</c>.
|
||
///
|
||
/// <para>
|
||
/// <b>The ×4.0 is byte-verified retail (UN-2 resolved 2026-06-12).</b>
|
||
/// The Binary Ninja pseudo-C (named-retail/acclient_2013_pseudo_c.txt:305127)
|
||
/// renders this function as <c>void</c> with a bare <c>this->my_run_rate;</c>
|
||
/// statement because it drops x87 instructions — a known BN artifact class.
|
||
/// Disassembling the PDB-matched v11.4186 binary at VA <c>0x00527cb0</c>
|
||
/// shows all THREE return paths end with
|
||
/// <c>fmul dword ptr [0x007C8918]</c>, and the .rdata dword at
|
||
/// <c>0x007C8918</c> is <c>0x40800000</c> = 4.0f (the sibling
|
||
/// <c>get_adjusted_max_speed</c> 0x00527d00 carries the same trailing
|
||
/// fmul). Re-derive with <c>py tools/verify_un2_fmul.py</c>. The three
|
||
/// retail paths: weenie_obj == null → 1.0×4; InqRunRate success →
|
||
/// queried×4; InqRunRate failure → my_run_rate×4. ACE's
|
||
/// MotionInterp.cs:665-676 ports it identically (RunAnimSpeed = 4.0f).
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Consequence: the dead-reckoning catch-up speed
|
||
/// (<c>InterpolationManager::adjust_offset</c> 0x00555d30, pc:353122)
|
||
/// is <c>2 × get_max_speed()</c> ≈ 23.5 m/s for a run-rate-2.94
|
||
/// (run-skill-200) character — that IS retail's value. An earlier
|
||
/// doc-comment here claimed the bare rate (~5.9 m/s catch-up) was
|
||
/// retail-correct and blamed the ×4 for the multi-second 1-Hz blip on
|
||
/// observed retail remotes; that reading trusted the BN x87 dropout
|
||
/// and is refuted by the binary. If the blip recurs, its root cause is
|
||
/// elsewhere (node-fail handling / progress-quantum abandonment /
|
||
/// position-queue feed — the #41 family), NOT this multiply.
|
||
/// </para>
|
||
/// </summary>
|
||
public float GetMaxSpeed()
|
||
{
|
||
// Retail 0x00527cb0: weenie null → 1.0; InqRunRate ok → queried;
|
||
// InqRunRate failed → my_run_rate. Every path × RunAnimSpeed (4.0,
|
||
// .rdata 0x007C8918). Note the weenie-null default is the LITERAL 1.0
|
||
// (.rdata 0x007928B0), not my_run_rate.
|
||
float rate = 1.0f;
|
||
if (WeenieObj is not null && !WeenieObj.InqRunRate(out rate))
|
||
rate = MyRunRate;
|
||
return RunAnimSpeed * rate;
|
||
}
|
||
|
||
// R3-W5: the former `ApplyMotionToInterpretedState` private helper
|
||
// (a hand-rolled switch approximating InterpretedMotionState.ApplyMotion)
|
||
// is DELETED per the plan (closes J3/J4) — the merged DoInterpretedMotion
|
||
// below calls the verbatim InterpretedMotionState.ApplyMotion(motion, p)
|
||
// directly (already ported, W1).
|
||
|
||
// ══ L.2g S2 — the inbound CMotionInterp funnel (DEV-1) ════════════════
|
||
// Spec: docs/research/2026-07-02-s2-inbound-funnel-pseudocode.md.
|
||
// Dispatch order validated against the LIVE retail-observer cdb trace
|
||
// (tools/cdb/l2g-observer.cdb): style → forward → sidestep(-stop) →
|
||
// turn(-stop) → actions, wholesale per UM.
|
||
|
||
/// <summary>
|
||
/// True when this interpreter belongs to the LOCAL player. Retail skips
|
||
/// replaying AUTONOMOUS actions on the local player (they are its own
|
||
/// echo — move_to_interpreted_state 305977) but applies them on remotes.
|
||
/// </summary>
|
||
public bool IsLocalPlayer;
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::server_action_stamp</c> — 15-bit wraparound stamp
|
||
/// of the last applied inbound action (move_to_interpreted_state
|
||
/// 305953-305989).
|
||
/// </summary>
|
||
public int ServerActionStamp;
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::move_to_interpreted_state</c> (0x005289c0,
|
||
/// pseudo-C 305936): adopt the style into the raw state, FLAT-copy the
|
||
/// incoming interpreted state (<c>copy_movement_from</c> 0x0051e750 —
|
||
/// every axis overwritten, defaults included), re-apply the whole
|
||
/// movement through the sink, then replay fresh actions under the
|
||
/// 15-bit stamp gate.
|
||
///
|
||
/// <para>
|
||
/// <b>R3-W4 (Adjacent finding, W0-pins.md):</b> raw 305946-305949
|
||
/// computes <c>eax_2 = motion_allows_jump(this->interpreted_state.forward_command)</c>
|
||
/// — the OLD forward command, BEFORE <c>copy_movement_from</c>
|
||
/// overwrites <see cref="InterpretedState"/> two lines later — then
|
||
/// calls <c>apply_current_movement(this, 1, allowJump)</c> where
|
||
/// <c>allowJump = (eax_2 == 0)</c> (the raw's own
|
||
/// <c>-((esi_1 - esi_1))</c> is decompiler noise for that same
|
||
/// boolean). Ported here as <see cref="MotionAllowsJump"/> on the
|
||
/// PRE-overwrite <see cref="InterpretedState"/>.ForwardCommand,
|
||
/// snapshotted before the flat-copy below. Passed through to
|
||
/// <see cref="ApplyInterpretedMovement"/> per its TODO(W5) — this
|
||
/// funnel calls the interpreted body directly (not the
|
||
/// <see cref="apply_current_movement"/> dual-dispatch gate), matching
|
||
/// the existing S2 architecture: inbound funnel state is always
|
||
/// server-authoritative, so it always wants the interpreted path
|
||
/// regardless of IsThePlayer/autonomous.
|
||
/// </para>
|
||
/// </summary>
|
||
public int MoveToInterpretedState(in InboundInterpretedState ims, IInterpretedMotionSink? sink = null)
|
||
{
|
||
if (PhysicsObj is null) return 0;
|
||
|
||
RawState.CurrentStyle = ims.CurrentStyle;
|
||
|
||
// motion_allows_jump on the OLD forward_command, BEFORE the
|
||
// flat-copy below overwrites it (Adjacent finding).
|
||
bool allowJump = MotionAllowsJump(InterpretedState.ForwardCommand) == WeenieError.None;
|
||
|
||
// copy_movement_from — flat overwrite, no per-field presence checks.
|
||
// current_style is the FIRST copied field (raw 0051e757) — it is
|
||
// what HitGround/LeaveGround re-applies as the style dispatch
|
||
// (#161: previously omitted, so the landing re-apply read a stale
|
||
// interpreted style).
|
||
InterpretedState.CurrentStyle = ims.CurrentStyle;
|
||
InterpretedState.ForwardCommand = ims.ForwardCommand;
|
||
InterpretedState.ForwardSpeed = ims.ForwardSpeed;
|
||
InterpretedState.SideStepCommand = ims.SideStepCommand;
|
||
InterpretedState.SideStepSpeed = ims.SideStepSpeed;
|
||
InterpretedState.TurnCommand = ims.TurnCommand;
|
||
InterpretedState.TurnSpeed = ims.TurnSpeed;
|
||
|
||
ApplyInterpretedMovement(ims.CurrentStyle, sink, cancelMoveTo: true, allowJump: allowJump);
|
||
|
||
if (ims.Actions is { } actions)
|
||
{
|
||
foreach (var a in actions)
|
||
{
|
||
// 15-bit wraparound "newer" gate vs server_action_stamp:
|
||
// abs diff > 0x3fff flips the comparison (305955-305969).
|
||
int incoming = a.Stamp & 0x7FFF;
|
||
int stored = ServerActionStamp & 0x7FFF;
|
||
int diff = incoming >= stored ? incoming - stored : stored - incoming;
|
||
bool newer = diff <= 0x3FFF ? stored < incoming : incoming < stored;
|
||
if (!newer) continue;
|
||
|
||
// Local player skips its own autonomous echoes (305977).
|
||
if (IsLocalPlayer && a.Autonomous) continue;
|
||
|
||
ServerActionStamp = incoming;
|
||
DispatchInterpretedMotion(a.Command, a.Speed, a.Autonomous, sink);
|
||
}
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::apply_interpreted_movement</c> (0x00528600,
|
||
/// pseudo-C 305713): cache <c>my_run_rate</c> from a RunForward speed,
|
||
/// then dispatch style / forward-or-Falling / sidestep-or-stop /
|
||
/// turn-or-stop in retail order. A non-zero turn EARLY-RETURNS (no
|
||
/// turn-stop, no idle bookkeeping).
|
||
///
|
||
/// <para>
|
||
/// <b>R3-W2 producer (closes J1's apply_interpreted_movement leg):</b>
|
||
/// the tail turn-stop call (raw 305766-305785) is retail's
|
||
/// <c>CPhysicsObj::StopInterpretedMotion(physics_obj, 0x6500000d, &var_2c)</c>
|
||
/// — our <see cref="IInterpretedMotionSink.StopMotion"/> call below IS
|
||
/// that dispatch. On success (<c>eax_10 == 0</c>, i.e. the sink accepted
|
||
/// the stop — this port has no failure signal from
|
||
/// <see cref="IInterpretedMotionSink.StopMotion"/>, so it is treated as
|
||
/// always-succeeding, matching every real sink implementation which is
|
||
/// void), retail immediately re-queues the canonical "return to none"
|
||
/// node: <c>add_to_queue(this, var_c /*uninitialized local in the raw
|
||
/// decompile — see the final-report contextId note*/, 0x41000003, eax_10)</c>
|
||
/// where <c>eax_10</c> (the stop's own return value) becomes the queued
|
||
/// node's <c>jump_error_code</c> — since the stop succeeded, that value
|
||
/// is <c>0</c>.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>#161 (2026-07-03, closes R3-W4's TODO(W5)):</b> retail builds ONE
|
||
/// <c>MovementParameters</c> for the whole pass
|
||
/// (<c>MovementParameters::MovementParameters(&var_2c)</c> @305719)
|
||
/// and then rewrites its bitfield: the "garbled tail expression" at raw
|
||
/// 305778 is the BN decompiler smearing that store into its single read
|
||
/// site — <c>(word & 0x37ff) | (arg2&1)<<15 |
|
||
/// (arg3&1)<<17</c> clears bits 11/14/15 (<c>SetHoldKey</c> /
|
||
/// <c>ModifyInterpretedState</c> / <c>CancelMoveTo</c>), re-sets bit 15
|
||
/// from <c>arg2</c> (= <paramref name="cancelMoveTo"/>) and bit 17 from
|
||
/// <c>arg3</c> (= <c>DisableJumpDuringLink</c>, so
|
||
/// <paramref name="allowJump"/> = <c>arg3 == 0</c>). ACE
|
||
/// MotionInterp.cs:444-449 confirms independently. The load-bearing bit
|
||
/// is <c>ModifyInterpretedState = false</c>: NO dispatch issued by this
|
||
/// pass writes <see cref="InterpretedState"/> — the airborne Falling
|
||
/// substitution leaves the wire's forward command intact, which is what
|
||
/// lets <see cref="HitGround"/>'s re-apply dispatch the PRESERVED
|
||
/// pre-fall command (the motion table then plays the Falling→X landing
|
||
/// link — the falling-pose exit needs no wire input). The previous
|
||
/// revision dispatched with ctor-default params
|
||
/// (<c>ModifyInterpretedState = true</c>) — the #161 stuck-falling-pose
|
||
/// root cause, and the true mechanism behind the W6 "press W and stop
|
||
/// instantly" regression (the style dispatch could never have clobbered
|
||
/// forward_command in retail; the entry-cache workaround this body used
|
||
/// to carry is deleted — with no mid-pass state writes possible, live
|
||
/// field reads ARE retail's semantics).
|
||
/// </para>
|
||
/// </summary>
|
||
public void ApplyInterpretedMovement(
|
||
uint currentStyle, IInterpretedMotionSink? sink,
|
||
bool cancelMoveTo = false, bool allowJump = false)
|
||
{
|
||
if (PhysicsObj is null) return;
|
||
|
||
// Retail's rewritten var_2c (raw 305778 decoded; ACE 444-449):
|
||
// Speed is re-set per axis below, everything else rides the pass.
|
||
var p = new MovementParameters
|
||
{
|
||
SetHoldKey = false,
|
||
ModifyInterpretedState = false,
|
||
CancelMoveTo = cancelMoveTo,
|
||
DisableJumpDuringLink = !allowJump,
|
||
};
|
||
|
||
if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
|
||
MyRunRate = InterpretedState.ForwardSpeed;
|
||
|
||
p.Speed = 1.0f;
|
||
DoInterpretedMotion(currentStyle, p, sink);
|
||
|
||
if (!contact_allows_move(InterpretedState.ForwardCommand))
|
||
{
|
||
p.Speed = 1.0f; // raw 305728: var_18_2 = 0x3f800000
|
||
DoInterpretedMotion(MotionCommand.Falling, p, sink);
|
||
}
|
||
else if (StandingLongJump)
|
||
{
|
||
p.Speed = 1.0f;
|
||
DoInterpretedMotion(MotionCommand.Ready, p, sink);
|
||
StopInterpretedMotion(MotionCommand.SideStepRight, p, sink);
|
||
}
|
||
else
|
||
{
|
||
p.Speed = InterpretedState.ForwardSpeed;
|
||
DoInterpretedMotion(InterpretedState.ForwardCommand, p, sink);
|
||
if (InterpretedState.SideStepCommand == 0)
|
||
{
|
||
StopInterpretedMotion(MotionCommand.SideStepRight, p, sink);
|
||
}
|
||
else
|
||
{
|
||
p.Speed = InterpretedState.SideStepSpeed;
|
||
DoInterpretedMotion(InterpretedState.SideStepCommand, p, sink);
|
||
}
|
||
}
|
||
|
||
if (InterpretedState.TurnCommand != 0)
|
||
{
|
||
p.Speed = InterpretedState.TurnSpeed;
|
||
DoInterpretedMotion(InterpretedState.TurnCommand, p, sink);
|
||
return; // retail early return — no idle-stop this call
|
||
}
|
||
|
||
// Tail (raw 305766-305786): unconditional StopInterpretedMotion(TurnRight,
|
||
// params) — the merged StopInterpretedMotion ITSELF performs the
|
||
// add_to_queue(context, Ready, 0) on success; with the pass params'
|
||
// ModifyInterpretedState=false the RemoveMotion(TurnRight) state
|
||
// clear does NOT fire (retail identical — ACE 497-498).
|
||
StopInterpretedMotion(MotionCommand.TurnRight, p, sink);
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::DoInterpretedMotion</c> (0x00528360, raw
|
||
/// 305575-305631, FULL BODY, R3-W5, closes J4). THE ONE
|
||
/// <c>DoInterpretedMotion</c> — merges the former legacy
|
||
/// <c>(uint, float, bool)</c> overload's state-management with the S2a
|
||
/// funnel's <c>DispatchInterpretedMotion</c> sink-dispatch backend.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305577-305631):
|
||
/// <code>
|
||
/// if (physics_obj == 0) return 8;
|
||
/// if (contact_allows_move(arg2)) {
|
||
/// if (standing_longjump != 0
|
||
/// && (arg2 == WalkForward || arg2 == RunForward || arg2 == SideStepRight))
|
||
/// goto label_528440; // StandingLongJump: state-only, no dispatch, no queue
|
||
/// if (arg2 == 0x40000011 /*Dead*/) RemoveLinkAnimations(physics_obj);
|
||
/// result = sink.ApplyMotion(arg2, arg3); // CPhysicsObj::DoInterpretedMotion stand-in
|
||
/// if (result == 0) {
|
||
/// if ((arg3->bitfield & 0x20000) == 0) { // NOT DisableJumpDuringLink
|
||
/// eax_5 = motion_allows_jump(arg2);
|
||
/// if (eax_5 == 0 && (arg2 & 0x10000000) == 0)
|
||
/// eax_5 = motion_allows_jump(interpreted_state.forward_command);
|
||
/// } else eax_5 = 0x48; // DisableJumpDuringLink FORCES blocked
|
||
/// add_to_queue(arg3->context_id, arg2, eax_5);
|
||
/// if (arg3->bitfield & 0x4000 /*ModifyInterpretedState*/)
|
||
/// InterpretedMotionState::ApplyMotion(arg2, arg3);
|
||
/// }
|
||
/// } else if ((arg2 & 0x10000000) == 0) {
|
||
/// label_528440:
|
||
/// if (arg3->bitfield & 0x4000) InterpretedMotionState::ApplyMotion(arg2, arg3);
|
||
/// result = 0;
|
||
/// } else result = 0x24;
|
||
/// if (physics_obj != 0 && physics_obj->cell == 0) RemoveLinkAnimations(physics_obj);
|
||
/// return result;
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>R3-W5 finding (not in the original W0 pins — discovered while
|
||
/// porting):</b> <see cref="IInterpretedMotionSink.ApplyMotion"/>'s
|
||
/// <c>bool</c> return IS retail's <c>result == 0</c> test gating the
|
||
/// queue/state-write block. This is load-bearing, not cosmetic: the
|
||
/// VERY FIRST dispatch of every <c>apply_interpreted_movement</c> call
|
||
/// is the style/stance id (<c>current_style</c>, always
|
||
/// <c>>= 0x80000000</c>), which has no locomotion <c>MotionData</c>
|
||
/// entry in the dat — <c>CMotionTable::DoObjectMotion</c> genuinely
|
||
/// fails for it. If that failure isn't propagated (an earlier revision
|
||
/// of this port treated the sink as a <c>void</c>, always-succeeding
|
||
/// call), <c>InterpretedMotionState::ApplyMotion</c>'s negative-motion
|
||
/// branch (<c>arg2 < 0 → forward_command = 0x41000003</c>) clobbers
|
||
/// <see cref="InterpretedMotionState.ForwardCommand"/> BEFORE the very
|
||
/// next line reads it for the real forward dispatch — regressed 74/183
|
||
/// cases of the live retail-observer conformance suite
|
||
/// (<c>RetailObserverTraceConformanceTests</c>) until the interface was
|
||
/// fixed to return the real result.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Note the ACTUAL animation-table dispatch (the sink call) happens
|
||
/// through <see cref="DefaultSink"/> — the entity's persistent
|
||
/// <c>IInterpretedMotionSink</c> binding (R2-Q5's
|
||
/// <c>MotionTableDispatchSink</c> in production). This is the SAME sink
|
||
/// <see cref="ApplyCurrentMovementInterpreted"/> already routes through
|
||
/// for the <c>apply_current_movement</c> dual-dispatch tail — retail has
|
||
/// exactly one <c>CPhysicsObj</c> per <c>CMotionInterp</c>, so there is
|
||
/// only ever one dispatch target per entity, matching
|
||
/// <see cref="DefaultSink"/>'s single-binding shape.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="motion">Retail <c>arg2</c>.</param>
|
||
/// <param name="p">Retail <c>arg3</c>.</param>
|
||
public WeenieError DoInterpretedMotion(uint motion, MovementParameters p)
|
||
=> DoInterpretedMotion(motion, p, DefaultSink);
|
||
|
||
/// <summary>
|
||
/// Sink-parameterized core shared by the public
|
||
/// <see cref="DoInterpretedMotion(uint,MovementParameters)"/> (which
|
||
/// always dispatches through <see cref="DefaultSink"/>, matching
|
||
/// retail's one-sink-per-<c>CPhysicsObj</c> shape) and the internal
|
||
/// <see cref="DispatchInterpretedMotion(uint,float,IInterpretedMotionSink?)"/>
|
||
/// primitive (which needs an EXPLICIT per-call sink for
|
||
/// <see cref="MoveToInterpretedState"/>'s remote-entity callers). Same
|
||
/// verbatim body either way — only the dispatch target varies.
|
||
/// </summary>
|
||
private WeenieError DoInterpretedMotion(uint motion, MovementParameters p, IInterpretedMotionSink? sink)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return WeenieError.NoPhysicsObject;
|
||
|
||
WeenieError result;
|
||
|
||
if (contact_allows_move(motion))
|
||
{
|
||
bool standingLongJumpStateOnly = StandingLongJump
|
||
&& (motion == MotionCommand.WalkForward
|
||
|| motion == MotionCommand.RunForward
|
||
|| motion == MotionCommand.SideStepRight);
|
||
|
||
if (standingLongJumpStateOnly)
|
||
{
|
||
// label_528440 — state-only: no dispatch, no queue.
|
||
if (p.ModifyInterpretedState)
|
||
InterpretedState.ApplyMotion(motion, p);
|
||
return WeenieError.None;
|
||
}
|
||
|
||
if (motion == MotionCommand.Dead)
|
||
RemoveLinkAnimations?.Invoke();
|
||
|
||
// sink.ApplyMotion stands in for CPhysicsObj::DoInterpretedMotion
|
||
// — its bool return IS retail's `result == 0` (a null sink, no
|
||
// dispatch backend wired, is treated as succeeding — matches
|
||
// every call site that doesn't care about the animation-table
|
||
// backend, e.g. bare unit tests exercising queue/state behavior
|
||
// only). A FAILED dispatch (the motion table found no cycle for
|
||
// this id — genuinely the case for style/stance ids) skips BOTH
|
||
// the queue AND the state-write, exactly like the "blocked,
|
||
// non-action" apply-only path below — but WITHOUT writing state.
|
||
bool dispatchOk = sink?.ApplyMotion(motion, p.Speed) ?? true;
|
||
|
||
if (!dispatchOk)
|
||
{
|
||
// Retail: `result = CPhysicsObj::DoInterpretedMotion(...)` is
|
||
// whatever the failed dispatch returned (raw 305591-305593;
|
||
// no `else` branch — `result` simply isn't overwritten by
|
||
// the `if (result == 0)` block), so DoInterpretedMotion's
|
||
// OWN return value on a failed sink dispatch is that
|
||
// nonzero code, NOT 0. The exact numeric value is sink-
|
||
// internal (MotionTableManagerError.MotionFailed = 0x43,
|
||
// not one of CMotionInterp's own WeenieError codes) — no
|
||
// CMotionInterp-level WeenieError constant maps to it
|
||
// 1:1, so this port surfaces the closest existing local
|
||
// analog (GeneralMovementFailure, 0x47) rather than
|
||
// inventing a new enum member for a sink-internal code
|
||
// that's never queued or otherwise observed by any
|
||
// CMotionInterp caller. Flagged as an ambiguity — no test
|
||
// in this repo asserts DoInterpretedMotion's return value
|
||
// on a failed dispatch (only the queue/state SIDE EFFECTS,
|
||
// which correctly do NOT run here).
|
||
result = WeenieError.GeneralMovementFailure;
|
||
}
|
||
else
|
||
{
|
||
WeenieError jumpErr;
|
||
if (!p.DisableJumpDuringLink)
|
||
{
|
||
jumpErr = MotionAllowsJump(motion);
|
||
if (jumpErr == WeenieError.None && (motion & 0x10000000u) == 0)
|
||
jumpErr = MotionAllowsJump(InterpretedState.ForwardCommand);
|
||
}
|
||
else
|
||
{
|
||
jumpErr = WeenieError.YouCantJumpFromThisPosition; // 0x48 — forced BLOCKED
|
||
}
|
||
|
||
AddToQueue(p.ContextId, motion, (uint)jumpErr);
|
||
|
||
if (p.ModifyInterpretedState)
|
||
InterpretedState.ApplyMotion(motion, p);
|
||
|
||
result = WeenieError.None;
|
||
}
|
||
}
|
||
else if ((motion & 0x10000000u) == 0)
|
||
{
|
||
// label_528440 (reached without the StandingLongJump goto) —
|
||
// apply-only: state kept, no cycle change, no queue. This is
|
||
// retail's real mechanism behind "airborne remotes keep their
|
||
// Falling cycle".
|
||
if (p.ModifyInterpretedState)
|
||
InterpretedState.ApplyMotion(motion, p);
|
||
result = WeenieError.None;
|
||
}
|
||
else
|
||
{
|
||
result = WeenieError.NotGrounded; // 0x24 (A10) — blocked action-class motion
|
||
}
|
||
|
||
// R4-V5 door-swing fix (2026-07-03): retail's guard is
|
||
// physics_obj->cell == 0 — DETACHED objects only (raw @305627).
|
||
// The old proxy (CellPosition.ObjCellId == 0, #145 machinery seeded
|
||
// only by the local player's SnapToCell) read every REMOTE body as
|
||
// detached, so every dispatch stripped the just-appended transition
|
||
// link (door swings snapped; remote walk↔run links died) — see
|
||
// PhysicsBody.InWorld (register row).
|
||
if (!PhysicsObj.InWorld)
|
||
RemoveLinkAnimations?.Invoke();
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>CMotionInterp::StopInterpretedMotion</c> (0x00528470, raw
|
||
/// 305635-305670, FULL BODY, R3-W5, closes J4's StopInterpretedMotion
|
||
/// leg). THE ONE <c>StopInterpretedMotion</c> — merges the former legacy
|
||
/// overload with the funnel's bare <c>sink.StopMotion</c> call.
|
||
///
|
||
/// <para>
|
||
/// Verbatim (raw 305638-305670):
|
||
/// <code>
|
||
/// if (physics_obj == 0) return 8;
|
||
/// if (contact_allows_move(arg2) == 0
|
||
/// || (standing_longjump != 0
|
||
/// && (arg2 == WalkForward || arg2 == RunForward || arg2 == SideStepRight))) {
|
||
/// if (arg3->bitfield & 0x4000) InterpretedMotionState::RemoveMotion(arg2);
|
||
/// result = 0;
|
||
/// } else {
|
||
/// result = sink.StopMotion(arg2, arg3); // CPhysicsObj::StopInterpretedMotion stand-in
|
||
/// if (result == 0) {
|
||
/// add_to_queue(arg3->context_id, 0x41000003, result); // result == 0 here
|
||
/// if (arg3->bitfield & 0x4000) InterpretedMotionState::RemoveMotion(arg2);
|
||
/// }
|
||
/// }
|
||
/// if (physics_obj != 0 && physics_obj->cell == 0) RemoveLinkAnimations(physics_obj);
|
||
/// return result;
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// R3-W5 (post-fix): <see cref="IInterpretedMotionSink.StopMotion"/>
|
||
/// returns a real success signal (see that interface member's doc for
|
||
/// why a <c>void</c> sink broke the 183-case live retail conformance
|
||
/// suite on the analogous <c>DoInterpretedMotion</c> path) — a FAILED
|
||
/// stop dispatch skips both the <c>add_to_queue</c> node and the
|
||
/// <c>InterpretedMotionState::RemoveMotion</c> state clear, matching
|
||
/// retail's <c>if (result == 0) { ... }</c> gate exactly. On success,
|
||
/// the queued node's <c>JumpErrorCode</c> is always <c>0</c> (the stop's
|
||
/// own "return value" that becomes the queued error code is
|
||
/// <c>result</c>, which is <c>0</c> on that path).
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="motion">Retail <c>arg2</c>.</param>
|
||
/// <param name="p">Retail <c>arg3</c>.</param>
|
||
public WeenieError StopInterpretedMotion(uint motion, MovementParameters p)
|
||
=> StopInterpretedMotion(motion, p, DefaultSink);
|
||
|
||
/// <summary>
|
||
/// Sink-parameterized core shared by the public
|
||
/// <see cref="StopInterpretedMotion(uint,MovementParameters)"/> and the
|
||
/// internal <see cref="DispatchStopInterpretedMotion"/> primitive — same
|
||
/// split rationale as
|
||
/// <see cref="DoInterpretedMotion(uint,MovementParameters,IInterpretedMotionSink?)"/>.
|
||
/// </summary>
|
||
private WeenieError StopInterpretedMotion(uint motion, MovementParameters p, IInterpretedMotionSink? sink)
|
||
{
|
||
if (PhysicsObj is null)
|
||
return WeenieError.NoPhysicsObject;
|
||
|
||
WeenieError result;
|
||
|
||
bool standingLongJumpStateOnly = StandingLongJump
|
||
&& (motion == MotionCommand.WalkForward
|
||
|| motion == MotionCommand.RunForward
|
||
|| motion == MotionCommand.SideStepRight);
|
||
|
||
if (!contact_allows_move(motion) || standingLongJumpStateOnly)
|
||
{
|
||
if (p.ModifyInterpretedState)
|
||
InterpretedState.RemoveMotion(motion);
|
||
result = WeenieError.None;
|
||
}
|
||
else
|
||
{
|
||
// sink.StopMotion stands in for CPhysicsObj::StopInterpretedMotion
|
||
// — its bool return IS retail's `result == 0`. A null sink (no
|
||
// dispatch backend wired) is treated as succeeding, matching
|
||
// DoInterpretedMotion's identical posture.
|
||
bool dispatchOk = sink?.StopMotion(motion) ?? true;
|
||
|
||
if (!dispatchOk)
|
||
{
|
||
// See DoInterpretedMotion's identical ambiguity note: retail
|
||
// returns the sink's own nonzero code here (raw 305652-305653,
|
||
// no `else` — `result` keeps whatever CPhysicsObj::StopInterpretedMotion
|
||
// returned); ported as the closest local analog.
|
||
result = WeenieError.GeneralMovementFailure;
|
||
}
|
||
else
|
||
{
|
||
result = WeenieError.None;
|
||
|
||
AddToQueue(p.ContextId, MotionCommand.Ready, (uint)result);
|
||
|
||
if (p.ModifyInterpretedState)
|
||
InterpretedState.RemoveMotion(motion);
|
||
}
|
||
}
|
||
|
||
// R4-V5 door-swing fix (2026-07-03): retail's guard is
|
||
// physics_obj->cell == 0 — DETACHED objects only (raw @305627).
|
||
// The old proxy (CellPosition.ObjCellId == 0, #145 machinery seeded
|
||
// only by the local player's SnapToCell) read every REMOTE body as
|
||
// detached, so every dispatch stripped the just-appended transition
|
||
// link (door swings snapped; remote walk↔run links died) — see
|
||
// PhysicsBody.InWorld (register row).
|
||
if (!PhysicsObj.InWorld)
|
||
RemoveLinkAnimations?.Invoke();
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// R3-W5 internal dispatch primitive (formerly the S2a funnel's public
|
||
/// surface), now used ONLY by <see cref="MoveToInterpretedState"/>'s
|
||
/// action-replay loop — retail's action dispatches (raw 305983) use the
|
||
/// UM apply's ctor-default <c>var_2c</c> (notably
|
||
/// <c>ModifyInterpretedState = true</c>, so replayed actions DO enter the
|
||
/// interpreted actions list via <c>InterpretedMotionState::AddAction</c>),
|
||
/// unlike the apply pass's rewritten params (see
|
||
/// <see cref="ApplyInterpretedMovement"/>). R5-V4 (#164): the params'
|
||
/// <c>Autonomous</c> bit (0x1000) is spliced from the ACTION's autonomy
|
||
/// flag — retail raw 305982:
|
||
/// <c>var_28 ^= ((action.autonomous << 0xc) ^ var_28) & 0x1000</c>
|
||
/// — so a replayed action enters the state lists
|
||
/// (<c>AddAction(motion, speed, stamp, autonomous)</c>) with its real
|
||
/// autonomy instead of the ctor-default false.
|
||
/// </summary>
|
||
private WeenieError DispatchInterpretedMotion(
|
||
uint motion, float speed, bool autonomous, IInterpretedMotionSink? sink)
|
||
=> DoInterpretedMotion(
|
||
motion, new MovementParameters { Speed = speed, Autonomous = autonomous }, sink);
|
||
}
|