v2 tagged-section container (D-030): raw ISLA magic + u32 version gate, [u32 tag][u64 length][payload] sections — params (resolved generation inputs incl. impact centre), heights f32, biomes u8, towns with the highway-node flag, four road tiers identified by tag not position. - BlueprintFormat.cs: the single tag/constant registry. - BlueprintWriter.WriteV2: blueprint-typed, callable outside a generation run; generator and harness are both just callers. - ExportMapData dual-writes: v2 under the primary seed name, legacy v1 beside it as _v1.dat (safety net; removal is a future task). - MapDataParser: LoadMapDataFromPath entry point; v1 parse body extracted intact as LoadV1 (v2 reader lands in the next commit). - RoundTripHarness scene: load v1 -> write v2 -> load v2 -> semantic equality, headless, seconds per cycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
180 lines
No EOL
6.1 KiB
C#
180 lines
No EOL
6.1 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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
|
|
}
|
|
|
|
public class WorldBlueprint
|
|
{
|
|
public int MapSize;
|
|
public float[,] HeightMap;
|
|
public Biome[,] BiomeMap;
|
|
public List<TownLocation> Towns = new List<TownLocation>();
|
|
|
|
// All 4 road tiers!
|
|
public List<Vector2[]> Highways = new List<Vector2[]>();
|
|
public List<Vector2[]> BranchRoads = new List<Vector2[]>();
|
|
public List<Vector2[]> RuggedRoads = new List<Vector2[]>();
|
|
public List<Vector2[]> TrailRoads = new List<Vector2[]>();
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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))
|
|
{
|
|
return LoadV1(reader);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===============================================================
|
|
// 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;
|
|
}
|
|
}
|
|
} |