using System.Globalization; using System.Net.Http.Headers; using Ehwrj.Core.Services; namespace Ehwrj.Tools.Capture; internal static class Program { private static readonly Endpoint[] Endpoints = { new("map_info.json", "map_info.json", Required: true), new("map_obj.json", "map_obj.json", Required: true), new("map.img", "map.img", Required: true), new("state", "state.json", Required: false), new("hudmsg", "hudmsg.json", Required: false), new("gamechat", "gamechat.json", Required: false) }; private static async Task Main(string[] args) { var options = CaptureOptions.Parse(args); if (options.ShowHelp) { CaptureOptions.PrintHelp(); return 0; } if (!string.IsNullOrWhiteSpace(options.ValidateDirectory)) { return WriteCaptureReport(options.ValidateDirectory); } var captureDir = options.OutputDirectory ?? Path.Combine( "captures", DateTimeOffset.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)); try { LoopbackGuard.EnsureLoopbackHttp(options.BaseAddress, "--base-url"); } catch (ArgumentException ex) { Console.Error.WriteLine($"error: {ex.Message}"); return 1; } Directory.CreateDirectory(captureDir); using var client = new HttpClient { BaseAddress = options.BaseAddress, Timeout = TimeSpan.FromMilliseconds(options.TimeoutMs) }; client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("EhwrjCapture", "1.0")); Console.WriteLine($"Capturing War Thunder local API from {client.BaseAddress}"); Console.WriteLine($"Output: {Path.GetFullPath(captureDir)}"); var failures = 0; foreach (var endpoint in Endpoints) { var ok = await CaptureEndpointAsync(client, endpoint, captureDir).ConfigureAwait(false); if (!ok && endpoint.Required) { failures++; } } File.WriteAllText( Path.Combine(captureDir, "README.txt"), $""" Ehwrj local API capture Captured at: {DateTimeOffset.Now:O} Source: {client.BaseAddress} Files: - map_info.json - map_obj.json - map.img - state.json, hudmsg.json, gamechat.json when available Use with: scripts/run-local-api-stub.sh 8111 {Path.GetFullPath(captureDir)} """); WriteCaptureReport(captureDir); if (failures > 0) { Console.Error.WriteLine($"{failures} required endpoint(s) failed."); return 1; } Console.WriteLine("Capture complete."); return 0; } private static int WriteCaptureReport(string captureDir) { if (!Directory.Exists(captureDir)) { Console.Error.WriteLine($"error: capture directory not found: {captureDir}"); return 1; } var report = CaptureFixtureAnalyzer.AnalyzeDirectory(captureDir); var reportPath = Path.Combine(report.Directory, "capture-report.txt"); File.WriteAllText(reportPath, report.ToText()); Console.WriteLine($"report: {reportPath}"); if (report.Warnings.Count > 0) { Console.WriteLine($"{report.Warnings.Count} capture warning(s); see capture-report.txt."); } return report.HasRequiredFiles ? 0 : 1; } private static async Task CaptureEndpointAsync(HttpClient client, Endpoint endpoint, string captureDir) { try { using var response = await client.GetAsync(endpoint.Path).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { var level = endpoint.Required ? "error" : "skip"; Console.WriteLine($"{level}: {endpoint.Path} returned {(int)response.StatusCode}"); return false; } var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); var outputPath = Path.Combine(captureDir, endpoint.FileName); await File.WriteAllBytesAsync(outputPath, bytes).ConfigureAwait(false); Console.WriteLine($"saved: {endpoint.Path} -> {outputPath} ({bytes.Length} bytes)"); return true; } catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) { var level = endpoint.Required ? "error" : "skip"; Console.WriteLine($"{level}: {endpoint.Path} failed: {ex.Message}"); return false; } } } internal sealed record Endpoint(string Path, string FileName, bool Required); internal sealed record CaptureOptions(Uri BaseAddress, string? OutputDirectory, string? ValidateDirectory, int TimeoutMs, bool ShowHelp) { public static CaptureOptions Parse(IReadOnlyList args) { var baseAddress = new Uri("http://127.0.0.1:8111/"); string? output = null; string? validate = null; var timeoutMs = 1200; for (var i = 0; i < args.Count; i++) { switch (args[i]) { case "-h": case "--help": return new CaptureOptions(baseAddress, output, validate, timeoutMs, ShowHelp: true); case "--base-url" when i + 1 < args.Count && Uri.TryCreate(args[++i], UriKind.Absolute, out var uri): baseAddress = uri; break; case "--out" when i + 1 < args.Count: output = args[++i]; break; case "--validate" when i + 1 < args.Count: validate = args[++i]; break; case "--timeout-ms" when i + 1 < args.Count && int.TryParse(args[++i], NumberStyles.None, CultureInfo.InvariantCulture, out var parsed): timeoutMs = Math.Clamp(parsed, 200, 10_000); break; default: Console.Error.WriteLine($"Ignoring unknown argument: {args[i]}"); break; } } return new CaptureOptions(EnsureTrailingSlash(baseAddress), output, validate, timeoutMs, ShowHelp: false); } public static void PrintHelp() { Console.WriteLine(""" Usage: dotnet run --project tools/Ehwrj.Tools.Capture -- [--base-url http://127.0.0.1:8111/] [--out captures/name] [--timeout-ms 1200] dotnet run --project tools/Ehwrj.Tools.Capture -- --validate captures/name Captures War Thunder local map, telemetry, and message endpoints into a fixture directory. Writes capture-report.txt with parser coverage and replay readiness. """); } private static Uri EnsureTrailingSlash(Uri uri) { var text = uri.ToString(); return text.EndsWith('/') ? uri : new Uri($"{text}/"); } }