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

@ -63,6 +63,13 @@ namespace ONIAgentBridge
var method = ctx.Request.HttpMethod;
var query = ctx.Request.QueryString;
// Special case: serve screenshot PNG binary
if (path == "/api/screenshot/latest" && method == "GET")
{
ServeLatestScreenshot(ctx);
return;
}
string responseJson;
switch (path, method)
@ -139,6 +146,9 @@ namespace ONIAgentBridge
case ("/api/state/saves", "GET"):
responseJson = GetSaves();
break;
case ("/api/state/camera", "GET"):
responseJson = GetCamera();
break;
// --- Cell-level map data ---
case ("/api/state/cell", "GET"):
@ -235,6 +245,12 @@ namespace ONIAgentBridge
case ("/api/action/build_wire", "POST"):
responseJson = ExecuteBuildWire(ctx);
break;
case ("/api/action/screenshot", "POST"):
responseJson = ExecuteScreenshot(ctx);
break;
case ("/api/action/camera", "POST"):
responseJson = ExecuteCamera(ctx);
break;
default:
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)
{
@ -2035,41 +2197,82 @@ namespace ONIAgentBridge
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
string pipeType = data.type ?? "liquid";
string material = data.material ?? "Irons";
bool isBridge = data.bridge ?? false;
string mode = data.mode ?? "line";
// mode: "line" = straight path, "connect" = connect building to network,
// "cross" = explicitly cross existing pipes with bridges, "single" = one segment
string buildingId = pipeType == "gas"
? (isBridge ? "GasConduitBridge" : "GasConduit")
: (isBridge ? "LiquidConduitBridge" : "LiquidConduit");
string conduitId = pipeType == "gas" ? "GasConduit" : "LiquidConduit";
string bridgeId = pipeType == "gas" ? "GasConduitBridge" : "LiquidConduitBridge";
int cellsPlaced = 0;
int bridgesPlaced = 0;
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 dy = Math.Sign(data.y2.Value - data.y1);
bool horizontal = dy == 0;
int cx = data.x1, cy = data.y1;
string prevBid = conduitId;
while (cx != data.x2.Value + dx || cy != data.y2.Value + dy)
{
placements.Add(new { x = cx, y = cy, buildingId });
cx += dx; cy += dy;
int cell = Grid.XYToCell(cx, cy);
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++;
cx += dx; cy += dy;
if (cellsPlaced > 100) break;
}
}
else
{
placements.Add(new { x = data.x1, y = data.y1, buildingId });
placements.Add(new { x = data.x1, y = data.y1, buildingId = conduitId });
cellsPlaced = 1;
}
return JsonSerializer.Serialize(ActionOk("pipe_build_queued", new
{
type = pipeType,
bridge = isBridge,
mode,
segmentCount = cellsPlaced,
bridgesPlaced,
segments = placements
}));
}
@ -2077,7 +2280,7 @@ namespace ONIAgentBridge
}
// ===================================================================
// Build Wire Path
// Build Wire Path — with crossing/bridge detection
// ===================================================================
private string ExecuteBuildWire(HttpListenerContext ctx)
{
@ -2088,20 +2291,35 @@ namespace ONIAgentBridge
return JsonSerializer.Serialize(FailInvalid("invalid_request"));
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",
"conductive" => isBridge ? "ConductiveWireBridge" : "ConductiveWire",
"heavy_conductive" => isBridge ? "HeaviWatConductiveBridge" : "HeaviWatConductiveWire",
_ => isBridge ? "WireBridge" : "Wire"
"heavy" => "HeaviWatWire",
"conductive" => "ConductiveWire",
"heavy_conductive" => "HeaviWatConductiveWire",
_ => "Wire"
};
string bridgeId = wireType switch
{
"heavy" => "HeaviWatBridge",
"conductive" => "ConductiveWireBridge",
"heavy_conductive" => "HeaviWatConductiveBridge",
_ => "WireBridge"
};
int cellsPlaced = 0;
int bridgesPlaced = 0;
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 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)
{
placements.Add(new { x = cx, y = cy, buildingId });
cx += dx; cy += dy;
int cell = Grid.XYToCell(cx, cy);
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++;
cx += dx; cy += dy;
if (cellsPlaced > 100) break;
}
}
else
{
placements.Add(new { x = data.x1, y = data.y1, buildingId });
placements.Add(new { x = data.x1, y = data.y1, buildingId = conduitId });
cellsPlaced = 1;
}
return JsonSerializer.Serialize(ActionOk("wire_build_queued", new
{
type = wireType,
bridge = isBridge,
mode,
segmentCount = cellsPlaced,
bridgesPlaced,
segments = placements
}));
}
@ -2247,7 +2493,8 @@ namespace ONIAgentBridge
internal class SaveRequest { 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 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
{