XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFE.SpaceEngineers.AgentBridge

【SpaceEngineer】AI调试插件

公开
关注 0 Fork 0 Star 0
UTF-8
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 { }
        }
    }
}