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 actions; } public class Mod : UserMod2 { private HttpListener _listener; internal static ConcurrentQueue cmdQueue = new ConcurrentQueue(); internal static List eventLog = new List(); 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(); 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 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(); var inv = world.worldInventory; if (inv == null) return new List(); var list = new List(); 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(); 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(); foreach (var minion in Components.MinionIdentities.Items) { var pos = minion.transform.position; var health = minion.gameObject.GetComponent(); 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(); 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(); var list = new List(); 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(); foreach (var building in Components.BuildingCompletes.Items) { var storage = building.gameObject.GetComponent(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(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(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(); 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(); 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(); 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(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(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(); 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(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(); 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(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(); 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(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(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(); 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(body); if (req?.actions == null || req.actions.Count == 0) return J(new { success = false, error = "invalid_request", errorMessage = "No actions" }); var results = new List(); 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(); 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(); 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; } }