938 lines
33 KiB
C#
938 lines
33 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
namespace UniRun.EditorTools
|
|
{
|
|
[InitializeOnLoad]
|
|
internal static class UnityCodexBridgeServer
|
|
{
|
|
private const int PreferredPort = 19744;
|
|
private const int MaxPortAttempts = 5;
|
|
private const string AutoStartPrefKey = "UniRun.CodexBridge.AutoStart";
|
|
private const int MaxLogs = 250;
|
|
|
|
private static readonly ConcurrentQueue<BridgeJob> Jobs = new ConcurrentQueue<BridgeJob>();
|
|
private static readonly List<LogEntry> Logs = new List<LogEntry>();
|
|
private static readonly object LogLock = new object();
|
|
|
|
private static TcpListener _listener;
|
|
private static Thread _serverThread;
|
|
private static bool _running;
|
|
private static int _port = PreferredPort;
|
|
private static int _logIndex;
|
|
|
|
static UnityCodexBridgeServer()
|
|
{
|
|
if (IsBackgroundEditorProcess())
|
|
return;
|
|
|
|
Application.logMessageReceived -= OnLogMessageReceived;
|
|
Application.logMessageReceived += OnLogMessageReceived;
|
|
|
|
EditorApplication.update -= ProcessJobs;
|
|
EditorApplication.update += ProcessJobs;
|
|
|
|
EditorApplication.quitting -= StopServer;
|
|
EditorApplication.quitting += StopServer;
|
|
|
|
AssemblyReloadEvents.beforeAssemblyReload -= StopServer;
|
|
AssemblyReloadEvents.beforeAssemblyReload += StopServer;
|
|
|
|
if (EditorPrefs.GetBool(AutoStartPrefKey, true))
|
|
StartServer();
|
|
}
|
|
|
|
[MenuItem("Tools/Codex Bridge/Start Server")]
|
|
private static void StartServerMenu()
|
|
{
|
|
StartServer();
|
|
}
|
|
|
|
[MenuItem("Tools/Codex Bridge/Stop Server")]
|
|
private static void StopServerMenu()
|
|
{
|
|
StopServer();
|
|
}
|
|
|
|
[MenuItem("Tools/Codex Bridge/Auto Start")]
|
|
private static void ToggleAutoStart()
|
|
{
|
|
bool enabled = !EditorPrefs.GetBool(AutoStartPrefKey, true);
|
|
EditorPrefs.SetBool(AutoStartPrefKey, enabled);
|
|
|
|
if (enabled)
|
|
StartServer();
|
|
}
|
|
|
|
[MenuItem("Tools/Codex Bridge/Auto Start", true)]
|
|
private static bool ValidateToggleAutoStart()
|
|
{
|
|
Menu.SetChecked("Tools/Codex Bridge/Auto Start", EditorPrefs.GetBool(AutoStartPrefKey, true));
|
|
return true;
|
|
}
|
|
|
|
[MenuItem("Tools/Codex Bridge/Capture Game View")]
|
|
private static void CaptureGameViewMenu()
|
|
{
|
|
BridgeResponse response = CaptureGameView(new Dictionary<string, string>());
|
|
Debug.Log("[CodexBridge] Capture result: " + response.Body);
|
|
}
|
|
|
|
private static void StartServer()
|
|
{
|
|
if (IsBackgroundEditorProcess())
|
|
return;
|
|
|
|
if (_running)
|
|
return;
|
|
|
|
try
|
|
{
|
|
_listener = CreateListener();
|
|
_running = true;
|
|
|
|
_serverThread = new Thread(ServerLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = "UnityCodexBridgeServer"
|
|
};
|
|
_serverThread.Start();
|
|
|
|
Debug.Log("[CodexBridge] Listening on http://127.0.0.1:" + _port);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_running = false;
|
|
try
|
|
{
|
|
if (_listener != null)
|
|
_listener.Stop();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore cleanup failures after a failed bind.
|
|
}
|
|
finally
|
|
{
|
|
_listener = null;
|
|
}
|
|
|
|
Debug.LogWarning("[CodexBridge] Failed to start: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static void StopServer()
|
|
{
|
|
_running = false;
|
|
|
|
try
|
|
{
|
|
if (_listener != null)
|
|
_listener.Stop();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore shutdown races.
|
|
}
|
|
|
|
_listener = null;
|
|
|
|
if (_serverThread != null && _serverThread.IsAlive)
|
|
_serverThread.Join(200);
|
|
|
|
_serverThread = null;
|
|
}
|
|
|
|
private static TcpListener CreateListener()
|
|
{
|
|
Exception lastException = null;
|
|
|
|
for (int i = 0; i < MaxPortAttempts; i++)
|
|
{
|
|
int port = PreferredPort + i;
|
|
TcpListener listener = null;
|
|
|
|
try
|
|
{
|
|
listener = new TcpListener(IPAddress.Loopback, port);
|
|
listener.Server.ExclusiveAddressUse = true;
|
|
listener.Start();
|
|
_port = port;
|
|
return listener;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lastException = ex;
|
|
|
|
try
|
|
{
|
|
if (listener != null)
|
|
listener.Stop();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore cleanup failures while trying fallback ports.
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastException ?? new SocketException();
|
|
}
|
|
|
|
private static bool IsBackgroundEditorProcess()
|
|
{
|
|
string commandLine = Environment.CommandLine;
|
|
|
|
return Application.isBatchMode ||
|
|
commandLine.IndexOf("AssetImportWorker", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
|
commandLine.IndexOf("-batchMode", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
|
|
private static void ServerLoop()
|
|
{
|
|
while (_running)
|
|
{
|
|
try
|
|
{
|
|
TcpClient client = _listener.AcceptTcpClient();
|
|
ThreadPool.QueueUserWorkItem(_ => HandleClient(client));
|
|
}
|
|
catch
|
|
{
|
|
if (_running)
|
|
Thread.Sleep(100);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void HandleClient(TcpClient client)
|
|
{
|
|
using (client)
|
|
{
|
|
try
|
|
{
|
|
client.ReceiveTimeout = 5000;
|
|
client.SendTimeout = 5000;
|
|
|
|
BridgeRequest request = ReadRequest(client.GetStream());
|
|
BridgeResponse response;
|
|
|
|
if (request == null)
|
|
{
|
|
response = BridgeResponse.Json(400, "{\"ok\":false,\"error\":\"invalid_request\"}");
|
|
}
|
|
else if (request.Method == "OPTIONS")
|
|
{
|
|
response = BridgeResponse.Json(204, string.Empty);
|
|
}
|
|
else
|
|
{
|
|
BridgeJob job = new BridgeJob(request);
|
|
Jobs.Enqueue(job);
|
|
|
|
if (!job.Done.Wait(TimeSpan.FromSeconds(10)))
|
|
response = BridgeResponse.Json(504, "{\"ok\":false,\"error\":\"unity_main_thread_timeout\"}");
|
|
else
|
|
response = job.Response;
|
|
}
|
|
|
|
WriteResponse(client.GetStream(), response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try
|
|
{
|
|
WriteResponse(client.GetStream(),
|
|
BridgeResponse.Json(500, "{\"ok\":false,\"error\":" + JsonString(ex.Message) + "}"));
|
|
}
|
|
catch
|
|
{
|
|
// Client has already gone away.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static BridgeRequest ReadRequest(Stream stream)
|
|
{
|
|
StreamReader reader = new StreamReader(stream, Encoding.UTF8, false, 4096, true);
|
|
string requestLine = reader.ReadLine();
|
|
if (string.IsNullOrEmpty(requestLine))
|
|
return null;
|
|
|
|
string[] requestParts = requestLine.Split(' ');
|
|
if (requestParts.Length < 2)
|
|
return null;
|
|
|
|
int contentLength = 0;
|
|
string line;
|
|
while (!string.IsNullOrEmpty(line = reader.ReadLine()))
|
|
{
|
|
int separatorIndex = line.IndexOf(':');
|
|
if (separatorIndex <= 0)
|
|
continue;
|
|
|
|
string headerName = line.Substring(0, separatorIndex).Trim();
|
|
string headerValue = line.Substring(separatorIndex + 1).Trim();
|
|
if (string.Equals(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase))
|
|
int.TryParse(headerValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentLength);
|
|
}
|
|
|
|
string body = string.Empty;
|
|
if (contentLength > 0)
|
|
{
|
|
char[] buffer = new char[contentLength];
|
|
int read = reader.ReadBlock(buffer, 0, contentLength);
|
|
body = new string(buffer, 0, read);
|
|
}
|
|
|
|
Uri uri = new Uri("http://127.0.0.1" + requestParts[1]);
|
|
return new BridgeRequest(requestParts[0].ToUpperInvariant(), uri.AbsolutePath, ParseQuery(uri.Query), body);
|
|
}
|
|
|
|
private static Dictionary<string, string> ParseQuery(string query)
|
|
{
|
|
Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
if (string.IsNullOrEmpty(query))
|
|
return values;
|
|
|
|
string trimmed = query[0] == '?' ? query.Substring(1) : query;
|
|
string[] pairs = trimmed.Split('&');
|
|
|
|
foreach (string pair in pairs)
|
|
{
|
|
if (string.IsNullOrEmpty(pair))
|
|
continue;
|
|
|
|
int separatorIndex = pair.IndexOf('=');
|
|
string key = separatorIndex >= 0 ? pair.Substring(0, separatorIndex) : pair;
|
|
string value = separatorIndex >= 0 ? pair.Substring(separatorIndex + 1) : string.Empty;
|
|
values[Uri.UnescapeDataString(key)] = Uri.UnescapeDataString(value.Replace("+", " "));
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static void WriteResponse(Stream stream, BridgeResponse response)
|
|
{
|
|
if (response == null)
|
|
response = BridgeResponse.Json(500, "{\"ok\":false,\"error\":\"null_response\"}");
|
|
|
|
byte[] body = Encoding.UTF8.GetBytes(response.Body ?? string.Empty);
|
|
string headers =
|
|
"HTTP/1.1 " + response.StatusCode + " " + StatusText(response.StatusCode) + "\r\n" +
|
|
"Content-Type: " + response.ContentType + "; charset=utf-8\r\n" +
|
|
"Content-Length: " + body.Length.ToString(CultureInfo.InvariantCulture) + "\r\n" +
|
|
"Access-Control-Allow-Origin: http://localhost\r\n" +
|
|
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" +
|
|
"Access-Control-Allow-Headers: Content-Type\r\n" +
|
|
"Connection: close\r\n\r\n";
|
|
|
|
byte[] headerBytes = Encoding.UTF8.GetBytes(headers);
|
|
stream.Write(headerBytes, 0, headerBytes.Length);
|
|
if (body.Length > 0)
|
|
stream.Write(body, 0, body.Length);
|
|
}
|
|
|
|
private static string StatusText(int statusCode)
|
|
{
|
|
switch (statusCode)
|
|
{
|
|
case 200: return "OK";
|
|
case 204: return "No Content";
|
|
case 400: return "Bad Request";
|
|
case 404: return "Not Found";
|
|
case 405: return "Method Not Allowed";
|
|
case 500: return "Internal Server Error";
|
|
case 504: return "Gateway Timeout";
|
|
default: return "OK";
|
|
}
|
|
}
|
|
|
|
private static void ProcessJobs()
|
|
{
|
|
while (Jobs.TryDequeue(out BridgeJob job))
|
|
{
|
|
try
|
|
{
|
|
job.Response = Execute(job.Request);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
job.Response = BridgeResponse.Json(500,
|
|
"{\"ok\":false,\"error\":" + JsonString(ex.Message) + "}");
|
|
}
|
|
finally
|
|
{
|
|
job.Done.Set();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static BridgeResponse Execute(BridgeRequest request)
|
|
{
|
|
switch (request.Path)
|
|
{
|
|
case "/health":
|
|
case "/state":
|
|
return GetHealth();
|
|
case "/capture":
|
|
return CaptureGameView(request.Query);
|
|
case "/logs":
|
|
return GetLogs(request.Query);
|
|
case "/scene/roots":
|
|
return GetSceneRoots();
|
|
case "/scene/objects":
|
|
return GetSceneObjects(request.Query);
|
|
case "/object":
|
|
return GetObjectDetails(request.Query);
|
|
case "/play":
|
|
return SetPlayState(true, false);
|
|
case "/pause":
|
|
return SetPlayState(true, true);
|
|
case "/stop":
|
|
return SetPlayState(false, false);
|
|
case "/transform":
|
|
if (request.Method != "POST")
|
|
return BridgeResponse.Json(405, "{\"ok\":false,\"error\":\"method_not_allowed\"}");
|
|
return SetTransform(request.Body);
|
|
default:
|
|
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"not_found\"}");
|
|
}
|
|
}
|
|
|
|
private static BridgeResponse GetHealth()
|
|
{
|
|
Scene scene = SceneManager.GetActiveScene();
|
|
string body =
|
|
"{\"ok\":true" +
|
|
",\"bridge\":\"unity-codex-bridge\"" +
|
|
",\"port\":" + _port.ToString(CultureInfo.InvariantCulture) +
|
|
",\"unityVersion\":" + JsonString(Application.unityVersion) +
|
|
",\"projectPath\":" + JsonString(Directory.GetCurrentDirectory()) +
|
|
",\"scene\":" + JsonString(scene.IsValid() ? scene.name : string.Empty) +
|
|
",\"isPlaying\":" + Bool(EditorApplication.isPlaying) +
|
|
",\"isPaused\":" + Bool(EditorApplication.isPaused) +
|
|
"}";
|
|
|
|
return BridgeResponse.Json(200, body);
|
|
}
|
|
|
|
private static BridgeResponse CaptureGameView(Dictionary<string, string> query)
|
|
{
|
|
int width = GetInt(query, "width", 1280, 320, 4096);
|
|
int height = GetInt(query, "height", 720, 180, 4096);
|
|
string relativePath = "Captures/latest.png";
|
|
string absolutePath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath));
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(absolutePath));
|
|
|
|
Camera camera = ResolveCaptureCamera();
|
|
string cameraName = camera != null ? camera.name : string.Empty;
|
|
bool usedCameraRender = false;
|
|
|
|
if (camera != null)
|
|
{
|
|
RenderTexture previousTarget = camera.targetTexture;
|
|
RenderTexture previousActive = RenderTexture.active;
|
|
RenderTexture renderTexture = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
|
|
Texture2D texture = null;
|
|
|
|
try
|
|
{
|
|
camera.targetTexture = renderTexture;
|
|
RenderTexture.active = renderTexture;
|
|
camera.Render();
|
|
|
|
texture = new Texture2D(width, height, TextureFormat.RGB24, false);
|
|
texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
|
texture.Apply();
|
|
|
|
File.WriteAllBytes(absolutePath, texture.EncodeToPNG());
|
|
usedCameraRender = true;
|
|
}
|
|
finally
|
|
{
|
|
camera.targetTexture = previousTarget;
|
|
RenderTexture.active = previousActive;
|
|
RenderTexture.ReleaseTemporary(renderTexture);
|
|
if (texture != null)
|
|
UnityEngine.Object.DestroyImmediate(texture);
|
|
}
|
|
}
|
|
|
|
if (!usedCameraRender)
|
|
ScreenCapture.CaptureScreenshot(relativePath);
|
|
|
|
string body =
|
|
"{\"ok\":true" +
|
|
",\"path\":" + JsonString(absolutePath) +
|
|
",\"relativePath\":" + JsonString(relativePath) +
|
|
",\"width\":" + width.ToString(CultureInfo.InvariantCulture) +
|
|
",\"height\":" + height.ToString(CultureInfo.InvariantCulture) +
|
|
",\"camera\":" + JsonString(cameraName) +
|
|
",\"mode\":" + JsonString(usedCameraRender ? "camera_render" : "screen_capture") +
|
|
"}";
|
|
|
|
return BridgeResponse.Json(200, body);
|
|
}
|
|
|
|
private static Camera ResolveCaptureCamera()
|
|
{
|
|
Camera main = Camera.main;
|
|
if (main != null && main.isActiveAndEnabled)
|
|
return main;
|
|
|
|
Camera[] cameras = UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
|
foreach (Camera camera in cameras)
|
|
{
|
|
if (camera != null && camera.isActiveAndEnabled)
|
|
return camera;
|
|
}
|
|
|
|
return main != null ? main : (cameras.Length > 0 ? cameras[0] : null);
|
|
}
|
|
|
|
private static BridgeResponse GetLogs(Dictionary<string, string> query)
|
|
{
|
|
int count = GetInt(query, "count", 80, 1, MaxLogs);
|
|
List<LogEntry> snapshot;
|
|
|
|
lock (LogLock)
|
|
{
|
|
int start = Mathf.Max(0, Logs.Count - count);
|
|
snapshot = Logs.GetRange(start, Logs.Count - start);
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append("{\"ok\":true,\"logs\":[");
|
|
|
|
for (int i = 0; i < snapshot.Count; i++)
|
|
{
|
|
if (i > 0)
|
|
sb.Append(',');
|
|
|
|
LogEntry entry = snapshot[i];
|
|
sb.Append('{');
|
|
sb.Append("\"index\":").Append(entry.Index.ToString(CultureInfo.InvariantCulture)).Append(',');
|
|
sb.Append("\"time\":").Append(JsonString(entry.Time)).Append(',');
|
|
sb.Append("\"type\":").Append(JsonString(entry.Type)).Append(',');
|
|
sb.Append("\"message\":").Append(JsonString(entry.Message)).Append(',');
|
|
sb.Append("\"stackTrace\":").Append(JsonString(entry.StackTrace));
|
|
sb.Append('}');
|
|
}
|
|
|
|
sb.Append("]}");
|
|
return BridgeResponse.Json(200, sb.ToString());
|
|
}
|
|
|
|
private static BridgeResponse GetSceneRoots()
|
|
{
|
|
Scene scene = SceneManager.GetActiveScene();
|
|
GameObject[] roots = scene.IsValid() ? scene.GetRootGameObjects() : Array.Empty<GameObject>();
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append("{\"ok\":true,\"scene\":").Append(JsonString(scene.IsValid() ? scene.name : string.Empty));
|
|
sb.Append(",\"roots\":[");
|
|
|
|
for (int i = 0; i < roots.Length; i++)
|
|
{
|
|
if (i > 0)
|
|
sb.Append(',');
|
|
|
|
AppendGameObjectSummary(sb, roots[i]);
|
|
}
|
|
|
|
sb.Append("]}");
|
|
return BridgeResponse.Json(200, sb.ToString());
|
|
}
|
|
|
|
private static BridgeResponse GetSceneObjects(Dictionary<string, string> query)
|
|
{
|
|
string filter = GetString(query, "query", string.Empty);
|
|
int limit = GetInt(query, "limit", 120, 1, 500);
|
|
string lowerFilter = filter.ToLowerInvariant();
|
|
|
|
Transform[] transforms = UnityEngine.Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append("{\"ok\":true,\"objects\":[");
|
|
|
|
int written = 0;
|
|
foreach (Transform transform in transforms)
|
|
{
|
|
if (transform == null || transform.gameObject == null)
|
|
continue;
|
|
if (EditorUtility.IsPersistent(transform.gameObject))
|
|
continue;
|
|
if (!transform.gameObject.scene.IsValid())
|
|
continue;
|
|
|
|
string path = GetHierarchyPath(transform.gameObject);
|
|
if (!string.IsNullOrEmpty(lowerFilter) &&
|
|
path.ToLowerInvariant().IndexOf(lowerFilter, StringComparison.Ordinal) < 0)
|
|
continue;
|
|
|
|
if (written > 0)
|
|
sb.Append(',');
|
|
|
|
AppendGameObjectSummary(sb, transform.gameObject);
|
|
written++;
|
|
|
|
if (written >= limit)
|
|
break;
|
|
}
|
|
|
|
sb.Append("],\"count\":").Append(written.ToString(CultureInfo.InvariantCulture)).Append('}');
|
|
return BridgeResponse.Json(200, sb.ToString());
|
|
}
|
|
|
|
private static BridgeResponse GetObjectDetails(Dictionary<string, string> query)
|
|
{
|
|
string path = GetString(query, "path", string.Empty);
|
|
GameObject go = FindGameObjectByPath(path);
|
|
|
|
if (go == null)
|
|
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"object_not_found\",\"path\":" + JsonString(path) + "}");
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append("{\"ok\":true,\"object\":");
|
|
AppendGameObjectDetails(sb, go);
|
|
sb.Append('}');
|
|
|
|
return BridgeResponse.Json(200, sb.ToString());
|
|
}
|
|
|
|
private static BridgeResponse SetPlayState(bool play, bool pause)
|
|
{
|
|
EditorApplication.isPlaying = play;
|
|
EditorApplication.isPaused = pause;
|
|
return GetHealth();
|
|
}
|
|
|
|
private static BridgeResponse SetTransform(string body)
|
|
{
|
|
string path = ExtractString(body, "path");
|
|
if (string.IsNullOrEmpty(path))
|
|
return BridgeResponse.Json(400, "{\"ok\":false,\"error\":\"missing_path\"}");
|
|
|
|
GameObject go = FindGameObjectByPath(path);
|
|
if (go == null)
|
|
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"object_not_found\",\"path\":" + JsonString(path) + "}");
|
|
|
|
Undo.RecordObject(go.transform, "Codex Bridge Set Transform");
|
|
|
|
if (TryExtractVector3(body, "position", out Vector3 position))
|
|
go.transform.position = position;
|
|
if (TryExtractVector3(body, "localPosition", out Vector3 localPosition))
|
|
go.transform.localPosition = localPosition;
|
|
if (TryExtractVector3(body, "rotationEuler", out Vector3 rotationEuler))
|
|
go.transform.eulerAngles = rotationEuler;
|
|
if (TryExtractVector3(body, "localRotationEuler", out Vector3 localRotationEuler))
|
|
go.transform.localEulerAngles = localRotationEuler;
|
|
if (TryExtractVector3(body, "scale", out Vector3 scale))
|
|
go.transform.localScale = scale;
|
|
|
|
EditorUtility.SetDirty(go.transform);
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append("{\"ok\":true,\"object\":");
|
|
AppendGameObjectDetails(sb, go);
|
|
sb.Append('}');
|
|
|
|
return BridgeResponse.Json(200, sb.ToString());
|
|
}
|
|
|
|
private static void AppendGameObjectSummary(StringBuilder sb, GameObject go)
|
|
{
|
|
sb.Append('{');
|
|
sb.Append("\"name\":").Append(JsonString(go.name)).Append(',');
|
|
sb.Append("\"path\":").Append(JsonString(GetHierarchyPath(go))).Append(',');
|
|
sb.Append("\"activeSelf\":").Append(Bool(go.activeSelf)).Append(',');
|
|
sb.Append("\"activeInHierarchy\":").Append(Bool(go.activeInHierarchy)).Append(',');
|
|
sb.Append("\"scene\":").Append(JsonString(go.scene.IsValid() ? go.scene.name : string.Empty)).Append(',');
|
|
sb.Append("\"position\":").Append(VectorJson(go.transform.position)).Append(',');
|
|
sb.Append("\"rotationEuler\":").Append(VectorJson(go.transform.eulerAngles)).Append(',');
|
|
sb.Append("\"scale\":").Append(VectorJson(go.transform.localScale));
|
|
sb.Append('}');
|
|
}
|
|
|
|
private static void AppendGameObjectDetails(StringBuilder sb, GameObject go)
|
|
{
|
|
AppendGameObjectSummary(sb, go);
|
|
sb.Length -= 1;
|
|
|
|
Component[] components = go.GetComponents<Component>();
|
|
sb.Append(",\"components\":[");
|
|
for (int i = 0; i < components.Length; i++)
|
|
{
|
|
if (i > 0)
|
|
sb.Append(',');
|
|
|
|
Component component = components[i];
|
|
sb.Append(JsonString(component != null ? component.GetType().FullName : "Missing Script"));
|
|
}
|
|
sb.Append("]}");
|
|
}
|
|
|
|
private static GameObject FindGameObjectByPath(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
return null;
|
|
|
|
string[] parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length == 0)
|
|
return null;
|
|
|
|
Scene scene = SceneManager.GetActiveScene();
|
|
if (!scene.IsValid())
|
|
return null;
|
|
|
|
foreach (GameObject root in scene.GetRootGameObjects())
|
|
{
|
|
if (root.name != parts[0])
|
|
continue;
|
|
|
|
Transform current = root.transform;
|
|
for (int i = 1; i < parts.Length && current != null; i++)
|
|
{
|
|
current = FindChildByName(current, parts[i]);
|
|
}
|
|
|
|
if (current != null)
|
|
return current.gameObject;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static Transform FindChildByName(Transform parent, string childName)
|
|
{
|
|
for (int i = 0; i < parent.childCount; i++)
|
|
{
|
|
Transform child = parent.GetChild(i);
|
|
if (child.name == childName)
|
|
return child;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string GetHierarchyPath(GameObject go)
|
|
{
|
|
Stack<string> parts = new Stack<string>();
|
|
Transform current = go.transform;
|
|
|
|
while (current != null)
|
|
{
|
|
parts.Push(current.name);
|
|
current = current.parent;
|
|
}
|
|
|
|
return string.Join("/", parts.ToArray());
|
|
}
|
|
|
|
private static void OnLogMessageReceived(string condition, string stackTrace, LogType type)
|
|
{
|
|
lock (LogLock)
|
|
{
|
|
Logs.Add(new LogEntry
|
|
{
|
|
Index = ++_logIndex,
|
|
Time = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
|
|
Type = type.ToString(),
|
|
Message = condition ?? string.Empty,
|
|
StackTrace = stackTrace ?? string.Empty
|
|
});
|
|
|
|
if (Logs.Count > MaxLogs)
|
|
Logs.RemoveAt(0);
|
|
}
|
|
}
|
|
|
|
private static int GetInt(Dictionary<string, string> query, string key, int fallback, int min, int max)
|
|
{
|
|
if (!query.TryGetValue(key, out string value) ||
|
|
!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
|
|
return fallback;
|
|
|
|
return Mathf.Clamp(parsed, min, max);
|
|
}
|
|
|
|
private static string GetString(Dictionary<string, string> query, string key, string fallback)
|
|
{
|
|
return query.TryGetValue(key, out string value) ? value : fallback;
|
|
}
|
|
|
|
private static string ExtractString(string json, string key)
|
|
{
|
|
if (string.IsNullOrEmpty(json))
|
|
return string.Empty;
|
|
|
|
Match match = Regex.Match(json,
|
|
"\"" + Regex.Escape(key) + "\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\"",
|
|
RegexOptions.CultureInvariant);
|
|
|
|
return match.Success ? Regex.Unescape(match.Groups[1].Value) : string.Empty;
|
|
}
|
|
|
|
private static bool TryExtractVector3(string json, string key, out Vector3 value)
|
|
{
|
|
value = Vector3.zero;
|
|
if (string.IsNullOrEmpty(json))
|
|
return false;
|
|
|
|
Match objectMatch = Regex.Match(json,
|
|
"\"" + Regex.Escape(key) + "\"\\s*:\\s*\\{(?<body>.*?)\\}",
|
|
RegexOptions.Singleline | RegexOptions.CultureInvariant);
|
|
|
|
if (!objectMatch.Success)
|
|
return false;
|
|
|
|
string objectBody = objectMatch.Groups["body"].Value;
|
|
if (!TryExtractFloat(objectBody, "x", out float x) ||
|
|
!TryExtractFloat(objectBody, "y", out float y) ||
|
|
!TryExtractFloat(objectBody, "z", out float z))
|
|
return false;
|
|
|
|
value = new Vector3(x, y, z);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryExtractFloat(string json, string key, out float value)
|
|
{
|
|
value = 0f;
|
|
Match match = Regex.Match(json,
|
|
"\"" + Regex.Escape(key) + "\"\\s*:\\s*(?<num>-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)",
|
|
RegexOptions.CultureInvariant);
|
|
|
|
return match.Success &&
|
|
float.TryParse(match.Groups["num"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
|
|
}
|
|
|
|
private static string VectorJson(Vector3 value)
|
|
{
|
|
return "{\"x\":" + FloatJson(value.x) +
|
|
",\"y\":" + FloatJson(value.y) +
|
|
",\"z\":" + FloatJson(value.z) + "}";
|
|
}
|
|
|
|
private static string FloatJson(float value)
|
|
{
|
|
if (float.IsNaN(value) || float.IsInfinity(value))
|
|
return "0";
|
|
|
|
return value.ToString("0.#####", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static string Bool(bool value)
|
|
{
|
|
return value ? "true" : "false";
|
|
}
|
|
|
|
private static string JsonString(string value)
|
|
{
|
|
if (value == null)
|
|
return "null";
|
|
|
|
StringBuilder sb = new StringBuilder(value.Length + 2);
|
|
sb.Append('"');
|
|
foreach (char c in value)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '\\':
|
|
sb.Append("\\\\");
|
|
break;
|
|
case '"':
|
|
sb.Append("\\\"");
|
|
break;
|
|
case '\n':
|
|
sb.Append("\\n");
|
|
break;
|
|
case '\r':
|
|
sb.Append("\\r");
|
|
break;
|
|
case '\t':
|
|
sb.Append("\\t");
|
|
break;
|
|
default:
|
|
if (c < 32)
|
|
sb.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
|
|
else
|
|
sb.Append(c);
|
|
break;
|
|
}
|
|
}
|
|
sb.Append('"');
|
|
return sb.ToString();
|
|
}
|
|
|
|
private sealed class BridgeRequest
|
|
{
|
|
public readonly string Method;
|
|
public readonly string Path;
|
|
public readonly Dictionary<string, string> Query;
|
|
public readonly string Body;
|
|
|
|
public BridgeRequest(string method, string path, Dictionary<string, string> query, string body)
|
|
{
|
|
Method = method;
|
|
Path = path;
|
|
Query = query;
|
|
Body = body ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
private sealed class BridgeResponse
|
|
{
|
|
public readonly int StatusCode;
|
|
public readonly string ContentType;
|
|
public readonly string Body;
|
|
|
|
private BridgeResponse(int statusCode, string contentType, string body)
|
|
{
|
|
StatusCode = statusCode;
|
|
ContentType = contentType;
|
|
Body = body;
|
|
}
|
|
|
|
public static BridgeResponse Json(int statusCode, string body)
|
|
{
|
|
return new BridgeResponse(statusCode, "application/json", body);
|
|
}
|
|
}
|
|
|
|
private sealed class BridgeJob
|
|
{
|
|
public readonly BridgeRequest Request;
|
|
public readonly ManualResetEventSlim Done = new ManualResetEventSlim(false);
|
|
public BridgeResponse Response;
|
|
|
|
public BridgeJob(BridgeRequest request)
|
|
{
|
|
Request = request;
|
|
}
|
|
}
|
|
|
|
private sealed class LogEntry
|
|
{
|
|
public int Index;
|
|
public string Time;
|
|
public string Type;
|
|
public string Message;
|
|
public string StackTrace;
|
|
}
|
|
}
|
|
}
|