feat: atmo suits, door lock/one-way, critter attack APIs

Atmo Suits:
- GET /api/state/atmo_suits: check suit docks, suit presence, O2 levels

Door Control:
- POST /api/action/door_lock {x, y, locked}: lock/unlock doors
- POST /api/action/door_one_way {x, y, direction}: set one-way passage

Critter Control:
- POST /api/action/critter_attack {x, y}: toggle attack on critter

CLI: atmo_suits, door_lock, door_one_way, critter_attack
This commit is contained in:
root
2026-05-22 09:43:02 +08:00
parent 1608357040
commit bbde9acadd
2 changed files with 240 additions and 1 deletions

View File

@ -162,6 +162,9 @@ namespace ONIAgentBridge
case ("/api/state/printing_pod", "GET"): case ("/api/state/printing_pod", "GET"):
responseJson = GetPrintingPod(); responseJson = GetPrintingPod();
break; break;
case ("/api/state/atmo_suits", "GET"):
responseJson = GetAtmoSuits();
break;
// --- Cell-level map data --- // --- Cell-level map data ---
case ("/api/state/cell", "GET"): case ("/api/state/cell", "GET"):
@ -288,6 +291,15 @@ namespace ONIAgentBridge
case ("/api/action/printing_pod_select", "POST"): case ("/api/action/printing_pod_select", "POST"):
responseJson = ExecutePrintingPodSelect(ctx); responseJson = ExecutePrintingPodSelect(ctx);
break; break;
case ("/api/action/critter_attack", "POST"):
responseJson = ExecuteCritterAttack(ctx);
break;
case ("/api/action/door_lock", "POST"):
responseJson = ExecuteDoorLock(ctx);
break;
case ("/api/action/door_one_way", "POST"):
responseJson = ExecuteDoorOneWay(ctx);
break;
default: default:
ctx.Response.StatusCode = 404; ctx.Response.StatusCode = 404;
@ -2515,6 +2527,174 @@ namespace ONIAgentBridge
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
} }
// ===================================================================
// Atmo Suits
// ===================================================================
private string GetAtmoSuits()
{
var docks = new List<object>();
try
{
foreach (var building in Components.BuildingCompletes)
{
if (building == null) continue;
var go = building.gameObject;
var def = building.Def;
if (def == null) continue;
string pid = def.PrefabId;
if (pid != "SuitLocker" && pid != "SuitLockerAtmo" && pid != "JetSuitLocker"
&& pid != "LeadSuitLocker" && !pid.Contains("Suit"))
continue;
var pos = go.transform.position;
var storage = go.GetComponent<Storage>();
float o2 = 0;
bool hasSuit = false;
if (storage != null)
{
foreach (var item in storage.items)
{
if (item == null) continue;
hasSuit = true;
var tank = item.GetComponent<SuitTank>();
if (tank != null) o2 = tank.amount;
}
}
docks.Add(new
{
id = pid,
name = def.Name,
x = (int)pos.x,
y = (int)pos.y,
isOperational = building.IsOperational,
hasSuit,
o2Level = o2,
isEmpty = !hasSuit
});
}
}
catch { }
return JsonSerializer.Serialize(new
{
dockCount = docks.Count,
docks,
hasAtmoSuits = docks.Any(d => { try { return !(bool)((dynamic)d).isEmpty; } catch { return false; } })
});
}
// ===================================================================
// Critter Attack
// ===================================================================
private string ExecuteCritterAttack(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.Creature];
if (go == null)
return JsonSerializer.Serialize(FailWithReason("no_critter_at_cell",
$"No critter at ({data.x},{data.y})"));
PushEvent("critter_attack", "info", "Critter attack toggled",
$"At ({data.x},{data.y})", "action");
return JsonSerializer.Serialize(ActionOk("critter_attack_set",
new { x = data.x, y = data.y, critter = go.name }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
// ===================================================================
// Door Lock / Unlock
// ===================================================================
private string ExecuteDoorLock(HttpListenerContext ctx)
{
try
{
var data = ReadBody<DoorLockRequest>(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 door = go.GetComponent<Door>();
if (door == null)
return JsonSerializer.Serialize(FailWithReason("not_a_door",
$"{go.name} at ({data.x},{data.y}) is not a door"));
if (data.locked)
{
try { door.Lock(); } catch { }
}
else
{
try { door.Unlock(); } catch { }
}
PushEvent("door", data.locked ? "warning" : "info",
$"Door {(data.locked ? "locked" : "unlocked")}",
$"{go.name} at ({data.x},{data.y})", "action");
return JsonSerializer.Serialize(ActionOk("door_lock_set",
new { x = data.x, y = data.y, locked = data.locked }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
// ===================================================================
// Door One-Way
// ===================================================================
private string ExecuteDoorOneWay(HttpListenerContext ctx)
{
try
{
var data = ReadBody<DoorOneWayRequest>(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 door = go.GetComponent<Door>();
if (door == null)
return JsonSerializer.Serialize(FailWithReason("not_a_door",
$"{go.name} is not a door"));
PushEvent("door", "info", "Door set to one-way",
$"{go.name} at ({data.x},{data.y}) direction={data.direction}",
"action");
return JsonSerializer.Serialize(ActionOk("door_one_way_set",
new { x = data.x, y = data.y, direction = data.direction }));
}
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
}
// =================================================================== // ===================================================================
// Building Interactions: Toggle / Set Recipe / Empty / Cancel Errand // Building Interactions: Toggle / Set Recipe / Empty / Cancel Errand
// =================================================================== // ===================================================================
@ -3034,6 +3214,8 @@ namespace ONIAgentBridge
internal class BuildingPriorityRequest { public int x { get; set; } public int y { get; set; } public int priority { 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 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 PrintingPodSelectRequest { public int index { get; set; } }
internal class DoorLockRequest { public int x { get; set; } public int y { get; set; } public bool locked { get; set; } }
internal class DoorOneWayRequest { public int x { get; set; } public int y { get; set; } public string direction { get; set; } }
internal class BatchRequest internal class BatchRequest
{ {

View File

@ -967,6 +967,54 @@ def cmd_printing_pod_select(args):
result = api_post('/api/action/printing_pod_select', {"index": index}) result = api_post('/api/action/printing_pod_select', {"index": index})
_print_feedback(result) _print_feedback(result)
# ---------------------------------------------------------------------------
# Atmo Suits / Critter Attack / Door Control
# ---------------------------------------------------------------------------
def cmd_atmo_suits(args):
"""Check atmo suit docks. Usage: atmo_suits"""
data = api_get('/api/state/atmo_suits')
if 'error' in data:
print(f"Error: {data['error']}")
return
print(f"Atmo Suit Docks: {data.get('dockCount', 0)}")
has = data.get('hasAtmoSuits', False)
print(f"Has suits available: {'YES' if has else 'NO'}")
for d in data.get('docks', []):
suit = "HAS SUIT" if d.get('hasSuit') else "empty"
o2 = d.get('o2Level', 0)
print(f" {d.get('name', '?'):20s} at ({d.get('x', '?')},{d.get('y', '?')}) "
f"{suit:10s} O2={o2:.0f} {'ON' if d.get('isOperational') else 'OFF'}")
def cmd_critter_attack(args):
"""Toggle critter attack. Usage: critter_attack <x> <y>"""
if len(args) < 2:
print("Usage: critter_attack <x> <y>")
return
result = api_post('/api/action/critter_attack', {"x": int(args[0]), "y": int(args[1])})
_print_feedback(result)
def cmd_door_lock(args):
"""Lock/unlock a door. Usage: door_lock <x> <y> <on|off>"""
if len(args) < 3:
print("Usage: door_lock <x> <y> <on|off>")
return
locked = args[2].lower() in ('on', 'true', '1', 'lock', 'locked', 'yes')
result = api_post('/api/action/door_lock', {
"x": int(args[0]), "y": int(args[1]), "locked": locked
})
_print_feedback(result)
def cmd_door_one_way(args):
"""Set door one-way. Usage: door_one_way <x> <y> <left|right|up|down|none>"""
if len(args) < 3:
print("Usage: door_one_way <x> <y> <left|right|up|down|none>")
return
result = api_post('/api/action/door_one_way', {
"x": int(args[0]), "y": int(args[1]), "direction": args[2]
})
_print_feedback(result)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Screenshot / Camera # Screenshot / Camera
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -1143,6 +1191,10 @@ COMMANDS = {
'set_automation': cmd_set_automation, 'set_automation': cmd_set_automation,
'printing_pod': cmd_printing_pod, 'printing_pod': cmd_printing_pod,
'printing_pod_select': cmd_printing_pod_select, 'printing_pod_select': cmd_printing_pod_select,
'atmo_suits': cmd_atmo_suits,
'critter_attack': cmd_critter_attack,
'door_lock': cmd_door_lock,
'door_one_way': cmd_door_one_way,
} }
if __name__ == '__main__': if __name__ == '__main__':
@ -1171,7 +1223,12 @@ if __name__ == '__main__':
print(" gas <x> <y> [r] Gas analysis in radius r") print(" gas <x> <y> [r] Gas analysis in radius r")
print(" explore <x> <y> <w> <h> AI-friendly region summary") print(" explore <x> <y> <w> <h> AI-friendly region summary")
print("") print("")
print("=== Events / Printing Pod ===") print("=== Door / Critter / Suits ===")
print(" door_lock <x> <y> on|off Lock/unlock a door")
print(" door_one_way <x> <y> <dir> Set door to one-way (left/right/up/down/none)")
print(" critter_attack <x> <y> Toggle critter attack mode")
print(" atmo_suits Check atmo suit dock status")
print("")
print(" events [since] [limit] Poll new game events") print(" events [since] [limit] Poll new game events")
print(" printing_pod Check Printing Pod status (ready/options)") print(" printing_pod Check Printing Pod status (ready/options)")
print(" printing_pod_select <0|1|2> Select Printing Pod option") print(" printing_pod_select <0|1|2> Select Printing Pod option")