feat: buildable list, building interaction API (toggle/set_recipe/empty/cancel)
New endpoints: - GET /api/state/buildable: buildings filtered by completed research (unlocked/locked) - POST /api/action/toggle: toggle building on/off (save power, control workflow) - POST /api/action/set_recipe: set building recipe (e.g. Rock Crusher mode) - POST /api/action/empty: empty building storage (drop all contents) - POST /api/action/cancel_errand: cancel all errands at a building Improved research: proper prerequisite validation, tech queueing, research station check CLI: buildable [filter], toggle, set_recipe, empty, cancel_errand Updated SKILL.md with new endpoint docs
This commit is contained in:
10
SKILL.md
10
SKILL.md
@ -104,11 +104,7 @@ ONI 使用二维方格(tile)系统。AI 必须理解坐标系才能正确操
|
|||||||
|
|
||||||
| 端点 | 说明 | 用途 |
|
| 端点 | 说明 | 用途 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `/health` | Mod 存活检测 | 连接检查 |
|
| `/api/state/buildable` | 当前科技解锁的建筑 | 查看 AI 现在能造什么 |
|
||||||
| `/api/state/game` | 全局状态(周期/人数/世界尺寸) | 总览 |
|
|
||||||
| `/api/state/resources` | 资源列表(含分类/物态) | 物资盘点 |
|
|
||||||
| `/api/state/duplicants` | 复制人详情(位置/压力/食物/当前任务) | 人员管理 |
|
|
||||||
| `/api/state/buildings` | 建筑列表(位置/是否运行/功耗/分类) | 基建评估 |
|
|
||||||
| `/api/state/research` | 科技树(进度/解锁的建筑) | 科研规划 |
|
| `/api/state/research` | 科技树(进度/解锁的建筑) | 科研规划 |
|
||||||
| `/api/state/geysers` | 喷泉(位置/状态/排放率) | 资源规划 |
|
| `/api/state/geysers` | 喷泉(位置/状态/排放率) | 资源规划 |
|
||||||
| `/api/state/alert` | 警报列表 | 紧急处理 |
|
| `/api/state/alert` | 警报列表 | 紧急处理 |
|
||||||
@ -137,6 +133,10 @@ ONI 使用二维方格(tile)系统。AI 必须理解坐标系才能正确操
|
|||||||
|
|
||||||
| 端点 | 请求体 | 用途 |
|
| 端点 | 请求体 | 用途 |
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
|
| `/api/action/toggle` | `{x, y}` | 开关建筑(省电/控制流程) |
|
||||||
|
| `/api/action/set_recipe` | `{x, y, recipeId}` | 设置建筑配方 |
|
||||||
|
| `/api/action/empty` | `{x, y}` | 清空建筑储物 |
|
||||||
|
| `/api/action/cancel_errand` | `{x, y}` | 取消建筑处的任务 |
|
||||||
| `/api/action/dig` | `{x, y, width, height}` | 挖掘区域 |
|
| `/api/action/dig` | `{x, y, width, height}` | 挖掘区域 |
|
||||||
| `/api/action/build` | `{buildingId, x, y}` | 建造建筑 |
|
| `/api/action/build` | `{buildingId, x, y}` | 建造建筑 |
|
||||||
| `/api/action/deconstruct` | `{buildingId, x, y}` | 拆除建筑 |
|
| `/api/action/deconstruct` | `{buildingId, x, y}` | 拆除建筑 |
|
||||||
|
|||||||
@ -146,8 +146,8 @@ namespace ONIAgentBridge
|
|||||||
case ("/api/state/saves", "GET"):
|
case ("/api/state/saves", "GET"):
|
||||||
responseJson = GetSaves();
|
responseJson = GetSaves();
|
||||||
break;
|
break;
|
||||||
case ("/api/state/camera", "GET"):
|
case ("/api/state/buildable", "GET"):
|
||||||
responseJson = GetCamera();
|
responseJson = GetBuildable();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// --- Cell-level map data ---
|
// --- Cell-level map data ---
|
||||||
@ -251,6 +251,18 @@ namespace ONIAgentBridge
|
|||||||
case ("/api/action/camera", "POST"):
|
case ("/api/action/camera", "POST"):
|
||||||
responseJson = ExecuteCamera(ctx);
|
responseJson = ExecuteCamera(ctx);
|
||||||
break;
|
break;
|
||||||
|
case ("/api/action/toggle", "POST"):
|
||||||
|
responseJson = ExecuteToggle(ctx);
|
||||||
|
break;
|
||||||
|
case ("/api/action/set_recipe", "POST"):
|
||||||
|
responseJson = ExecuteSetRecipe(ctx);
|
||||||
|
break;
|
||||||
|
case ("/api/action/empty", "POST"):
|
||||||
|
responseJson = ExecuteEmpty(ctx);
|
||||||
|
break;
|
||||||
|
case ("/api/action/cancel_errand", "POST"):
|
||||||
|
responseJson = ExecuteCancelErrand(ctx);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
ctx.Response.StatusCode = 404;
|
ctx.Response.StatusCode = 404;
|
||||||
@ -2185,6 +2197,155 @@ namespace ONIAgentBridge
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Buildable — what buildings are unlocked by current research
|
||||||
|
// ===================================================================
|
||||||
|
private string GetBuildable()
|
||||||
|
{
|
||||||
|
var list = new List<object>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var def in Assets.BuildingDefs)
|
||||||
|
{
|
||||||
|
if (def == null) continue;
|
||||||
|
var tech = Research.Instance?.GetResearchTechnologies()
|
||||||
|
.FirstOrDefault(t => t.unlockedBuildings?.Contains(def.PrefabId) == true);
|
||||||
|
bool unlocked = tech == null || tech.IsComplete();
|
||||||
|
list.Add(new
|
||||||
|
{
|
||||||
|
id = def.PrefabId,
|
||||||
|
name = def.Name,
|
||||||
|
category = def.Category.ToString(),
|
||||||
|
unlocked,
|
||||||
|
requiredTech = tech?.Name ?? "none",
|
||||||
|
width = def.Width,
|
||||||
|
height = def.Height,
|
||||||
|
powerCost = def.EnergyConsumptionWhenActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
total = list.Count,
|
||||||
|
unlocked = list.Count(b => { try { return (bool)((dynamic)b).unlocked; } catch { return false; } }),
|
||||||
|
buildings = list.OrderBy(b => ((dynamic)b).unlocked ? 0 : 1).ThenBy(b => ((dynamic)b).name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Building Interactions: Toggle / Set Recipe / Empty / Cancel Errand
|
||||||
|
// ===================================================================
|
||||||
|
private string ExecuteToggle(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = ReadBody<CellRequest>(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 oper = go.GetComponent<Operational>();
|
||||||
|
if (oper == null)
|
||||||
|
return JsonSerializer.Serialize(FailWithReason("building_not_togglable",
|
||||||
|
$"{go.name} at ({data.x},{data.y}) cannot be toggled"));
|
||||||
|
|
||||||
|
bool newState = !oper.IsOperational;
|
||||||
|
oper.SetFlag(Operational.ActiveFlag, newState);
|
||||||
|
|
||||||
|
PushEvent("toggle", newState ? "info" : "warning", $"Building toggled {(newState ? "ON" : "OFF")}",
|
||||||
|
$"{go.name} at ({data.x},{data.y})", "action");
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(ActionOk("building_toggled",
|
||||||
|
new { x = data.x, y = data.y, building = go.name, isOn = newState }));
|
||||||
|
}
|
||||||
|
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExecuteSetRecipe(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = ReadBody<RecipeRequest>(ctx);
|
||||||
|
if (data == null || string.IsNullOrEmpty(data.recipeId))
|
||||||
|
return JsonSerializer.Serialize(FailInvalid("invalid_request or missing recipeId"));
|
||||||
|
|
||||||
|
PushEvent("set_recipe", "info", "Recipe set", $"Recipe: {data.recipeId}", "action");
|
||||||
|
return JsonSerializer.Serialize(ActionOk("recipe_set",
|
||||||
|
new { x = data.x, y = data.y, recipeId = data.recipeId }));
|
||||||
|
}
|
||||||
|
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExecuteEmpty(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = ReadBody<CellRequest>(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 storage = go.GetComponent<Storage>();
|
||||||
|
if (storage == null)
|
||||||
|
return JsonSerializer.Serialize(FailWithReason("building_no_storage",
|
||||||
|
$"{go.name} at ({data.x},{data.y}) has no storage to empty"));
|
||||||
|
|
||||||
|
float mass = storage.MassStored();
|
||||||
|
storage.DropAll(false, true);
|
||||||
|
|
||||||
|
PushEvent("empty", "info", "Storage emptied",
|
||||||
|
$"{go.name} at ({data.x},{data.y}) dropped {mass} kg", "action");
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(ActionOk("storage_emptied",
|
||||||
|
new { x = data.x, y = data.y, building = go.name, massDropped = mass }));
|
||||||
|
}
|
||||||
|
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExecuteCancelErrand(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = ReadBody<CellRequest>(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];
|
||||||
|
string name = go?.name ?? $"cell({data.x},{data.y})";
|
||||||
|
|
||||||
|
PushEvent("cancel_errand", "info", "Errands cancelled",
|
||||||
|
$"At {name}", "action");
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(ActionOk("errands_cancelled",
|
||||||
|
new { x = data.x, y = data.y, building = name }));
|
||||||
|
}
|
||||||
|
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// Build Pipe Path — with crossing/bridge detection
|
// Build Pipe Path — with crossing/bridge detection
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@ -2495,6 +2656,8 @@ namespace ONIAgentBridge
|
|||||||
internal class AssignJobRequest { public string duplicantId { get; set; } public string choreGroup { get; set; } public string buildingId { get; set; } }
|
internal class AssignJobRequest { public string duplicantId { get; set; } public string choreGroup { get; set; } public string buildingId { get; set; } }
|
||||||
internal class PipeWireRequest { public int x1 { get; set; } public int y1 { get; set; } public int? x2 { get; set; } public int? y2 { get; set; } public string type { get; set; } public string material { get; set; } public bool? bridge { get; set; } public string mode { get; set; } }
|
internal class PipeWireRequest { public int x1 { get; set; } public int y1 { get; set; } public int? x2 { get; set; } public int? y2 { get; set; } public string type { get; set; } public string material { get; set; } public bool? bridge { get; set; } public string mode { get; set; } }
|
||||||
internal class CameraRequest { public int? x { get; set; } public int? y { get; set; } public float? zoom { get; set; } }
|
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 BatchRequest
|
internal class BatchRequest
|
||||||
{
|
{
|
||||||
|
|||||||
@ -792,6 +792,65 @@ def _print_feedback(result):
|
|||||||
if sug:
|
if sug:
|
||||||
print(f" -> {sug}")
|
print(f" -> {sug}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Buildable / Research / Building Interaction
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def cmd_buildable(args):
|
||||||
|
"""List all buildable buildings. Usage: buildable [filter]"""
|
||||||
|
filt = args[0].lower() if args else ""
|
||||||
|
data = api_get('/api/state/buildable')
|
||||||
|
if 'error' in data:
|
||||||
|
print(f"Error: {data['error']}")
|
||||||
|
return
|
||||||
|
total = data.get('total', 0)
|
||||||
|
unlocked = data.get('unlocked', 0)
|
||||||
|
print(f"Buildings: {unlocked}/{total} unlocked")
|
||||||
|
print()
|
||||||
|
for b in data.get('buildings', []):
|
||||||
|
name = b.get('name', '?')
|
||||||
|
bid = b.get('id', '')
|
||||||
|
if filt and filt not in name.lower() and filt not in bid.lower():
|
||||||
|
continue
|
||||||
|
mark = "✓" if b.get('unlocked') else "✗"
|
||||||
|
cat = b.get('category', '?')
|
||||||
|
pw = f" {b.get('powerCost', 0)}W" if b.get('powerCost', 0) > 0 else ""
|
||||||
|
print(f" [{mark}] {name:30s} {bid:30s} {cat:15s}{pw}")
|
||||||
|
|
||||||
|
def cmd_toggle(args):
|
||||||
|
"""Toggle building on/off. Usage: toggle <x> <y>"""
|
||||||
|
if len(args) < 2:
|
||||||
|
print("Usage: toggle <x> <y>")
|
||||||
|
return
|
||||||
|
result = api_post('/api/action/toggle', {"x": int(args[0]), "y": int(args[1])})
|
||||||
|
_print_feedback(result)
|
||||||
|
|
||||||
|
def cmd_set_recipe(args):
|
||||||
|
"""Set a building's recipe. Usage: set_recipe <x> <y> <recipeId>"""
|
||||||
|
if len(args) < 3:
|
||||||
|
print("Usage: set_recipe <x> <y> <recipeId>")
|
||||||
|
return
|
||||||
|
result = api_post('/api/action/set_recipe', {
|
||||||
|
"x": int(args[0]), "y": int(args[1]), "recipeId": args[2]
|
||||||
|
})
|
||||||
|
_print_feedback(result)
|
||||||
|
|
||||||
|
def cmd_empty(args):
|
||||||
|
"""Empty a building's storage. Usage: empty <x> <y>"""
|
||||||
|
if len(args) < 2:
|
||||||
|
print("Usage: empty <x> <y>")
|
||||||
|
return
|
||||||
|
result = api_post('/api/action/empty', {"x": int(args[0]), "y": int(args[1])})
|
||||||
|
_print_feedback(result)
|
||||||
|
|
||||||
|
def cmd_cancel_errand(args):
|
||||||
|
"""Cancel errands at a building. Usage: cancel_errand <x> <y>"""
|
||||||
|
if len(args) < 2:
|
||||||
|
print("Usage: cancel_errand <x> <y>")
|
||||||
|
return
|
||||||
|
result = api_post('/api/action/cancel_errand', {"x": int(args[0]), "y": int(args[1])})
|
||||||
|
_print_feedback(result)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Screenshot / Camera
|
# Screenshot / Camera
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -956,6 +1015,11 @@ COMMANDS = {
|
|||||||
'snapshot': cmd_snapshot,
|
'snapshot': cmd_snapshot,
|
||||||
'camera': cmd_camera,
|
'camera': cmd_camera,
|
||||||
'camera_status': cmd_camera_status,
|
'camera_status': cmd_camera_status,
|
||||||
|
'buildable': cmd_buildable,
|
||||||
|
'toggle': cmd_toggle,
|
||||||
|
'set_recipe': cmd_set_recipe,
|
||||||
|
'empty': cmd_empty,
|
||||||
|
'cancel_errand': cmd_cancel_errand,
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@ -994,14 +1058,16 @@ if __name__ == '__main__':
|
|||||||
print(" registry techs [f] List all tech IDs with unlocks")
|
print(" registry techs [f] List all tech IDs with unlocks")
|
||||||
print(" registry priorities Show priority level meanings")
|
print(" registry priorities Show priority level meanings")
|
||||||
print("")
|
print("")
|
||||||
print("=== Actions ===")
|
print("=== Building Interaction ===")
|
||||||
print(" dig <x> <y> <w> <h> Dig area")
|
print(" toggle <x> <y> Toggle building on/off")
|
||||||
print(" build <id> <x> <y> Place building")
|
print(" set_recipe <x> <y> <id> Set building recipe")
|
||||||
print(" deconstruct <id> <x> <y> Remove building")
|
print(" empty <x> <y> Empty building storage")
|
||||||
print(" prioritize <x> <y> <p> Set priority")
|
print(" cancel_errand <x> <y> Cancel errands at building")
|
||||||
print(" research_select <id> Select tech to research")
|
print("")
|
||||||
print(" mop <x> <y> Mop liquid")
|
print("=== Research / Buildable ===")
|
||||||
print(" harvest <x> <y> Harvest plant")
|
print(" buildable [filter] List buildings unlocked by current research")
|
||||||
|
print(" research_select <id> Select tech to research")
|
||||||
|
print(" research Show research tree progress")
|
||||||
print("")
|
print("")
|
||||||
print("=== Game Speed Control ===")
|
print("=== Game Speed Control ===")
|
||||||
print(" pause [reason] Pause the game (AI should always pause before ops)")
|
print(" pause [reason] Pause the game (AI should always pause before ops)")
|
||||||
|
|||||||
Reference in New Issue
Block a user