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
This commit is contained in:
root
2026-05-22 09:36:51 +08:00
parent 7a946aa555
commit bf24e95240
2 changed files with 313 additions and 5 deletions

View File

@ -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<BuildingComplete>();
var def = building?.Def;
var energy = go.GetComponent<EnergyConsumer>();
var storage = go.GetComponent<Storage>();
var oper = go.GetComponent<Operational>();
var health = go.GetComponent<Health>();
var auto = go.GetComponent<AutomationController>();
var storageItems = new List<object>();
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<string>()
});
}
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<object>();
try
{
foreach (var tech in Research.Instance?.GetActiveResearchTechnologies() ?? new List<Tech>())
{
activeTechs.Add(new
{
id = tech.Id,
name = tech.Name,
progress = tech.Progress(),
pointsRequired = tech.pointsForCompletion,
type = tech.category?.Name ?? ""
});
}
}
catch { }
var stations = new List<object>();
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<ResearchRequest>(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<BuildingPriorityRequest>(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<AutomationRequest>(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<AutomationController>();
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
{

View File

@ -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 <x> <y>"""
if len(args) < 2:
print("Usage: building_detail <x> <y>")
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 <x> <y> <priority>"""
if len(args) < 3:
print("Usage: set_building_priority <x> <y> <priority 1-9>")
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 <x> <y> <on|off>"""
if len(args) < 3:
print("Usage: set_automation <x> <y> <on|off>")
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 <id> Select tech to research")
print(" research_cancel [id] Cancel research (all or specific tech)")
print("")
print("=== Building Interaction ===")
print(" building_detail <x> <y> Full detail for a building (health/contents/automation)")
print(" toggle <x> <y> Toggle building on/off")
print(" set_recipe <x> <y> <id> Set building recipe")
print(" empty <x> <y> Empty building storage")
print(" cancel_errand <x> <y> Cancel errands at building")
print("")
print("=== Research / Buildable ===")
print(" buildable [filter] List buildings unlocked by current research")
print(" research_select <id> Select tech to research")
print(" research Show research tree progress")
print(" set_building_priority <x> <y> <p> Set building priority 1-9")
print(" set_automation <x> <y> on|off Toggle automation input")
print("")
print("=== Game Speed Control ===")
print(" pause [reason] Pause the game (AI should always pause before ops)")