using System; using System.Numerics; using AcDream.App.Input; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand; namespace AcDream.Core.Tests.Input; /// /// R3-W6 regression suite for the edge-driven local player — specifically /// the "press W and stop instantly" bug (2026-07-03): the funnel's apply /// pass live-read InterpretedState.ForwardCommand AFTER the style /// dispatch, whose ApplyMotion(style) state-write resets forward to /// Ready unconditionally (raw 0051ea6c, verbatim). Retail self-heals via /// register-cached entry reads (proven by the 183-case live observer trace: /// the fwd dispatch carries the pre-reset command); the fix entry-caches /// the axes in ApplyInterpretedMovement. /// /// The 183-case suite could NOT catch this: its RecordingSink's ApplyMotion /// return doesn't mirror the REAL , /// which returns TRUE for the style dispatch (manager Branch 1 /// style==target → success) — the true return is what gates the resetting /// state-write on. These tests bind the REAL sink over a real sequencer. /// public class W6EdgeDrivenMovementTests { private const uint NC = 0x8000003Du; private const uint Ready = 0x41000003u; private const uint Walk = 0x45000005u; private const uint Run = 0x44000007u; private sealed class Loader : 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; } private static Animation MakeAnim(int frames) { var anim = new Animation(); for (int f = 0; f < frames; f++) { var pf = new AnimationFrame(1); pf.Frames.Add(new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }); anim.PartFrames.Add(pf); } return anim; } private static MotionData MakeMd(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 AnimationSequencer MakeSequencer() { var setup = new Setup(); setup.Parts.Add(0x01000000u); setup.DefaultScale.Add(Vector3.One); var loader = new Loader(); loader.Register(0x300u, MakeAnim(4)); loader.Register(0x301u, MakeAnim(6)); loader.Register(0x302u, MakeAnim(6)); var mt = new MotionTable { DefaultStyle = (DRWMotionCommand)NC }; mt.StyleDefaults[(DRWMotionCommand)NC] = (DRWMotionCommand)Ready; mt.Cycles[(int)((NC << 16) | (Ready & 0xFFFFFFu))] = MakeMd(0x300u); mt.Cycles[(int)((NC << 16) | (Walk & 0xFFFFFFu))] = MakeMd(0x301u); mt.Cycles[(int)((NC << 16) | (Run & 0xFFFFFFu))] = MakeMd(0x302u); return new AnimationSequencer(setup, mt, loader); } private static PhysicsEngine MakeFlatEngine() { var engine = new PhysicsEngine(); var heights = new byte[81]; Array.Fill(heights, (byte)50); var heightTable = new float[256]; for (int i = 0; i < 256; i++) heightTable[i] = i * 1f; var terrain = new TerrainSurface(heights, heightTable); engine.AddLandblock(0xA9B4FFFFu, terrain, Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); return engine; } private static PlayerMovementController MakeControllerWithRealSink(out AnimationSequencer seq) { var controller = new PlayerMovementController(MakeFlatEngine()); controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001); controller.Yaw = 0f; seq = MakeSequencer(); // The full W6 GameWindow bind set (EnterPlayerModeNow equivalent). controller.Motion.DefaultSink = new MotionTableDispatchSink(seq); var s = seq; controller.Motion.RemoveLinkAnimations = s.RemoveAllLinkAnimations; controller.Motion.InitializeMotionTables = () => s.Manager.InitializeState(); controller.Motion.CheckForCompletedMotions = s.Manager.CheckForCompletedMotions; return controller; } /// /// The full apply pass the W6b live bug rode in on. Pre-R4-V5 the /// trigger was ApplyServerRunRate (the ACE autonomous-echo tap); /// V5's P1 gate drops that echo before it reaches the player, and the /// tap is deleted — but the regression these tests pin lives in /// ApplyInterpretedMovement's entry-caching, which any /// apply_current_movement pass (HitGround re-apply, future R6 per-tick /// order) still exercises. Same two statements the deleted tap ran. /// private static void RunApplyPass(PlayerMovementController controller, float forwardSpeed) { controller.Motion.InterpretedState.ForwardSpeed = forwardSpeed; controller.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false); } [Fact] public void ApplyPass_WithRealSink_ForwardSelfHeals() { // The distilled bug: a full apply pass (style dispatch included) // against the REAL sink must leave the interpreted forward exactly // where it started — the style apply's Ready reset is re-applied // over by the entry-cached fwd dispatch (retail self-heal). var controller = MakeControllerWithRealSink(out _); var input = new MovementInput { Forward = true, Run = true }; controller.Update(1f / 60f, input); // W press edge -> RunForward Assert.Equal(Run, controller.Motion.InterpretedState.ForwardCommand); // The live killer: a full apply pass mid-hold (was the ~10Hz // UM-echo tap pre-V5; see RunApplyPass). RunApplyPass(controller, 4.5f); Assert.Equal(Run, controller.Motion.InterpretedState.ForwardCommand); Assert.True(controller.Motion.get_state_velocity().Length() > 1f, "state velocity must survive the apply pass"); } [Fact] public void HoldW_WithRealSink_AndEchoes_BodyKeepsMoving() { var controller = MakeControllerWithRealSink(out _); var input = new MovementInput { Forward = true, Run = true }; float startX = 96f; Vector3 pos = new(startX, 96f, 50f); for (int f = 0; f < 120; f++) { if (f % 6 == 3) RunApplyPass(controller, 4.5f); // ex-ACE-echo cadence pos = controller.Update(1f / 60f, input).Position; } // 2 seconds of held-W running (post-fix ~9.5 m/s) must cover // meters, not centimeters. Pre-fix this stalled at ~one tick of // travel (the echo pass reset forward to Ready). Assert.True(pos.X - startX > 5f, $"expected sustained forward motion, got {pos.X - startX:F2} m"); } [Fact] public void ShiftToggle_MidHold_WalkRunTransitionSurvivesEcho() { var controller = MakeControllerWithRealSink(out _); // Hold W at run for 30 frames. for (int f = 0; f < 30; f++) controller.Update(1f / 60f, new MovementInput { Forward = true, Run = true }); Assert.Equal(Run, controller.Motion.InterpretedState.ForwardCommand); // Shift pressed (walk) — the set_hold_run edge demotes to walk. for (int f = 0; f < 30; f++) { if (f == 10) RunApplyPass(controller, 1.0f); // apply pass mid-walk controller.Update(1f / 60f, new MovementInput { Forward = true, Run = false }); } Assert.Equal(Walk, controller.Motion.InterpretedState.ForwardCommand); // Shift released — promote back to run; survives another echo. for (int f = 0; f < 30; f++) { if (f == 10) RunApplyPass(controller, 4.5f); controller.Update(1f / 60f, new MovementInput { Forward = true, Run = true }); } Assert.Equal(Run, controller.Motion.InterpretedState.ForwardCommand); } }