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;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Sandbox.Game.Entities.Blocks;
using Sandbox.ModAPI.Interfaces;
using VRage.Scripting.MemorySafeTypes;
using VRageMath;
using Terminal = Sandbox.ModAPI.Ingame.IMyTerminalBlock;
using ProgramBlock = Sandbox.ModAPI.IMyProgrammableBlock;

namespace XFE.SeAgent.Plugin.Game
{
    public sealed partial class GameDebugApi
    {
        private JObject ListActions(JObject args)
        {
            var block = Block(Id(args, "entityId"));
            var actions = new List<ITerminalAction>();
            block.GetActions(actions, null);
            var array = new JArray();
            foreach (var action in actions.Take(Limit(args)))
            {
                var value = new StringBuilder();
                action.WriteValue(block, value);
                array.Add(new JObject { ["id"] = action.Id, ["name"] = action.Name.ToString(), ["enabled"] = action.IsEnabled(block), ["value"] = Clip(value.ToString()) });
            }
            return new JObject { ["entityId"] = Sid(block.EntityId), ["actions"] = array, ["total"] = actions.Count };
        }

        private JObject ApplyAction(JObject args)
        {
            var block = Block(Id(args, "entityId"));
            var action = block.GetActionWithName(Text(args, "actionId"));
            if (action == null) throw new ArgumentException("The block does not expose that terminal action.");
            if (!action.IsEnabled(block)) throw new InvalidOperationException("The requested terminal action is disabled.");
            action.Apply(block);
            _log("Terminal action " + action.Id + " on " + Sid(block.EntityId));
            return new JObject { ["applied"] = true, ["entityId"] = Sid(block.EntityId), ["actionId"] = action.Id };
        }

        private JObject ListProperties(JObject args)
        {
            var block = Block(Id(args, "entityId"));
            var properties = new List<ITerminalProperty>();
            block.GetProperties(properties, null);
            var array = new JArray();
            foreach (var property in properties.Take(Limit(args)))
            {
                var item = new JObject { ["id"] = property.Id, ["type"] = property.TypeName };
                try { item["value"] = PropertyValue(block, property); item["supported"] = SupportedProperty(property); }
                catch (Exception error) { item["error"] = error.Message; }
                array.Add(item);
            }
            return new JObject { ["entityId"] = Sid(block.EntityId), ["properties"] = array, ["total"] = properties.Count };
        }

        private static JToken PropertyValue(Terminal block, ITerminalProperty property)
        {
            if (property is ITerminalProperty<bool> b) return new JValue(b.GetValue(block));
            if (property is ITerminalProperty<float> f) return new JValue(f.GetValue(block));
            if (property is ITerminalProperty<double> d) return new JValue(d.GetValue(block));
            if (property is ITerminalProperty<int> i) return new JValue(i.GetValue(block));
            if (property is ITerminalProperty<long> l) return new JValue(Sid(l.GetValue(block)));
            if (property is ITerminalProperty<string> s) return new JValue(Clip(s.GetValue(block)));
            if (property is ITerminalProperty<StringBuilder> sb) return new JValue(Clip(sb.GetValue(block)?.ToString()));
            if (property is ITerminalProperty<Color> c) { var v = c.GetValue(block); return new JObject { ["r"] = v.R, ["g"] = v.G, ["b"] = v.B, ["a"] = v.A }; }
            return JValue.CreateNull();
        }

        private static bool SupportedProperty(ITerminalProperty property)
        {
            return property is ITerminalProperty<bool> || property is ITerminalProperty<float> || property is ITerminalProperty<double> ||
                property is ITerminalProperty<int> || property is ITerminalProperty<long> || property is ITerminalProperty<string> ||
                property is ITerminalProperty<StringBuilder> || property is ITerminalProperty<Color>;
        }

