# Issue #48 fix — bilinear fallback triangle-pair test was swapped ## Diagnosis (from the diagnostic dump) ACDREAM_DUMP_SCENERY_Z=1 in landblock 0xA9B3 around the user's spawn showed: - Every scenery line had `source=bilinear` (physics engine had not registered the landblock at hydration time — typical streaming race). - The trees flagged as "floating" had multi-part setups whose lowest vertex extended into the ground per our calc (`partWorldZMin = groundZ - 3.825`) — i.e., per the diagnostic the tree should sit flush. The retail client at the same world coords does sit flush. - Player Z at the same world coords (~91 m) came from the physics sampler (`TerrainSurface.SampleZ`), which is correct. That meant the bilinear fallback in `GameWindow.SampleTerrainZ` was producing a different ground Z than the visible terrain mesh and the player physics path on sloped cells, so trees were placed at a ground-Z that didn't match the terrain the player was walking on. ## Root cause Two terrain Z samplers exist: 1. `AcDream.Core.Physics.TerrainSurface.SampleZ` (instance) — used by the physics engine for player + entity ground-snap. Triangle-aware, matches the visible terrain mesh. 2. `AcDream.App.Rendering.GameWindow.SampleTerrainZ` (private bilinear fallback) — used by scenery hydration when physics has not yet built a `TerrainSurface` for a streaming-in landblock. Both choose a per-cell **diagonal** with the AC2D `FSplitNESW` formula (constants `0x0CCAC033`, `0x421BE3BD`, `0x6C1AC587`, `0x519B8F25`). Both then choose one of the cell's two **triangles** based on the fractional position within the cell. **The fallback's two arms were swapped** relative to the chosen diagonal: | Diagonal | Correct dividing test | Correct triangles | Bilinear-fallback test (BUGGY) | |---|---|---|---| | `SWtoNE` (BL→TR, line y=x) | `tx > ty` | {BL,BR,TR} below / {BL,TR,TL} above | `s + t <= 1` (wrong — that's the SEtoNW test) | | `SEtoNW` (BR→TL, line x+y=1) | `tx + ty <= 1` | {BL,BR,TL} below / {BR,TR,TL} above | `s >= t` (wrong — that's the SWtoNE test) | On sloped cells the wrong triangle's plane gives a Z that disagrees with the rendered terrain by up to ~1.5 m. Flat cells happen to mask the bug because all four corners share one Z. ## Fix 1. Extract the correct triangle-picker math from `TerrainSurface.SampleZ` (instance) into a new public static method `TerrainSurface.SampleZFromHeightmap(byte[] heights, float[] heightTable, uint landblockX, uint landblockY, float localX, float localY)`. Same algorithm, but reads the four corner heights directly from the landblock's raw heightmap byte array instead of the pre-resolved instance cache. One source of truth for the triangle math. 2. Replace `GameWindow.SampleTerrainZ` body with a call to that static. 3. Conformance test in `tests/AcDream.Core.Tests/Physics/TerrainSurfaceTests.cs` exercising a sloped heightmap on both diagonals, asserting that the new static and the existing instance method return the same Z at the same `(localX, localY)` (especially at points near the cell diagonal where the previous bug manifested). ## Why this is the right fix - Retail (`docs/research/named-retail/acclient_2013_pseudo_c.txt`) places scenery via `CLandBlock::get_land_scenes` (0x00530460) → `Plane::set_height(plane, &pos)` (0x0052f050), which projects the position onto the terrain triangle's plane. The split direction comes from the same `FSplitNESW` formula. Our `TerrainSurface.SampleZ` is a faithful port of that algorithm and is already used by physics; the bilinear fallback should be identical. - The bug is purely in the fallback path. Player Z is unaffected. - No retail behavior changes; only acdream consistency. - "Don't break" constraints from the handoff are satisfied: - Player Z snap untouched (different code path). - Species that already render flush still do (their Z was correct on cells where bilinear and physics agreed; now it's correct everywhere). ## Pre-existing bugs out of scope The user reports separate (X, Y) misplacement at other locations. That's a different bug — likely in `SceneryGenerator`'s placement math or one of the terrain-mesh / region tables — and outside the scope of this fix. File as a follow-up issue.