using Godot; namespace IslaApocalypse.Tools { /// /// Raw height-field I/O: little-endian float32, x-major (index = x * MapSize + y). /// /// ⭐ THIS FILE IS WHY PRESENTATION AND GENERATION STAY SEPARATE. A relief render reads a dumped /// height field and re-colours it; it never re-runs the generator. So a look change provably /// cannot move the terrain — not by convention, but because the colours are computed from a /// file the renderer cannot write. /// /// It also makes a byte-level port-fidelity oracle possible later: two generators agree, or /// their dumps differ. → `Design - Tooling - Iteration and Batching.md`, "build the oracle /// before the taste-iteration". /// public static class HeightField { public static void Save(float[,] height, int mapSize, string absolutePath) { using var f = Godot.FileAccess.Open(absolutePath, Godot.FileAccess.ModeFlags.Write); if (f == null) { GD.PrintErr($"[HeightField] cannot write {absolutePath}: {Godot.FileAccess.GetOpenError()}"); return; } for (int x = 0; x < mapSize; x++) for (int y = 0; y < mapSize; y++) f.StoreFloat(height[x, y]); } /// Load a dump. Returns null if absent or the wrong size for . public static float[,] Load(string absolutePath, int mapSize) { if (!Godot.FileAccess.FileExists(absolutePath)) return null; using var f = Godot.FileAccess.Open(absolutePath, Godot.FileAccess.ModeFlags.Read); if (f == null) return null; long expected = (long)mapSize * mapSize * 4; if ((long)f.GetLength() != expected) { GD.PrintErr($"[HeightField] {absolutePath} is {f.GetLength()} bytes, expected {expected} " + $"for MapSize {mapSize} — ignoring it rather than guessing."); return null; } var h = new float[mapSize, mapSize]; for (int x = 0; x < mapSize; x++) for (int y = 0; y < mapSize; y++) h[x, y] = f.GetFloat(); return h; } } }