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.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json.Linq;
using Sandbox.Game.Entities;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using VRage.Game;
using VRage.Game.Entity;
using VRageMath;
using Terminal = Sandbox.ModAPI.Ingame.IMyTerminalBlock;

namespace XFE.SeAgent.Plugin.Game
{
    /// <summary>Game-thread-only, explicitly bounded debug operations. Never invokes arbitrary code.</summary>
    public sealed partial class GameDebugApi : IDisposable
    {
        private readonly Func<string, bool> _authorizedWorld;
        private readonly Action<string> _log;
        private bool _disposed;
        private string _loadingPath;
        private string _loadError;
        private DateTime _loadStarted;
        private MySession _pausedSession;

        public GameDebugApi(Func<string, bool> authorizedWorld, Action<string> log)
        {
            _authorizedWorld = authorizedWorld ?? throw new ArgumentNullException(nameof(authorizedWorld));
            _log = log ?? delegate { };
        }

        public JObject Execute(string method, JObject args)
        {
            if (_disposed) throw new ObjectDisposedException(nameof(GameDebugApi));
            args = args ?? new JObject();
            switch (method)
            {
                case "world.status": return WorldStatus();
                case "world.load": return LoadWorld(args);
                case "world.save": RequireWritableWorld(); return SaveWorld();
                case "world.exit": RequireWritableWorld(); return ExitWorld(args);
                case "world.pause": RequireWritableWorld(); return PauseWorld(args);
                case "grids.list": RequireWorld(); return ListGrids(args);
                case "grids.get": RequireWorld(); return DescribeGrid(Grid(Id(args, "entityId")));
                case "blocks.list": RequireWorld(); return ListBlocks(args);
                case "blocks.get": RequireWorld(); return DescribeBlock(Block(Id(args, "entityId")), true);
                case "blocks.actions": RequireWorld(); return ListActions(args);
                case "blocks.action": RequireWritableWorld(); return ApplyAction(args);
                case "blocks.properties": RequireWorld(); return ListProperties(args);
                case "blocks.setProperty": RequireWritableWorld(); return SetProperty(args);
                case "pb.read": RequireWorld(); return ReadProgram(args);
                case "pb.inspect": RequireWorld(); return InspectProgram(args);
                case "pb.deploy": RequireWritableWorld(); return DeployProgram(args);
                case "pb.run": RequireWritableWorld(); return RunProgram(args);
                case "cameras.scan": RequireWritableWorld(); return ScanCamera(args);
                case "telemetry.snapshot": RequireWorld(); return Telemetry(args);
                case "grid.stop": RequireWritableWorld(); return StopGrid(args);
                case "debug.screenshot": RequireWritableWorld(); return Screenshot();
                default: throw new ArgumentException("Unknown method: " + method);
            }
        }

        public void Update()
        {
            if (_disposed || _loadingPath == null) return;
            var session = MySession.Static;
            if (session != null && session.Ready && SamePath(session.CurrentPath, _loadingPath))
            {
                _log("Loaded authorized debug world: " + _loadingPath);
                _loadingPath = null;
            }
            else if (DateTime.UtcNow - _loadStarted > TimeSpan.FromMinutes(10))
            {
                _loadError = "World load did not complete within ten minutes; inspect the game loading screen/log.";
                _loadingPath = null;
            }
        }

        public void Dispose()
        {
            if (_pausedSession != null && ReferenceEquals(MySession.Static, _pausedSession)) Sandbox.MySandboxGame.PausePop();
            _pausedSession = null;
            _disposed = true;
        }

        private JObject WorldStatus()
        {
            var s = MySession.Static;
            var result = new JObject { ["loaded"] = s != null, ["loadingPath"] = _loadingPath, ["loadError"] = _loadError };
            if (s == null) return result;
            result["name"] = s.Name;
            result["path"] = s.CurrentPath;
            result["ready"] = s.Ready;
            result["isUnloading"] = s.IsUnloading;
            result["isServer"] = Sync.IsServer;
            result["onlineMode"] = s.OnlineMode.ToString();
            result["authorizedWorld"] = IsAuthorized(s.CurrentPath);
            result["canMutate"] = CanWrite(s);
            result["scriptsEnabled"] = s.EnableIngameScripts;
            result["saveInProgress"] = s.IsSaveInProgress;
            result["frame"] = s.GameplayFrameCounter;
            result["elapsedSeconds"] = s.ElapsedGameTime.TotalSeconds;
            result["simulationSpeed"] = Sandbox.Engine.Physics.MyPhysics.SimulationRatio;
            result["paused"] = Sandbox.MySandboxGame.IsPaused;
            result["pausedByBridge"] = ReferenceEquals(_pausedSession, s);
            return result;
        }

