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) + "…"; }
}
}
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) + "…"; }
}
}