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!"); } ///