using System; using System.IO; using System.Threading.Tasks; using System.Text.Json.Nodes; using Godot; using System.Linq; using Chessistics.Engine.Model; using Chessistics.Scripts.Input; namespace Chessistics.Scripts.Automation; /// /// Maps command strings → handlers. Each handler returns the "result" payload; /// the harness wraps it in the envelope. /// internal class CommandDispatcher { private readonly AutomationHarness _harness; private readonly AutomationFacade _facade; public CommandDispatcher(AutomationHarness harness, AutomationFacade facade) { _harness = harness; _facade = facade; } public async Task Dispatch(string cmd, JsonObject args) { return cmd switch { "screenshot" => await Screenshot(args), "get_state" => GetState(), "select_piece" => SelectPiece(args), "place" => Place(args), "click_cell" => ClickCell(args), "key" => Key(args), "play" => Play(), "pause" => Pause(), "step" => await Step(), "wait_idle" => await WaitIdle(args), "set_speed" => SetSpeed(args), "load_mission" => LoadMission(args), "back_to_menu" => BackToMenu(), "quit" => Quit(), _ => throw new InvalidOperationException($"Unknown command: {cmd}"), }; } // --- handlers --- private async Task Screenshot(JsonObject args) { var name = args["name"]?.GetValue() ?? "screenshot"; await _harness.ToSignalAsync(RenderingServer.Singleton, "frame_post_draw"); var image = _harness.GetViewport().GetTexture().GetImage(); var path = IpcFiles.ScreenshotPath(_harness.Root, name); var err = image.SavePng(path); if (err != Error.Ok) throw new InvalidOperationException($"SavePng failed: {err}"); var result = new JsonObject { ["path"] = Path.GetRelativePath(_harness.Root, path).Replace('\\', '/'), ["abs_path"] = path, ["width"] = image.GetWidth(), ["height"] = image.GetHeight(), }; return result; } private JsonNode? GetState() { var sim = _facade.Sim(); if (sim == null) return new JsonObject { ["loaded"] = false }; var snap = sim.GetSnapshot(); var obj = SnapshotSerializer.Serialize(snap); obj["loaded"] = true; obj["animating"] = _facade.Animator.IsAnimating; return obj; } private JsonNode? SelectPiece(JsonObject args) { var kind = ParsePieceKind(args["kind"]); _facade.Stock.SimulateSelect(kind); return new JsonObject { ["phase"] = _facade.Input.CurrentPhase.ToString(), ["kind"] = kind.ToString(), }; } private JsonNode? Place(JsonObject args) { var sim = _facade.Sim(); if (sim == null) throw new InvalidOperationException("No simulation loaded."); var kind = ParsePieceKind(args["kind"]); var start = ParseCoords(args["start"]); var end = ParseCoords(args["end"]); // Snapshot ids before so we can identify the new piece after. var before = sim.GetSnapshot(); var idsBefore = new HashSet(before.Pieces.Select(p => p.Id)); // Route through the UI signal — this mutates the sim exactly once and // runs the same path a human click would: HandleEditEvents, stock refresh, // SetSnapshot on InputMapper. _facade.Input.EmitSignal( InputMapper.SignalName.PlacementRequested, (int)kind, start.Col, start.Row, end.Col, end.Row); var after = sim.GetSnapshot(); var newPiece = after.Pieces.FirstOrDefault(p => !idsBefore.Contains(p.Id)); var placed = newPiece != null; return new JsonObject { ["placed"] = placed, ["pieceId"] = newPiece?.Id, ["reason"] = placed ? null : "Placement rejected (check Godot console log for PlacementRejectedEvent).", }; } private JsonNode? ClickCell(JsonObject args) { var col = args["col"]!.GetValue(); var row = args["row"]!.GetValue(); var button = args["button"]?.GetValue()?.ToLowerInvariant() ?? "left"; var mouseBtn = button switch { "right" => MouseButton.Right, _ => MouseButton.Left, }; _facade.Input.SimulateClick(new Coords(col, row), mouseBtn); return new JsonObject { ["phase"] = _facade.Input.CurrentPhase.ToString(), }; } private JsonNode? Key(JsonObject args) { var key = args["key"]!.GetValue(); switch (key.ToLowerInvariant()) { case "space": _facade.TogglePlayPause(); break; case "escape": case "esc": _facade.Input.Cancel(); break; default: throw new InvalidOperationException($"Unsupported key: {key}"); } return new JsonObject(); } private JsonNode? Play() { _facade.Play(); return PhaseInfo(); } private JsonNode? Pause() { _facade.Pause(); return PhaseInfo(); } private async Task Step() { _facade.Step(); // Dispatcher auto-waits for idle before writing the result. await WaitIdleInternal(timeoutMs: 10000); return PhaseInfo(); } private async Task WaitIdle(JsonObject args) { var timeout = args["timeoutMs"]?.GetValue() ?? 10000; var reached = await WaitIdleInternal(timeout); var info = PhaseInfo()!.AsObject(); info["idle"] = reached; return info; } internal async Task WaitIdleInternal(int timeoutMs) { var elapsed = 0.0; while (_facade.Animator.IsAnimating) { await _harness.ToSignalAsync(_harness.GetTree(), "process_frame"); elapsed += 1.0 / 60.0 * 1000.0; if (elapsed > timeoutMs) return false; } return true; } private JsonNode? SetSpeed(JsonObject args) { var interval = (float)args["interval"]!.GetValue(); _facade.SetSpeed(interval); return new JsonObject { ["interval"] = interval }; } private JsonNode? LoadMission(JsonObject args) { var campaign = args["campaign"]?.GetValue() ?? "campaign_01"; var index = args["missionIndex"]?.GetValue() ?? 0; _facade.LoadMission(campaign, index); var sim = _facade.Sim(); if (sim == null) return new JsonObject { ["loaded"] = false }; return SnapshotSerializer.Serialize(sim.GetSnapshot()); } private JsonNode? BackToMenu() { _facade.BackToMenu(); return new JsonObject(); } private JsonNode? Quit() { // Defer so we can write the result first. _harness.CallDeferred(nameof(AutomationHarness.RequestQuit)); return new JsonObject { ["quitting"] = true }; } // --- helpers --- private JsonNode PhaseInfo() { var sim = _facade.Sim(); var obj = new JsonObject(); if (sim != null) { var snap = sim.GetSnapshot(); obj["phase"] = snap.Phase.ToString(); obj["turn"] = snap.TurnNumber; obj["missionIndex"] = snap.Campaign?.CurrentMissionIndex; } obj["animating"] = _facade.Animator.IsAnimating; return obj; } private static PieceKind ParsePieceKind(JsonNode? node) { if (node == null) throw new InvalidOperationException("Missing 'kind'."); var val = node.AsValue(); if (val.TryGetValue(out var i)) return (PieceKind)i; var s = val.GetValue(); if (Enum.TryParse(s, ignoreCase: true, out var k)) return k; throw new InvalidOperationException($"Unknown piece kind: {s}"); } private static Coords ParseCoords(JsonNode? node) { if (node is JsonArray arr && arr.Count == 2) return new Coords(arr[0]!.GetValue(), arr[1]!.GetValue()); if (node is JsonObject obj && obj.ContainsKey("col") && obj.ContainsKey("row")) return new Coords(obj["col"]!.GetValue(), obj["row"]!.GetValue()); throw new InvalidOperationException("Coords must be [col,row] or {col,row}."); } }