feat: screenshot API, camera control, pipe/wire crossing detection

New endpoints:
- POST /api/action/screenshot: capture screenshot to file
- GET /api/screenshot/latest: serve PNG binary
- POST /api/action/camera: move camera to (x,y) + set zoom
- GET /api/state/camera: get current camera position/zoom

Pipe/Wire crossing intelligence:
- build_pipe/build_wire now detect existing pipes/wires at each cell
- mode='line' (default): new pipe merges with existing
- mode='cross': auto-places bridges at intersections to avoid connection
- mode='single': one segment

CLI: snapshot [file.png], camera <x> <y> [zoom], camera_status
All high-level commands: auto pause, execute, resume
This commit is contained in:
root
2026-05-22 09:17:12 +08:00
parent 205ff7668a
commit f91ac2eb95
3 changed files with 402 additions and 44 deletions

View File

@ -702,24 +702,60 @@ def cmd_assign_job(args):
# ---------------------------------------------------------------------------
def cmd_build_pipe_line(args):
"""Build a pipe line. Usage: build_pipe_line <gas|liquid> <x1> <y1> <x2> <y2> [bridge]"""
"""Build a pipe line with crossing mode. Usage: build_pipe_line <gas|liquid> <x1> <y1> <x2> <y2> [mode]"""
if len(args) < 5:
print("Usage: build_pipe_line gas|liquid <x1> <y1> <x2> <y2> [bridge]")
print("Usage: build_pipe_line gas|liquid <x1> <y1> <x2> <y2> [mode]")
print(" mode: 'line' (default, auto-merge), 'cross' (use bridges at intersections), 'single' (one segment)")
return
payload = {"type": args[0], "x1": int(args[1]), "y1": int(args[2]),
"x2": int(args[3]), "y2": int(args[4]), "bridge": len(args) > 5 and args[5] == 'bridge'}
result = api_post('/api/action/build_pipe', payload)
_print_feedback(result)
ptype = args[0]
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
mode = args[5] if len(args) > 5 else 'line'
if mode not in ('line', 'cross', 'single'):
print("[!] mode must be 'line', 'cross', or 'single'")
return
result = api_post('/api/action/build_pipe', {
"type": ptype, "x1": x1, "y1": y1,
"x2": x2, "y2": y2, "mode": mode
})
if result.get('success'):
d = result.get('data', {})
segs = d.get('segmentCount', 0)
bridges = d.get('bridgesPlaced', 0)
xing = sum(1 for s in d.get('segments', []) if s.get('crossing'))
print(f"[OK] {ptype} pipe: {segs} segments, {bridges} bridges, {xing} crossings handled")
if xing > 0:
print(f" Mode '{mode}': {'bridges used at crossings' if mode == 'cross' else 'merged at crossings'}")
else:
_print_feedback(result)
def cmd_build_wire_line(args):
"""Build a wire line. Usage: build_wire_line <type> <x1> <y1> <x2> <y2> [bridge]"""
"""Build a wire line with crossing mode. Usage: build_wire_line <type> <x1> <y1> <x2> <y2> [mode]"""
if len(args) < 5:
print("Usage: build_wire_line regular|heavy|conductive <x1> <y1> <x2> <y2> [bridge]")
print("Usage: build_wire_line regular|heavy|conductive <x1> <y1> <x2> <y2> [mode]")
print(" mode: 'line' (default, auto-merge), 'cross' (use bridges at intersections), 'single' (one segment)")
return
payload = {"type": args[0], "x1": int(args[1]), "y1": int(args[2]),
"x2": int(args[3]), "y2": int(args[4]), "bridge": len(args) > 5 and args[5] == 'bridge'}
result = api_post('/api/action/build_wire', payload)
_print_feedback(result)
wtype = args[0]
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
mode = args[5] if len(args) > 5 else 'line'
if mode not in ('line', 'cross', 'single'):
print("[!] mode must be 'line', 'cross', or 'single'")
return
result = api_post('/api/action/build_wire', {
"type": wtype, "x1": x1, "y1": y1,
"x2": x2, "y2": y2, "mode": mode
})
if result.get('success'):
d = result.get('data', {})
segs = d.get('segmentCount', 0)
bridges = d.get('bridgesPlaced', 0)
xing = sum(1 for s in d.get('segments', []) if s.get('crossing'))
print(f"[OK] {wtype} wire: {segs} segments, {bridges} bridges, {xing} crossings handled")
else:
_print_feedback(result)
def _print_feedback(result):
"""Pretty-print action feedback."""
@ -734,6 +770,52 @@ def _print_feedback(result):
if sug:
print(f" -> {sug}")
# ---------------------------------------------------------------------------
# Screenshot / Camera
# ---------------------------------------------------------------------------
def cmd_snapshot(args):
"""Take a screenshot and save locally. Usage: snapshot [output.png]"""
output = args[0] if args else f"oni_snapshot_{int(__import__('time').time())}.png"
result = api_post('/api/action/screenshot', {})
if not result.get('success'):
print(f"[!] Screenshot failed: {result.get('errorMessage', '')}")
return
filename = result.get('data', {}).get('filename', '?')
print(f"[OK] Screenshot taken: {filename}")
# Download the image
try:
import urllib.request
from oni_api import api_url
url = api_url('/api/screenshot/latest')
urllib.request.urlretrieve(url, output)
print(f"[OK] Saved to {output} ({os.path.getsize(output)} bytes)")
except Exception as e:
print(f"[!] Download failed: {e}")
def cmd_camera(args):
"""Control camera. Usage: camera <x> <y> [zoom]"""
if len(args) < 2:
print("Usage: camera <x> <y> [zoom]")
print(" zoom: 5 (close) to 80 (far), default 30")
return
x, y = int(args[0]), int(args[1])
zoom = float(args[2]) if len(args) > 2 else None
payload = {"x": x, "y": y}
if zoom is not None:
payload["zoom"] = zoom
result = api_post('/api/action/camera', payload)
_print_feedback(result)
def cmd_camera_status(args):
"""Get current camera position. Usage: camera_status"""
data = api_get('/api/state/camera')
if 'error' in data:
print(f"Error: {data['error']}")
return
print(f"Camera at ({data.get('x', 0):.0f}, {data.get('y', 0):.0f}), zoom={data.get('zoom', 30):.0f}")
def cmd_explore(args):
"""AI-friendly exploration: reads a region and returns structured text summary."""
x = int(args[0]) if len(args) > 0 else 0
@ -849,6 +931,9 @@ COMMANDS = {
'assign_job': cmd_assign_job,
'build_pipe_line': cmd_build_pipe_line,
'build_wire_line': cmd_build_wire_line,
'snapshot': cmd_snapshot,
'camera': cmd_camera,
'camera_status': cmd_camera_status,
}
if __name__ == '__main__':
@ -920,6 +1005,11 @@ if __name__ == '__main__':
print(" assign_job <name> <job> Assign dupe to a chore group")
print(" morale Duplicant morale levels")
print("")
print("=== Screenshot / Camera ===")
print(" snapshot [file.png] Take screenshot + save locally")
print(" camera <x> <y> [zoom] Move camera view to coordinates")
print(" camera_status Get current camera position/zoom")
print("")
print("=== Batch / Priority (Advanced) ===")
print(" batch <json_file> Execute batch plan")
print(" priority_global <t> <p> Set global default priority")

