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()); } /// /// 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. /// 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 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 _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; } }