using Theriapolis.Core; using Theriapolis.Core.Util; using Theriapolis.Core.World; using Theriapolis.Core.World.Generation; using Theriapolis.Core.World.Polylines; namespace Theriapolis.Tools.Commands; /// /// tile-inspect --seed <n> --tile X,Y [--radius N] [--data-dir <dir>] /// /// Runs the full pipeline headless and prints every road / rail / river polyline /// and every bridge whose geometry passes within radius tiles of (X, Y), /// along with the raw point sequence. Intended for diagnosing drawing bugs /// reported from the in-game debug overlay. /// public static class TileInspect { public static int Run(string[] args) { ulong seed = 12345; string dataDir = ResolveDataDir(); int tx = -1, ty = -1; int radius = 3; bool dumpAll = false; int stopAtStage = -1; for (int i = 0; i < args.Length; i++) { switch (args[i].ToLowerInvariant()) { case "--dump-all": dumpAll = true; break; case "--stop-at-stage": if (i + 1 < args.Length && int.TryParse(args[++i], out int s)) stopAtStage = s; break; case "--seed": if (i + 1 < args.Length) { string raw = args[++i]; seed = raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? Convert.ToUInt64(raw[2..], 16) : ulong.Parse(raw); } break; case "--tile": if (i + 1 < args.Length) { var parts = args[++i].Split(','); if (parts.Length == 2 && int.TryParse(parts[0], out tx) && int.TryParse(parts[1], out ty)) { } } break; case "--radius": if (i + 1 < args.Length && int.TryParse(args[++i], out int r)) radius = r; break; case "--data-dir": if (i + 1 < args.Length) dataDir = args[++i]; break; } } if (tx < 0 || ty < 0) { Console.Error.WriteLine("tile-inspect: --tile X,Y is required"); return 1; } // Optional --crop-png outPath argument — writes a crop of the biome+feature map. string? cropPath = null; for (int i = 0; i < args.Length - 1; i++) if (args[i].Equals("--crop-png", StringComparison.OrdinalIgnoreCase)) cropPath = args[i + 1]; Console.WriteLine($"[tile-inspect] seed={seed} tile=({tx},{ty}) radius={radius}" + (stopAtStage >= 0 ? $" stopAtStage={stopAtStage}" : "")); var ctx = new WorldGenContext(seed, dataDir); if (stopAtStage >= 0) WorldGenerator.RunThrough(ctx, stopAtStage); else WorldGenerator.RunAll(ctx); var world = ctx.World; int px = C.WORLD_TILE_PIXELS; float rpx = radius * px; float rpxSq = rpx * rpx; Vec2 target = new(tx * px + px * 0.5f, ty * px + px * 0.5f); // ── Tile state ──────────────────────────────────────────────────────── Console.WriteLine(); Console.WriteLine("== Tile state =="); for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) { int nx = tx + dx, ny = ty + dy; if ((uint)nx >= C.WORLD_WIDTH_TILES || (uint)ny >= C.WORLD_HEIGHT_TILES) continue; ref var t = ref world.TileAt(nx, ny); Console.WriteLine($" ({nx,4},{ny,4}) biome={t.Biome,-20} flags={t.Features,-40} riverDir={DirName(t.RiverFlowDir)} railDir={DirName(t.RailDir)}"); } // ── Polylines passing near the tile ─────────────────────────────────── PrintPolylines("Rivers", world.Rivers, target, rpxSq, px, dumpAll); PrintPolylines("Roads", world.Roads, target, rpxSq, px, dumpAll); PrintPolylines("Rails", world.Rails, target, rpxSq, px, dumpAll); // ── River bounding boxes (to find rivers visible but not indexed near the tile) ── Console.WriteLine(); Console.WriteLine($"== All {world.Rivers.Count} rivers (bounding box in tiles) =="); foreach (var r in world.Rivers) { if (r.Points.Count < 2) continue; float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue; foreach (var p in r.Points) { if (p.XmaxX)maxX=p.X; if (p.Y>maxY)maxY=p.Y; } Console.WriteLine($" [id={r.Id}] class={r.RiverClassification} flow={r.FlowAccumulation} pts={r.Points.Count} tiles x=[{(int)(minX/px),4}..{(int)(maxX/px),4}] y=[{(int)(minY/px),4}..{(int)(maxY/px),4}] first=({r.Points[0].X:F0},{r.Points[0].Y:F0}) last=({r.Points[^1].X:F0},{r.Points[^1].Y:F0})"); } // ── Tiles flagged HasRiver near target, with source polyline ────────── Console.WriteLine(); Console.WriteLine($"== HasRiver tiles within {radius} of ({tx},{ty}) =="); int hits = 0; for (int yy = Math.Max(0, ty - radius); yy <= Math.Min(C.WORLD_HEIGHT_TILES - 1, ty + radius); yy++) for (int xx = Math.Max(0, tx - radius); xx <= Math.Min(C.WORLD_WIDTH_TILES - 1, tx + radius); xx++) { if ((world.Tiles[xx, yy].Features & FeatureFlags.HasRiver) != 0) { Console.WriteLine($" ({xx,4},{yy,4})"); hits++; } } if (hits == 0) Console.WriteLine(" (none)"); // ── Settlements within radius ──────────────────────────────────────── Console.WriteLine(); Console.WriteLine($"== Settlements within {radius} tiles =="); foreach (var s in world.Settlements) { int dt = Math.Max(Math.Abs(s.TileX - tx), Math.Abs(s.TileY - ty)); if (dt <= radius) Console.WriteLine($" id={s.Id,4} tier={s.Tier} poi={s.IsPoi} tile=({s.TileX,4},{s.TileY,4}) dt={dt}"); } // ── Bridges near the tile ───────────────────────────────────────────── Console.WriteLine(); Console.WriteLine($"== Bridges (all {world.Bridges.Count}) =="); foreach (var b in world.Bridges) { int btx = (int)(b.WorldPixelX / px); int bty = (int)(b.WorldPixelY / px); int dt = Math.Max(Math.Abs(btx - tx), Math.Abs(bty - ty)); if (dt <= radius + 2) Console.WriteLine($" roadId={b.RoadId,4} tile=({btx,4},{bty,4}) deck=({b.Start.X:F1},{b.Start.Y:F1})-({b.End.X:F1},{b.End.Y:F1})"); } return 0; } private static void PrintPolylines(string label, List polys, Vec2 target, float rpxSq, int px, bool dumpAll) { Console.WriteLine(); Console.WriteLine($"== {label} near tile =="); for (int pi = 0; pi < polys.Count; pi++) { var p = polys[pi]; if (p.Points.Count < 2) continue; // Find closest segment float bestSq = float.MaxValue; int bestIdx = -1; for (int i = 0; i < p.Points.Count - 1; i++) { float dSq = NearestPointOnSegment(target, p.Points[i], p.Points[i + 1], out _); if (dSq < bestSq) { bestSq = dSq; bestIdx = i; } } if (bestSq > rpxSq) continue; string endpointInfo = p.Type == PolylineType.Road ? $" from={p.FromSettlementId} to={p.ToSettlementId}" : ""; Console.WriteLine($" [id={p.Id}] type={p.Type} class={(p.Type == PolylineType.Road ? p.RoadClassification.ToString() : p.RiverClassification.ToString())} pts={p.Points.Count}{endpointInfo} closestSeg=#{bestIdx} dist={MathF.Sqrt(bestSq) / px:F2}tiles"); int ctxStart = dumpAll ? 0 : Math.Max(0, bestIdx - 2); int ctxEnd = dumpAll ? p.Points.Count - 1 : Math.Min(p.Points.Count - 1, bestIdx + 3); for (int i = ctxStart; i <= ctxEnd; i++) { int t0x = (int)(p.Points[i].X / px); int t0y = (int)(p.Points[i].Y / px); string marker = i == bestIdx ? " <-" : ""; Console.WriteLine($" pt[{i,3}] world=({p.Points[i].X,7:F1},{p.Points[i].Y,7:F1}) tile=({t0x,4},{t0y,4}){marker}"); } } } private static float NearestPointOnSegment(Vec2 p, Vec2 a, Vec2 b, out Vec2 nearest) { Vec2 ab = b - a; float lenSq = ab.LengthSquared; if (lenSq < 1e-8f) { nearest = a; return Vec2.DistSq(p, a); } float t = Math.Clamp(Vec2.Dot(p - a, ab) / lenSq, 0f, 1f); nearest = a + ab * t; return Vec2.DistSq(p, nearest); } private static string DirName(byte d) => d switch { Dir.None => "—", Dir.N => "N", Dir.NE => "NE", Dir.E => "E", Dir.SE => "SE", Dir.S => "S", Dir.SW => "SW", Dir.W => "W", Dir.NW => "NW", _ => $"?{d}", }; 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; } }