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<JObject> events = new Queue<JObject>();
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<string> ReadAllowedWorlds(string path)
{
var result = new HashSet<string>(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", "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 { }
}
}
}
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<JObject> events = new Queue<JObject>();
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<string> ReadAllowedWorlds(string path)
{
var result = new HashSet<string>(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", "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 { }
}
}
}