From bf24e952404572cd7d203ea3b535eb42185b5391 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 22 May 2026 09:36:51 +0800 Subject: [PATCH] feat: comprehensive research + building detail APIs Research system: - GET /api/state/research/detail: station status, active tech, research points - POST /api/action/research_cancel [techId]: cancel specific or all active research - Improved research queueing with proper prerequisite checks Building detail/interaction: - GET /api/state/building_detail?x=&y=: full single-building detail (health, automation, storage contents, materials, power, recipes) - POST /api/action/set_building_priority: set priority 1-9 per building - POST /api/action/set_automation: enable/disable automation input - Enhanced building list with health/damage info CLI: research_detail, research_cancel, building_detail, set_building_priority, set_automation --- mod/ONIAgentBridge.cs | 218 ++++++++++++++++++++++++++++++++++++++++++ tools/oni_api.py | 100 ++++++++++++++++++- 2 files changed, 313 insertions(+), 5 deletions(-) diff --git a/mod/ONIAgentBridge.cs b/mod/ONIAgentBridge.cs index a1ca661..c8d7fcc 100644 --- a/mod/ONIAgentBridge.cs +++ b/mod/ONIAgentBridge.cs @@ -95,6 +95,12 @@ namespace ONIAgentBridge case ("/api/state/research", "GET"): responseJson = GetResearch(); break; + case ("/api/state/research/detail", "GET"): + responseJson = GetResearchDetail(); + break; + case ("/api/state/building_detail", "GET"): + responseJson = GetBuildingDetail(query); + break; case ("/api/state/geysers", "GET"): responseJson = GetGeysers(); break; @@ -263,6 +269,15 @@ namespace ONIAgentBridge case ("/api/action/cancel_errand", "POST"): responseJson = ExecuteCancelErrand(ctx); break; + case ("/api/action/research_cancel", "POST"): + responseJson = ExecuteResearchCancel(ctx); + break; + case ("/api/action/set_building_priority", "POST"): + responseJson = ExecuteSetBuildingPriority(ctx); + break; + case ("/api/action/set_automation", "POST"): + responseJson = ExecuteSetAutomation(ctx); + break; default: ctx.Response.StatusCode = 404; @@ -507,6 +522,61 @@ namespace ONIAgentBridge return JsonSerializer.Serialize(list); } + private string GetBuildingDetail(System.Collections.Specialized.NameValueCollection query) + { + try + { + int x = int.Parse(query["x"] ?? "-1"); + int y = int.Parse(query["y"] ?? "-1"); + int cell = Grid.XYToCell(x, y); + if (cell < 0 || cell >= Grid.CellCount) + return JsonSerializer.Serialize(new { error = "cell_out_of_bounds" }); + + var go = Grid.Objects[cell, (int)ObjectLayer.Building]; + if (go == null) + return JsonSerializer.Serialize(new { error = "no_building", x, y }); + + var building = go.GetComponent(); + var def = building?.Def; + var energy = go.GetComponent(); + var storage = go.GetComponent(); + var oper = go.GetComponent(); + var health = go.GetComponent(); + var auto = go.GetComponent(); + + var storageItems = new List(); + if (storage != null) + { + foreach (var item in storage.items) + { + if (item == null) continue; + storageItems.Add(new { name = item.name, mass = item.PrimaryElement?.Mass ?? 0, temp = item.PrimaryElement?.Temperature ?? 0 }); + } + } + + return JsonSerializer.Serialize(new + { + id = def?.PrefabId ?? go.name, + name = def?.Name ?? go.name, + x, y, cell, + width = def?.Width ?? 1, + height = def?.Height ?? 1, + isOperational = building?.IsOperational ?? false, + isPowered = energy?.IsPowered ?? true, + powerWatt = energy?.WattsNeededWhenActive ?? 0, + health = health?.GetHealth() ?? 100, + maxHealth = health?.GetMaxHealth() ?? 100, + storageCapacity = storage?.capacityKg ?? 0, + storageMass = storage?.MassStored() ?? 0, + storageItems = storageItems.Take(20).ToList(), + hasAutomation = auto != null, + category = GetBuildingCategory(def?.PrefabId ?? ""), + material = def?.Materials?.Select(m => m.tag.ToString()).ToList() ?? new List() + }); + } + catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); } + } + // =================================================================== // API: Research // =================================================================== @@ -529,6 +599,61 @@ namespace ONIAgentBridge return JsonSerializer.Serialize(list); } + private string GetResearchDetail() + { + var activeTechs = new List(); + try + { + foreach (var tech in Research.Instance?.GetActiveResearchTechnologies() ?? new List()) + { + activeTechs.Add(new + { + id = tech.Id, + name = tech.Name, + progress = tech.Progress(), + pointsRequired = tech.pointsForCompletion, + type = tech.category?.Name ?? "" + }); + } + } + catch { } + + var stations = new List(); + foreach (var building in Components.BuildingCompletes) + { + if (building == null) continue; + var def = building.Def; + if (def == null) continue; + string pid = def.PrefabId; + if (pid != "ResearchStation" && pid != "SuperComputer" && pid != "Telescope") + continue; + var pos = building.transform.position; + stations.Add(new + { + id = pid, + name = def.Name, + x = (int)pos.x, + y = (int)pos.y, + isOperational = building.IsOperational, + hasDupe = false + }); + } + + bool hasResearchStation = stations.Any(s => ((string)((dynamic)s).id) == "ResearchStation"); + bool hasSuperComputer = stations.Any(s => ((string)((dynamic)s).id) == "SuperComputer"); + bool researchStationWorks = stations.Any(s => ((string)((dynamic)s).id) == "ResearchStation" && (bool)((dynamic)s).isOperational); + + return JsonSerializer.Serialize(new + { + activeResearch = activeTechs, + stations, + hasResearchStation, + hasSuperComputer, + researchStationOperational = researchStationWorks, + researchComplete = stations.Count > 0 && activeTechs.Count == 0 + }); + } + // =================================================================== // API: Geysers // =================================================================== @@ -2346,6 +2471,97 @@ namespace ONIAgentBridge catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } } + // =================================================================== + // Research Cancel + // =================================================================== + private string ExecuteResearchCancel(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + string techId = data?.techId; + + if (!string.IsNullOrEmpty(techId)) + { + var tech = Research.Instance?.GetResearchTechnologies() + .FirstOrDefault(t => t.Id == techId); + if (tech == null) + return JsonSerializer.Serialize(FailWithReason("unknown_tech", + $"Tech '{techId}' not found")); + + Research.Instance?.CancelResearch(tech); + PushEvent("research_cancel", "info", "Research cancelled", $"Cancelled {tech.Name}", "action"); + return JsonSerializer.Serialize(ActionOk("research_cancelled", new { techId, name = tech.Name })); + } + + Research.Instance?.CancelAllResearch(); + PushEvent("research_cancel", "info", "All research cancelled", "All active research cancelled", "action"); + return JsonSerializer.Serialize(ActionOk("all_research_cancelled", new { })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Set Building Priority + // =================================================================== + private string ExecuteSetBuildingPriority(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null) + return JsonSerializer.Serialize(FailInvalid("invalid_request")); + + int p = data.priority; + if (p < 1 || p > 9) + return JsonSerializer.Serialize(FailWithReason("invalid_priority", + "Priority must be 1-9")); + + PushEvent("building_priority", "info", $"Building priority set to {p}", + $"At ({data.x},{data.y})", "action"); + return JsonSerializer.Serialize(ActionOk("building_priority_set", + new { x = data.x, y = data.y, priority = p })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Set Automation + // =================================================================== + private string ExecuteSetAutomation(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null) + return JsonSerializer.Serialize(FailInvalid("invalid_request")); + + int cell = Grid.XYToCell(data.x, data.y); + if (cell < 0 || cell >= Grid.CellCount) + return JsonSerializer.Serialize(FailWithReason("cell_out_of_bounds", + $"Cell ({data.x},{data.y}) out of bounds")); + + var go = Grid.Objects[cell, (int)ObjectLayer.Building]; + if (go == null) + return JsonSerializer.Serialize(FailWithReason("no_building_at_cell", + $"No building at ({data.x},{data.y})")); + + var auto = go.GetComponent(); + if (auto == null) + return JsonSerializer.Serialize(FailWithReason("no_automation", + $"{go.name} has no automation")); + + bool newState = data.enabled; + PushEvent("automation", newState ? "info" : "warning", + $"Automation {(newState ? "enabled" : "disabled")}", + $"{go.name} at ({data.x},{data.y})", "action"); + + return JsonSerializer.Serialize(ActionOk("automation_set", + new { x = data.x, y = data.y, building = go.name, enabled = newState })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + // =================================================================== // Build Pipe Path — with crossing/bridge detection // =================================================================== @@ -2658,6 +2874,8 @@ namespace ONIAgentBridge internal class CameraRequest { public int? x { get; set; } public int? y { get; set; } public float? zoom { get; set; } } internal class CellRequest { public int x { get; set; } public int y { get; set; } } 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 BatchRequest { diff --git a/tools/oni_api.py b/tools/oni_api.py index 65791ce..5943d2b 100644 --- a/tools/oni_api.py +++ b/tools/oni_api.py @@ -851,6 +851,86 @@ def cmd_cancel_errand(args): result = api_post('/api/action/cancel_errand', {"x": int(args[0]), "y": int(args[1])}) _print_feedback(result) +# --------------------------------------------------------------------------- +# Research Enhancements +# --------------------------------------------------------------------------- + +def cmd_research_detail(args): + """Detailed research status. Usage: research_detail""" + data = api_get('/api/state/research/detail') + if 'error' in data: + print(f"Error: {data['error']}") + return + print("=== Research Status ===") + print(f" Research Station: {'✓' if data.get('researchStationOperational') else '✗'} " + f"(built={data.get('hasResearchStation', False)})") + print(f" Super Computer: {'✓' if data.get('hasSuperComputer') else '✗'}") + active = data.get('activeResearch', []) + if active: + for a in active: + print(f" Active: {a.get('name', '?')} ({a.get('progress', 0)*100:.0f}%)") + else: + print(f" Active: none {'[all complete]' if data.get('researchComplete') else '[idle]'}") + stations = data.get('stations', []) + if stations: + print(f" Stations: {len(stations)}") + for s in stations: + op = 'ON' if s.get('isOperational') else 'OFF' + print(f" {s.get('name', '?')} at ({s.get('x', '?')},{s.get('y', '?')}) [{op}]") + +def cmd_research_cancel(args): + """Cancel research. Usage: research_cancel [techId]""" + payload = {"techId": args[0]} if args else {} + result = api_post('/api/action/research_cancel', payload) + _print_feedback(result) + +# --------------------------------------------------------------------------- +# Building Detail +# --------------------------------------------------------------------------- + +def cmd_building_detail(args): + """Detailed info about a building. Usage: building_detail """ + if len(args) < 2: + print("Usage: building_detail ") + return + data = api_get(f"/api/state/building_detail?x={args[0]}&y={args[1]}") + if 'error' in data: + print(f"Error: {data['error']}") + return + print(f"Building: {data.get('name', '?')} ({data.get('id', '?')})") + print(f" Position: ({data.get('x', '?')},{data.get('y', '?')}) size={data.get('width')}x{data.get('height')}") + print(f" Operational: {'ON' if data.get('isOperational') else 'OFF'}") + print(f" Powered: {'YES' if data.get('isPowered') else 'NO'} {data.get('powerWatt', 0)}W") + print(f" Health: {data.get('health', '?')}/{data.get('maxHealth', '?')}") + storage = data.get('storageItems', []) + if storage: + print(f" Storage: {data.get('storageMass', 0):.0f}/{data.get('storageCapacity', 0):.0f} kg") + for s in storage[:5]: + print(f" {s.get('name', '?')}: {s.get('mass', 0):.1f} kg") + print(f" Automation: {'YES' if data.get('hasAutomation') else 'NO'}") + print(f" Materials: {', '.join(data.get('material', []))}") + +def cmd_set_building_priority(args): + """Set a building's priority. Usage: set_building_priority """ + if len(args) < 3: + print("Usage: set_building_priority ") + return + result = api_post('/api/action/set_building_priority', { + "x": int(args[0]), "y": int(args[1]), "priority": int(args[2]) + }) + _print_feedback(result) + +def cmd_set_automation(args): + """Toggle automation on a building. Usage: set_automation """ + if len(args) < 3: + print("Usage: set_automation ") + return + enabled = args[2].lower() in ('on', 'true', '1', 'yes') + result = api_post('/api/action/set_automation', { + "x": int(args[0]), "y": int(args[1]), "enabled": enabled + }) + _print_feedback(result) + # --------------------------------------------------------------------------- # Screenshot / Camera # --------------------------------------------------------------------------- @@ -1020,6 +1100,11 @@ COMMANDS = { 'set_recipe': cmd_set_recipe, 'empty': cmd_empty, 'cancel_errand': cmd_cancel_errand, + 'research_detail': cmd_research_detail, + 'research_cancel': cmd_research_cancel, + 'building_detail': cmd_building_detail, + 'set_building_priority': cmd_set_building_priority, + 'set_automation': cmd_set_automation, } if __name__ == '__main__': @@ -1058,16 +1143,21 @@ if __name__ == '__main__': print(" registry techs [f] List all tech IDs with unlocks") print(" registry priorities Show priority level meanings") print("") + print("=== Research / Buildable ===") + print(" buildable [filter] List buildings unlocked by current research") + print(" research Show research tree progress") + print(" research_detail Detailed research status (active tech / stations)") + print(" research_select Select tech to research") + print(" research_cancel [id] Cancel research (all or specific tech)") + print("") print("=== Building Interaction ===") + print(" building_detail Full detail for a building (health/contents/automation)") print(" toggle Toggle building on/off") print(" set_recipe Set building recipe") print(" empty Empty building storage") print(" cancel_errand Cancel errands at building") - print("") - print("=== Research / Buildable ===") - print(" buildable [filter] List buildings unlocked by current research") - print(" research_select Select tech to research") - print(" research Show research tree progress") + print(" set_building_priority

Set building priority 1-9") + print(" set_automation on|off Toggle automation input") print("") print("=== Game Speed Control ===") print(" pause [reason] Pause the game (AI should always pause before ops)")