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 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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user