Files
oniagent/mod/ONIAgentBridge.cs
2026-05-30 11:00:36 +08:00

242 lines
21 KiB
C#

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<System.Action> cmdQueue = new ConcurrentQueue<System.Action>();
internal static List<GameEvent> eventLog = new List<GameEvent>();
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<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);
}
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<object>(); 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<object>()); } }
string ReadBuildings() { try { var l = new List<object>(); 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<object>()); } }
string ReadDupes() { try { var l = new List<object>(); 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<Health>()?.hitPoints ?? 100 }); } return J(l); } catch { return J(new List<object>()); } }
string ReadRooms() { try { var l = new List<object>(); 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<object>()); } }
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<object>(); 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<string, GasEntry>(); 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<object>(); 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<object>(); 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<object>(); 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<object>(); 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<object>()); } }
string ReadStorage() { try { var l = new List<object>(); foreach (var i in Components.BuildingCompletes) { var b = (BuildingComplete)i; var s = b.gameObject.GetComponent<Storage>(); 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<object>(); 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<object>(); 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<object>(); 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<DigReq>(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<BuildReq>(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<Tag>();
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<Tag> capturedTags = new List<Tag>(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<BuildingLoader>();
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; }
}