chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:41:59 +08:00
commit e365e5df4d
508 changed files with 163373 additions and 0 deletions
@@ -0,0 +1,872 @@
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<string, object> response = HandleRequest(requestJson);
writer.WriteLine(MercuryUnityProbeProtocol.Serialize(response));
}
catch (FormatException ex)
{
if (writer != null)
{
Dictionary<string, object> 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<string, object> HandleRequest(string requestJson)
{
if (string.IsNullOrWhiteSpace(requestJson))
{
return MercuryUnityProbeProtocol.Error("invalid", "Received an empty JSON request.");
}
try
{
Dictionary<string, object> 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<string, object> InvokeOnUnityThread(Func<Dictionary<string, object>> action, string reason)
{
if (Thread.CurrentThread.ManagedThreadId == mainThreadId)
{
return action();
}
if (unityContext != null)
{
Dictionary<string, object> 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<string, object> 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<string, object> ExecuteCommand(string command, IDictionary<string, object> 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<string, object> BuildStatus()
{
List<string> scenes = new List<string>();
int sceneCount = SceneManager.sceneCount;
for (int i = 0; i < sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
scenes.Add(scene.name ?? string.Empty);
}
Dictionary<string, object> data = new Dictionary<string, object>(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<string, object> BuildScenes()
{
List<Dictionary<string, object>> scenes = new List<Dictionary<string, object>>();
int sceneCount = SceneManager.sceneCount;
for (int i = 0; i < sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
Dictionary<string, object> record = new Dictionary<string, object>(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<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);
data["scenes"] = scenes;
return MercuryUnityProbeProtocol.Success("scenes", data);
}
private Dictionary<string, object> 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<Dictionary<string, object>> matches = new List<Dictionary<string, object>>();
UnityEngine.Object[] objects = Resources.FindObjectsOfTypeAll<UnityEngine.Object>();
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<string, object> left, Dictionary<string, object> right)
{
return string.CompareOrdinal(
Convert.ToString(left["type_name"]) + "|" + Convert.ToString(left["name"]),
Convert.ToString(right["type_name"]) + "|" + Convert.ToString(right["name"]));
});
Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);
data["query"] = needle;
data["matches"] = matches;
return MercuryUnityProbeProtocol.Success("find", data);
}
private Dictionary<string, object> 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<string, object> data = new Dictionary<string, object>(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<string, object> 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<string, object> data = new Dictionary<string, object>(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<UnityEngine.Object>();
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<string, object> DescribeObjectSummary(UnityEngine.Object obj)
{
Dictionary<string, object> record = new Dictionary<string, object>(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<Dictionary<string, object>> DescribeComponents(UnityEngine.Object obj)
{
List<Dictionary<string, object>> items = new List<Dictionary<string, object>>();
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<Component>();
for (int i = 0; i < components.Length; i++)
{
Component component = components[i];
if (component == null)
{
continue;
}
Dictionary<string, object> record = new Dictionary<string, object>(StringComparer.Ordinal);
record["instance_id"] = component.GetInstanceID();
record["type_name"] = component.GetType().FullName ?? component.GetType().Name;
items.Add(record);
}
return items;
}
private static List<Dictionary<string, object>> 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<Dictionary<string, object>> 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<Dictionary<string, object>> 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<Dictionary<string, object>> 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<string, object> MemberFormatter<TMember>(TMember member);
private static List<Dictionary<string, object>> DescribeMembers<TMember>(TMember[] members, int limit, MemberFormatter<TMember> formatter)
{
List<Dictionary<string, object>> items = new List<Dictionary<string, object>>();
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<string, object> record = formatter(members[i]);
if (record != null)
{
items.Add(record);
}
}
return items;
}
private static string GetMemberName<TMember>(TMember member)
{
MemberInfo info = member as MemberInfo;
return info == null ? string.Empty : info.Name ?? string.Empty;
}
private static Dictionary<string, object> CreateValueMember(string name, Type declaredType, object value)
{
Dictionary<string, object> record = new Dictionary<string, object>(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<string> parts = new List<string>();
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;
}
}
}