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>
The lifestone (and likely any weenie with closed shells using NoPos /
Negative / Both stippling) rendered with visible holes where you could
see inside it — confirmed via the user's "see into it" description.
Root cause: GfxObjMesh.Build skipped any polygon whose PosSurface was
out of range, which is exactly what a NoPos-stippled or
negative-only polygon looks like. Backface culling isn't involved
(acdream has it disabled); we were simply dropping triangles.
Ported the pos/neg emission rule from
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/
ObjectMeshManager.cs (lines 955-971 and 1510-1577):
pos side: emit when !Stippling.NoPos and PosSurface is valid
neg side: emit when Stippling.Negative, Stippling.Both, OR
(!Stippling.NoNeg && SidesType == CullMode.Clockwise)
The "Clockwise CullMode means NegUVIndices are on the wire" rule is
non-obvious but matches how Polygon.Unpack reads NegUVIndices, so
any closed mesh relying on that convention now renders correctly.
Neg-side triangles get the reversed fan winding and a negated vertex
normal. With culling off the winding only matters for lighting
consistency, but keeping the semantics right future-proofs the
fix if we ever enable back-face culling for a perf pass. The
dedup cache is keyed by (posIdx, uvIdx, isNeg) so the same vertex
can carry different normals on the pos and neg sides.
Pos-side winding is preserved at the original (0, i, i+1) order so
the existing single-triangle and fan-triangulation tests still pass
— neg side uses (i+1, i, 0), which is the same shape mirrored.
194 tests green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server sends UpdateMotion whenever an entity's motion state changes:
NPCs starting a walk cycle, creatures switching to a combat stance,
doors opening, a player waving, etc. Phase 6.1-6.4 already handles
rendering different (stance, forward-command) pairs for the INITIAL
CreateObject, but without this message NPCs freeze in whatever pose
they spawned with and never transition to walking/fighting.
Added UpdateMotion.TryParse with the same ServerMotionState the
CreateObject path uses, reached via a slightly different outer
layout (guid + instance seq + header'd MovementData; the MovementData
starts with the 8-byte sequence/autonomous header this time rather
than being preceded by a length field). Only the (stance, forward-
command) pair is extracted — same subset CreateObject grabs.
WorldSession dispatches MotionUpdated(guid, state) when a 0xF74C
body parses successfully. The App-side wiring (guid→entity lookup
and AnimatedEntity cycle swap) is intentionally deferred to a
separate commit because it touches GameWindow which is currently
being edited by the Phase 9.1 translucent-pass work.
89 Core.Net tests (was 83, +6 for UpdateMotion coverage).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>