diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 21e72ea..70e6d9d 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -183,6 +183,15 @@ public sealed class GameWindow : IDisposable private static readonly bool s_retailCloseDegrades = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_RETAIL_CLOSE_DEGRADES"), "1", StringComparison.Ordinal); + // Issue #48 diagnostic — dump per-scenery-spawn placement evidence + // (rendered gfx id, sample source physics-vs-bilinear, ground/baseLoc/finalZ, + // mesh vertex Z range, DIDDegrade slot 0). One log line per spawn lets + // the user identify a floating tree by its world coordinates and tell + // whether the cause is BaseLoc.Z addition (H1), bilinear-fallback drift + // (H2), or DIDDegrade selection (H3). Diagnostic-first per CLAUDE.md. + private static readonly bool s_dumpSceneryZ = + string.Equals(Environment.GetEnvironmentVariable("ACDREAM_DUMP_SCENERY_Z"), "1", StringComparison.Ordinal); + /// /// Issue #47 humanoid-setup detector. Matches Aluvian Male /// (0x02000001) and the 34-part heritage sibling setups @@ -4639,10 +4648,63 @@ public sealed class GameWindow : IDisposable // fall back to the local bilinear sample. var worldPx = localX + lbOffset.X; var worldPy = localY + lbOffset.Y; - float groundZ = _physicsEngine.SampleTerrainZ(worldPx, worldPy) + float? maybePhysicsZ = _physicsEngine.SampleTerrainZ(worldPx, worldPy); + float groundZ = maybePhysicsZ ?? SampleTerrainZ(lb.Heightmap, _heightTable, localX, localY); float finalZ = groundZ + spawn.LocalPosition.Z; + // Issue #48 diagnostic. One log line per (spawn, rendered-mesh) + // disambiguates H1 (BaseLoc.Z / mesh-zMin per-species), H2 + // (physics-vs-bilinear sampler drift), and H3 (DIDDegrade slot 0). + // User identifies a floating tree visually, finds the matching + // line by world coords + gfx id, the data picks the hypothesis. + if (s_dumpSceneryZ) + { + string source = maybePhysicsZ.HasValue ? "physics" : "bilinear"; + foreach (var mr in meshRefs) + { + var dgfx = _dats.Get(mr.GfxObjId); + if (dgfx is null) continue; + + float zMin = float.PositiveInfinity, zMax = float.NegativeInfinity; + foreach (var v in dgfx.VertexArray.Vertices.Values) + { + if (v.Origin.Z < zMin) zMin = v.Origin.Z; + if (v.Origin.Z > zMax) zMax = v.Origin.Z; + } + if (float.IsPositiveInfinity(zMin)) { zMin = 0f; zMax = 0f; } + + bool hasDD = dgfx.Flags.HasFlag(DatReaderWriter.Enums.GfxObjFlags.HasDIDDegrade); + string ddInfo = string.Empty; + if (hasDD && dgfx.DIDDegrade != 0) + { + var ddi = _dats.Get(dgfx.DIDDegrade); + if (ddi is not null && ddi.Degrades.Count > 0) + { + uint slot0Id = (uint)ddi.Degrades[0].Id; + float slot0Min = 0f; + var slot0Gfx = _dats.Get(slot0Id); + if (slot0Gfx is not null && slot0Gfx.VertexArray.Vertices.Count > 0) + { + slot0Min = float.PositiveInfinity; + foreach (var v in slot0Gfx.VertexArray.Vertices.Values) + if (v.Origin.Z < slot0Min) slot0Min = v.Origin.Z; + if (float.IsPositiveInfinity(slot0Min)) slot0Min = 0f; + } + ddInfo = $" deg[0]=0x{slot0Id:X8} deg[0]ZMin={slot0Min:F3}"; + } + } + + Console.WriteLine( + $"[scenery-z] lb=0x{lb.LandblockId:X8} root=0x{spawn.ObjectId:X8} gfx=0x{mr.GfxObjId:X8}" + + $" source={source}" + + $" world=({worldPx:F2},{worldPy:F2}) localXY=({localX:F2},{localY:F2})" + + $" groundZ={groundZ:F3} BaseLoc.Z={spawn.LocalPosition.Z:F3} finalZ={finalZ:F3}" + + $" zRange=[{zMin:F3}..{zMax:F3}] zSpan={zMax - zMin:F3}" + + $" hasDIDDegrade={hasDD}{ddInfo}"); + } + } + var hydrated = new AcDream.Core.World.WorldEntity { Id = sceneryIdBase + localIndex++,