diag(scenery): #48 ACDREAM_DUMP_SCENERY_Z dump

Per-spawn / per-rendered-mesh log line at scenery hydration: rendered
gfx id, sample source (physics vs bilinear), groundZ, BaseLoc.Z,
finalZ, mesh vertex Z range, and DIDDegrade slot 0 metadata. One log
line lets the user identify a floating tree by world coords and the
data picks the hypothesis (BaseLoc.Z addition / sampler drift /
DIDDegrade selection). Diagnostic-first per CLAUDE.md before the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-06 17:57:10 +02:00
parent e3d8a44c48
commit 8ee76deefd

View file

@ -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);
/// <summary>
/// Issue #47 humanoid-setup detector. Matches Aluvian Male
/// (<c>0x02000001</c>) 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<DatReaderWriter.DBObjs.GfxObj>(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<DatReaderWriter.DBObjs.GfxObjDegradeInfo>(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<DatReaderWriter.DBObjs.GfxObj>(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++,