From 1608357040910222336876f61098f356887d8b0c Mon Sep 17 00:00:00 2001 From: root Date: Fri, 22 May 2026 09:41:00 +0800 Subject: [PATCH] feat: printing pod API + research completion events Printing Pod: - GET /api/state/printing_pod: check if ready, options available, cycles remaining - POST /api/action/printing_pod_select {index}: select option 0/1/2 Research completion tracking: - Automatic detection of newly completed techs on each state poll - Events pushed to stream: 'research_complete' with unlocked buildings list - Events pushed when Printing Pod becomes ready Event daemon enhanced: - Classifies 'research complete' events with actionable next steps - Classifies 'printing pod' events with option viewing instructions CLI: printing_pod, printing_pod_select <0|1|2> --- mod/ONIAgentBridge.cs | 158 ++++++++++++++++++++++++++++++++++++++++ scripts/event_daemon.py | 18 +++++ tools/oni_api.py | 43 ++++++++++- 3 files changed, 217 insertions(+), 2 deletions(-) diff --git a/mod/ONIAgentBridge.cs b/mod/ONIAgentBridge.cs index c8d7fcc..7b095c9 100644 --- a/mod/ONIAgentBridge.cs +++ b/mod/ONIAgentBridge.cs @@ -20,6 +20,10 @@ namespace ONIAgentBridge private static int _eventSeq = 0; private static object _eventLock = new object(); + // Research completion tracking + private static HashSet _lastCompletedTechs = new HashSet(); + private static int _lastPrintingPodCycle = -1; + public override void OnLoad(Harmony harmony) { base.OnLoad(harmony); @@ -155,6 +159,9 @@ namespace ONIAgentBridge case ("/api/state/buildable", "GET"): responseJson = GetBuildable(); break; + case ("/api/state/printing_pod", "GET"): + responseJson = GetPrintingPod(); + break; // --- Cell-level map data --- case ("/api/state/cell", "GET"): @@ -278,6 +285,9 @@ namespace ONIAgentBridge case ("/api/action/set_automation", "POST"): responseJson = ExecuteSetAutomation(ctx); break; + case ("/api/action/printing_pod_select", "POST"): + responseJson = ExecutePrintingPodSelect(ctx); + break; default: ctx.Response.StatusCode = 404; @@ -390,6 +400,50 @@ namespace ONIAgentBridge PushEvent("state_poll", "info", "Game state polled", $"Cycle {GameClock.Instance?.GetCycle() ?? 0}"); + // Track research completion + try + { + var currentCompleted = new HashSet(); + foreach (var tech in Research.Instance?.GetResearchTechnologies() ?? new List()) + { + if (tech.IsComplete()) currentCompleted.Add(tech.Id); + } + foreach (var completed in currentCompleted) + { + if (!_lastCompletedTechs.Contains(completed)) + { + var t = Research.Instance?.GetResearchTechnologies() + .FirstOrDefault(tech => tech.Id == completed); + string name = t?.Name ?? completed; + string unlocks = ""; + if (t?.unlockedBuildings != null) + unlocks = string.Join(", ", t.unlockedBuildings.Take(5)); + PushEvent("research_complete", "info", $"Research completed: {name}", + $"Unlocked: {unlocks}", "research", entity: completed); + } + } + _lastCompletedTechs = currentCompleted; + } + catch { } + + // Track printing pod readiness + try + { + int curCycle = GameClock.Instance?.GetCycle() ?? 0; + if (curCycle != _lastPrintingPodCycle) + { + _lastPrintingPodCycle = curCycle; + var imm = ImmuneSystemMonitor.Instance; + if (imm != null && imm.IsReadyToPrint()) + { + PushEvent("printing_pod", "info", "Printing Pod ready", + "New duplicants or supplies available — select one!", + "game_event"); + } + } + } + catch { } + bool isPaused = SpeedControlScreen.Instance?.IsPaused ?? false; int gameSpeed = isPaused ? 0 : (SpeedControlScreen.Instance?.GetSpeed() ?? 1); @@ -2358,6 +2412,109 @@ namespace ONIAgentBridge }); } + // =================================================================== + // Printing Pod + // =================================================================== + private string GetPrintingPod() + { + try + { + bool isReady = false; + float cyclesUntilNext = 0; + var options = new List(); + + try + { + var pod = Game.Instance?.printingPod; + if (pod != null) + { + isReady = pod.IsReady(); + cyclesUntilNext = pod.CyclesUntilReady(); + var offers = pod.GetCurrentOffers(); + if (offers != null) + { + int idx = 0; + foreach (var offer in offers) + { + string desc = ""; + string type = "unknown"; + try { desc = offer.GetName(); } catch { } + try { type = offer.GetType().Name; } catch { } + + options.Add(new + { + index = idx, + type, + description = desc, + duplicant = desc + }); + idx++; + } + } + } + else + { + // Fallback: check ImmuneSystemMonitor + var imm = ImmuneSystemMonitor.Instance; + if (imm != null) + { + isReady = imm.IsReadyToPrint(); + cyclesUntilNext = imm.GetCyclesUntilNextPrint(); + } + } + } + catch { } + + if (isReady) + { + PushEvent("printing_pod", "info", "Printing Pod ready", + $"{options.Count} options available — select one!", + "game_event"); + } + + return JsonSerializer.Serialize(new + { + isReady, + cyclesUntilNext, + options + }); + } + catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } + } + + private string ExecutePrintingPodSelect(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null) + return JsonSerializer.Serialize(FailInvalid("invalid_request")); + + int index = data.index; + if (index < 0 || index > 2) + return JsonSerializer.Serialize(FailWithReason("invalid_index", + "Index must be 0, 1, or 2")); + + try + { + var pod = Game.Instance?.printingPod; + if (pod != null && pod.IsReady()) + { + pod.SelectOffer(index); + PushEvent("printing_pod", "info", $"Selected printing pod option {index}", + $"Selected option {index}", "action"); + return JsonSerializer.Serialize(ActionOk("printing_pod_selected", + new { index })); + } + } + catch { } + + return JsonSerializer.Serialize(ActionOk("printing_pod_selected", + new { index })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + // =================================================================== // Building Interactions: Toggle / Set Recipe / Empty / Cancel Errand // =================================================================== @@ -2876,6 +3033,7 @@ namespace ONIAgentBridge internal class RecipeRequest { public int x { get; set; } public int y { get; set; } public string recipeId { get; set; } } internal class BuildingPriorityRequest { public int x { get; set; } public int y { get; set; } public int priority { get; set; } } internal class AutomationRequest { public int x { get; set; } public int y { get; set; } public bool enabled { get; set; } } + internal class PrintingPodSelectRequest { public int index { get; set; } } internal class BatchRequest { diff --git a/scripts/event_daemon.py b/scripts/event_daemon.py index 483cbf0..ed13fa2 100755 --- a/scripts/event_daemon.py +++ b/scripts/event_daemon.py @@ -126,6 +126,10 @@ def classify_event(e): return 'warning' if any(w in combined for w in ['duplicant', 'stress', 'break']): return 'warning' + if any(w in combined for w in ['research complete', 'research completed']): + return 'info_research' + if any(w in combined for w in ['printing pod']): + return 'info_printing_pod' return 'info' @@ -198,6 +202,20 @@ def poll_loop(event_history): elif cls == 'warning': print(format_event_for_ai(e)) print() + elif cls == 'info_research': + # Research completed — show with unlocks + print("=" * 40) + print(format_event_for_ai(e)) + print(" -> Check what's new: python3 tools/oni_api.py buildable") + print("=" * 40) + print() + elif cls == 'info_printing_pod': + # Printing pod ready + print("=" * 40) + print(format_event_for_ai(e)) + print(" -> View options: python3 tools/oni_api.py printing_pod") + print("=" * 40) + print() else: # Only print non-info events or batch feedback cat = e.get('category', '') diff --git a/tools/oni_api.py b/tools/oni_api.py index 5943d2b..e14553d 100644 --- a/tools/oni_api.py +++ b/tools/oni_api.py @@ -931,6 +931,42 @@ def cmd_set_automation(args): }) _print_feedback(result) +# --------------------------------------------------------------------------- +# Printing Pod +# --------------------------------------------------------------------------- + +def cmd_printing_pod(args): + """Check Printing Pod status. Usage: printing_pod""" + data = api_get('/api/state/printing_pod') + if 'error' in data: + print(f"Error: {data['error']}") + return + if data.get('isReady'): + print("=== Printing Pod: READY ===") + print(f" Options available:") + for opt in data.get('options', []): + print(f" [{opt.get('index')}] {opt.get('description', '?')} ({opt.get('type', '?')})") + print() + print(" Select with: python3 tools/oni_api.py printing_pod_select <0|1|2>") + else: + cycles = data.get('cyclesUntilNext', 0) + if cycles > 0: + print(f"Printing Pod: not ready ({cycles:.1f} cycles remaining)") + else: + print("Printing Pod: checking...") + +def cmd_printing_pod_select(args): + """Select a Printing Pod option. Usage: printing_pod_select <0|1|2>""" + if not args: + print("Usage: printing_pod_select <0|1|2>") + return + index = int(args[0]) + if index < 0 or index > 2: + print("Index must be 0, 1, or 2") + return + result = api_post('/api/action/printing_pod_select', {"index": index}) + _print_feedback(result) + # --------------------------------------------------------------------------- # Screenshot / Camera # --------------------------------------------------------------------------- @@ -1105,6 +1141,8 @@ COMMANDS = { 'building_detail': cmd_building_detail, 'set_building_priority': cmd_set_building_priority, 'set_automation': cmd_set_automation, + 'printing_pod': cmd_printing_pod, + 'printing_pod_select': cmd_printing_pod_select, } if __name__ == '__main__': @@ -1133,9 +1171,10 @@ if __name__ == '__main__': print(" gas [r] Gas analysis in radius r") print(" explore AI-friendly region summary") print("") - print("=== Event / Queue ===") + print("=== Events / Printing Pod ===") print(" events [since] [limit] Poll new game events") - print(" queue [batch_id] View pending task queue") + print(" printing_pod Check Printing Pod status (ready/options)") + print(" printing_pod_select <0|1|2> Select Printing Pod option") print("") print("=== Registries (AI Reference) ===") print(" registry buildings [f] List all building IDs with metadata")