using HarmonyLib; using KMod; using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; using System.Net; using System.Text; using UnityEngine; namespace ONIAgentBridge { 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(); public class QueueProcessor : UnityEngine.MonoBehaviour { public void Update() { System.Action a; while (cmdQueue.TryDequeue(out a)) try { a(); } catch (System.Exception ex) { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[Q] "+ex.Message}); } } } } public override void OnLoad(Harmony harmony) { base.OnLoad(harmony); var go = new GameObject("ONI_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); } void OnRequest(IAsyncResult ar) { try { var ctx = _listener.EndGetContext(ar); _listener.BeginGetContext(OnRequest, null); Process(ctx); } catch { } } void Process(HttpListenerContext ctx) { try { var path = ctx.Request.Url.AbsolutePath.TrimEnd('/'); var method = ctx.Request.HttpMethod; var q = ctx.Request.QueryString; string json = null; System.Action respond = () => { var b = Encoding.UTF8.GetBytes(json ?? "{}"); ctx.Response.ContentType = "application/json"; ctx.Response.OutputStream.Write(b, 0, b.Length); ctx.Response.OutputStream.Close(); }; if (path == "/api/screenshot/latest" && method == "GET") { ServeScreenshot(ctx); return; } else if (path == "/health" && method == "GET") json = J(new { status = "ok" }); else if (path == "/api/state/game" && method == "GET") json = ReadGame(); else if (path == "/api/state/resources" && method == "GET") json = ReadResources(); else if (path == "/api/state/buildings" && method == "GET") json = ReadBuildings(); else if (path == "/api/state/duplicants" && method == "GET") json = ReadDupes(); else if (path == "/api/state/rooms" && method == "GET") json = ReadRooms(); 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/gas" && method == "GET") json = ReadGas(q); else if (path == "/api/state/co2" && method == "GET") json = ReadCO2(); else if (path == "/api/state/temperature/zones" && method == "GET") json = ReadTemp(); else if (path == "/api/state/power" && method == "GET") json = ReadPower(); else if (path == "/api/state/pipes" && method == "GET") json = ReadPipes(q); else if (path == "/api/state/events" && method == "GET") json = ReadEvents(q); else if (path == "/api/state/alert" && method == "GET") json = ReadAlerts(); else if (path == "/api/state/storage" && method == "GET") json = ReadStorage(); else if (path == "/api/state/saves" && method == "GET") json = ReadSaves(); else if (path == "/api/state/camera" && method == "GET") json = ReadCamera(); else if (path == "/api/registry/buildings" && method == "GET") json = ReadBuildingReg(); else if (path == "/api/registry/elements" && method == "GET") json = ReadElementReg(); else if (path == "/api/action/pause" && method == "POST") { cmdQueue.Enqueue(() => { try { SpeedControlScreen.Instance.Pause(false, true); } catch { } }); json = Ok("paused"); } else if (path == "/api/action/unpause" && method == "POST") { cmdQueue.Enqueue(() => { try { SpeedControlScreen.Instance.Unpause(true); SpeedControlScreen.Instance.SetSpeed(1); } catch { } }); json = Ok("unpaused"); } else if (path == "/api/action/dig" && method == "POST") json = QueueDig(ctx); else if (path == "/api/action/build" && method == "POST") json = QueueBuild(ctx); else if (path == "/api/action/test" && method == "POST") { cmdQueue.Enqueue(() => { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[Test] queue works!"}); } }); json = Ok("test_queued"); } else json = J(new { error = "not_found", path }); respond(); } catch { } } string J(object o) => JsonConvert.SerializeObject(o); string Ok(string r, object d = null) => J(new { success = true, result = r, data = d ?? new { } }); string Fail(string e, string m = null) => J(new { success = false, error = e, errorMessage = m ?? e }); void PushEvent(string t, string s, string title, string msg, string cat = "general") { lock (eventLock) { eventLog.Add(new GameEvent { id = eventSeq++, type = t, severity = s, timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), cycle = 0, category = cat, title = title, message = msg }); if (eventLog.Count > 500) eventLog.RemoveRange(0, eventLog.Count - 500); } } // === READERS (safe from background threads) === string ReadGame() => J(new { cycle = GameClock.Instance?.GetCycle() ?? 0, duplicantCount = Components.MinionIdentities.Count(), worldSize = Grid.CellCount, gridWidth = Grid.WidthInCells, gridHeight = Grid.HeightInCells, isPaused = SpeedControlScreen.Instance.IsPaused, gameSpeed = SpeedControlScreen.Instance.IsPaused ? 0 : SpeedControlScreen.Instance.GetSpeed() }); string ReadResources() { try { var l = new List(); foreach (var e in ElementLoader.elements) { float a = 0; try { var w = ClusterManager.Instance?.activeWorld; if (w != null) a = w.worldInventory.GetAmount(e.tag, false); } catch { } if (a > 0) l.Add(new { id = e.id.ToString(), name = e.name, tag = e.tag.ToString(), amount = a, unit = "kg", state = e.IsGas ? "gas" : e.IsLiquid ? "liquid" : "solid" }); } return J(l); } catch { return J(new List()); } } string ReadBuildings() { try { var l = new List(); foreach (var i in Components.BuildingCompletes) { var b = (BuildingComplete)i; var d = b.Def; l.Add(new { id = d.PrefabID, name = d.Name, x = (int)b.transform.position.x, y = (int)b.transform.position.y, width = d.WidthInCells, height = d.HeightInCells }); } return J(l); } catch { return J(new List()); } } string ReadDupes() { try { var l = new List(); foreach (var i in Components.MinionIdentities) { var m = (MinionIdentity)i; l.Add(new { name = m.GetProperName(), x = (int)m.transform.position.x, y = (int)m.transform.position.y, health = m.gameObject.GetComponent()?.hitPoints ?? 100 }); } return J(l); } catch { return J(new List()); } } string ReadRooms() { try { var l = new List(); var rp = Game.Instance.roomProber; if (rp != null) foreach (var r in rp.rooms) l.Add(new { id = r.roomType?.Id ?? "?", name = r.roomType?.Name ?? "?" }); return J(l); } catch { return J(new List()); } } string ReadCell(NameValueCollection q) { try { int x = int.Parse(q["x"] ?? "-1"), y = int.Parse(q["y"] ?? "-1"); int c = Grid.XYToCell(x, y); if (c < 0 || c >= Grid.CellCount) return J(new { error = "bounds" }); var el = Grid.Element[c]; return J(new { x, y, cell = c, element = el?.name ?? "Vacuum", massKg = Grid.Mass[c], temperatureC = Grid.Temperature[c] > 0 ? Grid.Temperature[c] - 273.15f : -273.15f, isSolid = Grid.Solid[c], isVisible = true, hasBuilding = Grid.Objects[c, (int)ObjectLayer.Building] != null, buildingName = Grid.Objects[c, (int)ObjectLayer.Building]?.name, hasDuplicant = Grid.Objects[c, (int)ObjectLayer.Minion] != null, isVacuum = el == null, isDiggable = Grid.Solid[c] && el != null && el.id != SimHashes.Unobtanium }); } catch { return J(new { error = "err" }); } } string ReadCells(NameValueCollection q) { try { int x = int.Parse(q["x"] ?? "0"), y = int.Parse(q["y"] ?? "0"), w = int.Parse(q["width"] ?? "10"), h = int.Parse(q["height"] ?? "10"); var cs = new List(); for (int cy = y; cy < y + h; cy++) for (int cx = x; cx < x + w; cx++) { int c = Grid.XYToCell(cx, cy); if (c >= 0 && c < Grid.CellCount) cs.Add(new { x = cx, y = cy, element = Grid.Element[c]?.name ?? "Vacuum", isSolid = Grid.Solid[c], hasBuilding = Grid.Objects[c, (int)ObjectLayer.Building] != null, hasDuplicant = Grid.Objects[c, (int)ObjectLayer.Minion] != null }); } return J(new { cells = cs }); } catch { return J(new { error = "err" }); } } string ReadGas(NameValueCollection q) { try { int x = int.Parse(q["x"] ?? "0"), y = int.Parse(q["y"] ?? "0"), r = int.Parse(q["radius"] ?? "20"); var g = 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 c = Grid.XYToCell(cx, cy); if (c < 0) continue; var el = Grid.Element[c]; if (el != null && el.IsGas) { float m = Grid.Mass[c]; if (g.ContainsKey(el.name)) { g[el.name].mass += m; g[el.name].count++; } else g[el.name] = new GasEntry { gas = el.name, mass = m, count = 1 }; } } return J(new { gases = g.Values }); } catch { return J(new { error = "err" }); } } string ReadCO2() { try { var p = new List(); int step = Math.Max(1, Grid.CellCount / 500); for (int i = 0; i < Grid.CellCount; i += step) { var el = Grid.Element[i]; if (el != null && el.id == SimHashes.CarbonDioxide && Grid.Mass[i] > 0.5f) { int x, y; Grid.CellToXY(i, out x, out y); p.Add(new { x, y, mass = Grid.Mass[i] }); } } return J(new { pockets = p }); } catch { return J(new { error = "err" }); } } string ReadTemp() { try { int s = Math.Max(1, Grid.CellCount / 300); float mn = float.MaxValue, mx = float.MinValue, sum = 0; int n = 0; for (int i = 0; i < Grid.CellCount; i += s) { float t = Grid.Temperature[i]; if (t <= 0) continue; float tc = t - 273.15f; sum += tc; n++; if (tc < mn) mn = tc; if (tc > mx) mx = tc; } return J(new { averageC = n > 0 ? sum / n : 0, minC = mn, maxC = mx }); } catch { return J(new { error = "err" }); } } string ReadPower() { try { var c = new List(); var m = Game.Instance.circuitManager; if (m != null) for (ushort i = 1; i <= 16; i++) { float u = m.GetWattsUsedByCircuit(i); if (u > 0 || m.HasGenerators(i)) c.Add(new { id = (int)i, wattsUsed = u, maxWatts = m.GetMaxSafeWattageForCircuit(i), isOverloaded = u > m.GetMaxSafeWattageForCircuit(i) }); } return J(new { circuits = c }); } catch { return J(new { error = "err" }); } } string ReadPipes(NameValueCollection q) { try { var s = new List(); string t = q["type"] ?? "all"; foreach (var f in new[] { new { n = "liquid", fl = Game.Instance.liquidConduitFlow }, new { n = "gas", fl = Game.Instance.gasConduitFlow } }) { if (t != "all" && t != f.n) continue; if (f.fl == null) continue; int cnt = 0; foreach (var i in Components.BuildingCompletes) { if (cnt > 100) break; var b = (BuildingComplete)i; var pid = b.Def.PrefabID; if (pid != (f.n == "gas" ? "GasConduit" : "LiquidConduit") && pid != (f.n == "gas" ? "GasConduitBridge" : "LiquidConduitBridge")) continue; var ct = f.fl.GetContents(b.GetCell()); if (ct.mass > 0) { s.Add(new { type = f.n, element = ct.element.ToString(), mass = ct.mass }); cnt++; } } } return J(new { segments = s }); } catch { return J(new { error = "err" }); } } string ReadEvents(NameValueCollection q) { int s = int.Parse(q["since"] ?? "-1"), l = Math.Min(int.Parse(q["limit"] ?? "50"), 200); lock (eventLock) { var ev = eventLog.Where(e => e.id > s).Take(l).ToList(); return J(new { events = ev, next_seq = ev.Any() ? ev.Last().id : s }); } } string ReadAlerts() { try { var l = new List(); var nm = global::NotificationManager.Instance; if (nm != null) { foreach (var fn in new[] { "notifications", "pendingNotifications" }) { try { var f = typeof(global::NotificationManager).GetField(fn, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); if (f == null) continue; var items = f.GetValue(nm) as System.Collections.IEnumerable; if (items != null) foreach (global::Notification n in items) l.Add(new { title = n.titleText, severity = n.Type.ToString() }); } catch { } } } return J(l); } catch { return J(new List()); } } string ReadStorage() { try { var l = new List(); foreach (var i in Components.BuildingCompletes) { var b = (BuildingComplete)i; var s = b.gameObject.GetComponent(); if (s == null || s.MassStored() <= 0) continue; l.Add(new { building = b.Def?.Name ?? b.name, x = (int)b.transform.position.x, y = (int)b.transform.position.y, mass = s.MassStored(), capacity = s.capacityKg }); } return J(new { storages = l.Take(50).ToList() }); } catch { return J(new { error = "err" }); } } string ReadSaves() { try { var l = new List(); string sp = SaveLoader.GetActiveSaveFilePath(); var d = Path.GetDirectoryName(sp); if (d != null && Directory.Exists(d)) foreach (var f in Directory.GetFiles(d, "*.sav")) { var fi = new FileInfo(f); l.Add(new { name = Path.GetFileNameWithoutExtension(f) }); } return J(new { saves = l }); } catch { return J(new { error = "err" }); } } string ReadCamera() { try { var c = CameraController.Instance; var p = c.transform.position; return J(new { x = p.x, y = p.y }); } catch { return J(new { error = "err" }); } } string ReadBuildingReg() { try { var l = new List(); foreach (var i in Assets.BuildingDefs) { var d = (BuildingDef)i; l.Add(new { id = d.PrefabID, name = d.Name, width = d.WidthInCells, height = d.HeightInCells }); } return J(l); } catch { return J(new { error = "err" }); } } string ReadElementReg() { try { var l = new List(); foreach (var e in ElementLoader.elements) l.Add(new { id = e.id.ToString(), name = e.name }); return J(l); } catch { return J(new { error = "err" }); } } string QueueDig(HttpListenerContext c) { try { var d = JsonConvert.DeserializeObject(new StreamReader(c.Request.InputStream).ReadToEnd()); if (d == null) return Fail("invalid"); int x = d.x, y = d.y, w = d.width, h = d.height; cmdQueue.Enqueue(() => { 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) try { DigTool.PlaceDig(cell, 0); } catch { } } }); return Ok("dig_queued"); } catch { return Fail("error"); } } string QueueBuild(HttpListenerContext c) { try { var d = JsonConvert.DeserializeObject(new StreamReader(c.Request.InputStream).ReadToEnd()); if (d == null) return Fail("invalid"); int x = d.x, y = d.y; string bid = d.buildingId; // Find available materials on HTTP thread (WorldInventory works from background) var availableTags = new List(); try { var worldInv = ClusterManager.Instance?.activeWorld?.worldInventory; if (worldInv != null) { var def = Assets.GetBuildingDef(bid); if (def != null) foreach (var cat in def.MaterialCategory ?? new string[0]) foreach (var elem in ElementLoader.elements) { if (worldInv.GetAmount(elem.tag, false) <= 0) continue; bool match = (cat == "RawMineral" && elem.HasTag(GameTags.ConsumableOre)); match = match || (cat == "Metal" && elem.HasTag(GameTags.Metal)); if (match) availableTags.Add(elem.tag); } } } catch { } // If no materials found, use default sand if (availableTags.Count == 0) availableTags.Add(new Tag("SandStone")); List capturedTags = new List(availableTags); cmdQueue.Enqueue(() => { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[Build] START "+bid}); } try { var def = Assets.GetBuildingDef(bid); if (def == null) { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] def null"}); } return; } int cell = Grid.XYToCell(x, y); if (cell < 0 || cell >= Grid.CellCount) { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] bounds"}); } return; } lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] cell="+cell}); } var world = ClusterManager.Instance?.activeWorld; lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] world ok"}); } for (int dy = 0; dy < def.HeightInCells; dy++) for (int dx = 0; dx < def.WidthInCells; dx++) { int c2 = Grid.XYToCell(x+dx, y+dy); if (c2 < 0 || c2 >= Grid.CellCount) continue; if (Grid.Objects[c2, (int)ObjectLayer.Building] != null) { lock(eventLock){eventLog.Add(new GameEvent{id=eventSeq++,title="[B] occupied"});} return; } } lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] cells ok"}); } var pos = Grid.CellToPos(cell); // Use game's build system (handles all initialization) lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] cells ok"}); } try { var loader = UnityEngine.Object.FindObjectOfType(); GameObject go = null; if (loader != null) go = loader.CreateBuildingUnderConstruction(def); if (go == null) { // Fallback: create complete building directly go = def.Instantiate(Grid.CellToPos(cell), Orientation.Neutral, null, (int)def.SceneLayer); } if (go != null) go.SetActive(true); lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[B] "+(go==null?"null":"ok")}); } } catch (Exception ex2) { lock(eventLock){eventLog.Add(new GameEvent{id=eventSeq++,title="[B] ex:"+ex2.Message});} } } catch (Exception ex) { lock(eventLock) { eventLog.Add(new GameEvent{id=eventSeq++,title="[Build] EX:"+ex.Message}); } } }); return Ok("build_queued"); } catch { return Fail("error"); } } static string _ss = null; void ServeScreenshot(HttpListenerContext ctx) { try { if (_ss != null && File.Exists(_ss)) { var b = File.ReadAllBytes(_ss); ctx.Response.ContentType = "image/png"; ctx.Response.ContentLength64 = b.Length; ctx.Response.OutputStream.Write(b, 0, b.Length); return; } } catch { } ctx.Response.StatusCode = 404; ctx.Response.OutputStream.Close(); } } public class GameEvent { public int id; public string type; public string severity; public long timestamp; public int cycle; public string category; public string title; public string message; } public class GasEntry { public string gas; public float mass; public int count; } public class DigReq { public int x; public int y; public int width; public int height; } public class BuildReq { public string buildingId; public int x; public int y; } }