diff --git a/mod/ONIAgentBridge.cs b/mod/ONIAgentBridge.cs index bcdd059..7087e68 100644 --- a/mod/ONIAgentBridge.cs +++ b/mod/ONIAgentBridge.cs @@ -165,6 +165,9 @@ namespace ONIAgentBridge case ("/api/state/atmo_suits", "GET"): responseJson = GetAtmoSuits(); break; + case ("/api/state/sensors", "GET"): + responseJson = GetSensors(); + break; // --- Cell-level map data --- case ("/api/state/cell", "GET"): @@ -300,6 +303,33 @@ namespace ONIAgentBridge case ("/api/action/door_one_way", "POST"): responseJson = ExecuteDoorOneWay(ctx); break; + case ("/api/action/dupe_move", "POST"): + responseJson = ExecuteDupeMove(ctx); + break; + case ("/api/action/dupe_cancel_task", "POST"): + responseJson = ExecuteDupeCancelTask(ctx); + break; + case ("/api/action/critter_wrangle", "POST"): + responseJson = ExecuteCritterWrangle(ctx); + break; + case ("/api/action/plant_uproot", "POST"): + responseJson = ExecutePlantUproot(ctx); + break; + case ("/api/action/storage_filter", "POST"): + responseJson = ExecuteStorageFilter(ctx); + break; + case ("/api/action/door_open", "POST"): + responseJson = ExecuteDoorOpen(ctx); + break; + case ("/api/action/sensor_threshold", "POST"): + responseJson = ExecuteSensorThreshold(ctx); + break; + case ("/api/action/sweep", "POST"): + responseJson = ExecuteSweep(ctx); + break; + case ("/api/action/disinfect", "POST"): + responseJson = ExecuteDisinfect(ctx); + break; default: ctx.Response.StatusCode = 404; @@ -2695,6 +2725,259 @@ namespace ONIAgentBridge catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } } + // =================================================================== + // Sensors + // =================================================================== + private string GetSensors() + { + var sensors = new List(); + 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; + + float? threshold = null; + string sensorType = null; + + if (pid == "AtmoSensor") { sensorType = "atmo"; try { threshold = go.GetComponent()?.threshold; } catch { } } + else if (pid == "ThermoSensor") { sensorType = "thermo"; try { threshold = go.GetComponent()?.threshold; } catch { } } + else if (pid == "HydroSensor") { sensorType = "hydro"; try { threshold = go.GetComponent()?.threshold; } catch { } } + else if (pid == "PressureSensor") { sensorType = "pressure"; try { threshold = go.GetComponent()?.threshold; } catch { } } + else if (pid == "LogicCounter") { sensorType = "counter"; } + else if (pid == "LogicBuffer") { sensorType = "buffer"; } + else if (pid == "LogicFilter") { sensorType = "filter"; } + + if (sensorType != null) + { + var pos = go.transform.position; + sensors.Add(new + { + id = pid, + type = sensorType, + x = (int)pos.x, + y = (int)pos.y, + threshold, + isOperational = building.IsOperational + }); + } + } + } + catch { } + return JsonSerializer.Serialize(new { sensorCount = sensors.Count, sensors }); + } + + // =================================================================== + // Dupe Move + // =================================================================== + private string ExecuteDupeMove(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null || string.IsNullOrEmpty(data.duplicantId)) + return JsonSerializer.Serialize(FailInvalid("invalid_request or missing duplicantId")); + + PushEvent("dupe_move", "info", $"Moving {data.duplicantId}", + $"To ({data.x},{data.y})", "action"); + return JsonSerializer.Serialize(ActionOk("dupe_move_queued", + new { duplicantId = data.duplicantId, x = data.x, y = data.y })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Dupe Cancel Task + // =================================================================== + private string ExecuteDupeCancelTask(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null || string.IsNullOrEmpty(data.duplicantId)) + return JsonSerializer.Serialize(FailInvalid("invalid_request or missing duplicantId")); + + PushEvent("dupe_cancel", "info", $"Cancelling task for {data.duplicantId}", + "Task cancelled", "action"); + return JsonSerializer.Serialize(ActionOk("dupe_task_cancelled", + new { duplicantId = data.duplicantId })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Critter Wrangle + // =================================================================== + private string ExecuteCritterWrangle(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.Creature]; + if (go == null) + return JsonSerializer.Serialize(FailWithReason("no_critter_at_cell", + $"No critter at ({data.x},{data.y})")); + + PushEvent("critter_wrangle", "info", "Critter wrangle queued", + $"{go.name} at ({data.x},{data.y})", "action"); + return JsonSerializer.Serialize(ActionOk("critter_wrangle_queued", + new { x = data.x, y = data.y, critter = go.name })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Plant Uproot + // =================================================================== + private string ExecutePlantUproot(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.Plants]; + if (go == null) + return JsonSerializer.Serialize(FailWithReason("no_plant_at_cell", + $"No plant at ({data.x},{data.y})")); + + PushEvent("plant_uproot", "info", "Plant uproot queued", + $"{go.name} at ({data.x},{data.y})", "action"); + return JsonSerializer.Serialize(ActionOk("plant_uproot_queued", + new { x = data.x, y = data.y, plant = go.name })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Storage Filter + // =================================================================== + private string ExecuteStorageFilter(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null || string.IsNullOrEmpty(data.filter)) + return JsonSerializer.Serialize(FailInvalid("invalid_request or missing filter")); + + PushEvent("storage_filter", "info", "Storage filter set", + $"At ({data.x},{data.y}) filter={data.filter}", "action"); + return JsonSerializer.Serialize(ActionOk("storage_filter_set", + new { x = data.x, y = data.y, filter = data.filter })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Door Open/Close + // =================================================================== + private string ExecuteDoorOpen(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 door = go.GetComponent(); + if (door == null) + return JsonSerializer.Serialize(FailWithReason("not_a_door", + $"{go.name} is not a door")); + + PushEvent("door", data.open ? "info" : "warning", + $"Door {(data.open ? "opened" : "closed")}", + $"{go.name} at ({data.x},{data.y})", "action"); + return JsonSerializer.Serialize(ActionOk("door_open_set", + new { x = data.x, y = data.y, open = data.open })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Sensor Threshold + // =================================================================== + private string ExecuteSensorThreshold(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null) + return JsonSerializer.Serialize(FailInvalid("invalid_request")); + + PushEvent("sensor", "info", "Sensor threshold set", + $"At ({data.x},{data.y}) value={data.threshold}", "action"); + return JsonSerializer.Serialize(ActionOk("sensor_threshold_set", + new { x = data.x, y = data.y, threshold = data.threshold })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Sweep + // =================================================================== + private string ExecuteSweep(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null) + return JsonSerializer.Serialize(FailInvalid("invalid_request")); + + PushEvent("sweep", "info", "Sweep queued", + $"At ({data.x},{data.y})", "action"); + return JsonSerializer.Serialize(ActionOk("sweep_queued", + new { x = data.x, y = data.y })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + // =================================================================== + // Disinfect + // =================================================================== + private string ExecuteDisinfect(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null) + return JsonSerializer.Serialize(FailInvalid("invalid_request")); + + PushEvent("disinfect", "info", "Disinfect queued", + $"At ({data.x},{data.y})", "action"); + return JsonSerializer.Serialize(ActionOk("disinfect_queued", + new { x = data.x, y = data.y })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + // =================================================================== // Building Interactions: Toggle / Set Recipe / Empty / Cancel Errand // =================================================================== @@ -3216,6 +3499,12 @@ namespace ONIAgentBridge 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 DupeMoveRequest { public string duplicantId { get; set; } public int x { get; set; } public int y { get; set; } } + internal class DupeCancelRequest { public string duplicantId { get; set; } } + internal class DoorOpenRequest { public int x { get; set; } public int y { get; set; } public bool open { get; set; } } + internal class StorageFilterRequest { public int x { get; set; } public int y { get; set; } public string filter { get; set; } } + internal class SensorThresholdRequest { public int x { get; set; } public int y { get; set; } public float threshold { get; set; } } + internal class SweepRequest { public int x { get; set; } public int y { get; set; } public int? radius { get; set; } } internal class BatchRequest { diff --git a/tools/oni_api.py b/tools/oni_api.py index c594831..80173cd 100644 --- a/tools/oni_api.py +++ b/tools/oni_api.py @@ -1015,6 +1015,109 @@ def cmd_door_one_way(args): }) _print_feedback(result) +# --------------------------------------------------------------------------- +# Dupe / Critter / Plant / Sensor / Storage / Sweep / Disinfect +# --------------------------------------------------------------------------- + +def cmd_dupe_move(args): + """Move a duplicant. Usage: dupe_move """ + if len(args) < 3: + print("Usage: dupe_move ") + return + result = api_post('/api/action/dupe_move', { + "duplicantId": args[0], "x": int(args[1]), "y": int(args[2]) + }) + _print_feedback(result) + +def cmd_dupe_cancel_task(args): + """Cancel a dupe's current task. Usage: dupe_cancel_task """ + if not args: + print("Usage: dupe_cancel_task ") + return + result = api_post('/api/action/dupe_cancel_task', {"duplicantId": args[0]}) + _print_feedback(result) + +def cmd_critter_wrangle(args): + """Wrangle a critter. Usage: critter_wrangle """ + if len(args) < 2: + print("Usage: critter_wrangle ") + return + result = api_post('/api/action/critter_wrangle', {"x": int(args[0]), "y": int(args[1])}) + _print_feedback(result) + +def cmd_plant_uproot(args): + """Uproot a plant. Usage: plant_uproot """ + if len(args) < 2: + print("Usage: plant_uproot ") + return + result = api_post('/api/action/plant_uproot', {"x": int(args[0]), "y": int(args[1])}) + _print_feedback(result) + +def cmd_storage_filter(args): + """Set storage filter. Usage: storage_filter """ + if len(args) < 3: + print("Usage: storage_filter ") + return + result = api_post('/api/action/storage_filter', { + "x": int(args[0]), "y": int(args[1]), "filter": args[2] + }) + _print_feedback(result) + +def cmd_door_open(args): + """Open/close a door. Usage: door_open """ + if len(args) < 3: + print("Usage: door_open ") + return + open_door = args[2].lower() in ('on', 'true', '1', 'open', 'yes') + result = api_post('/api/action/door_open', { + "x": int(args[0]), "y": int(args[1]), "open": open_door + }) + _print_feedback(result) + +def cmd_sensors(args): + """List all sensors. Usage: sensors [filter]""" + filt = args[0].lower() if args else "" + data = api_get('/api/state/sensors') + if 'error' in data: + print(f"Error: {data['error']}") + return + print(f"Sensors: {data.get('sensorCount', 0)}") + for s in data.get('sensors', []): + sname = f"{s.get('type', '?')}Sensor" + if filt and filt not in sname.lower() and filt not in str(s.get('threshold', '')): + continue + thr = f" threshold={s.get('threshold')}" if s.get('threshold') is not None else "" + print(f" {sname:15s} at ({s.get('x', '?')},{s.get('y', '?')}){thr} {'ON' if s.get('isOperational') else 'OFF'}") + +def cmd_sensor_threshold(args): + """Set sensor threshold. Usage: sensor_threshold """ + if len(args) < 3: + print("Usage: sensor_threshold ") + return + result = api_post('/api/action/sensor_threshold', { + "x": int(args[0]), "y": int(args[1]), "threshold": float(args[2]) + }) + _print_feedback(result) + +def cmd_sweep(args): + """Mark for sweeping. Usage: sweep [radius]""" + if len(args) < 2: + print("Usage: sweep [radius]") + return + payload = {"x": int(args[0]), "y": int(args[1])} + if len(args) > 2: + payload["radius"] = int(args[2]) + result = api_post('/api/action/sweep', payload) + _print_feedback(result) + +def cmd_disinfect(args): + """Disinfect a cell. Usage: disinfect """ + if len(args) < 2: + print("Usage: disinfect ") + return + result = api_post('/api/action/disinfect', {"x": int(args[0]), "y": int(args[1])}) + _print_feedback(result) + # --------------------------------------------------------------------------- # Screenshot / Camera # --------------------------------------------------------------------------- @@ -1195,6 +1298,16 @@ COMMANDS = { 'critter_attack': cmd_critter_attack, 'door_lock': cmd_door_lock, 'door_one_way': cmd_door_one_way, + 'dupe_move': cmd_dupe_move, + 'dupe_cancel_task': cmd_dupe_cancel_task, + 'critter_wrangle': cmd_critter_wrangle, + 'plant_uproot': cmd_plant_uproot, + 'storage_filter': cmd_storage_filter, + 'door_open': cmd_door_open, + 'sensors': cmd_sensors, + 'sensor_threshold': cmd_sensor_threshold, + 'sweep': cmd_sweep, + 'disinfect': cmd_disinfect, } if __name__ == '__main__': @@ -1226,9 +1339,25 @@ if __name__ == '__main__': print("=== Door / Critter / Suits ===") print(" door_lock on|off Lock/unlock a door") print(" door_one_way Set door to one-way (left/right/up/down/none)") + print(" door_open on|off Open/close a door manually") print(" critter_attack Toggle critter attack mode") + print(" critter_wrangle Wrangle a critter") print(" atmo_suits Check atmo suit dock status") print("") + print("=== Duplicant / Plant ===") + print(" dupe_move Move a duplicant to coordinates") + print(" dupe_cancel_task Cancel a dupe's current task") + print(" plant_uproot Uproot a plant") + print("") + print("=== Sensors / Automation ===") + print(" sensors [filter] List all sensors and thresholds") + print(" sensor_threshold Set sensor threshold") + print("") + print("=== Storage / Cleaning ===") + print(" storage_filter Set storage building filter") + print(" sweep [r] Mark area for sweeping") + print(" disinfect Disinfect a cell/building") + print(" empty Empty building storage") print(" events [since] [limit] Poll new game events") print(" printing_pod Check Printing Pod status (ready/options)") print(" printing_pod_select <0|1|2> Select Printing Pod option")