using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.IO.Pipes; using System.Reflection; using System.Security.AccessControl; using System.Security.Principal; using System.Text; using System.Threading; using BepInEx; using UnityEngine; using UnityEngine.SceneManagement; namespace Mercury.UnityProbe { [BepInPlugin(MercuryUnityProbeBuild.PluginGuid, MercuryUnityProbeBuild.PluginName, MercuryUnityProbeBuild.PluginVersion)] public sealed class MercuryUnityProbePlugin : BaseUnityPlugin { private Thread pipeThread; private NamedPipeServerStream activeServer; private SynchronizationContext unityContext; private int mainThreadId; private volatile bool stopRequested; private void Awake() { unityContext = SynchronizationContext.Current; mainThreadId = Thread.CurrentThread.ManagedThreadId; pipeThread = new Thread(PipeServerMain); pipeThread.Name = "MercuryUnityProbePipe"; pipeThread.IsBackground = true; pipeThread.Start(); Logger.LogInfo("Mercury Unity Probe pipe server thread started."); Logger.LogInfo("Mercury Unity Probe ready on pipe " + MercuryUnityProbeBuild.PipeName + "."); } private void OnDestroy() { Logger.LogInfo("Mercury Unity Probe BaseUnityPlugin OnDestroy fired; keeping pipe backend alive."); } private void OnApplicationQuit() { stopRequested = true; Logger.LogInfo("Mercury Unity Probe stopping pipe backend for application quit."); TryDisposeActiveServer(); } private void PipeServerMain() { while (!stopRequested) { NamedPipeServerStream server = null; try { server = CreatePipeServer(); activeServer = server; server.WaitForConnection(); ServeConnection(server); } catch (ObjectDisposedException) { if (!stopRequested) { Logger.LogWarning("Named pipe server disposed unexpectedly."); } } catch (Exception ex) { if (!stopRequested) { Logger.LogError("Named pipe server failed: " + ex); } } finally { TryDisposeServer(server); if (ReferenceEquals(activeServer, server)) { activeServer = null; } } } } private static NamedPipeServerStream CreatePipeServer() { PipeSecurity pipeSecurity = CreateCurrentUserPipeSecurity(); return new NamedPipeServerStream( MercuryUnityProbeBuild.PipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 4096, 4096, pipeSecurity); } private static PipeSecurity CreateCurrentUserPipeSecurity() { WindowsIdentity identity = WindowsIdentity.GetCurrent(); SecurityIdentifier user = identity == null ? null : identity.User; if (user == null) { throw new InvalidOperationException("Could not resolve the current Windows user for Mercury Unity Probe pipe security."); } PipeSecurity pipeSecurity = new PipeSecurity(); pipeSecurity.AddAccessRule(new PipeAccessRule( user, PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow)); return pipeSecurity; } private void TryDisposeActiveServer() { TryDisposeServer(activeServer); } private static void TryDisposeServer(NamedPipeServerStream server) { if (server == null) { return; } try { server.Dispose(); } catch { } } private void ServeConnection(NamedPipeServerStream server) { StreamReader reader = null; StreamWriter writer = null; try { reader = new StreamReader(server, Encoding.UTF8, false, 4096, true); writer = new StreamWriter(server, new UTF8Encoding(false), 4096, true); writer.AutoFlush = true; string requestJson = ReadBoundedRequestLine(reader); Dictionary response = HandleRequest(requestJson); writer.WriteLine(MercuryUnityProbeProtocol.Serialize(response)); } catch (FormatException ex) { if (writer != null) { Dictionary response = MercuryUnityProbeProtocol.Error("invalid", ex.Message); writer.WriteLine(MercuryUnityProbeProtocol.Serialize(response)); } } catch (IOException ex) { if (!IsBrokenPipe(ex)) { throw; } } finally { if (writer != null) { try { writer.Dispose(); } catch (IOException ex) { if (!IsBrokenPipe(ex)) { throw; } } } if (reader != null) { reader.Dispose(); } } } private static string ReadBoundedRequestLine(TextReader reader) { StringBuilder builder = new StringBuilder(); while (true) { int value = reader.Read(); if (value < 0) { return builder.ToString(); } char current = (char)value; if (current == '\n') { return builder.ToString(); } if (current == '\r') { if (reader.Peek() == '\n') { reader.Read(); } return builder.ToString(); } if (builder.Length >= MercuryUnityProbeProtocol.MaxRequestChars) { throw new FormatException("JSON request exceeds the maximum request length."); } builder.Append(current); } } private Dictionary HandleRequest(string requestJson) { if (string.IsNullOrWhiteSpace(requestJson)) { return MercuryUnityProbeProtocol.Error("invalid", "Received an empty JSON request."); } try { Dictionary request = MercuryUnityProbeProtocol.DeserializeRequest(requestJson); if (request == null) { return MercuryUnityProbeProtocol.Error("invalid", "Failed to deserialize the JSON request."); } string command = MercuryUnityProbeProtocol.ReadString(request, "command"); if (string.IsNullOrWhiteSpace(command)) { return MercuryUnityProbeProtocol.Error("invalid", "JSON request is missing the command field."); } return InvokeOnUnityThread(delegate { return ExecuteCommand(command, request); }, command); } catch (Exception ex) { return MercuryUnityProbeProtocol.Error("invalid", ex.GetType().Name + ": " + ex.Message); } } private static bool IsBrokenPipe(IOException error) { return error != null && error.Message != null && error.Message.IndexOf("pipe is broken", StringComparison.OrdinalIgnoreCase) >= 0; } private Dictionary InvokeOnUnityThread(Func> action, string reason) { if (Thread.CurrentThread.ManagedThreadId == mainThreadId) { return action(); } if (unityContext != null) { Dictionary result = null; Exception error = null; using (ManualResetEventSlim done = new ManualResetEventSlim(false)) { unityContext.Post(delegate(object _) { try { result = action(); } catch (Exception ex) { error = ex; } finally { done.Set(); } }, null); if (!done.Wait(5000)) { return MercuryUnityProbeProtocol.Error(reason, "Timed out waiting for the Unity SynchronizationContext."); } } if (error != null) { return MercuryUnityProbeProtocol.Error(reason, error.GetType().Name + ": " + error.Message); } return result; } if (ThreadingHelper.Instance == null) { return MercuryUnityProbeProtocol.Error(reason, "No Unity SynchronizationContext or ThreadingHelper is available."); } Dictionary fallbackResult = null; Exception fallbackError = null; using (ManualResetEventSlim fallbackDone = new ManualResetEventSlim(false)) { ThreadingHelper.Instance.StartSyncInvoke(delegate { try { fallbackResult = action(); } catch (Exception ex) { fallbackError = ex; } finally { fallbackDone.Set(); } }); if (!fallbackDone.Wait(5000)) { return MercuryUnityProbeProtocol.Error(reason, "Timed out waiting for ThreadingHelper."); } } if (fallbackError != null) { return MercuryUnityProbeProtocol.Error(reason, fallbackError.GetType().Name + ": " + fallbackError.Message); } return fallbackResult; } private Dictionary ExecuteCommand(string command, IDictionary request) { string normalized = command == null ? string.Empty : command.Trim().ToLowerInvariant(); if (normalized == "status") { return BuildStatus(); } if (normalized == "scenes") { return BuildScenes(); } if (normalized == "find") { return BuildFind(MercuryUnityProbeProtocol.ReadString(request, "query"), MercuryUnityProbeProtocol.ReadLimit(request, 25)); } if (normalized == "inspect") { return BuildInspect(MercuryUnityProbeProtocol.ReadInt(request, "instance_id")); } if (normalized == "static") { return BuildStatic(MercuryUnityProbeProtocol.ReadString(request, "type_name")); } return MercuryUnityProbeProtocol.Error(command, "Unsupported command."); } private Dictionary BuildStatus() { List scenes = new List(); int sceneCount = SceneManager.sceneCount; for (int i = 0; i < sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); scenes.Add(scene.name ?? string.Empty); } Dictionary data = new Dictionary(StringComparer.Ordinal); data["pipe_name"] = MercuryUnityProbeBuild.PipeName; data["plugin_version"] = MercuryUnityProbeBuild.PluginVersion; data["unity_version"] = Application.unityVersion ?? string.Empty; data["process_id"] = (uint)System.Diagnostics.Process.GetCurrentProcess().Id; data["scene_count"] = sceneCount; data["loaded_scene_names"] = scenes; return MercuryUnityProbeProtocol.Success("status", data); } private Dictionary BuildScenes() { List> scenes = new List>(); int sceneCount = SceneManager.sceneCount; for (int i = 0; i < sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); Dictionary record = new Dictionary(StringComparer.Ordinal); record["name"] = scene.name ?? string.Empty; record["path"] = scene.path ?? string.Empty; record["build_index"] = scene.buildIndex; record["loaded"] = scene.isLoaded; record["root_count"] = scene.rootCount; scenes.Add(record); } Dictionary data = new Dictionary(StringComparer.Ordinal); data["scenes"] = scenes; return MercuryUnityProbeProtocol.Success("scenes", data); } private Dictionary BuildFind(string query, int limit) { string needle = query == null ? string.Empty : query.Trim(); if (needle.Length == 0) { return MercuryUnityProbeProtocol.Error("find", "find requires a non-empty query."); } List> matches = new List>(); UnityEngine.Object[] objects = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < objects.Length; i++) { UnityEngine.Object obj = objects[i]; if (obj == null) { continue; } string typeName = obj.GetType().FullName ?? obj.GetType().Name; string objectName = obj.name ?? string.Empty; if (!ContainsIgnoreCase(typeName, needle) && !ContainsIgnoreCase(objectName, needle)) { continue; } matches.Add(DescribeObjectSummary(obj)); if (matches.Count >= limit) { break; } } matches.Sort(delegate(Dictionary left, Dictionary right) { return string.CompareOrdinal( Convert.ToString(left["type_name"]) + "|" + Convert.ToString(left["name"]), Convert.ToString(right["type_name"]) + "|" + Convert.ToString(right["name"])); }); Dictionary data = new Dictionary(StringComparer.Ordinal); data["query"] = needle; data["matches"] = matches; return MercuryUnityProbeProtocol.Success("find", data); } private Dictionary BuildInspect(int instanceId) { if (instanceId == 0) { return MercuryUnityProbeProtocol.Error("inspect", "inspect requires a non-zero instance id."); } UnityEngine.Object obj = FindObjectByInstanceId(instanceId); if (obj == null) { return MercuryUnityProbeProtocol.Error("inspect", "No runtime object matched the requested instance id."); } Dictionary data = new Dictionary(StringComparer.Ordinal); data["object"] = DescribeObjectSummary(obj); data["components"] = DescribeComponents(obj); data["fields"] = DescribeInstanceFields(obj); data["properties"] = DescribeInstanceProperties(obj); return MercuryUnityProbeProtocol.Success("inspect", data); } private Dictionary BuildStatic(string typeName) { string requested = typeName == null ? string.Empty : typeName.Trim(); if (requested.Length == 0) { return MercuryUnityProbeProtocol.Error("static", "static requires a managed type name."); } Type type = ResolveType(requested); if (type == null) { return MercuryUnityProbeProtocol.Error("static", "Could not resolve the requested managed type."); } Dictionary data = new Dictionary(StringComparer.Ordinal); data["type_name"] = type.FullName ?? type.Name; data["assembly_name"] = type.Assembly.GetName().Name ?? string.Empty; data["fields"] = DescribeStaticFields(type); data["properties"] = DescribeStaticProperties(type); return MercuryUnityProbeProtocol.Success("static", data); } private static bool ContainsIgnoreCase(string haystack, string needle) { if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(needle)) { return false; } return haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; } private static UnityEngine.Object FindObjectByInstanceId(int instanceId) { UnityEngine.Object[] objects = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < objects.Length; i++) { UnityEngine.Object obj = objects[i]; if (obj != null && obj.GetInstanceID() == instanceId) { return obj; } } return null; } private static Dictionary DescribeObjectSummary(UnityEngine.Object obj) { Dictionary record = new Dictionary(StringComparer.Ordinal); record["instance_id"] = obj.GetInstanceID(); record["name"] = obj.name ?? string.Empty; record["type_name"] = obj.GetType().FullName ?? obj.GetType().Name; record["scene_name"] = GetSceneName(obj); record["hierarchy_path"] = GetHierarchyPath(obj); record["active"] = IsObjectActive(obj); return record; } private static List> DescribeComponents(UnityEngine.Object obj) { List> items = new List>(); GameObject gameObject = null; if (obj is GameObject) { gameObject = (GameObject)obj; } else if (obj is Component) { gameObject = ((Component)obj).gameObject; } if (gameObject == null) { return items; } Component[] components = gameObject.GetComponents(); for (int i = 0; i < components.Length; i++) { Component component = components[i]; if (component == null) { continue; } Dictionary record = new Dictionary(StringComparer.Ordinal); record["instance_id"] = component.GetInstanceID(); record["type_name"] = component.GetType().FullName ?? component.GetType().Name; items.Add(record); } return items; } private static List> DescribeInstanceFields(object target) { return DescribeMembers( target.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), 24, delegate(FieldInfo field) { object value = null; try { value = field.GetValue(target); } catch { } return CreateValueMember(field.Name, field.FieldType, value); }); } private static List> DescribeInstanceProperties(object target) { return DescribeMembers( target.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), 24, delegate(PropertyInfo property) { if (property.GetIndexParameters().Length != 0 || property.GetGetMethod(true) == null) { return null; } object value = null; try { value = property.GetValue(target, null); } catch { } return CreateValueMember(property.Name, property.PropertyType, value); }); } private static List> DescribeStaticFields(Type type) { return DescribeMembers( type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static), 32, delegate(FieldInfo field) { object value = null; try { value = field.GetValue(null); } catch { } return CreateValueMember(field.Name, field.FieldType, value); }); } private static List> DescribeStaticProperties(Type type) { return DescribeMembers( type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static), 32, delegate(PropertyInfo property) { if (property.GetIndexParameters().Length != 0 || property.GetGetMethod(true) == null) { return null; } object value = null; try { value = property.GetValue(null, null); } catch { } return CreateValueMember(property.Name, property.PropertyType, value); }); } private delegate Dictionary MemberFormatter(TMember member); private static List> DescribeMembers(TMember[] members, int limit, MemberFormatter formatter) { List> items = new List>(); Array.Sort(members, delegate(TMember left, TMember right) { string leftName = GetMemberName(left); string rightName = GetMemberName(right); return string.CompareOrdinal(leftName, rightName); }); for (int i = 0; i < members.Length && items.Count < limit; i++) { Dictionary record = formatter(members[i]); if (record != null) { items.Add(record); } } return items; } private static string GetMemberName(TMember member) { MemberInfo info = member as MemberInfo; return info == null ? string.Empty : info.Name ?? string.Empty; } private static Dictionary CreateValueMember(string name, Type declaredType, object value) { Dictionary record = new Dictionary(StringComparer.Ordinal); record["name"] = name ?? string.Empty; record["declared_type"] = declaredType == null ? string.Empty : (declaredType.FullName ?? declaredType.Name ?? string.Empty); record["value"] = FormatValue(value); return record; } private static string FormatValue(object value) { if (value == null) { return "null"; } if (value is string) { return TrimValue((string)value); } if (value is bool || value is byte || value is sbyte || value is short || value is ushort || value is int || value is uint || value is long || value is ulong || value is float || value is double || value is decimal || value is char) { return Convert.ToString(value) ?? string.Empty; } if (value is Enum) { return value.GetType().Name + "." + value; } if (value is UnityEngine.Object) { UnityEngine.Object obj = (UnityEngine.Object)value; return (obj.GetType().FullName ?? obj.GetType().Name) + "#" + obj.GetInstanceID() + ":" + (obj.name ?? string.Empty); } if (value is IList) { IList list = (IList)value; return "list[count=" + list.Count + "]"; } if (value is IEnumerable) { return "enumerable"; } return TrimValue(Convert.ToString(value) ?? value.GetType().Name); } private static string TrimValue(string text) { if (text == null) { return string.Empty; } string trimmed = text.Replace("\r", "\\r").Replace("\n", "\\n"); return trimmed.Length <= 160 ? trimmed : trimmed.Substring(0, 157) + "..."; } private static string GetSceneName(UnityEngine.Object obj) { if (obj is GameObject) { return ((GameObject)obj).scene.name ?? string.Empty; } if (obj is Component) { Component component = (Component)obj; return component.gameObject == null ? string.Empty : (component.gameObject.scene.name ?? string.Empty); } return string.Empty; } private static string GetHierarchyPath(UnityEngine.Object obj) { Transform transform = null; if (obj is GameObject) { transform = ((GameObject)obj).transform; } else if (obj is Component) { transform = ((Component)obj).transform; } if (transform == null) { return string.Empty; } List parts = new List(); while (transform != null) { parts.Add(transform.name ?? string.Empty); transform = transform.parent; } parts.Reverse(); return "/" + string.Join("/", parts.ToArray()); } private static bool IsObjectActive(UnityEngine.Object obj) { if (obj is Behaviour) { return ((Behaviour)obj).isActiveAndEnabled; } if (obj is Component) { Component component = (Component)obj; return component.gameObject != null && component.gameObject.activeInHierarchy; } if (obj is GameObject) { return ((GameObject)obj).activeInHierarchy; } return true; } private static Type ResolveType(string requested) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { Type type = assemblies[i].GetType(requested, false); if (type != null) { return type; } } for (int i = 0; i < assemblies.Length; i++) { Type[] types; try { types = assemblies[i].GetTypes(); } catch { continue; } for (int j = 0; j < types.Length; j++) { Type type = types[j]; string fullName = type.FullName ?? string.Empty; string name = type.Name ?? string.Empty; if (string.Equals(fullName, requested, StringComparison.OrdinalIgnoreCase) || string.Equals(name, requested, StringComparison.OrdinalIgnoreCase)) { return type; } } } return null; } } }