feat: WBID/WBTB/WSRF blueprint sections + parser + harness extension (terrain-water task 03)

Three new v2 tagged sections carrying the water-bodies stage output:
WBID (MapSize^2 u16 body ids; 0 none, 1 ocean, 2..N lakes), WBTB
(count-prefixed 20-byte records: id, type, provisional salinity,
transitional flat surface level, pixel count, centroid), WSRF
(MapSize^2 u16 quantized levels; 0 = no-water sentinel, 1 + L*32768
encoding, ~7.7 mm world resolution). Written only when the blueprint
carries water; absent on legacy re-encodes. Parser registers all
three (length + type checks) into new WorldBlueprint members —
consumed by nothing at runtime. Round-trip harness now compares the
water sections whenever the source carries them; v1-source baseline
stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-07 01:31:50 -04:00
parent d296ef30c4
commit 3bf822dac6
5 changed files with 195 additions and 0 deletions

View file

@ -231,6 +231,8 @@ public partial class MapGenerator : TextureRect
BranchRoads = _branchPaths,
RuggedRoads = _ruggedPaths,
TrailRoads = _trailPaths,
WaterBodyIds = _waterBodyIds,
WaterBodies = _waterBodies ?? new List<WaterBodyInfo>(),
FormatVersion = 2,
Params = new BlueprintParams
{

View file

@ -119,6 +119,7 @@ public partial class RoundTripHarness : Node
ok &= CompareRoads("Branch", a.BranchRoads, b.BranchRoads);
ok &= CompareRoads("Rugged", a.RuggedRoads, b.RuggedRoads);
ok &= CompareRoads("Trail", a.TrailRoads, b.TrailRoads);
ok &= CompareWater(a, b);
if (ok)
GD.Print($"[Harness] Semantic equality holds: {a.MapSize}x{a.MapSize} grid, " +
@ -127,6 +128,63 @@ public partial class RoundTripHarness : Node
return ok;
}
// Water sections (WBID/WBTB/WSRF) are compared whenever the SOURCE carries them —
// the regression test grows with the format. A source without water (legacy v1)
// must round-trip to a file without water.
private bool CompareWater(WorldBlueprint a, WorldBlueprint b)
{
if (a.WaterBodyIds == null && b.WaterBodyIds == null)
{
GD.Print("[Harness] Water sections: absent in source — nothing to compare (and none reappeared).");
return true;
}
if (a.WaterBodyIds == null || b.WaterBodyIds == null)
{
GD.PrintErr($"[Harness] Water sections presence mismatch: source {(a.WaterBodyIds != null ? "has" : "lacks")} them, reread {(b.WaterBodyIds != null ? "has" : "lacks")} them.");
return false;
}
bool ok = true;
long idDiffs = 0, surfDiffs = 0;
int n = a.MapSize;
for (int x = 0; x < n; x++)
{
for (int y = 0; y < n; y++)
{
if (a.WaterBodyIds[x, y] != b.WaterBodyIds[x, y]) idDiffs++;
ushort sa = a.WaterSurfaceQ != null ? a.WaterSurfaceQ[x, y] : (ushort)0;
ushort sb = b.WaterSurfaceQ != null ? b.WaterSurfaceQ[x, y] : (ushort)0;
if (sa != sb) surfDiffs++;
}
}
if (idDiffs > 0) { GD.PrintErr($"[Harness] {idDiffs} WBID pixels differ."); ok = false; }
if (surfDiffs > 0) { GD.PrintErr($"[Harness] {surfDiffs} WSRF pixels differ."); ok = false; }
if (a.WaterBodies.Count != b.WaterBodies.Count)
{
GD.PrintErr($"[Harness] Water body count mismatch: {a.WaterBodies.Count} vs {b.WaterBodies.Count}");
ok = false;
}
else
{
for (int i = 0; i < a.WaterBodies.Count; i++)
{
var wa = a.WaterBodies[i];
var wb = b.WaterBodies[i];
if (wa.Id != wb.Id || wa.Type != wb.Type || wa.Salinity != wb.Salinity ||
System.BitConverter.SingleToInt32Bits(wa.SurfaceLevel) != System.BitConverter.SingleToInt32Bits(wb.SurfaceLevel) ||
wa.PixelCount != wb.PixelCount || wa.Centroid != wb.Centroid)
{
GD.PrintErr($"[Harness] Water body {i} mismatch (id {wa.Id} vs {wb.Id}).");
ok = false;
}
}
}
if (ok) GD.Print($"[Harness] Water sections equal: {a.WaterBodies.Count} bodies, WBID+WSRF grids identical.");
return ok;
}
private bool CompareRoads(string tier, List<Vector2[]> a, List<Vector2[]> b)
{
if (a.Count != b.Count)