diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index e9168ba..283497f 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -21,12 +21,6 @@ public sealed class GameWindow : IDisposable
private CameraController? _cameraController;
private IMouse? _capturedMouse;
private DatCollection? _dats;
- ///
- /// Adapter so instances
- /// can load Animation dats without knowing about DatCollection directly.
- /// Initialized alongside once the dat files are opened.
- ///
- private AcDream.Core.Physics.DatCollectionLoader? _animLoader;
private float _lastMouseX;
private float _lastMouseY;
private StaticMeshRenderer? _staticMesh;
@@ -97,16 +91,6 @@ public sealed class GameWindow : IDisposable
///
public required IReadOnlyList<(uint GfxObjId, IReadOnlyDictionary? SurfaceOverrides)> PartTemplate;
public float CurrFrame; // monotonically increasing within [LowFrame, HighFrame]
-
- ///
- /// Phase B.3: full animation sequencer with transition-link support.
- /// Non-null when a MotionTable was successfully loaded at spawn time.
- /// When present, delegates to this instead
- /// of performing the manual slerp, and motion changes call
- /// so
- /// transition frames blend smoothly.
- ///
- public AcDream.Core.Physics.AnimationSequencer? Sequencer;
}
// Phase B.2: player movement mode.
@@ -339,7 +323,6 @@ public sealed class GameWindow : IDisposable
_cameraController.ModeChanged += OnCameraModeChanged;
_dats = new DatCollection(_datDir, DatAccessType.Read);
- _animLoader = new AcDream.Core.Physics.DatCollectionLoader(_dats);
uint centerLandblockId = 0xA9B4FFFFu;
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
@@ -839,29 +822,6 @@ public sealed class GameWindow : IDisposable
for (int i = 0; i < meshRefs.Count; i++)
template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);
- // Phase B.3: try to build an AnimationSequencer for smooth
- // transition-link blending. Requires a MotionTable in the dats.
- AcDream.Core.Physics.AnimationSequencer? sequencer = null;
- if (_animLoader is not null)
- {
- uint mtId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable;
- var mtable = mtId != 0 ? _dats!.Get(mtId) : null;
- if (mtable is not null)
- {
- sequencer = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);
- // Prime the sequencer on the idle cycle so it starts playing
- // immediately. Use the resolved style from the spawn motion state,
- // defaulting to NonCombat (0x003D).
- uint spawnStyle = stanceOverride.HasValue
- ? ((uint)stanceOverride.Value << 16)
- : ((uint)mtable.DefaultStyle & 0xFFFF0000u);
- uint spawnMotion = commandOverride.HasValue
- ? (uint)commandOverride.Value
- : (uint)mtable.StyleDefaults.GetValueOrDefault(mtable.DefaultStyle);
- sequencer.SetCycle(spawnStyle, spawnMotion);
- }
- }
-
_animatedEntities[entity.Id] = new AnimatedEntity
{
Entity = entity,
@@ -873,7 +833,6 @@ public sealed class GameWindow : IDisposable
Scale = scale,
PartTemplate = template,
CurrFrame = idleCycle.LowFrame,
- Sequencer = sequencer,
};
}
@@ -961,18 +920,6 @@ public sealed class GameWindow : IDisposable
if (!newCycleIsGood)
return;
- // Phase B.3: if this entity has a sequencer, delegate the cycle switch
- // to it so transition-link frames are prepended automatically. The
- // sequencer's SetCycle fast-path will no-op if nothing actually changed.
- if (ae.Sequencer is not null)
- {
- uint styleCmd = (uint)stance << 16;
- uint motionCmd = command.HasValue ? (uint)command.Value : 0u;
- ae.Sequencer.SetCycle(styleCmd, motionCmd);
- // Keep ae.Animation/LowFrame/HighFrame in sync so the fallback
- // path (and diagnostics) are accurate if the sequencer is ever removed.
- }
-
ae.Animation = newCycle!.Animation;
ae.LowFrame = Math.Max(0, newCycle.LowFrame);
ae.HighFrame = Math.Min(newCycle.HighFrame, newCycle.Animation.PartFrames.Count - 1);
@@ -1878,154 +1825,104 @@ public sealed class GameWindow : IDisposable
}
///
- /// Phase 6.4 / B.3: advance every animated entity's playback by
- /// seconds, then rebuild that entity's MeshRefs
- /// from the resulting per-part transforms. Static entities (no
- /// AnimatedEntity record) are untouched.
- ///
- /// When the entity has a
- /// (Phase B.3), delegates frame advancement and blending to it — this gives
- /// retail-accurate quaternion slerp and smooth transition-link blending on
- /// motion changes. Falls back to the Phase 6.5 manual slerp for entities
- /// whose MotionTable couldn't be loaded (e.g. offline mode or a missing dat).
+ /// Phase 6.4: advance every animated entity's frame counter by
+ /// * Framerate, wrapping around the cycle's
+ /// [LowFrame..HighFrame] interval, then rebuild that entity's
+ /// MeshRefs from the new frame's per-part transforms. Static
+ /// entities (no AnimatedEntity record) are untouched. The static
+ /// renderer reads the new MeshRefs on the next Draw call.
///
private void TickAnimations(float dt)
{
foreach (var kv in _animatedEntities)
{
var ae = kv.Value;
+ int span = ae.HighFrame - ae.LowFrame;
+ if (span <= 0) continue;
- // ── Phase B.3 path: sequencer-driven ────────────────────────────
- if (ae.Sequencer is { } seq)
+ ae.CurrFrame += dt * ae.Framerate;
+ // Wrap into [LowFrame, HighFrame]. Use a guarded modulo so
+ // big dts (first frame after a stall) don't blow the loop.
+ if (ae.CurrFrame > ae.HighFrame)
{
- var transforms = seq.Advance(dt);
- int partCount = ae.PartTemplate.Count;
- var newMeshRefs = new List(partCount);
- var scaleMat = ae.Scale == 1.0f
- ? System.Numerics.Matrix4x4.Identity
- : System.Numerics.Matrix4x4.CreateScale(ae.Scale);
-
- for (int i = 0; i < partCount; i++)
- {
- var pt = i < transforms.Count ? transforms[i]
- : new AcDream.Core.Physics.PartTransform(
- System.Numerics.Vector3.Zero,
- System.Numerics.Quaternion.Identity);
-
- // Per-part default scale from the Setup, matching
- // SetupMesh.Flatten's composition order: scale → rotate → translate.
- var defaultScale = i < ae.Setup.DefaultScale.Count
- ? ae.Setup.DefaultScale[i]
- : System.Numerics.Vector3.One;
-
- var partTransform =
- System.Numerics.Matrix4x4.CreateScale(defaultScale) *
- System.Numerics.Matrix4x4.CreateFromQuaternion(pt.Orientation) *
- System.Numerics.Matrix4x4.CreateTranslation(pt.Origin);
-
- if (ae.Scale != 1.0f)
- partTransform = partTransform * scaleMat;
-
- var template = ae.PartTemplate[i];
- newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, partTransform)
- {
- SurfaceOverrides = template.SurfaceOverrides,
- });
- }
-
- ae.Entity.MeshRefs = newMeshRefs;
- continue;
+ float over = ae.CurrFrame - ae.LowFrame;
+ ae.CurrFrame = ae.LowFrame + (over % (span + 1));
+ }
+ else if (ae.CurrFrame < ae.LowFrame)
+ {
+ ae.CurrFrame = ae.LowFrame;
}
- // ── Phase 6.4 / 6.5 fallback path: manual slerp ─────────────────
+ // Phase 6.5: blend between adjacent keyframes using the fractional
+ // part of CurrFrame so the animation is smooth at any framerate
+ // instead of snapping to integer frame indices.
+ int frameIdx = (int)Math.Floor(ae.CurrFrame);
+ if (frameIdx < ae.LowFrame || frameIdx > ae.HighFrame
+ || frameIdx >= ae.Animation.PartFrames.Count)
+ frameIdx = ae.LowFrame;
+
+ int nextIdx = frameIdx + 1;
+ if (nextIdx > ae.HighFrame || nextIdx >= ae.Animation.PartFrames.Count)
+ nextIdx = ae.LowFrame; // cycle wraps within [LowFrame, HighFrame]
+
+ float t = ae.CurrFrame - frameIdx;
+ if (t < 0f) t = 0f; else if (t > 1f) t = 1f;
+
+ var partFrames = ae.Animation.PartFrames[frameIdx].Frames;
+ var partFramesNext = ae.Animation.PartFrames[nextIdx].Frames;
+
+ int partCount = ae.PartTemplate.Count;
+ var newMeshRefs = new List(partCount);
+ var scaleMat = ae.Scale == 1.0f
+ ? System.Numerics.Matrix4x4.Identity
+ : System.Numerics.Matrix4x4.CreateScale(ae.Scale);
+
+ for (int i = 0; i < partCount; i++)
{
- int span = ae.HighFrame - ae.LowFrame;
- if (span <= 0) continue;
-
- ae.CurrFrame += dt * ae.Framerate;
- // Wrap into [LowFrame, HighFrame]. Use a guarded modulo so
- // big dts (first frame after a stall) don't blow the loop.
- if (ae.CurrFrame > ae.HighFrame)
+ // Slerp between the current and next keyframe per part.
+ // Out-of-range parts get an identity transform — defensive
+ // for setups whose part count exceeds the animation's bone
+ // count.
+ System.Numerics.Vector3 origin;
+ System.Numerics.Quaternion orientation;
+ if (i < partFrames.Count)
{
- float over = ae.CurrFrame - ae.LowFrame;
- ae.CurrFrame = ae.LowFrame + (over % (span + 1));
+ var f0 = partFrames[i];
+ var f1 = i < partFramesNext.Count ? partFramesNext[i] : f0;
+ origin = System.Numerics.Vector3.Lerp(f0.Origin, f1.Origin, t);
+ orientation = System.Numerics.Quaternion.Slerp(f0.Orientation, f1.Orientation, t);
}
- else if (ae.CurrFrame < ae.LowFrame)
+ else
{
- ae.CurrFrame = ae.LowFrame;
+ origin = System.Numerics.Vector3.Zero;
+ orientation = System.Numerics.Quaternion.Identity;
}
+ var frame = new DatReaderWriter.Types.Frame { Origin = origin, Orientation = orientation };
- // Phase 6.5: blend between adjacent keyframes using the fractional
- // part of CurrFrame so the animation is smooth at any framerate
- // instead of snapping to integer frame indices.
- int frameIdx = (int)Math.Floor(ae.CurrFrame);
- if (frameIdx < ae.LowFrame || frameIdx > ae.HighFrame
- || frameIdx >= ae.Animation.PartFrames.Count)
- frameIdx = ae.LowFrame;
+ // Per-part default scale from the Setup, matching SetupMesh.Flatten's
+ // composition order: scale → rotate → translate.
+ var defaultScale = i < ae.Setup.DefaultScale.Count
+ ? ae.Setup.DefaultScale[i]
+ : System.Numerics.Vector3.One;
- int nextIdx = frameIdx + 1;
- if (nextIdx > ae.HighFrame || nextIdx >= ae.Animation.PartFrames.Count)
- nextIdx = ae.LowFrame; // cycle wraps within [LowFrame, HighFrame]
+ var partTransform =
+ System.Numerics.Matrix4x4.CreateScale(defaultScale) *
+ System.Numerics.Matrix4x4.CreateFromQuaternion(frame.Orientation) *
+ System.Numerics.Matrix4x4.CreateTranslation(frame.Origin);
- float t = ae.CurrFrame - frameIdx;
- if (t < 0f) t = 0f; else if (t > 1f) t = 1f;
+ // Bake the entity's ObjScale on top, matching the hydration
+ // order (PartTransform * scaleMat) — see comment in OnLiveEntitySpawned.
+ if (ae.Scale != 1.0f)
+ partTransform = partTransform * scaleMat;
- var partFrames = ae.Animation.PartFrames[frameIdx].Frames;
- var partFramesNext = ae.Animation.PartFrames[nextIdx].Frames;
-
- int partCount = ae.PartTemplate.Count;
- var newMeshRefs = new List(partCount);
- var scaleMat = ae.Scale == 1.0f
- ? System.Numerics.Matrix4x4.Identity
- : System.Numerics.Matrix4x4.CreateScale(ae.Scale);
-
- for (int i = 0; i < partCount; i++)
+ var template = ae.PartTemplate[i];
+ newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, partTransform)
{
- // Slerp between the current and next keyframe per part.
- // Out-of-range parts get an identity transform — defensive
- // for setups whose part count exceeds the animation's bone
- // count.
- System.Numerics.Vector3 origin;
- System.Numerics.Quaternion orientation;
- if (i < partFrames.Count)
- {
- var f0 = partFrames[i];
- var f1 = i < partFramesNext.Count ? partFramesNext[i] : f0;
- origin = System.Numerics.Vector3.Lerp(f0.Origin, f1.Origin, t);
- orientation = System.Numerics.Quaternion.Slerp(f0.Orientation, f1.Orientation, t);
- }
- else
- {
- origin = System.Numerics.Vector3.Zero;
- orientation = System.Numerics.Quaternion.Identity;
- }
- var frame = new DatReaderWriter.Types.Frame { Origin = origin, Orientation = orientation };
-
- // Per-part default scale from the Setup, matching SetupMesh.Flatten's
- // composition order: scale → rotate → translate.
- var defaultScale = i < ae.Setup.DefaultScale.Count
- ? ae.Setup.DefaultScale[i]
- : System.Numerics.Vector3.One;
-
- var partTransform =
- System.Numerics.Matrix4x4.CreateScale(defaultScale) *
- System.Numerics.Matrix4x4.CreateFromQuaternion(frame.Orientation) *
- System.Numerics.Matrix4x4.CreateTranslation(frame.Origin);
-
- // Bake the entity's ObjScale on top, matching the hydration
- // order (PartTransform * scaleMat) — see comment in OnLiveEntitySpawned.
- if (ae.Scale != 1.0f)
- partTransform = partTransform * scaleMat;
-
- var template = ae.PartTemplate[i];
- newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, partTransform)
- {
- SurfaceOverrides = template.SurfaceOverrides,
- });
- }
-
- ae.Entity.MeshRefs = newMeshRefs;
+ SurfaceOverrides = template.SurfaceOverrides,
+ });
}
+
+ ae.Entity.MeshRefs = newMeshRefs;
}
}
@@ -2071,16 +1968,6 @@ public sealed class GameWindow : IDisposable
for (int i = 0; i < pe.MeshRefs.Count; i++)
template[i] = (pe.MeshRefs[i].GfxObjId, pe.MeshRefs[i].SurfaceOverrides);
- // Phase B.3: try to attach an AnimationSequencer for the player.
- AcDream.Core.Physics.AnimationSequencer? playerSeq = null;
- if (_animLoader is not null)
- {
- uint mtId = _playerMotionTableId ?? (uint)setup.DefaultMotionTable;
- var mtable = mtId != 0 ? _dats.Get(mtId) : null;
- if (mtable is not null)
- playerSeq = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);
- }
-
ae = new AnimatedEntity
{
Entity = pe,
@@ -2092,7 +1979,6 @@ public sealed class GameWindow : IDisposable
Scale = 1f,
PartTemplate = template,
CurrFrame = 0f,
- Sequencer = playerSeq,
};
_animatedEntities[pe.Id] = ae;
}
@@ -2112,16 +1998,6 @@ public sealed class GameWindow : IDisposable
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
- // Phase B.3: if the entity has a sequencer, route the motion change
- // through it so transition-link frames blend smoothly. NonCombat
- // stance is 0x003D0000 in full style-command form.
- if (ae.Sequencer is not null)
- {
- const uint NonCombatStyleFull = 0x003D0000u;
- uint motionCmd = (uint)cmdOverride;
- ae.Sequencer.SetCycle(NonCombatStyleFull, motionCmd);
- }
-
ae.Animation = cycle.Animation;
ae.LowFrame = Math.Max(0, cycle.LowFrame);
ae.HighFrame = Math.Min(cycle.HighFrame, cycle.Animation.PartFrames.Count - 1);