using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using VRage.Plugins; using XFE.SeAgent.Plugin.Game; using XFE.SeAgent.Plugin.Transport; namespace XFE.SeAgent.Plugin { public sealed class AgentPlugin : IPlugin { private readonly object logLock = new object(); private readonly Queue events = new Queue(); private readonly DateTime started = DateTime.UtcNow; private AgentServer server; private GameDebugApi game; private string directory, endpoint, logFile; private long ticks, eventSequence; private bool disposed; public void Init(object gameInstance) { if (server != null || disposed) return; directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "XFE", "SpaceEngineersAgent"); Directory.CreateDirectory(directory); logFile = Path.Combine(directory, "agent.log"); var allowed = ReadAllowedWorlds(Path.Combine(directory, "config.json")); game = new GameDebugApi(path => allowed.Contains(Canonical(path)), Log); server = new AgentServer(Execute, Log); server.Start(); string endpoints = Path.Combine(directory, "endpoints"); Directory.CreateDirectory(endpoints); using (var process = Process.GetCurrentProcess()) { endpoint = Path.Combine(endpoints, process.Id + ".json"); var data = new JObject { ["version"] = "1.0.0", ["protocolVersion"] = 1, ["pipeName"] = server.PipeName, ["pid"] = process.Id, ["startTimeUtc"] = process.StartTime.ToUniversalTime().ToString("o"), ["executablePath"] = process.MainModule.FileName, ["createdUtc"] = DateTime.UtcNow.ToString("o") }; File.WriteAllText(endpoint + ".tmp", data.ToString(Formatting.Indented)); if (File.Exists(endpoint)) File.Replace(endpoint + ".tmp", endpoint, null); else File.Move(endpoint + ".tmp", endpoint); } Log("Agent Bridge ready: " + server.PipeName + "; authorized debug worlds: " + allowed.Count); } private static string Canonical(string path) => Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); private HashSet ReadAllowedWorlds(string path) { var result = new HashSet(StringComparer.OrdinalIgnoreCase); if (!File.Exists(path)) return result; if (new FileInfo(path).Length > 65536) throw new InvalidDataException("Agent configuration exceeds 64 KiB."); var config = JObject.Parse(File.ReadAllText(path)); foreach (var item in config["allowedWorldPaths"] as JArray ?? new JArray()) { string value = (string)item; if (!string.IsNullOrWhiteSpace(value) && Path.IsPathRooted(value)) result.Add(Canonical(value)); } return result; } private JObject Execute(string method, JObject args) { if (method == "agent.ping") return new JObject { ["version"] = "1.0.0", ["protocolVersion"] = 1, ["ticks"] = ticks, ["utc"] = DateTime.UtcNow.ToString("o"), ["uptimeSeconds"] = (DateTime.UtcNow - started).TotalSeconds }; if (method == "agent.capabilities") return new JObject { ["protocolVersion"] = 1, ["transport"] = "current-user local Windows named pipe", ["maxRequestBytes"] = 1048576, ["maxResponseBytes"] = 4194304, ["maxTimeoutMs"] = 30000, ["entityIds"] = "decimal strings; do not round through JavaScript Number", ["mutationPolicy"] = "Only explicitly authorized offline test worlds; all game access runs on the game thread.", ["methods"] = new JArray("agent.ping", "agent.capabilities", "agent.events", "world.status", "world.load", "world.save", "world.pause", "world.exit", "grids.list", "grids.get", "grid.stop", "blocks.list", "blocks.get", "blocks.actions", "blocks.action", "blocks.properties", "blocks.setProperty", "pb.read", "pb.inspect", "pb.deploy", "pb.run", "cameras.scan", "telemetry.snapshot", "inventory.route", "sorters.setFilters", "debug.screenshot") }; if (method == "agent.events") { long since = (long?)args["after"] ?? 0; var items = new JArray(); lock (logLock) foreach (var item in events) if ((long)item["sequence"] > since) items.Add(item.DeepClone()); return new JObject { ["events"] = items, ["lastSequence"] = eventSequence }; } return game.Execute(method, args); } public void Update() { if (disposed || server == null) return; ticks++; try { game.Update(); server.Pump(2); } catch (Exception error) { Log("Update error: " + error); } } private void Log(string message) { lock (logLock) { var item = new JObject { ["sequence"] = ++eventSequence, ["utc"] = DateTime.UtcNow.ToString("o"), ["message"] = message }; events.Enqueue(item); while (events.Count > 256) events.Dequeue(); try { if (logFile == null) return; if (File.Exists(logFile) && new FileInfo(logFile).Length > 4 * 1024 * 1024) { if (File.Exists(logFile + ".1")) File.Delete(logFile + ".1"); File.Move(logFile, logFile + ".1"); } File.AppendAllText(logFile, item.ToString(Formatting.None) + Environment.NewLine); } catch { } } } public void Dispose() { if (disposed) return; disposed = true; server?.Dispose(); game?.Dispose(); Log("Agent Bridge stopped."); try { if (endpoint != null && File.Exists(endpoint)) File.Delete(endpoint); } catch { } } } }