feat: timestamped generation progress + staged snapshots (biomes/towns/roads)
Two additions, both observability only — no generation, routing, weight, terrain or mesher behaviour changes. 1. Every progress print is now prefixed with elapsed time since generation started, e.g. [+ 142.7s], including a line per pipeline stage and per county road. The per-stage split (setup / continental loop / branch / county) can now be read straight off the console. Two A* fix attempts have been reasoned from structure rather than measurement; this makes the measurement free. 2. The end-of-run PNG capture was extracted into a reusable stage capture that can be called at any pipeline boundary. Snapshots are now taken after biomes, after towns, and after roads. The draw proxy renders whatever exists at the time, so stages that have not run simply do not appear — and a future water stage will show up automatically once it populates its data. The existing capture path is reused unchanged: same offscreen SubViewport, same draw proxy, same frame-yield plus double GPU wait. Those waits are load-bearing, not superstition, and each staged capture honours them. Snapshot filenames gain a stage suffix: Map_Seed_<seed>_1_biomes.png, _2_towns.png, _3_roads.png. Nothing reads these but a human, verified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e070bbc9da
commit
ad541dfbd8
1 changed files with 73 additions and 21 deletions
|
|
@ -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();
|
||||
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()
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
|
@ -99,27 +149,29 @@ public partial class MapGenerator : TextureRect
|
|||
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<TownData> connectedTowns = new List<TownData>(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<TownData> 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!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Reference in a new issue