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:
@ -63,6 +63,13 @@ namespace ONIAgentBridge
|
|||||||
var method = ctx.Request.HttpMethod;
|
var method = ctx.Request.HttpMethod;
|
||||||
var query = ctx.Request.QueryString;
|
var query = ctx.Request.QueryString;
|
||||||
|
|
||||||
|
// Special case: serve screenshot PNG binary
|
||||||
|
if (path == "/api/screenshot/latest" && method == "GET")
|
||||||
|
{
|
||||||
|
ServeLatestScreenshot(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string responseJson;
|
string responseJson;
|
||||||
|
|
||||||
switch (path, method)
|
switch (path, method)
|
||||||
@ -139,6 +146,9 @@ namespace ONIAgentBridge
|
|||||||
case ("/api/state/saves", "GET"):
|
case ("/api/state/saves", "GET"):
|
||||||
responseJson = GetSaves();
|
responseJson = GetSaves();
|
||||||
break;
|
break;
|
||||||
|
case ("/api/state/camera", "GET"):
|
||||||
|
responseJson = GetCamera();
|
||||||
|
break;
|
||||||
|
|
||||||
// --- Cell-level map data ---
|
// --- Cell-level map data ---
|
||||||
case ("/api/state/cell", "GET"):
|
case ("/api/state/cell", "GET"):
|
||||||
@ -235,6 +245,12 @@ namespace ONIAgentBridge
|
|||||||
case ("/api/action/build_wire", "POST"):
|
case ("/api/action/build_wire", "POST"):
|
||||||
responseJson = ExecuteBuildWire(ctx);
|
responseJson = ExecuteBuildWire(ctx);
|
||||||
break;
|
break;
|
||||||
|
case ("/api/action/screenshot", "POST"):
|
||||||
|
responseJson = ExecuteScreenshot(ctx);
|
||||||
|
break;
|
||||||
|
case ("/api/action/camera", "POST"):
|
||||||
|
responseJson = ExecuteCamera(ctx);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
ctx.Response.StatusCode = 404;
|
ctx.Response.StatusCode = 404;
|
||||||
@ -2024,7 +2040,153 @@ namespace ONIAgentBridge
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// Build Pipe Path
|
// Camera
|
||||||
|
// ===================================================================
|
||||||
|
private string GetCamera()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cam = CameraController.Instance;
|
||||||
|
if (cam == null) return JsonSerializer.Serialize(new { error = "camera_unavailable" });
|
||||||
|
|
||||||
|
var pos = cam.transform.position;
|
||||||
|
float zoom = 1f;
|
||||||
|
try { zoom = Camera.main?.orthographicSize ?? 30f; } catch { }
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
x = pos.x,
|
||||||
|
y = pos.y,
|
||||||
|
z = pos.z,
|
||||||
|
zoom
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception e) { return JsonSerializer.Serialize(new { error = e.Message }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExecuteCamera(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = ReadBody<CameraRequest>(ctx);
|
||||||
|
if (data == null)
|
||||||
|
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
|
||||||
|
|
||||||
|
var cam = CameraController.Instance;
|
||||||
|
if (cam == null)
|
||||||
|
return JsonSerializer.Serialize(FailWithReason("camera_unavailable", "Camera not available"));
|
||||||
|
|
||||||
|
if (data.x.HasValue && data.y.HasValue)
|
||||||
|
{
|
||||||
|
float targetX = data.x.Value;
|
||||||
|
float targetY = data.y.Value;
|
||||||
|
cam.transform.position = new UnityEngine.Vector3(targetX, targetY, cam.transform.position.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.zoom.HasValue)
|
||||||
|
{
|
||||||
|
float z = Mathf.Clamp(data.zoom.Value, 5f, 80f);
|
||||||
|
try { Camera.main.orthographicSize = z; } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
PushEvent("camera", "info", "Camera moved", $"Camera to ({data.x},{data.y}) zoom={data.zoom}", "system");
|
||||||
|
|
||||||
|
var pos = cam.transform.position;
|
||||||
|
float currentZoom = 1f;
|
||||||
|
try { currentZoom = Camera.main?.orthographicSize ?? 30f; } catch { }
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(ActionOk("camera_moved", new
|
||||||
|
{
|
||||||
|
x = pos.x,
|
||||||
|
y = pos.y,
|
||||||
|
zoom = currentZoom
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Screenshot
|
||||||
|
// ===================================================================
|
||||||
|
private static string _lastScreenshotPath = null;
|
||||||
|
|
||||||
|
private string ExecuteScreenshot(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string dir = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetDirectoryName(SaveLoader.GetActiveSaveFilePath()),
|
||||||
|
"oni_agent_screenshots");
|
||||||
|
if (!System.IO.Directory.Exists(dir))
|
||||||
|
System.IO.Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
string filename = $"screenshot_{GameClock.Instance?.GetCycle() ?? 0}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}.png";
|
||||||
|
string path = System.IO.Path.Combine(dir, filename);
|
||||||
|
string relPath = path;
|
||||||
|
|
||||||
|
// Use Unity's ScreenCapture
|
||||||
|
ScreenCapture.CaptureScreenshot(path);
|
||||||
|
_lastScreenshotPath = path;
|
||||||
|
|
||||||
|
PushEvent("screenshot", "info", "Screenshot taken", filename, "system");
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(ActionOk("screenshot_taken", new
|
||||||
|
{
|
||||||
|
filename,
|
||||||
|
path,
|
||||||
|
url = $"/api/screenshot/latest"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ServeLatestScreenshot(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string path = _lastScreenshotPath;
|
||||||
|
if (path == null || !System.IO.File.Exists(path))
|
||||||
|
{
|
||||||
|
// Fallback: look for most recent screenshot
|
||||||
|
string dir = System.IO.Path.Combine(
|
||||||
|
System.IO.Path.GetDirectoryName(SaveLoader.GetActiveSaveFilePath()),
|
||||||
|
"oni_agent_screenshots");
|
||||||
|
if (System.IO.Directory.Exists(dir))
|
||||||
|
{
|
||||||
|
var files = System.IO.Directory.GetFiles(dir, "*.png")
|
||||||
|
.OrderByDescending(f => new System.IO.FileInfo(f).LastWriteTime)
|
||||||
|
.ToArray();
|
||||||
|
if (files.Length > 0) path = files[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path != null && System.IO.File.Exists(path))
|
||||||
|
{
|
||||||
|
var bytes = System.IO.File.ReadAllBytes(path);
|
||||||
|
ctx.Response.ContentType = "image/png";
|
||||||
|
ctx.Response.ContentLength64 = bytes.Length;
|
||||||
|
ctx.Response.OutputStream.Write(bytes, 0, bytes.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 404;
|
||||||
|
ctx.Response.ContentType = "text/plain";
|
||||||
|
var buf = Encoding.UTF8.GetBytes("no_screenshot_available");
|
||||||
|
ctx.Response.OutputStream.Write(buf, 0, buf.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 500;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ctx.Response.OutputStream.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Build Pipe Path — with crossing/bridge detection
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
private string ExecuteBuildPipe(HttpListenerContext ctx)
|
private string ExecuteBuildPipe(HttpListenerContext ctx)
|
||||||
{
|
{
|
||||||
@ -2035,41 +2197,82 @@ namespace ONIAgentBridge
|
|||||||
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
|
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
|
||||||
|
|
||||||
string pipeType = data.type ?? "liquid";
|
string pipeType = data.type ?? "liquid";
|
||||||
string material = data.material ?? "Irons";
|
string mode = data.mode ?? "line";
|
||||||
bool isBridge = data.bridge ?? false;
|
// mode: "line" = straight path, "connect" = connect building to network,
|
||||||
|
// "cross" = explicitly cross existing pipes with bridges, "single" = one segment
|
||||||
|
|
||||||
string buildingId = pipeType == "gas"
|
string conduitId = pipeType == "gas" ? "GasConduit" : "LiquidConduit";
|
||||||
? (isBridge ? "GasConduitBridge" : "GasConduit")
|
string bridgeId = pipeType == "gas" ? "GasConduitBridge" : "LiquidConduitBridge";
|
||||||
: (isBridge ? "LiquidConduitBridge" : "LiquidConduit");
|
|
||||||
|
|
||||||
int cellsPlaced = 0;
|
int cellsPlaced = 0;
|
||||||
|
int bridgesPlaced = 0;
|
||||||
var placements = new List<object>();
|
var placements = new List<object>();
|
||||||
|
|
||||||
if (data.x2.HasValue && data.y2.HasValue)
|
if (mode == "single" || mode == "segment")
|
||||||
|
{
|
||||||
|
// Single segment at (x1,y1), optional direction and bridge
|
||||||
|
string bid = (data.bridge ?? false) ? bridgeId : conduitId;
|
||||||
|
placements.Add(new { x = data.x1, y = data.y1, buildingId = bid });
|
||||||
|
cellsPlaced = 1;
|
||||||
|
if (data.bridge ?? false) bridgesPlaced = 1;
|
||||||
|
}
|
||||||
|
else if (data.x2.HasValue && data.y2.HasValue)
|
||||||
{
|
{
|
||||||
int dx = Math.Sign(data.x2.Value - data.x1);
|
int dx = Math.Sign(data.x2.Value - data.x1);
|
||||||
int dy = Math.Sign(data.y2.Value - data.y1);
|
int dy = Math.Sign(data.y2.Value - data.y1);
|
||||||
|
bool horizontal = dy == 0;
|
||||||
int cx = data.x1, cy = data.y1;
|
int cx = data.x1, cy = data.y1;
|
||||||
|
string prevBid = conduitId;
|
||||||
|
|
||||||
while (cx != data.x2.Value + dx || cy != data.y2.Value + dy)
|
while (cx != data.x2.Value + dx || cy != data.y2.Value + dy)
|
||||||
{
|
{
|
||||||
placements.Add(new { x = cx, y = cy, buildingId });
|
int cell = Grid.XYToCell(cx, cy);
|
||||||
cx += dx; cy += dy;
|
bool hasCrossing = false;
|
||||||
|
|
||||||
|
// Detect existing pipe of same type
|
||||||
|
if (cell >= 0 && cell < Grid.CellCount)
|
||||||
|
{
|
||||||
|
var building = Grid.Objects[cell, (int)ObjectLayer.Building];
|
||||||
|
if (building != null)
|
||||||
|
{
|
||||||
|
string bName = building.name;
|
||||||
|
if (pipeType == "gas" && (bName == "GasConduit" || bName == "GasConduitBridge"))
|
||||||
|
hasCrossing = true;
|
||||||
|
else if (pipeType == "liquid" && (bName == "LiquidConduit" || bName == "LiquidConduitBridge"))
|
||||||
|
hasCrossing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string bid;
|
||||||
|
if (hasCrossing && mode == "cross")
|
||||||
|
{
|
||||||
|
// Place bridge to cross without connecting
|
||||||
|
bid = bridgeId;
|
||||||
|
bridgesPlaced++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bid = conduitId;
|
||||||
|
}
|
||||||
|
|
||||||
|
placements.Add(new { x = cx, y = cy, buildingId = bid, crossing = hasCrossing });
|
||||||
cellsPlaced++;
|
cellsPlaced++;
|
||||||
|
cx += dx; cy += dy;
|
||||||
if (cellsPlaced > 100) break;
|
if (cellsPlaced > 100) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
placements.Add(new { x = data.x1, y = data.y1, buildingId });
|
placements.Add(new { x = data.x1, y = data.y1, buildingId = conduitId });
|
||||||
cellsPlaced = 1;
|
cellsPlaced = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonSerializer.Serialize(ActionOk("pipe_build_queued", new
|
return JsonSerializer.Serialize(ActionOk("pipe_build_queued", new
|
||||||
{
|
{
|
||||||
type = pipeType,
|
type = pipeType,
|
||||||
bridge = isBridge,
|
mode,
|
||||||
segmentCount = cellsPlaced,
|
segmentCount = cellsPlaced,
|
||||||
|
bridgesPlaced,
|
||||||
segments = placements
|
segments = placements
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -2077,7 +2280,7 @@ namespace ONIAgentBridge
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// Build Wire Path
|
// Build Wire Path — with crossing/bridge detection
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
private string ExecuteBuildWire(HttpListenerContext ctx)
|
private string ExecuteBuildWire(HttpListenerContext ctx)
|
||||||
{
|
{
|
||||||
@ -2088,20 +2291,35 @@ namespace ONIAgentBridge
|
|||||||
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
|
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
|
||||||
|
|
||||||
string wireType = data.type ?? "regular";
|
string wireType = data.type ?? "regular";
|
||||||
bool isBridge = data.bridge ?? false;
|
string mode = data.mode ?? "line";
|
||||||
|
|
||||||
string buildingId = wireType switch
|
string conduitId = wireType switch
|
||||||
{
|
{
|
||||||
"heavy" => isBridge ? "HeaviWatBridge" : "HeaviWatWire",
|
"heavy" => "HeaviWatWire",
|
||||||
"conductive" => isBridge ? "ConductiveWireBridge" : "ConductiveWire",
|
"conductive" => "ConductiveWire",
|
||||||
"heavy_conductive" => isBridge ? "HeaviWatConductiveBridge" : "HeaviWatConductiveWire",
|
"heavy_conductive" => "HeaviWatConductiveWire",
|
||||||
_ => isBridge ? "WireBridge" : "Wire"
|
_ => "Wire"
|
||||||
|
};
|
||||||
|
string bridgeId = wireType switch
|
||||||
|
{
|
||||||
|
"heavy" => "HeaviWatBridge",
|
||||||
|
"conductive" => "ConductiveWireBridge",
|
||||||
|
"heavy_conductive" => "HeaviWatConductiveBridge",
|
||||||
|
_ => "WireBridge"
|
||||||
};
|
};
|
||||||
|
|
||||||
int cellsPlaced = 0;
|
int cellsPlaced = 0;
|
||||||
|
int bridgesPlaced = 0;
|
||||||
var placements = new List<object>();
|
var placements = new List<object>();
|
||||||
|
|
||||||
if (data.x2.HasValue && data.y2.HasValue)
|
if (mode == "single" || mode == "segment")
|
||||||
|
{
|
||||||
|
string bid = (data.bridge ?? false) ? bridgeId : conduitId;
|
||||||
|
placements.Add(new { x = data.x1, y = data.y1, buildingId = bid });
|
||||||
|
cellsPlaced = 1;
|
||||||
|
if (data.bridge ?? false) bridgesPlaced = 1;
|
||||||
|
}
|
||||||
|
else if (data.x2.HasValue && data.y2.HasValue)
|
||||||
{
|
{
|
||||||
int dx = Math.Sign(data.x2.Value - data.x1);
|
int dx = Math.Sign(data.x2.Value - data.x1);
|
||||||
int dy = Math.Sign(data.y2.Value - data.y1);
|
int dy = Math.Sign(data.y2.Value - data.y1);
|
||||||
@ -2109,23 +2327,51 @@ namespace ONIAgentBridge
|
|||||||
|
|
||||||
while (cx != data.x2.Value + dx || cy != data.y2.Value + dy)
|
while (cx != data.x2.Value + dx || cy != data.y2.Value + dy)
|
||||||
{
|
{
|
||||||
placements.Add(new { x = cx, y = cy, buildingId });
|
int cell = Grid.XYToCell(cx, cy);
|
||||||
cx += dx; cy += dy;
|
bool hasCrossing = false;
|
||||||
|
|
||||||
|
if (cell >= 0 && cell < Grid.CellCount)
|
||||||
|
{
|
||||||
|
var building = Grid.Objects[cell, (int)ObjectLayer.Building];
|
||||||
|
if (building != null)
|
||||||
|
{
|
||||||
|
string bName = building.name;
|
||||||
|
if (bName == "Wire" || bName == "WireBridge" ||
|
||||||
|
bName == "HeaviWatWire" || bName == "HeaviWatBridge" ||
|
||||||
|
bName == "ConductiveWire" || bName == "ConductiveWireBridge")
|
||||||
|
hasCrossing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string bid;
|
||||||
|
if (hasCrossing && mode == "cross")
|
||||||
|
{
|
||||||
|
bid = bridgeId;
|
||||||
|
bridgesPlaced++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bid = conduitId;
|
||||||
|
}
|
||||||
|
|
||||||
|
placements.Add(new { x = cx, y = cy, buildingId = bid, crossing = hasCrossing });
|
||||||
cellsPlaced++;
|
cellsPlaced++;
|
||||||
|
cx += dx; cy += dy;
|
||||||
if (cellsPlaced > 100) break;
|
if (cellsPlaced > 100) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
placements.Add(new { x = data.x1, y = data.y1, buildingId });
|
placements.Add(new { x = data.x1, y = data.y1, buildingId = conduitId });
|
||||||
cellsPlaced = 1;
|
cellsPlaced = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonSerializer.Serialize(ActionOk("wire_build_queued", new
|
return JsonSerializer.Serialize(ActionOk("wire_build_queued", new
|
||||||
{
|
{
|
||||||
type = wireType,
|
type = wireType,
|
||||||
bridge = isBridge,
|
mode,
|
||||||
segmentCount = cellsPlaced,
|
segmentCount = cellsPlaced,
|
||||||
|
bridgesPlaced,
|
||||||
segments = placements
|
segments = placements
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -2247,7 +2493,8 @@ namespace ONIAgentBridge
|
|||||||
internal class SaveRequest { public string name { get; set; } }
|
internal class SaveRequest { public string name { get; set; } }
|
||||||
internal class LoadRequest { public string name { get; set; } }
|
internal class LoadRequest { public string name { get; set; } }
|
||||||
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; } }
|
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 BatchRequest
|
internal class BatchRequest
|
||||||
{
|
{
|
||||||
|
|||||||
114
tools/oni_api.py
114
tools/oni_api.py
@ -702,24 +702,60 @@ def cmd_assign_job(args):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def cmd_build_pipe_line(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:
|
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
|
return
|
||||||
payload = {"type": args[0], "x1": int(args[1]), "y1": int(args[2]),
|
ptype = args[0]
|
||||||
"x2": int(args[3]), "y2": int(args[4]), "bridge": len(args) > 5 and args[5] == 'bridge'}
|
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
|
||||||
result = api_post('/api/action/build_pipe', payload)
|
mode = args[5] if len(args) > 5 else 'line'
|
||||||
_print_feedback(result)
|
|
||||||
|
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):
|
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:
|
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
|
return
|
||||||
payload = {"type": args[0], "x1": int(args[1]), "y1": int(args[2]),
|
wtype = args[0]
|
||||||
"x2": int(args[3]), "y2": int(args[4]), "bridge": len(args) > 5 and args[5] == 'bridge'}
|
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
|
||||||
result = api_post('/api/action/build_wire', payload)
|
mode = args[5] if len(args) > 5 else 'line'
|
||||||
_print_feedback(result)
|
|
||||||
|
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):
|
def _print_feedback(result):
|
||||||
"""Pretty-print action feedback."""
|
"""Pretty-print action feedback."""
|
||||||
@ -734,6 +770,52 @@ def _print_feedback(result):
|
|||||||
if sug:
|
if sug:
|
||||||
print(f" -> {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):
|
def cmd_explore(args):
|
||||||
"""AI-friendly exploration: reads a region and returns structured text summary."""
|
"""AI-friendly exploration: reads a region and returns structured text summary."""
|
||||||
x = int(args[0]) if len(args) > 0 else 0
|
x = int(args[0]) if len(args) > 0 else 0
|
||||||
@ -849,6 +931,9 @@ COMMANDS = {
|
|||||||
'assign_job': cmd_assign_job,
|
'assign_job': cmd_assign_job,
|
||||||
'build_pipe_line': cmd_build_pipe_line,
|
'build_pipe_line': cmd_build_pipe_line,
|
||||||
'build_wire_line': cmd_build_wire_line,
|
'build_wire_line': cmd_build_wire_line,
|
||||||
|
'snapshot': cmd_snapshot,
|
||||||
|
'camera': cmd_camera,
|
||||||
|
'camera_status': cmd_camera_status,
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@ -920,6 +1005,11 @@ if __name__ == '__main__':
|
|||||||
print(" assign_job <name> <job> Assign dupe to a chore group")
|
print(" assign_job <name> <job> Assign dupe to a chore group")
|
||||||
print(" morale Duplicant morale levels")
|
print(" morale Duplicant morale levels")
|
||||||
print("")
|
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 / Priority (Advanced) ===")
|
||||||
print(" batch <json_file> Execute batch plan")
|
print(" batch <json_file> Execute batch plan")
|
||||||
print(" priority_global <t> <p> Set global default priority")
|
print(" priority_global <t> <p> Set global default priority")
|
||||||
|
|||||||
@ -141,13 +141,14 @@ def cmd_fix_overload(args):
|
|||||||
|
|
||||||
|
|
||||||
def cmd_build_pipe_line(args):
|
def cmd_build_pipe_line(args):
|
||||||
"""铺设管道路径。"""
|
"""铺设管道路径(带交叉模式)。"""
|
||||||
if len(args) < 5:
|
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
|
return
|
||||||
ptype = args[0]
|
ptype = args[0]
|
||||||
x1, y1, x2, y2 = int(args[1]), int(args[2]), int(args[3]), int(args[4])
|
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()
|
pause_before()
|
||||||
|
|
||||||
@ -157,11 +158,15 @@ def cmd_build_pipe_line(args):
|
|||||||
|
|
||||||
result = api_post('/api/action/build_pipe', {
|
result = api_post('/api/action/build_pipe', {
|
||||||
"type": ptype, "x1": x1, "y1": y1,
|
"type": ptype, "x1": x1, "y1": y1,
|
||||||
"x2": x2, "y2": y2, "bridge": bridge
|
"x2": x2, "y2": y2, "mode": mode
|
||||||
})
|
})
|
||||||
if result.get('success'):
|
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})")
|
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:
|
else:
|
||||||
print(f"[!] Failed: {result.get('errorMessage', 'unknown')}")
|
print(f"[!] Failed: {result.get('errorMessage', 'unknown')}")
|
||||||
|
|
||||||
@ -295,8 +300,20 @@ COMMANDS = {
|
|||||||
'emergency_o2': cmd_emergency_o2,
|
'emergency_o2': cmd_emergency_o2,
|
||||||
'build_pipe_line': cmd_build_pipe_line,
|
'build_pipe_line': cmd_build_pipe_line,
|
||||||
'build_wire_line': cmd_build_wire_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__':
|
if __name__ == '__main__':
|
||||||
cmd = sys.argv[1] if len(sys.argv) > 1 else 'help'
|
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(" expand_base <x> <y> <w> <h> 挖掘+建造墙壁(一键拓展房间)")
|
||||||
print()
|
print()
|
||||||
print("=== Pipe / Wire Lines ===")
|
print("=== Pipe / Wire Lines ===")
|
||||||
print(" build_pipe_line <t> <x1> <y1> <x2> <y2> [bridge]")
|
print(" build_pipe_line <t> <x1> <y1> <x2> <y2> [mode]")
|
||||||
print(" t: gas | liquid")
|
print(" t: gas | liquid | mode: line(merge) | cross(bridge)")
|
||||||
print(" build_wire_line <t> <x1> <y1> <x2> <y2> [bridge]")
|
print(" build_wire_line <t> <x1> <y1> <x2> <y2> [mode]")
|
||||||
print(" t: regular | heavy | conductive | heavy_conductive")
|
print(" t: regular | heavy | conductive | heavy_conductive")
|
||||||
print()
|
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.")
|
print("All high-level commands auto-pause/resume the game.")
|
||||||
else:
|
else:
|
||||||
COMMANDS[cmd](sys.argv[2:])
|
COMMANDS[cmd](sys.argv[2:])
|
||||||
|
|||||||
Reference in New Issue
Block a user