1236 lines
59 KiB
C#
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; }
|
|
}
|