        private JObject SetProperty(JObject args)
        {
            var block = Block(Id(args, "entityId"));
            var property = block.GetProperty(Text(args, "propertyId"));
            if (property == null) throw new ArgumentException("The block does not expose that terminal property.");
            var value = args["value"] ?? throw new ArgumentException("value is required.");
            if (property is ITerminalProperty<bool> b)
            {
                if (value.Type != JTokenType.Boolean) throw new ArgumentException("value must be a JSON boolean.");
                b.SetValue(block, (bool)value);
            }
            else if (property is ITerminalProperty<float> f)
            {
                double n = Finite(value); if (n < f.GetMinimum(block) || n > f.GetMaximum(block)) throw new ArgumentOutOfRangeException("value");
                f.SetValue(block, (float)n);
            }
            else if (property is ITerminalProperty<double> d) { double n = Finite(value); if (n < d.GetMinimum(block) || n > d.GetMaximum(block)) throw new ArgumentOutOfRangeException("value"); d.SetValue(block, n); }
            else if (property is ITerminalProperty<int> i) { int n = value.Value<int>(); if (n < i.GetMinimum(block) || n > i.GetMaximum(block)) throw new ArgumentOutOfRangeException("value"); i.SetValue(block, n); }
            else if (property is ITerminalProperty<long> l) { long n = value.Value<long>(); if (n < l.GetMinimum(block) || n > l.GetMaximum(block)) throw new ArgumentOutOfRangeException("value"); l.SetValue(block, n); }
            else if (property is ITerminalProperty<string> s) { if (value.Type != JTokenType.String) throw new ArgumentException("value must be a string."); s.SetValue(block, (string)value); }
            else if (property is ITerminalProperty<StringBuilder> sb) { if (value.Type != JTokenType.String) throw new ArgumentException("value must be a string."); sb.SetValue(block, new StringBuilder((string)value)); }
            else if (property is ITerminalProperty<Color> c) { var color = value as JObject ?? throw new ArgumentException("Color requires {r,g,b,a} byte values."); c.SetValue(block, new Color(color.Value<byte>("r"), color.Value<byte>("g"), color.Value<byte>("b"), (byte?)color["a"] ?? (byte)255)); }
            else throw new NotSupportedException("Property type is read-only to this bridge: " + property.TypeName);
            _log("Set terminal property " + property.Id + " on " + Sid(block.EntityId));
            return new JObject { ["entityId"] = Sid(block.EntityId), ["propertyId"] = property.Id, ["value"] = PropertyValue(block, property) };
        }

        private static double Finite(JToken value)
        {
            if (value.Type != JTokenType.Integer && value.Type != JTokenType.Float) throw new ArgumentException("value must be a JSON number.");
            double n = value.Value<double>();
            if (double.IsNaN(n) || double.IsInfinity(n)) throw new ArgumentException("value must be finite.");
            return n;
        }

        private static ProgramBlock Pb(JObject args) { return Block(Id(args, "entityId")) as ProgramBlock ?? throw new ArgumentException("Entity is not a programmable block."); }
        // Deliberately fixed field names, never a caller-supplied reflection path.
        private static object PbField(ProgramBlock block, string field)
        { return typeof(MyProgrammableBlock).GetField(field, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(block); }

        private JObject ReadProgram(JObject args)
        {
            var pb = Pb(args);
            string source = pb.ProgramData ?? "";
            var result = DescribeBlock(pb, true);
            result["programData"] = source;
            result["source"] = source;
            result["sha256"] = Sha(source);
            result["customData"] = pb.CustomData ?? "";
            result["storage"] = PbField(pb, "m_storageData") as string ?? "";
            result["hasCompileErrors"] = pb.HasCompileErrors;
            result["compilerErrors"] = new JArray((PbField(pb, "m_compilerErrors") as IEnumerable<string> ?? Enumerable.Empty<string>()).Take(256));
            result["echo"] = Clip(PbField(pb, "m_echoOutput")?.ToString(), 32768);
            result["terminationReason"] = PbField(pb, "m_terminationReason")?.ToString();
            result["isRunning"] = PbField(pb, "m_isRunning") as bool? ?? false;
            var instance = PbField(pb, "m_instance") as Sandbox.ModAPI.IMyGridProgram;
            result["hasInstance"] = instance != null;
            if (instance != null)
            {
                result["liveStorage"] = instance.Storage;
                result["runtime"] = Runtime(instance.Runtime);
            }
            return result;
        }

