Files
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

354 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Generation;
using Theriapolis.Core.World.Generation.Stages;
using Theriapolis.Core.World.Polylines;
namespace Theriapolis.Tools.Commands;
/// <summary>
/// worldgen-dump --seed &lt;n&gt; --out &lt;file.png&gt; [--data-dir &lt;dir&gt;]
///
/// Runs the full pipeline headless and exports a PNG of the biome map overlaid
/// with rivers (blue), roads (tan), rail (dark), and settlement icons.
/// </summary>
public static class WorldgenDump
{
public static int Run(string[] args)
{
ulong seed = 12345;
string outPath = "world.png";
string dataDir = ResolveDataDir();
bool showViolations = false;
for (int i = 0; i < args.Length; i++)
{
switch (args[i].ToLowerInvariant())
{
case "--seed":
if (i + 1 < args.Length)
{
string raw = args[++i];
if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
seed = Convert.ToUInt64(raw[2..], 16);
else
seed = ulong.Parse(raw);
}
break;
case "--out":
if (i + 1 < args.Length) outPath = args[++i];
break;
case "--data-dir":
if (i + 1 < args.Length) dataDir = args[++i];
break;
case "--show-violations":
showViolations = true;
break;
}
}
Console.WriteLine($"[worldgen-dump] seed=0x{seed:X} out={outPath} data-dir={dataDir}");
if (!Directory.Exists(dataDir))
{
Console.Error.WriteLine($"Data directory not found: {dataDir}");
return 1;
}
var ctx = new WorldGenContext(seed, dataDir)
{
ProgressCallback = (name, frac) =>
Console.Write($"\r {name,-28} {frac * 100f:F0}% "),
Log = msg => Console.WriteLine(msg),
};
WorldGenerator.RunAll(ctx);
Console.WriteLine();
// Collect biome colours from the loaded BiomeDef array
var colorMap = BuildColorMap(ctx.World.BiomeDefs!);
int W = C.WORLD_WIDTH_TILES;
int H = C.WORLD_HEIGHT_TILES;
using var image = new Image<Rgb24>(W, H);
// ── 1. Biome base layer ───────────────────────────────────────────────
for (int ty = 0; ty < H; ty++)
for (int tx = 0; tx < W; tx++)
{
var biome = ctx.World.Tiles[tx, ty].Biome;
if (!colorMap.TryGetValue(biome, out var px))
px = new Rgb24(255, 0, 255);
image[tx, ty] = px;
}
if (showViolations)
OverlayViolations(image, ctx, W, H);
// ── 2. Roads ─────────────────────────────────────────────────────────
foreach (var road in ctx.World.Roads)
{
var color = road.RoadClassification switch
{
RoadType.Highway => new Rgb24(210, 180, 80),
RoadType.PostRoad => new Rgb24(180, 155, 70),
_ => new Rgb24(150, 130, 90),
};
DrawPolyline(image, road, color, W, H);
}
// ── 3. Rivers ─────────────────────────────────────────────────────────
foreach (var river in ctx.World.Rivers)
{
var color = river.RiverClassification switch
{
RiverClass.MajorRiver => new Rgb24(40, 100, 200),
RiverClass.River => new Rgb24(60, 120, 200),
_ => new Rgb24(100, 150, 220),
};
DrawPolyline(image, river, color, W, H);
}
// ── 4. Rail ───────────────────────────────────────────────────────────
var railColor = new Rgb24(80, 65, 50);
foreach (var rail in ctx.World.Rails)
DrawPolyline(image, rail, railColor, W, H);
// ── 4b. Bridges ──────────────────────────────────────────────────────
var bridgeColor = new Rgb24(160, 140, 100);
foreach (var bridge in ctx.World.Bridges)
{
int bx = (int)(bridge.WorldPixelX / C.WORLD_TILE_PIXELS);
int by = (int)(bridge.WorldPixelY / C.WORLD_TILE_PIXELS);
// Draw a small cross at the bridge location
for (int d = -1; d <= 1; d++)
{
int px = bx + d, py = by;
if ((uint)px < (uint)W && (uint)py < (uint)H) image[px, py] = bridgeColor;
px = bx; py = by + d;
if ((uint)px < (uint)W && (uint)py < (uint)H) image[px, py] = bridgeColor;
}
}
// ── 5. Settlements ────────────────────────────────────────────────────
foreach (var s in ctx.World.Settlements)
{
var (color, radius) = s.Tier switch
{
1 => (new Rgb24(255, 215, 0), 4), // gold, capital
2 => (new Rgb24(230, 230, 230), 3), // white, city
3 => (new Rgb24(150, 200, 255), 2), // blue, town
4 => (new Rgb24(200, 200, 200), 1), // grey, village
_ => (new Rgb24(200, 60, 60), 1), // red, PoI
};
DrawDot(image, s.TileX, s.TileY, color, radius, W, H);
}
// ── 6. Biome coverage stats ────────────────────────────────────────────
PrintBiomeCoverage(ctx, W, H);
// ── 7. Settlement summary ─────────────────────────────────────────────
PrintSettlementSummary(ctx);
image.Save(outPath);
Console.WriteLine($"[worldgen-dump] Saved {outPath} ({W}×{H} px)");
return 0;
}
private static void DrawPolyline(Image<Rgb24> img, Polyline poly, Rgb24 color, int W, int H)
{
var pts = poly.Points;
if (pts.Count < 2) return;
for (int i = 0; i < pts.Count - 1; i++)
{
int x0 = (int)(pts[i].X / C.WORLD_TILE_PIXELS);
int y0 = (int)(pts[i].Y / C.WORLD_TILE_PIXELS);
int x1 = (int)(pts[i + 1].X / C.WORLD_TILE_PIXELS);
int y1 = (int)(pts[i + 1].Y / C.WORLD_TILE_PIXELS);
BresenhamLine(img, x0, y0, x1, y1, color, W, H);
}
}
private static void BresenhamLine(Image<Rgb24> img, int x0, int y0, int x1, int y1, Rgb24 color, int W, int H)
{
int dx = Math.Abs(x1 - x0), dy = Math.Abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
while (true)
{
if ((uint)x0 < (uint)W && (uint)y0 < (uint)H)
img[x0, y0] = color;
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
}
private static void DrawDot(Image<Rgb24> img, int cx, int cy, Rgb24 color, int radius, int W, int H)
{
for (int dy = -radius; dy <= radius; dy++)
for (int dx = -radius; dx <= radius; dx++)
{
if (dx * dx + dy * dy > radius * radius) continue;
int nx = cx + dx, ny = cy + dy;
if ((uint)nx < (uint)W && (uint)ny < (uint)H)
img[nx, ny] = color;
}
}
private static void OverlayViolations(Image<Rgb24> image, WorldGenContext ctx, int W, int H)
{
var viols = BorderDistortionGenStage.FindStraightViolations(ctx);
var byLen = viols.OrderByDescending(v => v.len).ToList();
Console.WriteLine($"[worldgen-dump] Total straight-run violations: {viols.Count}");
var buckets = new (int min, int max, int count)[]
{
(6, 7, 0), (8, 9, 0), (10, 11, 0), (12, 14, 0),
(15, 19, 0), (20, 29, 0), (30, 49, 0), (50, 99, 0),
};
foreach (var v in viols)
{
for (int i = 0; i < buckets.Length; i++)
{
if (v.len >= buckets[i].min && v.len <= buckets[i].max)
{
buckets[i].count++;
break;
}
}
}
Console.WriteLine($"[worldgen-dump] Run length distribution:");
foreach (var b in buckets)
Console.WriteLine($" len {b.min,2}-{b.max,2}: {b.count,5}");
Console.WriteLine($"[worldgen-dump] Top 25 longest runs:");
for (int i = 0; i < Math.Min(25, byLen.Count); i++)
{
var v = byLen[i];
string orient = (v.dx, v.dy) switch
{
(1, 0) => "→",
(0, 1) => "↓",
(1, 1) => "↘",
(1,-1) => "↗",
_ => "?",
};
Console.WriteLine($" #{i+1,2} ({v.x,4},{v.y,4}) {orient} len={v.len}");
}
var red = new Rgb24(255, 0, 0);
int overlayCount = Math.Min(50, byLen.Count);
for (int i = 0; i < overlayCount; i++)
{
var v = byLen[i];
int px = v.x, py = v.y;
for (int step = 0; step < v.len; step++)
{
if ((uint)px < (uint)W && (uint)py < (uint)H)
image[px, py] = red;
px += v.dx;
py += v.dy;
}
}
}
private static void PrintBiomeCoverage(WorldGenContext ctx, int W, int H)
{
var biomeCounts = new Dictionary<BiomeId, int>();
int total = W * H;
for (int ty = 0; ty < H; ty++)
for (int tx = 0; tx < W; tx++)
{
var b = ctx.World.Tiles[tx, ty].Biome;
biomeCounts.TryGetValue(b, out int c);
biomeCounts[b] = c + 1;
}
int oceanCount = biomeCounts.GetValueOrDefault(BiomeId.Ocean);
int landTotal = total - oceanCount;
Console.WriteLine($"[worldgen-dump] Biome coverage: ocean={100.0 * oceanCount / total:F1}%, land tiles={landTotal}");
foreach (var kv in biomeCounts.OrderByDescending(kv => kv.Value))
{
if (kv.Key == BiomeId.Ocean) continue;
double pct = landTotal > 0 ? 100.0 * kv.Value / landTotal : 0;
Console.WriteLine($" {kv.Key,-22} {pct,6:F2}% ({kv.Value} tiles)");
}
}
private static void PrintSettlementSummary(WorldGenContext ctx)
{
var ss = ctx.World.Settlements;
if (ss.Count == 0) return;
Console.WriteLine($"[worldgen-dump] Settlements: {ss.Count} total");
for (int tier = 1; tier <= 4; tier++)
{
var ts = ss.Where(s => s.Tier == tier && !s.IsPoi).ToList();
Console.WriteLine($" Tier {tier}: {ts.Count}");
foreach (var s in ts)
Console.WriteLine($" [{s.TileX,4},{s.TileY,4}] {s.Name,-24} {s.Economy,-14} wealth={s.WealthLevel:F2}");
}
int poiCount = ss.Count(s => s.IsPoi);
Console.WriteLine($" PoIs: {poiCount}");
Console.WriteLine($"[worldgen-dump] Rivers: {ctx.World.Rivers.Count}, Roads: {ctx.World.Roads.Count}, Rails: {ctx.World.Rails.Count}, Bridges: {ctx.World.Bridges.Count}");
}
private static Dictionary<BiomeId, Rgb24> BuildColorMap(BiomeDef[] defs)
{
var map = new Dictionary<BiomeId, Rgb24>();
foreach (var def in defs)
{
var biomeId = ParseBiomeId(def.Id);
var (r, g, b) = def.ParsedColor();
map[biomeId] = new Rgb24(r, g, b);
}
return map;
}
private static BiomeId ParseBiomeId(string id) => id.ToLowerInvariant() switch
{
"ocean" => BiomeId.Ocean,
"tundra" => BiomeId.Tundra,
"boreal" => BiomeId.Boreal,
"temperate_deciduous" => BiomeId.TemperateDeciduous,
"temperate_grassland" => BiomeId.TemperateGrassland,
"mountain_alpine" => BiomeId.MountainAlpine,
"mountain_forested" => BiomeId.MountainForested,
"subtropical_forest" => BiomeId.SubtropicalForest,
"wetland" => BiomeId.Wetland,
"coastal" => BiomeId.Coastal,
"river_valley" => BiomeId.RiverValley,
"scrubland" => BiomeId.Scrubland,
"desert_cold" => BiomeId.DesertCold,
"forest_edge" => BiomeId.ForestEdge,
"foothills" => BiomeId.Foothills,
"marsh_edge" => BiomeId.MarshEdge,
"beach" => BiomeId.Beach,
"cliff" => BiomeId.Cliff,
"tidal_flat" => BiomeId.TidalFlat,
"mangrove" => BiomeId.Mangrove,
_ => BiomeId.TemperateGrassland,
};
private static string ResolveDataDir()
{
string local = Path.Combine(AppContext.BaseDirectory, "Data");
if (Directory.Exists(local)) return local;
string? dir = AppContext.BaseDirectory.TrimEnd(
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
for (int i = 0; i < 6; i++)
{
if (dir is null) break;
string candidate = Path.Combine(dir, "Content", "Data");
if (Directory.Exists(candidate)) return candidate;
dir = Path.GetDirectoryName(dir);
}
return local;
}
}