        private JObject LoadWorld(JObject args)
        {
            string path = Path.GetFullPath(Text(args, "path"));
            if (!IsAuthorized(path)) throw new InvalidOperationException("world.load accepts only an explicitly authorized local test-copy path.");
            if (!File.Exists(Path.Combine(path, "Sandbox.sbc"))) throw new FileNotFoundException("The authorized world has no Sandbox.sbc checkpoint.");
            if (_loadingPath != null) throw new InvalidOperationException("A world load is already pending.");
            var current = MySession.Static;
            if (current != null)
            {
                if (SamePath(current.CurrentPath, path) && current.Ready) return WorldStatus();
                // Loading a different world can unload unsaved state. Let the user/game return to the menu first.
                throw new InvalidOperationException("Return to the main menu before loading another test world; the bridge does not unload an active world.");
            }
            _loadError = null;
            _loadingPath = path;
            _loadStarted = DateTime.UtcNow;
            try
            {
                MySessionLoader.LoadSingleplayerSession(path, null, null, MyOnlineModeEnum.OFFLINE, 1, null, null);
            }
            catch { _loadingPath = null; throw; }
            return new JObject { ["accepted"] = true, ["path"] = path, ["state"] = "loading", ["pollMethod"] = "world.status" };
        }

        private JObject SaveWorld()
        {
            var s = RequireWorld();
            if (s.IsSaveInProgress) throw new InvalidOperationException("A save is already in progress.");
            bool saved = s.Save(null, null);
            return new JObject { ["saved"] = saved, ["path"] = s.CurrentPath, ["saveInProgress"] = s.IsSaveInProgress };
        }

        private JObject ExitWorld(JObject args)
        {
            if (args["save"]?.Type != JTokenType.Boolean || (bool)args["save"] != true)
                throw new ArgumentException("world.exit requires save:true; exiting without saving is not supported.");
            var session = RequireWorld();
            if (session.IsSaveInProgress) throw new InvalidOperationException("A save is already in progress; the game was not closed.");
            // In SE 1.210 MySession.Save calls snapshot.Save synchronously and returns its disk-save result.
            if (!session.Save(null, null)) throw new InvalidOperationException("The test world could not be saved; the game was not closed.");
            if (session.IsSaveInProgress) throw new InvalidOperationException("Saving has not finished; the game was not closed.");
            string path = session.CurrentPath;
            _log("Saved authorized debug world and requested normal game exit: " + path);
            Sandbox.MySandboxGame.ExitThreadSafe();
            return new JObject { ["saved"] = true, ["path"] = path, ["exitRequested"] = true };
        }

        private JObject PauseWorld(JObject args)
        {
            if (args["paused"]?.Type != JTokenType.Boolean) throw new ArgumentException("paused must be a boolean.");
            bool pause = (bool)args["paused"];
            var session = RequireWorld();
            if (!ReferenceEquals(_pausedSession, session)) _pausedSession = null;
            if (pause && _pausedSession == null) { Sandbox.MySandboxGame.PausePush(); _pausedSession = session; }
            else if (!pause && _pausedSession != null) { Sandbox.MySandboxGame.PausePop(); _pausedSession = null; }
            return new JObject { ["paused"] = Sandbox.MySandboxGame.IsPaused, ["pausedByBridge"] = _pausedSession != null,
                ["note"] = !pause && Sandbox.MySandboxGame.IsPaused ? "The bridge released its pause; another game/menu pause remains." : null };
        }

        private JObject Screenshot()
        {
            string directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "XFE", "SpaceEngineersAgent", "Screenshots");
            Directory.CreateDirectory(directory);
            string path = Path.Combine(directory, "se-" + DateTime.UtcNow.ToString("yyyyMMddTHHmmssfffffff", CultureInfo.InvariantCulture) + ".png");
            VRageRender.MyRenderProxy.TakeScreenshot(Vector2.One, path, false, false, false);
            return new JObject { ["accepted"] = true, ["path"] = path, ["note"] = "The render thread writes this file asynchronously." };
        }

