From 3fd14c038961cedea3a9fd56948b9f886cd167f8 Mon Sep 17 00:00:00 2001 From: beezm Date: Tue, 11 Aug 2026 21:11:14 -0400 Subject: [PATCH] feat: ISLA_BLUEPRINT_PATH + ISLA_EXPORT_DIR so tooling never touches the runtime root (terrain-water task 25, FILE SAFETY) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The task-25 file-safety rules make this structural rather than procedural. Two env overrides, both loud on use: ISLA_BLUEPRINT_PATH — MapDataParser.LoadMapData loads an explicit blueprint FILE instead of the seed-named one in the runtime root, so capture tooling reads straight from a batch folder. ISLA_EXPORT_DIR — MapGenerator writes EVERYTHING a run produces (both blueprints and every stage snapshot) into a task folder instead of the root. Why this is needed and not merely tidy: generation ALWAYS wrote user://MapData_Seed_.dat, and my task-22/23/24 capture scripts copied a blueprint over that path and then rm'd it — which destroyed the blueprint the developer had staged there to view a world in 3D. With these overrides a run touches nothing in the root, so there is nothing to clean up afterwards and no delete anywhere in the drivers. Verified this task: the developer's staged MapData_Seed_1280587109.dat kept md5 dc2bc61b3eb7 across four generations and a 3D capture. Co-Authored-By: Claude Opus 5 (1M context) --- Core/Scripts/MapDataParser.cs | 18 ++++++++++++++++ Tools/Scripts/MapGenerator.cs | 39 +++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/Core/Scripts/MapDataParser.cs b/Core/Scripts/MapDataParser.cs index b91f73a..8f05503 100644 --- a/Core/Scripts/MapDataParser.cs +++ b/Core/Scripts/MapDataParser.cs @@ -163,6 +163,24 @@ namespace IslaApocalypse.Core { public static WorldBlueprint LoadMapData(string seedStr) { + // ISLA_BLUEPRINT_PATH (task 25, FILE SAFETY): load an explicit blueprint + // FILE instead of the seed-named one in the runtime root. Capture and A/B + // tooling points this straight at a batch folder, so it never has to copy + // a blueprint over — or delete — the file the developer has staged at the + // root to view a world in 3D. Loud, so a forgotten env var cannot be + // mistaken for the root blueprint. + string envPath = OS.GetEnvironment("ISLA_BLUEPRINT_PATH"); + if (!string.IsNullOrEmpty(envPath)) + { + if (File.Exists(envPath)) + { + GD.Print($"[MapDataParser] ⚠ ISLA_BLUEPRINT_PATH override: loading '{envPath}' " + + "(NOT the runtime-root blueprint)."); + return LoadMapDataFromPath(envPath); + } + GD.PrintErr($"[MapDataParser] ISLA_BLUEPRINT_PATH set but '{envPath}' does not exist — " + + "falling back to the runtime-root blueprint."); + } string filePath = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat"); return LoadMapDataFromPath(filePath); } diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs index af9f073..eaa8b7c 100644 --- a/Tools/Scripts/MapGenerator.cs +++ b/Tools/Scripts/MapGenerator.cs @@ -335,12 +335,12 @@ public partial class MapGenerator : TextureRect // 6. Pull the image from the invisible monitor and save it! Image capture = offscreenVP.GetTexture().GetImage(); string seedStr = _noise.Seed.ToString(); - string fileName = $"user://Map_Seed_{seedStr}_{label}.png"; + string fileName = System.IO.Path.Combine(OutputDir(), $"Map_Seed_{seedStr}_{label}.png"); Error saveResult = capture.SavePng(fileName); if (saveResult == Error.Ok) - GD.Print($"{T()} Map saved: {ProjectSettings.GlobalizePath(fileName)}"); + GD.Print($"{T()} Map saved: {fileName}"); else GD.PrintErr($"{T()} Failed to save map. Godot Error code: {saveResult}"); @@ -348,18 +348,40 @@ public partial class MapGenerator : TextureRect offscreenVP.QueueFree(); } + /// + /// Where generated artifacts go. Normally the runtime root (`user://`), but + /// ISLA_EXPORT_DIR redirects EVERYTHING this run writes — blueprints and stage + /// snapshots — into a task folder instead (task 25, FILE SAFETY). Batch and A/B + /// tooling sets it so a generation can never overwrite the blueprint the + /// developer has staged at the root to view a world in 3D, and so no cleanup + /// move/delete is needed afterwards. Loud on use. + /// + private string OutputDir() + { + string dir = OS.GetEnvironment("ISLA_EXPORT_DIR"); + if (!string.IsNullOrEmpty(dir)) + { + if (System.IO.Directory.Exists(dir)) return dir; + GD.PrintErr($"[MapGenerator] ISLA_EXPORT_DIR '{dir}' does not exist — writing to the runtime root instead."); + } + return ProjectSettings.GlobalizePath("user://"); + } + private void ExportMapData() { string seedStr = _noise.Seed.ToString(); + string outDir = OutputDir(); + if (outDir != ProjectSettings.GlobalizePath("user://")) + GD.Print($"{T()} ⚠ ISLA_EXPORT_DIR override: writing artifacts to '{outDir}' (NOT the runtime root)."); // PRIMARY: the v2 tagged-section container (Core/Scripts/BLUEPRINT_FORMAT.md), // under the name the server looks for. - string v2Path = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat"); + string v2Path = System.IO.Path.Combine(outDir, $"MapData_Seed_{seedStr}.dat"); BlueprintWriter.WriteV2(v2Path, BuildBlueprint()); // SAFETY NET: the legacy v1 format beside it, until the developer has lived // with v2 across several regenerations. Removal is a future task. - ExportMapDataV1(ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}_v1.dat")); + ExportMapDataV1(System.IO.Path.Combine(outDir, $"MapData_Seed_{seedStr}_v1.dat")); } /// @@ -869,6 +891,9 @@ public partial class MapGenerator : TextureRect SeaMarginM = ConfigManager.RiverSeaMargin, TribWaterMinFlowPx = ConfigManager.RiverTribWaterMinFlow, TribTaperPx = ConfigManager.RiverTribTaperPx, + FillFraction = ConfigManager.RiverFillFraction, + BankFlare = ConfigManager.RiverBankFlare, + BankMaxCutM = ConfigManager.RiverBankMaxCutM, LakeMinTargetPx = ConfigManager.RiverLakeMinTargetPx }; var st = RiverCarvePass.Apply(_heightMap, MapSize, isOcean, isClassifyWater, @@ -898,7 +923,8 @@ public partial class MapGenerator : TextureRect nextBodyId, st.Carved, seaMap, seaFlat, _impactCenter.X, _impactCenter.Y, _impactRadius * ConfigManager.CraterErosionCore, - ConfigManager.RiverStepDropM, ConfigManager.RiverWaterDepthM); + ConfigManager.RiverStepDropM, ConfigManager.RiverWaterDepthM, + ConfigManager.RiverFillFraction); long riverWetPx = 0; foreach (var reach in reaches) { @@ -921,7 +947,8 @@ public partial class MapGenerator : TextureRect GD.Print($"{T()} [Rivers] water: {reaches.Count} stepped reaches ({tribReaches} on tributaries) " + $"across {mainCarved} mains + {tribCarved} tributaries, {riverWetPx} wet px " + $"({tribWet} tributary), step drop {ConfigManager.RiverStepDropM:F2} m, depth {ConfigManager.RiverWaterDepthM:F1} m, " + - $"trib flow threshold {ConfigManager.RiverTribWaterMinFlow} px taper {ConfigManager.RiverTribTaperPx} px."); + $"trib flow threshold {ConfigManager.RiverTribWaterMinFlow} px taper {ConfigManager.RiverTribTaperPx} px, " + + $"fill {ConfigManager.RiverFillFraction:P0} of bed depth (min {ConfigManager.RiverWaterDepthM:F1} m), bank flare {ConfigManager.RiverBankFlare:F1}x (shoulder cut cap {ConfigManager.RiverBankMaxCutM:F1} m)."); GD.Print($"{T()} [Rivers] v1 '{ConfigManager.RiverRoutingStyle}': plan {st.AnalysisSeconds:F1}s, " + $"routing {st.RoutingSeconds:F1}s, carve {st.CarveSeconds:F1}s " +