        private static JObject Runtime(Sandbox.ModAPI.Ingame.IMyGridProgramRuntimeInfo runtime)
        {
            var result = new JObject();
            if (runtime == null) return result;
            result["lastRunTimeMs"] = runtime.LastRunTimeMs;
            result["timeSinceLastRunSeconds"] = runtime.TimeSinceLastRun.TotalSeconds;
            result["updateFrequency"] = runtime.UpdateFrequency.ToString();
            result["lifetimeTicks"] = runtime.LifetimeTicks;
            try { result["maxInstructionCount"] = runtime.MaxInstructionCount; result["maxCallChainDepth"] = runtime.MaxCallChainDepth; result["currentInstructionCount"] = runtime.CurrentInstructionCount; result["currentCallChainDepth"] = runtime.CurrentCallChainDepth; }
            catch (Exception error) { result["counterUnavailable"] = error.Message; }
            return result;
        }

        private JObject DeployProgram(JObject args)
        {
            var pb = Pb(args);
            string source = Text(args, "source");
            if (source.Length > 100000) throw new ArgumentException("A programmable block source may contain at most 100000 characters.");
            string expected = Text(args, "expectedSha256");
            string current = pb.ProgramData ?? "";
            if (!string.Equals(expected, Sha(current), StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Programmable block source changed; read it again before deploying (SHA-256 conflict).");
            if (!RequireWorld().EnableIngameScripts) throw new InvalidOperationException("Ingame scripts are disabled in this test world.");
            var instance = PbField(pb, "m_instance") as Sandbox.ModAPI.IMyGridProgram;
            string directory = Path.Combine(RequireWorld().CurrentPath, "Storage", "XFE.AgentBridge", "Backups");
            Directory.CreateDirectory(directory);
            string backup = Path.Combine(directory, Sid(pb.EntityId) + "-" + DateTime.UtcNow.ToString("yyyyMMddTHHmmssfffffff", CultureInfo.InvariantCulture) + "-" + Guid.NewGuid().ToString("N") + ".json");
            var snapshot = new JObject { ["formatVersion"] = 1, ["entityId"] = Sid(pb.EntityId), ["utc"] = DateTime.UtcNow, ["programData"] = current, ["sha256"] = Sha(current), ["customData"] = pb.CustomData ?? "", ["storage"] = PbField(pb, "m_storageData") as string ?? "", ["liveStorage"] = instance?.Storage };
            // CreateNew + durable flush ensures a complete backup exists before the setter compiles/runs the constructor.
            using (var stream = new FileStream(backup, FileMode.CreateNew, FileAccess.Write, FileShare.Read))
            {
                var bytes = Encoding.UTF8.GetBytes(snapshot.ToString(Formatting.Indented));
                stream.Write(bytes, 0, bytes.Length);
                stream.Flush(true);
            }
            pb.ProgramData = source; // The server setter synchronously Recompile(true); do not compile twice.
            var result = ReadProgram(args);
            result["backupPath"] = backup;
            result["deployed"] = true;
            result["compiled"] = !pb.HasCompileErrors && PbField(pb, "m_instance") != null;
            _log("Deployed PB " + Sid(pb.EntityId) + " SHA256=" + Sha(source) + " backup=" + backup);
            return result;
        }

        private JObject RunProgram(JObject args)
        {
            var pb = Pb(args);
            string argument = (string)args["argument"] ?? "";
            if (argument.Length > 16384) throw new ArgumentException("argument is too long.");
            bool ran = pb.TryRun(argument);
            var result = ReadProgram(args);
            result["ran"] = ran;
            return result;
        }

        private JObject InspectProgram(JObject args)
        {
            var pb = Pb(args);
            object instance = PbField(pb, "m_instance");
            if (instance == null) return new JObject { ["entityId"] = Sid(pb.EntityId), ["hasInstance"] = false };
            int depth = Math.Max(0, Math.Min(3, (int?)args["depth"] ?? 2));
            int budget = 600;
            var visited = new HashSet<object>(ReferenceComparer.Instance);
            HashSet<string> selected = null;
            if (args["fields"] != null)
            {
                var fields = args["fields"] as JArray ?? throw new ArgumentException("fields must be an array of root script field names.");
                if (fields.Count > 32 || fields.Any(f => f.Type != JTokenType.String)) throw new ArgumentException("fields accepts at most 32 root script field names.");
                selected = new HashSet<string>(fields.Values<string>(), StringComparer.Ordinal);
                var available = new HashSet<string>(instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                    .Where(f => f.DeclaringType.Assembly == instance.GetType().Assembly).Select(f => f.Name), StringComparer.Ordinal);
                if (selected.Any(name => !available.Contains(name))) throw new ArgumentException("Unknown root script field: " + string.Join(", ", selected.Where(name => !available.Contains(name))));
            }
            return new JObject { ["entityId"] = Sid(pb.EntityId), ["hasInstance"] = true, ["fields"] = Inspect(instance, instance.GetType().Assembly, depth, ref budget, visited, selected), ["budgetRemaining"] = budget };
        }

        private static JToken Inspect(object value, Assembly script, int depth, ref int budget, HashSet<object> visited, HashSet<string> rootFields = null)
        {
            if (value == null) return JValue.CreateNull();
            if (--budget < 0) return new JValue("<budget exhausted>");
            var type = value.GetType();
            if (value is string text) return new JValue(Clip(text));
            if (value is StringBuilder sb) return new JValue(Clip(sb.ToString()));
            if (type == typeof(MemorySafeStringBuilder))
            {
                var builder = (MemorySafeStringBuilder)value;
                int length = builder.Length;
                return new JValue(builder.ToString(0, Math.Min(4096, length)) + (length > 4096 ? "…" : ""));
            }
            if (type.IsEnum) return new JValue(value.ToString());
            if (type.IsPrimitive || value is decimal) return JToken.FromObject(value);
            if (value is Vector3D vd) return Vec(vd);
            if (value is Vector3 vf) return Vec((Vector3D)vf);
            if (value is Vector3I vi) return Vec(vi);
            if (value is MatrixD matrix) return Pose(matrix);
            if (value is DateTime date) return new JValue(date);
            if (value is TimeSpan time) return new JValue(time.TotalSeconds);
            // Never traverse game entities, delegates, runtime handles or arbitrary framework object graphs.
            bool collection = IsInspectableCollection(type);
            if (type.Assembly != script && !collection) return new JValue("<" + type.FullName + ">");
            if (depth < 0) return new JValue("<depth limit: " + type.Name + ">");
            if (!type.IsValueType && !visited.Add(value)) return new JValue("<reference>");
            if (collection && value is IDictionary dictionary)
            {
                var entries = new JArray(); int count = 0;
                foreach (DictionaryEntry entry in dictionary)
                {
                    if (++count > 64 || budget <= 0) { entries.Add("<truncated>"); break; }
                    // Collection wrappers are transparent to object-field depth. Item/count and global budgets still apply.
                    entries.Add(new JObject { ["key"] = Inspect(entry.Key, script, depth, ref budget, visited), ["value"] = Inspect(entry.Value, script, depth, ref budget, visited) });
                }
                return entries;
            }
            if (collection && value is IEnumerable sequence)
            {
                var entries = new JArray(); int count = 0;
                foreach (var entry in sequence) { if (++count > 64 || budget <= 0) { entries.Add("<truncated>"); break; } entries.Add(Inspect(entry, script, depth, ref budget, visited)); }
                return entries;
            }
            var result = new JObject();
            foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(f => rootFields == null || rootFields.Contains(f.Name)).Take(128))
            {
                if (budget <= 0) { result["$truncated"] = true; break; }
                if (field.DeclaringType.Assembly != script) continue;
                try { result[field.Name] = Inspect(field.GetValue(value), script, depth - 1, ref budget, visited); }
                catch (Exception error) { result[field.Name] = "<unavailable: " + error.GetType().Name + ">"; }
            }
            return result;
        }

        private static bool IsInspectableCollection(Type type)
        {
            if (type.IsArray) return true;
            if (!type.IsGenericType) return false;
            var definition = type.GetGenericTypeDefinition();
            // Exact runtime type identity excludes custom subclasses and lookalikes from other assemblies.
            return definition == typeof(List<>) || definition == typeof(Dictionary<,>) || definition == typeof(HashSet<>) ||
                definition == typeof(Queue<>) || definition == typeof(Stack<>) || definition == typeof(MemorySafeList<>) ||
                definition == typeof(MemorySafeDictionary<,>) || definition == typeof(MemorySafeHashSet<>) ||
                definition == typeof(MemorySafeQueue<>) || definition == typeof(MemorySafeStack<>);
        }

        private sealed class ReferenceComparer : IEqualityComparer<object>
        {
            public static readonly ReferenceComparer Instance = new ReferenceComparer();
            public new bool Equals(object x, object y) { return ReferenceEquals(x, y); }
            public int GetHashCode(object obj) { return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); }
        }

