Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates. Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean. Co-authored-by: OpenAI Codex <codex@openai.com>
159 lines
5.4 KiB
C#
159 lines
5.4 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
|
|
using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand;
|
|
|
|
namespace AcDream.Core.Tests.Physics.Motion;
|
|
|
|
/// <summary>
|
|
/// R2-Q5 — <see cref="MotionTableDispatchSink"/>: the funnel's dispatches go
|
|
/// straight into <see cref="AnimationSequencer.PerformMovement"/> (no axis
|
|
/// collection, no priority pick, no fallback chain — GetObjectSequence
|
|
/// 0x00522860 + is_allowed decide).
|
|
/// </summary>
|
|
public class MotionTableDispatchSinkTests
|
|
{
|
|
private const uint NC = 0x8000003Du;
|
|
private const uint Ready = 0x41000003u;
|
|
private const uint Walk = 0x45000005u;
|
|
private const uint TurnRight = 0x6500000Du;
|
|
|
|
private const uint ReadyAnim = 0x200u;
|
|
private const uint WalkAnim = 0x201u;
|
|
|
|
private sealed class Loader : 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;
|
|
}
|
|
|
|
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, Vector3? omega = null)
|
|
{
|
|
var md = new MotionData();
|
|
QualifiedDataId<Animation> qid = animId;
|
|
md.Anims.Add(new AnimData { AnimId = qid, LowFrame = 0, HighFrame = -1, Framerate = 30f });
|
|
if (omega is { } o)
|
|
{
|
|
md.Omega = o;
|
|
md.Flags |= DatReaderWriter.Enums.MotionDataFlags.HasOmega;
|
|
}
|
|
return md;
|
|
}
|
|
|
|
private static AnimationSequencer MakeSequencer(bool withTurnModifier = true)
|
|
{
|
|
var setup = new Setup();
|
|
setup.Parts.Add(0x01000000u);
|
|
setup.DefaultScale.Add(Vector3.One);
|
|
|
|
var loader = new Loader();
|
|
loader.Register(ReadyAnim, MakeAnim(4));
|
|
loader.Register(WalkAnim, MakeAnim(6));
|
|
|
|
var mt = new MotionTable { DefaultStyle = (DRWMotionCommand)NC };
|
|
mt.StyleDefaults[(DRWMotionCommand)NC] = (DRWMotionCommand)Ready;
|
|
mt.Cycles[(int)((NC << 16) | (Ready & 0xFFFFFFu))] = MakeMd(ReadyAnim);
|
|
mt.Cycles[(int)((NC << 16) | (Walk & 0xFFFFFFu))] = MakeMd(WalkAnim);
|
|
if (withTurnModifier)
|
|
{
|
|
mt.Modifiers[(int)((NC << 16) | (TurnRight & 0xFFFFFFu))] =
|
|
MakeMd(ReadyAnim, omega: new Vector3(0f, 0f, 1.2f));
|
|
}
|
|
|
|
return new AnimationSequencer(setup, mt, loader);
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyMotion_CycleClass_InstallsSubstate_LazyInitialized()
|
|
{
|
|
var seq = MakeSequencer();
|
|
var sink = new MotionTableDispatchSink(seq);
|
|
|
|
// Fresh sequencer: the first dispatch lazily runs initialize_state
|
|
// (retail lazy-create, r3-motioninterp-decomp §6g) then installs
|
|
// the requested substate.
|
|
sink.ApplyMotion(Walk, 2.0f);
|
|
|
|
Assert.Equal(Walk, seq.CurrentMotion);
|
|
Assert.Equal(2.0f, seq.CurrentSpeedMod);
|
|
// add_motion preserves the literal zero MotionData velocity; body
|
|
// speed belongs to CMotionInterp, not CSequence.
|
|
Assert.Equal(Vector3.Zero, seq.CurrentVelocity);
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyMotion_Turn_Branch4Modifier_PreservesCycleAndAppliesDatOmega()
|
|
{
|
|
var seq = MakeSequencer();
|
|
var sink = new MotionTableDispatchSink(seq);
|
|
|
|
sink.ApplyMotion(Walk, 1.0f);
|
|
sink.ApplyMotion(TurnRight, 1.5f);
|
|
|
|
// The AP-73 mechanism for real: substate cycle untouched, the turn
|
|
// is a MotionState modifier with its dat omega combined.
|
|
Assert.Equal(Walk, seq.CurrentMotion);
|
|
Assert.Equal(1.2f * 1.5f, seq.CurrentOmega.Z, 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void StopMotion_Turn_UnwindsModifierPhysics()
|
|
{
|
|
var seq = MakeSequencer();
|
|
var sink = new MotionTableDispatchSink(seq);
|
|
|
|
sink.ApplyMotion(Walk, 1.0f);
|
|
sink.ApplyMotion(TurnRight, 1.5f);
|
|
sink.StopMotion(TurnRight);
|
|
|
|
// StopSequenceMotion Case B (0x00522fc0): subtract_motion of the dat
|
|
// omega + modifier unlinked.
|
|
Assert.Equal(0f, seq.CurrentOmega.Z, 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void StopMotion_CurrentSubstate_ReDrivesToStyleDefault()
|
|
{
|
|
var seq = MakeSequencer();
|
|
var sink = new MotionTableDispatchSink(seq);
|
|
|
|
sink.ApplyMotion(Walk, 1.0f);
|
|
sink.StopMotion(Walk);
|
|
|
|
// StopSequenceMotion Case A: stopping the active cycle re-drives
|
|
// GetObjectSequence toward the style default (Ready).
|
|
Assert.Equal(Ready, seq.CurrentMotion);
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyMotion_MissingEverywhere_NoOp_DefaultKeepsPlaying()
|
|
{
|
|
var seq = MakeSequencer(withTurnModifier: false);
|
|
var sink = new MotionTableDispatchSink(seq);
|
|
|
|
sink.ApplyMotion(Walk, 1.0f);
|
|
// 0x44000007 RunForward: no cycle, no modifier -> dispatch fails,
|
|
// sequence + state untouched (no fallback chain — H4/H5).
|
|
sink.ApplyMotion(0x44000007u, 2.0f);
|
|
|
|
Assert.Equal(Walk, seq.CurrentMotion);
|
|
Assert.Equal(1.0f, seq.CurrentSpeedMod);
|
|
}
|
|
}
|