diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs
index adc69a5..5a0f655 100644
--- a/Tools/Scripts/MapGenerator.cs
+++ b/Tools/Scripts/MapGenerator.cs
@@ -17,6 +17,10 @@ public partial class MapGenerator : TextureRect
public bool IsHighwayNode;
}
+ // When generation began, in engine milliseconds. Every progress print is stamped with
+ // the time since this, so the cost of each stage is readable straight off the console.
+ private ulong _pipelineStartMs;
+
private FastNoiseLite _noise; // The one and only original detailed noise!
private Vector2 _impactCenter;
private float _impactRadius;
@@ -35,6 +39,8 @@ public partial class MapGenerator : TextureRect
public override async void _Ready()
{
+ _pipelineStartMs = Time.GetTicksMsec();
+
// 1. LOAD CONFIGURATION
ConfigManager.LoadConfig();
MapSize = ConfigManager.MapSize;
@@ -66,27 +72,71 @@ public partial class MapGenerator : TextureRect
float randomY = (float)GD.RandRange(0.05f, 0.12f);
_impactCenter = new Vector2(MapSize * randomX, MapSize * randomY);
+ // --- THE GENERATION PIPELINE ---
+ // Each stage prints its elapsed time and drops a snapshot, so you can watch the
+ // world appear in layers instead of waiting for the whole run to judge any of it.
+ // A future water stage slots in as another CaptureStage call at its own boundary.
+
GenerateTopography();
+ GD.Print($"{T()} Topography done (height + temperature).");
+
CalculateTrueOcean();
- CalculateMainland();
+ CalculateMainland();
+ GD.Print($"{T()} Ocean and mainland masks done.");
+
AssignBiomesAndDraw();
+ GD.Print($"{T()} Biomes done.");
+ await CaptureStage("1_biomes");
+
GenerateTowns();
+ GD.Print($"{T()} Towns placed: {_towns.Count}.");
+ await CaptureStage("2_towns");
// NEW: We await the roads so the engine doesn't freeze!
await GenerateRoadsAsync();
ExportMapData();
- SaveMapSnapshot();
+ GD.Print($"{T()} Blueprint exported.");
+
+ await CaptureStage("3_roads");
+ GD.Print($"{T()} GENERATION COMPLETE.");
}
- private async void SaveMapSnapshot()
+ ///
+ /// Elapsed time since generation started, e.g. "[+12.4s]". Prefixed onto the progress
+ /// prints so the cost of each stage can be read straight off the console — no stopwatch.
+ ///
+ private string T()
{
- // 1. THE FIX: Wait exactly one frame to let Godot finish _Ready() and unlock the Scene Tree!
+ double seconds = (Time.GetTicksMsec() - _pipelineStartMs) / 1000.0;
+ return $"[+{seconds,6:F1}s]";
+ }
+
+ ///
+ /// Saves a PNG of the map AS IT STANDS RIGHT NOW, tagged with a stage label.
+ ///
+ /// Call this at any pipeline boundary. It draws whatever exists at that moment — after
+ /// biomes you get bare terrain, after towns the town markers appear, after roads the
+ /// road network does. Nothing needs to be passed in or turned on; the draw proxy just
+ /// renders the lists as they currently are, so a future stage (water, for instance)
+ /// shows up automatically once it populates its data.
+ ///
+ /// ⚠ DO NOT "SIMPLIFY" THE WAITS BELOW. They look like superstition and are not — each
+ /// one is there because removing it produced blank or half-drawn images. The design
+ /// vault records why (01_Design/tooling_notes.md). Symptoms of getting this wrong are
+ /// postage-stamp, black, or road-less captures.
+ ///
+ private async Task CaptureStage(string label)
+ {
+ // 1. Yield one frame. Godot locks the scene tree while it is processing, and we are
+ // about to add nodes to it. Without this, the AddChild below is unsafe.
await ToSignal(GetTree(), "process_frame");
- GD.Print("Starting offscreen map capture...");
+ GD.Print($"{T()} Capturing snapshot: {label}...");
- // 2. Build the invisible, offscreen 4096 monitor
+ // 2. Build an invisible, offscreen "monitor" at the map's real size. It must be a
+ // SubViewport we size ourselves — rendering through the scene's UI layout makes
+ // Godot shrink the image to fit the physical screen.
var offscreenVP = new SubViewport();
offscreenVP.Size = new Vector2I(MapSize, MapSize);
offscreenVP.RenderTargetUpdateMode = SubViewport.UpdateMode.Always;
@@ -95,31 +145,33 @@ public partial class MapGenerator : TextureRect
// 3. Stamp the raw terrain texture onto it
var bgRect = new TextureRect();
- bgRect.Texture = this.Texture;
+ bgRect.Texture = this.Texture;
bgRect.CustomMinimumSize = new Vector2(MapSize, MapSize);
offscreenVP.AddChild(bgRect);
- // 4. Attach the proxy to draw the roads and towns on top
+ // 4. Attach the proxy to draw the roads and towns on top. Reading the viewport's
+ // texture alone would miss them — _Draw() output is not part of the base texture.
var drawProxy = new MapDrawProxy();
drawProxy.Source = this;
drawProxy.CustomMinimumSize = new Vector2(MapSize, MapSize);
offscreenVP.AddChild(drawProxy);
- // 5. WAIT FOR THE GPU! (Wait 2 render frames to guarantee the framebuffer is painted)
+ // 5. WAIT FOR THE GPU! Issuing draw calls is not the same as having drawn.
+ // Two frames — one proved insufficient in practice.
await ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw);
await ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw);
// 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}.png";
-
+ string fileName = $"user://Map_Seed_{seedStr}_{label}.png";
+
Error saveResult = capture.SavePng(fileName);
if (saveResult == Error.Ok)
- GD.Print($"Map saved successfully to: {ProjectSettings.GlobalizePath(fileName)}");
+ GD.Print($"{T()} Map saved: {ProjectSettings.GlobalizePath(fileName)}");
else
- GD.PrintErr($"Failed to save map. Godot Error code: {saveResult}");
+ GD.PrintErr($"{T()} Failed to save map. Godot Error code: {saveResult}");
// 7. Delete the invisible monitor to free up RAM
offscreenVP.QueueFree();
@@ -622,7 +674,7 @@ public partial class MapGenerator : TextureRect
// --- LOGISTICS & ROAD GENERATION ---
private async Task GenerateRoadsAsync()
{
- GD.Print($"[A*] Starting Logistics Pathfinding for {MapSize}x{MapSize} world...");
+ GD.Print($"{T()} [A*] Starting Logistics Pathfinding for {MapSize}x{MapSize} world...");
_highwayPaths.Clear(); _branchPaths.Clear(); _ruggedPaths.Clear(); _trailPaths.Clear();
AStarGrid2D astar = new AStarGrid2D();
@@ -645,7 +697,7 @@ public partial class MapGenerator : TextureRect
return angleA.CompareTo(angleB);
});
- GD.Print($"[A*] 1/4: Plotting the {highwayNodes.Count}-Node Continental Loop...");
+ GD.Print($"{T()} [A*] 1/4: Plotting the {highwayNodes.Count}-Node Continental Loop...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
if (highwayNodes.Count >= 3) {
@@ -668,7 +720,7 @@ public partial class MapGenerator : TextureRect
}
// --- 2. THE MOUNTAIN BOSS BRANCH ---
- GD.Print("[A*] 2/4: Connecting the Snow Boss...");
+ GD.Print($"{T()} [A*] 2/4: Connecting the Snow Boss...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
List connectedTowns = new List(highwayNodes);
@@ -700,7 +752,7 @@ public partial class MapGenerator : TextureRect
}
// --- 3 & 4. COUNTY ROADS (The Daisy-Chain Protocol) ---
- GD.Print("[A*] 3/4 & 4/4: Weaving County Roads (Daisy-Chaining)...");
+ GD.Print($"{T()} [A*] 3/4 & 4/4: Weaving County Roads (Daisy-Chaining)...");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE
List unconnectedTowns = _towns.FindAll(t => !connectedTowns.Contains(t) && t.Tier != TownTier.IslandLoot);
@@ -731,7 +783,7 @@ public partial class MapGenerator : TextureRect
float actualDist = Mathf.Sqrt(shortestDist);
// NEW: Live Diagnostics! Print the exact town we are testing BEFORE we calculate.
- GD.Print($" -> ({count}/{totalToConnect}) Pathing {bestUnconnected.Tier} to grid. Dist: {actualDist:F1}px");
+ GD.Print($"{T()} -> ({count}/{totalToConnect}) Pathing {bestUnconnected.Tier} to grid. Dist: {actualDist:F1}px");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // BREATHE!
Vector2[] countyPath = new Vector2[0];
@@ -739,7 +791,7 @@ public partial class MapGenerator : TextureRect
// THE ABANDON PROTOCOL: If it's more than 8% of the map away, it's a hermit camp. Leave it!
if (actualDist > (MapSize * 0.08f))
{
- GD.Print($" [!] Too isolated. Abandoning road.");
+ GD.Print($"{T()} [!] Too isolated. Abandoning road.");
}
else
{
@@ -759,14 +811,14 @@ public partial class MapGenerator : TextureRect
}
else if (actualDist <= (MapSize * 0.08f))
{
- GD.Print($" [!] Path impossible (Trapped by terrain). Abandoning road.");
+ GD.Print($"{T()} [!] Path impossible (Trapped by terrain). Abandoning road.");
}
unconnectedTowns.Remove(bestUnconnected);
connectedTowns.Add(bestUnconnected);
}
- GD.Print("[A*] Logistics Network Complete!");
+ GD.Print($"{T()} [A*] Logistics Network Complete!");
}
///