using Theriapolis.Core; using Theriapolis.Core.World; using Theriapolis.Core.World.Generation; namespace Theriapolis.Tools.Commands; /// /// settlement-report --seed <n> [--data-dir <dir>] /// /// Runs the full pipeline and prints a human-readable settlement report: /// narrative anchors, tier breakdown, economy distribution, and PoI list. /// public static class SettlementReport { public static int Run(string[] args) { ulong seed = 12345; string dataDir = ResolveDataDir(); 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 "--data-dir": if (i + 1 < args.Length) dataDir = args[++i]; break; } } Console.WriteLine($"[settlement-report] seed=0x{seed:X} 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, _) => Console.Write($"\r Running {name,-28} "), Log = _ => { }, // suppress detailed logs }; WorldGenerator.RunAll(ctx); Console.WriteLine("\r "); var world = ctx.World; var ss = world.Settlements; if (ss.Count == 0) { Console.WriteLine("No settlements generated."); return 0; } // ── Narrative anchors ───────────────────────────────────────────────── Console.WriteLine("═══════════════════════════════════════════════════════"); Console.WriteLine(" NARRATIVE ANCHORS"); Console.WriteLine("═══════════════════════════════════════════════════════"); foreach (var s in ss.Where(s => s.Anchor.HasValue).OrderBy(s => s.Anchor)) { Console.WriteLine($" [{s.Anchor}]"); Console.WriteLine($" Name : {s.Name}"); Console.WriteLine($" Tier : {s.Tier}"); Console.WriteLine($" Position : ({s.TileX}, {s.TileY})"); Console.WriteLine($" Economy : {s.Economy}"); Console.WriteLine($" Wealth : {s.WealthLevel:F3}"); Console.WriteLine($" Pop : ~{s.Population}"); Console.WriteLine($" River : {s.IsOnRiver}"); Console.WriteLine($" Rail : {s.HasRailStation}"); Console.WriteLine(); } // ── Tier breakdown ──────────────────────────────────────────────────── Console.WriteLine("═══════════════════════════════════════════════════════"); Console.WriteLine(" SETTLEMENTS BY TIER"); Console.WriteLine("═══════════════════════════════════════════════════════"); for (int tier = 1; tier <= 4; tier++) { var ts = ss.Where(s => s.Tier == tier && !s.IsPoi).ToList(); if (ts.Count == 0) continue; Console.WriteLine($" Tier {tier} ({ts.Count}):"); foreach (var s in ts.OrderBy(s => s.Name)) { string anchor = s.Anchor.HasValue ? $" [{s.Anchor}]" : ""; Console.WriteLine($" ({s.TileX,4},{s.TileY,4}) {s.Name,-24}{anchor}"); Console.WriteLine($" Economy={s.Economy,-14} Gov={s.Governance,-16} Wealth={s.WealthLevel:F2}"); } } Console.WriteLine(); // ── Economy distribution ────────────────────────────────────────────── Console.WriteLine("═══════════════════════════════════════════════════════"); Console.WriteLine(" ECONOMY DISTRIBUTION (Tier 1-4)"); Console.WriteLine("═══════════════════════════════════════════════════════"); var econGroups = ss.Where(s => !s.IsPoi) .GroupBy(s => s.Economy) .OrderByDescending(g => g.Count()); foreach (var g in econGroups) Console.WriteLine($" {g.Key,-18}: {g.Count(),3}"); Console.WriteLine(); // ── PoI list ────────────────────────────────────────────────────────── var pois = ss.Where(s => s.IsPoi).ToList(); if (pois.Count > 0) { Console.WriteLine("═══════════════════════════════════════════════════════"); Console.WriteLine($" POINTS OF INTEREST ({pois.Count})"); Console.WriteLine("═══════════════════════════════════════════════════════"); foreach (var p in pois.OrderBy(p => p.PoiType).ThenBy(p => p.TileX)) Console.WriteLine($" ({p.TileX,4},{p.TileY,4}) {p.Name,-24} [{p.PoiType}]"); Console.WriteLine(); } // ── Linear features ──────────────────────────────────────────────────── Console.WriteLine("═══════════════════════════════════════════════════════"); Console.WriteLine(" LINEAR FEATURES"); Console.WriteLine("═══════════════════════════════════════════════════════"); Console.WriteLine($" Rivers : {world.Rivers.Count}"); Console.WriteLine($" Roads : {world.Roads.Count}"); Console.WriteLine($" Rails : {world.Rails.Count}"); // ── Validation summary ───────────────────────────────────────────────── if (world.StageHashes.TryGetValue("ValidationPass", out ulong vhash)) { int violations = (int)(vhash / 1000); int warnings = (int)(vhash % 1000); Console.WriteLine(); Console.WriteLine("═══════════════════════════════════════════════════════"); Console.WriteLine(" VALIDATION"); Console.WriteLine("═══════════════════════════════════════════════════════"); Console.WriteLine($" Violations : {violations}"); Console.WriteLine($" Warnings : {warnings}"); } return 0; } 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; } }