using Godot; using System.IO; using System.Collections.Generic; namespace IslaApocalypse.Core { // 1. The Data Container // This holds the unpacked data in RAM so the Server can ask it questions // without having to re-read the hard drive every single frame. public struct TownLocation { public Vector2 Position; public TownTier Tier; // Whether the generator marked this settlement as a highway node (it drives the // road topology at generation time). Carried by the v2 blueprint; always false // when loading a legacy v1 file, which never stored it. Nothing server-side // consumes it yet — it is carried and exposed, not acted on. public bool IsHighwayNode; } /// /// The resolved generation inputs embedded in a v2 blueprint, so a file is /// self-describing and config desync is detectable. Null on a legacy v1 load. /// A v2 file re-encoded from a v1 source carries the BlueprintFormat sentinel /// values instead of real inputs. /// public class BlueprintParams { public int WorldSeed; // the generator's RESOLVED noise seed (names the file) public int MapSize; public float CraterRadius; public float DensityMultiplier; public Vector2 ImpactCenter; // the crater's _impactCenter, in map pixels public string GeneratedUtc = ""; // stamped at write time, ISO-8601 public string GeneratorGitHash = ""; // short hash of the generator repo, "" if unknown } /// /// One water body from the blueprint's WBTB section: the ocean (exactly one, id 1) /// or a lake (ids 2..N). SurfaceLevel is the documented TRANSITIONAL rule — one flat /// level per body, GetSeaLevel at the body's pixel centroid under the still-live /// latitude field; superseded when the flat-scalar sea model lands. Salinity is a /// provisional placeholder for the future fresh/salt mechanic (ocean salt, lake fresh). /// public class WaterBodyInfo { public const byte TYPE_OCEAN = 0; public const byte TYPE_LAKE = 1; public const byte SALINITY_FRESH = 0; public const byte SALINITY_SALT = 1; public ushort Id; public byte Type; public byte Salinity; public float SurfaceLevel; // raw blueprint height units public int PixelCount; public Vector2 Centroid; // map pixels } /// /// The height-redistribution curve that shaped this blueprint's heights (v2 TCRV /// section, terrain-water task 05). Null when the blueprint was generated with the /// curve off (or predates it) — heights are then the raw legacy profile. Pure /// metadata: the server never re-applies the curve; exported heights are already /// curved. /// public class TerrainCurveInfo { public ushort Version; // Input knots. SpikeMax is the spike domain's top: under curve v1 it was the // pooled calibration max (identical every seed); from v2 it is the SEED'S own // effective raw maximum — blueprints are no longer reproducible from curve // constants alone, which is exactly why it is recorded here. public float T1, T2, T3, T4, SpikeMax; public float Sea, OrangeCeil, RedCeil, PlateauLo, PlateauHi, PeakCap, TailSlope; // output bands } public class WorldBlueprint { public int MapSize; public float[,] HeightMap; public Biome[,] BiomeMap; public List Towns = new List(); // All 4 road tiers! public List Highways = new List(); public List BranchRoads = new List(); public List RuggedRoads = new List(); public List TrailRoads = new List(); // Which on-disk format this blueprint was parsed from (1 or 2), and the embedded // generation parameters. Params is null when the source was a legacy v1 file. public int FormatVersion = 1; public BlueprintParams Params; // Water-bodies data (v2 WBID/WBTB/WSRF sections, terrain-water task 03). // Null / empty when the source carries no water sections (legacy v1, or a v2 // written before the water stage existed). Consumed by nothing at runtime yet. public ushort[,] WaterBodyIds; // 0 = no water, 1 = ocean, 2..N = lakes public List WaterBodies = new List(); public ushort[,] WaterSurfaceQ; // quantized levels — BlueprintFormat.DecodeWaterLevel // The curve that shaped HeightMap (v2 TCRV section); null = raw legacy profile. public TerrainCurveInfo TerrainCurve; } // 2. The Parser Utility public static class MapDataParser { public static WorldBlueprint LoadMapData(string seedStr) { string filePath = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat"); return LoadMapDataFromPath(filePath); } /// /// Loads a blueprint from an explicit absolute path. Same result as LoadMapData; /// exists so tooling (the round-trip harness) can load files that do not follow /// the seed-derived naming. /// public static WorldBlueprint LoadMapDataFromPath(string filePath) { if (!File.Exists(filePath)) { GD.PrintErr($"[MapDataParser] CRITICAL ERROR: Map file not found at {filePath}"); return null; } using (FileStream stream = File.OpenRead(filePath)) { using (BinaryReader reader = new BinaryReader(stream)) { // Format dispatch by the very first byte: v2 opens with raw "ISLA" // (0x49 'I'), v1 opens with 0x07 — the 7-bit length prefix of its // length-prefixed "ISLA_V1" string. Unambiguous by construction. int firstByte = stream.ReadByte(); stream.Position = 0; if (firstByte == 0x49) return LoadV2(reader); if (firstByte == 0x07) { GD.Print("[MapDataParser] ⚠ DEPRECATED: loading a legacy v1 blueprint — regenerate to produce v2."); return LoadV1(reader); } GD.PrintErr($"[MapDataParser] ERROR: {filePath} is neither a v2 nor a v1 blueprint (first byte 0x{firstByte:X2})."); return null; } } } // =============================================================== // v2 PARSE PATH — the tagged-section container. // Layout: Core/Scripts/BLUEPRINT_FORMAT.md. Constants: BlueprintFormat. // Reader rule: known tag -> parse, unknown tag -> skip by length. // =============================================================== private static WorldBlueprint LoadV2(BinaryReader reader) { Stream stream = reader.BaseStream; long fileLength = stream.Length; uint magic = reader.ReadUInt32(); if (magic != BlueprintFormat.MAGIC) { GD.PrintErr($"[MapDataParser] ERROR: bad v2 magic 0x{magic:X8} (expected \"ISLA\")."); return null; } uint version = reader.ReadUInt32(); if (version != BlueprintFormat.VERSION) { GD.PrintErr($"[MapDataParser] ERROR: unsupported blueprint format version {version} " + $"(this build reads version {BlueprintFormat.VERSION}). Refusing to load."); return null; } WorldBlueprint blueprint = new WorldBlueprint { FormatVersion = 2 }; var seenTags = new HashSet(); bool isFirstSection = true; while (stream.Position < fileLength) { if (fileLength - stream.Position < 12) { GD.PrintErr($"[MapDataParser] ERROR: truncated section header at offset {stream.Position}."); return null; } uint tag = reader.ReadUInt32(); ulong payloadLength = reader.ReadUInt64(); long payloadStart = stream.Position; if (payloadLength > (ulong)(fileLength - payloadStart)) { GD.PrintErr($"[MapDataParser] ERROR: section '{BlueprintFormat.TagToString(tag)}' declares " + $"{payloadLength} bytes but only {fileLength - payloadStart} remain. Truncated or corrupt file."); return null; } if (isFirstSection && tag != BlueprintFormat.TAG_PARAMS) { GD.PrintErr($"[MapDataParser] ERROR: first section is '{BlueprintFormat.TagToString(tag)}' — " + "the params section must come first."); return null; } isFirstSection = false; if (!seenTags.Add(tag)) { GD.PrintErr($"[MapDataParser] ERROR: duplicate section '{BlueprintFormat.TagToString(tag)}'."); return null; } bool sectionOk; if (tag == BlueprintFormat.TAG_PARAMS) sectionOk = ParseParams(reader, blueprint); else if (tag == BlueprintFormat.TAG_HEIGHTS) sectionOk = ParseHeights(reader, blueprint, payloadLength); else if (tag == BlueprintFormat.TAG_BIOMES) sectionOk = ParseBiomes(reader, blueprint, payloadLength); else if (tag == BlueprintFormat.TAG_TOWNS) sectionOk = ParseTowns(reader, blueprint); else if (tag == BlueprintFormat.TAG_ROADS_HIGHWAY) sectionOk = ParseRoadTier(reader, blueprint.Highways); else if (tag == BlueprintFormat.TAG_ROADS_BRANCH) sectionOk = ParseRoadTier(reader, blueprint.BranchRoads); else if (tag == BlueprintFormat.TAG_ROADS_RUGGED) sectionOk = ParseRoadTier(reader, blueprint.RuggedRoads); else if (tag == BlueprintFormat.TAG_ROADS_TRAIL) sectionOk = ParseRoadTier(reader, blueprint.TrailRoads); else if (tag == BlueprintFormat.TAG_WATER_BODY_IDS) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: false); else if (tag == BlueprintFormat.TAG_WATER_SURFACE) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: true); else if (tag == BlueprintFormat.TAG_WATER_BODY_TABLE) sectionOk = ParseWaterBodyTable(reader, blueprint); else if (tag == BlueprintFormat.TAG_TERRAIN_CURVE) sectionOk = ParseTerrainCurve(reader, blueprint); else { // The property the redesign exists to buy: future sections (water, // anything) are invisible to a reader that predates them. GD.Print($"[MapDataParser] Skipping unknown section '{BlueprintFormat.TagToString(tag)}' ({payloadLength} bytes)."); stream.Seek((long)payloadLength, SeekOrigin.Current); continue; } if (!sectionOk) return null; if (stream.Position != payloadStart + (long)payloadLength) { GD.PrintErr($"[MapDataParser] ERROR: section '{BlueprintFormat.TagToString(tag)}' parsed " + $"{stream.Position - payloadStart} bytes but declared {payloadLength}."); return null; } } if (blueprint.Params == null || blueprint.HeightMap == null || blueprint.BiomeMap == null) { GD.PrintErr("[MapDataParser] ERROR: v2 blueprint is missing a mandatory section (params/heights/biomes)."); return null; } if (!seenTags.Contains(BlueprintFormat.TAG_TOWNS)) GD.Print("[MapDataParser] ⚠ v2 blueprint has no towns section — town list is empty."); if (!seenTags.Contains(BlueprintFormat.TAG_ROADS_HIGHWAY) && !seenTags.Contains(BlueprintFormat.TAG_ROADS_BRANCH) && !seenTags.Contains(BlueprintFormat.TAG_ROADS_RUGGED) && !seenTags.Contains(BlueprintFormat.TAG_ROADS_TRAIL)) GD.Print("[MapDataParser] ⚠ v2 blueprint has no road sections — road lists are empty."); GD.Print($"[MapDataParser] Successfully loaded v2 Blueprint. Dimension: {blueprint.MapSize}x{blueprint.MapSize}, " + $"seed {blueprint.Params.WorldSeed}, generated {blueprint.Params.GeneratedUtc}."); return blueprint; } private static bool ParseParams(BinaryReader reader, WorldBlueprint blueprint) { var p = new BlueprintParams(); p.WorldSeed = reader.ReadInt32(); p.MapSize = reader.ReadInt32(); p.CraterRadius = reader.ReadSingle(); p.DensityMultiplier = reader.ReadSingle(); p.ImpactCenter = new Vector2(reader.ReadSingle(), reader.ReadSingle()); p.GeneratedUtc = reader.ReadString(); p.GeneratorGitHash = reader.ReadString(); if (p.MapSize < BlueprintFormat.MIN_MAP_SIZE || p.MapSize > BlueprintFormat.MAX_MAP_SIZE) { GD.PrintErr($"[MapDataParser] ERROR: MapSize {p.MapSize} outside sane bounds " + $"[{BlueprintFormat.MIN_MAP_SIZE}, {BlueprintFormat.MAX_MAP_SIZE}]. Refusing to allocate."); return false; } blueprint.Params = p; blueprint.MapSize = p.MapSize; return true; } private static bool ParseHeights(BinaryReader reader, WorldBlueprint blueprint, ulong payloadLength) { int n = blueprint.MapSize; if (payloadLength != 4UL * (ulong)n * (ulong)n) { GD.PrintErr($"[MapDataParser] ERROR: heights section is {payloadLength} bytes, expected {4UL * (ulong)n * (ulong)n}."); return false; } blueprint.HeightMap = new float[n, n]; for (int x = 0; x < n; x++) for (int y = 0; y < n; y++) blueprint.HeightMap[x, y] = reader.ReadSingle(); return true; } private static bool ParseBiomes(BinaryReader reader, WorldBlueprint blueprint, ulong payloadLength) { int n = blueprint.MapSize; if (payloadLength != (ulong)n * (ulong)n) { GD.PrintErr($"[MapDataParser] ERROR: biomes section is {payloadLength} bytes, expected {(ulong)n * (ulong)n}."); return false; } int biomeCount = System.Enum.GetValues().Length; blueprint.BiomeMap = new Biome[n, n]; for (int x = 0; x < n; x++) { for (int y = 0; y < n; y++) { byte ordinal = reader.ReadByte(); if (ordinal >= biomeCount) { GD.PrintErr($"[MapDataParser] ERROR: biome ordinal {ordinal} at ({x},{y}) is out of range " + $"(known biomes: 0..{biomeCount - 1}). Corrupt file or enum drift."); return false; } blueprint.BiomeMap[x, y] = (Biome)ordinal; } } return true; } private static bool ParseTowns(BinaryReader reader, WorldBlueprint blueprint) { int tierCount = System.Enum.GetValues().Length; int townCount = reader.ReadInt32(); for (int i = 0; i < townCount; i++) { TownLocation town = new TownLocation(); town.Position = new Vector2(reader.ReadSingle(), reader.ReadSingle()); byte tier = reader.ReadByte(); if (tier >= tierCount) { GD.PrintErr($"[MapDataParser] ERROR: town {i} tier ordinal {tier} is out of range " + $"(known tiers: 0..{tierCount - 1})."); return false; } town.Tier = (TownTier)tier; town.IsHighwayNode = reader.ReadByte() != 0; blueprint.Towns.Add(town); } return true; } private static bool ParseWaterGrid(BinaryReader reader, WorldBlueprint blueprint, ulong payloadLength, bool isSurface) { int n = blueprint.MapSize; string name = isSurface ? "WSRF" : "WBID"; if (payloadLength != 2UL * (ulong)n * (ulong)n) { GD.PrintErr($"[MapDataParser] ERROR: {name} section is {payloadLength} bytes, expected {2UL * (ulong)n * (ulong)n}."); return false; } ushort[,] grid = new ushort[n, n]; for (int x = 0; x < n; x++) for (int y = 0; y < n; y++) grid[x, y] = reader.ReadUInt16(); if (isSurface) blueprint.WaterSurfaceQ = grid; else blueprint.WaterBodyIds = grid; return true; } private static bool ParseWaterBodyTable(BinaryReader reader, WorldBlueprint blueprint) { int count = reader.ReadInt32(); for (int i = 0; i < count; i++) { var body = new WaterBodyInfo(); body.Id = reader.ReadUInt16(); body.Type = reader.ReadByte(); body.Salinity = reader.ReadByte(); body.SurfaceLevel = reader.ReadSingle(); body.PixelCount = reader.ReadInt32(); body.Centroid = new Vector2(reader.ReadSingle(), reader.ReadSingle()); if (body.Type > WaterBodyInfo.TYPE_LAKE) { GD.PrintErr($"[MapDataParser] ERROR: water body {i} has unknown type {body.Type}."); return false; } blueprint.WaterBodies.Add(body); } return true; } private static bool ParseTerrainCurve(BinaryReader reader, WorldBlueprint blueprint) { var c = new TerrainCurveInfo(); c.Version = reader.ReadUInt16(); c.T1 = reader.ReadSingle(); c.T2 = reader.ReadSingle(); c.T3 = reader.ReadSingle(); c.T4 = reader.ReadSingle(); c.SpikeMax = reader.ReadSingle(); c.Sea = reader.ReadSingle(); c.OrangeCeil = reader.ReadSingle(); c.RedCeil = reader.ReadSingle(); c.PlateauLo = reader.ReadSingle(); c.PlateauHi = reader.ReadSingle(); c.PeakCap = reader.ReadSingle(); c.TailSlope = reader.ReadSingle(); blueprint.TerrainCurve = c; return true; } private static bool ParseRoadTier(BinaryReader reader, List into) { int pathCount = reader.ReadInt32(); for (int i = 0; i < pathCount; i++) { int pathLength = reader.ReadInt32(); Vector2[] path = new Vector2[pathLength]; for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); } into.Add(path); } return true; } // =============================================================== // LEGACY v1 PARSE PATH — the original "ISLA_V1" positional format. // Kept intact and untouched; do not extend it. New content belongs // in the v2 tagged-section format. // =============================================================== private static WorldBlueprint LoadV1(BinaryReader reader) { WorldBlueprint blueprint = new WorldBlueprint(); { { // 1. Header Validation (CRITICAL: Must read string first to align bytes!) string header = reader.ReadString(); if (header != "ISLA_V1") { GD.PrintErr("[MapDataParser] ERROR: Invalid map version or corrupted file."); return null; } // 2. Read Map Dimensions & Initialize Arrays blueprint.MapSize = reader.ReadInt32(); blueprint.HeightMap = new float[blueprint.MapSize, blueprint.MapSize]; blueprint.BiomeMap = new Biome[blueprint.MapSize, blueprint.MapSize]; // 3. Read Raw Voxel Data (Height & Biome) for (int x = 0; x < blueprint.MapSize; x++) { for (int y = 0; y < blueprint.MapSize; y++) { blueprint.HeightMap[x, y] = reader.ReadSingle(); blueprint.BiomeMap[x, y] = (Biome)reader.ReadInt32(); } } // 4. Read Logistical Anchors (Towns & POIs) int townCount = reader.ReadInt32(); for (int i = 0; i < townCount; i++) { TownLocation town = new TownLocation(); town.Position = new Vector2(reader.ReadSingle(), reader.ReadSingle()); town.Tier = (TownTier)reader.ReadInt32(); blueprint.Towns.Add(town); } // 5. Read Road Networks // Highway int highwayCount = reader.ReadInt32(); for (int i = 0; i < highwayCount; i++) { int pathLength = reader.ReadInt32(); Vector2[] path = new Vector2[pathLength]; for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); } blueprint.Highways.Add(path); } // Branch int branchCount = reader.ReadInt32(); for (int i = 0; i < branchCount; i++) { int pathLength = reader.ReadInt32(); Vector2[] path = new Vector2[pathLength]; for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); } blueprint.BranchRoads.Add(path); } // Rugged int ruggedCount = reader.ReadInt32(); for (int i = 0; i < ruggedCount; i++) { int pathLength = reader.ReadInt32(); Vector2[] path = new Vector2[pathLength]; for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); } blueprint.RuggedRoads.Add(path); } // Trails int trailCount = reader.ReadInt32(); for (int i = 0; i < trailCount; i++) { int pathLength = reader.ReadInt32(); Vector2[] path = new Vector2[pathLength]; for (int p = 0; p < pathLength; p++) { path[p] = new Vector2(reader.ReadSingle(), reader.ReadSingle()); } blueprint.TrailRoads.Add(path); } } } GD.Print($"[MapDataParser] Successfully loaded v1 Blueprint. Dimension: {blueprint.MapSize}x{blueprint.MapSize}."); return blueprint; } } }