Four fixes from the ACME StaticObjectManager cross-reference:
1. GfxObjMesh: normalize vertex normals (1d). Dat normals may not be
unit-length; without normalization, lighting is wrong per-vertex.
2. SetupMesh: add third-fallback placement frame (2a). If neither
Resting nor Default exists, use the first available frame from
PlacementFrames. Matches ACME's GetDefaultPlacementFrame.
3. SceneryGenerator: building cell exclusion (4d). Compute which
terrain vertices have buildings (from LandBlockInfo.Objects +
Buildings), skip scenery spawns in those cells. Prevents trees
from spawning inside building footprints.
4. SceneryGenerator: slope filter (4e). Compute terrain normal Z at
each displaced position and check against ObjectDesc.MinSlope /
MaxSlope bounds. Prevents trees from spawning on cliff faces.
Also confirmed 4f (scenery Z=0) is NOT a bug — GameWindow's hydrator
lifts scenery to terrain Z at line 1213. The Z=0 in SceneryGenerator
is a placeholder correctly overridden at render time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Walks each entity's Setup → MotionTable → Animation chain to get the
per-part frame for its default idle pose, then uses that frame in
SetupMesh.Flatten instead of the static PlacementFrames lookup. For
creatures and characters this should produce the upright "Resting"
pose (e.g. for the Nullified Statue of a Drudge) instead of the
Setup-default crouch.
This is a minimal Phase 6 cut: render the FIRST frame of the IDLE
motion as a static pose. No per-frame interpolation, no walking, no
attack motions, no transitions. Those are larger pieces tracked in
docs/plans/2026-04-11-roadmap.md under Phase 6.
Algorithm ported from references/ACViewer/.../Physics/Animation/
MotionTable.SetDefaultState:
1. Look up Setup.DefaultMotionTable (0x09XXXXXX). 0 → no motion,
fall back to PlacementFrames.
2. MotionTable.StyleDefaults[DefaultStyle] → default substate.
3. cycleKey = (DefaultStyle << 16) | (substate & 0xFFFFFF)
4. MotionTable.Cycles[cycleKey] → MotionData.
5. MotionData.Anims[0].AnimId → Animation dat.
6. Animation.PartFrames[animData.LowFrame] → AnimationFrame
containing the per-part transforms for the idle pose.
Added:
- Core/Meshing/MotionResolver.cs: pure function GetIdleFrame(setup,
dats) that walks the chain and returns an AnimationFrame or null.
null is the "no motion data" sentinel and means caller should fall
back to PlacementFrames.
- SetupMesh.Flatten now takes an optional AnimationFrame override
parameter. Pose source priority is:
override → PlacementFrames[Resting] → PlacementFrames[Default]
So existing call sites that don't pass an override get the Phase 5d
Resting-fallback behavior unchanged. Static scenery is unaffected.
- GameWindow.OnLiveEntitySpawned (live-mode hydrator) calls
MotionResolver.GetIdleFrame and passes the result to Flatten.
Other Flatten call sites (offline scenery, interior EnvCells, scenery
generator) NOT yet wired — those use static dat hydration where the
entities don't have meaningful motion tables. The user-visible win
from this commit is in the live spawn pipeline only.
Things I'm not certain about and will check via the live run:
- Whether Animation.PartFrames are in entity-root-relative space
(matching PlacementFrames) or parent-relative (would need a parent
walk we don't do). ACViewer's UpdateParts applies frames per-part
without walking parents, suggesting root-relative — same convention
as PlacementFrames.
- Whether the resolver's null fallback is hit for creatures whose
Setup.DefaultMotionTable happens to be 0 (would silently regress
to Default placement). Worth checking if drudge looks the same.
Tests: 77 core + 83 net = 160, all green. No new tests yet because
the change is data-driven and best validated end-to-end via the live
run rather than synthetic dat fixtures (which would require fabricating
a complete MotionTable + Animation chain just to test the lookup).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Nullified Statue of a Drudge renders correctly in scale + color +
texture after Phases 5a/b/c, but the user reported the figure on top
looks like the wrong drudge model — specifically the pose is wrong.
acdream shows a hunched aggressive crouch with arms forward, retail
shows an upright statue stance with arms at sides. Same drudge mesh,
different pose.
Diagnosis from a targeted statue dump:
[STATUE] objScale=3.500
[STATUE] base Setup 0x020007DD has 17 parts (full drudge body rig)
[STATUE] animPart index=1 newModel=0x01001B91 ← NO-OP, same as default
[STATUE] placementFrames count=1
The animPart change is a no-op (replaces part 1 with the id it already
has). The Setup is the standard drudge body. So the difference HAS to
come from the per-part placement frame. With only 1 placement frame,
there's exactly one pose to use — and our SetupMesh.Flatten only checks
Placement.Default.
Fix found by reading ACViewer's Physics/PartArray.cs::CreateMesh:
public static PartArray CreateMesh(PhysicsObj owner, uint setupDID) {
var mesh = new PartArray();
...
if (!mesh.SetMeshID(setupDID)) return null;
mesh.SetPlacementFrame(0x65); // ← always Resting after create
return mesh;
}
0x65 = 101 = Placement.Resting. ACViewer puts EVERY mesh into the
Resting pose immediately after creation, regardless of object type.
For drudges/characters/creatures:
- Default = aggressive battle crouch (what we render)
- Resting = upright idle pose (what retail's statue actually shows)
The statue's single placement frame is keyed by Resting, so our
"only check Default" code returned no frame and the parts ended up at
Setup-root with identity orientation — which happened to look like a
clawing-forward pose because each part's local mesh starts in roughly
that shape.
Fix: SetupMesh.Flatten now tries Resting first and falls back to
Default. Static scenery setups (which only define Default) are
unaffected; creatures and characters now render in their proper idle
pose. One-line conceptual change with effects on every multi-part
live entity in the world.
The reference-priority rule in CLAUDE.md saved me here: I'd already
chased this through ACE.Server and DatReaderWriter looking for a parent-
hierarchy walking algorithm before checking ACViewer's Physics/PartArray.
The pose fix lives in ACViewer's renderer, exactly where I'd expect
"the canonical client-side visual pipeline" per the rule's wording.
Tests: 77 core + 83 net = 160, all green. Existing scenery
SetupMesh tests still pass because their setups only define Default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>