        private JObject StopGrid(JObject args)
        {
            var grid = Grid(Id(args, "entityId"));
            int thrusters = 0, gyros = 0, programs = 0;
            foreach (var block in grid.GetFatBlocks())
            {
                if (block is Sandbox.ModAPI.Ingame.IMyThrust thrust) { thrust.ThrustOverridePercentage = 0; thrust.ThrustOverride = 0; thrusters++; }
                if (block is Sandbox.ModAPI.Ingame.IMyGyro gyro) { gyro.Yaw = 0; gyro.Pitch = 0; gyro.Roll = 0; gyro.GyroOverride = false; gyros++; }
                if ((bool?)args["disableProgrammableBlocks"] == true && block is Sandbox.ModAPI.Ingame.IMyProgrammableBlock pb) { pb.Enabled = false; programs++; }
            }
            if (grid.Physics != null) grid.Physics.ClearSpeed();
            var result = DescribeGrid(grid);
            result["clearedThrusters"] = thrusters; result["clearedGyros"] = gyros; result["disabledProgrammableBlocks"] = programs;
            result["note"] = "One-time stop of this grid; running controllers/scripts or connected grids can apply new forces.";
            return result;
        }

        private MySession RequireWorld()
        {
            var s = MySession.Static;
            if (s == null || !s.Ready || s.IsUnloading) throw new InvalidOperationException("No ready game world is loaded.");
            return s;
        }

        private void RequireWritableWorld()
        {
            var s = RequireWorld();
            if (!CanWrite(s)) throw new InvalidOperationException("Mutation is restricted to an explicitly authorized offline test world on the local server.");
        }

        private bool CanWrite(MySession s) { return s.Ready && !s.IsUnloading && Sync.IsServer && s.OnlineMode == MyOnlineModeEnum.OFFLINE && IsAuthorized(s.CurrentPath); }
        private bool IsAuthorized(string path) { return !string.IsNullOrWhiteSpace(path) && _authorizedWorld(Path.GetFullPath(path)); }
        private static bool SamePath(string a, string b) { return !string.IsNullOrEmpty(a) && !string.IsNullOrEmpty(b) && string.Equals(Path.GetFullPath(a).TrimEnd('\\', '/'), Path.GetFullPath(b).TrimEnd('\\', '/'), StringComparison.OrdinalIgnoreCase); }

        private static long Id(JObject args, string key)
        {
            long id;
            if (!long.TryParse((string)args[key], NumberStyles.Integer, CultureInfo.InvariantCulture, out id) || id == 0)
                throw new ArgumentException(key + " must be a nonzero decimal entity ID string.");
            return id;
        }
        private static string Text(JObject args, string key)
        {
            if (args[key] == null || args[key].Type != JTokenType.String) throw new ArgumentException(key + " must be a string.");
            return (string)args[key];
        }
        private static int Limit(JObject args, int fallback = 256, int max = 2048)
        { return Math.Max(1, Math.Min(max, (int?)args["limit"] ?? fallback)); }
        private static double Number(JObject args, string key, double fallback)
        {
            double value = (double?)args[key] ?? fallback;
            if (double.IsNaN(value) || double.IsInfinity(value)) throw new ArgumentException(key + " must be finite.");
            return value;
        }
        private static MyEntity Entity(long id)
        {
            MyEntity entity;
            if (!MyEntities.TryGetEntityById(id, out entity, false) || entity == null || entity.Closed)
                throw new KeyNotFoundException("Entity not found: " + id.ToString(CultureInfo.InvariantCulture));
            return entity;
        }
        private static MyCubeGrid Grid(long id) { return Entity(id) as MyCubeGrid ?? throw new ArgumentException("Entity is not a grid."); }
        private static Terminal Block(long id) { return Entity(id) as Terminal ?? throw new ArgumentException("Entity is not a terminal block."); }
        private static string Sid(long id) { return id.ToString(CultureInfo.InvariantCulture); }
        private static JObject Vec(Vector3D value) { return new JObject { ["x"] = value.X, ["y"] = value.Y, ["z"] = value.Z }; }
        private static JObject Vec(Vector3I value) { return new JObject { ["x"] = value.X, ["y"] = value.Y, ["z"] = value.Z }; }
        private static JObject Bounds(BoundingBoxD b) { return new JObject { ["min"] = Vec(b.Min), ["max"] = Vec(b.Max) }; }
        private static JObject Pose(MatrixD m) { return new JObject { ["position"] = Vec(m.Translation), ["forward"] = Vec(m.Forward), ["up"] = Vec(m.Up), ["right"] = Vec(m.Right) }; }
        private static string Sha(string value)
        {
            using (var hash = SHA256.Create()) return BitConverter.ToString(hash.ComputeHash(Encoding.UTF8.GetBytes(value ?? ""))).Replace("-", "").ToLowerInvariant();
        }
        private static string Clip(string value, int length = 4096) { return value == null || value.Length <= length ? value : value.Substring(0, length) + "…"; }
    }
}