Files
oniagent/mod/ONIAgentBridge.cs

1236 lines
59 KiB
C#

using HarmonyLib;
using KMod;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using UnityEngine;
namespace ONIAgentBridge
{
public class AgentEvent
{
public int id;
public string type, severity, title, message, category;
public long timestamp;
public int cycle;
}
public class DigRequest { public int x; public int y; public int width; public int height; }
public class BuildRequest { public string buildingId; public int x; public int y; }
public class CoordRequest { public int x; public int y; }
public class CoordWidthRequest { public int x; public int y; public int width; public int height; }
public class ResearchRequest { public string techId; }
public class PriorReq { public int x; public int y; public int priority; }
public class SpeedReq { public int speed; }
public class CameraReq { public int x; public int y; public float zoom; }
public class PriorityGlobalReq { public string target; public int priority; }
public class PriorityTypeReq { public string buildingType; public int priority; }
public class SaveReq { public string name; }
public class BatchActionData { public string type; public int? x, y, width, height, priority; public string buildingId, techId; }
public class BatchReq { public List<BatchActionData> actions; }
public class Mod : UserMod2
{
private HttpListener _listener;
internal static ConcurrentQueue<System.Action> cmdQueue = new ConcurrentQueue<System.Action>();
internal static List<AgentEvent> eventLog = new List<AgentEvent>();
internal static int eventSeq = 0;
internal static object eventLock = new object();
internal static string lastScreenshotPath;
// Helper: enqueue action with optional auto-camera to position
static void Q(System.Action a) { cmdQueue.Enqueue(a); }
static void QWithCamera(int x, int y, System.Action a)
{
cmdQueue.Enqueue(() =>
{
try { CameraController.Instance?.SetPosition(Grid.CellToPos(Grid.XYToCell(x, y))); } catch { }
try { a(); } catch (Exception ex) { LogEventStatic("error","critical","[Action]",ex.Message); }
});
}
public class QueueProcessor : UnityEngine.MonoBehaviour
{
public void Update()
{
System.Action a;
while (cmdQueue.TryDequeue(out a))
{
try { a(); }
catch (Exception ex) { LogEventStatic("error", "critical", "[Queue]", ex.Message); }
}
}
}
static void LogEventStatic(string type, string severity, string title, string message, string category = "general")
{
lock (eventLock)
{
eventLog.Add(new AgentEvent
{
id = eventSeq++,
type = type,
severity = severity,
timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
cycle = GameClock.Instance?.GetCycle() ?? 0,
category = category,
title = title ?? "",
message = message ?? ""
});
if (eventLog.Count > 500)
eventLog.RemoveRange(0, eventLog.Count - 500);
}
}
void LogEvent(string type, string severity, string title, string message, string category = "general")
{
LogEventStatic(type, severity, title, message, category);
}
public override void OnLoad(Harmony harmony)
{
try
{
base.OnLoad(harmony);
var go = new GameObject("ONI_Agent_Processor");
go.AddComponent<QueueProcessor>();
UnityEngine.Object.DontDestroyOnLoad(go);
int port = 23876;
_listener = new HttpListener();
_listener.Prefixes.Add($"http://127.0.0.1:{port}/");
_listener.Start();
_listener.BeginGetContext(OnRequest, null);
LogEvent("info", "info", "[System]", $"ONI Agent Bridge started on port {port}");
}
catch { }
}
void OnRequest(IAsyncResult ar)
{
try
{
var ctx = _listener.EndGetContext(ar);
_listener.BeginGetContext(OnRequest, null);
Process(ctx);
}
catch (HttpListenerException) { }
catch { }
}
void Process(HttpListenerContext ctx)
{
try
{
var path = ctx.Request.Url.AbsolutePath.TrimEnd('/');
var method = ctx.Request.HttpMethod;
var q = ctx.Request.QueryString;
var body = new StreamReader(ctx.Request.InputStream).ReadToEnd();
string json = null;
// Health
if (path == "/health" && method == "GET")
json = J(new { success = true, data = new { status = "ok", service = "oni-agent-bridge", version = "2.0.0", marker = "ONI_AGENT_V2_LOADED" } });
// State queries (GET) - queued to main thread for safety
else if (path == "/api/state/game" && method == "GET")
json = EnqueueRead(ReadGameState);
else if (path == "/api/state/resources" && method == "GET")
json = EnqueueRead(ReadResources);
else if (path == "/api/state/buildings" && method == "GET")
json = EnqueueRead(ReadBuildings);
else if (path == "/api/state/duplicants" && method == "GET")
json = EnqueueRead(ReadDuplicants);
else if (path == "/api/state/research" && method == "GET")
json = EnqueueRead(ReadResearch);
else if (path == "/api/state/rooms" && method == "GET")
json = EnqueueRead(ReadRooms);
else if (path == "/api/state/events" && method == "GET")
json = ReadEvents(q);
else if (path == "/api/state/storage" && method == "GET")
json = EnqueueRead(ReadStorage);
else if (path == "/api/state/saves" && method == "GET")
json = EnqueueRead(ReadSaves);
else if (path == "/api/state/alert" && method == "GET")
json = EnqueueRead(ReadAlerts);
else if (path == "/api/state/camera" && method == "GET")
json = EnqueueRead(ReadCamera);
// Cell/map data (GET) - safe on background thread via raw Grid arrays
else if (path == "/api/state/cell" && method == "GET")
json = ReadCell(q);
else if (path == "/api/state/cells" && method == "GET")
json = ReadCells(q);
else if (path == "/api/state/cells/slice" && method == "GET")
json = ReadSlice(q);
else if (path == "/api/state/gas" && method == "GET")
json = ReadGas(q);
// Registry (GET)
else if (path == "/api/registry/buildings" && method == "GET")
json = EnqueueRead(ReadBuildingRegistry);
else if (path == "/api/registry/elements" && method == "GET")
json = EnqueueRead(ReadElementRegistry);
else if (path == "/api/registry/techs" && method == "GET")
json = EnqueueRead(ReadTechRegistry);
// Actions (POST)
else if (path == "/api/action/pause" && method == "POST")
{
string reason = null;
try { var d = JsonConvert.DeserializeAnonymousType(body, new { reason = "" }); reason = d?.reason; } catch { }
string r = reason;
Q(() => { var s = SpeedControlScreen.Instance; if (s != null && !s.IsPaused) { s.Pause(false, true); LogEvent("game_state","info","[Pause]",r??"AI paused"); } });
json = J(new { success = true, data = new { result = "paused" } });
}
else if (path == "/api/action/unpause" && method == "POST")
{
int sp = 1;
try { var d = JsonConvert.DeserializeAnonymousType(body, new { speed = 1 }); sp = d?.speed ?? 1; } catch { }
int sp2 = sp;
Q(() => { var s = SpeedControlScreen.Instance; if (s != null && s.IsPaused) { s.Unpause(true); s.SetSpeed(Mathf.Clamp(sp2,1,3)); LogEvent("game_state","info","[Unpause]",$"Speed: {sp2}x"); } });
json = J(new { success = true, data = new { result = "unpaused" } });
}
else if (path == "/api/action/speed" && method == "POST")
{
int sp = 1;
try { var d = JsonConvert.DeserializeAnonymousType(body, new { speed = 1 }); sp = d?.speed ?? 1; } catch { }
int sp2 = sp;
Q(() => { var s = SpeedControlScreen.Instance; if (s != null) s.SetSpeed(Mathf.Clamp(sp2,1,3)); });
json = J(new { success = true, data = new { result = "speed_set", speed = sp2 } });
}
else if (path == "/api/action/dig" && method == "POST")
json = QueueDig(body);
else if (path == "/api/action/build" && method == "POST")
json = QueueBuild(body);
else if (path == "/api/action/deconstruct" && method == "POST")
json = QueueDeconstruct(body);
else if (path == "/api/action/prioritize" && method == "POST")
json = QueuePrioritize(body);
else if (path == "/api/action/research" && method == "POST")
json = QueueResearch(body);
else if (path == "/api/action/mop" && method == "POST")
json = QueueMop(body);
else if (path == "/api/action/harvest" && method == "POST")
json = QueueHarvest(body);
else if (path == "/api/action/batch" && method == "POST")
json = QueueBatch(body);
else if (path == "/api/action/save" && method == "POST")
json = QueueSave(body);
else if (path == "/api/action/load" && method == "POST")
json = QueueLoad(body);
else if (path == "/api/action/priority_global" && method == "POST")
json = QueuePriorityGlobal(body);
else if (path == "/api/action/priority_type" && method == "POST")
json = QueuePriorityType(body);
else if (path == "/api/action/camera" && method == "POST")
json = QueueCamera(body);
// Screenshot
else if (path == "/api/screenshot/latest" && method == "GET")
{ ServeScreenshot(ctx); return; }
else
json = J(new { success = false, error = "not_found", errorMessage = $"Unknown: {method} {path}" });
if (json != null) Respond(ctx, json);
}
catch (Exception ex) { Respond(ctx, J(new { success = false, error = "internal_error", errorMessage = ex.Message })); }
}
void Respond(HttpListenerContext ctx, string json)
{
var b = Encoding.UTF8.GetBytes(json);
ctx.Response.ContentType = "application/json";
ctx.Response.OutputStream.Write(b, 0, b.Length);
ctx.Response.OutputStream.Close();
}
static string J(object o) => JsonConvert.SerializeObject(o);
string EnqueueRead(Func<object> reader)
{
object result = new { error = "timeout" };
var ev = new ManualResetEvent(false);
Q(() => {
try { result = reader(); }
catch (Exception ex) { result = new { error = "read_error", message = ex.Message }; }
ev.Set();
});
ev.WaitOne(10000);
return J(new { success = true, data = result });
}
// ── State Readers ─────────────────────────────────────
object ReadGameState()
{
var gc = GameClock.Instance;
var scs = SpeedControlScreen.Instance;
return new
{
cycle = gc?.GetCycle() ?? 0,
duplicantCount = Components.MinionIdentities.Items.Count,
worldSize = Grid.CellCount,
gridWidth = Grid.WidthInCells,
gridHeight = Grid.HeightInCells,
isPaused = scs?.IsPaused ?? true,
gameSpeed = scs?.GetSpeed() ?? 0,
elapsedTime = gc?.GetTime() ?? 0f
};
}
object ReadResources()
{
var world = ClusterManager.Instance?.activeWorld;
if (world == null) return new List<object>();
var inv = world.worldInventory;
if (inv == null) return new List<object>();
var list = new List<object>();
foreach (var e in ElementLoader.elements)
{
float a = inv.GetAmount(e.tag, false);
if (a > 0)
{
string state = "solid";
if (e.IsGas) state = "gas";
else if (e.IsLiquid) state = "liquid";
list.Add(new { id = e.id.ToString(), name = e.name, tag = e.tag.ToString(), amountKg = a, state });
}
}
return list;
}
object ReadBuildings()
{
var list = new List<object>();
foreach (var building in Components.BuildingCompletes.Items)
{
var def = building.Def;
var pos = building.transform.position;
list.Add(new
{
id = def.PrefabID,
name = def.Name,
x = (int)pos.x,
y = (int)pos.y,
width = def.WidthInCells,
height = def.HeightInCells
});
}
return list;
}
object ReadDuplicants()
{
var list = new List<object>();
foreach (var minion in Components.MinionIdentities.Items)
{
var pos = minion.transform.position;
var health = minion.gameObject.GetComponent<Health>();
list.Add(new
{
name = minion.GetProperName(),
x = (int)pos.x,
y = (int)pos.y,
health = health?.hitPoints ?? 100
});
}
return list;
}
object ReadResearch()
{
var research = Research.Instance;
if (research == null) return new { };
var completed = new List<string>();
try
{
var f = research.GetType().GetField("completedTechs", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
if (f != null && f.GetValue(research) is System.Collections.IList list)
{
foreach (var t in list)
{
var idProp = t.GetType().GetProperty("Id") ?? t.GetType().GetField("Id") as System.Reflection.MemberInfo;
if (idProp is System.Reflection.PropertyInfo pi) completed.Add(pi.GetValue(t)?.ToString() ?? "");
else if (idProp is System.Reflection.FieldInfo fi) completed.Add(fi.GetValue(t)?.ToString() ?? "");
}
}
}
catch { }
return new { completedTechs = completed };
}
object ReadRooms()
{
var rp = Game.Instance?.roomProber;
if (rp == null) return new List<object>();
var list = new List<object>();
foreach (var room in rp.rooms)
{
if (room?.roomType == null) continue;
list.Add(new { id = room.roomType.Id, name = room.roomType.Name });
}
return list;
}
object ReadStorage()
{
var list = new List<object>();
foreach (var building in Components.BuildingCompletes.Items)
{
var storage = building.gameObject.GetComponent<Storage>();
if (storage == null || storage.MassStored() <= 0) continue;
list.Add(new
{
building = building.Def?.Name ?? building.name,
x = (int)building.transform.position.x,
y = (int)building.transform.position.y,
massStored = storage.MassStored(),
capacity = storage.capacityKg
});
}
return list;
}
object ReadSaves()
{
try
{
var saves = new List<object>();
string sp = SaveLoader.GetActiveSaveFilePath();
var dir = Path.GetDirectoryName(sp);
if (dir != null && Directory.Exists(dir))
foreach (var f in Directory.GetFiles(dir, "*.sav"))
saves.Add(new { name = Path.GetFileNameWithoutExtension(f) });
return new { currentSave = sp, saves = saves };
}
catch { return new { error = "cannot_read_saves" }; }
}
object ReadAlerts()
{
var list = new List<object>();
try
{
var nm = NotificationManager.Instance;
if (nm != null)
{
var field = typeof(NotificationManager).GetField("notifications",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (field?.GetValue(nm) is System.Collections.IEnumerable notifications)
foreach (global::Notification n in notifications)
list.Add(new { title = n.titleText, severity = n.Type.ToString() });
}
}
catch { }
return list;
}
object ReadCamera()
{
var cc = CameraController.Instance;
if (cc == null) return new { };
var p = cc.transform.position;
return new { x = p.x, y = p.y };
}
// ── Cell/Map readers (background-thread safe) ────────
string ReadCell(System.Collections.Specialized.NameValueCollection q)
{
try
{
int x = int.Parse(q["x"] ?? "-1"), y = int.Parse(q["y"] ?? "-1");
int cell = Grid.XYToCell(x, y);
if (cell < 0 || cell >= Grid.CellCount) return J(new { success = false, error = "bounds" });
var el = Grid.Element[cell];
return J(new
{
success = true, data = new
{
x, y, cell,
element = el?.name ?? "Vacuum",
elementId = el?.id.ToString() ?? "Vacuum",
massKg = Grid.Mass[cell],
temperatureC = Grid.Temperature[cell] > 0 ? Grid.Temperature[cell] - 273.15f : -273.15f,
isSolid = Grid.Solid[cell],
isLiquid = el != null && el.IsLiquid,
isGas = el != null && el.IsGas,
isVacuum = el == null,
hasBuilding = Grid.Objects[cell, (int)ObjectLayer.Building] != null,
hasDuplicant = Grid.Objects[cell, (int)ObjectLayer.Minion] != null,
isDiggable = Grid.Solid[cell] && el != null && el.id != SimHashes.Unobtanium
}
});
}
catch (Exception ex) { return J(new { success = false, error = "read_error", message = ex.Message }); }
}
string ReadCells(System.Collections.Specialized.NameValueCollection q)
{
try
{
int x = int.Parse(q["x"] ?? "0"), y = int.Parse(q["y"] ?? "0");
int w = int.Parse(q["width"] ?? "10"), h = int.Parse(q["height"] ?? "10");
var cells = new List<object>();
for (int cy = y; cy < y + h; cy++)
for (int cx = x; cx < x + w; cx++)
{
int cell = Grid.XYToCell(cx, cy);
if (cell < 0 || cell >= Grid.CellCount) continue;
var el = Grid.Element[cell];
cells.Add(new
{
x = cx, y = cy,
element = el?.name ?? "Vacuum",
isSolid = Grid.Solid[cell],
isLiquid = el != null && el.IsLiquid,
isGas = el != null && el.IsGas,
isVacuum = el == null,
massKg = Grid.Mass[cell],
temperatureC = Grid.Temperature[cell] > 0 ? Grid.Temperature[cell] - 273.15f : -273.15f,
hasBuilding = Grid.Objects[cell, (int)ObjectLayer.Building] != null,
isDiggable = Grid.Solid[cell] && el != null && el.id != SimHashes.Unobtanium
});
}
return J(new { success = true, data = new { cells } });
}
catch (Exception ex) { return J(new { success = false, error = "read_error", message = ex.Message }); }
}
string ReadSlice(System.Collections.Specialized.NameValueCollection q)
{
try
{
string axis = q["axis"] ?? "x";
int index = int.Parse(q["index"] ?? "0");
int start = int.Parse(q["start"] ?? "0");
int end = int.Parse(q["end"] ?? "100");
var cells = new List<object>();
if (axis == "y")
for (int cx = start; cx <= end; cx++)
{
int cell = Grid.XYToCell(cx, index);
if (cell < 0 || cell >= Grid.CellCount) continue;
var el = Grid.Element[cell];
cells.Add(new { x = cx, y = index, element = el?.name ?? "Vacuum", isSolid = Grid.Solid[cell], massKg = Grid.Mass[cell], temperatureC = Grid.Temperature[cell] > 0 ? Grid.Temperature[cell] - 273.15f : -273.15f, hasBuilding = Grid.Objects[cell, (int)ObjectLayer.Building] != null });
}
else
for (int cy = start; cy <= end; cy++)
{
int cell = Grid.XYToCell(index, cy);
if (cell < 0 || cell >= Grid.CellCount) continue;
var el = Grid.Element[cell];
cells.Add(new { x = index, y = cy, element = el?.name ?? "Vacuum", isSolid = Grid.Solid[cell], massKg = Grid.Mass[cell], temperatureC = Grid.Temperature[cell] > 0 ? Grid.Temperature[cell] - 273.15f : -273.15f, hasBuilding = Grid.Objects[cell, (int)ObjectLayer.Building] != null });
}
return J(new { success = true, data = new { cells } });
}
catch (Exception ex) { return J(new { success = false, error = "read_error", message = ex.Message }); }
}
string ReadGas(System.Collections.Specialized.NameValueCollection q)
{
try
{
int x = int.Parse(q["x"] ?? "0"), y = int.Parse(q["y"] ?? "0"), r = int.Parse(q["radius"] ?? "20");
var gases = new Dictionary<string, GasData>();
for (int cy = Math.Max(0, y - r); cy <= Math.Min(Grid.HeightInCells - 1, y + r); cy++)
for (int cx = Math.Max(0, x - r); cx <= Math.Min(Grid.WidthInCells - 1, x + r); cx++)
{
int cell = Grid.XYToCell(cx, cy);
if (cell < 0) continue;
var el = Grid.Element[cell];
if (el != null && el.IsGas)
{
float m = Grid.Mass[cell];
if (gases.TryGetValue(el.name, out var existing)) { existing.mass += m; existing.count++; }
else { gases[el.name] = new GasData { gas = el.name, mass = m, count = 1 }; }
}
}
return J(new { success = true, data = new { gases = gases.Values } });
}
catch (Exception ex) { return J(new { success = false, error = "read_error", message = ex.Message }); }
}
// ── Registry readers ──────────────────────────────────
object ReadBuildingRegistry()
{
var list = new List<object>();
foreach (var def in Assets.BuildingDefs)
{
list.Add(new
{
id = def.PrefabID,
name = def.Name,
width = def.WidthInCells,
height = def.HeightInCells,
buildLocationRule = def.BuildLocationRule.ToString(),
materialCategory = def.MaterialCategory,
mass = def.Mass,
health = def.HitPoints
});
}
return list;
}
object ReadElementRegistry()
{
var list = new List<object>();
foreach (var e in ElementLoader.elements)
{
string state = "solid";
if (e.IsGas) state = "gas";
else if (e.IsLiquid) state = "liquid";
list.Add(new { id = e.id.ToString(), name = e.name, state, tag = e.tag.ToString(), specificHeatCapacity = e.specificHeatCapacity, thermalConductivity = e.thermalConductivity });
}
return list;
}
object ReadTechRegistry()
{
var list = new List<object>();
try
{
var db = Db.Get();
if (db == null) return list;
var techsProp = db.GetType().GetProperty("Techs");
if (techsProp == null) return list;
var techs = techsProp.GetValue(db);
if (techs == null) return list;
// Try IEnumerable
var enumerable = techs as System.Collections.IEnumerable;
if (enumerable != null)
{
foreach (var tech in enumerable)
{
if (tech == null) continue;
var idProp = tech.GetType().GetProperty("Id") ?? tech.GetType().GetField("Id") as System.Reflection.MemberInfo;
var nameProp = tech.GetType().GetProperty("Name") ?? tech.GetType().GetField("Name") as System.Reflection.MemberInfo;
string id = "", name = "";
if (idProp is System.Reflection.PropertyInfo pi) id = pi.GetValue(tech)?.ToString() ?? "";
else if (idProp is System.Reflection.FieldInfo fi) id = fi.GetValue(tech)?.ToString() ?? "";
if (nameProp is System.Reflection.PropertyInfo pi2) name = pi2.GetValue(tech)?.ToString() ?? "";
else if (nameProp is System.Reflection.FieldInfo fi2) name = fi2.GetValue(tech)?.ToString() ?? "";
list.Add(new { id, name });
}
}
}
catch { }
return list;
}
// ── Events ────────────────────────────────────────────
string ReadEvents(System.Collections.Specialized.NameValueCollection q)
{
int since = int.Parse(q["since"] ?? "-1");
int limit = Math.Min(int.Parse(q["limit"] ?? "50"), 200);
lock (eventLock)
{
var ev = eventLog.Where(e => e.id > since).Take(limit).ToList();
return J(new { success = true, data = new { events = ev, nextSeq = ev.Any() ? ev.Last().id : since } });
}
}
// ── Action Handlers ───────────────────────────────────
string QueueDig(string body)
{
try
{
var req = JsonConvert.DeserializeObject<CoordWidthRequest>(body);
if (req == null) return J(new { success = false, error = "invalid_request" });
int x = req.x, y = req.y, w = req.width, h = req.height;
int diggableCount = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
{
int cell = Grid.XYToCell(x + dx, y + dy);
if (cell >= 0 && cell < Grid.CellCount && Diggable.IsDiggable(cell))
diggableCount++;
}
if (diggableCount == 0)
return J(new { success = false, error = "nothing_to_dig", errorMessage = "No diggable cells in area" });
QWithCamera(x, y, () =>
{
int q = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
{
int cell = Grid.XYToCell(x + dx, y + dy);
if (cell >= 0 && cell < Grid.CellCount && Diggable.IsDiggable(cell))
{ DigTool.PlaceDig(cell, 0); q++; }
}
LogEvent("action", "info", "[Dig]", $"Queued {q} digs at ({x},{y}) {w}x{h}");
});
return J(new { success = true, data = new { result = "dig_queued", count = diggableCount } });
}
catch (Exception ex) { return J(new { success = false, error = "dig_error", errorMessage = ex.Message }); }
}
string QueueBuild(string body)
{
try
{
var req = JsonConvert.DeserializeObject<BuildRequest>(body);
if (req == null) return J(new { success = false, error = "invalid_request" });
string buildingId = req.buildingId;
int x = req.x, y = req.y;
var def = Assets.GetBuildingDef(buildingId);
if (def == null)
return J(new { success = false, error = "unknown_building", errorMessage = $"Building '{buildingId}' not found. Use /api/registry/buildings to list" });
int cell = Grid.XYToCell(x, y);
if (cell < 0 || cell >= Grid.CellCount)
return J(new { success = false, error = "bounds", errorMessage = "Target cell out of bounds" });
// Check material availability
var world = ClusterManager.Instance?.activeWorld;
var inv = world?.worldInventory;
var materialInfo = new List<object>();
bool hasAllMaterials = true;
if (def.MaterialCategory != null && inv != null)
{
foreach (var cat in def.MaterialCategory)
{
if (string.IsNullOrEmpty(cat)) continue;
var available = ElementLoader.elements
.Where(el => inv.GetAmount(el.tag, false) > 0)
.Where(el => ElementMatchesCategory(el, cat))
.OrderByDescending(el => inv.GetAmount(el.tag, false))
.ToList();
if (available.Count == 0)
{
hasAllMaterials = false;
materialInfo.Add(new { category = cat, available = false, suggestion = $"No '{cat}' materials available. Produce or dig more." });
}
else
{
var best = available.First();
materialInfo.Add(new { category = cat, available = true, bestElement = best.name, bestElementId = best.id.ToString(), availableKg = inv.GetAmount(best.tag, false) });
}
}
}
// Select materials for construction
var selectedElements = new List<Tag>();
if (def.MaterialCategory != null && inv != null)
{
foreach (var cat in def.MaterialCategory)
{
if (string.IsNullOrEmpty(cat)) continue;
var best = ElementLoader.elements
.Where(el => inv.GetAmount(el.tag, false) > 0)
.FirstOrDefault(el => ElementMatchesCategory(el, cat));
if (best != null) selectedElements.Add(best.tag);
}
}
// Validate we have real element tags before queuing
var validatedElements = new List<Tag>();
foreach (var t in selectedElements)
{
if (t.IsValid && ElementLoader.GetElement(t) != null)
validatedElements.Add(t);
}
// If no valid elements, use SandStone as default building material
if (validatedElements.Count == 0 && def.MaterialCategory != null && def.MaterialCategory.Length > 0)
{
var defaultEl = ElementLoader.elements.FirstOrDefault(e => e.id == SimHashes.SandStone);
if (defaultEl != null) validatedElements.Add(defaultEl.tag);
}
var capturedElements = new List<Tag>(validatedElements);
string capturedBid = buildingId;
QWithCamera(x, y, () =>
{
try
{
int c = Grid.XYToCell(x, y);
if (c < 0 || c >= Grid.CellCount) return;
var d = Assets.GetBuildingDef(capturedBid);
if (d == null) return;
var pos = Grid.CellToPos(c);
var go = d.Instantiate(pos, Orientation.Neutral, capturedElements, (int)d.SceneLayer);
if (go != null)
{
go.SetActive(true);
LogEvent("action", "info", "[Build]", $"Queued {capturedBid} at ({x},{y})");
}
}
catch (Exception ex) { LogEvent("build_error", "critical", "[Build]", $"{capturedBid} at ({x},{y}): {ex.Message}"); }
});
return J(new { success = true, data = new { result = hasAllMaterials ? "build_queued" : "build_queued_material_shortage", buildingId, x, y, materialInfo, hasAllMaterials } });
}
catch (Exception ex) { return J(new { success = false, error = "build_error", errorMessage = ex.Message }); }
}
static bool ElementMatchesCategory(Element el, string category)
{
switch (category)
{
case "RawMineral": return el.HasTag(GameTags.ConsumableOre);
case "Metal": return el.HasTag(GameTags.Metal);
case "RefinedMetal": return el.HasTag(GameTags.RefinedMetal);
case "Plastic": return el.HasTag(GameTags.Plastic);
case "Glass": return el.HasTag(GameTags.Glass);
case "BuildingFiber": return el.HasTag(GameTags.BuildingFiber);
case "Transparent": return el.HasTag(GameTags.Transparent);
default: return true;
}
}
string QueueDeconstruct(string body)
{
try
{
var req = JsonConvert.DeserializeObject<CoordRequest>(body);
if (req == null) return J(new { success = false, error = "invalid_request" });
int x = req.x, y = req.y, cell = Grid.XYToCell(x, y);
if (cell < 0 || cell >= Grid.CellCount) return J(new { success = false, error = "bounds" });
var buildingGo = Grid.Objects[cell, (int)ObjectLayer.Building];
if (buildingGo == null) return J(new { success = false, error = "no_building", errorMessage = $"No building at ({x},{y})" });
var deconstructable = buildingGo.GetComponent<Deconstructable>();
if (deconstructable == null) return J(new { success = false, error = "cannot_deconstruct" });
QWithCamera(x, y, () => { deconstructable.QueueDeconstruction(true); LogEvent("action", "info", "[Deconstruct]", $"Deconstruct at ({x},{y})"); });
return J(new { success = true, data = new { result = "deconstruct_queued" } });
}
catch (Exception ex) { return J(new { success = false, error = "deconstruct_error", errorMessage = ex.Message }); }
}
string QueuePrioritize(string body)
{
try
{
var req = JsonConvert.DeserializeObject<PriorReq>(body);
if (req == null) return J(new { success = false, error = "invalid_request" });
int x = req.x, y = req.y, p = req.priority;
if (p < 1 || p > 9) return J(new { success = false, error = "invalid_priority", errorMessage = "Priority 1-9" });
int cell = Grid.XYToCell(x, y);
if (cell < 0 || cell >= Grid.CellCount) return J(new { success = false, error = "bounds" });
var buildingGo = Grid.Objects[cell, (int)ObjectLayer.Building];
if (buildingGo == null) return J(new { success = false, error = "no_building" });
var prioritizable = buildingGo.GetComponent<Prioritizable>();
if (prioritizable == null) return J(new { success = false, error = "cannot_prioritize" });
int p2 = p;
QWithCamera(x, y, () => { SetPrioritizablePriority(prioritizable, p2); });
return J(new { success = true, data = new { result = "priority_set", priority = p2 } });
}
catch (Exception ex) { return J(new { success = false, error = "prioritize_error", errorMessage = ex.Message }); }
}
string QueueResearch(string body)
{
try
{
var req = JsonConvert.DeserializeObject<ResearchRequest>(body);
if (req == null) return J(new { success = false, error = "invalid_request" });
string techId = req.techId;
// Find tech through Techs collection via reflection
object targetTech = null;
string targetTechId = techId;
try
{
var db = Db.Get();
var techsProp = db.GetType().GetProperty("Techs");
if (techsProp != null)
{
var techs = techsProp.GetValue(db) as System.Collections.IEnumerable;
if (techs != null)
{
foreach (var t in techs)
{
if (t == null) continue;
var idProp = t.GetType().GetProperty("Id");
var tid = idProp?.GetValue(t)?.ToString() ?? "";
if (tid == techId) { targetTech = t; break; }
}
}
}
}
catch { }
if (targetTech == null)
return J(new { success = false, error = "unknown_tech", errorMessage = $"Tech '{techId}' not found" });
var research = Research.Instance;
if (research == null) return J(new { success = false, error = "no_research" });
// Check prerequisites using reflection
var missingPrereqs = new List<string>();
var requiredTechsField = targetTech.GetType().GetField("requiredTech", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
if (requiredTechsField != null && requiredTechsField.GetValue(targetTech) is System.Collections.IEnumerable requiredTechs)
{
foreach (var t in requiredTechs)
{
if (t == null) continue;
var idProp = t.GetType().GetProperty("Id");
var tid = idProp?.GetValue(t)?.ToString() ?? "";
if (!IsTechCompleteReflect(research, t))
missingPrereqs.Add(tid);
}
}
if (missingPrereqs.Count > 0)
return J(new { success = false, error = "missing_prerequisites", errorMessage = $"Requires: {string.Join(", ", missingPrereqs)}" });
if (IsTechCompleteReflect(research, targetTech))
return J(new { success = true, data = new { result = "already_completed", techId } });
Q(() => { SetActiveResearch(research, targetTech); LogEvent("action", "info", "[Research]", $"Active: {targetTechId}"); });
return J(new { success = true, data = new { result = "research_queued", techId } });
}
catch (Exception ex) { return J(new { success = false, error = "research_error", errorMessage = ex.Message }); }
}
string QueueMop(string body)
{
try
{
var req = JsonConvert.DeserializeObject<CoordRequest>(body);
if (req == null) return J(new { success = false, error = "invalid_request" });
int x = req.x, y = req.y, cell = Grid.XYToCell(x, y);
if (cell < 0 || cell >= Grid.CellCount) return J(new { success = false, error = "bounds" });
var el = Grid.Element[cell];
if (el == null || !el.IsLiquid) return J(new { success = false, error = "not_liquid", errorMessage = $"No liquid at ({x},{y})" });
if (Grid.Mass[cell] < 0.001f) return J(new { success = false, error = "too_little", errorMessage = "Mass too small" });
int c2 = cell;
QWithCamera(x, y, () => { PlaceMopFallback(c2); LogEvent("action", "info", "[Mop]", $"Mop at ({x},{y})"); });
return J(new { success = true, data = new { result = "mop_queued" } });
}
catch (Exception ex) { return J(new { success = false, error = "mop_error", errorMessage = ex.Message }); }
}
string QueueHarvest(string body)
{
try
{
var req = JsonConvert.DeserializeObject<CoordRequest>(body);
if (req == null) return J(new { success = false, error = "invalid_request" });
int x = req.x, y = req.y, cell = Grid.XYToCell(x, y);
if (cell < 0 || cell >= Grid.CellCount) return J(new { success = false, error = "bounds" });
var plantGo = Grid.Objects[cell, (int)ObjectLayer.Plants];
if (plantGo == null) return J(new { success = false, error = "no_plant", errorMessage = $"No plant at ({x},{y})" });
var harvestable = plantGo.GetComponent<Harvestable>();
if (harvestable == null) return J(new { success = false, error = "not_harvestable" });
QWithCamera(x, y, () => { harvestable.Harvest(); LogEvent("action", "info", "[Harvest]", $"Harvest at ({x},{y})"); });
return J(new { success = true, data = new { result = "harvest_queued" } });
}
catch (Exception ex) { return J(new { success = false, error = "harvest_error", errorMessage = ex.Message }); }
}
string QueueBatch(string body)
{
try
{
var req = JsonConvert.DeserializeObject<BatchReq>(body);
if (req?.actions == null || req.actions.Count == 0)
return J(new { success = false, error = "invalid_request", errorMessage = "No actions" });
var results = new List<object>();
int successCount = 0, failCount = 0;
foreach (var a in req.actions)
{
try
{
string subResult = null;
if (a.type == "dig")
subResult = QueueDig(J(new CoordWidthRequest { x = a.x ?? 0, y = a.y ?? 0, width = a.width ?? 1, height = a.height ?? 1 }));
else if (a.type == "build")
subResult = QueueBuild(J(new BuildRequest { buildingId = a.buildingId, x = a.x ?? 0, y = a.y ?? 0 }));
else if (a.type == "deconstruct")
subResult = QueueDeconstruct(J(new CoordRequest { x = a.x ?? 0, y = a.y ?? 0 }));
else if (a.type == "prioritize")
subResult = QueuePrioritize(J(new PriorReq { x = a.x ?? 0, y = a.y ?? 0, priority = a.priority ?? 5 }));
else if (a.type == "research")
subResult = QueueResearch(J(new ResearchRequest { techId = a.techId }));
else
{ failCount++; results.Add(new { type = a.type, success = false, error = "unknown_type" }); continue; }
if (subResult != null && subResult.Contains("\"success\":true"))
{ successCount++; results.Add(new { type = a.type, buildingId = a.buildingId, success = true }); }
else
{ failCount++; results.Add(new { type = a.type, buildingId = a.buildingId, success = false, error = subResult }); }
}
catch (Exception ex) { failCount++; results.Add(new { type = a.type, success = false, error = ex.Message }); }
}
return J(new { success = true, data = new { total = req.actions.Count, successCount, failCount, results } });
}
catch (Exception ex) { return J(new { success = false, error = "batch_error", errorMessage = ex.Message }); }
}
string QueueSave(string body)
{
string saveName = null;
try { var d = JsonConvert.DeserializeAnonymousType(body, new { name = "" }); saveName = d?.name; } catch { }
string sn = saveName;
Q(() =>
{
try
{
string path;
if (!string.IsNullOrEmpty(sn))
{ var dir = Path.GetDirectoryName(SaveLoader.GetActiveSaveFilePath()); path = Path.Combine(dir ?? ".", sn + ".sav"); }
else
{ path = SaveLoader.GetActiveSaveFilePath(); var ts = System.DateTime.Now.ToString("yyyyMMdd_HHmmss"); path = path.Replace(".sav", $"_{ts}.sav"); }
SaveLoader.Instance.Save(path, false, false);
LogEvent("action", "info", "[Save]", $"Saved: {Path.GetFileName(path)}");
}
catch (Exception ex) { LogEvent("action_error", "critical", "[Save]", ex.Message); }
});
return J(new { success = true, data = new { result = "save_queued", saveName = sn } });
}
string QueueLoad(string body)
{
string saveName = null;
try { var d = JsonConvert.DeserializeAnonymousType(body, new { name = "" }); saveName = d?.name; } catch { }
if (string.IsNullOrEmpty(saveName)) return J(new { success = false, error = "invalid_request", errorMessage = "save name required" });
string sn = saveName;
Q(() =>
{
try
{
var dir = Path.GetDirectoryName(SaveLoader.GetActiveSaveFilePath());
string path = Path.Combine(dir ?? ".", sn + ".sav");
if (File.Exists(path)) { LoadSave(path); LogEvent("action", "warning", "[Load]", $"Loading: {sn}"); }
else LogEvent("action_error", "critical", "[Load]", $"Save not found: {sn}");
}
catch (Exception ex) { LogEvent("action_error", "critical", "[Load]", ex.Message); }
});
return J(new { success = true, data = new { result = "load_queued", saveName = sn } });
}
string QueuePriorityGlobal(string body)
{
try
{
var d = JsonConvert.DeserializeAnonymousType(body, new { target = "", priority = 5 });
if (d == null || string.IsNullOrEmpty(d.target)) return J(new { success = false, error = "invalid_request" });
if (d.priority < 1 || d.priority > 9) return J(new { success = false, error = "invalid_priority" });
int p = d.priority; string t = d.target;
int p2 = p; string t2 = t;
Q(() =>
{
foreach (var b in Components.BuildingCompletes.Items)
if (b.Def.PrefabID == t2 || b.Def.Name == t2)
{
var pri = b.GetComponent<Prioritizable>();
if (pri != null) SetPrioritizablePriority(pri, p2);
}
LogEvent("action", "info", "[PriorityGlobal]", $"{t2} -> {p2}");
});
return J(new { success = true, data = new { result = "priority_global_set", target = t, priority = p } });
}
catch (Exception ex) { return J(new { success = false, error = "priority_error", errorMessage = ex.Message }); }
}
string QueuePriorityType(string body)
{
try
{
var d = JsonConvert.DeserializeAnonymousType(body, new { buildingType = "", priority = 5 });
if (d == null || string.IsNullOrEmpty(d.buildingType)) return J(new { success = false, error = "invalid_request" });
if (d.priority < 1 || d.priority > 9) return J(new { success = false, error = "invalid_priority" });
int p = d.priority; string t = d.buildingType;
int p2 = p; string t2 = t;
Q(() =>
{
foreach (var b in Components.BuildingCompletes.Items)
if (b.Def.PrefabID == t2 || b.Def.Name == t2)
{
var pri = b.GetComponent<Prioritizable>();
if (pri != null) SetPrioritizablePriority(pri, p2);
}
LogEvent("action", "info", "[PriorityType]", $"{t2} -> {p2}");
});
return J(new { success = true, data = new { result = "priority_type_set", buildingType = t, priority = p } });
}
catch (Exception ex) { return J(new { success = false, error = "priority_error", errorMessage = ex.Message }); }
}
string QueueCamera(string body)
{
try
{
int x = 0, y = 0; float zoom = -1;
var d = JsonConvert.DeserializeAnonymousType(body, new { x = 0, y = 0, zoom = -1f });
if (d != null) { x = d.x; y = d.y; zoom = d.zoom; }
int cx = x, cy = y; float cz = zoom;
Q(() =>
{
var cc = CameraController.Instance;
if (cc != null)
{
var pos = Grid.CellToPos(Grid.XYToCell(cx, cy));
pos.z = -35f;
cc.transform.SetPosition(pos);
if (cz > 0 && Camera.main != null) Camera.main.orthographicSize = Mathf.Clamp(cz, 5f, 80f);
LogEvent("action", "info", "[Camera]", $"Moved to ({cx},{cy})");
}
});
return J(new { success = true, data = new { result = "camera_moved", x, y, zoom } });
}
catch (Exception ex) { return J(new { success = false, error = "camera_error", errorMessage = ex.Message }); }
}
// ── Screenshot ────────────────────────────────────────
void ServeScreenshot(HttpListenerContext ctx)
{
try
{
string ssPath = null;
var ev = new ManualResetEvent(false);
Q(() =>
{
try
{
string dir = Path.Combine(Application.dataPath, "..", "oni_agent_screenshots");
Directory.CreateDirectory(dir);
string path = Path.Combine(dir, $"oni_agent_cycle{GameClock.Instance.GetCycle()}.png");
ScreenCapture.CaptureScreenshot(path);
ssPath = path;
lastScreenshotPath = path;
}
catch { }
ev.Set();
});
ev.WaitOne(15000);
if (ssPath != null && File.Exists(ssPath))
{
for (int i = 0; i < 30 && !IsFileReady(ssPath); i++)
Thread.Sleep(200);
if (IsFileReady(ssPath))
{
var b = File.ReadAllBytes(ssPath);
ctx.Response.ContentType = "image/png";
ctx.Response.ContentLength64 = b.Length;
ctx.Response.OutputStream.Write(b, 0, b.Length);
ctx.Response.OutputStream.Close();
return;
}
}
}
catch { }
ctx.Response.StatusCode = 404;
try { ctx.Response.OutputStream.Close(); } catch { }
}
static bool IsFileReady(string path)
{
try { using (var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None)) return true; }
catch { return false; }
}
// ── Reflection helpers for APIs that may be internal ──
static void SetActiveResearch(object research, object tech)
{
try
{
var method = research.GetType().GetMethod("SetActiveResearch", new[] { typeof(Tech), typeof(bool) });
if (method != null) method.Invoke(research, new object[] { tech, true });
else
{
method = research.GetType().GetMethod("SetActiveResearch", new[] { tech.GetType(), typeof(bool) });
if (method != null) method.Invoke(research, new object[] { tech, true });
}
}
catch { }
}
static void SetPrioritizablePriority(Prioritizable p, int priority)
{
try
{
var setting = new PrioritySetting(PriorityScreen.PriorityClass.basic, priority);
var method = typeof(Prioritizable).GetMethod("SetPriority", new[] { typeof(PrioritySetting) });
if (method != null) method.Invoke(p, new object[] { setting });
else
{
// Try reflection-based approach
var field = typeof(Prioritizable).GetField("_prioritySetting", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (field != null) field.SetValue(p, setting);
}
}
catch { }
}
static bool IsTechCompleteReflect(object research, object tech)
{
try
{
var method = research.GetType().GetMethod("IsTechComplete", new[] { tech.GetType() });
if (method != null) return (bool)method.Invoke(research, new object[] { tech });
// Try check completed list
var completedField = research.GetType().GetField("completedTechs", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (completedField != null && completedField.GetValue(research) is System.Collections.IList list)
{
var techIdProp = tech.GetType().GetProperty("Id");
string techId = techIdProp?.GetValue(tech)?.ToString() ?? "";
foreach (var t in list)
{
var idProp = t.GetType().GetProperty("Id");
if (idProp != null && idProp.GetValue(t)?.ToString() == techId) return true;
}
}
}
catch { }
return false;
}
static void PlaceMopFallback(int cell)
{
try
{
// Try MopTool.PlaceMop (may not exist in all versions)
var mopType = typeof(MopTool);
var placeMop = mopType.GetMethod("PlaceMop", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
if (placeMop != null) { placeMop.Invoke(null, new object[] { cell, 0 }); return; }
// Fallback: place dig order + mop placer
var mopGo = Grid.Objects[cell, (int)ObjectLayer.MopPlacer];
if (mopGo == null)
{
DigTool.PlaceDig(cell, 0);
}
}
catch { }
}
static void LoadSave(string path)
{
try
{
var sl = SaveLoader.Instance;
if (sl == null) return;
var method = sl.GetType().GetMethod("Load", new[] { typeof(string), typeof(bool) });
if (method != null) method.Invoke(sl, new object[] { path, false });
else
{
method = sl.GetType().GetMethod("Load", new[] { typeof(string) });
if (method != null) method.Invoke(sl, new object[] { path });
}
}
catch (Exception ex) { LogEventStatic("action_error", "critical", "[Load]", ex.Message); }
}
}
public class GasData { public string gas; public float mass; public int count; }
}