feat(R2-Q4): adapter cutover — SetCycle/PlayAction rehosted on PerformMovement; Fix B / fast-path / stop-anim fallback / G17 gate DELETED
AnimationSequencer becomes a thin shim over the verbatim R2 stack:
SetCycle dispatches the style change (Branch 1) then the motion (Branch
2/3/4) through MotionTableManager.PerformMovement; PlayAction is the
same dispatch (actions rebuild via Branch 3; modifiers are Branch 4
physics-only combine_motion — the AP-73 turn-blend mechanism now runs
for real). CurrentStyle/CurrentMotion/CurrentSpeedMod are read-only
mirrors of MotionState (post-adjust_motion, signed mod). Lazy
initialize_state on first drive (retail lazy-create analog) keeps
undriven sequencers do-nothing.
DELETED legacy inventions (net -648 lines): the adapter fast-path
(replaced by Branch-2 fast re-speed: change_cycle_speed +
subtract/combine_motion), Fix B + IsLocomotionCycleLowByte (replaced by
remove_redundant_links on the pending queue — the retail mechanism the
2026-05-03 cdb trace pointed at), the stop-anim low-byte fallback
(replaced by get_link's reversed-key double-hop — retail's actual
backward-walk settle plays the windup link REVERSED), GetLink/BuildNode/
EnqueueMotionData + the G17 HasVelocity/HasOmega gate (add_motion sets
unconditionally), MultiplyCyclicFramerate + the G13 composite,
PlayAction's insert-before-tail machinery, SCFAST/SCFULL diagnostics.
KEPT at the adapter (register rows): the boundary adjust_motion remap +
locomotion velocity/omega synthesis (AP-75, retire R3-W6/R6), K-fix18
skip-link as post-dispatch RemoveAllLinkAnimations (AP-74, retire
R3-W4 when LeaveGround fires it).
GameWindow queue-drain wiring (the §4 G6 seam): each drained AnimDone
hook → manager.AnimationDone(true) (retail AnimDoneHook::Execute
0x00526c20 → Hook_AnimDone 0x0050fda0 chain), UseTime once per tick
(0x00517d57/0x00517d67); IMotionDoneSink bound to an
ACDREAM_DUMP_MOTION recorder (AD-36 — R3 rebinds to
MotionInterpreter.MotionDone).
Conformance: the 11-scenario pre-cutover trace suite (a6235a36) replays
green with six EXPECTED-DIFF annotations (double-hop walk-run routing,
reversed settle links, Branch-4 physics-only modifiers, Branch-1
style-change links, post-adjust mirrors) — everything unannotated is
byte-identical. Legacy suites repaired by a dedicated agent (fixtures
gain the retail-mandatory StyleDefaults + class-bit-tagged ids;
reflection state-poking replaced with real dispatch; zero category-D
regressions — the suspected cursor bug was a fixture class-bit gap),
plus two vacuously-passing tests fixed. Register: stale IA-4 deleted
(R1-P1 ported the negative-factor swap).
Full suite green: 3,458 passed (374+425+713+1946).
Closes H6, H7, H8, H9, H10-adapter, H16-wiring (r2-port-plan.md §3 Q4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cd0289bea2
commit
3b9d9bb6be
6 changed files with 562 additions and 646 deletions
|
|
@ -37,14 +37,13 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 1. Intentional architecture (IA) — 17 rows
|
||||
## 1. Intentional architecture (IA) — 16 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
| IA-1 | Contact-plane pre-seed on grounded movers (**#96 ACCEPTED** per ISSUES.md) — retail's `CTransition::init` clears `contact_plane_valid`; we seed from the body's previous-frame plane | `src/AcDream.Core/Physics/PhysicsEngine.cs:919` | Removing it broke last-step stair `step_up` (`892019b`, reverted); seed propagates the body's *real current* plane, behavior matched retail in the A6.P3 gates | A stale pre-seeded plane lets `AdjustOffset` project sub-step 1 onto a plane retail wouldn't have yet — wrong slope motion / step-up acceptance right after leaving a surface | `CTransition::init`, pc:272547 family |
|
||||
| IA-2 | Lateral self-heal beyond retail's keep-curr: when no candidate contains the sphere, try `FindVisibleChildCell` over the claim's stab-list before keeping the claim | `src/AcDream.Core/Physics/CellTransit.cs:912` | Reuses the recovery retail's own `AdjustPosition` performs (:280028 stab-list mode), applied at the `find_cell_list` site to heal near-miss claims without a doorway crossing | In containment-gap geometry, membership flips to a neighbouring room where retail keeps curr — wrong render root / collision cell at gap positions | `find_cell_list` keep-curr pc:308788-308825; `find_visible_child_cell` :311444 |
|
||||
| IA-3 | `get_state_velocity` prefers dat cycle velocity (`MotionData.Velocity × speedMod`) over the decompiled constant; constant kept only as max-speed clamp | `src/AcDream.Core/Physics/MotionInterpreter.cs:315` | Retail's constant equals the Humanoid RunForward `MotionData.Velocity`, so both paths agree on retail dats; dat is ground truth for other MotionTables (r03 §1.3) | Where dat velocity ≠ constant, body speed differs from the retail binary — DR / observer drift on exotic creatures or modded dats | `FUN_00528960`; `_DAT_007c96e0` RunAnimSpeed |
|
||||
| IA-4 | `MultiplyFramerate` omits retail's negative-factor StartFrame↔EndFrame swap (direction encoded in Framerate sign instead) | `src/AcDream.Core/Physics/AnimationSequencer.cs:129` | Our callers (ForwardSpeed updates) only pass positive factors; Advance loop handles negative framerates against StartFrame as lower bound | A future negative-factor caller (reverse playback) scales without swapping bounds — wrong frame range traversal instead of clean reversal | `FUN_005267E0`; ACE Sequence.cs L277-287 |
|
||||
| IA-5 | Per-ENTITY vertex-derived AABB culling (+5 m animated-drift margin; animated entities bypass cull) vs retail per-PART dat drawing spheres | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:693` (bounds at `src/AcDream.Core/World/WorldEntity.cs:153`, `src/AcDream.Core/Meshing/GfxObjBounds.cs:14`; dead `PerEntityCullRadius=5.0f` at dispatcher :210) | Batched MDI rendering can't cheaply cull per part; bounds derive from the SAME dat vertex data that gets drawn (containment by construction — the **#119** fix, `6a9b529`; memory: feedback_culling_bounds_from_drawn_data) | Geometry escaping bounds+margin (pose drift >5 m, a hydration path skipping `SetLocalBounds`) makes the whole entity vanish on-screen — the #119 vanishing-staircase class | `CGfxObj.drawing_sphere` / viewconeCheck 0x005a09a4 |
|
||||
| IA-6 | Chat scrollback 500 lines vs retail ~200 (configurable) | `src/AcDream.Core/Chat/ChatLog.cs:19` | Strictly more useful for a dev client + plugins; deliberate default | Negligible — only if a plugin/UI behavior is ever specified against retail's exact retention cap | retail chat scrollback (~200) |
|
||||
| IA-7 | PhysicsScript replay keyed by (scriptId, entityId) replaces the prior instance; retail's ScriptManager linked list could hold duplicates | `src/AcDream.Core/Vfx/PhysicsScriptRunner.cs:51` | Prevents duplicate-stacking on server retriggers; flat keyed list simpler than retail's linked schedule; hedged to retail's common path | A server intentionally layering the same script on the same object shows ONE effect where retail shows several (overlapping casts/impacts) | `ScriptManager::Start` FUN_0051be40 / tick FUN_0051bfb0 |
|
||||
|
|
@ -61,7 +60,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 2. Adaptation (AD) — 34 rows
|
||||
## 2. Adaptation (AD) — 35 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -99,10 +98,11 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AD-33 | `CSequence.frame_number` at C# `double` (64-bit); retail is x87 `long double` (80-bit extended) — every frame-boundary comparison ran at extended precision on retail (Phase R1, 2026-07-02) | `src/AcDream.Core/Physics/Motion/CSequence.cs` (`FrameNumber`) | `double` is the widest C# float type; the R1 port removes ACE-style boundary epsilons so comparisons are exact-int against bare boundaries, minimizing ULP sensitivity (ACE's `float` is far worse) | A frame landing within 1 double-ULP of an integer boundary could classify differently than retail's 80-bit compare — sub-frame timing skew at pathological framerate×dt combinations | `acclient.h:30747` (`long double frame_number`) |
|
||||
| AD-34 | Retail's intrusive `DLListBase`/`DLListData` lists are managed `LinkedList<T>`s; node identity via `LinkedListNode<>` references (Phase R1 anim list, 2026-07-02; extended R2-Q3 to `MotionTableManager.pending_animations`) | `src/AcDream.Core/Physics/Motion/CSequence.cs` (`_animList`); `src/AcDream.Core/Physics/Motion/MotionTableManager.cs` (`_pendingAnimations`) | Same topology + cursor semantics (curr_anim/first_cyclic/tail-anchored scans are node references); unlink/delete becomes `Remove(node)`; conformance tests pin the surgery state tables | Any retail behavior depending on the −4 pointer adjustment or node memory reuse (none observed in the decomp) would diverge | `acclient.h` DLListBase; `r1-csequence-decomp.md` §0; `r2-motiontable-decomp.md` §11 |
|
||||
| AD-35 | `MotionTableManager.PerformMovement`'s unhandled-type default case returns the named sentinel `0xFFFFFFFF` (`MotionTableManagerError.NotHandled`); retail's compiled code leaks the `CSequence*` pointer reinterpreted as the return code (BN-confirmed artifact, dead/unreachable — callers gate on type first) (R2-Q3, 2026-07-02) | `src/AcDream.Core/Physics/Motion/MotionTableManager.cs` (`PerformMovement` default case) | No retail caller consults the return value for unhandled types (RawCommand/StopRawCommand/MoveTo\*/TurnTo\* route elsewhere); returning a stable non-zero sentinel preserves the only observable contract (non-zero = not success) without fabricating a pointer-shaped number | If a future port wires a caller that passes unhandled types AND branches on the exact return value, it would see `0xFFFFFFFF` where retail saw an arbitrary pointer — flag at that port | `PerformMovement` 0x0051c0b0 (`r2-motiontable-decomp.md` §11 default-case note) |
|
||||
| AD-36 | `IMotionDoneSink.MotionDone` is OBSERVED, not consumed: R2-Q4 binds the seam to an ACDREAM_DUMP_MOTION diagnostic recorder (`GameWindow` TickAnimations bind site); retail's chain continues `CPhysicsObj::MotionDone` → `MovementManager::MotionDone` → `CMotionInterp::MotionDone` popping `pending_motions` (R2-Q4, 2026-07-02) | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`MotionDoneTarget`) | Stage boundary: `CMotionInterp.pending_motions` does not exist until R3-W2, which rebinds the seam to `MotionInterpreter.MotionDone` (r3-port-plan.md §4) | Until R3, nothing tracks queued-motion completion on the interp side — jump_is_allowed's pending-head peek and action RemoveAction mirrors are inert (they don't exist yet either) | `CPhysicsObj::MotionDone` 0x0050fdb0; r2-port-plan.md §4 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Documented approximation (AP) — 69 rows
|
||||
## 3. Documented approximation (AP) — 72 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -180,6 +180,8 @@ accepted-divergence entries (#96, #49, #50).
|
|||
---
|
||||
|
||||
| AP-73 | **Remote motion sink maps retail's substate+modifier animation model onto ONE cycle** — the L.2g S2 funnel dispatches retail-verbatim (style/forward/sidestep/turn per `apply_interpreted_movement` order + action-stamp gate), but `RemoteMotionSink.Commit` resolves the axes into a single `SetCycle` (forward locomotion > sidestep > turn priority + Run→Walk→Ready missing-cycle fallback); retail BLENDS modifiers over the substate cycle via `CMotionTable::re_modify` (DEV-9) | `src/AcDream.App/Rendering/RemoteMotionSink.cs` (`Commit`) | The pick + fallback are the pre-S2 behavior moved verbatim (user-verified across L.1/L.3); the STATE side is now fully retail through the funnel, so only the visible-animation composition approximates. Retire with the S3/S6 modifier port (`re_modify` + `subtract_motion` omega/velocity combine) | Run-while-turning shows pure run legs (no lean/turn blend); sidestep-while-walking shows one cycle only — animation-composition mismatch vs retail observers, position unaffected | `CMotionTable::re_modify` (GetObjectSequence tail, pc:298636); `StopSequenceMotion` modifier branch pc:298954 |
|
||||
| AP-74 | **K-fix18 `skipTransitionLink`** post-Q4 expression: `SetCycle(..., skipTransitionLink:true)` strips the just-dispatched transition links via `CSequence.RemoveAllLinkAnimations` so Falling engages instantly on jump start; retail gets the same strip from `LeaveGround` → `CPhysicsObj::RemoveLinkAnimations` (0x0050fe20) fired by the physics layer detecting ground departure (R2-Q4 rehost of the 2026-04-26 K-fix18) | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` skip branch); GameWindow :4830/:10201 callers | Same primitive retail uses, fired from the caller instead of the ground-transition event; pending-queue tick counts intentionally not zeroed (retail's RemoveLinkAnimations doesn't touch the manager queue — the countdown chain absorbs the stall) | If a code path sets the flag outside a genuine ground-departure, links retail would play get eaten; queue completion for the stripped entry reports late (next sentinel) | `LeaveGround` 0x00528b00 → `RemoveLinkAnimations` 0x0050fe20; retire in R3-W4 (flag + call sites deleted, LeaveGround fires it) |
|
||||
| AP-75 | **Adapter-boundary `adjust_motion` + locomotion velocity/omega synthesis**: `SetCycle` still (a) remaps TurnLeft/SideStepLeft/WalkBackward to their mirrors with negated speed BEFORE dispatch (retail adjusts in `CMotionInterp` — R3 scope; GameWindow's local-player path passes raw ids), and (b) post-dispatch overwrites sequence velocity (low-bytes 05/06/07/0F/10) and omega (0D/0E, only when dat-silent) with the retail locomotion constants — retail drives BODY velocity from `get_state_velocity`, not the sequence accumulators (R2-Q4 carry-over of the pre-Q4 adapter tail) | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap + tail synthesis) | (a) preserves unadjusted GameWindow callers until R3-W6 unifies the local player onto MotionInterpreter; (b) preserves the remote-DR and local Option-B velocity consumers until R6 root motion drives the body (sibling of IA-3) | (a) none while callers stay in the known set; (b) a dat whose locomotion MotionData carries a REAL velocity different from the constants gets overwritten — exotic-creature speed skew (same class as IA-3's risk) | `CMotionInterp::adjust_motion` @305343; `get_state_velocity` 0x00528960; retire (a) R3-W6, (b) R6 |
|
||||
|
||||
## 4. Temporary stopgap (TS) — 33 rows
|
||||
|
||||
|
|
@ -221,7 +223,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 5. Unclear (UN) — 4 rows
|
||||
## 5. Unclear (UN) — 5 rows
|
||||
|
||||
These rows have a missing, contradictory, or never-argued justification.
|
||||
They are the highest-priority audits: each needs either a recorded
|
||||
|
|
|
|||
|
|
@ -9875,10 +9875,33 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
seqFrames = ae.Sequencer.Advance(dt);
|
||||
|
||||
// R2-Q4: bind the MotionDone diagnostic recorder once per
|
||||
// sequencer (the IMotionDoneSink seam's interim consumer —
|
||||
// R3 binds MotionInterpreter.MotionDone here instead).
|
||||
if (ae.Sequencer.MotionDoneTarget is null)
|
||||
{
|
||||
uint mdGuid = serverGuid;
|
||||
ae.Sequencer.MotionDoneTarget = (m, ok) =>
|
||||
{
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
System.Console.WriteLine(
|
||||
$"[MOTIONDONE] guid={mdGuid:X8} motion=0x{m:X8} success={ok}");
|
||||
};
|
||||
}
|
||||
|
||||
// Phase E.1: drain animation hooks (footstep sounds, attack
|
||||
// damage frames, particle spawns, part swaps, etc.) and fan
|
||||
// them out to registered subsystems via the hook router.
|
||||
// Mirrors ACE's PhysicsObj.add_anim_hook dispatch path.
|
||||
//
|
||||
// R2-Q4 queue-drain wiring (r2-port-plan.md §4, the G6 seam;
|
||||
// per-tick PLACEMENT provisional until R6 installs retail's
|
||||
// UpdateObjectInternal order): each drained AnimDone hook
|
||||
// advances the entity's MotionTableManager countdown
|
||||
// (retail AnimDoneHook::Execute 0x00526c20 → Hook_AnimDone
|
||||
// 0x0050fda0 → CPartArray::AnimationDone(1)), and UseTime
|
||||
// runs once per tick to sweep zero-tick queue entries
|
||||
// (retail call sites 0x00517d57/0x00517d67).
|
||||
var hooks = ae.Sequencer.ConsumePendingHooks();
|
||||
if (hooks.Count > 0)
|
||||
{
|
||||
|
|
@ -9888,8 +9911,11 @@ public sealed class GameWindow : IDisposable
|
|||
var hook = hooks[hi];
|
||||
if (hook is null) continue;
|
||||
_hookRouter.OnHook(ae.Entity.Id, worldPos, hook);
|
||||
if (hook is DatReaderWriter.Types.AnimationDoneHook)
|
||||
ae.Sequencer.Manager.AnimationDone(success: true);
|
||||
}
|
||||
}
|
||||
ae.Sequencer.Manager.UseTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -119,19 +119,28 @@ public sealed class AnimationSequencer
|
|||
{
|
||||
// ── Public state ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Current style (stance) command.</summary>
|
||||
public uint CurrentStyle { get; private set; }
|
||||
|
||||
/// <summary>Current cyclic motion command.</summary>
|
||||
public uint CurrentMotion { get; private set; }
|
||||
/// <summary>
|
||||
/// Current style (stance) command — R2-Q4: read-only mirror of the
|
||||
/// retail <see cref="MotionState.Style"/> (MotionState OWNS
|
||||
/// style/substate/substate_mod; the adapter no longer keeps copies).
|
||||
/// </summary>
|
||||
public uint CurrentStyle => _state.Style;
|
||||
|
||||
/// <summary>
|
||||
/// Speed multiplier currently applied to the cyclic tail. Starts at 1.0
|
||||
/// and is updated by <see cref="SetCycle"/> when the same motion is
|
||||
/// re-issued with a different speed (which triggers
|
||||
/// <see cref="MultiplyCyclicFramerate"/> instead of a cycle restart).
|
||||
/// Current cyclic motion command — mirror of
|
||||
/// <see cref="MotionState.Substate"/>. NOTE (Q4): this is the
|
||||
/// POST-adjust_motion substate (WalkBackward reads back as WalkForward
|
||||
/// with a negative <see cref="CurrentSpeedMod"/>) — retail's interpreted
|
||||
/// state is post-adjustment.
|
||||
/// </summary>
|
||||
public float CurrentSpeedMod { get; private set; } = 1f;
|
||||
public uint CurrentMotion => _state.Substate;
|
||||
|
||||
/// <summary>
|
||||
/// Speed multiplier of the current substate — mirror of
|
||||
/// <see cref="MotionState.SubstateMod"/> (signed; see
|
||||
/// <see cref="CurrentMotion"/>).
|
||||
/// </summary>
|
||||
public float CurrentSpeedMod => _state.SubstateMod;
|
||||
|
||||
/// <summary>
|
||||
/// Sequence-wide velocity mirror of the core's <see cref="CSequence.Velocity"/>
|
||||
|
|
@ -221,6 +230,23 @@ public sealed class AnimationSequencer
|
|||
// accumulators.
|
||||
private readonly CSequence _core;
|
||||
|
||||
// R2-Q4: the verbatim motion-selection stack. CMotionTable resolves
|
||||
// (style, substate, speed) requests (Q2); MotionState owns
|
||||
// style/substate/mod + the modifier/action chains (Q1);
|
||||
// MotionTableManager owns the pending-animation queue + the
|
||||
// tick-countdown completion machinery (Q3). SetCycle/PlayAction are
|
||||
// thin shims over MotionTableManager.PerformMovement.
|
||||
private readonly CMotionTable _table;
|
||||
private readonly MotionState _state;
|
||||
private readonly MotionTableManager _manager;
|
||||
|
||||
// Retail lazy-create analog (MovementManager::get_minterp →
|
||||
// enter_default_state, r3-motioninterp-decomp §6g): the manager's
|
||||
// initialize_state runs on the FIRST SetCycle/PlayAction, not at
|
||||
// construction, so a sequencer that is never driven stays do-nothing
|
||||
// (RenderBootstrap invariant).
|
||||
private bool _initialized;
|
||||
|
||||
// Hooks pending dispatch. Accumulated during Advance (via the
|
||||
// AdapterHookQueue seam below); drained via ConsumePendingHooks.
|
||||
private readonly List<AnimationHook> _pendingHooks = new();
|
||||
|
|
@ -229,14 +255,6 @@ public sealed class AnimationSequencer
|
|||
// (retail CSequence::update(Frame*), 0x00525b80) — the old adapter
|
||||
// accumulator fields are gone with ConsumeRootMotionDelta.
|
||||
|
||||
// ── Diagnostics (Commit A 2026-05-03) ───────────────────────────────────
|
||||
// Throttle clock for the [SCFAST] / [SCFULL] / [SCNULLFALLBACK] log lines
|
||||
// emitted from SetCycle. Gated on env var ACDREAM_REMOTE_VEL_DIAG=1; reads
|
||||
// the env var inline rather than caching so a launch can be re-toggled
|
||||
// without restarting. 0.5s per sequencer instance keeps logs readable
|
||||
// while still capturing meaningful state changes.
|
||||
private double _lastSetCycleDiagTime;
|
||||
|
||||
// ── Constructor ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -259,6 +277,41 @@ public sealed class AnimationSequencer
|
|||
_loader = loader;
|
||||
_core = new CSequence(loader);
|
||||
_core.HookObj = new AdapterHookQueue(this);
|
||||
_table = new CMotionTable(motionTable);
|
||||
_state = new MotionState();
|
||||
_manager = new MotionTableManager(_table, _state, _core, new ForwardingMotionDoneSink(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R2-Q4: the entity's <see cref="MotionTableManager"/> — the caller's
|
||||
/// per-tick drive surface (<see cref="MotionTableManager.AnimationDone"/>
|
||||
/// per drained AnimDone hook, <see cref="MotionTableManager.UseTime"/>
|
||||
/// once per tick).
|
||||
/// </summary>
|
||||
public MotionTableManager Manager => _manager;
|
||||
|
||||
/// <summary>
|
||||
/// R2-Q4 seam: where <see cref="IMotionDoneSink.MotionDone"/> lands.
|
||||
/// Null → dropped (diagnostic recorder bound by the host under
|
||||
/// ACDREAM_DUMP_MOTION); R3 binds MotionInterpreter.MotionDone here
|
||||
/// (register row: MotionDone observed-not-consumed until R3).
|
||||
/// </summary>
|
||||
public Action<uint, bool>? MotionDoneTarget { get; set; }
|
||||
|
||||
private sealed class ForwardingMotionDoneSink : IMotionDoneSink
|
||||
{
|
||||
private readonly AnimationSequencer _owner;
|
||||
public ForwardingMotionDoneSink(AnimationSequencer owner) => _owner = owner;
|
||||
public void MotionDone(uint motion, bool success)
|
||||
=> _owner.MotionDoneTarget?.Invoke(motion, success);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
_initialized = true;
|
||||
_manager.InitializeState();
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
|
@ -315,10 +368,16 @@ public sealed class AnimationSequencer
|
|||
/// preserve normal smooth transitions for everything else.</param>
|
||||
public void SetCycle(uint style, uint motion, float speedMod = 1f, bool skipTransitionLink = false)
|
||||
{
|
||||
// ── adjust_motion: remap left→right / backward→forward variants ───
|
||||
// ACE MotionInterp.cs:394-428. The MotionTable never stores TurnLeft,
|
||||
// SideStepLeft, or WalkBackward cycles; the client plays the mirror
|
||||
// animation with a negated speed so it runs backward.
|
||||
EnsureInitialized();
|
||||
|
||||
// ── Q4 boundary normalization (adjust_motion cyclic subset) ───────
|
||||
// Retail adjusts BEFORE the table sees the motion (CMotionInterp::
|
||||
// adjust_motion, R3 scope) — the interpreted state on the wire is
|
||||
// already post-adjustment, so remote-driven callers never pass these.
|
||||
// GameWindow's LOCAL-player path (UpdatePlayerAnimation) still passes
|
||||
// raw TurnLeft/SideStepLeft/WalkBackward, so the adapter normalizes
|
||||
// ONCE at this boundary. DELETED in R3-W6 when the local player
|
||||
// unifies onto MotionInterpreter (r3-port-plan.md J15).
|
||||
uint adjustedMotion = motion;
|
||||
float adjustedSpeed = speedMod;
|
||||
switch (motion & 0xFFFFu)
|
||||
|
|
@ -337,199 +396,35 @@ public sealed class AnimationSequencer
|
|||
break;
|
||||
}
|
||||
|
||||
// Fast-path: already playing this exact motion.
|
||||
//
|
||||
// Retail (ACE MotionTable.cs:132-139): when motion == current and
|
||||
// sign(speedMod) matches, DON'T restart the cycle — just rescale the
|
||||
// in-flight cyclic-tail's framerate via multiply_cyclic_animation_fr.
|
||||
// This keeps the run/walk loop smooth when a new UpdateMotion arrives
|
||||
// with a different ForwardSpeed (e.g. when the server broadcasts a
|
||||
// player's updated RunRate mid-step).
|
||||
//
|
||||
// **Sign-flip case (2026-05-02):** when the server sends adjust_motion'd
|
||||
// backward walk as `WalkForward + speed=-N`, motion stays 0x45000005
|
||||
// but speedMod sign flips. We MUST do a full cycle restart in that case
|
||||
// so the new (negative) framerate takes effect; otherwise the cycle
|
||||
// keeps playing forward with the old positive framerate and the
|
||||
// observer sees the player walking forward despite the negative speed.
|
||||
if (CurrentStyle == style && CurrentMotion == motion
|
||||
&& _core.FirstCyclic != null && _core.Count > 0
|
||||
&& MathF.Sign(speedMod) == MathF.Sign(CurrentSpeedMod))
|
||||
{
|
||||
if (MathF.Abs(speedMod - CurrentSpeedMod) > 1e-4f
|
||||
&& MathF.Abs(CurrentSpeedMod) > 1e-6f)
|
||||
{
|
||||
MultiplyCyclicFramerate(speedMod / CurrentSpeedMod);
|
||||
CurrentSpeedMod = speedMod;
|
||||
}
|
||||
// ── R2-Q4: dispatch through the verbatim motion-selection stack ───
|
||||
// Style change first (style-class ids route GetObjectSequence
|
||||
// Branch 1 — exit link + entry hop + target style's default cycle),
|
||||
// then the motion itself (Branch 2 cycle / 3 action / 4 modifier;
|
||||
// the Branch-2 fast re-speed path replaces the old adapter
|
||||
// fast-path, remove_redundant_links replaces Fix B, get_link's
|
||||
// reversed-key double-hop replaces the stop-anim fallback).
|
||||
if (style != 0 && style != _state.Style)
|
||||
_manager.PerformMovement(MotionTableMovement.Interpreted(style, 1f));
|
||||
|
||||
// D3 (Commit A 2026-05-03): SCFAST — proves whether the fast-path
|
||||
// is firing instead of the full rebuild. Throttled to 0.5s per
|
||||
// instance (re-throttled after A.1 unthrottled experiment).
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (nowSec - _lastSetCycleDiagTime > 0.5)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SCFAST] motion=0x{motion:X8} speedMod={speedMod:F3} "
|
||||
+ $"oldSpeedMod={CurrentSpeedMod:F3} "
|
||||
+ $"qCount={_core.Count} "
|
||||
+ $"currNodeIsCyclic={ReferenceEquals(_core.CurrAnim, _core.FirstCyclic)}");
|
||||
_lastSetCycleDiagTime = nowSec;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint dispatchResult = _manager.PerformMovement(
|
||||
MotionTableMovement.Interpreted(adjustedMotion, adjustedSpeed));
|
||||
|
||||
// Resolve transition link (currentSubstate → adjustedMotion). Pass
|
||||
// both speeds — GetLink switches lookup branches based on sign.
|
||||
// CurrentSpeedMod defaults to 1.0 (positive) on a fresh sequencer,
|
||||
// so a Ready → WalkBackward transition correctly enters GetLink's
|
||||
// negative-speed (reversed-key) branch.
|
||||
// K-fix18: when the caller asked to skip the transition link
|
||||
// (instant-engage cases like Falling on jump start), force
|
||||
// linkData to null so only the cycle gets enqueued.
|
||||
MotionData? linkData = (skipTransitionLink || CurrentMotion == 0)
|
||||
? null
|
||||
: GetLink(style, CurrentMotion, CurrentSpeedMod, adjustedMotion, adjustedSpeed);
|
||||
|
||||
// Stop-anim fallback: dat-authored leg-settle / turn-stop links are
|
||||
// keyed under the FORWARD/RIGHT variant only. Stopping from
|
||||
// WalkBackward / SideStepLeft / TurnLeft hits a null linkData and
|
||||
// would visibly snap to Ready. The settle anim is direction-agnostic
|
||||
// (legs come to standing the same way regardless of which way you
|
||||
// were walking), so retry GetLink with the substate's low-byte
|
||||
// remapped to its forward/right peer.
|
||||
if (linkData is null && !skipTransitionLink && CurrentMotion != 0)
|
||||
{
|
||||
uint substateLow = CurrentMotion & 0xFFu;
|
||||
uint adjustedSubstate = substateLow switch
|
||||
{
|
||||
0x06u => (CurrentMotion & 0xFFFFFF00u) | 0x05u, // WalkBackward → WalkForward
|
||||
0x10u => (CurrentMotion & 0xFFFFFF00u) | 0x0Fu, // SideStepLeft → SideStepRight
|
||||
0x0Eu => (CurrentMotion & 0xFFFFFF00u) | 0x0Du, // TurnLeft → TurnRight
|
||||
_ => CurrentMotion,
|
||||
};
|
||||
if (adjustedSubstate != CurrentMotion)
|
||||
{
|
||||
linkData = GetLink(style, adjustedSubstate, CurrentSpeedMod, adjustedMotion, adjustedSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve target cycle using the ADJUSTED motion (TurnRight not TurnLeft).
|
||||
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (adjustedMotion & 0xFFFFFFu));
|
||||
_mtable.Cycles.TryGetValue(cycleKey, out var cycleData);
|
||||
|
||||
// ── Rebuild path (R1-P5) ───────────────────────────────────────────
|
||||
// remove_cyclic_anims (0x00524e40): drop the old cyclic tail; keep
|
||||
// any non-cyclic head that hasn't been played yet — the core snaps
|
||||
// curr_anim back to the preceding link node (or clears to empty).
|
||||
_core.RemoveCyclicAnims();
|
||||
|
||||
// K-fix18: when the caller asked for instant-engage, ALSO drain
|
||||
// any in-flight non-cyclic transition frames from the previous
|
||||
// cycle. Without this, the old RunForward → ??? link would
|
||||
// continue draining for ~100 ms before the new Falling cycle
|
||||
// starts, defeating the "skip the link" intent.
|
||||
// clear_animations (0x00524dc0): full wipe.
|
||||
// K-fix18 (2026-04-26, register row survives → R3): instant-engage
|
||||
// cases (Falling on jump start) strip the just-selected transition
|
||||
// links so the new cycle starts immediately. Post-Q4 expression:
|
||||
// the retail primitive CPhysicsObj::RemoveLinkAnimations
|
||||
// (0x0050fe20 → CSequence::remove_all_link_animations) — exactly
|
||||
// what R3-W4's LeaveGround will call, at which point this flag and
|
||||
// its GameWindow call sites are deleted. The pending-queue entry's
|
||||
// tick count intentionally stays (retail's RemoveLinkAnimations
|
||||
// does not touch the manager queue; the countdown chain absorbs it).
|
||||
if (skipTransitionLink)
|
||||
{
|
||||
_core.ClearAnimations();
|
||||
}
|
||||
|
||||
// Clear sequence-wide physics before the rebuild. Retail's
|
||||
// GetObjectSequence calls sequence.clear_physics() before each
|
||||
// add_motion chain (MotionTable.cs L100-L101, L152-L153).
|
||||
_core.ClearPhysics();
|
||||
|
||||
// Enqueue link frames (with adjusted speed for left→right remapping).
|
||||
if (linkData is { Anims.Count: > 0 })
|
||||
EnqueueMotionData(linkData, adjustedSpeed);
|
||||
|
||||
// Enqueue new cycle.
|
||||
if (cycleData is { Anims.Count: > 0 })
|
||||
{
|
||||
EnqueueMotionData(cycleData, adjustedSpeed);
|
||||
}
|
||||
else if (_core.Count == 0)
|
||||
{
|
||||
// No cycle and no link — nothing to play; reset fully.
|
||||
CurrentStyle = style;
|
||||
CurrentMotion = motion;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix B (both old AND new locomotion low-byte, #39 2026-05-06): for
|
||||
// direct cyclic-locomotion → cyclic-locomotion transitions
|
||||
// (Walk↔Run on Shift toggle, W↔S direct flip, A↔D, Forward↔Strafe),
|
||||
// land curr_anim on the new CYCLE, not on the link, and remove the
|
||||
// just-enqueued link nodes from the list entirely.
|
||||
//
|
||||
// Why: the transition link's drain time (~100–300 ms at
|
||||
// Framerate 30 × link runSpeed) gets restarted before it can
|
||||
// end if the user toggles Shift faster than that. curr_anim
|
||||
// sits on a fresh link every UM and Advance never reaches
|
||||
// the cycle. User observes "blips forward in walking
|
||||
// animation" — what they're seeing is the link's
|
||||
// interpolation pose, never the new cycle.
|
||||
//
|
||||
// Conditional on BOTH old AND new being locomotion cycles to
|
||||
// avoid regressing the cases where the link IS the right
|
||||
// animation:
|
||||
// - Idle (Ready) → any cycle: link is the wind-up pose
|
||||
// - Falling → Ready: landing animation
|
||||
// - Ready → Sitting/Crouching: pose-change links
|
||||
// - Combat substates (attack/parry/ready transitions)
|
||||
// Commit c06b6c5 (reverted in a2ae2ae) demonstrated that
|
||||
// unconditionally skipping the link breaks all of these.
|
||||
//
|
||||
// Retail reference: cdb live trace 2026-05-03 of a Walk→Run
|
||||
// direct transition logged
|
||||
// add_to_queue(45000005, looping=1) walk
|
||||
// add_to_queue(44000007, looping=1) run
|
||||
// with truncate_animation_list never firing — i.e. retail
|
||||
// appends the new cycle directly without a separate link
|
||||
// enqueue or visible link pose for cyclic→cyclic. Our
|
||||
// structural mismatch was always enqueueing link+cycle and
|
||||
// forcing curr onto the link; this fix matches retail's
|
||||
// observed semantics for the locomotion subset.
|
||||
//
|
||||
// Retail mechanism = remove_redundant_links (0x0051bf20, R2 scope,
|
||||
// gap map "Invented behaviors" note). Ported here as
|
||||
// remove_all_link_animations (the R1 core's verbatim remove-family
|
||||
// primitive) — this IS Fix B's outcome for the locomotion subset,
|
||||
// now expressed through the core instead of hand-rolled queue
|
||||
// surgery.
|
||||
bool prevIsLocomotion = IsLocomotionCycleLowByte(CurrentMotion & 0xFFu);
|
||||
bool newIsLocomotion = IsLocomotionCycleLowByte(motion & 0xFFu);
|
||||
if (prevIsLocomotion && newIsLocomotion && _core.FirstCyclic is not null)
|
||||
{
|
||||
_core.RemoveAllLinkAnimations();
|
||||
}
|
||||
|
||||
// D3 (Commit A 2026-05-03): SCFULL — counterpart to SCFAST. Fires on
|
||||
// the full-rebuild SetCycle path. Throttled to 0.5s per instance.
|
||||
// Logs prev CurrentMotion so the line shows the transition directly
|
||||
// (e.g. "Run → Ready" = cycle just got reset).
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (nowSec - _lastSetCycleDiagTime > 0.5)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SCFULL] prev=0x{CurrentMotion:X8} -> motion=0x{motion:X8} adjustedMotion=0x{adjustedMotion:X8} "
|
||||
+ $"speedMod={speedMod:F3} "
|
||||
+ $"qCount={_core.Count} "
|
||||
+ $"currNodeIsCyclic={ReferenceEquals(_core.CurrAnim, _core.FirstCyclic)} "
|
||||
+ $"firstCyclicNull={(_core.FirstCyclic is null)}");
|
||||
_lastSetCycleDiagTime = nowSec;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentStyle = style;
|
||||
CurrentMotion = motion;
|
||||
CurrentSpeedMod = speedMod;
|
||||
// Failed dispatch (missing cycle for this style, is_allowed reject):
|
||||
// retail leaves sequence AND state untouched — no synthesis either.
|
||||
if (dispatchResult != MotionTableManagerError.Success)
|
||||
return;
|
||||
|
||||
// ── Synthesize CurrentVelocity for locomotion cycles ──────────────
|
||||
// The Humanoid motion table ships every locomotion MotionData with
|
||||
|
|
@ -637,48 +532,11 @@ public sealed class AnimationSequencer
|
|||
private const float RunAnimSpeed = 4.0f;
|
||||
private const float SidestepAnimSpeed = 1.25f;
|
||||
|
||||
/// <summary>
|
||||
/// Scale every cyclic node's framerate by <paramref name="factor"/>,
|
||||
/// mirroring retail's <c>multiply_cyclic_animation_fr</c> (0x00524940):
|
||||
/// walks <c>first_cyclic</c> through the tail of the list and scales
|
||||
/// each node's framerate. The non-cyclic head (link frames) is
|
||||
/// untouched — those drain at their original framerate, which matches
|
||||
/// retail: the sequencer "catches up" the transition before applying
|
||||
/// the new run speed.
|
||||
///
|
||||
/// <para>
|
||||
/// Called from <see cref="SetCycle"/> when the same (style, motion) pair
|
||||
/// is re-issued with a different speedMod — for instance, when a remote
|
||||
/// player's ForwardSpeed changes mid-run. Does NOT restart the animation,
|
||||
/// so footsteps keep planting where they are.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// R1-P5 / gap-map G13: the core's <c>MultiplyCyclicAnimationFramerate</c>
|
||||
/// touches ONLY node framerates (retail-verbatim). The velocity/omega
|
||||
/// rescale below is the ADAPTER-level stand-in for retail's
|
||||
/// <c>change_cycle_speed</c> composite (<c>subtract_motion(oldSpeed)</c>
|
||||
/// + <c>combine_motion(newSpeed)</c>, algebraically equal to scaling by
|
||||
/// newSpeed/oldSpeed) — an R2 mechanism this adapter keeps inline until
|
||||
/// R2 routes speed changes through the real subtract/combine_motion
|
||||
/// pair. See gap map G13 for the register-row rationale.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="factor">Framerate multiplier (newSpeed / oldSpeed).</param>
|
||||
public void MultiplyCyclicFramerate(float factor)
|
||||
{
|
||||
if (_core.FirstCyclic == null) return;
|
||||
if (factor < 0f || float.IsNaN(factor) || float.IsInfinity(factor))
|
||||
return;
|
||||
|
||||
_core.MultiplyCyclicAnimationFramerate(factor);
|
||||
|
||||
// R2-composite stand-in (G13): retail's change_cycle_speed rescales
|
||||
// velocity/omega via subtract_motion(old)+combine_motion(new), which
|
||||
// is algebraically newSpeed/oldSpeed — exactly this factor.
|
||||
_core.SetVelocity(_core.Velocity * factor);
|
||||
_core.SetOmega(_core.Omega * factor);
|
||||
}
|
||||
// R2-Q4: MultiplyCyclicFramerate DELETED (zero external callers). The
|
||||
// gap-map-G13 composite stand-in (framerate scale + velocity/omega
|
||||
// rescale) is retired — same-motion re-speeds now route through the
|
||||
// verbatim CMotionTable fast re-speed path (change_cycle_speed +
|
||||
// subtract_motion(old) + combine_motion(new), decomp §5).
|
||||
|
||||
/// <summary>
|
||||
/// Advance the animation by <paramref name="dt"/> seconds and return the
|
||||
|
|
@ -751,150 +609,48 @@ public sealed class AnimationSequencer
|
|||
// contract — for R6's per-tick wiring.
|
||||
|
||||
/// <summary>
|
||||
/// Play a one-shot action/modifier motion (Jump, emote, attack, etc.)
|
||||
/// on top of the current cycle. The action frames are inserted in the
|
||||
/// list immediately before the looping cyclic tail; they drain once
|
||||
/// and then the cycle resumes naturally.
|
||||
/// Play a one-shot action / modifier motion (Jump, emote, attack, etc.)
|
||||
/// on top of the current cycle.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail semantics: actions and modifiers live in
|
||||
/// <see cref="MotionTable.Modifiers"/> (a separate dict from
|
||||
/// <see cref="MotionTable.Cycles"/>) keyed by
|
||||
/// <c>(style << 16) | (motion & 0xFFFFFF)</c>. A motion like
|
||||
/// <c>Jump = 0x2500003b</c> is a Modifier (class byte 0x25) not a
|
||||
/// SubState — feeding it to <see cref="SetCycle"/> silently fails the
|
||||
/// cycle lookup. Routing through <c>PlayAction</c> instead resolves
|
||||
/// from the Modifiers table and interleaves the action frames with
|
||||
/// the ongoing cyclic motion.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// If no entry is found in the Modifiers table for the requested
|
||||
/// motion, this is a no-op.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// R1-P5: inserts directly on the core's internal list via
|
||||
/// <see cref="CSequence.AnimList"/> / <see cref="CSequence.FirstCyclicNode"/> /
|
||||
/// <see cref="CSequence.CurrAnimNode"/> (internal members visible within
|
||||
/// this assembly). Preserves the exact pre-cutover semantics: action
|
||||
/// nodes insert ahead of the cyclic tail and play once; if the cursor
|
||||
/// was sitting on the cyclic tail (or nothing was playing), it jumps to
|
||||
/// the first inserted action node so the action plays immediately.
|
||||
/// R2-Q4: a thin shim over
|
||||
/// <see cref="MotionTableManager.PerformMovement"/> — action-class ids
|
||||
/// (0x10000000) route <c>GetObjectSequence</c> Branch 3 (rebuild:
|
||||
/// substate→action link + base cycle re-added, the action tracked on the
|
||||
/// MotionState action FIFO and popped by the manager's countdown);
|
||||
/// modifier-class ids (0x20000000) route Branch 4 (PHYSICS-ONLY
|
||||
/// <c>combine_motion</c> — no animation frames; the pre-Q4
|
||||
/// insert-before-tail modifier-anim mechanism was an acdream invention,
|
||||
/// deleted). Unknown ids fail the dispatch and are a no-op, matching the
|
||||
/// pre-Q4 contract.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="motionCommand">Raw MotionCommand (e.g. 0x2500003b for Jump).</param>
|
||||
/// <param name="speedMod">Speed multiplier for the action's framerate.</param>
|
||||
public void PlayAction(uint motionCommand, float speedMod = 1f)
|
||||
{
|
||||
// Resolve motion data. The lookup depends on the command's mask class:
|
||||
//
|
||||
// - Action (mask 0x10): stored in the Links dict as the transition
|
||||
// FROM currentSubstate TO the action motion. Matches ACE
|
||||
// MotionTable.GetObjectSequence @ line 189-207 (CommandMask.Action).
|
||||
// - Modifier (mask 0x20): stored in the Modifiers dict, keyed by
|
||||
// (style<<16) | (motion&0xFFFFFF) (or unstyled key). Matches ACE
|
||||
// @ line 234-242 (CommandMask.Modifier).
|
||||
//
|
||||
// Jump (0x2500003B) has BOTH bits set (0x20|0x04|0x01) but ACE treats
|
||||
// it via the Modifier path. FallDown (0x10000050) / Jumpup (0x1000004B)
|
||||
// are pure Actions (mask 0x10) and live in Links.
|
||||
//
|
||||
// We try Links first (via GetLink, which reproduces ACE's get_link
|
||||
// fallback chain). If that fails and the motion is a Modifier, fall
|
||||
// through to the Modifiers dict.
|
||||
const uint ActionMask = 0x10000000u;
|
||||
const uint ModifierMask = 0x20000000u;
|
||||
|
||||
MotionData? data = null;
|
||||
if ((motionCommand & ActionMask) != 0 && CurrentMotion != 0)
|
||||
{
|
||||
// Action: look up the transition link from current substate → action.
|
||||
// Action overlays always play forward (positive speeds) — the
|
||||
// action speed mod is the caller-supplied modifier, not part of
|
||||
// the substate cycle's direction.
|
||||
data = GetLink(CurrentStyle, CurrentMotion, /*substateSpeed:*/ 1f, motionCommand, /*speed:*/ 1f);
|
||||
}
|
||||
if (data is null && (motionCommand & ModifierMask) != 0)
|
||||
{
|
||||
uint styleKey = CurrentStyle << 16;
|
||||
int keyStyled = (int)(styleKey | (motionCommand & 0xFFFFFFu));
|
||||
int keyPlain = (int)(motionCommand & 0xFFFFFFu);
|
||||
if (!_mtable.Modifiers.TryGetValue(keyStyled, out data))
|
||||
_mtable.Modifiers.TryGetValue(keyPlain, out data);
|
||||
}
|
||||
|
||||
if (data is null || data.Anims.Count == 0)
|
||||
return;
|
||||
|
||||
// Build AnimSequenceNodes from the action's AnimData list. All
|
||||
// non-looping — they drain once, then the list falls through to
|
||||
// first_cyclic. Velocity/omega gate matches EnqueueMotionData (G17).
|
||||
Vector3 vel = data.Flags.HasFlag(MotionDataFlags.HasVelocity)
|
||||
? data.Velocity * speedMod : Vector3.Zero;
|
||||
Vector3 omg = data.Flags.HasFlag(MotionDataFlags.HasOmega)
|
||||
? data.Omega * speedMod : Vector3.Zero;
|
||||
|
||||
var newNodes = new List<AnimSequenceNode>(data.Anims.Count);
|
||||
foreach (var ad in data.Anims)
|
||||
{
|
||||
var node = BuildNode(ad, speedMod);
|
||||
if (node.HasAnim) newNodes.Add(node);
|
||||
}
|
||||
if (newNodes.Count == 0) return;
|
||||
|
||||
var animList = _core.AnimList;
|
||||
var firstCyclicNode = _core.FirstCyclicNode;
|
||||
|
||||
// Insert before the cyclic tail (so the action plays, then cycle
|
||||
// resumes). If there's no cyclic tail yet, append at the end.
|
||||
LinkedListNode<AnimSequenceNode>? firstInserted = null;
|
||||
if (firstCyclicNode != null)
|
||||
{
|
||||
foreach (var n in newNodes)
|
||||
{
|
||||
var inserted = animList.AddBefore(firstCyclicNode, n);
|
||||
firstInserted ??= inserted;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var n in newNodes)
|
||||
{
|
||||
var inserted = animList.AddLast(n);
|
||||
firstInserted ??= inserted;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Flags.HasFlag(MotionDataFlags.HasVelocity))
|
||||
_core.SetVelocity(vel);
|
||||
if (data.Flags.HasFlag(MotionDataFlags.HasOmega))
|
||||
_core.SetOmega(omg);
|
||||
|
||||
// If we're currently on the cyclic tail (or nothing was playing),
|
||||
// jump the cursor back to the first newly-inserted action node so
|
||||
// the action plays immediately instead of after the next cycle wrap.
|
||||
var currNode = _core.CurrAnimNode;
|
||||
bool cursorOnCyclic = currNode != null && ReferenceEquals(currNode, firstCyclicNode);
|
||||
if (cursorOnCyclic || currNode == null)
|
||||
{
|
||||
_core.CurrAnimNode = firstInserted;
|
||||
if (firstInserted != null)
|
||||
_core.FrameNumber = firstInserted.Value.GetStartingFrame();
|
||||
}
|
||||
EnsureInitialized();
|
||||
_manager.PerformMovement(MotionTableMovement.Interpreted(motionCommand, speedMod));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the sequencer to an unplaying state without clearing the
|
||||
/// motion table reference.
|
||||
/// motion table reference. R2-Q4: drains the pending-animation queue
|
||||
/// (exit-world semantics — each entry fires MotionDone(success:false))
|
||||
/// and zeroes the MotionState so the next SetCycle re-runs
|
||||
/// initialize_state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_manager.HandleExitWorld();
|
||||
_core.Clear();
|
||||
_pendingHooks.Clear();
|
||||
CurrentStyle = 0;
|
||||
CurrentMotion = 0;
|
||||
CurrentSpeedMod = 1f;
|
||||
_state.Style = 0;
|
||||
_state.Substate = 0;
|
||||
_state.SubstateMod = 1f;
|
||||
_state.ClearModifiers();
|
||||
_state.ClearActions();
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
// ── Private helpers ──────────────────────────────────────────────────────
|
||||
|
|
@ -925,150 +681,13 @@ public sealed class AnimationSequencer
|
|||
private static readonly AnimationDoneHook AnimationDoneSentinel =
|
||||
new() { Direction = AnimationHookDir.Both };
|
||||
|
||||
/// <summary>
|
||||
/// Look up the transition MotionData for going from <paramref name="substate"/>
|
||||
/// (current state, played at <paramref name="substateSpeed"/>) to
|
||||
/// <paramref name="motion"/> (new state, played at <paramref name="speed"/>).
|
||||
///
|
||||
/// <para>
|
||||
/// Port of ACE's MotionTable.get_link (MotionTable.cs:395-426). The lookup
|
||||
/// path differs by sign of the speeds — the retail/ACE mechanism is two
|
||||
/// distinct branches:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Both speeds positive</b> (forward → forward, normal case):
|
||||
/// Look up Links[(style<<16) | substate][motion] — the link FROM
|
||||
/// substate TO motion. Played forward.</item>
|
||||
/// <item><b>Either speed negative</b> (any direction reversal —
|
||||
/// WalkBackward, SideStepLeft, TurnLeft): Look up the REVERSED key
|
||||
/// Links[(style<<16) | motion][substate] — the link FROM motion TO
|
||||
/// substate. Played in reverse, this anim visually transitions
|
||||
/// substate → motion's pose, then the cycle continues from where it
|
||||
/// left off. Without this branch, Ready→WalkBackward would queue the
|
||||
/// "start walking forward" link played in reverse, which strands the
|
||||
/// cursor at the wrong cycle frame and causes the user-visible
|
||||
/// "left leg twitches forward two times" glitch on the X key.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// DatReaderWriter encodes Links as Dictionary<int, MotionCommandData>
|
||||
/// where MotionCommandData.MotionData is Dictionary<int, MotionData>.
|
||||
/// </summary>
|
||||
private MotionData? GetLink(uint style, uint substate, float substateSpeed, uint motion, float speed)
|
||||
{
|
||||
if (speed < 0f || substateSpeed < 0f)
|
||||
{
|
||||
// Reversed-direction path: link FROM motion TO substate.
|
||||
int reversedKey = (int)((style << 16) | (motion & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(reversedKey, out var revLink)
|
||||
&& revLink.MotionData.TryGetValue((int)substate, out var revResult))
|
||||
{
|
||||
return revResult;
|
||||
}
|
||||
// R2-Q4: GetLink DELETED - re-homed verbatim as CMotionTable.GetLink
|
||||
// (Q2, field-validated reversed-key branch preserved; Q0-pins A1).
|
||||
|
||||
// Style-defaults fallback per ACE MotionTable.cs:405-409.
|
||||
if (_mtable.StyleDefaults.TryGetValue(
|
||||
(DatReaderWriter.Enums.MotionCommand)style, out var defaultMotion))
|
||||
{
|
||||
int subKey = (int)((style << 16) | (substate & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(subKey, out var subLink)
|
||||
&& subLink.MotionData.TryGetValue((int)defaultMotion, out var subResult))
|
||||
{
|
||||
return subResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Forward-direction path: link FROM substate TO motion (the original
|
||||
// implementation pre-K-fix6).
|
||||
int outerKey1 = (int)((style << 16) | (substate & 0xFFFFFFu));
|
||||
if (_mtable.Links.TryGetValue(outerKey1, out var cmd1)
|
||||
&& cmd1.MotionData.TryGetValue((int)motion, out var result1))
|
||||
{
|
||||
return result1;
|
||||
}
|
||||
|
||||
// Fallback: style-level catch-all (ACE line 419-422).
|
||||
int outerKey2 = (int)(style << 16);
|
||||
if (_mtable.Links.TryGetValue(outerKey2, out var cmd2)
|
||||
&& cmd2.MotionData.TryGetValue((int)motion, out var result2))
|
||||
{
|
||||
return result2;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build one <see cref="AnimSequenceNode"/> from an <see cref="AnimData"/>,
|
||||
/// speed-scaled retail-style (only Framerate scales — retail
|
||||
/// <c>AnimData::operator*</c>, 0x00525d00). Returns a node whose
|
||||
/// <see cref="AnimSequenceNode.HasAnim"/> may be false (unresolved dat
|
||||
/// id); callers filter those out.
|
||||
/// </summary>
|
||||
private AnimSequenceNode BuildNode(AnimData ad, float speedMod)
|
||||
{
|
||||
var scaled = new AnimData
|
||||
{
|
||||
AnimId = ad.AnimId,
|
||||
LowFrame = ad.LowFrame,
|
||||
HighFrame = ad.HighFrame,
|
||||
Framerate = ad.Framerate * speedMod,
|
||||
};
|
||||
return new AnimSequenceNode(scaled, _loader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append all AnimData entries from <paramref name="motionData"/> to the
|
||||
/// core's animation list (retail free function <c>add_motion</c>,
|
||||
/// 0x005224b0). Each AnimData becomes one <see cref="AnimSequenceNode"/>
|
||||
/// via <see cref="CSequence.AppendAnimation"/>, which slides
|
||||
/// <c>first_cyclic</c> to the just-appended node on EVERY call (G10) —
|
||||
/// so a multi-anim MotionData's loop membership is naturally "only the
|
||||
/// LAST AnimData loops" once a subsequent MotionData is appended, and a
|
||||
/// single MotionData with N AnimData entries makes the WHOLE MotionData
|
||||
/// the cyclic tail as of its last entry.
|
||||
/// </summary>
|
||||
/// <param name="motionData">The MotionData to enqueue.</param>
|
||||
/// <param name="speedMod">Speed multiplier for AnimData framerate scaling.</param>
|
||||
/// <remarks>
|
||||
/// The <c>isLooping</c> parameter from the pre-cutover signature is
|
||||
/// deleted — retail has no per-call loop flag (G10); loop membership is
|
||||
/// purely a function of append order via <c>first_cyclic</c>.
|
||||
/// </remarks>
|
||||
private void EnqueueMotionData(MotionData motionData, float speedMod)
|
||||
{
|
||||
// Sequence-wide velocity/omega: retail add_motion (0x005224b0) calls
|
||||
// set_velocity/set_omega UNCONDITIONALLY (G17 core semantics — a
|
||||
// MotionData without HasVelocity carries a zero Velocity field, so
|
||||
// "unconditional replace" and "replace with zero" are the same
|
||||
// retail call). The adapter keeps today's HasVelocity/HasOmega GATE
|
||||
// (G17 adapter split) so a transient zero-velocity modifier doesn't
|
||||
// stomp a running cycle's velocity — this is the documented R2
|
||||
// stand-in until modifiers route through combine_motion instead of
|
||||
// add_motion. See gap map G17.
|
||||
if (motionData.Flags.HasFlag(MotionDataFlags.HasVelocity))
|
||||
_core.SetVelocity(motionData.Velocity * speedMod);
|
||||
if (motionData.Flags.HasFlag(MotionDataFlags.HasOmega))
|
||||
_core.SetOmega(motionData.Omega * speedMod);
|
||||
|
||||
foreach (var ad in motionData.Anims)
|
||||
{
|
||||
// Speed-scale (retail AnimData::operator*, 0x00525d00 — ONLY
|
||||
// framerate scales) then hand the scaled AnimData straight to
|
||||
// append_animation; the core resolves + clamps via
|
||||
// AnimSequenceNode.SetAnimationId and silently discards nodes
|
||||
// whose dat id doesn't resolve (mirrors BuildNode's HasAnim
|
||||
// filter without constructing a throwaway node here).
|
||||
_core.AppendAnimation(new AnimData
|
||||
{
|
||||
AnimId = ad.AnimId,
|
||||
LowFrame = ad.LowFrame,
|
||||
HighFrame = ad.HighFrame,
|
||||
Framerate = ad.Framerate * speedMod,
|
||||
});
|
||||
}
|
||||
}
|
||||
// R2-Q4: BuildNode + EnqueueMotionData DELETED - MotionData appends are
|
||||
// owned by CMotionTable.AddMotion (add_motion 0x005224b0, unconditional
|
||||
// velocity/omega set = G17 core semantics; the adapter's HasVelocity/
|
||||
// HasOmega gate is retired with them).
|
||||
|
||||
/// <summary>
|
||||
/// Build the per-part blended transform from the current animation frame.
|
||||
|
|
@ -1166,19 +785,10 @@ public sealed class AnimationSequencer
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the given motion-low-byte names a locomotion cycle —
|
||||
/// WalkForward (0x05), WalkBackward (0x06), RunForward (0x07),
|
||||
/// SideStepRight (0x0F), or SideStepLeft (0x10).
|
||||
/// Used by <see cref="SetCycle"/> to recognise cyclic→cyclic
|
||||
/// direct transitions and bypass the transition link in that case
|
||||
/// (retail's observed add_to_queue semantics).
|
||||
/// </summary>
|
||||
private static bool IsLocomotionCycleLowByte(uint lowByte)
|
||||
{
|
||||
return lowByte == 0x05u || lowByte == 0x06u || lowByte == 0x07u
|
||||
|| lowByte == 0x0Fu || lowByte == 0x10u;
|
||||
}
|
||||
// R2-Q4: IsLocomotionCycleLowByte DELETED with Fix B - the cyclic-to-
|
||||
// cyclic link-skip is now retail's remove_redundant_links (0x0051bf20,
|
||||
// MotionTableManager.RemoveRedundantLinks) operating on the pending
|
||||
// queue, not a locomotion-subset special case in the adapter.
|
||||
|
||||
/// <summary>
|
||||
/// Quaternion slerp matching the retail client's <c>FUN_005360d0</c>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
|
@ -24,7 +25,13 @@ public sealed class AnimationCommandRouterTests
|
|||
[Fact]
|
||||
public void RouteWireCommand_SubState_UsesSetCycle()
|
||||
{
|
||||
var seq = MakeEmptySequencer();
|
||||
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
|
||||
// — GetObjectSequence also refuses substate==0, so the MotionTable
|
||||
// needs both a StyleDefaults entry AND cycles for the style's default
|
||||
// substate (Ready, installed by initialize_state) and for the routed
|
||||
// Dead substate, or the whole dispatch chain silently no-ops and
|
||||
// CurrentStyle/CurrentMotion stay at their zero defaults.
|
||||
var seq = MakeRoutableSequencer();
|
||||
|
||||
var route = AnimationCommandRouter.RouteWireCommand(seq, NonCombat, 0x0011);
|
||||
|
||||
|
|
@ -59,8 +66,66 @@ public sealed class AnimationCommandRouterTests
|
|||
return new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R2-Q4: a MotionTable that can actually complete a SubState dispatch —
|
||||
/// StyleDefaults[NonCombat]=Ready (required by initialize_state's
|
||||
/// SetDefaultState) plus cycles for both Ready (the installed baseline)
|
||||
/// and Dead (the substate this test routes to). One shared part/anim is
|
||||
/// enough; RouteWireCommand only inspects CurrentStyle/CurrentMotion.
|
||||
/// </summary>
|
||||
private static AnimationSequencer MakeRoutableSequencer()
|
||||
{
|
||||
const uint Ready = 0x41000003u;
|
||||
const uint Dead = 0x40000011u;
|
||||
const uint AnimId = 0x03000001u;
|
||||
|
||||
var setup = new Setup();
|
||||
setup.Parts.Add(0x01000000u);
|
||||
setup.DefaultScale.Add(System.Numerics.Vector3.One);
|
||||
|
||||
var mt = new MotionTable
|
||||
{
|
||||
DefaultStyle = (DatReaderWriter.Enums.MotionCommand)NonCombat,
|
||||
};
|
||||
mt.StyleDefaults[(DatReaderWriter.Enums.MotionCommand)NonCombat] =
|
||||
(DatReaderWriter.Enums.MotionCommand)Ready;
|
||||
|
||||
int ReadyKey = (int)((NonCombat << 16) | (Ready & 0xFFFFFFu));
|
||||
int DeadKey = (int)((NonCombat << 16) | (Dead & 0xFFFFFFu));
|
||||
mt.Cycles[ReadyKey] = MakeMotionData(AnimId);
|
||||
mt.Cycles[DeadKey] = MakeMotionData(AnimId);
|
||||
|
||||
var loader = new NullAnimationLoader();
|
||||
loader.Register(AnimId, MakeAnim());
|
||||
|
||||
return new AnimationSequencer(setup, mt, loader);
|
||||
}
|
||||
|
||||
private static MotionData MakeMotionData(uint animId)
|
||||
{
|
||||
var md = new MotionData();
|
||||
QualifiedDataId<Animation> qid = animId;
|
||||
md.Anims.Add(new AnimData { AnimId = qid, LowFrame = 0, HighFrame = -1, Framerate = 30f });
|
||||
return md;
|
||||
}
|
||||
|
||||
private static Animation MakeAnim()
|
||||
{
|
||||
var anim = new Animation();
|
||||
var pf = new AnimationFrame(1);
|
||||
pf.Frames.Add(new Frame
|
||||
{
|
||||
Origin = System.Numerics.Vector3.Zero,
|
||||
Orientation = System.Numerics.Quaternion.Identity,
|
||||
});
|
||||
anim.PartFrames.Add(pf);
|
||||
return anim;
|
||||
}
|
||||
|
||||
private sealed class NullAnimationLoader : IAnimationLoader
|
||||
{
|
||||
public Animation? LoadAnimation(uint id) => null;
|
||||
private readonly System.Collections.Generic.Dictionary<uint, Animation> _anims = new();
|
||||
public void Register(uint id, Animation anim) => _anims[id] = anim;
|
||||
public Animation? LoadAnimation(uint id) => _anims.TryGetValue(id, out var a) ? a : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,8 +254,16 @@ public sealed class AnimationSequencerCutoverTraceTests
|
|||
seq.SetCycle(NC, Ready);
|
||||
seq.SetCycle(NC, Walk, 1.0f);
|
||||
seq.SetCycle(NC, Run, 2.0f);
|
||||
// EXPECTED-DIFF(Q4): pre-cutover Fix B stripped every link leaving
|
||||
// "102@60.0*^". The verbatim GetObjectSequence (Branch 2, no direct
|
||||
// Walk->Run link in this fixture) routes the DOUBLE-HOP: Walk->Ready
|
||||
// settle (105 at the OLD substate mod) + Ready->Run windup (104 at
|
||||
// the new speed) + the Run cycle; the old Ready->Walk link (103)
|
||||
// keeps draining first (pending-queue discipline). Rapid same-motion
|
||||
// re-issues now collapse via remove_redundant_links (the retail
|
||||
// Fix B), not an adapter locomotion special case.
|
||||
Assert.Equal(
|
||||
"102@60.0*^ | frame=0.0 vel=(0.00,8.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=44000007 mod=2.00",
|
||||
"103@30.0^,105@30.0,104@60.0,102@60.0* | frame=0.0 vel=(0.00,8.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=44000007 mod=2.00",
|
||||
Describe(seq, loader));
|
||||
}
|
||||
|
||||
|
|
@ -267,8 +275,12 @@ public sealed class AnimationSequencerCutoverTraceTests
|
|||
seq.SetCycle(NC, Walk, 1.0f);
|
||||
seq.SetCycle(NC, Run, 2.0f);
|
||||
seq.SetCycle(NC, Run, 2.5f);
|
||||
// EXPECTED-DIFF(Q4): inherits S3's double-hop list; the re-speed
|
||||
// itself is the verbatim Branch-2 fast path (change_cycle_speed
|
||||
// scales ONLY first_cyclic..tail: 102 60->75; links untouched;
|
||||
// velocity via subtract_motion(2.0)+combine_motion(2.5)).
|
||||
Assert.Equal(
|
||||
"102@75.0*^ | frame=0.0 vel=(0.00,10.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=44000007 mod=2.50",
|
||||
"103@30.0^,105@30.0,104@60.0,102@75.0* | frame=0.0 vel=(0.00,10.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=44000007 mod=2.50",
|
||||
Describe(seq, loader));
|
||||
}
|
||||
|
||||
|
|
@ -278,11 +290,18 @@ public sealed class AnimationSequencerCutoverTraceTests
|
|||
var seq = NewSeq(out var loader);
|
||||
seq.SetCycle(NC, Ready);
|
||||
seq.SetCycle(NC, WalkBack, 1.0f);
|
||||
// Reversed-key GetLink resolves the Walk→Ready link (0x105) played in
|
||||
// reverse (fr=-19.5 = 30 × -0.65 BackwardsFactor); cursor starts at
|
||||
// the link's reverse starting frame (HighFrame+1 = 3).
|
||||
// Node list BYTE-IDENTICAL to pre-cutover: the reversed-key get_link
|
||||
// resolves the Walk->Ready link (0x105) played in reverse (fr=-19.5 =
|
||||
// 30 x -0.65 BackwardsFactor); cursor at the reverse starting frame
|
||||
// (HighFrame+1 = 3).
|
||||
// EXPECTED-DIFF(Q4), mirrors only: CurrentMotion now reads the
|
||||
// POST-adjust_motion substate (45000005, was 45000006) and
|
||||
// CurrentSpeedMod the signed mod (-0.65, was 1.00) - MotionState owns
|
||||
// the state and retail's interpreted state is post-adjustment. The
|
||||
// om=(-0.00,...) is IEEE negative zero from add_motion's
|
||||
// zero-omega x negative-speed multiply (numerically equal to 0).
|
||||
Assert.Equal(
|
||||
"105@-19.5^,101@-19.5* | frame=3.0 vel=(0.00,-2.03,0.00) om=(0.00,0.00,0.00) style=8000003D motion=45000006 mod=1.00",
|
||||
"105@-19.5^,101@-19.5* | frame=3.0 vel=(0.00,-2.03,0.00) om=(-0.00,-0.00,-0.00) style=8000003D motion=45000005 mod=-0.65",
|
||||
Describe(seq, loader));
|
||||
}
|
||||
|
||||
|
|
@ -293,12 +312,14 @@ public sealed class AnimationSequencerCutoverTraceTests
|
|||
seq.SetCycle(NC, Ready);
|
||||
seq.SetCycle(NC, WalkBack, 1.0f);
|
||||
seq.SetCycle(NC, Ready, 1.0f);
|
||||
// Pre-cutover: the OLD reversed link (0x105@-19.5, mid-drain) stays at
|
||||
// the head while the settle link (0x105@30, via the stop-anim
|
||||
// low-byte fallback 0x06→0x05) and the Ready cycle append behind it —
|
||||
// link-before-link stacking.
|
||||
// EXPECTED-DIFF(Q4): pre-cutover the stop-anim low-byte fallback
|
||||
// re-keyed the settle as 105@30 (forward). The verbatim get_link
|
||||
// (SubstateMod=-0.65 routes the REVERSED-key branch) resolves the
|
||||
// Ready->Walk windup (103) played in REVERSE (fr=-30) - retail's
|
||||
// actual backward-walk settle. The old reversed link (105@-19.5,
|
||||
// mid-drain) still drains first, unchanged from pre-cutover.
|
||||
Assert.Equal(
|
||||
"105@-19.5^,105@30.0,100@30.0* | frame=3.0 vel=(0.00,0.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=41000003 mod=1.00",
|
||||
"105@-19.5^,103@-30.0,100@30.0* | frame=3.0 vel=(0.00,0.00,0.00) om=(0.00,0.00,0.00) style=8000003D motion=41000003 mod=1.00",
|
||||
Describe(seq, loader));
|
||||
}
|
||||
|
||||
|
|
@ -332,8 +353,13 @@ public sealed class AnimationSequencerCutoverTraceTests
|
|||
seq.SetCycle(NC, Ready);
|
||||
seq.SetCycle(NC, Walk, 1.0f);
|
||||
seq.PlayAction(TurnMod, 1.0f);
|
||||
// EXPECTED-DIFF(Q4): pre-cutover PlayAction INSERTED the modifier's
|
||||
// anim (109) before the cyclic tail - an acdream invention. Retail
|
||||
// Branch 4 is PHYSICS-ONLY combine_motion (the AP-73 mechanism): the
|
||||
// walk cycle's frames are untouched, the modifier contributes omega
|
||||
// (0,0,1.5) and is tracked on the MotionState modifier stack.
|
||||
Assert.Equal(
|
||||
"103@30.0^,109@30.0,101@30.0* | frame=0.0 vel=(0.00,3.12,0.00) om=(0.00,0.00,1.50) style=8000003D motion=45000005 mod=1.00",
|
||||
"103@30.0^,101@30.0* | frame=0.0 vel=(0.00,3.12,0.00) om=(0.00,0.00,1.50) style=8000003D motion=45000005 mod=1.00",
|
||||
Describe(seq, loader));
|
||||
}
|
||||
|
||||
|
|
@ -343,8 +369,12 @@ public sealed class AnimationSequencerCutoverTraceTests
|
|||
var seq = NewSeq(out var loader);
|
||||
seq.SetCycle(NC, Ready);
|
||||
seq.SetCycle(Style2, Ready, 1.0f);
|
||||
// EXPECTED-DIFF(Q4): pre-cutover switched cycles bare ("107@30.0*^").
|
||||
// GetObjectSequence Branch 1 (style-change) plays the cross-style
|
||||
// entry link (10A = Ready->Style2) before the target style's default
|
||||
// cycle - retail's stance-transition animation.
|
||||
Assert.Equal(
|
||||
"107@30.0*^ | frame=0.0 vel=(0.00,0.00,0.00) om=(0.00,0.00,0.00) style=8000004C motion=41000003 mod=1.00",
|
||||
"10A@30.0^,107@30.0* | frame=0.0 vel=(0.00,0.00,0.00) om=(0.00,0.00,0.00) style=8000004C motion=41000003 mod=1.00",
|
||||
Describe(seq, loader));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,13 +333,21 @@ public sealed class AnimationSequencerTests
|
|||
[Fact]
|
||||
public void SetCycle_WithTransitionLink_PrependLinkFrames()
|
||||
{
|
||||
// Two animations: link (2 frames at Y=1) and cycle (4 frames at X=1).
|
||||
const uint Style = 0x003Du;
|
||||
const uint IdleMotion = 0x0003u;
|
||||
const uint WalkMotion = 0x0005u;
|
||||
// R2-Q4: GetObjectSequence gates Branch 2 (cyclic substates) on the
|
||||
// 0x40000000 class bit and Branch 1 (style change) on the top bit —
|
||||
// bare low-word ids like the pre-cutover 0x0003/0x0005 never satisfy
|
||||
// those gates and the dispatch silently fails. Tag Style/IdleMotion/
|
||||
// WalkMotion with their class bits (masking to the low 24 bits for
|
||||
// the cycle/link key hash is unaffected — CMotionTable keys on
|
||||
// `id & 0xFFFFFF`).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint IdleMotion = 0x40000003u;
|
||||
const uint WalkMotion = 0x40000005u;
|
||||
const uint IdleAnim = 0x03000012u;
|
||||
const uint CycleAnim = 0x03000010u;
|
||||
const uint LinkAnim = 0x03000011u;
|
||||
|
||||
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var cycleAnim = Fixtures.MakeAnim(4, 1, new Vector3(1, 0, 0), Quaternion.Identity);
|
||||
var linkAnim = Fixtures.MakeAnim(2, 1, new Vector3(0, 1, 0), Quaternion.Identity);
|
||||
|
||||
|
|
@ -352,15 +360,26 @@ public sealed class AnimationSequencerTests
|
|||
fromMotion: IdleMotion,
|
||||
toMotion: WalkMotion,
|
||||
linkAnimId: LinkAnim);
|
||||
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0) —
|
||||
// route it at IdleMotion (the state this test "was already playing"
|
||||
// before priming) and give IdleMotion its own cycle so the real
|
||||
// SetCycle(Style, IdleMotion) priming call below actually dispatches.
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 30f);
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(IdleAnim, idleAnim);
|
||||
loader.Register(CycleAnim, cycleAnim);
|
||||
loader.Register(LinkAnim, linkAnim);
|
||||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
|
||||
// Prime the sequencer as if it was already playing IdleMotion.
|
||||
SetCurrentMotion(seq, Style, IdleMotion);
|
||||
// Prime the sequencer as if it was already playing IdleMotion — a
|
||||
// real SetCycle call now that CurrentStyle/CurrentMotion are
|
||||
// read-only mirrors of MotionState (R2-Q4; reflection SetValue no
|
||||
// longer works, "Property set method not found").
|
||||
seq.SetCycle(Style, IdleMotion);
|
||||
|
||||
seq.SetCycle(Style, WalkMotion);
|
||||
|
||||
|
|
@ -382,9 +401,13 @@ public sealed class AnimationSequencerTests
|
|||
// link's starting pose at the link→cycle boundary. Symptoms: door
|
||||
// swing-open flap (frame 0 = closed); player run-stop twitch
|
||||
// (frame 0 = mid-stride).
|
||||
const uint Style = 0x003Du;
|
||||
const uint IdleMotion = 0x0003u;
|
||||
const uint WalkMotion = 0x0005u;
|
||||
// R2-Q4: class-bit-tagged ids (GetObjectSequence gates Branch 1/2 on
|
||||
// the 0x80000000/0x40000000 bits) — masking to the low 24 bits for
|
||||
// the cycle/link key hash is unaffected.
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint IdleMotion = 0x40000003u;
|
||||
const uint WalkMotion = 0x40000005u;
|
||||
const uint IdleAnim = 0x03000082u;
|
||||
const uint CycleAnim = 0x03000080u;
|
||||
const uint LinkAnim = 0x03000081u;
|
||||
|
||||
|
|
@ -402,6 +425,7 @@ public sealed class AnimationSequencerTests
|
|||
|
||||
// Cycle anim: single frame at Y=0 (the "open" / "idle" rest pose).
|
||||
var cycleAnim = Fixtures.MakeAnim(1, 1, new Vector3(0, 0, 0), Quaternion.Identity);
|
||||
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = Fixtures.MakeMtable(
|
||||
|
|
@ -412,13 +436,23 @@ public sealed class AnimationSequencerTests
|
|||
toMotion: WalkMotion,
|
||||
linkAnimId: LinkAnim,
|
||||
framerate: 30f);
|
||||
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
|
||||
// — route it at IdleMotion (the state we prime through below) with
|
||||
// its own cycle so the priming SetCycle call actually dispatches.
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 30f);
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(IdleAnim, idleAnim);
|
||||
loader.Register(CycleAnim, cycleAnim);
|
||||
loader.Register(LinkAnim, linkAnim);
|
||||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
SetCurrentMotion(seq, Style, IdleMotion);
|
||||
// Prime the sequencer as if it was already playing IdleMotion — a
|
||||
// real SetCycle call (reflection SetValue no longer works against
|
||||
// the now-read-only CurrentStyle/CurrentMotion mirrors).
|
||||
seq.SetCycle(Style, IdleMotion);
|
||||
seq.SetCycle(Style, WalkMotion);
|
||||
|
||||
// Advance to _framePosition ≈ 2.5 — past the last integer frame (2)
|
||||
|
|
@ -440,53 +474,71 @@ public sealed class AnimationSequencerTests
|
|||
[Fact]
|
||||
public void SetCycle_StopFromWalkBackward_FallsBackToWalkForwardStopLink()
|
||||
{
|
||||
// Stop-anim asymmetry: the Humanoid motion table only authors a
|
||||
// "stop walking" link under WalkForward (low byte 0x05). Stopping
|
||||
// from WalkBackward (0x06) without a fallback returns null linkData
|
||||
// and the cycle snaps to Ready with no settle blend. Fix: when the
|
||||
// primary GetLink lookup fails, retry with WalkBackward's low byte
|
||||
// remapped to WalkForward.
|
||||
const uint Style = 0x003Du;
|
||||
const uint WalkForwardCmd = 0x0005u;
|
||||
const uint WalkBackCmd = 0x0006u;
|
||||
const uint ReadyCmd = 0x0003u;
|
||||
// R2-Q4 EXPECTED-DIFF (mirrors AnimationSequencerCutoverTraceTests.
|
||||
// S6_WalkBackToReady_StopSettleFallback): the pre-cutover adapter had
|
||||
// an invented "stop-anim low-byte fallback" that re-keyed a
|
||||
// WalkForward->Ready link when a WalkBackward->Ready lookup missed.
|
||||
// Retail has no such fallback — CMotionTable.GetLink's verbatim
|
||||
// reversed-key branch (Q0-pins A1) does the real job: adjust_motion
|
||||
// remaps WalkBackward to WalkForward with a NEGATIVE SubstateMod, so
|
||||
// stopping from it drives GetLink's "either speed negative -> swapped
|
||||
// keys" path, which resolves the link stored FROM the style default
|
||||
// (Ready) TO WalkForward and plays it IN REVERSE (the Ready->Walk
|
||||
// windup run backward as a settle). The fixture below stores that
|
||||
// link under Links[(style,Ready)][WalkForward] — the opposite
|
||||
// direction from the old WalkForward->Ready fallback entry — because
|
||||
// that's the key GetLink's reversed branch actually probes.
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint WalkForwardCmd = 0x40000005u;
|
||||
const uint WalkBackCmd = 0x40000006u;
|
||||
const uint ReadyCmd = 0x40000003u;
|
||||
const uint CycleAnim = 0x03000090u; // Ready cycle (Y=0)
|
||||
const uint LinkAnim = 0x03000091u; // stop-link (Y=7)
|
||||
const uint LinkAnim = 0x03000091u; // Ready->Walk windup (Y=7), played reversed as the settle
|
||||
|
||||
var cycleAnim = Fixtures.MakeAnim(1, 1, new Vector3(0, 0, 0), Quaternion.Identity);
|
||||
var linkAnim = Fixtures.MakeAnim(4, 1, new Vector3(0, 7, 0), Quaternion.Identity);
|
||||
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
// Table: Ready cycle + WalkForward→Ready link. NO WalkBackward→Ready link.
|
||||
// Table: Ready cycle + Ready->WalkForward windup link (probed
|
||||
// REVERSED by GetLink's swapped-key branch when settling out of a
|
||||
// negative-SubstateMod substate). No forward WalkBackward cycle is
|
||||
// needed — adjust_motion remaps WalkBackward to WalkForward with a
|
||||
// negated + BackwardsFactor-scaled speed before dispatch ever sees it.
|
||||
var mt = Fixtures.MakeMtable(
|
||||
style: Style,
|
||||
motion: ReadyCmd,
|
||||
cycleAnimId: CycleAnim,
|
||||
fromMotion: WalkForwardCmd,
|
||||
toMotion: ReadyCmd,
|
||||
fromMotion: ReadyCmd,
|
||||
toMotion: WalkForwardCmd,
|
||||
linkAnimId: LinkAnim,
|
||||
framerate: 30f);
|
||||
// WalkForward also needs a cycle — adjust_motion's WalkBackward remap
|
||||
// dispatches WalkForward's cycle (with the negated/scaled speed) as
|
||||
// part of entering "backward" motion below.
|
||||
int walkKey = (int)((Style << 16) | (WalkForwardCmd & 0xFFFFFFu));
|
||||
mt.Cycles[walkKey] = Fixtures.MakeMotionData(CycleAnim, framerate: 30f);
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(CycleAnim, cycleAnim);
|
||||
loader.Register(LinkAnim, linkAnim);
|
||||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
// Simulate "we were walking backward" — substate = WalkBackward,
|
||||
// substateSpeed = +1 (the original speedMod stored by SetCycle).
|
||||
SetCurrentMotion(seq, Style, WalkBackCmd);
|
||||
// Enter WalkBackward for real — SetCycle's adjust_motion head remaps
|
||||
// this to WalkForward with SubstateMod = -0.65 (BackwardsFactor),
|
||||
// which is what makes the SUBSEQUENT stop-to-Ready call route
|
||||
// GetLink's reversed-key branch.
|
||||
seq.SetCycle(Style, WalkBackCmd, 1.0f);
|
||||
|
||||
seq.SetCycle(Style, ReadyCmd);
|
||||
|
||||
// Advance a tiny dt — should land on link frame 0 (Y=7), not the
|
||||
// cycle (Y=0). Without the fallback, linkData is null, only the
|
||||
// Ready cycle is enqueued, and we read Y=0 immediately.
|
||||
// Advance a tiny dt — should land on the reversed windup link
|
||||
// (Y=7), not the Ready cycle (Y=0).
|
||||
var transforms = seq.Advance(0.001f);
|
||||
Assert.Single(transforms);
|
||||
Assert.True(transforms[0].Origin.Y > 5f,
|
||||
$"Stop-from-backward should fall back to WalkForward→Ready link "
|
||||
+ $"(expect Y≈7 from link); got Y={transforms[0].Origin.Y} "
|
||||
+ "(Y=0 means linkData was null and we snapped to Ready cycle).");
|
||||
$"Stop-from-backward should resolve GetLink's reversed-key branch "
|
||||
+ $"(expect Y≈7 from the reversed windup link); got Y={transforms[0].Origin.Y} "
|
||||
+ "(Y=0 means the link didn't resolve and we snapped to the Ready cycle).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -581,10 +633,14 @@ public sealed class AnimationSequencerTests
|
|||
// with negated speed, so the animation plays in reverse.
|
||||
// We verify this by checking CurrentMotion is still TurnLeft (the
|
||||
// original command), but the sequencer internally uses TurnRight's anim.
|
||||
// R2-Q4: Style needs the 0x80000000 top bit and TurnRight/TurnLeft the
|
||||
// 0x40000000 cycle-class bit — GetObjectSequence's entry/branch gates
|
||||
// (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity) test the
|
||||
// FULL command word, not just the low 16 bits adjust_motion remaps.
|
||||
|
||||
const uint Style = 0x003Du; // NonCombat
|
||||
const uint TurnRight = 0x0045000Du; // bit pattern for TurnRight in NonCombat
|
||||
const uint TurnLeft = 0x0045000Eu; // bit pattern for TurnLeft
|
||||
const uint Style = 0x8000003Du; // NonCombat
|
||||
const uint TurnRight = 0x4045000Du; // bit pattern for TurnRight in NonCombat
|
||||
const uint TurnLeft = 0x4045000Eu; // bit pattern for TurnLeft
|
||||
const uint AnimId = 0x03000050u;
|
||||
|
||||
// 4-frame animation; each frame has a distinct Z-origin so we can tell
|
||||
|
|
@ -600,6 +656,11 @@ public sealed class AnimationSequencerTests
|
|||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
|
||||
// — route the default straight at TurnRight (the only cycle this
|
||||
// fixture defines) so initialize_state's baseline install succeeds
|
||||
// and state.Style/Substate are non-zero before the explicit dispatch.
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)TurnRight;
|
||||
|
||||
// Register TurnRight cycle (adjusted motion, not TurnLeft).
|
||||
int cycleKey = (int)((Style << 16) | (TurnRight & 0xFFFFFFu));
|
||||
|
|
@ -611,8 +672,16 @@ public sealed class AnimationSequencerTests
|
|||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
seq.SetCycle(Style, TurnLeft, speedMod: 1f);
|
||||
|
||||
// CurrentMotion should record the original TurnLeft command.
|
||||
Assert.Equal(TurnLeft, seq.CurrentMotion);
|
||||
// R2-Q4 EXPECTED-DIFF (mirrors AnimationSequencerCutoverTraceTests.
|
||||
// S5_WalkBackward_RemapNegativeSpeed's "mirrors only" diff):
|
||||
// CurrentMotion is now a GET-ONLY mirror of MotionState.Substate,
|
||||
// and MotionState owns the POST-adjust_motion substate — retail's
|
||||
// interpreted state IS the adjusted one (TurnRight played reversed),
|
||||
// not the raw TurnLeft the caller passed in. Pre-cutover the adapter
|
||||
// kept its own separate CurrentMotion field and never overwrote it
|
||||
// with the adjusted id; that field no longer exists.
|
||||
Assert.Equal(TurnRight, seq.CurrentMotion);
|
||||
Assert.Equal(-1f, seq.CurrentSpeedMod, 3);
|
||||
|
||||
// R1-P5 (2026-07-02): pre-cutover this pinned the ACE-fabricated
|
||||
// epsilon boundary ((EndFrame+1)-eps ~= 3.99999). Retail's
|
||||
|
|
@ -633,14 +702,17 @@ public sealed class AnimationSequencerTests
|
|||
public void Advance_NegativeSpeed_FramePositionDecreases()
|
||||
{
|
||||
// Verify that a cycle loaded with negative framerate counts downward.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0003u;
|
||||
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
|
||||
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000003u;
|
||||
const uint AnimId = 0x03000060u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(8, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
|
||||
// Register cycle with NEGATIVE framerate to simulate reverse playback.
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
|
|
@ -726,9 +798,11 @@ public sealed class AnimationSequencerTests
|
|||
{
|
||||
// Queue: [linkNode (2 frames, 10fps, non-looping)] → [cycleNode (4 frames, looping)]
|
||||
// Advance enough to exhaust the link node, then verify we're in the cycle.
|
||||
const uint Style = 0x003Du;
|
||||
const uint IdleMotion = 0x0003u;
|
||||
const uint WalkMotion = 0x0005u;
|
||||
// R2-Q4: class-bit-tagged ids (see SetCycle_WithTransitionLink_PrependLinkFrames).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint IdleMotion = 0x40000003u;
|
||||
const uint WalkMotion = 0x40000005u;
|
||||
const uint IdleAnim = 0x03000082u;
|
||||
const uint CycleAnim = 0x03000080u;
|
||||
const uint LinkAnim = 0x03000081u;
|
||||
|
||||
|
|
@ -736,6 +810,7 @@ public sealed class AnimationSequencerTests
|
|||
var linkAnim = Fixtures.MakeAnim(2, 1, new Vector3(0, 5, 0), Quaternion.Identity);
|
||||
// Cycle anim: 4 frames, X=9 (distinct marker).
|
||||
var cycleAnim = Fixtures.MakeAnim(4, 1, new Vector3(9, 0, 0), Quaternion.Identity);
|
||||
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = Fixtures.MakeMtable(
|
||||
|
|
@ -746,13 +821,22 @@ public sealed class AnimationSequencerTests
|
|||
toMotion: WalkMotion,
|
||||
linkAnimId: LinkAnim,
|
||||
framerate: 10f);
|
||||
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
|
||||
// — route it at IdleMotion with its own cycle so the priming
|
||||
// SetCycle call below actually dispatches.
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 10f);
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(IdleAnim, idleAnim);
|
||||
loader.Register(CycleAnim, cycleAnim);
|
||||
loader.Register(LinkAnim, linkAnim);
|
||||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
SetCurrentMotion(seq, Style, IdleMotion);
|
||||
// Prime as if already playing IdleMotion — real SetCycle call
|
||||
// (reflection SetValue no longer works, see WithTransitionLink test).
|
||||
seq.SetCycle(Style, IdleMotion);
|
||||
seq.SetCycle(Style, WalkMotion);
|
||||
|
||||
// Link node is 2 frames at 10fps → 0.2s to exhaust.
|
||||
|
|
@ -923,8 +1007,11 @@ public sealed class AnimationSequencerTests
|
|||
public void Advance_ForwardHookDoesNotFire_OnReversePlayback()
|
||||
{
|
||||
// A hook tagged Direction.Forward should NOT fire when playback is reversed.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0003u;
|
||||
// R2-Q4: class-bit-tagged ids + retail-mandatory StyleDefaults — the bare
|
||||
// ids made this test pass VACUOUSLY (dispatch silently failed, no anim
|
||||
// played, so "hook did not fire" held for the wrong reason).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000003u;
|
||||
const uint AnimId = 0x03000103u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
|
@ -937,6 +1024,7 @@ public sealed class AnimationSequencerTests
|
|||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
var md = new MotionData();
|
||||
QualifiedDataId<Animation> qid = AnimId;
|
||||
|
|
@ -951,7 +1039,10 @@ public sealed class AnimationSequencerTests
|
|||
seq.ConsumePendingHooks();
|
||||
|
||||
// Reverse playback: cursor starts near frame 4 and counts down.
|
||||
seq.Advance(0.15f);
|
||||
// 0.25s at -10fps = -2.5 frames → crosses the 3→2 boundary, so the
|
||||
// hooked frame IS reached (same advance as the Backward sibling test
|
||||
// — the direction filter, not distance, is what's under test).
|
||||
seq.Advance(0.25f);
|
||||
var hooks = seq.ConsumePendingHooks();
|
||||
|
||||
// Forward-only hook on frame 2 should NOT fire on reverse playback.
|
||||
|
|
@ -961,8 +1052,10 @@ public sealed class AnimationSequencerTests
|
|||
[Fact]
|
||||
public void Advance_BackwardHook_FiresOnReversePlayback()
|
||||
{
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0003u;
|
||||
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
|
||||
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000003u;
|
||||
const uint AnimId = 0x03000104u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
|
@ -975,6 +1068,7 @@ public sealed class AnimationSequencerTests
|
|||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
var md = new MotionData();
|
||||
QualifiedDataId<Animation> qid = AnimId;
|
||||
|
|
@ -1045,8 +1139,13 @@ public sealed class AnimationSequencerTests
|
|||
public void CurrentVelocity_ExposedFromMotionData_WhenHasVelocity()
|
||||
{
|
||||
// MotionData with HasVelocity flag should surface via CurrentVelocity.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0003u;
|
||||
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0) —
|
||||
// without it, initialize_state's SetDefaultState fails, state.Style
|
||||
// stays 0, and GetObjectSequence's entry guard rejects every
|
||||
// dispatch. Route the default straight at Motion (the cycle this
|
||||
// test cares about).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000003u;
|
||||
const uint AnimId = 0x03000120u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
|
@ -1054,6 +1153,7 @@ public sealed class AnimationSequencerTests
|
|||
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
var md = new MotionData
|
||||
{
|
||||
|
|
@ -1077,8 +1177,12 @@ public sealed class AnimationSequencerTests
|
|||
[Fact]
|
||||
public void CurrentVelocity_ScaledBySpeedMod()
|
||||
{
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0003u;
|
||||
// R2-Q4: retail-mandatory StyleDefaults, and Motion needs its
|
||||
// 0x40000000 cycle-class bit — the same-motion re-speed fast path
|
||||
// (Branch 2, target==substate) still requires the class-bit gate to
|
||||
// be reached in the first place.
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000003u;
|
||||
const uint AnimId = 0x03000121u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
|
@ -1086,6 +1190,7 @@ public sealed class AnimationSequencerTests
|
|||
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
var md = new MotionData
|
||||
{
|
||||
|
|
@ -1112,27 +1217,37 @@ public sealed class AnimationSequencerTests
|
|||
// When a non-cyclic link node exhausts and we advance_to_next_animation,
|
||||
// an AnimationDoneHook should be queued so consumers can react (e.g. UI
|
||||
// wake-on-idle-complete).
|
||||
const uint Style = 0x003Du;
|
||||
const uint IdleMotion = 0x0003u;
|
||||
const uint WalkMotion = 0x0005u;
|
||||
// R2-Q4: class-bit-tagged ids (see SetCycle_WithTransitionLink_PrependLinkFrames).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint IdleMotion = 0x40000003u;
|
||||
const uint WalkMotion = 0x40000005u;
|
||||
const uint IdleAnim = 0x03000132u;
|
||||
const uint CycleAnim = 0x03000130u;
|
||||
const uint LinkAnim = 0x03000131u;
|
||||
|
||||
var linkAnim = Fixtures.MakeAnim(2, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var cycleAnim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = Fixtures.MakeMtable(
|
||||
style: Style, motion: WalkMotion, cycleAnimId: CycleAnim,
|
||||
fromMotion: IdleMotion, toMotion: WalkMotion, linkAnimId: LinkAnim,
|
||||
framerate: 10f);
|
||||
// R2-Q4: retail-mandatory StyleDefaults — route it at IdleMotion with
|
||||
// its own cycle so the priming SetCycle call below actually dispatches.
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 10f);
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(IdleAnim, idleAnim);
|
||||
loader.Register(CycleAnim, cycleAnim);
|
||||
loader.Register(LinkAnim, linkAnim);
|
||||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
SetCurrentMotion(seq, Style, IdleMotion);
|
||||
// Prime as if already playing IdleMotion — real SetCycle call.
|
||||
seq.SetCycle(Style, IdleMotion);
|
||||
seq.SetCycle(Style, WalkMotion);
|
||||
seq.ConsumePendingHooks();
|
||||
|
||||
|
|
@ -1150,8 +1265,11 @@ public sealed class AnimationSequencerTests
|
|||
{
|
||||
// A 10-frame cycle at 10 fps = 1.0s per loop. If we halve the playback
|
||||
// rate (factor 0.5), advancing 1.0s should produce half a loop (5 frames).
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0007u; // RunForward
|
||||
// R2-Q4: Motion needs the 0x40000000 cycle-class bit — GetObjectSequence
|
||||
// Branch 2 (and its same-motion fast re-speed path) never triggers on
|
||||
// a bare low-word id (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000007u; // RunForward
|
||||
const uint AnimId = 0x03000401u;
|
||||
|
||||
// Unique per-frame Z so we can tell where the cursor lands.
|
||||
|
|
@ -1177,6 +1295,11 @@ public sealed class AnimationSequencerTests
|
|||
Framerate = 10f,
|
||||
});
|
||||
mt.Cycles[cycleKey] = md;
|
||||
// R2-Q4: the dispatch stack needs the retail-mandatory StyleDefaults
|
||||
// entry (SetDefaultState 0x005230a0 requires StyleDefaults[DefaultStyle];
|
||||
// GetObjectSequence refuses a zero style/substate). Real dat tables
|
||||
// always carry it.
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(AnimId, anim);
|
||||
|
|
@ -1184,8 +1307,11 @@ public sealed class AnimationSequencerTests
|
|||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
seq.SetCycle(Style, Motion, speedMod: 1f);
|
||||
|
||||
// Halve the playback rate.
|
||||
seq.MultiplyCyclicFramerate(0.5f);
|
||||
// R2-Q4: halve the playback rate via the retail same-motion re-speed
|
||||
// (GetObjectSequence Branch-2 fast path: change_cycle_speed +
|
||||
// subtract_motion(old) + combine_motion(new), decomp §5) — the old
|
||||
// MultiplyCyclicFramerate adapter composite is deleted.
|
||||
seq.SetCycle(Style, Motion, speedMod: 0.5f);
|
||||
|
||||
// 10 frames at 5 fps = 2.0s per loop. Advance 1.0s → cursor ~= frame 5.
|
||||
seq.Advance(1.0f);
|
||||
|
|
@ -1193,7 +1319,8 @@ public sealed class AnimationSequencerTests
|
|||
Assert.Single(frames);
|
||||
Assert.InRange(frames[0].Origin.Z, 4f, 6f);
|
||||
|
||||
// Velocity also scales: originally (0,4,0), now (0,2,0).
|
||||
// Velocity also scales: originally (0,4,0), now (0,2,0)
|
||||
// (subtract_motion(1.0) + combine_motion(0.5) = ×0.5 net).
|
||||
Assert.Equal(2f, seq.CurrentVelocity.Y, 1);
|
||||
}
|
||||
|
||||
|
|
@ -1202,8 +1329,11 @@ public sealed class AnimationSequencerTests
|
|||
{
|
||||
// Changing speed mid-cycle must NOT reset the frame cursor — the
|
||||
// animation keeps playing from where it was, just faster/slower.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0007u;
|
||||
// R2-Q4: class-bit-tagged ids — the bare ids made this test pass
|
||||
// VACUOUSLY (dispatch silently failed; "cursor unchanged" held
|
||||
// because nothing moved at all).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000007u;
|
||||
const uint AnimId = 0x03000402u;
|
||||
|
||||
var anim = new Animation();
|
||||
|
|
@ -1219,6 +1349,9 @@ public sealed class AnimationSequencerTests
|
|||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(AnimId, framerate: 10f);
|
||||
// R2-Q4: retail-mandatory StyleDefaults entry (see
|
||||
// MultiplyCyclicFramerate_HalvesPlaybackRate).
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(AnimId, anim);
|
||||
|
|
@ -1228,7 +1361,9 @@ public sealed class AnimationSequencerTests
|
|||
seq.Advance(0.3f); // cursor ~ frame 3
|
||||
double before = GetFramePosition(seq);
|
||||
|
||||
seq.MultiplyCyclicFramerate(2.0f);
|
||||
// R2-Q4: mid-cycle re-speed via the retail Branch-2 fast path — must
|
||||
// not touch the cursor (change_cycle_speed scales framerates only).
|
||||
seq.SetCycle(Style, Motion, speedMod: 2.0f);
|
||||
double after = GetFramePosition(seq);
|
||||
|
||||
Assert.Equal(before, after, 5);
|
||||
|
|
@ -1241,8 +1376,10 @@ public sealed class AnimationSequencerTests
|
|||
// NOT reset the cursor — it should call MultiplyCyclicFramerate to
|
||||
// keep the run loop smooth (retail behavior for a mid-run RunRate
|
||||
// broadcast). Mirror of ACE MotionTable.cs:132-139 fast-path.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0007u;
|
||||
// R2-Q4: Motion needs the 0x40000000 cycle-class bit — see
|
||||
// CurrentVelocity_ExposedFromMotionData_WhenHasVelocity.
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000007u;
|
||||
const uint AnimId = 0x03000403u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(10, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
|
@ -1273,14 +1410,17 @@ public sealed class AnimationSequencerTests
|
|||
// surface as (0,4,0) at speedMod=1.0, (0,6,0) at 1.5×, (0,2,0) at
|
||||
// 0.5×. The dead-reckoning integrator in TickAnimations reads
|
||||
// CurrentVelocity each tick, so this has to be accurate.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0007u;
|
||||
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
|
||||
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000007u;
|
||||
const uint AnimId = 0x03000405u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
|
||||
var md = new MotionData { Flags = MotionDataFlags.HasVelocity, Velocity = new Vector3(0, 4, 0) };
|
||||
|
|
@ -1316,8 +1456,10 @@ public sealed class AnimationSequencerTests
|
|||
{
|
||||
// Guard: the new speed-path must not break the classic
|
||||
// "identical call = no state change" behavior.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x0007u;
|
||||
// R2-Q4: Motion needs the 0x40000000 cycle-class bit — see
|
||||
// CurrentVelocity_ExposedFromMotionData_WhenHasVelocity.
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000007u;
|
||||
const uint AnimId = 0x03000404u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(10, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
|
@ -1344,14 +1486,17 @@ public sealed class AnimationSequencerTests
|
|||
// A turn cycle with MotionData.Omega = (0, 0, 1) rad/sec (yaw)
|
||||
// should surface as CurrentOmega = (0, 0, 1) after SetCycle.
|
||||
// Scales with speedMod exactly like Velocity.
|
||||
const uint Style = 0x003Du;
|
||||
const uint Motion = 0x000Du; // TurnRight
|
||||
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
|
||||
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x4000000Du; // TurnRight
|
||||
const uint AnimId = 0x03000701u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)Motion;
|
||||
int cycleKey = (int)((Style << 16) | (Motion & 0xFFFFFFu));
|
||||
|
||||
var md = new MotionData { Flags = MotionDataFlags.HasOmega, Omega = new Vector3(0, 0, 1.0f) };
|
||||
|
|
@ -1380,19 +1525,27 @@ public sealed class AnimationSequencerTests
|
|||
// reads the cycle's run-speed and moves the entity smoothly.
|
||||
// Crucial: otherwise remote entities would stutter at every stance
|
||||
// transition while the link plays.
|
||||
const uint Style = 0x003Du;
|
||||
const uint IdleMotion = 0x0003u;
|
||||
const uint WalkMotion = 0x0005u;
|
||||
// R2-Q4: class-bit-tagged ids (see SetCycle_WithTransitionLink_PrependLinkFrames).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint IdleMotion = 0x40000003u;
|
||||
const uint WalkMotion = 0x40000005u;
|
||||
const uint IdleAnim = 0x03000603u;
|
||||
const uint CycleAnim = 0x03000601u;
|
||||
const uint LinkAnim = 0x03000602u;
|
||||
|
||||
var cycleAnim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var linkAnim = Fixtures.MakeAnim(2, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var idleAnim = Fixtures.MakeAnim(1, 1, Vector3.Zero, Quaternion.Identity);
|
||||
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)WalkMotion;
|
||||
// R2-Q4: retail-mandatory StyleDefaults (SetDefaultState 0x005230a0)
|
||||
// — route it at IdleMotion (the state we prime through below).
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
|
||||
int idleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[idleKey] = Fixtures.MakeMotionData(IdleAnim, framerate: 10f);
|
||||
|
||||
int cycleKey = (int)((Style << 16) | (WalkMotion & 0xFFFFFFu));
|
||||
var cycleMd = new MotionData { Flags = MotionDataFlags.HasVelocity, Velocity = new Vector3(0, 3.12f, 0) };
|
||||
|
|
@ -1410,11 +1563,13 @@ public sealed class AnimationSequencerTests
|
|||
mt.Links[linkOuter] = linkCmdData;
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(IdleAnim, idleAnim);
|
||||
loader.Register(CycleAnim, cycleAnim);
|
||||
loader.Register(LinkAnim, linkAnim);
|
||||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
SetCurrentMotion(seq, Style, IdleMotion);
|
||||
// Prime as if already playing IdleMotion — real SetCycle call.
|
||||
seq.SetCycle(Style, IdleMotion);
|
||||
seq.SetCycle(Style, WalkMotion);
|
||||
|
||||
// We just enqueued [link(0)][cycle(3.12 forward)]. Current node is
|
||||
|
|
@ -1438,7 +1593,11 @@ public sealed class AnimationSequencerTests
|
|||
// An Action-class command (mask 0x10) resolves via the Links dict
|
||||
// keyed by (style, currentSubstate) → motion. Example: a ThrustMed
|
||||
// attack while in SwordCombat stance.
|
||||
const uint Style = 0x003Eu; // SwordCombat
|
||||
// R2-Q4: Style needs the 0x80000000 top bit + a StyleDefaults entry
|
||||
// (SetDefaultState 0x005230a0 is retail-mandatory — GetObjectSequence
|
||||
// refuses style==0/substate==0, see
|
||||
// CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
|
||||
const uint Style = 0x8000003Eu; // SwordCombat
|
||||
const uint IdleMotion = 0x41000003u; // Ready
|
||||
const uint ActionMotion = 0x10000058u; // ThrustMed (Action class)
|
||||
const uint IdleAnimId = 0x03000501u;
|
||||
|
|
@ -1451,6 +1610,7 @@ public sealed class AnimationSequencerTests
|
|||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);
|
||||
|
||||
|
|
@ -1484,7 +1644,9 @@ public sealed class AnimationSequencerTests
|
|||
// values followed by Ready. Retail keeps currState.Substate at Ready
|
||||
// while the action link drains, so the Ready echo must not abort the
|
||||
// in-flight swing.
|
||||
const uint Style = 0x003Du;
|
||||
// R2-Q4: Style needs the 0x80000000 top bit + a StyleDefaults entry
|
||||
// (see PlayAction_Action_ResolvesFromLinksDict).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint IdleMotion = 0x41000003u;
|
||||
const uint AttackMotion = 0x10000052u;
|
||||
const uint IdleAnimId = 0x03000503u;
|
||||
|
|
@ -1492,6 +1654,7 @@ public sealed class AnimationSequencerTests
|
|||
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable { DefaultStyle = (DRWMotionCommand)Style };
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);
|
||||
|
||||
|
|
@ -1519,11 +1682,19 @@ public sealed class AnimationSequencerTests
|
|||
[Fact]
|
||||
public void PlayAction_Modifier_ResolvesFromModifiersDict()
|
||||
{
|
||||
// A Modifier-class command (mask 0x20) — like Jump (0x2500003B) —
|
||||
// resolves from the Modifiers dict, first with style-specific key
|
||||
// then with unstyled fallback. Empirically: the modifier's anim
|
||||
// plays on top of the current cycle.
|
||||
const uint Style = 0x003Du;
|
||||
// R2-Q4 EXPECTED-DIFF (mirrors AnimationSequencerCutoverTraceTests.
|
||||
// S9_TurnModifier): a Modifier-class command (mask 0x20) — like Jump
|
||||
// (0x2500003B) or a turn-while-moving overlay — resolves from the
|
||||
// Modifiers dict, first with style-specific key then with unstyled
|
||||
// fallback (CMotionTable.GetObjectSequence Branch 4). Pre-cutover the
|
||||
// adapter INSERTED the modifier's anim before the cyclic tail — an
|
||||
// acdream invention. Retail Branch 4 is PHYSICS-ONLY combine_motion:
|
||||
// the base cycle's anim list is untouched (no new nodes), and the
|
||||
// modifier contributes velocity/omega on top of the cycle's own,
|
||||
// tracked on the MotionState modifier stack (AP-73 mechanism).
|
||||
// R2-Q4: Style needs the 0x80000000 top bit + a StyleDefaults entry
|
||||
// (see PlayAction_Action_ResolvesFromLinksDict).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint IdleMotion = 0x41000003u;
|
||||
const uint JumpMotion = 0x2500003Bu; // Modifier class
|
||||
const uint IdleAnimId = 0x03000510u;
|
||||
|
|
@ -1535,12 +1706,18 @@ public sealed class AnimationSequencerTests
|
|||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);
|
||||
|
||||
// Modifier: (Style, Jump)
|
||||
// Modifier: (Style, Jump) — carries an omega contribution (a jump
|
||||
// kick's angular nudge) rather than an anim payload, since Branch 4
|
||||
// never touches the anim list.
|
||||
int modKey = (int)((Style << 16) | (JumpMotion & 0xFFFFFFu));
|
||||
mt.Modifiers[modKey] = Fixtures.MakeMotionData(JumpAnimId, framerate: 10f);
|
||||
var jumpMd = Fixtures.MakeMotionData(JumpAnimId, framerate: 10f);
|
||||
jumpMd.Flags = MotionDataFlags.HasOmega;
|
||||
jumpMd.Omega = new Vector3(0f, 0f, 2.5f);
|
||||
mt.Modifiers[modKey] = jumpMd;
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(IdleAnimId, idleAnim);
|
||||
|
|
@ -1548,12 +1725,15 @@ public sealed class AnimationSequencerTests
|
|||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
seq.SetCycle(Style, IdleMotion);
|
||||
int queueBefore = seq.QueueCount;
|
||||
|
||||
seq.PlayAction(JumpMotion);
|
||||
|
||||
var fr = seq.Advance(0.01f);
|
||||
Assert.Single(fr);
|
||||
Assert.Equal(77f, fr[0].Origin.Z, 1);
|
||||
// No anim nodes inserted — the queue is unchanged from before the
|
||||
// modifier fired.
|
||||
Assert.Equal(queueBefore, seq.QueueCount);
|
||||
// The modifier's omega is combined onto the sequence's physics.
|
||||
Assert.Equal(2.5f, seq.CurrentOmega.Z, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1563,7 +1743,9 @@ public sealed class AnimationSequencerTests
|
|||
// Action(0x10) | ChatEmote(0x02) | Mappable(0x01). Because the
|
||||
// Action bit is set, they route through the Links-dict lookup just
|
||||
// like attacks. Verifies the class-bit math.
|
||||
const uint Style = 0x003Du;
|
||||
// R2-Q4: Style needs the 0x80000000 top bit + a StyleDefaults entry
|
||||
// (see PlayAction_Action_ResolvesFromLinksDict).
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint IdleMotion = 0x41000003u;
|
||||
const uint WaveMotion = 0x13000087u;
|
||||
const uint IdleAnimId = 0x03000520u;
|
||||
|
|
@ -1575,6 +1757,7 @@ public sealed class AnimationSequencerTests
|
|||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable();
|
||||
mt.DefaultStyle = (DRWMotionCommand)Style;
|
||||
mt.StyleDefaults[(DRWMotionCommand)Style] = (DRWMotionCommand)IdleMotion;
|
||||
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue