From 69bf91b3d6bd2c0ca424681e75d4015c5d1ed343 Mon Sep 17 00:00:00 2001 From: beezm Date: Mon, 10 Aug 2026 00:56:34 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20shoreline=20seam=20=E2=80=94=20water=20f?= =?UTF-8?q?ollows=20the=20RENDERED=20coastline=20(terrain-water=20task=201?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the C1-polish carryover. The cause was confirmed by a read-only dump BEFORE any code was written, per the filed note's discipline; the chat had guessed twice from screenshots. DIAGNOSIS. WBID is classified from the UNCURVED heightmap — the classify path that keeps the biome/water oracle byte-identical through all of Phase B — while the mesh renders the CURVED one. Outside the crater those two agree EXACTLY, and provably so: the curve is identity at sea and monotonic, so Apply(raw) < sea iff raw < sea. Inside the crater they do not. The carve lerps two different bases toward one target -- classify from raw, rendered from Apply(raw) -- and Apply(raw) < raw throughout the lowland band (the toe compresses raw 0.15..0.516 into 0.15..0.206). So the rendered surface sinks FASTER than the classify surface, leaving an annulus that renders below the waterline while WBID still calls it dry. MEASURED, seed 1825907253, map-wide: wet columns 32,799,866 WATER-OVER-DRY (wet, mesh above Y) 1,101 scattered, not the story DRY-UNDER-WATER (dry, mesh below sea) 375,824 <- the seam ...of which inside the crater carve: 100 % Dump across the gradient at z=905: at x=3800 the classify surface sits at 47.55 m while the mesh renders 22.38 m -- 25 m apart, no water drawn. The ring is west of the Capitol, which is exactly the developer's "flush on the right, lifted toward the left". Neither of the task's two candidate causes, and worth saying so: the water LEVEL is flat and correct at 37.65 everywhere it is drawn (rules out B), and the per-cell flat sheet is a rounding error next to this (1,101 px vs 375,824). It is the filed note's classify-vs-render mismatch, with the crater carve as the mechanism. THE FIX. Presence now takes two clauses: the blueprint's classification, OR the rendered ground actually being under the ocean's surface. The second clause tests exactly what the mesh draws (exactSurfaceY) against the OCEAN BODY'S OWN level from WBTB -- the runtime still derives nothing, it only notices that the ground it is drawing is under a surface the blueprint gave it. By the identity above that clause can only ever fire inside the carve, which is precisely the flooded bay it exists for. MEASURED after, same seed: 241,415 -> 358,576 water columns, 454 -> 648 chunks, 117,161 seam columns recovered in the loaded region. Each run now reports its own seam count. 2D PIPELINE UNTOUCHED, verified rather than asserted: the only changed file is the runtime chunk manager, and a regeneration is byte-identical to task 13's in every section but PRMS (timestamp + git hash). All four snapshot PNGs md5-identical. Static flat sheet, no animation, no water DATA change. Co-Authored-By: Claude Opus 5 (1M context) --- Server/Scripts/ServerChunkManager.cs | 90 ++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/Server/Scripts/ServerChunkManager.cs b/Server/Scripts/ServerChunkManager.cs index 56f1f60..2bf74b7 100644 --- a/Server/Scripts/ServerChunkManager.cs +++ b/Server/Scripts/ServerChunkManager.cs @@ -34,6 +34,12 @@ namespace IslaApocalypse.Server private int _chunksWithWater = 0; private float _waterMinY = float.MaxValue, _waterMaxY = float.MinValue, _waterMaxDepth = 0f; + // Shoreline seam (task 15). The OCEAN body's own surface level, from the + // blueprint's WBTB table; -1 when the blueprint carries no ocean. Columns the + // mesh renders below this but WBID calls dry are the seam, and are counted. + private float _oceanLevelRaw = -1f; + private long _seamColumnsRecovered = 0; + public override void _Ready() { @@ -60,6 +66,13 @@ namespace IslaApocalypse.Server $"vs config MapSize {ConfigManager.MapSize}."); } + // The ocean's surface level, for the shoreline-seam rule below. Taken + // from the blueprint's body table (type OCEAN), never derived here — + // the runtime does not compute sea level (D-033). + if (_blueprint.WaterBodies != null) + foreach (var body in _blueprint.WaterBodies) + if (body.Type == WaterBodyInfo.TYPE_OCEAN) { _oceanLevelRaw = body.SurfaceLevel; break; } + GD.Print("[Server] Blueprint loaded. Locating Capitol City..."); // 2. Find the Capitol in the parsed data @@ -99,7 +112,8 @@ namespace IslaApocalypse.Server GD.Print($"[Server] Water at rest: {_waterColumnsRendered} water columns across " + $"{_chunksWithWater} of {_activeChunks.Count} chunks" + (_waterColumnsRendered > 0 - ? $"; surface Y {_waterMinY:F1}..{_waterMaxY:F1} m, depth up to {_waterMaxDepth:F1} m." + ? $"; surface Y {_waterMinY:F1}..{_waterMaxY:F1} m, depth up to {_waterMaxDepth:F1} m; " + + $"{_seamColumnsRecovered} shoreline-seam columns recovered (mesh below the ocean surface, WBID dry)." : " — nothing to draw here (check the blueprint carries WBID/WSRF).")); // 5. Teleport the Camera to look down at our creation! @@ -268,43 +282,69 @@ namespace IslaApocalypse.Server newChunk.ColumnBiomes[x, z] = columnBiome; newChunk.ColumnRoadMaterial[x, z] = roadSurface; - // --- WATER AT REST (task 13) --------------------------------- - // The blueprint is the AUTHORITY on where water is; the runtime - // only draws it. WBID decides presence (it is the water stage's - // own classification output, the same set the biome grid's - // Ocean/Lake pixels form by construction), WSRF gives the level - // per pixel, and the WBTB body table is the fallback if a column - // is flagged wet but carries the WSRF no-water sentinel. + // --- WATER AT REST (task 13) + SHORELINE SEAM (task 15) ------ + // The blueprint is the AUTHORITY on where water is and at what + // level; the runtime only draws it. WSRF gives the level per pixel, + // and the WBTB body table is the fallback if a column is flagged wet + // but carries the WSRF no-water sentinel. + // + // PRESENCE, though, takes two clauses. WBID was classified from the UNCURVED + // heightmap — the classify path that keeps the biome/water oracle + // byte-identical — while the mesh renders the CURVED one. Outside + // the crater those two agree exactly, because the curve is identity + // at sea and monotonic, so Apply(raw) < sea iff raw < sea. INSIDE + // the crater they do not: the carve lerps two different bases toward + // one target (classify from raw, rendered from Apply(raw), and + // Apply(raw) < raw throughout the lowland band), so the rendered + // surface sinks faster and leaves a ring that renders below the + // waterline while WBID still calls it dry. Measured on seed + // 1825907253: 375,824 such columns, 100 % of them inside the carve. + // + // So presence is decided by BOTH: the blueprint's classification, + // OR the rendered ground actually being under the ocean's surface. + // The second clause can only ever fire inside the carve (see the + // identity above), which is precisely the flooded bay it exists for. if (_blueprint.WaterBodyIds != null) { ushort bodyId = _blueprint.WaterBodyIds[globalX, globalZ]; + float levelRaw = -1f; + if (bodyId != 0) { - float levelRaw = -1f; if (_blueprint.WaterSurfaceQ != null) { ushort q = _blueprint.WaterSurfaceQ[globalX, globalZ]; if (q != 0) levelRaw = BlueprintFormat.DecodeWaterLevel(q); } if (levelRaw < 0f) levelRaw = BodyLevel(bodyId); + } + else if (_oceanLevelRaw >= 0f + && exactSurfaceY < _oceanLevelRaw * Constants.HEIGHT_SCALE) + { + // Rendered ground below the ocean's own surface level. The + // level still comes from the blueprint (the ocean body's + // WBTB entry) — the runtime derives nothing, it only notices + // that the ground the MESH draws is under that surface. + levelRaw = _oceanLevelRaw; + _seamColumnsRecovered++; + } - if (levelRaw >= 0f) - { - // Same mapping as the terrain, so the sheet and the - // seabed cannot drift apart. - newChunk.WaterSurfaceY[x, z] = Mathf.Clamp( - levelRaw * Constants.HEIGHT_SCALE, 2.0f, Constants.CHUNK_HEIGHT - 2.0f); - // TRUE depth, from blueprint units — see ChunkData. - newChunk.WaterDepthM[x, z] = - Mathf.Max(0f, (levelRaw - _blueprint.HeightMap[globalX, globalZ]) * Constants.HEIGHT_SCALE); - newChunk.HasAnyWater = true; + if (levelRaw >= 0f) + { + // Same mapping as the terrain, so the sheet and the seabed + // cannot drift apart. + newChunk.WaterSurfaceY[x, z] = Mathf.Clamp( + levelRaw * Constants.HEIGHT_SCALE, 2.0f, Constants.CHUNK_HEIGHT - 2.0f); + // TRUE depth, from blueprint units — see ChunkData. + newChunk.WaterDepthM[x, z] = + Mathf.Max(0f, (levelRaw - _blueprint.HeightMap[globalX, globalZ]) * Constants.HEIGHT_SCALE); + newChunk.HasAnyWater = true; - _waterColumnsRendered++; - float wy = newChunk.WaterSurfaceY[x, z]; - if (wy < _waterMinY) _waterMinY = wy; - if (wy > _waterMaxY) _waterMaxY = wy; - if (newChunk.WaterDepthM[x, z] > _waterMaxDepth) _waterMaxDepth = newChunk.WaterDepthM[x, z]; - } + _waterColumnsRendered++; + float wy = newChunk.WaterSurfaceY[x, z]; + if (wy < _waterMinY) _waterMinY = wy; + if (wy > _waterMaxY) _waterMaxY = wy; + if (newChunk.WaterDepthM[x, z] > _waterMaxDepth) _waterMaxDepth = newChunk.WaterDepthM[x, z]; } }