#!/usr/bin/env python3 """ ONI Agent API Client ==================== CLI tool for interacting with the ONI Agent Bridge Mod (v2). All commands return structured JSON data. Usage: python oni_api.py [args...] Commands: # Status health Check mod connection status Full game overview resources All resource amounts buildings All buildings duplicants Duplicant details research Tech progress rooms Room layout # Map data cell Single cell details cells Rectangular area slice Row/column scan gas Gas distribution # Registry registry buildings [filter] Building definitions registry elements [filter] Element definitions registry techs Tech tree # Events events [since] Poll events since sequence number # Actions dig Dig area build Build structure deconstruct Deconstruct prioritize

Set priority (1-9) research Set active research mop Mop liquid harvest Harvest plant pause [reason] Pause game unpause [speed] Resume game speed <1|2|3> Set game speed batch Execute batch operations camera [zoom] Move camera save [name] Save game load Load save priority_global

Set global priority priority_type

Set type priority # Utility explore AI-friendly area summary """ import json, os, sys, urllib.request, urllib.error # Fix GBK encoding on Windows CJK consoles if sys.stdout.encoding and sys.stdout.encoding.upper() in ('GBK', 'GB2312', 'CP936'): import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') CONFIG_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json') def load_config(): with open(CONFIG_PATH) as f: return json.load(f) def api_get(endpoint): cfg = load_config() url = f"http://{cfg['modHost']}:{cfg['modPort']}{endpoint}" try: req = urllib.request.Request(url, method='GET') with urllib.request.urlopen(req, timeout=cfg['timeout']) as resp: return json.loads(resp.read().decode()) except urllib.error.URLError as e: return {"success": False, "error": "connection_error", "errorMessage": str(e)} def api_post(endpoint, data): cfg = load_config() url = f"http://{cfg['modHost']}:{cfg['modPort']}{endpoint}" try: req = urllib.request.Request( url, data=json.dumps(data).encode(), headers={'Content-Type': 'application/json'}, method='POST' ) with urllib.request.urlopen(req, timeout=cfg['timeout']) as resp: return json.loads(resp.read().decode()) except urllib.error.URLError as e: return {"success": False, "error": "connection_error", "errorMessage": str(e)} def get_data(endpoint): """Get the data field from a response, handling the wrapper.""" r = api_get(endpoint) if r.get("success"): return r.get("data", r) print(f"Error: {r.get('errorMessage', r.get('error', 'unknown'))}", file=sys.stderr) return None def post_action(endpoint, data=None): """Post and print result.""" r = api_post(endpoint, data or {}) if r.get("success"): d = r.get("data", {}) if d: print(json.dumps(d, indent=2, ensure_ascii=False)) else: print("OK") else: print(f"Error: {r.get('errorMessage', r.get('error', 'unknown'))}", file=sys.stderr) return r # ── Commands ───────────────────────────────────────────────── def cmd_health(args): r = api_get('/health') # Support both new format {success, data} and old format {status} if r.get("success"): d = r.get("data", r) print(f"Status: {d.get('status', '?')}") print(f"Version: {d.get('version', 'unknown — old mod, deploy v2')}") elif r.get("status") == "ok": print(f"Status: ok (old mod v1 — restart game to load v2)") else: print(f"Connection failed: {r}") def cmd_status(args): game = get_data('/api/state/game') if not game: return resources = get_data('/api/state/resources') or [] dups = get_data('/api/state/duplicants') or [] alerts = get_data('/api/state/alert') or [] print(f"=== Game ===") print(f" Cycle: {game.get('cycle', '?')}") print(f" Dupes: {game.get('duplicantCount', '?')}") print(f" Grid: {game.get('gridWidth', '?')} x {game.get('gridHeight', '?')}") print(f" Paused: {game.get('isPaused', '?')}") print(f" Speed: {game.get('gameSpeed', '?')}x") print(f"\n=== Top Resources ===") if isinstance(resources, list): for r in sorted(resources, key=lambda x: x.get('amountKg', 0), reverse=True)[:20]: icon = {'solid': '■', 'liquid': '≈', 'gas': '◌'}.get(r.get('state', ''), '?') print(f" {icon} {r.get('name', '?'):20s} {r.get('amountKg', 0):>10.1f} kg") print(f"\n=== Duplicants ===") for d in dups if isinstance(dups, list) else []: print(f" {d.get('name', '?'):12s} at ({d.get('x', '?'):4d},{d.get('y', '?'):4d}) hp={d.get('health', 0)}") if alerts: print(f"\n=== Alerts ===") for a in alerts: print(f" [{a.get('severity', '?')}] {a.get('title', '?')}") def cmd_resources(args): data = get_data('/api/state/resources') if data: print(json.dumps(data, indent=2, ensure_ascii=False)) def cmd_buildings(args): data = get_data('/api/state/buildings') if data: print(json.dumps(data, indent=2, ensure_ascii=False)) def cmd_duplicants(args): data = get_data('/api/state/duplicants') if data: print(json.dumps(data, indent=2, ensure_ascii=False)) def cmd_research(args): data = get_data('/api/state/research') if data: print(json.dumps(data, indent=2, ensure_ascii=False)) def cmd_rooms(args): data = get_data('/api/state/rooms') if data: print(json.dumps(data, indent=2, ensure_ascii=False)) def cmd_cell(args): if len(args) < 2: print("Usage: cell ", file=sys.stderr); return x, y = int(args[0]), int(args[1]) r = api_get(f'/api/state/cell?x={x}&y={y}') if r.get("success"): d = r.get("data", {}) print(f"Cell ({d.get('x')}, {d.get('y')}):") print(f" Element: {d.get('element', '?')} ({d.get('elementId', '?')})") print(f" Mass: {d.get('massKg', 0):.2f} kg") print(f" Temp: {d.get('temperatureC', 0):.1f} °C") print(f" State: {'Solid' if d.get('isSolid') else 'Liquid' if d.get('isLiquid') else 'Gas' if d.get('isGas') else 'Vacuum'}") print(f" Building: {d.get('buildingName', 'None')}") print(f" Diggable: {d.get('isDiggable', False)}") print(f" Duplicant: {d.get('hasDuplicant', False)}") else: print(f"Error: {r.get('errorMessage', r.get('error', 'unknown'))}", file=sys.stderr) def cmd_cells(args): if len(args) < 4: print("Usage: cells ", file=sys.stderr); return x, y, w, h = int(args[0]), int(args[1]), int(args[2]), int(args[3]) r = api_get(f'/api/state/cells?x={x}&y={y}&width={w}&height={h}') if r.get("success"): d = r.get("data", {}) cells = d.get("cells", []) print(f"{len(cells)} cells in ({x},{y})-({x+w},{y+h}):") for c in cells: icon = '■' if c.get('isSolid') else '≈' if c.get('isLiquid') else '◌' if c.get('isGas') else ' ' b = '⚙' if c.get('hasBuilding') else ' ' d = '🧑' if c.get('hasDuplicant') else ' ' print(f" ({c['x']:3d},{c['y']:3d}) {icon} {c.get('element','?'):15s} {c.get('massKg',0):6.1f}kg {c.get('temperatureC',0):5.1f}°C{b}{d}") else: print(f"Error: {r.get('errorMessage', r.get('error', 'unknown'))}", file=sys.stderr) def cmd_slice(args): if len(args) < 3: print("Usage: slice ", file=sys.stderr); return axis, idx, start, end = args[0], int(args[1]), int(args[2]), int(args[3]) r = api_get(f'/api/state/cells/slice?axis={axis}&index={idx}&start={start}&end={end}') if r.get("success"): d = r.get("data", {}) cells = d.get("cells", []) for c in cells: icon = '■' if c.get('isSolid') else '≈' if c.get('isLiquid') else '◌' if c.get('isGas') else ' ' print(f" ({c['x']:3d},{c['y']:3d}) {icon} {c.get('element','?'):15s} {c.get('massKg',0):6.1f}kg {c.get('temperatureC',0):5.1f}°C") else: print(f"Error: {r.get('errorMessage', r.get('error', 'unknown'))}", file=sys.stderr) def cmd_gas(args): if len(args) < 3: print("Usage: gas ", file=sys.stderr); return x, y, r = int(args[0]), int(args[1]), int(args[2]) result = api_get(f'/api/state/gas?x={x}&y={y}&radius={r}') if result.get("success"): d = result.get("data", {}) gases = d.get("gases", []) total = sum(g.get('mass', 0) for g in gases) print(f"Gas analysis around ({x},{y}) radius {r}:") for g in sorted(gases, key=lambda x: x.get('mass', 0), reverse=True): pct = g['mass'] / total * 100 if total > 0 else 0 print(f" {g['gas']:20s} {g['mass']:8.1f} kg ({pct:4.1f}%) across {g.get('count', 0)} cells") else: print(f"Error: {result.get('errorMessage', result.get('error', 'unknown'))}", file=sys.stderr) def cmd_registry(args): if not args: print("Usage: registry [filter]", file=sys.stderr); return sub = args[0] filt = args[1] if len(args) > 1 else None if sub == "buildings": data = get_data('/api/registry/buildings') elif sub == "elements": data = get_data('/api/registry/elements') elif sub == "techs": data = get_data('/api/registry/techs') else: print(f"Unknown registry: {sub}", file=sys.stderr); return if not data: return if filt: filt_lower = filt.lower() data = [x for x in data if filt_lower in x.get('id', '').lower() or filt_lower in x.get('name', '').lower()] if isinstance(data, list): if sub == "buildings": for b in sorted(data, key=lambda x: x.get('id', '')): print(f" {b.get('id', '?'):30s} {b.get('name', '?'):30s} {b.get('width',0)}x{b.get('height',0)} rule={b.get('buildLocationRule','?')} mat={b.get('materialCategory','?')}") elif sub == "elements": for e in sorted(data, key=lambda x: x.get('name', '')): print(f" {e.get('name', '?'):20s} id={e.get('id', '?'):30s} state={e.get('state','?'):8s} SHC={e.get('specificHeatCapacity',0):.1f} TC={e.get('thermalConductivity',0):.2f}") elif sub == "techs": for t in sorted(data, key=lambda x: x.get('id', '')): reqs = ",".join(t.get('requiredTechs', [])) print(f" {t.get('id', '?'):30s} {t.get('name', '?'):30s} req={reqs}") else: print(json.dumps(data, indent=2, ensure_ascii=False)) def cmd_events(args): since = int(args[0]) if args else -1 r = api_get(f'/api/state/events?since={since}') if r.get("success"): d = r.get("data", {}) events = d.get("events", []) for e in events: print(f" [{e.get('id',0):4d}] [{e.get('severity','?'):8s}] [{e.get('category','?')}] {e.get('title','')} {e.get('message','')}") if events: print(f" --- next_seq={d.get('nextSeq', since)} (showing {len(events)} events) ---") else: print(f"Error: {r.get('errorMessage', r.get('error', 'unknown'))}", file=sys.stderr) def cmd_dig(args): if len(args) < 4: print("Usage: dig ", file=sys.stderr); return x, y, w, h = int(args[0]), int(args[1]), int(args[2]), int(args[3]) post_action('/api/action/dig', {"x": x, "y": y, "width": w, "height": h}) def cmd_build(args): if len(args) < 3: print("Usage: build ", file=sys.stderr); return bid, x, y = args[0], int(args[1]), int(args[2]) r = post_action('/api/action/build', {"buildingId": bid, "x": x, "y": y}) if r.get("success"): d = r.get("data", {}) if d.get("materialInfo"): print("\nMaterial check:") for m in d["materialInfo"]: status = "ok" if m.get("available") else "missing" print(f" [{status}] {m.get('category','?')}: {m.get('bestElement','none available')}") def cmd_deconstruct(args): if len(args) < 2: print("Usage: deconstruct ", file=sys.stderr); return x, y = int(args[0]), int(args[1]) post_action('/api/action/deconstruct', {"x": x, "y": y}) def cmd_prioritize(args): if len(args) < 3: print("Usage: prioritize ", file=sys.stderr); return x, y, p = int(args[0]), int(args[1]), int(args[2]) post_action('/api/action/prioritize', {"x": x, "y": y, "priority": p}) def cmd_research_select(args): if not args: print("Usage: research ", file=sys.stderr); return post_action('/api/action/research', {"techId": args[0]}) def cmd_mop(args): if len(args) < 2: print("Usage: mop ", file=sys.stderr); return x, y = int(args[0]), int(args[1]) post_action('/api/action/mop', {"x": x, "y": y}) def cmd_harvest(args): if len(args) < 2: print("Usage: harvest ", file=sys.stderr); return x, y = int(args[0]), int(args[1]) post_action('/api/action/harvest', {"x": x, "y": y}) def cmd_pause(args): reason = " ".join(args) if args else None r = api_post('/api/action/pause', {"reason": reason or "user request"}) if r.get("success"): print("Paused") else: print(f"Error: {r.get('errorMessage', r.get('error'))}", file=sys.stderr) def cmd_unpause(args): speed = int(args[0]) if args else 1 r = api_post('/api/action/unpause', {"speed": speed}) if r.get("success"): print(f"Unpaused at {speed}x") else: print(f"Error: {r.get('errorMessage', r.get('error'))}", file=sys.stderr) def cmd_speed(args): if not args: print("Usage: speed <1|2|3>", file=sys.stderr); return s = int(args[0]) r = api_post('/api/action/speed', {"speed": s}) if r.get("success"): print(f"Speed set to {s}x") else: print(f"Error: {r.get('errorMessage', r.get('error'))}", file=sys.stderr) def cmd_batch(args): if not args: print("Usage: batch ", file=sys.stderr); return with open(args[0]) as f: actions = json.load(f) if isinstance(actions, list): actions = {"actions": actions} r = api_post('/api/action/batch', actions) if r.get("success"): d = r.get("data", {}) print(f"Batch: {d.get('successCount',0)} succeeded, {d.get('failCount',0)} failed of {d.get('total',0)}") for res in d.get("results", []): st = "✓" if res.get("success") else "✗" print(f" {st} {res.get('type','?'):15s} {res.get('buildingId','')}") else: print(f"Error: {r.get('errorMessage', r.get('error'))}", file=sys.stderr) def cmd_save(args): name = args[0] if args else None post_action('/api/action/save', {"name": name}) def cmd_load(args): if not args: print("Usage: load ", file=sys.stderr); return post_action('/api/action/load', {"name": args[0]}) def cmd_priority_global(args): if len(args) < 2: print("Usage: priority_global ", file=sys.stderr); return post_action('/api/action/priority_global', {"target": args[0], "priority": int(args[1])}) def cmd_priority_type(args): if len(args) < 2: print("Usage: priority_type ", file=sys.stderr); return post_action('/api/action/priority_type', {"buildingType": args[0], "priority": int(args[1])}) def cmd_camera(args): if len(args) < 2: print("Usage: camera [zoom]", file=sys.stderr); return x, y = int(args[0]), int(args[1]) body = {"x": x, "y": y} if len(args) > 2: body["zoom"] = float(args[2]) post_action('/api/action/camera', body) def cmd_explore(args): """AI-friendly area summary: combines cell data with building/dupe info.""" if len(args) < 4: print("Usage: explore ", file=sys.stderr); return x, y, w, h = int(args[0]), int(args[1]), int(args[2]), int(args[3]) cells_r = api_get(f'/api/state/cells?x={x}&y={y}&width={w}&height={h}') buildings_r = api_get('/api/state/buildings') dups_r = api_get('/api/state/duplicants') print(f"\n=== Explore ({x},{y}) to ({x+w},{y+h}) ===") # Element distribution if cells_r.get("success"): cells = cells_r["data"].get("cells", []) dist = {} for c in cells: el = c.get("element", "Vacuum") if el not in dist: dist[el] = {"count": 0, "mass": 0, "state": "solid"} dist[el]["count"] += 1 dist[el]["mass"] += c.get("massKg", 0) if c.get("isLiquid"): dist[el]["state"] = "liquid" elif c.get("isGas"): dist[el]["state"] = "gas" elif c.get("isVacuum"): dist[el]["state"] = "vacuum" print("\nElement distribution:") for el, info in sorted(dist.items(), key=lambda x: x[1]["count"], reverse=True): print(f" {info['state']:8s} {el:20s} {info['count']:4d} cells {info['mass']:8.1f} kg") diggable = sum(1 for c in cells if c.get("isDiggable")) print(f"\nDiggable cells: {diggable}/{len(cells)}") # Buildings in area if buildings_r.get("success"): buildings = buildings_r.get("data", buildings_r) if isinstance(buildings, list): area_buildings = [b for b in buildings if x <= b.get('x', -1) < x + w and y <= b.get('y', -1) < y + h] if area_buildings: print(f"\nBuildings in area:") for b in area_buildings: print(f" {b.get('id','?'):25s} at ({b.get('x')},{b.get('y')}) {b.get('width',1)}x{b.get('height',1)}") # Dupes in area if dups_r.get("success"): dups = dups_r.get("data", dups_r) if isinstance(dups, list): area_dupes = [d for d in dups if x <= d.get('x', -1) < x + w and y <= d.get('y', -1) < y + h] if area_dupes: print(f"\nDuplicants in area:") for d in area_dupes: print(f" {d.get('name','?'):15s} at ({d.get('x')},{d.get('y')}) hp={d.get('health',0)}") print() CMD_MAP = { "health": cmd_health, "status": cmd_status, "resources": cmd_resources, "buildings": cmd_buildings, "duplicants": cmd_duplicants, "research": cmd_research, "rooms": cmd_rooms, "cell": cmd_cell, "cells": cmd_cells, "slice": cmd_slice, "gas": cmd_gas, "explore": cmd_explore, "registry": cmd_registry, "events": cmd_events, "dig": cmd_dig, "build": cmd_build, "deconstruct": cmd_deconstruct, "prioritize": cmd_prioritize, "research_select": cmd_research_select, "mop": cmd_mop, "harvest": cmd_harvest, "pause": cmd_pause, "unpause": cmd_unpause, "speed": cmd_speed, "batch": cmd_batch, "camera": cmd_camera, "save": cmd_save, "load": cmd_load, "priority_global": cmd_priority_global, "priority_type": cmd_priority_type, } def main(): if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"): print(__doc__) return cmd = sys.argv[1] args = sys.argv[2:] if cmd in CMD_MAP: CMD_MAP[cmd](args) else: print(f"Unknown command: {cmd}", file=sys.stderr) print("Available:", ", ".join(sorted(CMD_MAP.keys())), file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()