b451f83174
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>
229 lines
10 KiB
C#
229 lines
10 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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 <c>radius</c> tiles of (X, Y),
|
|
/// along with the raw point sequence. Intended for diagnosing drawing bugs
|
|
/// reported from the in-game debug overlay.
|
|
/// </summary>
|
|
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.X<minX)minX=p.X; if (p.Y<minY)minY=p.Y; if (p.X>maxX)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<Polyline> 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;
|
|
}
|
|
}
|