diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 09f7652..fbf91c4 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -621,7 +621,13 @@ public sealed class GameWindow : IDisposable
return;
}
- var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
+ // Phase 6: resolve the entity's idle motion frame from its
+ // MotionTable chain. For creatures and characters this gives us
+ // the upright "Resting" pose instead of the Setup's Default
+ // (T-pose / aggressive crouch). Static items with no motion table
+ // get null and fall back to PlacementFrames in Flatten.
+ var idleFrame = AcDream.Core.Meshing.MotionResolver.GetIdleFrame(setup, _dats);
+ var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup, idleFrame);
// Apply the server's AnimPartChanges: "replace part at index N
// with GfxObj M". This is how characters become clothed (head →
diff --git a/src/AcDream.Core/Meshing/MotionResolver.cs b/src/AcDream.Core/Meshing/MotionResolver.cs
new file mode 100644
index 0000000..932d756
--- /dev/null
+++ b/src/AcDream.Core/Meshing/MotionResolver.cs
@@ -0,0 +1,100 @@
+using DatReaderWriter;
+using DatReaderWriter.DBObjs;
+using DatReaderWriter.Types;
+
+namespace AcDream.Core.Meshing;
+
+///
+/// Resolves an entity's idle / default-state per-part animation frame from
+/// its Setup → MotionTable → Animation chain. Used by the rendering
+/// hydration path so creatures and characters render in their proper idle
+/// pose instead of the static Setup.PlacementFrames[Default] pose
+/// (which for drudges and most creatures is "T-pose-ish" or aggressive
+/// crouch — the wrong starting point for a calm idle).
+///
+///
+/// The resolution algorithm matches ACViewer's
+/// Physics/Animation/MotionTable.SetDefaultState for the static
+/// (non-animated) case: pick the first frame of the default cycle's
+/// first AnimData. We don't run the animation forward over time yet —
+/// the goal here is "render the FIRST frame of the IDLE motion" which
+/// gives us a sensible static pose.
+///
+///
+///
+/// References (per CLAUDE.md cross-reference rule):
+///
+/// - references/ACViewer/.../Physics/Animation/MotionTable.cs::SetDefaultState
+/// — the algorithm we port
+/// - references/ACE/.../FileTypes/MotionTable.cs
+/// — server-side parser, confirms key encoding
+/// - references/DatReaderWriter/.../Generated/DBObjs/MotionTable.generated.cs
+/// — exact field names and types
+///
+///
+///
+public static class MotionResolver
+{
+ ///
+ /// Walk an entity's motion-table chain and return the per-part
+ /// animation frame for its default idle state. Returns null
+ /// if any link in the chain is missing — caller should fall back
+ /// to Setup.PlacementFrames.
+ ///
+ /// The entity's base Setup dat.
+ /// Dat collection used to load the linked MotionTable + Animation.
+ ///
+ /// Optional override for the motion table id. Defaults to
+ /// Setup.DefaultMotionTable. The server's CreateObject can
+ /// supply a per-instance MotionTable id via PhysicsDescriptionFlag.MTable
+ /// — pass that here when present so character outfits/states use the
+ /// correct table.
+ ///
+ public static AnimationFrame? GetIdleFrame(
+ Setup setup,
+ DatCollection dats,
+ uint? motionTableIdOverride = null)
+ {
+ ArgumentNullException.ThrowIfNull(setup);
+ ArgumentNullException.ThrowIfNull(dats);
+
+ uint mtableId = motionTableIdOverride ?? (uint)setup.DefaultMotionTable;
+ if (mtableId == 0) return null;
+
+ var mtable = dats.Get(mtableId);
+ if (mtable is null) return null;
+
+ // Step 1: find the substate that DefaultStyle maps to.
+ if (!mtable.StyleDefaults.TryGetValue(mtable.DefaultStyle, out var defaultSubstate))
+ return null;
+
+ // Step 2: compose the cycle key. ACViewer's encoding:
+ // cycle = (DefaultStyle << 16) | (substate & 0xFFFFFF)
+ // Cast through uint then int because Cycles is keyed by int.
+ uint defaultStyleVal = (uint)mtable.DefaultStyle;
+ uint substateVal = (uint)defaultSubstate;
+ int cycleKey = (int)((defaultStyleVal << 16) | (substateVal & 0xFFFFFF));
+
+ if (!mtable.Cycles.TryGetValue(cycleKey, out var motionData) || motionData is null)
+ return null;
+ if (motionData.Anims.Count == 0) return null;
+
+ var animData = motionData.Anims[0];
+
+ // Step 3: load the Animation and grab the first frame.
+ uint animId = (uint)animData.AnimId;
+ if (animId == 0) return null;
+
+ var animation = dats.Get(animId);
+ if (animation is null) return null;
+ if (animation.PartFrames.Count == 0) return null;
+
+ // animData.LowFrame is the start frame index of the cycle within the
+ // animation. Clamp defensively in case it's out of range.
+ int frameIdx = animData.LowFrame;
+ if (frameIdx < 0 || frameIdx >= animation.PartFrames.Count)
+ frameIdx = 0;
+
+ return animation.PartFrames[frameIdx];
+ }
+}
diff --git a/src/AcDream.Core/Meshing/SetupMesh.cs b/src/AcDream.Core/Meshing/SetupMesh.cs
index 1be9baa..26470b4 100644
--- a/src/AcDream.Core/Meshing/SetupMesh.cs
+++ b/src/AcDream.Core/Meshing/SetupMesh.cs
@@ -14,24 +14,28 @@ public static class SetupMesh
/// Does NOT walk ParentIndex — each part's transform is local to the setup root.
/// This is simplification for Phase 2; complex hierarchical rigs are Phase 3.
///
- public static IReadOnlyList Flatten(Setup setup)
+ public static IReadOnlyList Flatten(Setup setup, AnimationFrame? motionFrameOverride = null)
{
- // ACViewer's CreateMesh always calls SetPlacementFrame(0x65) =
- // Placement.Resting after creating any mesh, regardless of object
- // type. For creatures and characters this matters a lot — Default
- // is the aggressive battle crouch pose with arms extended forward,
- // and Resting is the upright idle pose. Without this preference,
- // every drudge/skeleton/character renders mid-attack, including
- // the Nullified Statue of a Drudge whose only placement frame is
- // keyed by Resting.
+ // Pose source priority:
+ // 1. motionFrameOverride — caller has resolved an idle/animation
+ // frame from the entity's MotionTable (Phase 6). This is the
+ // best source for creatures and characters because their
+ // Setup.PlacementFrames don't define an upright idle pose.
+ // 2. Setup.PlacementFrames[Resting] — used by static objects
+ // that have a Resting frame defined (e.g. signs, doors).
+ // 3. Setup.PlacementFrames[Default] — fallback for everything
+ // else (most scenery), which is the only frame those setups
+ // define and renders correctly.
//
- // Fall back to Default if Resting isn't present (most scenery
- // setups only define Default). The user-visible difference is
- // most dramatic for creatures; static dat scenery is unaffected.
- AnimationFrame? defaultAnim = null;
- if (setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting))
+ // Without an override, creatures used to render in their Default
+ // pose (T-pose-ish or aggressive crouch) because their MotionTable
+ // wasn't consulted. The Phase 6 MotionResolver provides the override
+ // by walking Setup.DefaultMotionTable → MotionTable.Cycles →
+ // Animation.PartFrames[LowFrame].
+ AnimationFrame? defaultAnim = motionFrameOverride;
+ if (defaultAnim is null && setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting))
defaultAnim = resting;
- else if (setup.PlacementFrames.TryGetValue(Placement.Default, out var af))
+ if (defaultAnim is null && setup.PlacementFrames.TryGetValue(Placement.Default, out var af))
defaultAnim = af;
var result = new List(setup.Parts.Count);