        private JObject ScanCamera(JObject args)
        {
            var camera = Block(Id(args, "entityId")) as Sandbox.ModAPI.Ingame.IMyCameraBlock ?? throw new ArgumentException("Entity is not a camera.");
            if ((bool?)args["enableRaycast"] == true) camera.EnableRaycast = true;
            if (!camera.EnableRaycast) throw new InvalidOperationException("Raycast is disabled; request enableRaycast:true explicitly.");
            double distance = Number(args, "distance", 100);
            if (distance <= 0 || distance > 1000000) throw new ArgumentOutOfRangeException("distance", "Scan distance must be within (0, 1000000] metres.");
            float pitch = (float)Number(args, "pitch", 0), yaw = (float)Number(args, "yaw", 0);
            if (Math.Abs(pitch) > camera.RaycastConeLimit || Math.Abs(yaw) > camera.RaycastConeLimit) throw new ArgumentOutOfRangeException("pitch/yaw", "Scan angles exceed the camera cone.");
            if (camera.RaycastDistanceLimit >= 0 && distance > camera.RaycastDistanceLimit) throw new ArgumentOutOfRangeException("distance", "Scan exceeds this camera's maximum distance.");
            if (!camera.CanScan(distance)) return new JObject { ["scanned"] = false, ["reason"] = "insufficientCharge", ["availableScanRange"] = camera.AvailableScanRange, ["timeUntilScanMs"] = camera.TimeUntilScan(distance) };
            var hit = camera.Raycast(distance, pitch, yaw);
            return new JObject { ["scanned"] = true, ["cameraId"] = Sid(camera.EntityId), ["availableScanRange"] = camera.AvailableScanRange, ["empty"] = hit.IsEmpty(),
                ["entityId"] = Sid(hit.EntityId), ["name"] = hit.Name, ["type"] = hit.Type.ToString(), ["position"] = Vec(hit.Position),
                ["hitPosition"] = hit.HitPosition.HasValue ? Vec(hit.HitPosition.Value) : null, ["velocity"] = Vec(hit.Velocity), ["bounds"] = Bounds(hit.BoundingBox),
                ["relationship"] = hit.Relationship.ToString(), ["timestamp"] = hit.TimeStamp };
        }
    }
}