docs(R3-W-1): CMotionInterp-completion research base — decomp extraction + ACE cross-ref + port work-list
Workflow-produced R3 research (3 docs, 3,061 lines):
- r3-motioninterp-decomp.md: verbatim pseudo-C + anchors for the full R3
scope — pending_motions lifecycle (add_to_queue 0x00527b80, MotionDone
0x00527ec0), DoMotion 0x00528d20 + PerformMovement 0x00528e80, the whole
jump family, HitGround 0x00528ac0 / LeaveGround 0x00528b00 (stale
0x00529710 doc-comment corrected), enter_default_state, MovementManager
relay surface, struct anchors, constants inventory. Negative results
(IsAnimating / HandleUpdateTarget / CMotionInterp::HandleEnterWorld NOT
in retail) explicitly recorded.
- r3-ace-motioninterp.md: ACE MotionInterp/MovementManager/MotionNode map
with flagged ACE-isms (jump_is_allowed L747 NPE typo, Falling-vs-Fallen
boundary discrepancy).
- r3-port-plan.md: 10 pinned ambiguities (headline: motion_allows_jump
0x48 polarity INVERTED in the BN annotation — ranges are a BLOCKLIST;
apply_current_movement dispatch gate IsThePlayer vs IsCreature), 19
itemized gaps J1-J19, keep-list, 7-commit sequence W0-W7 ending in the
local-player unification, exact IMotionDoneSink wiring spec vs R2 §4.
Precondition paragraph updated at vaulting: Q2 committed 98f58db9.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
615cd4dd74
commit
8eff397801
3 changed files with 3062 additions and 0 deletions
987
docs/research/2026-07-02-r3-motioninterp/r3-ace-motioninterp.md
Normal file
987
docs/research/2026-07-02-r3-motioninterp/r3-ace-motioninterp.md
Normal file
|
|
@ -0,0 +1,987 @@
|
||||||
|
# R3 — ACE port map: MotionInterp + MovementManager + MotionDone chain
|
||||||
|
|
||||||
|
Sources (full-file reads, all line numbers are 1-indexed in the ACE source tree):
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/Animation/MotionInterp.cs` (836 lines, whole file read)
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/Managers/MovementManager.cs` (216 lines, whole file read)
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/Animation/MotionNode.cs` (21 lines, whole file read)
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/Managers/MotionTableManager.cs` (252 lines, whole file read — AnimationDone/CheckForCompletedMotions, the actual MotionDone producer)
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs` (targeted reads: L80-109 IsAnimating/IsMovingOrAnimating, L290-301 CheckForCompletedMotions, L890-903 MotionDone, L4235-4247 ShowPendingMotions/motions_pending)
|
||||||
|
- `references/ACE/Source/ACE.Server/Physics/PartArray.cs` (L60-77 CheckForCompletedMotions passthrough)
|
||||||
|
|
||||||
|
No named-retail cross-reference was performed in this pass (ACE-side mapping only, per R3 scope). ACE class is `MotionInterp` (note: **not** `CMotionInterp` — ACE dropped the `C` Hungarian prefix project-wide, this is a naming-convention divergence not a functional one).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Class fields (MotionInterp.cs L14-32)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public bool Initted;
|
||||||
|
public WeenieObject WeenieObj;
|
||||||
|
public PhysicsObj PhysicsObj;
|
||||||
|
public RawMotionState RawState;
|
||||||
|
public InterpretedMotionState InterpretedState;
|
||||||
|
public float CurrentSpeedFactor;
|
||||||
|
public bool StandingLongJump;
|
||||||
|
public float JumpExtent;
|
||||||
|
public int ServerActionStamp;
|
||||||
|
public float MyRunRate;
|
||||||
|
public LinkedList<MotionNode> PendingMotions;
|
||||||
|
|
||||||
|
public const float BackwardsFactor = 6.4999998e-1f; // 0.65
|
||||||
|
public const float MaxSidestepAnimRate = 3.0f;
|
||||||
|
public const float RunAnimSpeed = 4.0f;
|
||||||
|
public const float RunTurnFactor = 1.5f;
|
||||||
|
public const float SidestepAnimSpeed = 1.25f;
|
||||||
|
public const float SidestepFactor = 0.5f;
|
||||||
|
public const float WalkAnimSpeed = 3.1199999f; // 3.12
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `PendingMotions` is `LinkedList<MotionNode>`, not an array/ring buffer — ACE reimplemented retail's pending-motion queue as a doubly-linked list. `MotionNode` (see §2) carries `ContextID`, `Motion`, `JumpErrorCode` — this is the FIFO queue of in-flight interpreted motions waiting on animation completion.
|
||||||
|
- `CurrentSpeedFactor` is declared but the only read site in this file is `get_adjusted_max_speed()` (L629) — no write site anywhere in MotionInterp.cs. Likely set externally (WeenieObj or PhysicsObj) — not visible in this file.
|
||||||
|
- `JumpExtent` doubles as both "how hard is the current jump charging" (set in `jump()`, read in `charge_jump()`/`get_jump_v_z()`) and as a guard the WeenieObj layer checks via `CanJump(JumpExtent)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. MotionNode (queue element) — MotionNode.cs, whole file
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MotionNode
|
||||||
|
{
|
||||||
|
public int ContextID;
|
||||||
|
public uint Motion;
|
||||||
|
public WeenieError JumpErrorCode;
|
||||||
|
|
||||||
|
public MotionNode() { }
|
||||||
|
public MotionNode(int contextID, uint motion, WeenieError jumpErrorCode)
|
||||||
|
{
|
||||||
|
ContextID = contextID;
|
||||||
|
Motion = motion;
|
||||||
|
JumpErrorCode = jumpErrorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Trivial DTO. `JumpErrorCode` is the pre-computed "would a jump be legal right now, if the player pressed jump while this pending motion is still in flight" — computed once at `add_to_queue` time (see `motion_allows_jump` call sites in `DoInterpretedMotion`/`StopCompletely`), then consumed lazily by `jump_is_allowed()` (§7) via `PendingMotions.First.Value.JumpErrorCode` when `PendingMotions.Count > 1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. add_to_queue / MotionDone / RemoveMotion (queue lifecycle)
|
||||||
|
|
||||||
|
### add_to_queue (L388-392)
|
||||||
|
```csharp
|
||||||
|
public void add_to_queue(int contextID, uint motion, WeenieError jumpErrorCode)
|
||||||
|
{
|
||||||
|
PendingMotions.AddLast(new MotionNode(contextID, motion, jumpErrorCode));
|
||||||
|
PhysicsObj.IsAnimating = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Simple append + set the `IsAnimating` flag unconditionally true (even if this is the first entry). Called from:
|
||||||
|
- `DoInterpretedMotion` (L85) — normal interpreted-motion success path, `jump_error_code` precomputed at L73-83.
|
||||||
|
- `StopCompletely` (L321) — with hardcoded `MotionCommand.Ready` and a `jump` precomputed at L307.
|
||||||
|
- `StopInterpretedMotion` (L348) — with hardcoded `MotionCommand.Ready` and `WeenieError.None`.
|
||||||
|
- `apply_interpreted_movement` (L495) — the turn-stop fallback branch, hardcoded `MotionCommand.Ready` / `WeenieError.None`.
|
||||||
|
|
||||||
|
### MotionDone (L210-234) — the dequeue-on-animation-complete handler
|
||||||
|
```csharp
|
||||||
|
public void MotionDone(bool success)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return;
|
||||||
|
|
||||||
|
var motionData = PendingMotions.First;
|
||||||
|
|
||||||
|
// null or != last in list?
|
||||||
|
if (motionData != null)
|
||||||
|
{
|
||||||
|
var pendingMotion = motionData.Value;
|
||||||
|
if ((pendingMotion.Motion & (uint)CommandMask.Action) != 0)
|
||||||
|
{
|
||||||
|
PhysicsObj.unstick_from_object();
|
||||||
|
InterpretedState.RemoveAction();
|
||||||
|
RawState.RemoveAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
motionData = PendingMotions.First;
|
||||||
|
if (motionData != null)
|
||||||
|
{
|
||||||
|
PendingMotions.Remove(motionData);
|
||||||
|
PhysicsObj.IsAnimating = PendingMotions.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Note the ACE dev's own inline comment `// null or != last in list?` — flags this as a spot they weren't fully certain about vs. retail (a "did I get this right" breadcrumb). Two things happen if the head node exists:
|
||||||
|
1. If the head's Motion has the `Action` bit set (`CommandMask.Action`), it's treated as an "action" motion (e.g. attack/emote) that stuck the object to something — `unstick_from_object()` + pop the action off both InterpretedState and RawState action stacks.
|
||||||
|
2. **Re-fetches `PendingMotions.First` a second time** (redundant re-read — `motionData` unchanged since nothing between the two reads mutates the list) before removing it and recomputing `IsAnimating` as `Count > 0` (as opposed to `add_to_queue`'s unconditional `true`).
|
||||||
|
|
||||||
|
`success` parameter is accepted but **never read** in this method body — dead parameter as far as MotionInterp.MotionDone goes. (It IS read/propagated further down in `MotionTableManager.AnimationDone`/`CheckForCompletedMotions`, see §8 — but `MotionInterp.MotionDone` itself ignores it entirely.)
|
||||||
|
|
||||||
|
### RemoveMotion
|
||||||
|
Not defined in MotionInterp.cs — it's called on `InterpretedState`/`RawState` (`InterpretedState.RemoveMotion(motion)`, `RawState.RemoveMotion(motion)`), i.e. lives in `InterpretedMotionState`/`RawMotionState`/`MotionState` classes, NOT in MotionInterp. Call sites inside this file: L340, L351, L358, L498. Out of scope for this file-bounded pass (not in MotionInterp.cs or MovementManager.cs).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. DoMotion / StopMotion / StopCompletely (top-level motion API)
|
||||||
|
|
||||||
|
### DoMotion (L112-158) — raw-command entry point
|
||||||
|
```csharp
|
||||||
|
public WeenieError DoMotion(uint motion, MovementParameters movementParams)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return WeenieError.NoPhysicsObject;
|
||||||
|
|
||||||
|
var currentParams = new MovementParameters();
|
||||||
|
currentParams.CopySome(movementParams);
|
||||||
|
|
||||||
|
var currentMotion = motion;
|
||||||
|
|
||||||
|
if (movementParams.CancelMoveTo) PhysicsObj.cancel_moveto();
|
||||||
|
if (movementParams.SetHoldKey) SetHoldKey(movementParams.HoldKeyToApply, movementParams.CancelMoveTo);
|
||||||
|
|
||||||
|
adjust_motion(ref currentMotion, ref currentParams.Speed, movementParams.HoldKeyToApply);
|
||||||
|
|
||||||
|
if (InterpretedState.CurrentStyle != (uint)MotionCommand.NonCombat)
|
||||||
|
{
|
||||||
|
switch (motion)
|
||||||
|
{
|
||||||
|
case (uint)MotionCommand.Crouch: return WeenieError.CantCrouchInCombat;
|
||||||
|
case (uint)MotionCommand.Sitting: return WeenieError.CantSitInCombat;
|
||||||
|
case (uint)MotionCommand.Sleeping: return WeenieError.CantLieDownInCombat;
|
||||||
|
}
|
||||||
|
if ((motion & (uint)CommandMask.ChatEmote) != 0) return WeenieError.CantChatEmoteInCombat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((motion & (uint)CommandMask.Action) != 0)
|
||||||
|
{
|
||||||
|
if (InterpretedState.GetNumActions() >= 6) return WeenieError.TooManyActions;
|
||||||
|
}
|
||||||
|
var result = DoInterpretedMotion(currentMotion, currentParams);
|
||||||
|
|
||||||
|
if (result == WeenieError.None && movementParams.ModifyRawState)
|
||||||
|
RawState.ApplyMotion(motion, movementParams);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Key points:
|
||||||
|
- `switch (motion)` (combat-blocked motions) tests the **original** `motion` parameter, not `currentMotion` (post-`adjust_motion` mutation) — combat-block checks happen on the raw pre-adjustment command.
|
||||||
|
- Hardcoded action cap of 6 concurrent actions (`GetNumActions() >= 6`).
|
||||||
|
- `RawState.ApplyMotion` uses the **original** `motion`, not `currentMotion` — raw state records what was actually requested, interpreted state (via `DoInterpretedMotion`) gets the post-`adjust_motion` (walk→run promoted, backward-inverted, etc.) version.
|
||||||
|
|
||||||
|
### StopMotion (L367-386)
|
||||||
|
Mirror of DoMotion but for the "stop" side: `cancel_moveto` gate, `adjust_motion`, delegates to `StopInterpretedMotion`, then conditionally calls `RawState.RemoveMotion(motion)` (original motion, not adjusted) on success.
|
||||||
|
|
||||||
|
### StopCompletely (L301-327)
|
||||||
|
```csharp
|
||||||
|
public WeenieError StopCompletely()
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return WeenieError.NoPhysicsObject;
|
||||||
|
|
||||||
|
PhysicsObj.cancel_moveto();
|
||||||
|
|
||||||
|
var jump = motion_allows_jump(InterpretedState.ForwardCommand);
|
||||||
|
|
||||||
|
RawState.ForwardCommand = (uint)MotionCommand.Ready;
|
||||||
|
RawState.ForwardSpeed = 1.0f;
|
||||||
|
RawState.SideStepCommand = 0;
|
||||||
|
RawState.TurnCommand = 0;
|
||||||
|
|
||||||
|
InterpretedState.ForwardCommand = (uint)MotionCommand.Ready;
|
||||||
|
InterpretedState.ForwardSpeed = 1.0f;
|
||||||
|
InterpretedState.SideStepCommand = 0;
|
||||||
|
InterpretedState.TurnCommand = 0;
|
||||||
|
|
||||||
|
PhysicsObj.StopCompletely_Internal();
|
||||||
|
|
||||||
|
add_to_queue(0, (uint)MotionCommand.Ready, jump);
|
||||||
|
|
||||||
|
if (PhysicsObj.CurCell == null)
|
||||||
|
PhysicsObj.RemoveLinkAnimations();
|
||||||
|
|
||||||
|
return WeenieError.None;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Hard-resets both Raw and Interpreted state (Forward/SideStep/Turn) to Ready/1.0/0/0 directly by field assignment — bypasses the normal `ApplyMotion`/`RemoveMotion` mutators entirely. `jump` (the eligibility precomputed BEFORE the reset, off the OLD `InterpretedState.ForwardCommand`) is stashed into the queued `Ready` node's `JumpErrorCode`. Delegates the physics-side reset to `PhysicsObj.StopCompletely_Internal()` (not in this file).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. DoInterpretedMotion / StopInterpretedMotion (interpreted-command layer)
|
||||||
|
|
||||||
|
### DoInterpretedMotion (L51-110)
|
||||||
|
```csharp
|
||||||
|
public WeenieError DoInterpretedMotion(uint motion, MovementParameters movementParams)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return WeenieError.NoPhysicsObject;
|
||||||
|
var result = WeenieError.None;
|
||||||
|
|
||||||
|
if (contact_allows_move(motion))
|
||||||
|
{
|
||||||
|
if (StandingLongJump && (motion == WalkForward || motion == RunForward || motion == SideStepRight))
|
||||||
|
{
|
||||||
|
if (movementParams.ModifyInterpretedState)
|
||||||
|
InterpretedState.ApplyMotion(motion, movementParams);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (motion == (uint)MotionCommand.Dead)
|
||||||
|
PhysicsObj.RemoveLinkAnimations();
|
||||||
|
|
||||||
|
result = PhysicsObj.DoInterpretedMotion(motion, movementParams);
|
||||||
|
|
||||||
|
if (result == WeenieError.None)
|
||||||
|
{
|
||||||
|
var jump_error_code = WeenieError.None;
|
||||||
|
if (movementParams.DisableJumpDuringLink)
|
||||||
|
jump_error_code = WeenieError.YouCantJumpFromThisPosition;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
jump_error_code = motion_allows_jump(motion);
|
||||||
|
if (jump_error_code == WeenieError.None && (motion & (uint)CommandMask.Action) == 0)
|
||||||
|
jump_error_code = motion_allows_jump(InterpretedState.ForwardCommand);
|
||||||
|
}
|
||||||
|
add_to_queue(movementParams.ContextID, motion, jump_error_code);
|
||||||
|
|
||||||
|
if (movementParams.ModifyInterpretedState)
|
||||||
|
InterpretedState.ApplyMotion(motion, movementParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ((motion & (uint)CommandMask.Action) != 0)
|
||||||
|
result = WeenieError.YouCantJumpWhileInTheAir;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (movementParams.ModifyInterpretedState)
|
||||||
|
InterpretedState.ApplyMotion(motion, movementParams);
|
||||||
|
result = WeenieError.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PhysicsObj.CurCell == null)
|
||||||
|
PhysicsObj.RemoveLinkAnimations();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Control-flow shape:
|
||||||
|
1. Gate on `contact_allows_move(motion)` (§7) — governs whether the object is grounded enough to accept this motion at all.
|
||||||
|
2. **StandingLongJump special-case**: if mid-standing-long-jump AND the incoming motion is one of {WalkForward, RunForward, SideStepRight}, the motion is applied to InterpretedState ONLY (no `PhysicsObj.DoInterpretedMotion` call, no queue entry) — the animation itself is suppressed while charging/airborne from a standing jump, but the *intent* state still updates so movement resumes correctly on landing.
|
||||||
|
3. Otherwise: `Dead` motion clears link animations first; then delegates the actual animation dispatch to `PhysicsObj.DoInterpretedMotion` (physics/animation-table layer, out of file scope); on success, computes the jump-error-code for THIS pending motion (double motion_allows_jump check: first against the incoming `motion` itself, then — only if the incoming motion isn't itself an Action AND passed — against the current `ForwardCommand`), queues it, and conditionally applies to InterpretedState.
|
||||||
|
4. If contact does NOT allow movement: Action-class motions fail outright with `YouCantJumpWhileInTheAir`; everything else silently updates InterpretedState (if requested) and returns success — i.e., non-action motions (turning, etc.) are allowed to update intent state even while airborne, just not animate/queue.
|
||||||
|
5. Unconditional tail: if `CurCell == null` (off the cell grid — e.g. despawned/uninitialized), strip link animations.
|
||||||
|
|
||||||
|
### StopInterpretedMotion (L329-365)
|
||||||
|
Structural mirror of DoInterpretedMotion for the "stop" direction: same `contact_allows_move` gate, same StandingLongJump special-case (calls `InterpretedState.RemoveMotion` instead of `ApplyMotion`), same delegate-to-PhysicsObj pattern (`PhysicsObj.StopInterpretedMotion`), same add_to_queue-with-Ready-on-success pattern, same CurCell==null tail cleanup. The "contact disallows" else-branch here has NO action-vs-non-action split (StopInterpretedMotion always just conditionally calls `RemoveMotion` and returns `WeenieError.None`) — asymmetric with DoInterpretedMotion's stricter "action motions error out while airborne" rule.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. HitGround / LeaveGround (contact transition hooks)
|
||||||
|
|
||||||
|
### HitGround (L175-185)
|
||||||
|
```csharp
|
||||||
|
public void HitGround()
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return;
|
||||||
|
if (WeenieObj != null && !WeenieObj.IsCreature()) return;
|
||||||
|
if (!PhysicsObj.State.HasFlag(PhysicsState.Gravity)) return;
|
||||||
|
|
||||||
|
PhysicsObj.RemoveLinkAnimations();
|
||||||
|
apply_current_movement(false, true);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Guarded to creature-only (or WeenieObj==null, i.e. non-weenie-backed physics objects) AND gravity-affected objects. Strips link animations and re-applies current movement intent with `cancelMoveTo=false, allowJump=true`.
|
||||||
|
|
||||||
|
### LeaveGround (L192-208)
|
||||||
|
```csharp
|
||||||
|
public void LeaveGround()
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return;
|
||||||
|
if (WeenieObj != null && !WeenieObj.IsCreature()) return;
|
||||||
|
if (!PhysicsObj.State.HasFlag(PhysicsState.Gravity)) return;
|
||||||
|
|
||||||
|
var velocity = get_leave_ground_velocity();
|
||||||
|
PhysicsObj.set_local_velocity(velocity, true);
|
||||||
|
|
||||||
|
StandingLongJump = false;
|
||||||
|
JumpExtent = 0;
|
||||||
|
|
||||||
|
PhysicsObj.RemoveLinkAnimations();
|
||||||
|
apply_current_movement(false, true);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Same guard pattern as HitGround. Computes leave-ground velocity (§9), pushes it into physics as a LOCAL velocity (`set_local_velocity(velocity, true)` — second arg likely "isLocal"/"autonomous", not resolved in this file), clears the jump-charge state (`StandingLongJump=false`, `JumpExtent=0`), strips link anims, reapplies movement. **Called from `enter_default_state()` (L615)** as the terminal step of default-state setup — i.e. every newly-initialized MotionInterp starts as if it just left the ground.
|
||||||
|
|
||||||
|
MovementManager wrappers (MovementManager.cs):
|
||||||
|
- `HitGround()` (L66-73): calls `MotionInterpreter.HitGround()` THEN `MoveToManager.HitGround()` — both interpreters get the hit-ground signal, MotionInterp first.
|
||||||
|
- `LeaveGround()` (L104-110): calls `MotionInterpreter.LeaveGround()` only; has a dead commented-out line `// NoticeHandler::RecvNotice_PrevSpellSection` (retail-symbol trace left as a comment — not ported to ACE — a network-notice hook ACE apparently didn't implement here).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Jump family
|
||||||
|
|
||||||
|
### jump (L710-727)
|
||||||
|
```csharp
|
||||||
|
public WeenieError jump(float extent, int adjustStamina)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return WeenieError.NoPhysicsObject;
|
||||||
|
|
||||||
|
PhysicsObj.cancel_moveto();
|
||||||
|
|
||||||
|
var result = jump_is_allowed(extent, adjustStamina);
|
||||||
|
|
||||||
|
if (result == WeenieError.None)
|
||||||
|
{
|
||||||
|
JumpExtent = extent;
|
||||||
|
PhysicsObj.set_on_walkable(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
StandingLongJump = false;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Note: on success it sets `JumpExtent` and clears the walkable flag but does NOT itself call `LeaveGround()` or apply velocity — that must happen elsewhere (likely triggered by the physics tick detecting `on_walkable==false` + `JumpExtent>0`, out of file scope). On failure it clears `StandingLongJump` (defensive — aborts an in-progress standing-long-jump charge if the actual jump call fails).
|
||||||
|
|
||||||
|
### jump_is_allowed (L742-768)
|
||||||
|
```csharp
|
||||||
|
public WeenieError jump_is_allowed(float extent, int staminaCost)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return WeenieError.NoPhysicsObject;
|
||||||
|
|
||||||
|
if (WeenieObj == null && !WeenieObj.IsCreature() || !PhysicsObj.State.HasFlag(PhysicsState.Gravity) ||
|
||||||
|
PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact | TransientStateFlags.OnWalkable))
|
||||||
|
{
|
||||||
|
if (PhysicsObj.IsFullyConstrained())
|
||||||
|
return WeenieError.GeneralMovementFailure;
|
||||||
|
|
||||||
|
if (PendingMotions.Count > 1 && PendingMotions.First.Value.JumpErrorCode != 0)
|
||||||
|
return PendingMotions.First.Value.JumpErrorCode;
|
||||||
|
|
||||||
|
var jumpError = jump_charge_is_allowed();
|
||||||
|
|
||||||
|
if (jumpError == WeenieError.None)
|
||||||
|
{
|
||||||
|
jumpError = motion_allows_jump(InterpretedState.ForwardCommand);
|
||||||
|
|
||||||
|
if (jumpError == WeenieError.None && WeenieObj != null && WeenieObj.JumpStaminaCost(extent, staminaCost) == 0)
|
||||||
|
jumpError = WeenieError.GeneralMovementFailure;
|
||||||
|
}
|
||||||
|
return jumpError;
|
||||||
|
}
|
||||||
|
return WeenieError.YouCantJumpWhileInTheAir;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**LIKELY BUG (ACE-side, flag for cross-check against named-retail):** `WeenieObj == null && !WeenieObj.IsCreature()` — if `WeenieObj` actually is null, `!WeenieObj.IsCreature()` would NPE; C#'s `&&` short-circuits left-to-right so `WeenieObj == null` being true still evaluates the right side... wait, no: `&&` short-circuits so if `WeenieObj == null` is `true`, C# does NOT evaluate `!WeenieObj.IsCreature()` — correct, no NPE. But semantically this condition is almost certainly meant to be `WeenieObj != null && !WeenieObj.IsCreature()` (matching the guard pattern used identically in HitGround/LeaveGround/apply_current_movement/adjust_motion — all of which use `WeenieObj != null && !WeenieObj.IsCreature()`). As written, `WeenieObj == null && !WeenieObj.IsCreature()` can **never be true** (if WeenieObj==null is true, the whole condition short-circuits false because `&&` needs the null-check false... actually WeenieObj==null must be TRUE for this branch, and if it's true the right operand isn't even evaluated, so `WeenieObj==null && [anything]` reduces to just needing `WeenieObj==null` — no, `A && B` requires BOTH true; if A is true B is still checked. If A is true (WeenieObj IS null) and B accesses `WeenieObj.IsCreature()` on a null WeenieObj, this WOULD NPE.** This is a genuine apparent typo vs. the established pattern elsewhere in the same file (should be `WeenieObj != null && !WeenieObj.IsCreature()`) — **flag as a divergence risk to verify against `docs/research/named-retail/` before porting this exact condition to acdream.**
|
||||||
|
- Entry condition: allowed to even check jump-legality if EITHER not-a-creature-weenie, OR not gravity-affected, OR (already in Contact+OnWalkable state).
|
||||||
|
- `IsFullyConstrained()` → GeneralMovementFailure.
|
||||||
|
- **Queue-lookahead check**: if there's more than one pending motion AND the head's precomputed `JumpErrorCode != 0`, return that cached error immediately (short-circuit — reuses the jump-eligibility computed back when that motion was queued, rather than recomputing).
|
||||||
|
- Otherwise chains `jump_charge_is_allowed()` → `motion_allows_jump(ForwardCommand)` → `WeenieObj.JumpStaminaCost(extent, staminaCost) == 0` (stamina check, external to this file) → `GeneralMovementFailure` if stamina call returns 0.
|
||||||
|
- If the outer gate fails, returns `YouCantJumpWhileInTheAir` (airborne + gravity + not a special-cased non-creature).
|
||||||
|
|
||||||
|
### jump_charge_is_allowed (L729-740)
|
||||||
|
```csharp
|
||||||
|
public WeenieError jump_charge_is_allowed()
|
||||||
|
{
|
||||||
|
if (WeenieObj != null && !WeenieObj.CanJump(JumpExtent))
|
||||||
|
return WeenieError.CantJumpLoadedDown;
|
||||||
|
|
||||||
|
var forward = InterpretedState.ForwardCommand;
|
||||||
|
|
||||||
|
if (forward == (uint)MotionCommand.Fallen || forward >= (uint)MotionCommand.Crouch && forward <= (uint)MotionCommand.Sleeping)
|
||||||
|
return WeenieError.YouCantJumpFromThisPosition;
|
||||||
|
|
||||||
|
return WeenieError.None;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`WeenieObj.CanJump(JumpExtent)` is presumably an encumbrance/burden check (name: "loaded down"). Blocks jump-charging while `Fallen` or in the [Crouch..Sleeping] MotionCommand range (a contiguous enum-range test — same idiom used throughout this file for "in one of these seated/prone states").
|
||||||
|
|
||||||
|
### charge_jump (L564-582)
|
||||||
|
```csharp
|
||||||
|
public int charge_jump()
|
||||||
|
{
|
||||||
|
if (WeenieObj != null && !WeenieObj.CanJump(JumpExtent))
|
||||||
|
return 0x49;
|
||||||
|
|
||||||
|
var forward = InterpretedState.ForwardCommand;
|
||||||
|
|
||||||
|
if (forward == (uint)MotionCommand.Falling || forward >= (uint)MotionCommand.Crouch && forward < (uint)MotionCommand.Sleeping)
|
||||||
|
return 0x48;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact | TransientStateFlags.OnWalkable) && forward == (uint)MotionCommand.Ready &&
|
||||||
|
InterpretedState.SideStepCommand == 0 && InterpretedState.TurnCommand == 0)
|
||||||
|
{
|
||||||
|
StandingLongJump = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Returns raw `int` error codes (`0x49`, `0x48`, `0`) rather than `WeenieError` enum — a leftover-looking retail-style raw-hresult-ish return convention (likely these hex values ARE `WeenieError` underlying ints but the method signature wasn't converted — worth checking `0x49`/`0x48` against the `WeenieError` enum values for `CantJumpLoadedDown`/`YouCantJumpFromThisPosition` equivalents). **Note the range-check here uses `Falling` (not `Fallen` as in `jump_charge_is_allowed`) and `< Sleeping` (exclusive) vs `jump_charge_is_allowed`'s `<= Sleeping` (inclusive)** — these two "similar" gating functions have subtly different boundary conditions; flag for retail cross-check, this looks like it could be an ACE transcription inconsistency OR an intentional retail distinction between "starting to charge a jump" vs "already mid-charge-and-issuing-the-actual-jump."
|
||||||
|
The actual `StandingLongJump = true` side-effect only fires when: grounded+walkable, current forward command is exactly `Ready` (standing still), AND no sidestep/turn in progress — i.e. this is the "player pressed-and-held jump while standing still" detector that arms the long-jump-charge special path used throughout DoInterpretedMotion/StopInterpretedMotion/apply_interpreted_movement.
|
||||||
|
|
||||||
|
### get_jump_v_z (L634-652)
|
||||||
|
```csharp
|
||||||
|
public float get_jump_v_z()
|
||||||
|
{
|
||||||
|
if (JumpExtent < PhysicsGlobals.EPSILON) return 0.0f;
|
||||||
|
|
||||||
|
var extent = JumpExtent;
|
||||||
|
if (extent > 1.0f) extent = 1.0f;
|
||||||
|
|
||||||
|
if (WeenieObj == null) return 10.0f;
|
||||||
|
|
||||||
|
float vz = extent;
|
||||||
|
if (WeenieObj.InqJumpVelocity(extent, out vz))
|
||||||
|
return vz;
|
||||||
|
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Clamps `JumpExtent` to [something-above-EPSILON, 1.0], delegates the actual extent→velocity curve to `WeenieObj.InqJumpVelocity` (out of file scope — presumably reads jump skill). Fallback of `10.0f` if there's no WeenieObj at all (non-weenie physics object jumping at max power, e.g. test/editor objects). `float vz = extent;` is a dead initializer — immediately overwritten by the `out vz` param if `InqJumpVelocity` returns true, else the method returns `0.0f` on the false path (never returns the dead-initialized `extent` value).
|
||||||
|
|
||||||
|
### get_leave_ground_velocity (L654-663)
|
||||||
|
```csharp
|
||||||
|
public Vector3 get_leave_ground_velocity()
|
||||||
|
{
|
||||||
|
var velocity = get_state_velocity();
|
||||||
|
velocity.Z = get_jump_v_z();
|
||||||
|
|
||||||
|
if (Vec.IsZero(velocity))
|
||||||
|
velocity = PhysicsObj.Position.GlobalToLocalVec(PhysicsObj.Velocity);
|
||||||
|
|
||||||
|
return velocity;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Composes horizontal velocity from current interpreted-state motion (§9 `get_state_velocity`) with vertical velocity from the jump charge, UNLESS the composed vector is exactly zero — in which case it falls back to converting the physics object's actual (global) velocity into local space. This fallback matters for e.g. falling-off-a-ledge (no jump input, no WASD, but the object still has downward/lateral velocity from having walked off an edge) vs. a genuine standing-still jump.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. HitGround/MotionDone chain — full call graph (cross-file, MotionTableManager.cs)
|
||||||
|
|
||||||
|
The actual "animation finished playing" signal originates in `MotionTableManager` (a SEPARATE class from `MotionInterp`, owned by `PartArray`, NOT by `MovementManager`):
|
||||||
|
|
||||||
|
```
|
||||||
|
MotionTableManager.AnimationDone(bool success) [L28-61]
|
||||||
|
MotionTableManager.CheckForCompletedMotions() [L63-85]
|
||||||
|
-> PhysicsObj.MotionDone(motionID, success) [PhysicsObj.cs L899-903]
|
||||||
|
-> MovementManager.MotionDone(motion, success) [MovementManager.cs L118-122]
|
||||||
|
-> MotionInterpreter.MotionDone(success) [MotionInterp.cs L210-234]
|
||||||
|
-> PhysicsObj.WeenieObj.OnMotionDone(motionID, success) [parallel notify, weenie-layer]
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `AnimationDone` and `CheckForCompletedMotions` in MotionTableManager independently walk `PendingAnimations` (a `LinkedList<AnimNode>`, the animation-table-layer's OWN pending queue — distinct from `MotionInterp.PendingMotions`) and call `PhysicsObj.MotionDone` once per completed entry, followed immediately by `WeenieObj.OnMotionDone` — i.e. **every completed motion fires two independent listeners**: the MotionInterp queue-pop (state-machine bookkeeping) and the WeenieObject notification (game-logic reaction, e.g. quest triggers, sound cues — out of scope here).
|
||||||
|
|
||||||
|
`CheckForCompletedMotions()` reachability:
|
||||||
|
```
|
||||||
|
MotionInterp.PerformMovement(mvs) [L260] -- called after EVERY movement dispatch (Do/Stop/StopCompletely)
|
||||||
|
-> PhysicsObj.CheckForCompletedMotions() [PhysicsObj.cs L296-300]
|
||||||
|
-> PartArray.CheckForCompletedMotions() [PartArray.cs L72-76]
|
||||||
|
-> MotionTableManager.CheckForCompletedMotions() [L63-85]
|
||||||
|
```
|
||||||
|
So every `MovementManager.PerformMovement` → `MotionInterp.PerformMovement` call (§10) ends with an immediate synchronous drain of any already-finished animations at the head of `PendingAnimations` — this is how a 0-frame-length animation (e.g. an instant `Ready` transition) gets its `MotionDone` fired same-tick rather than waiting for the next animation-tick callback.
|
||||||
|
|
||||||
|
`MotionTableManager.UseTime()` (L158-161) also calls `CheckForCompletedMotions()` — this is the periodic (per-tick, presumably driven by `MovementManager.UseTime()` → `MoveToManager.UseTime()`... **NOTE:** `MovementManager.UseTime()` (MovementManager.cs L176-179) only forwards to `MoveToManager.UseTime()`, NOT to `MotionInterpreter`/`MotionTableManager` — so `MotionTableManager.UseTime()`'s caller is NOT in this file pair; it must be driven directly by `PartArray`/`Sequence`'s own per-tick update, bypassing MovementManager entirely).
|
||||||
|
|
||||||
|
`AnimationDone` (L28-61) is the OTHER path into the same `PhysicsObj.MotionDone` call — driven by whatever animation-tick/callback system invokes it directly (not visible in these two files; likely `Sequence`/`AFrame` playback completion). Its loop differs from `CheckForCompletedMotions` in that it decrements a running `AnimationCounter` against each node's `NumAnims` rather than checking `NumAnims != 0` directly — i.e. `AnimationDone` is the incremental "one more anim-frame-group finished" tick, while `CheckForCompletedMotions` is the "resync/drain everything already at zero" batch pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. apply_raw / apply_interpreted / apply_current_movement (state → animation dispatch)
|
||||||
|
|
||||||
|
### apply_current_movement (L430-438) — dispatcher
|
||||||
|
```csharp
|
||||||
|
public void apply_current_movement(bool cancelMoveTo, bool allowJump)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null || !Initted) return;
|
||||||
|
|
||||||
|
if (WeenieObj != null && !WeenieObj.IsCreature() || !PhysicsObj.movement_is_autonomous())
|
||||||
|
apply_interpreted_movement(cancelMoveTo, allowJump);
|
||||||
|
else
|
||||||
|
apply_raw_movement(cancelMoveTo, allowJump);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Routes to raw-vs-interpreted based on: non-creature-weenie OR not-autonomous-movement → interpreted path; creature AND autonomous movement → raw path. (`movement_is_autonomous()` presumably distinguishes server-driven/scripted motion from player-input-driven motion — out of file scope.) Requires `Initted == true` (set at the tail of `enter_default_state`, §11) — calls before init are no-ops.
|
||||||
|
|
||||||
|
### apply_raw_movement (L506-523)
|
||||||
|
```csharp
|
||||||
|
public void apply_raw_movement(bool cancelMoveTo, bool allowJump)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return;
|
||||||
|
|
||||||
|
InterpretedState.CurrentStyle = RawState.CurrentStyle;
|
||||||
|
InterpretedState.ForwardCommand = RawState.ForwardCommand;
|
||||||
|
InterpretedState.ForwardSpeed = RawState.ForwardSpeed;
|
||||||
|
InterpretedState.SideStepCommand = RawState.SideStepCommand;
|
||||||
|
InterpretedState.SideStepSpeed = RawState.SideStepSpeed;
|
||||||
|
InterpretedState.TurnCommand = RawState.TurnCommand;
|
||||||
|
InterpretedState.TurnSpeed = RawState.TurnSpeed;
|
||||||
|
|
||||||
|
adjust_motion(ref InterpretedState.ForwardCommand, ref InterpretedState.ForwardSpeed, RawState.ForwardHoldKey);
|
||||||
|
adjust_motion(ref InterpretedState.SideStepCommand, ref InterpretedState.SideStepSpeed, RawState.SideStepHoldKey);
|
||||||
|
adjust_motion(ref InterpretedState.TurnCommand, ref InterpretedState.TurnSpeed, RawState.TurnHoldKey);
|
||||||
|
|
||||||
|
apply_interpreted_movement(cancelMoveTo, allowJump);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Copies all 7 raw-state fields verbatim into interpreted-state, THEN runs `adjust_motion` (§12) independently over each of the three command/speed pairs (Forward, SideStep, Turn) with their respective per-axis hold-keys, THEN falls through to `apply_interpreted_movement`. This is the "translate the low-level input intent into the higher-level interpreted/animation intent" step — walk→run promotion, backward-inversion, sidestep animation-rate scaling all happen here per-axis.
|
||||||
|
|
||||||
|
### apply_interpreted_movement (L440-504)
|
||||||
|
```csharp
|
||||||
|
public void apply_interpreted_movement(bool cancelMoveTo, bool allowJump)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return;
|
||||||
|
|
||||||
|
var movementParams = new MovementParameters();
|
||||||
|
movementParams.SetHoldKey = false;
|
||||||
|
movementParams.ModifyInterpretedState = false;
|
||||||
|
movementParams.CancelMoveTo = cancelMoveTo;
|
||||||
|
movementParams.DisableJumpDuringLink = !allowJump;
|
||||||
|
|
||||||
|
if (InterpretedState.ForwardCommand == (uint)MotionCommand.RunForward)
|
||||||
|
MyRunRate = InterpretedState.ForwardSpeed;
|
||||||
|
|
||||||
|
DoInterpretedMotion(InterpretedState.CurrentStyle, movementParams);
|
||||||
|
|
||||||
|
if (contact_allows_move(InterpretedState.ForwardCommand))
|
||||||
|
{
|
||||||
|
if (!StandingLongJump)
|
||||||
|
{
|
||||||
|
movementParams.Speed = InterpretedState.ForwardSpeed;
|
||||||
|
DoInterpretedMotion(InterpretedState.ForwardCommand, movementParams);
|
||||||
|
|
||||||
|
if (InterpretedState.SideStepCommand != 0)
|
||||||
|
{
|
||||||
|
movementParams.Speed = InterpretedState.SideStepSpeed;
|
||||||
|
DoInterpretedMotion(InterpretedState.SideStepCommand, movementParams);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
StopInterpretedMotion((uint)MotionCommand.SideStepRight, movementParams);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
movementParams.Speed = 1.0f;
|
||||||
|
DoInterpretedMotion((uint)MotionCommand.Ready, movementParams);
|
||||||
|
StopInterpretedMotion((uint)MotionCommand.SideStepRight, movementParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
movementParams.Speed = 1.0f;
|
||||||
|
DoInterpretedMotion((uint)MotionCommand.Falling, movementParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InterpretedState.TurnCommand != 0)
|
||||||
|
{
|
||||||
|
movementParams.Speed = InterpretedState.TurnSpeed;
|
||||||
|
DoInterpretedMotion(InterpretedState.TurnCommand, movementParams);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var result = PhysicsObj.StopInterpretedMotion((uint)MotionCommand.TurnRight, movementParams);
|
||||||
|
if (result == WeenieError.None)
|
||||||
|
{
|
||||||
|
add_to_queue(movementParams.ContextID, (uint)MotionCommand.Ready, WeenieError.None);
|
||||||
|
if (movementParams.ModifyInterpretedState)
|
||||||
|
InterpretedState.RemoveMotion((uint)MotionCommand.TurnRight);
|
||||||
|
}
|
||||||
|
if (PhysicsObj.CurCell == null)
|
||||||
|
PhysicsObj.RemoveLinkAnimations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
This is the per-axis re-dispatch of the CURRENT interpreted state as fresh `DoInterpretedMotion`/`StopInterpretedMotion` calls (used both after raw→interpreted translation, and directly whenever contact/gravity transitions need to re-trigger animation — e.g. HitGround/LeaveGround/SetPhysicsObject/SetWeenieObject/SetHoldKey/set_hold_run all funnel here via `apply_current_movement`). Sequence:
|
||||||
|
1. `CurrentStyle` motion (stance) always re-dispatched first.
|
||||||
|
2. `MyRunRate` cache updated from `ForwardSpeed` whenever currently running (side-channel — persists the "last known run speed" independent of whatever WeenieObj.InqRunRate later reports).
|
||||||
|
3. If contact allows movement: normal case re-dispatches Forward + (SideStep OR explicit SideStepRight-stop); StandingLongJump case instead forces Ready + explicit SideStepRight-stop (suppresses forward/sidestep animation while charging a standing jump, matching the earlier note in DoInterpretedMotion).
|
||||||
|
4. If contact does NOT allow movement: unconditionally dispatches `Falling` (Speed=1.0) regardless of what ForwardCommand was.
|
||||||
|
5. Turn is handled as its own independent branch — Turn re-dispatches like Forward/SideStep, but the "stop" path when TurnCommand==0 is inlined directly here (calls `PhysicsObj.StopInterpretedMotion` — the PHYSICS layer method, not `this.StopInterpretedMotion` — and duplicates the add_to_queue/RemoveMotion/RemoveLinkAnimations bookkeeping inline rather than delegating to `this.StopInterpretedMotion`). This inline duplication vs delegating is a structural oddity worth flagging — every other "stop with bookkeeping" path in this file goes through the member `StopInterpretedMotion` method, but this one open-codes it against `PhysicsObj.StopInterpretedMotion` directly.
|
||||||
|
|
||||||
|
### get_state_velocity (L678-700) — used by get_leave_ground_velocity (§7)
|
||||||
|
```csharp
|
||||||
|
public Vector3 get_state_velocity()
|
||||||
|
{
|
||||||
|
var velocity = Vector3.Zero;
|
||||||
|
|
||||||
|
if (InterpretedState.SideStepCommand == (uint)MotionCommand.SideStepRight)
|
||||||
|
velocity.X = SidestepAnimSpeed * InterpretedState.SideStepSpeed;
|
||||||
|
if (InterpretedState.ForwardCommand == (uint)MotionCommand.WalkForward)
|
||||||
|
velocity.Y = WalkAnimSpeed * InterpretedState.ForwardSpeed;
|
||||||
|
else if (InterpretedState.ForwardCommand == (uint)MotionCommand.RunForward)
|
||||||
|
velocity.Y = RunAnimSpeed * InterpretedState.ForwardSpeed;
|
||||||
|
|
||||||
|
var rate = MyRunRate;
|
||||||
|
if (WeenieObj != null) WeenieObj.InqRunRate(ref rate);
|
||||||
|
|
||||||
|
var maxSpeed = RunAnimSpeed * rate;
|
||||||
|
if (velocity.Length() > maxSpeed)
|
||||||
|
{
|
||||||
|
velocity = Vector3.Normalize(velocity);
|
||||||
|
velocity *= maxSpeed;
|
||||||
|
}
|
||||||
|
return velocity;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
X = lateral (sidestep) component, Y = forward component (local space — X/Y here are NOT world axes). Clamped to a max speed derived from `RunAnimSpeed * runRate` — note `WeenieObj.InqRunRate(ref rate)`'s return value is discarded here (unlike `get_adjusted_max_speed`/`get_max_speed` which check the bool return and fall back to `MyRunRate` only if it returns false) — `rate` is pre-seeded with `MyRunRate` and then `InqRunRate` is allowed to overwrite it unconditionally regardless of success/failure return.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. PerformMovement (MotionInterp) vs PerformMovement (MovementManager)
|
||||||
|
|
||||||
|
### MotionInterp.PerformMovement (L236-262)
|
||||||
|
```csharp
|
||||||
|
public WeenieError PerformMovement(MovementStruct mvs)
|
||||||
|
{
|
||||||
|
var result = WeenieError.None;
|
||||||
|
switch (mvs.Type)
|
||||||
|
{
|
||||||
|
case MovementType.RawCommand: result = DoMotion(mvs.Motion, mvs.Params); break;
|
||||||
|
case MovementType.InterpretedCommand: result = DoInterpretedMotion(mvs.Motion, mvs.Params); break;
|
||||||
|
case MovementType.StopRawCommand: result = StopMotion(mvs.Motion, mvs.Params); break;
|
||||||
|
case MovementType.StopInterpretedCommand: result = StopInterpretedMotion(mvs.Motion, mvs.Params); break;
|
||||||
|
case MovementType.StopCompletely: result = StopCompletely(); break;
|
||||||
|
default: return WeenieError.GeneralMovementFailure;
|
||||||
|
}
|
||||||
|
PhysicsObj.CheckForCompletedMotions();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Dispatch table over `MovementType`; unconditionally drains completed motions (§8) after every dispatched call (except the `default`/unknown-type early-return, which skips the drain).
|
||||||
|
|
||||||
|
### MovementManager.PerformMovement (L124-157)
|
||||||
|
```csharp
|
||||||
|
public WeenieError PerformMovement(MovementStruct mvs)
|
||||||
|
{
|
||||||
|
PhysicsObj.set_active(true);
|
||||||
|
|
||||||
|
switch (mvs.Type)
|
||||||
|
{
|
||||||
|
case MovementType.RawCommand:
|
||||||
|
case MovementType.InterpretedCommand:
|
||||||
|
case MovementType.StopRawCommand:
|
||||||
|
case MovementType.StopInterpretedCommand:
|
||||||
|
case MovementType.StopCompletely:
|
||||||
|
if (MotionInterpreter == null)
|
||||||
|
{
|
||||||
|
MotionInterpreter = MotionInterp.Create(PhysicsObj, WeenieObj);
|
||||||
|
if (PhysicsObj != null) MotionInterpreter.enter_default_state();
|
||||||
|
}
|
||||||
|
return MotionInterpreter.PerformMovement(mvs);
|
||||||
|
|
||||||
|
case MovementType.MoveToObject:
|
||||||
|
case MovementType.MoveToPosition:
|
||||||
|
case MovementType.TurnToObject:
|
||||||
|
case MovementType.TurnToHeading:
|
||||||
|
if (MoveToManager == null)
|
||||||
|
MoveToManager = MoveToManager.Create(PhysicsObj, WeenieObj);
|
||||||
|
return MoveToManager.PerformMovement(mvs);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return WeenieError.GeneralMovementFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Outer layer: unconditionally activates the physics object (`set_active(true)`) BEFORE dispatch, lazy-constructs `MotionInterpreter` (with `enter_default_state()`) on first use for the motion-command group, lazy-constructs `MoveToManager` on first use for the moveto/turnto group. This is the true entry point most game-logic code calls — `MotionInterp.PerformMovement` is only reachable through this wrapper (or directly if something already holds a `MotionInterp` reference, e.g. via `get_minterp()`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Lifecycle: enter_default_state / HandleExitWorld / HandleEnterWorld
|
||||||
|
|
||||||
|
### MotionInterp.enter_default_state (L604-616)
|
||||||
|
```csharp
|
||||||
|
public void enter_default_state()
|
||||||
|
{
|
||||||
|
RawState = new RawMotionState();
|
||||||
|
InterpretedState = new InterpretedMotionState();
|
||||||
|
|
||||||
|
PhysicsObj.InitializeMotionTables();
|
||||||
|
PendingMotions = new LinkedList<MotionNode>(); // ??
|
||||||
|
|
||||||
|
add_to_queue(0, (uint)MotionCommand.Ready, 0);
|
||||||
|
|
||||||
|
Initted = true;
|
||||||
|
LeaveGround();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Note the ACE dev's own `// ??` comment on the `PendingMotions` reset — another self-flagged uncertainty spot (worth checking retail decomp for whether PendingMotions is genuinely reset here or whether retail preserves/asserts-empty). Order: fresh Raw/Interpreted state objects → physics-layer motion table init → fresh pending-motion queue → seed the queue with one `Ready` motion (contextID=0, jumpErrorCode=`WeenieError.None`=0) → flip `Initted=true` → call `LeaveGround()` (§6) which itself is now unlocked since `Initted` gates nothing in LeaveGround directly, but LeaveGround calls `apply_current_movement` which DOES gate on `Initted`.
|
||||||
|
|
||||||
|
### MovementManager.EnterDefaultState (L38-46)
|
||||||
|
```csharp
|
||||||
|
public void EnterDefaultState()
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return;
|
||||||
|
if (MotionInterpreter == null)
|
||||||
|
MotionInterpreter = MotionInterp.Create(PhysicsObj, WeenieObj);
|
||||||
|
MotionInterpreter.enter_default_state();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Thin lazy-construct + delegate wrapper.
|
||||||
|
|
||||||
|
### HandleExitWorld — two versions
|
||||||
|
**MotionInterp.HandleExitWorld (L160-173):**
|
||||||
|
```csharp
|
||||||
|
public void HandleExitWorld()
|
||||||
|
{
|
||||||
|
foreach (var pendingMotion in PendingMotions)
|
||||||
|
{
|
||||||
|
if (PhysicsObj != null && (pendingMotion.Motion & (uint)CommandMask.Action) != 0)
|
||||||
|
{
|
||||||
|
PhysicsObj.unstick_from_object();
|
||||||
|
InterpretedState.RemoveAction();
|
||||||
|
RawState.RemoveAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PendingMotions.Clear();
|
||||||
|
if (PhysicsObj != null) PhysicsObj.IsAnimating = false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Iterates ALL pending motions (not just the head, unlike `MotionDone`) — for EVERY pending motion that has the Action bit set, unsticks + pops an action off both state stacks (this will over-pop if multiple pending Action motions exist simultaneously and InterpretedState/RawState only track a bounded action stack — worth checking `GetNumActions()`'s cap of 6 against how many action-removals this loop could trigger). Then clears the whole queue and force-sets `IsAnimating = false` directly (bypassing the `Count > 0` recompute that `MotionDone` uses).
|
||||||
|
|
||||||
|
**MovementManager.HandleExitWorld (L54-58):** thin delegate, `if (MotionInterpreter != null) MotionInterpreter.HandleExitWorld();` — no MoveToManager involvement (contrast with `HitGround` which drives both).
|
||||||
|
|
||||||
|
### MovementManager.HandleEnterWorld (L48-52)
|
||||||
|
```csharp
|
||||||
|
public void HandleEnterWorld()
|
||||||
|
{
|
||||||
|
//if (MotionInterpreter != null)
|
||||||
|
//NoticeHandler.RecvNotice_PrevSpellSelection(MotionInterpreter);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Entirely commented out — dead/no-op in ACE. The commented reference to `NoticeHandler.RecvNotice_PrevSpellSelection` is a retail-symbol breadcrumb (spell-selection restore notice on world-enter) that ACE chose not to implement. **Flag for acdream: if named-retail decomp confirms this notice is meaningful (e.g. restoring a previously-selected spell/combat-style UI state on relog), this is a genuine ACE gap, not just dead weight — acdream may need to port it from decomp directly since ACE has nothing to copy.**
|
||||||
|
|
||||||
|
Also note: `MotionTableManager.HandleEnterWorld(Sequence sequence)` (MotionTableManager.cs L103-108) is a DIFFERENT, unrelated `HandleEnterWorld` on a different class — it drains `PendingAnimations` via repeated `AnimationDone(false)` calls and clears link animations on the sequence. Not reachable from `MovementManager.HandleEnterWorld` (which is a no-op) — must be invoked from elsewhere (PartArray or the weenie enter-world path, out of scope).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. adjust_motion / apply_run_to_command (raw→interpreted transform helpers)
|
||||||
|
|
||||||
|
### adjust_motion (L394-428)
|
||||||
|
```csharp
|
||||||
|
public void adjust_motion(ref uint motion, ref float speed, HoldKey holdKey)
|
||||||
|
{
|
||||||
|
if (WeenieObj != null && !WeenieObj.IsCreature())
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (motion)
|
||||||
|
{
|
||||||
|
case (uint)MotionCommand.RunForward:
|
||||||
|
return;
|
||||||
|
|
||||||
|
case (uint)MotionCommand.WalkBackwards:
|
||||||
|
motion = (uint)MotionCommand.WalkForward;
|
||||||
|
speed *= -BackwardsFactor; // -0.65
|
||||||
|
break;
|
||||||
|
|
||||||
|
case (uint)MotionCommand.TurnLeft:
|
||||||
|
motion = (uint)MotionCommand.TurnRight;
|
||||||
|
speed *= -1.0f;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case (uint)MotionCommand.SideStepLeft:
|
||||||
|
motion = (uint)MotionCommand.SideStepRight;
|
||||||
|
speed *= -1.0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (motion == (uint)MotionCommand.SideStepRight)
|
||||||
|
speed *= SidestepFactor * (WalkAnimSpeed / SidestepAnimSpeed); // 0.5 * (3.12/1.25) = 1.248
|
||||||
|
|
||||||
|
if (holdKey == HoldKey.Invalid)
|
||||||
|
holdKey = RawState.CurrentHoldKey;
|
||||||
|
|
||||||
|
if (holdKey == HoldKey.Run)
|
||||||
|
apply_run_to_command(ref motion, ref speed);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Canonicalizes "left/backward" variants into their "right/forward" counterparts with negated speed (single-animation-per-axis-direction retail convention: there's no separate WalkBackwards/TurnLeft/SideStepLeft animation state, just the canonical one played at negative speed). `WalkBackwards` gets an EXTRA `-BackwardsFactor` (0.65) scalar on top of the sign flip — backward walking is intentionally slower than forward. `SideStepRight` always gets rescaled by `SidestepFactor * (WalkAnimSpeed/SidestepAnimSpeed)` ≈ 1.248 regardless of hold-key, to convert a walk-speed-denominated input into the sidestep animation's own speed scale. `HoldKey.Invalid` falls back to whatever `RawState.CurrentHoldKey` currently is; if the resolved hold key is `Run`, defers to `apply_run_to_command` for the walk→run promotion.
|
||||||
|
|
||||||
|
### apply_run_to_command (L525-562)
|
||||||
|
```csharp
|
||||||
|
public void apply_run_to_command(ref uint motion, ref float speed)
|
||||||
|
{
|
||||||
|
var speedMod = 1.0f;
|
||||||
|
|
||||||
|
if (WeenieObj != null)
|
||||||
|
{
|
||||||
|
var runFactor = 0.0f;
|
||||||
|
if (WeenieObj.InqRunRate(ref runFactor))
|
||||||
|
speedMod = runFactor;
|
||||||
|
else
|
||||||
|
speedMod = MyRunRate;
|
||||||
|
}
|
||||||
|
switch (motion)
|
||||||
|
{
|
||||||
|
case (uint)MotionCommand.WalkForward:
|
||||||
|
if (speed > 0.0f)
|
||||||
|
motion = (uint)MotionCommand.RunForward;
|
||||||
|
speed *= speedMod;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case (uint)MotionCommand.TurnRight:
|
||||||
|
speed *= RunTurnFactor; // 1.5
|
||||||
|
break;
|
||||||
|
|
||||||
|
case (uint)MotionCommand.SideStepRight:
|
||||||
|
speed *= speedMod;
|
||||||
|
if (MaxSidestepAnimRate < Math.Abs(speed))
|
||||||
|
speed = speed > 0.0f ? MaxSidestepAnimRate : -MaxSidestepAnimRate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`WalkForward` promotes to `RunForward` ONLY if `speed > 0.0f` — i.e. a negative-speed "walk forward" (which per `adjust_motion` is actually a canonicalized WalkBackwards) does NOT get promoted to running even while the Run hold-key is active; backward movement stays at walk-animation regardless of hold-key. `TurnRight` while running gets a flat 1.5x speed multiplier (turns faster while running). `SideStepRight` gets the run-rate multiplier applied AND is then hard-clamped to ±`MaxSidestepAnimRate` (3.0) — this clamp exists ONLY in the run-path, not in the base `adjust_motion` sidestep scaling, meaning sidestep-while-walking is unclamped but sidestep-while-running is capped.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. contact_allows_move / motion_allows_jump / is_standing_still (gating predicates)
|
||||||
|
|
||||||
|
### contact_allows_move (L584-602)
|
||||||
|
```csharp
|
||||||
|
public bool contact_allows_move(uint motion)
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null) return false;
|
||||||
|
|
||||||
|
if (motion == (uint)MotionCommand.Dead || motion == (uint)MotionCommand.Falling ||
|
||||||
|
motion >= (uint)MotionCommand.TurnRight && motion <= (uint)MotionCommand.TurnLeft)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (WeenieObj != null && !WeenieObj.IsCreature())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!PhysicsObj.State.HasFlag(PhysicsState.Gravity))
|
||||||
|
return true;
|
||||||
|
if (!PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact))
|
||||||
|
return false;
|
||||||
|
if (PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Always-allowed motion classes: `Dead`, `Falling`, and the [TurnRight..TurnLeft] enum range (turning is always allowed regardless of contact — matches `apply_interpreted_movement`'s independent Turn-branch handling). Non-creature weenies bypass all contact gating. Non-gravity objects bypass gating. Otherwise: requires BOTH `Contact` AND `OnWalkable` transient flags to allow movement; `Contact` without `OnWalkable` (e.g. touching a wall/ceiling, not a floor) explicitly disallows.
|
||||||
|
|
||||||
|
### motion_allows_jump (L770-779)
|
||||||
|
```csharp
|
||||||
|
public WeenieError motion_allows_jump(uint substate)
|
||||||
|
{
|
||||||
|
if (substate >= (uint)MotionCommand.Reload && substate <= (uint)MotionCommand.Pickup ||
|
||||||
|
substate >= (uint)MotionCommand.TripleThrustLow && substate <= (uint)MotionCommand.MagicPowerUp07Purple ||
|
||||||
|
substate >= (uint)MotionCommand.MagicPowerUp01 && substate <= (uint)MotionCommand.MagicPowerUp10 ||
|
||||||
|
substate >= (uint)MotionCommand.Crouch && substate <= (uint)MotionCommand.Sleeping ||
|
||||||
|
substate >= (uint)MotionCommand.AimLevel && substate <= (uint)MotionCommand.MagicPray ||
|
||||||
|
substate == (uint)MotionCommand.Falling)
|
||||||
|
{
|
||||||
|
return WeenieError.YouCantJumpFromThisPosition;
|
||||||
|
}
|
||||||
|
return WeenieError.None;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Five contiguous MotionCommand enum ranges + one exact match, ALL block jumping: [Reload..Pickup], [TripleThrustLow..MagicPowerUp07Purple], [MagicPowerUp01..MagicPowerUp10], [Crouch..Sleeping], [AimLevel..MagicPray], and exactly `Falling`. This depends entirely on the ORDER of values in the `MotionCommand` enum matching retail's numeric ordering — **any acdream MotionCommand enum that doesn't preserve retail's exact ordinal layout for these ranges will silently break this range-check port.** Called from: `DoInterpretedMotion` (jump-error precompute, twice — against incoming motion AND against ForwardCommand), `StopCompletely` (against ForwardCommand before reset), `jump_is_allowed` (against ForwardCommand), `move_to_interpreted_state` (against ForwardCommand, to gate `allowJump`).
|
||||||
|
|
||||||
|
### is_standing_still (L702-708)
|
||||||
|
```csharp
|
||||||
|
public bool is_standing_still()
|
||||||
|
{
|
||||||
|
return PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact | TransientStateFlags.OnWalkable) &&
|
||||||
|
InterpretedState.ForwardCommand == (uint)MotionCommand.Ready &&
|
||||||
|
InterpretedState.SideStepCommand == 0 &&
|
||||||
|
InterpretedState.TurnCommand == 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Same predicate shape as the `StandingLongJump` arm-condition inlined in `charge_jump()` (§7) — grounded+walkable, Ready forward, zero sidestep, zero turn. Not called anywhere within these two files (public API surface for external callers, e.g. WeenieObj/game-logic layer, out of scope).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Misc small methods
|
||||||
|
|
||||||
|
- **InqStyle (L187-190):** `return InterpretedState.CurrentStyle;` — trivial accessor, returned as `long` despite `CurrentStyle` presumably being `uint` (implicit widening).
|
||||||
|
- **SetHoldKey (L274-287):** only handles `HoldKey.None` explicitly — if the NEW key is `None` AND the current key was `Run`, clears to `None` and re-applies movement; the `HoldKey.Run` case (or others) falls through the switch with NO explicit case, meaning setting the hold key TO `Run` via `SetHoldKey` is a silent no-op unless `key == RawState.CurrentHoldKey` returns early first — the only way `HoldKey.Run` actually gets set appears to be `set_hold_run` (below), not `SetHoldKey`. Early-return guard: `if (key == RawState.CurrentHoldKey) return;` — no-op if unchanged.
|
||||||
|
- **set_hold_run (L826-833):**
|
||||||
|
```csharp
|
||||||
|
public void set_hold_run(int val, bool cancelMoveTo)
|
||||||
|
{
|
||||||
|
if ((val == 0) != (RawState.CurrentHoldKey != HoldKey.Run))
|
||||||
|
{
|
||||||
|
RawState.CurrentHoldKey = val != 0 ? HoldKey.Run : HoldKey.None;
|
||||||
|
apply_current_movement(cancelMoveTo, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
XOR-style guard: `(val==0) != (CurrentHoldKey != Run)` is true exactly when `val` and the "is currently Run" state disagree, i.e. this is a real toggle. Sets `HoldKey.Run` or `HoldKey.None` (never `Invalid` or others) and re-applies movement with `allowJump=true` unconditionally.
|
||||||
|
- **SetPhysicsObject (L289-293) / SetWeenieObject (L295-299):** both simply assign the field then call `apply_current_movement(true, true)` (force cancelMoveTo=true, allowJump=true) — any (re)binding of either owning object triggers a full movement re-application.
|
||||||
|
- **get_adjusted_max_speed (L618-632):**
|
||||||
|
```csharp
|
||||||
|
public float get_adjusted_max_speed()
|
||||||
|
{
|
||||||
|
var rate = 1.0f;
|
||||||
|
if (WeenieObj != null)
|
||||||
|
{
|
||||||
|
if (!WeenieObj.InqRunRate(ref rate))
|
||||||
|
rate = MyRunRate;
|
||||||
|
}
|
||||||
|
if (InterpretedState.ForwardCommand == (uint)MotionCommand.RunForward)
|
||||||
|
rate = InterpretedState.ForwardSpeed / CurrentSpeedFactor;
|
||||||
|
return rate * 4.0f;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Note: if currently running, `rate` is OVERWRITTEN entirely by `ForwardSpeed / CurrentSpeedFactor` — the `WeenieObj.InqRunRate`/`MyRunRate` lookup above becomes dead work in that branch. `4.0f` is presumably `RunAnimSpeed` inlined rather than referencing the constant (magic-number duplication — flag for acdream: use the named constant, don't inline `4.0f`).
|
||||||
|
- **get_max_speed (L665-676):** simpler sibling — `RunAnimSpeed * rate` where `rate` comes from `InqRunRate`/`MyRunRate` fallback, no ForwardCommand override. Two "max speed" methods with different semantics (adjusted = "current effective speed accounting for what's actually playing," max = "theoretical max run speed") — both present, worth checking retail naming/usage split before porting just one.
|
||||||
|
- **motions_pending (L784-787) — MotionInterp:** `return PendingMotions.Count > 0;` with an XML doc comment noting `PhysicsObj.IsAnimating` is the faster equivalent. **PhysicsObj.motions_pending() (PhysicsObj.cs L4244-4247)** is a DIFFERENT, simpler method: `return IsAnimating;` directly — i.e. PhysicsObj's own `motions_pending()` already takes the fast path the MotionInterp doc-comment recommends; **MovementManager.motions_pending() (L195-200)** delegates to `MotionInterpreter.motions_pending()` (the SLOW `Count>0` path, not the fast `IsAnimating` path) when `MotionInterpreter != null`, else returns `false` — so the three `motions_pending()` overloads across PhysicsObj/MovementManager/MotionInterp are NOT all equivalent-cost, and MovementManager's public-facing one takes the slower route.
|
||||||
|
- **move_to_interpreted_state (L789-824):** syncs an externally-supplied `InterpretedMotionState` (e.g. from a network update / DR snapshot) into this instance. Computes `allowJump` from the OLD `ForwardCommand` (before `copy_movement_from` overwrites it) via `motion_allows_jump(...) == WeenieError.None`. Replays the incoming state's `Actions` list through a **sequence-stamp wraparound comparator**: `currentStamp = action.Stamp & 0x7FFF`, `serverStamp = ServerActionStamp & 0x7FFFF` (**note: differing mask widths, `0x7FFF` (15 bits) vs `0x7FFFF` (19 bits) — likely a typo, should probably both be the same mask; flag for retail cross-check**), `deltaStamp = abs(currentStamp - serverStamp)`, and picks a "is this newer" test based on whether the delta exceeds `0x3FFF` (half of 15-bit range) — a classic sequence-number wraparound comparison. Actions only replay if `WeenieObj.IsCreature() || action.Autonomous`.
|
||||||
|
- **ReportExhaustion (L264-272):**
|
||||||
|
```csharp
|
||||||
|
public void ReportExhaustion()
|
||||||
|
{
|
||||||
|
if (PhysicsObj == null || !Initted) return;
|
||||||
|
if (WeenieObj == null || WeenieObj.IsCreature() && PhysicsObj.movement_is_autonomous())
|
||||||
|
apply_raw_movement(false, false);
|
||||||
|
else
|
||||||
|
apply_interpreted_movement(false, false);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Same raw-vs-interpreted split condition shape as `apply_current_movement` but with the WeenieObj null-check ADDED to the raw-path condition (here: `WeenieObj==null || (IsCreature && autonomous)` routes to raw; there: `WeenieObj!=null && !IsCreature || !autonomous` routes to interpreted) — these two conditions are NOT exact logical complements of each other across the two methods (worth a careful truth-table comparison before assuming they're meant to be identical dispatch logic). Both calls pass `cancelMoveTo=false, allowJump=false` — exhaustion reporting never cancels an active moveto and never allows a fresh jump.
|
||||||
|
MovementManager.ReportExhaustion (L159-165) is a thin delegate with a dead commented-out `// NoticeHandler::RecvNotice_PrevSpellSelection` line, same as HandleEnterWorld.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. MovementManager remaining surface (not covered above)
|
||||||
|
|
||||||
|
- **CancelMoveTo / HandleUpdateTarget / IsMovingTo / MakeMoveToManager / UseTime:** all thin null-guarded delegates to `MoveToManager` — no MotionInterp involvement. `MakeMoveToManager()` (L112-116) lazy-constructs `MoveToManager` WITHOUT calling any init-state method (contrast with the `MotionInterpreter` lazy-construct sites which always follow with `enter_default_state()`).
|
||||||
|
- **InqInterpretedMotionState / InqRawMotionState / get_minterp:** identical three-line lazy-construct-with-enter_default_state pattern, just returning a different field (`.InterpretedState`, `.RawState`, or the interpreter itself).
|
||||||
|
- **unpack_movement(object addr, uint size) (L213):** empty body — `{ }`. Stub/no-op, presumably a retail network-deserialization entry point ACE doesn't need (server already has structured data, doesn't need to unpack a wire buffer here) or hasn't ported. Flag: if named-retail decomp shows this doing real work, it's client-side-only logic ACE correctly skips (server doesn't need to interpret its own outbound format), OR it's a genuine gap — check the decomp before assuming either way.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary table — ACE method inventory vs retail-decomp cross-check TODO
|
||||||
|
|
||||||
|
| ACE method | File:Line | Retail decomp checked this pass? |
|
||||||
|
|---|---|---|
|
||||||
|
| `add_to_queue` | MotionInterp.cs:388 | No — ACE-only pass |
|
||||||
|
| `MotionDone` | MotionInterp.cs:210 | No |
|
||||||
|
| `DoMotion` | MotionInterp.cs:112 | No |
|
||||||
|
| `DoInterpretedMotion` | MotionInterp.cs:51 | No |
|
||||||
|
| `StopMotion` | MotionInterp.cs:367 | No |
|
||||||
|
| `StopInterpretedMotion` | MotionInterp.cs:329 | No |
|
||||||
|
| `StopCompletely` | MotionInterp.cs:301 | No |
|
||||||
|
| `HitGround` | MotionInterp.cs:175 | No |
|
||||||
|
| `LeaveGround` | MotionInterp.cs:192 | No |
|
||||||
|
| `jump` | MotionInterp.cs:710 | No |
|
||||||
|
| `jump_is_allowed` | MotionInterp.cs:742 | No — **flag: possible null-guard typo L747** |
|
||||||
|
| `jump_charge_is_allowed` | MotionInterp.cs:729 | No |
|
||||||
|
| `charge_jump` | MotionInterp.cs:564 | No — **flag: Falling/Fallen + range-inclusivity mismatch vs jump_charge_is_allowed** |
|
||||||
|
| `get_jump_v_z` | MotionInterp.cs:634 | No |
|
||||||
|
| `get_leave_ground_velocity` | MotionInterp.cs:654 | No |
|
||||||
|
| `enter_default_state` | MotionInterp.cs:604 | No — **flag: `// ??` on PendingMotions reset** |
|
||||||
|
| `apply_raw_movement` | MotionInterp.cs:506 | No |
|
||||||
|
| `apply_interpreted_movement` | MotionInterp.cs:440 | No — **flag: inline PhysicsObj.StopInterpretedMotion duplication in Turn-stop branch** |
|
||||||
|
| `apply_current_movement` | MotionInterp.cs:430 | No |
|
||||||
|
| `adjust_motion` | MotionInterp.cs:394 | No |
|
||||||
|
| `apply_run_to_command` | MotionInterp.cs:525 | No |
|
||||||
|
| `contact_allows_move` | MotionInterp.cs:584 | No |
|
||||||
|
| `motion_allows_jump` | MotionInterp.cs:770 | No — **depends on MotionCommand enum ordinal layout matching retail exactly** |
|
||||||
|
| `move_to_interpreted_state` | MotionInterp.cs:789 | No — **flag: 0x7FFF vs 0x7FFFF mask width mismatch** |
|
||||||
|
| `MovementManager.PerformMovement` | MovementManager.cs:124 | No |
|
||||||
|
| `MovementManager.HandleEnterWorld` | MovementManager.cs:48 | No — **dead/commented; retail NoticeHandler::RecvNotice_PrevSpellSelection not ported** |
|
||||||
|
| `MotionTableManager.AnimationDone` | MotionTableManager.cs:28 | No |
|
||||||
|
| `MotionTableManager.CheckForCompletedMotions` | MotionTableManager.cs:63 | No |
|
||||||
|
|
||||||
|
All rows above are candidates for Step 0 (`grep named-retail/acclient_2013_pseudo_c.txt` by `CMotionInterp::<method>` — note retail almost certainly uses the `C`-prefixed class name ACE dropped) before any acdream port work proceeds, per CLAUDE.md's mandatory workflow.
|
||||||
1698
docs/research/2026-07-02-r3-motioninterp/r3-motioninterp-decomp.md
Normal file
1698
docs/research/2026-07-02-r3-motioninterp/r3-motioninterp-decomp.md
Normal file
File diff suppressed because it is too large
Load diff
377
docs/research/2026-07-02-r3-motioninterp/r3-port-plan.md
Normal file
377
docs/research/2026-07-02-r3-motioninterp/r3-port-plan.md
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
# R3 port work-list — CMotionInterp completion + local-player unification
|
||||||
|
|
||||||
|
Inputs: `r3-motioninterp-decomp.md` (verbatim retail extraction, same dir),
|
||||||
|
`r3-ace-motioninterp.md` (ACE cross-ref, same dir), plan of record
|
||||||
|
`docs/plans/2026-07-02-retail-motion-animation-rewrite.md` (stage R3), R2 boundary
|
||||||
|
contract `docs/research/2026-07-02-r2-motiontable/r2-port-plan.md` §4, current code
|
||||||
|
`src/AcDream.Core/Physics/MotionInterpreter.cs` (1426 lines, post-D6/S2a),
|
||||||
|
`src/AcDream.App/Input/PlayerMovementController.cs` (1521 lines),
|
||||||
|
`src/AcDream.Core/Physics/InterpretedMotionFunnel.cs`, GameWindow K-fix18 sites.
|
||||||
|
|
||||||
|
**Precondition / state at R3 start (updated at vaulting, 2026-07-02):** R1 complete
|
||||||
|
(`a987cad1` P6). R2 is IN FLIGHT: Q0 (`dc54a3e4`) + Q1 MotionState (`2345da30`) +
|
||||||
|
Q2 `Motion/CMotionTable.cs` (`98f58db9`, 44 tests) committed;
|
||||||
|
Q3 (MotionTableManager + `IMotionDoneSink`) in flight, Q4 (adapter cutover +
|
||||||
|
queue-drain wiring) and Q5 (RemoteMotionSink deletion) NOT started.
|
||||||
|
`RemoteMotionSink.cs` still exists (217 lines). **R3 commits W2+ hard-depend on
|
||||||
|
R2 Q3/Q4/Q5 being shipped** (the `IMotionDoneSink` seam, the
|
||||||
|
sentinel→`AnimationDone` drain, and funnel-direct `PerformMovement` dispatch are
|
||||||
|
R2 deliverables R3 consumes). W0/W1 are independent and can start while R2
|
||||||
|
finishes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. DECOMP AMBIGUITIES TO PIN before porting (the W0 pseudocode commit)
|
||||||
|
|
||||||
|
| # | Ambiguity | Evidence each way | Pin method |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A1 | **`motion_allows_jump` 0x48 polarity is INVERTED in the BN extraction's "cleaned" note.** r3-motioninterp-decomp §3a annotates `0x48` = "jump allowed" and the range set as a whitelist. But the CALLER `jump_is_allowed` (0x005282b0, §3h) does `eax_7 = motion_allows_jump(fwd); if (eax_7 != 0) return eax_7;` — nonzero is returned AS THE ERROR, so **0x48 = "YouCantJumpFromThisPosition"-class BLOCK and the ranges are the blocklist** (matches ACE MotionInterp.cs:770-779: five blocked ranges + one exact id, return None otherwise). Under the corrected polarity the retail blocklist is: `[0x1000006f,0x10000078]`, `[0x10000128,0x10000131]`, `0x40000008 (Fallen)`, `[0x40000016,0x40000018]`, `[0x4000001e,0x40000039]`, `[0x41000012,0x41000014] (Crouch..Sleeping)`; everything else (incl. **0x40000015 Falling → 0 = pass**) falls through. ACE instead blocks exactly `Falling` and never `Fallen` — one of the two mis-transcribed a constant. The same wrong "0x48 = allowed" annotation propagates into the decomp doc's §10 constants inventory. | decomp §3a/§3h vs ACE L770-779; acdream MotionCommand.cs: Fallen=0x40000008, Falling=0x40000015 | Re-read raw pseudo-C @305 line 304908 for the exact comparison chain; map every boundary constant through the dat MotionCommand table (does 0x40000015 really pass?); decisive cdb: bp `acclient!CMotionInterp::motion_allows_jump` log arg2+eax while user jumps during walk/run/mid-fall/crouch. **Port the pinned blocklist as literal uint ranges, NOT enum-ordinal ranges** (ACE's enum-order dependence is the flagged fragility). |
|
||||||
|
| A2 | **`jump_is_allowed` pending-head peek gate.** Retail (§3h full body): peek `pending_motions.head_` whenever non-empty; if `head.jump_error_code != 0` return it. ACE (L756): `PendingMotions.Count > 1 && First.JumpErrorCode != 0` — the `Count > 1` has no retail counterpart in this decomp. | decomp §3h (no count check) vs ACE L756 | Retail decomp is full-body and unambiguous → port retail (head peek whenever queue non-empty). Note in pseudocode why ACE differs (their head = currently-playing heuristic). ACE's L747 `WeenieObj == null && !WeenieObj.IsCreature()` NPE-typo is CONFIRMED wrong vs retail's `weenie != 0 && IsCreature()==0` gate — do not copy. |
|
||||||
|
| A3 | **`apply_current_movement` / `SetWeenieObject` / `SetPhysicsObject` dual-dispatch gate: `IsThePlayer` or `IsCreature`?** The decomp extraction (§4c) states ReportExhaustion gates on `IsThePlayer() && movement_is_autonomous()` and claims the identical pattern in apply_current_movement/SetWeenieObject/SetPhysicsObject. ACE apply_current_movement (L430-438) gates on `IsCreature`. These are NOT the same predicate: a remote player is a creature but not the player. Client-side, `IsThePlayer` is the coherent reading (raw_state is only meaningful for the locally-controlled object); ACE is a server where its own physics objects are all "the player". Getting this wrong sends remotes down `apply_raw_movement` and corrupts their interpreted state from an empty raw state. | decomp §4c text vs ACE L430/L497 | Re-read raw pseudo-C at 0x00528870 (line 305838, apply_current_movement), 0x00528920 (SetWeenieObject), 0x00528970 (SetPhysicsObject) — the vtable slot called distinguishes `IsThePlayer` from `IsCreature`. If still ambiguous, cdb bp with `this` dump on a remote-visible session. Default: **IsThePlayer-based for apply_current_movement/ReportExhaustion** (decomp's explicit ReportExhaustion body shows IsThePlayer; §4c says the others are identical). |
|
||||||
|
| A4 | **MovementParameters bit numbering.** DoMotion reads byte-1 bits: `&8` → SetHoldKey (=0x800 overall), `&0x20` → raw mirror (=0x2000); acclient.h's union comment says "bit0x8=SetHoldKey, bit0x20=Raw mirror, bit0x40=Interpreted mirror" (byte-relative). Sign bit = interrupt. Ctor default = `(bitfield & 0xfffdee0f) | 0x1ee0f`. | decomp §0/§2 vs acclient.h:31453 comment | Derive the absolute bit positions from the ctor default 0x1ee0f + ACE MovementParameters property names (CancelMoveTo/SetHoldKey/ModifyRawState/ModifyInterpretedState/DisableJumpDuringLink/…) which ACE mapped from the same bitfield. Document the full flag table in the pseudocode doc; the C# port uses named bool properties + a `ToBitfield()` only if the wire ever needs it. |
|
||||||
|
| A5 | **`get_jump_v_z` no-weenie fallback + near-zero path.** BN garbles the `extent < 0.0002` path (x87 flag noise) and reads the no-weenie return as "clamped extent"; ACE: `< EPSILON → 0.0f`, no-weenie → `10.0f`, InqJumpVelocity false → `0.0f`. Current acdream uses ACE's shape but with epsilon **0.001** (wrong — retail literal is `0.000199999995f`). | decomp §3c vs ACE L634-652 vs MotionInterpreter.cs:278 | Textual pin at 0x00527aa0 raw. Low stakes (creatures always have a weenie); fix the epsilon regardless. |
|
||||||
|
| A6 | **`get_leave_ground_velocity` zero-fallback matrix direction.** BN reads `m_fl2gv` (local→global) applied to the velocity; ACE does `GlobalToLocalVec(Velocity)`. Since the result feeds `set_local_velocity` (a LOCAL vector) and `m_velocityVector` is global, global→local is the only coherent reading — BN row/column confusion. Current `Quaternion.Inverse(Orientation)` transform (MotionInterpreter.cs:1024) IS global→local: keep. Epsilon must become 0.0002 (currently 0.001 via JumpExtentEpsilon). | decomp §3d vs ACE L654-663 vs current :1011-1029 | Adjudicate textually in pseudocode; conformance test: walk-off-ledge with nonzero world velocity preserves momentum direction. |
|
||||||
|
| A7 | **`MovementManager::MotionDone` uninit-`edx` relay + dead `arg2` in `CMotionInterp::MotionDone`.** Decompiler artifact (adjudicated in the decomp doc §6d). Port as `MotionDone(uint motion, bool success)` passing the completed node's motion id; the interp body reads NEITHER param in this build — **pop the HEAD unconditionally, never match by motion id**. Keep the params for R5 signature parity. | decomp §1c/§6d | Documented adjudication only. |
|
||||||
|
| A8 | **`enter_default_state` does NOT clear `pending_motions`.** Retail (§4d): resets raw+interp state, InitializeMotionTables, then APPENDS the `{0, 0x41000003, 0}` sentinel to whatever queue exists — no drain. ACE resets `PendingMotions = new LinkedList` with the author's own `// ??`. Port retail (append, no clear); note the MovementManager lazy-create path calls enter_default_state TWICE on first touch (retail double-call, §6g) — genuine retail, do not "fix", but R3's direct-bind construction calls it once explicitly. | decomp §4d/§6g vs ACE L604-616 | Documented adjudication; test asserts pre-existing nodes survive an enter_default_state. |
|
||||||
|
| A9 | **`StopCompletely` jump-snapshot quirk**: `motion_allows_jump` is evaluated on the OLD forward_command, then the state is overwritten and the OLD result is stashed in the queued Ready node. Port verbatim including the quirk (ACE L301-327 agrees). | decomp §5a + ACE | Textual pin only; golden asserts the queued node's error code reflects the pre-stop command. |
|
||||||
|
| A10 | **Error-code renumber vs current acdream `WeenieError`.** Retail numeric usage: `8`=no physics obj, `0x24`=not grounded (jump_is_allowed airborne), `0x3f/0x40/0x41`=combat-stance crouch/sit/sleep rejects, `0x42`=combat-stance chat-emote-bit reject, `0x45`=action depth ≥6, `0x47`=constrained OR stamina OR bad MovementStruct.type, `0x48`=motion/position blocks jump, `0x49`=CanJump refused. acdream's current enum (MotionInterpreter.cs:113-127) has the NAMES shuffled (0x24 named GeneralMovementFailure but used where retail uses 0x48-class; jump_is_allowed returns 0x48 for airborne where retail returns 0x24). | decomp §10 vs MotionInterpreter.cs:113-127, :1064 | W1 fixes the enum to retail's numeric semantics (add 0x3f/0x40/0x41/0x42/0x45); W3/W5 use them. Codes are local-only (never on the wire) — safe rename. |
|
||||||
|
|
||||||
|
**W0 cdb capture (one session serves all of R3):** bp `CMotionInterp::MotionDone` /
|
||||||
|
`add_to_queue` / `jump` / `jump_is_allowed` / `jump_charge_is_allowed` / `charge_jump`
|
||||||
|
/ `motion_allows_jump` / `HitGround` / `LeaveGround` / `StopCompletely` / `set_hold_run`
|
||||||
|
/ `SetHoldKey` / `enter_default_state` with arg+ret logging (pattern:
|
||||||
|
tools/cdb/l2g-observer.cdb). User protocol: idle / walk / run / shift-toggle mid-walk /
|
||||||
|
tap-jump / full-charge jump / running jump / standing long-jump (charge while idle,
|
||||||
|
then W mid-charge — the StandingLongJump suppression) / jump attempt while crouched
|
||||||
|
(blocked codes) / jump attempt in combat stance (0x3f-0x42 family) / walk off a ledge
|
||||||
|
(LeaveGround without jump — the momentum fallback) / land / emote-while-running
|
||||||
|
(action-class node through MotionDone) / 7 rapid emotes (0x45 depth cap) / logout
|
||||||
|
(HandleExitWorld drain).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ITEMIZED GAPS — current vs retail (R3 scope)
|
||||||
|
|
||||||
|
Severity: **BLOCKER** = R3's conformance harness meaningless without it; **HIGH** =
|
||||||
|
visible behavior wrongness / blocks R4-R6; **MED** = edge-visible; **LOW** = textual.
|
||||||
|
|
||||||
|
| # | Retail behavior acdream lacks/diverges on | Retail anchor | Current-code anchor | Severity |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| J1 | **No `pending_motions` queue** — no MotionNode, no `add_to_queue` (append `{context_id, motion, jump_error_code}`), no `MotionDone` (head action-class 0x10000000 → unstick + Interpreted+Raw `RemoveAction`, then unconditional head pop), no `motions_pending`, no `HandleExitWorld` drain loop. The R2 `IMotionDoneSink` signal dead-ends in a diagnostic recorder (per r2-port-plan Q4). | add_to_queue 0x00527b80 @305032; MotionDone 0x00527ec0 @305238; motions_pending 0x00527fe0; HandleExitWorld 0x00527f30; ACE MotionInterp.cs:210-234/388-392/160-173 | `MotionInterpreter.cs:1394` ("Idle add_to_queue(Ready) bookkeeping lands with S3") + `:1417` ("add_to_queue … S3 bookkeeping") — both TODO comments; no queue field exists | **BLOCKER** |
|
||||||
|
| J2 | **No action FIFO on the states** — retail `RawMotionState`/`InterpretedMotionState` each carry an actions list (`ApplyMotion`/`RemoveMotion`/`AddAction`/`RemoveAction`/`GetNumActions`); MotionDone's action-class pop and DoMotion's ≥6 depth cap operate on it. acdream's `InterpretedMotionState` (MotionInterpreter.cs:185-210) + `LegacyRawMotionState` (:148-179) are flat 6/7-field structs; the outbound `RawMotionState` packs an always-empty action list (register TS-24). | acclient.h RawMotionState/InterpretedMotionState; DoMotion @306159 GetNumActions≥6; MotionDone RemoveAction pair | `MotionInterpreter.cs:148-210`; `Core/Physics/RawMotionState.cs` (packer type, no actions) | **BLOCKER** |
|
||||||
|
| J3 | **DoMotion is a 12-line approximation.** Retail 0x00528d20: interrupt on sign bit; `SetHoldKey` on bit-SetHoldKey BEFORE `adjust_motion`; fresh local `MovementParameters` re-default (caller's distance/heading fields discarded for the interpreted call); combat-stance gate (`current_style != 0x8000003d` rejects 0x41000012/13/14 → 0x3f/0x40/0x41 and `motion & 0x2000000` → 0x42 — tested on the ORIGINAL id); action-depth cap → 0x45; delegate `DoInterpretedMotion(adjusted, localParams)`; mirror `RawState.ApplyMotion(ORIGINAL id)` on the mirror bit. Current: writes RawState.Forward\* then calls the legacy DoInterpretedMotion — no params, no gates, no mirror discipline. | 0x00528d20 @306159; ACE L112-158 | `MotionInterpreter.cs:448-462` | **BLOCKER** |
|
||||||
|
| J4 | **TWO parallel DoInterpretedMotion implementations, both partial.** The legacy overload (:473-491) hand-applies state + apply_current_movement; the S2a funnel `DispatchInterpretedMotion` (:1408-1425) has the verbatim contact gate + sink dispatch but no StandingLongJump special-case (Walk/Run/SideStepRight while charging → state-only, NO dispatch, NO queue), no `Dead → RemoveLinkAnimations`, no jump_error_code compute (double `motion_allows_jump` check: incoming motion, then ForwardCommand if non-action) + `add_to_queue`, no `ModifyInterpretedState` param, no `CurCell==null → RemoveLinkAnimations` tail. Same story for StopInterpretedMotion (:527-553 vs the funnel's bare `sink.StopMotion`): missing post-stop `add_to_queue(ctx, Ready, None)` + raw `RemoveMotion` mirror. They must MERGE into one verbatim pair. | DoInterpretedMotion 0x00528360 (S2a base) + queue callers @305607/305657/305775; ACE L51-110/329-365 | `MotionInterpreter.cs:473-491`, `:498-553`, `:1408-1425`, `:1358-1396` | **BLOCKER** |
|
||||||
|
| J5 | **Jump gate family absent**: no `motion_allows_jump` (0x005279e0 literal-range blocklist, A1), no `jump_charge_is_allowed` (0x00527a50: CanJump → 0x49; Fallen/Crouch..Sleeping → 0x48), no `charge_jump` (0x005281c0: arms `standing_longjump` only when on-ground + fwd==Ready + no sidestep/turn). `jump_is_allowed` (:1053-1071) is a 15-line approximation missing the creature/state-0x400 entry shape, IsFullyConstrained → 0x47, the pending-head jump_error_code peek (A2), the charge→motion→stamina chain, and `JumpStaminaCost` (not on IWeenieObject). Wrong airborne code (0x48, retail 0x24 — A10). | §3a/3b/3e/3h; ACE L729-779 | `MotionInterpreter.cs:1053-1071`; IWeenieObject :239-256 (no JumpStaminaCost/IsThePlayer) | **BLOCKER** |
|
||||||
|
| J6 | **StandingLongJump armed in the WRONG function** — the S2a-flagged misattribution: acdream arms it as a side effect of `contact_allows_move` (:1139-1148, explicitly marked "PRE-EXISTING acdream side effect (not part of 0x00528240)"); retail arms it ONLY in `charge_jump`. Consequence today: every grounded idle contact check flips the flag, so the funnel's StandingLongJump branch (:1371-1375) can fire without any jump charge. | charge_jump 0x005281c0 @305448 | `MotionInterpreter.cs:1139-1148` | **HIGH** |
|
||||||
|
| J7 | **jump() missing `interrupt_current_movement`** (retail always cancels the in-flight transition first; ACE `cancel_moveto`). And the CONTROLLER manually calls `LeaveGround()` + pre-captures `get_jump_v_z` at key-release (PlayerMovementController.cs:996-1028) — retail's LeaveGround fires from the physics layer detecting ground departure (`set_on_walkable(false)` → transition → MovementManager::LeaveGround). Walk-off-a-ledge never calls LeaveGround at all today (the get_leave_ground_velocity momentum fallback is dead code). | jump 0x00528780 @305792; LeaveGround 0x00528b00 (NOT the doc-comment's 0x00529710) | `MotionInterpreter.cs:943-958`; PlayerMovementController.cs:996-1028, :1226-1257 (transition block calls HitGround but never LeaveGround) | **HIGH** |
|
||||||
|
| J8 | **HitGround/LeaveGround bodies incomplete**: both missing the creature gate (`weenie==null OR IsCreature`) and `RemoveLinkAnimations`; LeaveGround missing the state-0x400 gate, `apply_current_movement(0,0)` re-sync, and the autonomous flag on `set_local_velocity(v, 1)`. **This is the retail mechanism K-fix18 fakes**: leaving ground strips pending link animations, so Falling engages instantly — no `skipTransitionLink` needed. | HitGround 0x00528ac0 @305996; LeaveGround 0x00528b00 @306022 | `MotionInterpreter.cs:1166-1198`; K-fix18 sites: AnimationSequencer.cs:305/313/390/401/433, GameWindow.cs:4817-4831, :10194-10224 | **HIGH** |
|
||||||
|
| J9 | **StopCompletely missing** interrupt, the A9 jump snapshot, `add_to_queue(0, Ready, jumpErr)`, and cell-null `RemoveLinkAnimations`; extra divergence: resets sidestep/turn SPEEDS to 1.0 (retail writes only fwd cmd/speed + zeroes sidestep/turn COMMANDS). `set_velocity(Zero)` stands in for `StopCompletely_Internal` (acceptable, keep + note). | 0x00527e40 @305208 | `MotionInterpreter.cs:577-602` | **HIGH** |
|
||||||
|
| J10 | **No `enter_default_state` / `initted`**: no state reset + InitializeMotionTables + Ready-sentinel enqueue (A8) + `initted=1` + unconditional LeaveGround; nothing gates `apply_current_movement`/`ReportExhaustion` on initted. | 0x00528c80 @306124 | constructors at `MotionInterpreter.cs:386-398` only | **BLOCKER** |
|
||||||
|
| J11 | **`apply_current_movement` is a direct-velocity approximation**, not the retail dual dispatch (`initted` gate → IsThePlayer/autonomous (A3) → `apply_raw_movement(cancelMoveTo, allowJump)` else `apply_interpreted_movement(...)`). The grounded `set_local_velocity(get_state_velocity())` write is an acdream adaptation of root-motion-driven velocity — survives R3 (register row), retires in R6. Also: no `(cancelMoveTo, allowJump)` param plumbing anywhere — `DisableJumpDuringLink = !allowJump` feeds the jump_error_code path (ACE L538/L232). | 0x00528870 @305838 (A3 pin); ACE L430-438 | `MotionInterpreter.cs:905-925`; funnel `ApplyInterpretedMovement` :1358 (no params) | **HIGH** |
|
||||||
|
| J12 | **No `ReportExhaustion`** (initted gate + IsThePlayer/autonomous dual dispatch, both `(0,0)` args); no `movement_is_autonomous` flag on PhysicsBody; no `IsThePlayer` on IWeenieObject. Retail relay chain: MovementManager::ReportExhaustion → CMotionInterp (+ MoveToManager, R4). The dead ACE `NoticeHandler.RecvNotice_PrevSpellSelection` breadcrumb is NOT R3 scope (spell-UI notice) — file as a note, don't port. | 0x005288d0 @305861 | nothing | MED-HIGH (needed for stamina-exhaustion demotes; wire caller lands with server stamina events) |
|
||||||
|
| J13 | **No `SetHoldKey`/`set_hold_run`** — retail's Shift edge handling (XOR toggle guard, None-only-meaningful-from-Run, both re-apply movement). acdream fakes it by rebuilding the whole RawMotionState every frame (J15). `HoldKey_None=1 / HoldKey_Run=2` encoding already matches acdream's HoldKey enum usage. | set_hold_run 0x00528b70 @306053; SetHoldKey 0x00528bb0 @306072 | `MotionInterpreter.cs:340` (bare CurrentHoldKey field, written by apply_raw_movement only) | **HIGH** |
|
||||||
|
| J14 | **No `CMotionInterp::PerformMovement` 5-way dispatch** with `CheckForCompletedMotions` after EVERY op (the synchronous zero-tick drain — retail calls it at 306234-306262 after each of DoMotion/DoInterpretedMotion/StopMotion/StopInterpretedMotion/StopCompletely). Current PerformMovement (:416-430) dispatches to the approximations and has a comment explicitly skipping the flush. Needs a seam to the entity's MotionTableManager (R2-Q3). | 0x00528e80 @306221; CPhysicsObj::CheckForCompletedMotions 0x0050fe30 | `MotionInterpreter.cs:400-430` | **BLOCKER** |
|
||||||
|
| J15 | **Local player motion state machine lives in PlayerMovementController, level-triggered.** Per-frame RawMotionState rebuild from held keys (:887-910) instead of retail's edge-driven DoMotion/StopMotion + set_hold_run through PerformMovement; jump charge/fire block calls `_motion.jump` + manual LeaveGround (:986-1028); outbound wire commands re-derived from `input` (:1264-1328) instead of read from the raw state; `LocalAnimationCommand`/`LocalAnimationSpeed` synthesized (:1284-1296, :1418-1456) instead of the interpreted state driving the sequencer through the same funnel path remotes use. This is the plan-of-record's "motion half of PlayerMovementController" — R3's unification target. | DoMotion/StopMotion callers = CommandInterpreter edges; set_hold_run call sites §5c (0x0058b303 / 0x006b33ca) | PlayerMovementController.cs:887-910, :986-1028, :1264-1359, :1418-1456; GameWindow.cs:10062-10234 (UpdatePlayerAnimation fallback chain + SetCycle drive) | **BLOCKER** for the stage's named deliverable |
|
||||||
|
| J16 | **Epsilons + codes**: `JumpExtentEpsilon = 0.001` used in get_jump_v_z AND get_leave_ground_velocity — retail is `0.000199999995f` in both (§10); WeenieError names shuffled vs retail numerics (A10). | §3c/§3d/§10 | `MotionInterpreter.cs:278`, :978, :1020; :113-127 | MED |
|
||||||
|
| J17 | **`is_standing_still` absent** (on-ground + Ready + no sidestep/turn) — trivial; needed by callers (input layer, R7 cadence) and by the decomp's documented substitute set for the not-in-retail `IsAnimating`/`IsMovingOrAnimating`. **Do NOT invent a combined IsAnimating helper** — retail has none (decomp §7f negative result); ACE's `PhysicsObj.IsAnimating` flag is an ACE-ism (retail queries `motions_pending`). | 0x00527fa0 @305309 | nothing | LOW |
|
||||||
|
| J18 | **`adjust_motion` creature guard unwired** (register TS-34): `IWeenieObject.IsCreature()` exists (default true) but adjust_motion ignores it (comment ":758 Creature guard unwired"). One-line fix; retires TS-34. | adjust_motion @305343 gate | `MotionInterpreter.cs:756-758` | LOW |
|
||||||
|
| J19 | **VectorUpdate remote-jump path bypasses the interp** (GameWindow.cs:4782-4840): sets body velocity/airborne + forces the Falling cycle via `SetCycle(..., skipTransitionLink:true)` (K-fix18 site 1). Retail: the remote's physics leaves ground → LeaveGround → RemoveLinkAnimations + apply_current_movement → contact-blocked funnel dispatches Falling. After J8, this becomes `rm.Motion.LeaveGround()` (+ the funnel's existing Falling dispatch). | SmartBox::DoVectorUpdate 0x004521C0 → set_velocity/set_omega; LeaveGround chain | GameWindow.cs:4802-4833 | **HIGH** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. KEEP LIST — already matching retail (do not re-port)
|
||||||
|
|
||||||
|
| Behavior | Retail anchor | acdream anchor |
|
||||||
|
|---|---|---|
|
||||||
|
| Inbound funnel dispatch order: `MoveToInterpretedState` (flat copy + 15-bit stamp gate + local-echo skip) / `ApplyInterpretedMovement` axis order (style → fwd-or-Falling → sidestep(-stop) → turn(-stop, early return)) + 183-case live-trace suite | 0x005289c0 / 0x00528600 (S2a, `7b0cbbda`) | `MotionInterpreter.cs:1312-1396` — W5 merges the Dispatch backend into verbatim DoInterpretedMotion WITHOUT touching dispatch order; suite is the parity bar |
|
||||||
|
| `contact_allows_move` verbatim body (turns/Falling/Dead always-allowed, non-creature bypass, gravity bypass, Contact+OnWalkable) | 0x00528240 (S2a rewrite) | `MotionInterpreter.cs:1096-1151` — MINUS the :1139-1148 StandingLongJump side effect (J6 deletes it) |
|
||||||
|
| `adjust_motion` / `apply_run_to_command` / `apply_raw_movement` / `get_state_velocity` incl. the RunForward-early-return, sign-gated promotion, ±3.0 sidestep clamp, max-speed clamp | D6/D6.2a (`0f099bb6`) | `MotionInterpreter.cs:654-888` — untouched except J18's one-line guard |
|
||||||
|
| `GetMaxSpeed` ×4.0 (UN-2 byte-verified vs the BN x87 dropout) | 0x00527cb0 | `MotionInterpreter.cs:1235-1245` |
|
||||||
|
| `GetCycleVelocity` Option-B forward-velocity source (MotionData.Velocity over the constant) | adaptation, r03 §1.3 | `MotionInterpreter.cs:382`, :668-683 — keep; verify register row exists, retire when R6 root motion drives the body |
|
||||||
|
| `StopCompletely_Internal` stand-in = `PhysicsObj.set_velocity(Zero)` | 0x0050f5a0 is CPhysicsObj-side | `MotionInterpreter.cs:599` — keep + register note (full port is physics-layer scope) |
|
||||||
|
| Jump charge accumulation UI timing (2.0 extent/s, AP-24) + extent→height formula | GetPowerBarLevel 0x0056ADE0 illegible | PlayerMovementController.cs:167-178 — charge STAYS controller-side (retail's charge lives at the SmartBox/input boundary too, §3e caller 0x0056afac); AP-24 row survives |
|
||||||
|
| Physics tick gate (30 Hz MinQuantum), Resolve/bounce/landing detection, AP diff cadence, PortalSpace, SetPosition/SnapToCell, NotePositionSent | L.5/L.2c/B.6 anchors in-file | PlayerMovementController.cs sections 4-5, 8 — R3 does not touch; R6 owns tick order |
|
||||||
|
| Auto-walk (B.6) block incl. `DoMotion(Ready)`/`DoMotion(WalkForward)` drive | MovementManager case 6 stand-in | PlayerMovementController.cs:254-766 — survives R3 verbatim (calls route through the new DoMotion transparently); REPLACED in R4 by MoveToManager |
|
||||||
|
| Outbound wire packers (RawMotionState::Pack, MoveToStatePack, JumpPack) + dual command catalogs | L.2b/D6.2b/L.1b | untouched; W6 changes only WHERE the packed values are read from (raw state instead of re-derivation) |
|
||||||
|
| `MotionSequenceGate` (S1), `InterpolationManager` (L.3), R1 CSequence core, R2 CMotionTable/MotionState/MotionTableManager | plan "absorbed" + R2 | untouched — R3 sits between MovementManager(-to-be) and the R2 queue |
|
||||||
|
| `0x41000003` Ready sentinel / `0x10000000` action-class bit / `0x8000003d` NonCombat conventions | §10 | already the file-wide constants |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. COMMIT SEQUENCE — dependency-sorted, each ONE commit, tests-first
|
||||||
|
|
||||||
|
New code target: extend `src/AcDream.Core/Physics/MotionInterpreter.cs` in place (it IS
|
||||||
|
the CMotionInterp port; plan rule 4's `Motion/` dir holds the R2 table/queue classes it
|
||||||
|
talks to). Tests: `tests/AcDream.Core.Tests/Physics/` (existing MotionInterpreter
|
||||||
|
suites + `Motion/` for new fixtures). Every commit: build+test green, register rows
|
||||||
|
added/retired in-commit.
|
||||||
|
|
||||||
|
**W0 — pseudocode + ambiguity pinning (docs only).**
|
||||||
|
`docs/research/2026-07-0x-motioninterp-pseudocode.md` from r3-motioninterp-decomp.md,
|
||||||
|
resolving A1-A10 (§0) — the A1 polarity fix and A3 dispatch-gate pin are load-bearing;
|
||||||
|
correct the decomp doc's §3a/§10 annotations in the same commit. Run the ONE cdb
|
||||||
|
session (§0 protocol) — feeds W2/W3/W5/W6 goldens.
|
||||||
|
Fixture source: **cdb** (live retail). Deps: none (parallel with R2 completion).
|
||||||
|
|
||||||
|
**W1 — retail state completion: action FIFO + MovementParameters + WeenieError renumber.** (closes J2, J16-codes/A10)
|
||||||
|
`InterpretedMotionState`/`RawMotionState` gain the actions list
|
||||||
|
(`AddAction`/`RemoveAction`/`GetNumActions`/`ApplyMotion`/`RemoveMotion` per retail
|
||||||
|
member set; the outbound packer's empty-list TS-24 comment updates to "populated by
|
||||||
|
CMotionInterp"); fold `LegacyRawMotionState` into the full-field
|
||||||
|
`AcDream.Core.Physics.RawMotionState` (one raw-state type; delete the legacy struct);
|
||||||
|
`MovementParameters` verbatim class (ctor defaults: distance_to_object=0.6,
|
||||||
|
fail_distance=FLT_MAX, speed=1, walk_run_threshhold=15, hold_key=Invalid, bitfield
|
||||||
|
0x1ee0f per A4 pin; named bool properties); `MotionNode {ContextId, Motion,
|
||||||
|
JumpErrorCode}`; WeenieError renumbered to retail (add 0x3f/40/41/42/45, fix
|
||||||
|
0x24/0x47/0x48 semantics).
|
||||||
|
Tests first: FIFO discipline; params defaults vs ctor decomp; existing suites green
|
||||||
|
under the raw-state fold.
|
||||||
|
Fixture source: **synthetic**. Deps: W0.
|
||||||
|
|
||||||
|
**W2 — pending_motions lifecycle + the MotionDone consumer (R2 seam lands).** (closes J1, J17; §4 below is this commit's wiring spec)
|
||||||
|
`MotionInterpreter` gains `PendingMotions` (LinkedList<MotionNode> — register note
|
||||||
|
reuses AD-34 wording), `add_to_queue` 0x00527b80, `motions_pending` 0x00527fe0,
|
||||||
|
`MotionDone(uint motion, bool success)` 0x00527ec0 (head-peek action-class → unstick
|
||||||
|
seam (no-op Action? callback until R5 StickyManager — register row) +
|
||||||
|
`InterpretedState.RemoveAction()` + `RawState.RemoveAction()`; unconditional head pop;
|
||||||
|
NEVER match by motion id per A7), `HandleExitWorld` 0x00527f30 (loop),
|
||||||
|
`is_standing_still` 0x00527fa0, and `motion_allows_jump` 0x005279e0 (pure fn, pinned
|
||||||
|
per A1 — needed here because every add_to_queue caller computes jump_error_code).
|
||||||
|
Wire the queue's PRODUCERS into the funnel: `DispatchInterpretedMotion` success path
|
||||||
|
computes jump_error_code (double-check per ACE L231-239 shape pinned at W0) +
|
||||||
|
`add_to_queue(ctx, motion, err)`; `ApplyInterpretedMovement`'s turn-stop adds
|
||||||
|
`add_to_queue(ctx, Ready, None)` (@305775); funnel StopMotion path adds the post-stop
|
||||||
|
Ready node (@305657). GameWindow: replace R2-Q4's diagnostic `IMotionDoneSink` binding
|
||||||
|
with the real per-entity bind (§4); teleport/despawn/exit paths call BOTH
|
||||||
|
`manager.HandleExitWorld()` (R2) and `interp.HandleExitWorld()` (retail has both
|
||||||
|
layers' drains).
|
||||||
|
Tests first: queue tables (enqueue per successful dispatch incl. Ready stops; pop
|
||||||
|
order; action-class pops from BOTH states + fires unstick; exit drain); S2a 183-case
|
||||||
|
suite green (dispatch unchanged, queue side effects asserted additively); end-to-end
|
||||||
|
chain test: MotionTableManager.AnimationDone → sink → interp pops (§4 diagram).
|
||||||
|
Fixture source: **synthetic + W0 cdb goldens** (add_to_queue arg conformance).
|
||||||
|
Deps: W1 + **R2 Q3/Q4 shipped**.
|
||||||
|
|
||||||
|
**W3 — jump family verbatim.** (closes J5, J6, J7-interp-side, J16-epsilons)
|
||||||
|
`jump_charge_is_allowed` 0x00527a50; `charge_jump` 0x005281c0 (arms StandingLongJump;
|
||||||
|
**DELETE the contact_allows_move side effect :1139-1148** — J6); `get_jump_v_z`
|
||||||
|
(epsilon → 0.000199999995f; A5 fallback pinned); `get_leave_ground_velocity` (same
|
||||||
|
epsilon; A6 direction confirmed); `jump_is_allowed` 0x005282b0 full chain (creature/
|
||||||
|
state-0x400 entry shape → 0x24; `IsFullyConstrained` seam on PhysicsBody (stub false +
|
||||||
|
register row); pending-head peek per A2; charge → motion_allows_jump(fwd) →
|
||||||
|
`JumpStaminaCost` (new IWeenieObject member; PlayerWeenie implements — real gating
|
||||||
|
stays TS-5-deferred, row survives)); `jump` 0x00528780 (+`interrupt_current_movement`
|
||||||
|
seam — no-op Action? until R4 cancel_moveto exists; register row). IWeenieObject gains
|
||||||
|
`IsThePlayer()` (default false; PlayerWeenie true) for W4's A3 dispatch.
|
||||||
|
Tests first: per-error-code gate tables from W0 goldens; StandingLongJump arming
|
||||||
|
matrix (grounded+idle only, cleared on failed jump); head-peek short-circuit;
|
||||||
|
blocked-motion jump attempts (crouch range per pinned A1 table).
|
||||||
|
Fixture source: **W0 cdb goldens + synthetic**. Deps: W2.
|
||||||
|
|
||||||
|
**W4 — ground transitions + lifecycle: HitGround/LeaveGround/enter_default_state/ReportExhaustion/hold keys; K-fix18 DELETED.** (closes J8, J10, J11-shape, J12, J13, J19; J18 one-liner rides along)
|
||||||
|
`HitGround` 0x00528ac0 verbatim (creature gate + Gravity + `RemoveLinkAnimations` seam
|
||||||
|
+ `apply_current_movement(false, true)`); `LeaveGround` 0x00528b00 verbatim (velocity
|
||||||
|
+ autonomous flag arg on set_local_velocity + resets + RemoveLinkAnimations +
|
||||||
|
apply_current_movement); `RemoveLinkAnimations` seam = `Action?` on MotionInterpreter,
|
||||||
|
App binds to the entity's AnimationSequencer (CSequence.RemoveAllLinkAnimations per
|
||||||
|
CPhysicsObj::RemoveLinkAnimations 0x0050fe20 → CPartArray → sequence);
|
||||||
|
`enter_default_state` 0x00528c80 (A8: append sentinel, no clear; `Initted` field;
|
||||||
|
LeaveGround tail); `Initted` gates apply_current_movement + ReportExhaustion;
|
||||||
|
`apply_current_movement` becomes the verbatim dual dispatch per A3 (the direct
|
||||||
|
grounded-velocity write MOVES to the controller-side call site unchanged — register
|
||||||
|
row: velocity-from-state adaptation until R6 root motion); `ReportExhaustion`
|
||||||
|
0x005288d0 (A3 gate; `movement_is_autonomous` flag added to PhysicsBody, set true at
|
||||||
|
the local-player chokepoint, false on DR-applied updates); `SetHoldKey` 0x00528bb0 +
|
||||||
|
`set_hold_run` 0x00528b70; `SetPhysicsObject`/`SetWeenieObject` re-apply pattern.
|
||||||
|
**PlayerMovementController transition wiring:** grounded→airborne edge (section 5,
|
||||||
|
:1245-1257) now calls `_motion.LeaveGround()`; the jump-fire block stops calling
|
||||||
|
LeaveGround manually — `jump()` sets on_walkable(false) and the SAME frame's
|
||||||
|
transition detection fires LeaveGround (ordering test); JumpAction wire velocity read
|
||||||
|
from the LeaveGround-computed vector (get_leave_ground_velocity at fire time —
|
||||||
|
byte-identical values, source unified).
|
||||||
|
**K-fix18 DELETION (both sites + the parameter):** GameWindow :4817-4833 remote
|
||||||
|
VectorUpdate → `rm.Motion.LeaveGround()` replaces the forced SetCycle (J19); GameWindow
|
||||||
|
:10194-10224 skipLink computation + arg deleted; `AnimationSequencer.SetCycle`'s
|
||||||
|
`skipTransitionLink` parameter + its post-dispatch link-clear (whatever shape R2-Q4
|
||||||
|
left it in) deleted; K-fix18 register row (r2-port-plan keep-list says "register row
|
||||||
|
kept → R3") retired — if no row exists, the sweep note records it was comment-only.
|
||||||
|
Tests first: HitGround/LeaveGround call RemoveLinkAnimations + re-apply (seam mock);
|
||||||
|
ledge walk-off momentum fallback (A6); jump-fire → LeaveGround same-tick ordering;
|
||||||
|
enter_default_state seeds queue + initted + LeaveGround; ReportExhaustion dispatch
|
||||||
|
truth table (A3 pin); hold-key toggle tables (set_hold_run XOR guard; SetHoldKey
|
||||||
|
None-only-from-Run). Visual acceptance carried to W6's pass: jump engages Falling
|
||||||
|
instantly WITHOUT K-fix18.
|
||||||
|
Fixture source: **synthetic + W0 cdb goldens (LeaveGround/HitGround arg traces)**.
|
||||||
|
Deps: W3 (+ R2-Q4 committed for the sequencer seam shape).
|
||||||
|
|
||||||
|
**W5 — DoMotion/StopMotion/StopCompletely verbatim + the ONE DoInterpretedMotion + PerformMovement flush.** (closes J3, J4, J9, J14)
|
||||||
|
`DoMotion` 0x00528d20 (interrupt bit; SetHoldKey bit BEFORE adjust_motion; params
|
||||||
|
re-default; combat-stance gate on ORIGINAL id → 0x3f/0x40/0x41/0x42; depth cap 0x45;
|
||||||
|
raw mirror ORIGINAL id); `StopMotion` 0x00528530 (mirror shape, no gates, raw
|
||||||
|
RemoveMotion ORIGINAL id); `StopCompletely` 0x00527e40 (A9 verbatim incl. quirk +
|
||||||
|
exact-fields-only reset + add_to_queue + cell-null RemoveLinkAnimations); **merge**
|
||||||
|
legacy `DoInterpretedMotion`/`StopInterpretedMotion` overloads with the funnel
|
||||||
|
Dispatch backend into ONE verbatim `DoInterpretedMotion(uint, MovementParameters)` /
|
||||||
|
`StopInterpretedMotion(uint, MovementParameters)` (StandingLongJump state-only branch;
|
||||||
|
Dead → RemoveLinkAnimations; jump_error_code + add_to_queue (moves here from W2's
|
||||||
|
interim funnel site); ModifyInterpretedState; CurCell-null tail — CurCell proxy =
|
||||||
|
`PhysicsObj.CellId == 0`); **DELETE** `ApplyMotionToInterpretedState` (:1253-1282) +
|
||||||
|
the legacy overloads; `PerformMovement` gains the per-op
|
||||||
|
`CheckForCompletedMotions` seam call (bound to the entity's MotionTableManager
|
||||||
|
UseTime/CheckForCompletedMotions — R2-Q3 object) after every dispatch.
|
||||||
|
Tests first: DoMotion gate tables (each combat-stance code, depth cap, mirror
|
||||||
|
discipline: raw gets ORIGINAL id, interpreted gets adjusted) from W0 goldens; S2a
|
||||||
|
183-case suite green under the merged backend (assertions unchanged — dispatch order
|
||||||
|
is funnel-owned); zero-tick flush test (stop-with-no-link completes same call).
|
||||||
|
Fixture source: **W0 cdb goldens + suite**. Deps: W2+W4 (+ R2-Q5 so the funnel's sink
|
||||||
|
IS PerformMovement dispatch — otherwise the merge has two consumers).
|
||||||
|
|
||||||
|
**W6 — LOCAL PLAYER UNIFICATION (the GameWindow + controller commit; do NOT fan out — feedback_dont_parallelize_coupled_plan_slices).** (closes J15)
|
||||||
|
PlayerMovementController sheds the motion state machine; edge-driven retail input:
|
||||||
|
- **DIES in the controller:** per-frame RawMotionState rebuild + `apply_raw_movement`
|
||||||
|
call (:878-910); the manual LeaveGround + get_jump_v_z pre-capture (done in W4);
|
||||||
|
`localAnimCmd`/`LocalAnimationSpeed` synthesis (:1283-1296, :1340-1359 change
|
||||||
|
detection on localAnimCmd, :1412-1456 K-fix5 block + auto-walk anim overrides —
|
||||||
|
the sequencer is now driven by the interp's own dispatch, not by
|
||||||
|
UpdatePlayerAnimation); `MovementResult.LocalAnimationCommand/LocalAnimationSpeed`
|
||||||
|
fields (and GameWindow consumers).
|
||||||
|
- **MOVES to MotionInterpreter calls (edge-driven):** input edges → `PerformMovement`:
|
||||||
|
W/S press → `DoMotion(WalkForward/WalkBackward, params)`; release →
|
||||||
|
`StopMotion(same)`; strafe/turn keys likewise; Shift edge → `set_hold_run(run,
|
||||||
|
interrupt:true)` (retail 0x006b33ca shape); jump release → `jump(extent)` (charge
|
||||||
|
accumulation STAYS controller-side, AP-24). The controller keeps a tiny
|
||||||
|
prev-key-state edge detector — that IS retail's CommandInterpreter altitude.
|
||||||
|
- **STAYS in the controller (input/camera/physics/wire):** Yaw + mouse turn
|
||||||
|
(:938-946); keyboard-turn Yaw integration reading
|
||||||
|
`_motion.InterpretedState.TurnCommand/TurnSpeed` (:932-937, unchanged); grounded
|
||||||
|
velocity write from `get_state_velocity` (:966-976 — the R6-deferred adaptation);
|
||||||
|
physics integrate/Resolve/bounce/landing (sections 4-5); heartbeat/AP (section 8);
|
||||||
|
auto-walk (B.6, until R4); SetPosition/PortalSpace (the :825 `DoMotion(Ready)`
|
||||||
|
arrival-idle becomes `StopCompletely()` — retail's teleport idle is a full stop;
|
||||||
|
note in commit); outbound section 6 now READS `_motion.RawState` (commands + per-axis
|
||||||
|
hold keys) instead of re-deriving from `input` — wire bytes identical (golden-byte
|
||||||
|
packer tests prove it).
|
||||||
|
- **GameWindow call sites that change:** `UpdatePlayerAnimation(result)` (:10062-10234)
|
||||||
|
DELETED — the player's AnimatedEntity sequencer/manager is bound to the player's
|
||||||
|
MotionInterpreter via the same funnel/PerformMovement path remotes use (R2-Q5
|
||||||
|
sink shape); the airborne-Falling override falls out of contact_allows_move +
|
||||||
|
apply_current_movement (retail mechanism, already live in the funnel); the #45
|
||||||
|
sidestep animSpeed factor + ACDREAM_ANIM_SPEED_SCALE knob move into (or are
|
||||||
|
re-verified against) the CMotionTable-driven speedMod path — expected-diff
|
||||||
|
annotations required; :4344 `ApplyServerRunRate` retargets to `my_run_rate` +
|
||||||
|
`apply_current_movement` (same effect, no InterpretedState poke); :7953-7971
|
||||||
|
JumpAction build reads the W4-unified launch vector; :7975 call deleted with
|
||||||
|
UpdatePlayerAnimation.
|
||||||
|
Tests first: FULL suite green; pre-cutover recorded local-player SetCycle/dispatch
|
||||||
|
traces (captured BEFORE this commit under the W0 protocol) replayed → same cycle
|
||||||
|
identities + speeds, expected-diffs documented (Falling engage now via
|
||||||
|
link-strip; sidestep factor source); outbound golden-byte parity (MoveToState/AP/Jump
|
||||||
|
byte-identical for the same input script); live smoke ACDREAM_DUMP_MOTION +
|
||||||
|
observer view; **ONE user visual pass** (walk/run/toggle/strafe/turn/jump instant
|
||||||
|
Falling/landing/emote-mid-run — the stage acceptance).
|
||||||
|
Fixture source: **pre-cutover recorded traces + golden-byte + suite**. Deps: W5.
|
||||||
|
|
||||||
|
**W7 — register sweep + roadmap + digest (docs/cleanup only).**
|
||||||
|
Retired rows: MotionDone-unconsumed (R2's), K-fix18, TS-34 (adjust_motion guard),
|
||||||
|
plus the S2a StandingLongJump-misattribution note. Surviving rows (verify/add):
|
||||||
|
TS-5 (CanJump always-true → now ALSO JumpStaminaCost stub), AP-24 (charge rate),
|
||||||
|
TS-24 updated (actions now populated locally — packing next), TS-21/TS-23/AP-30
|
||||||
|
(re-anchor line numbers after controller edits), NEW rows: unstick_from_object no-op
|
||||||
|
seam (→R5 StickyManager), interrupt_current_movement/cancel_moveto no-op (→R4),
|
||||||
|
grounded velocity-from-state write (→R6 root motion), managed LinkedList<MotionNode>
|
||||||
|
vs LList (AD-34 wording), A5/A6 pinned-reading notes if either resolves against ACE.
|
||||||
|
Roadmap stage table (R3 shipped); milestones "currently working toward" check; memory
|
||||||
|
digest note (animation-sequencer deep-dive: pending_motions gap CLOSED end-to-end).
|
||||||
|
Deps: W6.
|
||||||
|
|
||||||
|
Parallelization: W0∥W1 only. W2→W3→W4→W5→W6 are sequential-coupled (queue → gates
|
||||||
|
using queue → transitions using gates → dispatchers using all → cutover). W4 and W6
|
||||||
|
both touch GameWindow — single-agent each.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. IMotionDoneSink → MotionInterpreter.MotionDone — exact wiring vs R2's seam
|
||||||
|
|
||||||
|
R2's contract (r2-port-plan §4) delivers: `CSequence` G5 gate → `AnimationDoneSentinel`
|
||||||
|
→ GameWindow anim tick drain (:9876-9890 area) → `manager.AnimationDone(true)` per
|
||||||
|
sentinel + `manager.UseTime()` once per tick → `MotionTableManager` countdown pop →
|
||||||
|
action-class head pops the MANAGER's own `MotionState.RemoveActionHead()` (R2-OWNED)
|
||||||
|
→ `IMotionDoneSink.MotionDone(uint motion, bool success)` — bound in R2-Q4 to a
|
||||||
|
diagnostic recorder.
|
||||||
|
|
||||||
|
R3-W2 replaces the recorder:
|
||||||
|
|
||||||
|
```
|
||||||
|
MotionTableManager (per entity) [R2-Q3, exists]
|
||||||
|
└─ IMotionDoneSink.MotionDone(motion, success) [R2 seam — signature UNCHANGED]
|
||||||
|
└─ R3: the entity's MotionInterpreter.MotionDone(motion, success)
|
||||||
|
[stands in for retail CPhysicsObj::MotionDone 0x0050fdb0 →
|
||||||
|
MovementManager::MotionDone 0x005242d0 → CMotionInterp::MotionDone
|
||||||
|
0x00527ec0 — both intermediates are null-guarded relays with ZERO
|
||||||
|
logic; R5 inserts MovementManager between manager and interp
|
||||||
|
WITHOUT changing this behavior]
|
||||||
|
body: head-peek pending_motions;
|
||||||
|
head.Motion & 0x10000000 → unstick seam (no-op → R5)
|
||||||
|
+ InterpretedState.RemoveAction()
|
||||||
|
+ RawState.RemoveAction();
|
||||||
|
pop head UNCONDITIONALLY (A7: motion id + success are IGNORED —
|
||||||
|
positional protocol, never match-by-id, never branch on success)
|
||||||
|
```
|
||||||
|
|
||||||
|
Binding points (App, per entity, at creation):
|
||||||
|
- **Remote:** where `RemoteMotion` is constructed (each remote already owns a
|
||||||
|
`MotionInterpreter` — `remoteMot.Motion`, GameWindow.cs:4646): the same construction
|
||||||
|
that gives the entity its MotionTableManager (R2-Q4/Q5) passes
|
||||||
|
`sink: remoteMot.Motion` (MotionInterpreter implements IMotionDoneSink directly).
|
||||||
|
- **Local player:** `PlayerMovementController` exposes its `MotionInterpreter`
|
||||||
|
(new internal property `Motion`); GameWindow binds the player entity's manager to it
|
||||||
|
when the player's AnimatedEntity/sequencer is created (same site as
|
||||||
|
`AttachCycleVelocityAccessor`, :384).
|
||||||
|
|
||||||
|
Two structural rules carried from R2 §4 (decomp-confirmed in r3 extraction §7b):
|
||||||
|
1. **Two queues, never merged**: `MotionTableManager.pending_animations` (CPartArray
|
||||||
|
side, duration-in-ticks) vs `CMotionInterp.pending_motions` (movement side,
|
||||||
|
waiting-for-callback). THREE action trackers stay in lockstep via the 0x10000000
|
||||||
|
bit: manager's MotionState action FIFO (popped in R2's AnimationDone),
|
||||||
|
InterpretedState.actions + RawState.actions (popped in R3's MotionDone).
|
||||||
|
2. **Tick placement provisional until R6**: the sentinel drain + UseTime stay at the
|
||||||
|
R2-documented GameWindow drain point (G6 seam). R3 adds only the SYNCHRONOUS flush:
|
||||||
|
`CMotionInterp.PerformMovement` calls the manager's CheckForCompletedMotions after
|
||||||
|
every op (W5), which fires MotionDone same-call for zero-tick nodes.
|
||||||
|
|
||||||
|
Success-flag adjudication (closes r2-plan §4's warning): retail
|
||||||
|
`CMotionInterp::MotionDone` never reads the flag in this build — R3's jump/HitGround
|
||||||
|
logic does NOT key off it after all. The flag's only real consumers are
|
||||||
|
server/weenie-side (`OnMotionDone`) and the R2 manager's own countdown semantics.
|
||||||
|
Preserve pass-through (true from Hook_AnimDone, false from world drains, hardcoded
|
||||||
|
true from CheckForCompletedMotions) for R5 signature parity; document as inert at the
|
||||||
|
interp.
|
||||||
|
|
||||||
|
Enter/exit world: exit paths call `manager.HandleExitWorld()` (drains
|
||||||
|
pending_animations, each firing sink.MotionDone(motion, false) → interp pops in step)
|
||||||
|
THEN `interp.HandleExitWorld()` (drains any remainder — retail runs both layers'
|
||||||
|
drains via CPartArray::HandleExitWorld and MovementManager::HandleExitWorld
|
||||||
|
independently). Enter-world: manager-side only (`remove_all_link_animations` + drain);
|
||||||
|
`MovementManager::HandleEnterWorld`'s interp relay is the mis-symbolicated tailcall
|
||||||
|
(decomp §6e) — CMotionInterp has NO HandleEnterWorld; do not invent one (negative
|
||||||
|
result, decomp §6e/§1d).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. NEGATIVE RESULTS / DO-NOT-INVENT (binding on subagents)
|
||||||
|
|
||||||
|
- `CPhysicsObj::IsAnimating` / `IsMovingOrAnimating` — **not in this PDB** (decomp
|
||||||
|
§7f). ACE's `PhysicsObj.IsAnimating` bool is an ACE-ism. acdream queries
|
||||||
|
`motions_pending()` / `is_standing_still()` / `CPartArray.HasAnims` individually.
|
||||||
|
- `MovementManager::HandleUpdateTarget` — not in this PDB (§6j); likely
|
||||||
|
MoveToManager-internal (R4). Do not stub.
|
||||||
|
- `CMotionInterp::HandleEnterWorld` — does not exist (§6e); enter-world work is
|
||||||
|
manager/CPartArray-side only.
|
||||||
|
- `MakeMoveToManager` / MoveToManager internals — R4. R3 leaves the B.6 auto-walk +
|
||||||
|
`interrupt_current_movement`/`cancel_moveto` as registered no-op seams.
|
||||||
|
- `NoticeHandler::RecvNotice_PrevSpellSelection` (HandleEnterWorld/ReportExhaustion
|
||||||
|
relays) — retail spell-UI notice ACE never ported; out of R3 scope; file a research
|
||||||
|
note only (no ACE reference exists — decomp-direct if ever needed).
|
||||||
|
- `LeaveGround` address is **0x00528b00** — the 0x00529710 in MotionInterpreter.cs's
|
||||||
|
doc comment (:1153, :1159) is a stale Ghidra-chunk guess; W4 fixes the citations
|
||||||
|
(several header comments cite old FUN_ addresses — sweep them to named symbols).
|
||||||
Loading…
Add table
Add a link
Reference in a new issue