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>
131 lines
4.8 KiB
C#
131 lines
4.8 KiB
C#
using AcDream.Core.Physics;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
public sealed class AnimationCommandRouterTests
|
|
{
|
|
private const uint NonCombat = 0x8000003Du;
|
|
|
|
[Theory]
|
|
[InlineData(0x00000000u, AnimationCommandRouteKind.None)]
|
|
[InlineData(0x10000057u, AnimationCommandRouteKind.Action)] // Sanctuary
|
|
[InlineData(0x2500003Bu, AnimationCommandRouteKind.Modifier)] // Jump
|
|
[InlineData(0x13000087u, AnimationCommandRouteKind.ChatEmote)] // Wave
|
|
[InlineData(0x41000003u, AnimationCommandRouteKind.SubState)] // Ready
|
|
[InlineData(0x40000011u, AnimationCommandRouteKind.SubState)] // Dead
|
|
[InlineData(0x8000003Du, AnimationCommandRouteKind.Ignored)] // NonCombat style
|
|
public void Classify_ReturnsRetailRouteKind(uint command, AnimationCommandRouteKind expected)
|
|
{
|
|
Assert.Equal(expected, AnimationCommandRouter.Classify(command));
|
|
}
|
|
|
|
[Fact]
|
|
public void RouteWireCommand_SubState_UsesSetCycle()
|
|
{
|
|
// 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);
|
|
|
|
Assert.Equal(AnimationCommandRouteKind.SubState, route);
|
|
Assert.Equal(NonCombat, seq.CurrentStyle);
|
|
Assert.Equal(MotionCommand.Dead, seq.CurrentMotion);
|
|
}
|
|
|
|
[Fact]
|
|
public void RouteWireCommand_Sanctuary_IsActionNotDeadCycle()
|
|
{
|
|
var seq = MakeEmptySequencer();
|
|
|
|
var route = AnimationCommandRouter.RouteWireCommand(seq, NonCombat, 0x0057);
|
|
|
|
Assert.Equal(AnimationCommandRouteKind.Action, route);
|
|
Assert.Equal(0u, seq.CurrentMotion);
|
|
}
|
|
|
|
[Fact]
|
|
public void RouteWireCommand_Wave_IsChatEmote()
|
|
{
|
|
var seq = MakeEmptySequencer();
|
|
|
|
var route = AnimationCommandRouter.RouteWireCommand(seq, NonCombat, 0x0087);
|
|
|
|
Assert.Equal(AnimationCommandRouteKind.ChatEmote, route);
|
|
}
|
|
|
|
private static AnimationSequencer MakeEmptySequencer()
|
|
{
|
|
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
|
|
{
|
|
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;
|
|
}
|
|
}
|