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]; } }