using System; using System.Collections.Generic; using System.IO; using System.Text.Json.Nodes; using System.Threading.Tasks; using Godot; namespace Chessistics.Scripts.Automation; /// /// Lives at the root of the scene tree when the game is launched with /// --automation=<dir>. Polls <dir>/inbox/ each frame for JSON command /// files, dispatches them, and writes results to <dir>/outbox/. /// public partial class AutomationHarness : Node { public string Root { get; } private readonly AutomationFacade _facade; private CommandDispatcher _dispatcher = null!; private bool _busy; private readonly HashSet _processed = new(StringComparer.Ordinal); internal AutomationHarness(string root, AutomationFacade facade) { Root = root; _facade = facade; Name = "AutomationHarness"; } public override void _Ready() { IpcFiles.EnsureDirs(Root); _dispatcher = new CommandDispatcher(this, _facade); GD.Print($"[Automation] Harness ready at {Root}"); var ready = new JsonObject { ["ready"] = true, ["pid"] = OS.GetProcessId(), ["godotVersion"] = (string)Godot.Engine.GetVersionInfo()["string"], ["viewportWidth"] = GetViewport().GetVisibleRect().Size.X, ["viewportHeight"] = GetViewport().GetVisibleRect().Size.Y, }; IpcFiles.AtomicWrite(Path.Combine(Root, IpcFiles.ReadyFile), ready.ToJsonString()); } public override void _Process(double delta) { if (_busy) return; var next = IpcFiles.NextInbox(Root, _processed); if (next == null) return; _processed.Add(next); _busy = true; _ = ProcessCommandAsync(next); } private async Task ProcessCommandAsync(string inboxPath) { string id = Path.GetFileNameWithoutExtension(inboxPath); JsonObject envelope; try { var text = await ReadAllTextRetry(inboxPath); envelope = (JsonObject)JsonNode.Parse(text)!; id = envelope["id"]?.GetValue() ?? id; } catch (Exception ex) { GD.PrintErr($"[Automation] Cannot parse {inboxPath}: {ex.Message}"); _busy = false; return; } var response = new JsonObject { ["id"] = id }; try { var cmd = envelope["cmd"]!.GetValue(); var args = envelope["args"]?.AsObject() ?? new JsonObject(); GD.Print($"[Automation] → {cmd}"); var result = await _dispatcher.Dispatch(cmd, args); response["ok"] = true; response["result"] = result; } catch (Exception ex) { GD.PrintErr($"[Automation] Error on {inboxPath}: {ex}"); response["ok"] = false; response["error"] = ex.Message; } try { IpcFiles.AtomicWrite(IpcFiles.OutboxPath(Root, id), response.ToJsonString()); } catch (Exception ex) { GD.PrintErr($"[Automation] Cannot write outbox for {id}: {ex}"); } try { if (File.Exists(inboxPath)) File.Delete(inboxPath); } catch { /* best effort */ } _busy = false; } /// Awaiting a signal from C#; wrapper so dispatcher can use it. internal SignalAwaiter ToSignalAsync(GodotObject source, string signal) => ToSignal(source, signal); /// Called via CallDeferred by the quit command. public void RequestQuit() => GetTree().Quit(); private static async Task ReadAllTextRetry(string path) { // The agent writes .tmp→rename atomically, but a brief race can still occur. for (int i = 0; i < 5; i++) { try { return File.ReadAllText(path); } catch (IOException) { await Task.Delay(20); } } return File.ReadAllText(path); } }