View File

@ -141,13 +141,14 @@ def cmd_fix_overload(args):
def cmd_build_pipe_line(args):
"""铺设管道路径。"""
"""铺设管道路径(带交叉模式)。"""
if len(args) < 5:
print("Usage: build_pipe_line gas|liquid <x1> <y1> <x2> <y2> [bridge]")
print("Usage: build_pipe_line gas|liquid <x1> <y1> <x2> <y2> [mode]")
print(" mode: 'line'(默认,与已有管线合并) | 'cross'(跨接器跳过) | 'single'(单段)")
return
ptype = args[0]
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
bridge = len(args) > 5 and args[5] == 'bridge'
mode = args[5] if len(args) > 5 else 'line'
pause_before()
@ -157,11 +158,15 @@ def cmd_build_pipe_line(args):
result = api_post('/api/action/build_pipe', {
"type": ptype, "x1": x1, "y1": y1,
"x2": x2, "y2": y2, "bridge": bridge
"x2": x2, "y2": y2, "mode": mode
})
if result.get('success'):
segs = result.get('data', {}).get('segmentCount', 0)
d = result.get('data', {})
segs = d.get('segmentCount', 0)
bridges = d.get('bridgesPlaced', 0)
print(f"[OK] {ptype} pipe: {segs} segments from ({x1},{y1}) to ({x2},{y2})")
if bridges > 0:
print(f" {bridges} bridges placed at crossings (mode={mode})")
else:
print(f"[!] Failed: {result.get('errorMessage', 'unknown')}")
@ -295,8 +300,20 @@ COMMANDS = {
'emergency_o2': cmd_emergency_o2,
'build_pipe_line': cmd_build_pipe_line,
'build_wire_line': cmd_build_wire_line,
'snapshot': cmd_snapshot,
'camera': cmd_camera,
}
def cmd_snapshot(args):
"""Take screenshot + view. Usage: snapshot [name]"""
from oni_api import cmd_snapshot as api_snapshot
api_snapshot(args)
def cmd_camera(args):
"""Move camera. Usage: camera <x> <y> [zoom]"""
from oni_api import cmd_camera as api_camera
api_camera(args)
if __name__ == '__main__':
cmd = sys.argv[1] if len(sys.argv) > 1 else 'help'
@ -316,11 +333,15 @@ if __name__ == '__main__':
print(" expand_base <x> <y> <w> <h> 挖掘+建造墙壁(一键拓展房间)")
print()
print("=== Pipe / Wire Lines ===")
print(" build_pipe_line <t> <x1> <y1> <x2> <y2> [bridge]")
print(" t: gas | liquid")
print(" build_wire_line <t> <x1> <y1> <x2> <y2> [bridge]")
print(" build_pipe_line <t> <x1> <y1> <x2> <y2> [mode]")
print(" t: gas | liquid | mode: line(merge) | cross(bridge)")
print(" build_wire_line <t> <x1> <y1> <x2> <y2> [mode]")
print(" t: regular | heavy | conductive | heavy_conductive")
print()
print("=== Screenshot / Camera ===")
print(" snapshot [file.png] Take screenshot")
print(" camera <x> <y> [zoom] Move camera view")
print()
print("All high-level commands auto-pause/resume the game.")
else:
COMMANDS[cmd](sys.argv[2:])