From 60681b2c882fcae5a4c5eba523a9cbc50e6fe178 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Mon, 20 Oct 2025 20:51:11 +0200 Subject: [PATCH 1/3] Refactor PatchInspector --- .../PatchInspector/ILViewerWindow.cs | 306 +++- .../PatchInspector/PatchInspector.cs | 1548 +++++++---------- 2 files changed, 936 insertions(+), 918 deletions(-) diff --git a/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs b/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs index 7f24976..ee9508f 100644 --- a/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs +++ b/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs @@ -1,34 +1,286 @@ +using HarmonyLib; +using RuntimeUnityEditor.Core; +using RuntimeUnityEditor.Core.Utils.Abstractions; +using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using UnityEngine; namespace RuntimeUnityEditor.Bepin5.PatchInspector { - internal class ILViewerWindow - { - public int WindowId; - public Rect WindowRect; - public MethodBase Method; - public string OriginalIL; - public List PatchMethods; - public Vector2 ScrollPosition; - public Vector2 PatchListScrollPosition; - public bool IsOpen; - public ILViewMode CurrentView; - public int SelectedPatchIndex; - - public ILViewerWindow(int windowId, MethodBase method, string originalIL, List patchMethods) - { - WindowId = windowId; - Method = method; - OriginalIL = originalIL; - PatchMethods = patchMethods; - WindowRect = new Rect(100 + (windowId % 5) * 50, 100 + (windowId % 5) * 50, 900, 650); - ScrollPosition = Vector2.zero; - PatchListScrollPosition = Vector2.zero; - IsOpen = true; - CurrentView = ILViewMode.Original; - SelectedPatchIndex = -1; - } - } + internal sealed class ILViewerWindow : Window + { + public MethodBase Method => _method; + + private readonly MethodBase _method; + private readonly string _originalIL; + private readonly List _patchMethods; + + private Vector2 _scrollPosition; + private Vector2 _patchListScrollPosition; + private ILViewMode _currentView = ILViewMode.Original; + private int _selectedPatchIndex = -1; + + public ILViewerWindow(int windowId, MethodBase method, string originalIL, List patchMethods) + { + WindowId = windowId; + _method = method ?? throw new ArgumentNullException(nameof(method)); + _originalIL = originalIL ?? throw new ArgumentNullException(nameof(originalIL)); + _patchMethods = patchMethods ?? throw new ArgumentNullException(nameof(patchMethods)); + + Enabled = true; + DisplayType = FeatureDisplayType.Hidden; + DefaultScreenPosition = ScreenPartition.CenterUpper; + Title = $"IL Code: {_method.DeclaringType?.Name}.{_method.Name}"; + + ResetWindowRect(); + //WindowRect = new Rect(100 + (windowId % 5) * 50, 100 + (windowId % 5) * 50, 900, 650); + } + + protected override void Initialize(InitSettings initSettings) => throw new InvalidOperationException("This window should not be initialized"); + internal void DoOnGUI() => OnGUI(); + internal void DoVisibleChanged(bool visible) => VisibleChanged(visible); + + protected override void DrawContents() + { + GUILayout.BeginVertical(); + // todo cache + tooltip maybe buttons + GUILayout.Label($"Method: {_method.DeclaringType?.FullName}.{_method.Name}"); + GUILayout.Label($"Parameters: {GetMethodParameters(_method)}"); + GUILayout.Label($"Return Type: {GetReturnType(_method)}"); + + GUILayout.Space(10); + + GUILayout.BeginHorizontal(); + + bool opSelected = _currentView == ILViewMode.Original; + if (opSelected) GUI.color = Color.yellow; + if (GUILayout.Button("Original Method", GUILayout.Height(30))) + { + _currentView = ILViewMode.Original; + _scrollPosition = Vector2.zero; + } + if (opSelected) GUI.color = Color.white; + + bool patchMethodsSelected = _currentView == ILViewMode.PatchMethods; + if (patchMethodsSelected) GUI.color = Color.yellow; + if (GUILayout.Button($"Patch Manager ({_patchMethods.Count})", GUILayout.Height(30))) + { + _currentView = ILViewMode.PatchMethods; + _scrollPosition = Vector2.zero; + } + if (patchMethodsSelected) GUI.color = Color.white; + + GUILayout.FlexibleSpace(); + + GUILayout.EndHorizontal(); + + GUILayout.Space(5); + + switch (_currentView) + { + case ILViewMode.Original: + DrawOriginalMethodView(); + break; + case ILViewMode.PatchMethods: + DrawPatchManagerView(); + break; + } + + GUILayout.EndVertical(); + } + + public void DrawOriginalMethodView() + { + GUILayout.Label("Original Method IL Code:"); + _scrollPosition = GUILayout.BeginScrollView(_scrollPosition, GUILayoutShim.ExpandHeight(true)); + GUILayout.TextArea(_originalIL); + GUILayout.EndScrollView(); + } + + private void DrawPatchManagerView() + { + if (_patchMethods.Count == 0) + { + GUILayout.Label("No patches found for this method."); + return; + } + + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(GUILayout.Width(400)); + GUILayout.Label("Patches:"); + + if (GUILayout.Button("Refresh Patch List", GUILayout.Height(25))) + RefreshPatchList(); + + _patchListScrollPosition = GUILayout.BeginScrollView(_patchListScrollPosition, GUILayoutShim.ExpandHeight(true)); + + for (var i = 0; i < _patchMethods.Count; i++) + { + var patch = _patchMethods[i]; + + GUILayout.BeginVertical("box"); + GUILayout.BeginHorizontal(); + + bool newEnabled = GUILayout.Toggle(patch.IsEnabled, "", GUILayout.Width(20)); + if (newEnabled != patch.IsEnabled) + { + TogglePatch(i, newEnabled); + //SearchPatches(); // not needed? + } + + GUILayout.BeginVertical(); + + bool isSelected = _selectedPatchIndex == i; + if (isSelected) GUI.color = Color.cyan; + + if (GUILayout.Button($"{patch.PatchType}: {patch.PatchMethod.DeclaringType?.Name}.{patch.PatchMethod.Name}", "label")) + { + _selectedPatchIndex = i; + _scrollPosition = Vector2.zero; + } + + if (isSelected) GUI.color = Color.white; + + GUILayout.Label($"Priority: {patch.Priority} | {patch.PatcherNamespace}"); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + GUILayout.Space(2); + } + + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(); + + if (_selectedPatchIndex >= 0 && _selectedPatchIndex < _patchMethods.Count) + { + var selectedPatch = _patchMethods[_selectedPatchIndex]; + GUILayout.Label($"IL Code for: {selectedPatch.PatchType} - {selectedPatch.PatchMethod.DeclaringType?.Name}.{selectedPatch.PatchMethod.Name}", GUI.skin.label); + + _scrollPosition = GUILayout.BeginScrollView(_scrollPosition, GUILayoutShim.ExpandHeight(true)); + + GUILayout.TextArea(selectedPatch.ILCode); + + GUILayout.EndScrollView(); + } + else + { + GUILayout.Label("Select a patch from the list to view its IL code.", GUI.skin.label); + } + + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + + private void TogglePatch(int patchIndex, bool enable) + { + if (patchIndex < 0 || patchIndex >= _patchMethods.Count) + return; + + var patch = _patchMethods[patchIndex]; + + try + { + if (enable && !patch.IsEnabled) + { + var harmony = new Harmony(patch.HarmonyId ?? "harmony.patch.inspector.temp"); + + switch (patch.PatchType) + { + case "Prefix": + harmony.Patch(_method, prefix: patch.HarmonyPatch); + break; + case "Postfix": + harmony.Patch(_method, postfix: patch.HarmonyPatch); + break; + case "Transpiler": + harmony.Patch(_method, transpiler: patch.HarmonyPatch); + break; + case "Finalizer": + harmony.Patch(_method, finalizer: patch.HarmonyPatch); + break; + } + + patch.IsEnabled = true; + } + else if (!enable && patch.IsEnabled) + { + var harmony = new Harmony(patch.HarmonyId ?? "harmony.patch.inspector.temp"); + harmony.Unpatch(_method, patch.PatchMethod as MethodInfo); + patch.IsEnabled = false; + } + } + catch (Exception e) + { + //patch.IsEnabled = !enable; + RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, $"Failed to {(enable ? "enable" : "disable")} Harmony patch {patch.HarmonyId ?? ""}:{patch.HarmonyPatch?.methodName}"); + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); + } + } + + private void RefreshPatchList() + { + try + { + var currentPatchInfo = Harmony.GetPatchInfo(_method); + var currentPatches = new List(); + + if (currentPatchInfo != null) + { + if (currentPatchInfo.Prefixes != null) + currentPatches.AddRange(currentPatchInfo.Prefixes.Select(p => p.PatchMethod)); + if (currentPatchInfo.Postfixes != null) + currentPatches.AddRange(currentPatchInfo.Postfixes.Select(p => p.PatchMethod)); + if (currentPatchInfo.Transpilers != null) + currentPatches.AddRange(currentPatchInfo.Transpilers.Select(p => p.PatchMethod)); + if (currentPatchInfo.Finalizers != null) + currentPatches.AddRange(currentPatchInfo.Finalizers.Select(p => p.PatchMethod)); + } + + foreach (var patch in _patchMethods) + { + patch.IsEnabled = currentPatches.Contains(patch.PatchMethod as MethodInfo); + } + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); + } + } + + private static string GetMethodParameters(MethodBase method) + { + try + { + var parameters = method.GetParameters(); + if (parameters.Length == 0) + return "()"; + + var paramStrings = parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"); + return $"({string.Join(", ", paramStrings.ToArray())})"; + } + catch + { + return "(unknown)"; + } + } + + private static string GetReturnType(MethodBase method) + { + try + { + if (method is MethodInfo methodInfo) + return methodInfo.ReturnType.Name; + return "void"; + } + catch + { + return "Unknown"; + } + } + } } \ No newline at end of file diff --git a/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInspector.cs b/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInspector.cs index c214790..1066f30 100644 --- a/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInspector.cs +++ b/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInspector.cs @@ -2,907 +2,673 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Reflection.Emit; -using System.Text; using BepInEx; using HarmonyLib; using RuntimeUnityEditor.Core; -using RuntimeUnityEditor.Core.Utils; using RuntimeUnityEditor.Core.Utils.Abstractions; using UnityEngine; namespace RuntimeUnityEditor.Bepin5.PatchInspector { - public class PatchInspector : Window - { - private string searchInput = String.Empty; - private Vector2 scrollPos; - private List foundPatches = new List(); - private bool showFilePaths = true; - - private int nextWindowId = 13000; - private Dictionary ilViewerWindows = new Dictionary(); - private Dictionary> opPatchStates = new Dictionary>(); - protected override void Initialize(InitSettings initSettings) - { - Enabled = false; - DisplayName = "Patch Inspector"; - Title = "Patch Inspector"; - WindowRect = new Rect(50, 50, 700, 600); - } - - protected override void OnVisibleChanged(bool visible) - { - if (visible) - { - searchInput = string.Empty; - foundPatches.Clear(); - } - base.OnVisibleChanged(visible); - } - - protected override void OnGUI() - { - foreach (var window in ilViewerWindows) - { - if (window.Value.IsOpen) - { - window.Value.WindowRect = GUI.Window(window.Value.WindowId, window.Value.WindowRect, (GUI.WindowFunction)(id => DrawILViewerWindow(id, window.Value)), - $"IL Code: {window.Value.Method.DeclaringType?.Name}.{window.Value.Method.Name}"); - if (window.Value.WindowRect.Contains(Event.current.mousePosition)) - { - Input.ResetInputAxes(); - } - } - else - { - ilViewerWindows.Remove(window.Key); - return; - } - } - base.OnGUI(); - } - - protected override void DrawContents() - { - GUILayout.BeginVertical(); - - GUILayout.Label("Search for patches by method, class, or namespace:", GUI.skin.label); - GUILayout.Label("Examples: 'OnClick', 'method:OnClick class:AddButtonCtrl', 'namespace:SimpleGame'"); - - GUILayout.BeginHorizontal(); - GUILayout.Label("Search:", GUILayout.Width(60)); - - string newSearchInput = GUILayout.TextField(searchInput); - if (newSearchInput != searchInput) - { - searchInput = newSearchInput; - SearchPatches(); - return; - } - - // Replaced by upper OnValueChanged - /*if (GUILayout.Button("Search", GUILayout.Width(80))) + /// + /// Window for inspecting and managing Harmony patches applied to methods. + /// + public class PatchInspector : Window + { + private string _searchInput = String.Empty; + private Vector2 _scrollPos; + private List _foundPatches = new List(); + private bool _showFilePaths = true; + + private int _nextWindowId = 13000; + private readonly List _ilViewerWindows = new List(); + private readonly Dictionary> _opPatchStates = new Dictionary>(); + + /// + protected override void Initialize(InitSettings initSettings) + { + Enabled = false; + DefaultScreenPosition = ScreenPartition.LeftUpper; + DisplayName = "Patch Inspector"; + Title = "Patch Inspector"; + } + + /// + protected override void VisibleChanged(bool visible) + { + if (visible) + { + //_searchInput = string.Empty; + //_foundPatches.Clear(); + SearchPatches(); + } + + foreach (var window in _ilViewerWindows) + { + if (window.Enabled) + window.DoVisibleChanged(visible); + } + + base.VisibleChanged(visible); + } + + /// + protected override void OnGUI() + { + for (var i = 0; i < _ilViewerWindows.Count; i++) + { + var window = _ilViewerWindows[i]; + if (window.Enabled) + { + window.DoOnGUI(); + } + else + { + _ilViewerWindows.RemoveAt(i); + i--; + } + } + + base.OnGUI(); + } + + /// + protected override void DrawContents() + { + GUILayout.BeginVertical(); + + GUILayout.Label("Search for patches by method, class, or namespace:", GUI.skin.label); + GUILayout.Label("Examples: 'OnClick', 'method:OnClick class:AddButtonCtrl', 'namespace:SimpleGame'"); + + GUILayout.BeginHorizontal(); + GUILayout.Label("Search:", GUILayout.Width(60)); + + string newSearchInput = GUILayout.TextField(_searchInput); + if (newSearchInput != _searchInput) + { + _searchInput = newSearchInput; + SearchPatches(); + return; + } + + // Replaced by upper OnValueChanged + /*if (GUILayout.Button("Search", GUILayout.Width(80))) { SearchPatches(); }*/ - if (GUILayout.Button("Clear", GUILayout.Width(60))) - { - searchInput = string.Empty; - foundPatches.Clear(); - } - GUILayout.EndHorizontal(); - - showFilePaths = GUILayout.Toggle(showFilePaths, "Show file paths"); - - GUILayout.Space(10); - - if (foundPatches.Count > 0) - { - GUILayout.Label($"Found {foundPatches.Count} patches:"); - scrollPos = GUILayout.BeginScrollView(scrollPos, GUILayoutShim.ExpandHeight(true)); - - foreach (var patch in foundPatches) - { - GUILayout.BeginVertical(); - Color bgColor = patch.IsEnabled ? Color.white : new Color(1f, 0.39f, 0.39f, 0.3f); - - GUILayout.BeginVertical("box"); - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(); - - GUI.color = bgColor; - GUILayout.Label($"Method: {patch.TargetType}.{patch.MethodName}"); - GUILayout.Label($"Patch Type: {patch.PatchType}"); - GUILayout.Label($"Patcher: {patch.PatcherNamespace}"); - GUILayout.Label($"Assembly: {patch.PatcherAssembly}"); - - if (showFilePaths && !string.IsNullOrEmpty(patch.FilePath)) - { - GUILayout.Label($"File: {patch.FilePath}"); - } - - GUI.color = Color.white; - GUILayout.EndVertical(); - - GUILayout.BeginVertical(GUILayout.Width(80)); - - bool newEnabled = GUILayout.Toggle(patch.IsEnabled, "Enabled"); - if (newEnabled != patch.IsEnabled) - { - TogglePatchDirect(patch, newEnabled); - SearchPatches(); - return; - } - - if (GUILayout.Button("View IL", GUILayout.Height(25))) - { - OpenILViewer(patch.TargetMethod); - } - - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - GUILayout.EndVertical(); - GUILayout.Space(5); - } - - GUILayout.EndScrollView(); - } - else if (!string.IsNullOrEmpty(searchInput)) - { - GUILayout.Label("No patches found."); - } - else - { - GUILayout.Label("Enter a method name, namespace, or type to search for patches."); - } - - GUILayout.Space(10); - GUILayout.EndVertical(); - } - - private void SearchPatches() - { - foundPatches.Clear(); - - string searchTerm = searchInput.Trim(); - - if (string.IsNullOrEmpty(searchTerm)) - return; - - try - { - var searchCriteria = ParseSearchInput(searchTerm); - // Not entirely sure why I had this in the old project? PatchInspector does not own any Patches... - // var harmony = Harmony.CreateAndPatchAll(typeof(PatchInspector)); - var patchedMethods = Harmony.GetAllPatchedMethods(); - - foreach (var method in patchedMethods) - { - var patches = Harmony.GetPatchInfo(method); - if (patches == null) continue; - - if (!MatchesSearchCriteria(method, searchCriteria)) - continue; - - AddPatchesToList(patches.Prefixes.ToArray(), method, "Prefix"); - AddPatchesToList(patches.Postfixes.ToArray(), method, "Postfix"); - AddPatchesToList(patches.Transpilers.ToArray(), method, "Transpiler"); - AddPatchesToList(patches.Finalizers.ToArray(), method, "Finalizer"); - } - - foundPatches = foundPatches.OrderBy(info => info.TargetType).ThenBy(info => info.MethodName).ToList(); - } - catch - { - } - } - - private SearchCriteria ParseSearchInput(string input) - { - var criteria = new SearchCriteria(); - - if (input.Contains(":")) - { - criteria.IsStructured = true; - var parts = input.Split(' '); - - foreach (string part in parts) - { - if (part.StartsWith("method:", StringComparison.OrdinalIgnoreCase)) - criteria.Method = part.Substring(7); - else if (part.StartsWith("class:", StringComparison.OrdinalIgnoreCase)) - criteria.Class = part.Substring(6); - else if (part.StartsWith("type:", StringComparison.OrdinalIgnoreCase)) - criteria.Class = part.Substring(5); - else if (part.StartsWith("namespace:", StringComparison.OrdinalIgnoreCase)) - criteria.Namespace = part.Substring(10); - else if (!part.Contains(":")) - criteria.Text = part; - } - } - else - { - criteria.Text = input; - criteria.IsStructured = false; - } - - return criteria; - } - - private bool MatchesSearchCriteria(MethodBase method, SearchCriteria criteria) - { - if (criteria.IsStructured) - { - bool matches = true; - - if (!string.IsNullOrEmpty(criteria.Method)) - matches &= method.Name.IndexOf(criteria.Method, StringComparison.OrdinalIgnoreCase) >= 0; - - if (!string.IsNullOrEmpty(criteria.Class)) - matches &= method.DeclaringType?.Name.IndexOf(criteria.Class, StringComparison.OrdinalIgnoreCase) >= 0 || - method.DeclaringType?.FullName?.IndexOf(criteria.Class, StringComparison.OrdinalIgnoreCase) >= 0; - - if (!string.IsNullOrEmpty(criteria.Namespace)) - matches &= method.DeclaringType?.Namespace?.IndexOf(criteria.Namespace, StringComparison.OrdinalIgnoreCase) >= 0; - - if (!string.IsNullOrEmpty(criteria.Text)) - { - string searchTerm = criteria.Text.ToLower(); - string fullMethodName = $"{method.DeclaringType?.FullName}.{method.Name}".ToLower(); - string methodWithParams = GetMethodSignature(method).ToLower(); - - matches &= method.Name.ToLower().Contains(searchTerm) || - method.DeclaringType?.Name.ToLower().Contains(searchTerm) == true || - method.DeclaringType?.Namespace?.ToLower().Contains(searchTerm) == true || - method.DeclaringType?.FullName?.ToLower().Contains(searchTerm) == true || - fullMethodName.Contains(searchTerm) || - methodWithParams.Contains(searchTerm); - } - - return matches; - } - else - { - string searchTerm = criteria.Text.ToLower(); - string fullMethodName = $"{method.DeclaringType?.FullName}.{method.Name}".ToLower(); - string methodWithParams = GetMethodSignature(method).ToLower(); - - return method.Name.ToLower().Contains(searchTerm) || - method.DeclaringType?.Name.ToLower().Contains(searchTerm) == true || - method.DeclaringType?.Namespace?.ToLower().Contains(searchTerm) == true || - method.DeclaringType?.FullName?.ToLower().Contains(searchTerm) == true || - fullMethodName.Contains(searchTerm) || - methodWithParams.Contains(searchTerm); - } - } - - private void AddPatchesToList(Patch[] patches, MethodBase targetMethod, string patchType) - { - if (patches == null) return; - foreach (var patch in patches) - { - var patchMethod = patch.PatchMethod; - var assembly = patchMethod.DeclaringType?.Assembly; - - var patchInfo = new PatchInfo - { - MethodName = targetMethod.Name, - TargetType = targetMethod.DeclaringType?.FullName ?? "Unknown", - PatcherAssembly = assembly?.GetName().Name ?? "Unknown", - PatchType = patchType, - FilePath = GetAssemblyFilePath(assembly), - PatcherNamespace = patchMethod.DeclaringType?.Namespace ?? "Unknown", - TargetMethod = targetMethod, - IsEnabled = true - }; - - foundPatches.Add(patchInfo); - } - - string methodKey = GetMethodSignature(targetMethod); - if (opPatchStates.ContainsKey(methodKey)) - { - var storedPatches = opPatchStates[methodKey]; - foreach (var storedPatch in storedPatches) - { - if (!storedPatch.IsEnabled && storedPatch.PatchType == patchType) - { - bool alreadyAdded = foundPatches.Any(fp => fp.TargetMethod == targetMethod && fp.PatchType == patchType && fp.PatcherNamespace == storedPatch.PatcherNamespace); - - if (!alreadyAdded) - { - var assembly = storedPatch.PatchMethod.DeclaringType?.Assembly; - var patchInfo = new PatchInfo - { - MethodName = targetMethod.Name, - TargetType = targetMethod.DeclaringType?.FullName ?? "Unknown", - PatcherAssembly = assembly?.GetName().Name ?? "Unknown", - PatchType = patchType, - FilePath = GetAssemblyFilePath(assembly), - PatcherNamespace = storedPatch.PatcherNamespace, - TargetMethod = targetMethod, - IsEnabled = false - }; - - foundPatches.Add(patchInfo); - } - } - } - } - } - - private string GetAssemblyFilePath(Assembly assembly) - { - try - { - if (assembly != null) - { - return assembly.Location; - } - } - catch (Exception e) - { - RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); - } - - return "Dynamic Assembly"; - } - - private void OpenILViewer(MethodBase method) - { - if (method == null) return; - - string methodKey = GetMethodSignature(method); - - if (ilViewerWindows.ContainsKey(methodKey) && ilViewerWindows[methodKey].IsOpen) - return; - - try - { - string opIL = IL.DisassembleMethod(method); - List patchMethods; - - if (opPatchStates.ContainsKey(methodKey)) - { - patchMethods = opPatchStates[methodKey]; - RefreshPatchListInternal(method, patchMethods); - } - else - { - patchMethods = new List(); - var patchInfo = Harmony.GetPatchInfo(method); - - if (patchInfo != null) - { - if (patchInfo.Prefixes != null) - { - foreach (var patch in patchInfo.Prefixes) - { - patchMethods.Add(new PatchMethodInfo - { - PatchType = "Prefix", - PatchMethod = patch.PatchMethod, - PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", - ILCode = IL.DisassembleMethod(patch.PatchMethod), - Priority = patch.priority, - IsEnabled = true, - HarmonyPatch = new HarmonyMethod(patch.PatchMethod), - HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) - }); - } - } - - if (patchInfo.Postfixes != null) - { - foreach (var patch in patchInfo.Postfixes) - { - patchMethods.Add(new PatchMethodInfo - { - PatchType = "Postfix", - PatchMethod = patch.PatchMethod, - PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", - ILCode = IL.DisassembleMethod(patch.PatchMethod), - Priority = patch.priority, - IsEnabled = true, - HarmonyPatch = new HarmonyMethod(patch.PatchMethod), - HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) - }); - } - } - - - if (patchInfo.Transpilers != null) - { - foreach (var patch in patchInfo.Transpilers) - { - patchMethods.Add(new PatchMethodInfo - { - PatchType = "Transpiler", - PatchMethod = patch.PatchMethod, - PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", - ILCode = IL.DisassembleMethod(patch.PatchMethod), - Priority = patch.priority, - IsEnabled = true, - HarmonyPatch = new HarmonyMethod(patch.PatchMethod), - HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) - }); - } - } - - - if (patchInfo.Finalizers != null) - { - foreach (var patch in patchInfo.Finalizers) - { - patchMethods.Add(new PatchMethodInfo - { - PatchType = "Finalizer", - PatchMethod = patch.PatchMethod, - PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", - ILCode = IL.DisassembleMethod(patch.PatchMethod), - Priority = patch.priority, - IsEnabled = true, - HarmonyPatch = new HarmonyMethod(patch.PatchMethod), - HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) - }); - } - } - } - - opPatchStates[methodKey] = patchMethods; - } - - var window = new ILViewerWindow(nextWindowId++, method, opIL, patchMethods); - ilViewerWindows[methodKey] = window; - } - catch (Exception e) - { - RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); - } - } - - private void RefreshPatchListInternal(MethodBase method, List storedPatches) - { - try - { - var currentPatchInfo = Harmony.GetPatchInfo(method); - var currentPatches = new List(); - - if (currentPatchInfo != null) - { - if (currentPatchInfo.Prefixes != null) - currentPatches.AddRange(currentPatchInfo.Prefixes.Select(p => p.PatchMethod)); - if (currentPatchInfo.Postfixes != null) - currentPatches.AddRange(currentPatchInfo.Postfixes.Select(p => p.PatchMethod)); - if (currentPatchInfo.Transpilers != null) - currentPatches.AddRange(currentPatchInfo.Transpilers.Select(p => p.PatchMethod)); - if (currentPatchInfo.Finalizers != null) - currentPatches.AddRange(currentPatchInfo.Finalizers.Select(p => p.PatchMethod)); - } - - foreach (var patch in storedPatches) - { - patch.IsEnabled = currentPatches.Contains(patch.PatchMethod as MethodInfo); - } - } - catch (Exception e) - { - RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); - } - } - - private void DrawILViewerWindow(int windowId, ILViewerWindow window) - { - GUILayout.BeginVertical(); - GUILayout.Label($"Method: {window.Method.DeclaringType?.FullName}.{window.Method.Name}"); - GUILayout.Label($"Parameters: {GetMethodParameters(window.Method)}"); - GUILayout.Label($"Return Type: {GetReturnType(window.Method)}"); - - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - - bool opSelected = window.CurrentView == ILViewMode.Original; - if (opSelected) GUI.color = Color.yellow; - if (GUILayout.Button("Original Method", GUILayout.Height(30))) - { - window.CurrentView = ILViewMode.Original; - window.ScrollPosition = Vector2.zero; - } - if (opSelected) GUI.color = Color.white; - - bool patchMethodsSelected = window.CurrentView == ILViewMode.PatchMethods; - if (patchMethodsSelected) GUI.color = Color.yellow; - if (GUILayout.Button($"Patch Manager ({window.PatchMethods.Count})", GUILayout.Height(30))) - { - window.CurrentView = ILViewMode.PatchMethods; - window.ScrollPosition = Vector2.zero; - } - if (patchMethodsSelected) GUI.color = Color.white; - - GUILayout.FlexibleSpace(); - - if (GUILayout.Button("Close", GUILayout.Width(60), GUILayout.Height(30))) - window.IsOpen = false; - - GUILayout.EndHorizontal(); - - GUILayout.Space(5); - - switch (window.CurrentView) - { - case ILViewMode.Original: - DrawOriginalMethodView(window); - break; - case ILViewMode.PatchMethods: - DrawPatchManagerView(window); - break; - } - - GUILayout.EndVertical(); - window.WindowRect = IMGUIUtils.DragResizeEat(windowId, window.WindowRect); - } - - private void DrawOriginalMethodView(ILViewerWindow window) - { - GUILayout.Label("Original Method IL Code:"); - window.ScrollPosition = GUILayout.BeginScrollView(window.ScrollPosition, GUILayoutShim.ExpandHeight(true)); - GUILayout.TextArea(window.OriginalIL); - GUILayout.EndScrollView(); - } - - private void DrawPatchManagerView(ILViewerWindow window) - { - if (window.PatchMethods.Count == 0) - { - GUILayout.Label("No patches found for this method."); - return; - } - - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(GUILayout.Width(400)); - GUILayout.Label("Patches:"); - - if (GUILayout.Button("Refresh Patch List", GUILayout.Height(25))) - RefreshPatchList(window); - - window.PatchListScrollPosition = GUILayout.BeginScrollView(window.PatchListScrollPosition, GUILayoutShim.ExpandHeight(true)); - - for (var i = 0; i < window.PatchMethods.Count; i++) - { - var patch = window.PatchMethods[i]; - - GUILayout.BeginVertical("box"); - GUILayout.BeginHorizontal(); - - bool newEnabled = GUILayout.Toggle(patch.IsEnabled, "", GUILayout.Width(20)); - if (newEnabled != patch.IsEnabled) - { - TogglePatch(window, i, newEnabled); - SearchPatches(); // not needed? - } - - GUILayout.BeginVertical(); - - bool isSelected = window.SelectedPatchIndex == i; - if (isSelected) GUI.color = Color.cyan; - - if (GUILayout.Button($"{patch.PatchType}: {patch.PatchMethod.DeclaringType?.Name}.{patch.PatchMethod.Name}", "label")) - { - window.SelectedPatchIndex = i; - window.ScrollPosition = Vector2.zero; - } - - if (isSelected) GUI.color = Color.white; - - GUILayout.Label($"Priority: {patch.Priority} | {patch.PatcherNamespace}"); - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - GUILayout.Space(2); - } - - GUILayout.EndScrollView(); - GUILayout.EndVertical(); - - GUILayout.BeginVertical(); - - if (window.SelectedPatchIndex >= 0 && window.SelectedPatchIndex < window.PatchMethods.Count) - { - var selectedPatch = window.PatchMethods[window.SelectedPatchIndex]; - GUILayout.Label($"IL Code for: {selectedPatch.PatchType} - {selectedPatch.PatchMethod.DeclaringType?.Name}.{selectedPatch.PatchMethod.Name}", GUI.skin.label); - - window.ScrollPosition = GUILayout.BeginScrollView(window.ScrollPosition, GUILayoutShim.ExpandHeight(true)); - - GUILayout.TextArea(selectedPatch.ILCode); - - GUILayout.EndScrollView(); - } - else - { - GUILayout.Label("Select a patch from the list to view its IL code.", GUI.skin.label); - } - - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - } - - private void AddPatchHInfo(Patches patchInfo, List patchMethods) - { - if (patchInfo.Prefixes != null) - { - foreach (var patch in patchInfo.Prefixes) - { - patchMethods.Add(new PatchMethodInfo - { - PatchType = "Prefix", - PatchMethod = patch.PatchMethod, - PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", - Priority = patch.priority, - IsEnabled = true, - HarmonyPatch = new HarmonyMethod(patch.PatchMethod), - HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) - }); - } - } - - if (patchInfo.Postfixes != null) - { - foreach (var patch in patchInfo.Postfixes) - { - patchMethods.Add(new PatchMethodInfo - { - PatchType = "Postfix", - PatchMethod = patch.PatchMethod, - PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", - Priority = patch.priority, - IsEnabled = true, - HarmonyPatch = new HarmonyMethod(patch.PatchMethod), - HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) - }); - } - } - if (patchInfo.Transpilers != null) - { - foreach (var patch in patchInfo.Transpilers) - { - patchMethods.Add(new PatchMethodInfo - { - PatchType = "Transpiler", - PatchMethod = patch.PatchMethod, - PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", - Priority = patch.priority, - IsEnabled = true, - HarmonyPatch = new HarmonyMethod(patch.PatchMethod), - HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) - }); - } - } - if (patchInfo.Finalizers != null) - { - foreach (var patch in patchInfo.Finalizers) - { - patchMethods.Add(new PatchMethodInfo - { - PatchType = "Finalizer", - PatchMethod = patch.PatchMethod, - PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", - Priority = patch.priority, - IsEnabled = true, - HarmonyPatch = new HarmonyMethod(patch.PatchMethod), - HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) - }); - } - } - } - - private void TogglePatchDirect(PatchInfo patch, bool enable) - { - try - { - string methodKey = GetMethodSignature(patch.TargetMethod); - - if (!opPatchStates.ContainsKey(methodKey)) - { - var patchMethods = new List(); - var harmonyPatchInfo = Harmony.GetPatchInfo(patch.TargetMethod); - - if (harmonyPatchInfo != null) - { - AddPatchHInfo(harmonyPatchInfo, patchMethods); - } - - opPatchStates[methodKey] = patchMethods; - } - - var patchMethodInfo = opPatchStates[methodKey].FirstOrDefault(e => e.PatchType == patch.PatchType && e.PatcherNamespace == patch.PatcherNamespace); - - if (patchMethodInfo != null) - { - var harmonyId = patchMethodInfo.HarmonyId ?? "harmony.patch.inspector.temp"; - var harmony = new Harmony(harmonyId); - - if (enable && !patchMethodInfo.IsEnabled) - { - switch (patchMethodInfo.PatchType) - { - case "Prefix": - harmony.Patch(patch.TargetMethod, prefix: patchMethodInfo.HarmonyPatch); - break; - case "Postfix": - harmony.Patch(patch.TargetMethod, postfix: patchMethodInfo.HarmonyPatch); - break; - case "Transpiler": - harmony.Patch(patch.TargetMethod, transpiler: patchMethodInfo.HarmonyPatch); - break; - case "Finalizer": - harmony.Patch(patch.TargetMethod, finalizer: patchMethodInfo.HarmonyPatch); - break; - } - - patchMethodInfo.IsEnabled = true; - patch.IsEnabled = true; - } - else if (!enable && patchMethodInfo.IsEnabled) - { - harmony.Unpatch(patch.TargetMethod, patchMethodInfo.PatchMethod as MethodInfo); - patchMethodInfo.IsEnabled = false; - patch.IsEnabled = false; - } - } - } - catch (Exception e) - { - RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); - } - } - - private void TogglePatch(ILViewerWindow window, int patchIndex, bool enable) - { - if (patchIndex < 0 || patchIndex >= window.PatchMethods.Count) - return; - - var patch = window.PatchMethods[patchIndex]; - - try - { - if (enable && !patch.IsEnabled) - { - var harmony = new Harmony(patch.HarmonyId ?? "harmony.patch.inspector.temp"); - - switch (patch.PatchType) - { - case "Prefix": - harmony.Patch(window.Method, prefix: patch.HarmonyPatch); - break; - case "Postfix": - harmony.Patch(window.Method, postfix: patch.HarmonyPatch); - break; - case "Transpiler": - harmony.Patch(window.Method, transpiler: patch.HarmonyPatch); - break; - case "Finalizer": - harmony.Patch(window.Method, finalizer: patch.HarmonyPatch); - break; - } - - patch.IsEnabled = true; - } - else if (!enable && patch.IsEnabled) - { - var harmony = new Harmony(patch.HarmonyId ?? "harmony.patch.inspector.temp"); - harmony.Unpatch(window.Method, patch.PatchMethod as MethodInfo); - patch.IsEnabled = false; - } - } - catch (Exception e) - { - patch.IsEnabled = !enable; - RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); - } - } - - private void RefreshPatchList(ILViewerWindow window) - { - try - { - var currentPatchInfo = Harmony.GetPatchInfo(window.Method); - var currentPatches = new List(); - - if (currentPatchInfo != null) - { - if (currentPatchInfo.Prefixes != null) - currentPatches.AddRange(currentPatchInfo.Prefixes.Select(p => p.PatchMethod)); - if (currentPatchInfo.Postfixes != null) - currentPatches.AddRange(currentPatchInfo.Postfixes.Select(p => p.PatchMethod)); - if (currentPatchInfo.Transpilers != null) - currentPatches.AddRange(currentPatchInfo.Transpilers.Select(p => p.PatchMethod)); - if (currentPatchInfo.Finalizers != null) - currentPatches.AddRange(currentPatchInfo.Finalizers.Select(p => p.PatchMethod)); - } - - foreach (var patch in window.PatchMethods) - { - patch.IsEnabled = currentPatches.Contains(patch.PatchMethod as MethodInfo); - } - } - catch (Exception e) - { - RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); - } - } - - private string GetHarmonyIdFromPatch(MethodBase patchMethod) - { - try - { - var assembly = patchMethod.DeclaringType?.Assembly; - if (assembly != null) - { - var bepinPluginAttr = assembly.GetCustomAttributes(typeof(BepInPlugin), false).FirstOrDefault() as BepInPlugin; - if (bepinPluginAttr != null) - { - return bepinPluginAttr.GUID; - } - - return assembly.GetName().Name; - } - - return patchMethod.DeclaringType?.FullName ?? "unknown.harmony.id"; - } - catch (Exception) - { - return "unknown.harmony.id"; - } - } - - private string GetMethodSignature(MethodBase method) - { - try - { - var parameters = method.GetParameters(); - var paramTypes = parameters.Select(p => p.ParameterType.Name).ToArray(); - var paramString = parameters.Length > 0 ? $"({string.Join(", ", paramTypes)})" : "()"; - return $"{method.DeclaringType?.FullName}.{method.Name}{paramString}"; - } - catch - { - return $"{method.DeclaringType?.FullName}.{method.Name}"; - } - } - - private string GetMethodParameters(MethodBase method) - { - try - { - var parameters = method.GetParameters(); - if (parameters.Length == 0) - return "()"; - - var paramStrings = parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"); - return $"({string.Join(", ", paramStrings.ToArray())})"; - } - catch - { - return "(unknown)"; - } - } - - private string GetReturnType(MethodBase method) - { - try - { - if (method is MethodInfo methodInfo) - return methodInfo.ReturnType.Name; - return "void"; - } - catch - { - return "Unknown"; - } - } - } + if (GUILayout.Button("Clear", GUILayout.Width(60))) + { + _searchInput = string.Empty; + _foundPatches.Clear(); + } + GUILayout.EndHorizontal(); + + _showFilePaths = GUILayout.Toggle(_showFilePaths, "Show file paths"); + + GUILayout.Space(10); + + if (_foundPatches.Count > 0) + { + GUILayout.Label($"Found {_foundPatches.Count} patches:"); + _scrollPos = GUILayout.BeginScrollView(_scrollPos, GUILayoutShim.ExpandHeight(true)); + + foreach (var patch in _foundPatches) + { + GUILayout.BeginVertical(); + Color bgColor = patch.IsEnabled ? Color.white : new Color(1f, 0.39f, 0.39f, 0.3f); + + GUILayout.BeginVertical("box"); + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + + GUI.color = bgColor; + GUILayout.Label($"Method: {patch.TargetType}.{patch.MethodName}"); + GUILayout.Label($"Patch Type: {patch.PatchType}"); + GUILayout.Label($"Patcher: {patch.PatcherNamespace}"); + GUILayout.Label($"Assembly: {patch.PatcherAssembly}"); + + if (_showFilePaths && !string.IsNullOrEmpty(patch.FilePath)) + { + GUILayout.Label($"File: {patch.FilePath}"); + } + + GUI.color = Color.white; + GUILayout.EndVertical(); + + GUILayout.BeginVertical(GUILayout.Width(80)); + + bool newEnabled = GUILayout.Toggle(patch.IsEnabled, "Enabled"); + if (newEnabled != patch.IsEnabled) + { + TogglePatchDirect(patch, newEnabled); + SearchPatches(); + return; + } + + if (GUILayout.Button("View IL", GUILayout.Height(25))) + { + OpenILViewer(patch.TargetMethod); + } + + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + GUILayout.EndVertical(); + GUILayout.Space(5); + } + + GUILayout.EndScrollView(); + } + else if (!string.IsNullOrEmpty(_searchInput)) + { + GUILayout.Label("No patches found."); + } + else + { + GUILayout.Label("Enter a method name, namespace, or type to search for patches."); + } + + GUILayout.Space(10); + GUILayout.EndVertical(); + } + + private void SearchPatches() + { + _foundPatches.Clear(); + + string searchTerm = _searchInput?.Trim(); + + if (string.IsNullOrEmpty(searchTerm)) + return; + + try + { + var searchCriteria = ParseSearchInput(searchTerm); + // Not entirely sure why I had this in the old project? PatchInspector does not own any Patches... + // var harmony = Harmony.CreateAndPatchAll(typeof(PatchInspector)); + var patchedMethods = Harmony.GetAllPatchedMethods(); + + foreach (var method in patchedMethods) + { + var patches = Harmony.GetPatchInfo(method); + if (patches == null) continue; + + if (!MatchesSearchCriteria(method, searchCriteria)) + continue; + + AddPatchesToList(patches.Prefixes.ToArray(), method, "Prefix"); + AddPatchesToList(patches.Postfixes.ToArray(), method, "Postfix"); + AddPatchesToList(patches.Transpilers.ToArray(), method, "Transpiler"); + AddPatchesToList(patches.Finalizers.ToArray(), method, "Finalizer"); + } + + _foundPatches = _foundPatches.OrderBy(info => info.TargetType).ThenBy(info => info.MethodName).ToList(); + } + catch (Exception ex) + { + // todo cleaner logging + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, ex); + } + } + + private static SearchCriteria ParseSearchInput(string input) + { + var criteria = new SearchCriteria(); + + if (input.Contains(":")) + { + criteria.IsStructured = true; + var parts = input.Split(' '); + + foreach (string part in parts) + { + if (part.StartsWith("method:", StringComparison.OrdinalIgnoreCase)) + criteria.Method = part.Substring(7); + else if (part.StartsWith("class:", StringComparison.OrdinalIgnoreCase)) + criteria.Class = part.Substring(6); + else if (part.StartsWith("type:", StringComparison.OrdinalIgnoreCase)) + criteria.Class = part.Substring(5); + else if (part.StartsWith("namespace:", StringComparison.OrdinalIgnoreCase)) + criteria.Namespace = part.Substring(10); + else if (!part.Contains(":")) + criteria.Text = part; + } + } + else + { + criteria.Text = input; + criteria.IsStructured = false; + } + + return criteria; + } + + private static bool MatchesSearchCriteria(MethodBase method, SearchCriteria criteria) + { + if (criteria.IsStructured) + { + bool matches = true; + + if (!string.IsNullOrEmpty(criteria.Method)) + matches &= method.Name.IndexOf(criteria.Method, StringComparison.OrdinalIgnoreCase) >= 0; + + if (!string.IsNullOrEmpty(criteria.Class)) + matches &= method.DeclaringType?.Name.IndexOf(criteria.Class, StringComparison.OrdinalIgnoreCase) >= 0 || + method.DeclaringType?.FullName?.IndexOf(criteria.Class, StringComparison.OrdinalIgnoreCase) >= 0; + + if (!string.IsNullOrEmpty(criteria.Namespace)) + matches &= method.DeclaringType?.Namespace?.IndexOf(criteria.Namespace, StringComparison.OrdinalIgnoreCase) >= 0; + + if (!string.IsNullOrEmpty(criteria.Text)) + { + string searchTerm = criteria.Text.ToLower(); + string fullMethodName = $"{method.DeclaringType?.FullName}.{method.Name}".ToLower(); + string methodWithParams = GetMethodSignature(method).ToLower(); + + matches &= method.Name.ToLower().Contains(searchTerm) || + method.DeclaringType?.Name.ToLower().Contains(searchTerm) == true || + method.DeclaringType?.Namespace?.ToLower().Contains(searchTerm) == true || + method.DeclaringType?.FullName?.ToLower().Contains(searchTerm) == true || + fullMethodName.Contains(searchTerm) || + methodWithParams.Contains(searchTerm); + } + + return matches; + } + else + { + string searchTerm = criteria.Text.ToLower(); + string fullMethodName = $"{method.DeclaringType?.FullName}.{method.Name}".ToLower(); + string methodWithParams = GetMethodSignature(method).ToLower(); + + return method.Name.ToLower().Contains(searchTerm) || + method.DeclaringType?.Name.ToLower().Contains(searchTerm) == true || + method.DeclaringType?.Namespace?.ToLower().Contains(searchTerm) == true || + method.DeclaringType?.FullName?.ToLower().Contains(searchTerm) == true || + fullMethodName.Contains(searchTerm) || + methodWithParams.Contains(searchTerm); + } + } + + private void AddPatchesToList(Patch[] patches, MethodBase targetMethod, string patchType) + { + if (patches == null) return; + foreach (var patch in patches) + { + var patchMethod = patch.PatchMethod; + var assembly = patchMethod.DeclaringType?.Assembly; + + var patchInfo = new PatchInfo + { + MethodName = targetMethod.Name, + TargetType = targetMethod.DeclaringType?.FullName ?? "Unknown", + PatcherAssembly = assembly?.GetName().Name ?? "Unknown", + PatchType = patchType, + FilePath = GetAssemblyFilePath(assembly), + PatcherNamespace = patchMethod.DeclaringType?.Namespace ?? "Unknown", + TargetMethod = targetMethod, + IsEnabled = true + }; + + _foundPatches.Add(patchInfo); + } + + string methodKey = GetMethodSignature(targetMethod); + if (_opPatchStates.TryGetValue(methodKey, out var storedPatches)) + { + foreach (var storedPatch in storedPatches) + { + if (!storedPatch.IsEnabled && storedPatch.PatchType == patchType) + { + bool alreadyAdded = _foundPatches.Any(fp => fp.TargetMethod == targetMethod && fp.PatchType == patchType && fp.PatcherNamespace == storedPatch.PatcherNamespace); + + if (!alreadyAdded) + { + var assembly = storedPatch.PatchMethod.DeclaringType?.Assembly; + var patchInfo = new PatchInfo + { + MethodName = targetMethod.Name, + TargetType = targetMethod.DeclaringType?.FullName ?? "Unknown", + PatcherAssembly = assembly?.GetName().Name ?? "Unknown", + PatchType = patchType, + FilePath = GetAssemblyFilePath(assembly), + PatcherNamespace = storedPatch.PatcherNamespace, + TargetMethod = targetMethod, + IsEnabled = false + }; + + _foundPatches.Add(patchInfo); + } + } + } + } + } + + private static string GetAssemblyFilePath(Assembly assembly) + { + try + { + if (assembly != null) + { + return assembly.Location; + } + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); + } + + return "Dynamic Assembly"; + } + + private void OpenILViewer(MethodBase method) + { + if (method == null) return; + + if (_ilViewerWindows.Any(w => w.Method == method)) + return; + + try + { + string opIL = IL.DisassembleMethod(method); + + string methodKey = GetMethodSignature(method); + if (_opPatchStates.TryGetValue(methodKey, out var patchMethods)) + { + RefreshPatchListInternal(method, patchMethods); + } + else + { + patchMethods = new List(); + var patchInfo = Harmony.GetPatchInfo(method); + + if (patchInfo != null) + { + if (patchInfo.Prefixes != null) + { + foreach (var patch in patchInfo.Prefixes) + { + patchMethods.Add(new PatchMethodInfo + { + PatchType = "Prefix", + PatchMethod = patch.PatchMethod, + PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", + ILCode = IL.DisassembleMethod(patch.PatchMethod), + Priority = patch.priority, + IsEnabled = true, + HarmonyPatch = new HarmonyMethod(patch.PatchMethod), + HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) + }); + } + } + + if (patchInfo.Postfixes != null) + { + foreach (var patch in patchInfo.Postfixes) + { + patchMethods.Add(new PatchMethodInfo + { + PatchType = "Postfix", + PatchMethod = patch.PatchMethod, + PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", + ILCode = IL.DisassembleMethod(patch.PatchMethod), + Priority = patch.priority, + IsEnabled = true, + HarmonyPatch = new HarmonyMethod(patch.PatchMethod), + HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) + }); + } + } + + + if (patchInfo.Transpilers != null) + { + foreach (var patch in patchInfo.Transpilers) + { + patchMethods.Add(new PatchMethodInfo + { + PatchType = "Transpiler", + PatchMethod = patch.PatchMethod, + PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", + ILCode = IL.DisassembleMethod(patch.PatchMethod), + Priority = patch.priority, + IsEnabled = true, + HarmonyPatch = new HarmonyMethod(patch.PatchMethod), + HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) + }); + } + } + + + if (patchInfo.Finalizers != null) + { + foreach (var patch in patchInfo.Finalizers) + { + patchMethods.Add(new PatchMethodInfo + { + PatchType = "Finalizer", + PatchMethod = patch.PatchMethod, + PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", + ILCode = IL.DisassembleMethod(patch.PatchMethod), + Priority = patch.priority, + IsEnabled = true, + HarmonyPatch = new HarmonyMethod(patch.PatchMethod), + HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) + }); + } + } + } + + _opPatchStates[methodKey] = patchMethods; + } + + var window = new ILViewerWindow(_nextWindowId++, method, opIL, patchMethods); + _ilViewerWindows.Add(window); + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); + } + } + + private static void RefreshPatchListInternal(MethodBase method, List storedPatches) + { + try + { + var currentPatchInfo = Harmony.GetPatchInfo(method); + var currentPatches = new List(); + + if (currentPatchInfo != null) + { + if (currentPatchInfo.Prefixes != null) + currentPatches.AddRange(currentPatchInfo.Prefixes.Select(p => p.PatchMethod)); + if (currentPatchInfo.Postfixes != null) + currentPatches.AddRange(currentPatchInfo.Postfixes.Select(p => p.PatchMethod)); + if (currentPatchInfo.Transpilers != null) + currentPatches.AddRange(currentPatchInfo.Transpilers.Select(p => p.PatchMethod)); + if (currentPatchInfo.Finalizers != null) + currentPatches.AddRange(currentPatchInfo.Finalizers.Select(p => p.PatchMethod)); + } + + foreach (var patch in storedPatches) + { + patch.IsEnabled = currentPatches.Contains(patch.PatchMethod as MethodInfo); + } + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); + } + } + + + private static void AddPatchHInfo(Patches patchInfo, List patchMethods) + { + if (patchInfo.Prefixes != null) + { + foreach (var patch in patchInfo.Prefixes) + { + patchMethods.Add(new PatchMethodInfo + { + PatchType = "Prefix", + PatchMethod = patch.PatchMethod, + PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", + Priority = patch.priority, + IsEnabled = true, + HarmonyPatch = new HarmonyMethod(patch.PatchMethod), + HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) + }); + } + } + + if (patchInfo.Postfixes != null) + { + foreach (var patch in patchInfo.Postfixes) + { + patchMethods.Add(new PatchMethodInfo + { + PatchType = "Postfix", + PatchMethod = patch.PatchMethod, + PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", + Priority = patch.priority, + IsEnabled = true, + HarmonyPatch = new HarmonyMethod(patch.PatchMethod), + HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) + }); + } + } + if (patchInfo.Transpilers != null) + { + foreach (var patch in patchInfo.Transpilers) + { + patchMethods.Add(new PatchMethodInfo + { + PatchType = "Transpiler", + PatchMethod = patch.PatchMethod, + PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", + Priority = patch.priority, + IsEnabled = true, + HarmonyPatch = new HarmonyMethod(patch.PatchMethod), + HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) + }); + } + } + if (patchInfo.Finalizers != null) + { + foreach (var patch in patchInfo.Finalizers) + { + patchMethods.Add(new PatchMethodInfo + { + PatchType = "Finalizer", + PatchMethod = patch.PatchMethod, + PatcherNamespace = patch.PatchMethod.DeclaringType?.Namespace ?? "Unknown", + Priority = patch.priority, + IsEnabled = true, + HarmonyPatch = new HarmonyMethod(patch.PatchMethod), + HarmonyId = GetHarmonyIdFromPatch(patch.PatchMethod) + }); + } + } + } + + private void TogglePatchDirect(PatchInfo patch, bool enable) + { + try + { + string methodKey = GetMethodSignature(patch.TargetMethod); + + if (!_opPatchStates.ContainsKey(methodKey)) + { + var patchMethods = new List(); + var harmonyPatchInfo = Harmony.GetPatchInfo(patch.TargetMethod); + + if (harmonyPatchInfo != null) + { + AddPatchHInfo(harmonyPatchInfo, patchMethods); + } + + _opPatchStates[methodKey] = patchMethods; + } + + var patchMethodInfo = _opPatchStates[methodKey].FirstOrDefault(e => e.PatchType == patch.PatchType && e.PatcherNamespace == patch.PatcherNamespace); + + if (patchMethodInfo != null) + { + var harmonyId = patchMethodInfo.HarmonyId ?? "harmony.patch.inspector.temp"; + var harmony = new Harmony(harmonyId); + + if (enable && !patchMethodInfo.IsEnabled) + { + switch (patchMethodInfo.PatchType) + { + case "Prefix": + harmony.Patch(patch.TargetMethod, prefix: patchMethodInfo.HarmonyPatch); + break; + case "Postfix": + harmony.Patch(patch.TargetMethod, postfix: patchMethodInfo.HarmonyPatch); + break; + case "Transpiler": + harmony.Patch(patch.TargetMethod, transpiler: patchMethodInfo.HarmonyPatch); + break; + case "Finalizer": + harmony.Patch(patch.TargetMethod, finalizer: patchMethodInfo.HarmonyPatch); + break; + } + + patchMethodInfo.IsEnabled = true; + patch.IsEnabled = true; + } + else if (!enable && patchMethodInfo.IsEnabled) + { + harmony.Unpatch(patch.TargetMethod, patchMethodInfo.PatchMethod as MethodInfo); + patchMethodInfo.IsEnabled = false; + patch.IsEnabled = false; + } + } + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); + } + } + + private static string GetHarmonyIdFromPatch(MethodBase patchMethod) + { + try + { + var assembly = patchMethod.DeclaringType?.Assembly; + if (assembly != null) + { + if (assembly.GetCustomAttributes(typeof(BepInPlugin), false).FirstOrDefault() is BepInPlugin bepinPluginAttr) + { + return bepinPluginAttr.GUID; + } + + return assembly.GetName().Name; + } + + return patchMethod.DeclaringType?.FullName ?? "unknown.harmony.id"; + } + catch (Exception) + { + return "unknown.harmony.id"; + } + } + + private static string GetMethodSignature(MethodBase method) + { + try + { + var parameters = method.GetParameters(); + var paramTypes = parameters.Select(p => p.ParameterType.Name).ToArray(); + var paramString = parameters.Length > 0 ? $"({string.Join(", ", paramTypes)})" : "()"; + return $"{method.DeclaringType?.FullName}.{method.Name}{paramString}"; + } + catch + { + return $"{method.DeclaringType?.FullName}.{method.Name}"; + } + } + } } \ No newline at end of file From 9d499c9c297eb55c440faa17fba5e084a8fedde8 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:57:29 +0200 Subject: [PATCH 2/3] Many improvements to PatchInspector window --- .../PatchInspector/ILViewerWindow.cs | 5 +- .../PatchInspector/PatchInfo.cs | 4 +- .../PatchInspector/PatchInspector.cs | 189 ++++++++++-------- RuntimeUnityEditor.Core/Utils/IMGUIUtils.cs | 48 ++++- 4 files changed, 161 insertions(+), 85 deletions(-) diff --git a/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs b/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs index ee9508f..54ef8e7 100644 --- a/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs +++ b/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs @@ -28,14 +28,12 @@ public ILViewerWindow(int windowId, MethodBase method, string originalIL, List

throw new InvalidOperationException("This window should not be initialized"); @@ -45,6 +43,7 @@ public ILViewerWindow(int windowId, MethodBase method, string originalIL, List

public class PatchInspector : Window { - private string _searchInput = String.Empty; - private Vector2 _scrollPos; - private List _foundPatches = new List(); - private bool _showFilePaths = true; - - private int _nextWindowId = 13000; + private readonly List _foundPatches = new List(); private readonly List _ilViewerWindows = new List(); private readonly Dictionary> _opPatchStates = new Dictionary>(); + private int _nextWindowId = 13000; + private Vector2 _scrollPos; + private string _searchInput = string.Empty; + + private string SearchInput + { + get => _searchInput; + set => _searchInput = value ?? string.Empty; + } + /// protected override void Initialize(InitSettings initSettings) { Enabled = false; DefaultScreenPosition = ScreenPartition.LeftUpper; DisplayName = "Patch Inspector"; - Title = "Patch Inspector"; + Title = "Harmony Patch Inspector"; } /// @@ -75,109 +84,125 @@ protected override void OnGUI() /// protected override void DrawContents() { - GUILayout.BeginVertical(); - - GUILayout.Label("Search for patches by method, class, or namespace:", GUI.skin.label); - GUILayout.Label("Examples: 'OnClick', 'method:OnClick class:AddButtonCtrl', 'namespace:SimpleGame'"); - - GUILayout.BeginHorizontal(); - GUILayout.Label("Search:", GUILayout.Width(60)); - - string newSearchInput = GUILayout.TextField(_searchInput); - if (newSearchInput != _searchInput) + //GUILayout.BeginVertical(); + GUILayout.BeginHorizontal(GUI.skin.box); { - _searchInput = newSearchInput; - SearchPatches(); - return; - } + GUILayout.Label("Search: ", IMGUIUtils.LayoutOptionsExpandWidthFalse); - // Replaced by upper OnValueChanged - /*if (GUILayout.Button("Search", GUILayout.Width(80))) - { - SearchPatches(); - }*/ + string newSearchInput = GUILayout.TextField(SearchInput, IMGUIUtils.LayoutOptionsExpandWidthTrue); + if (newSearchInput != SearchInput) + { + SearchInput = newSearchInput; + SearchPatches(); + return; + } - if (GUILayout.Button("Clear", GUILayout.Width(60))) - { - _searchInput = string.Empty; - _foundPatches.Clear(); + if (GUILayout.Button("Clear", IMGUIUtils.LayoutOptionsExpandWidthFalse)) + { + SearchInput = string.Empty; + _foundPatches.Clear(); + } } GUILayout.EndHorizontal(); - _showFilePaths = GUILayout.Toggle(_showFilePaths, "Show file paths"); - - GUILayout.Space(10); - if (_foundPatches.Count > 0) { - GUILayout.Label($"Found {_foundPatches.Count} patches:"); - _scrollPos = GUILayout.BeginScrollView(_scrollPos, GUILayoutShim.ExpandHeight(true)); + _scrollPos = GUILayout.BeginScrollView(_scrollPos, GUI.skin.box); + GUILayout.Label($"Found {_foundPatches.Count} patches", IMGUIUtils.UpperCenterLabelStyle); - foreach (var patch in _foundPatches) + for (var i = 0; i < _foundPatches.Count; i++) { - GUILayout.BeginVertical(); - Color bgColor = patch.IsEnabled ? Color.white : new Color(1f, 0.39f, 0.39f, 0.3f); + var patch = _foundPatches[i]; - GUILayout.BeginVertical("box"); - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(); + Color prevColor = GUI.color; + GUI.color = patch.IsEnabled ? Color.white : new Color(0.8f, 0.8f, 0.8f, 1f); - GUI.color = bgColor; - GUILayout.Label($"Method: {patch.TargetType}.{patch.MethodName}"); - GUILayout.Label($"Patch Type: {patch.PatchType}"); - GUILayout.Label($"Patcher: {patch.PatcherNamespace}"); - GUILayout.Label($"Assembly: {patch.PatcherAssembly}"); - - if (_showFilePaths && !string.IsNullOrEmpty(patch.FilePath)) + GUILayout.BeginVertical(GUI.skin.box); { - GUILayout.Label($"File: {patch.FilePath}"); - } + GUILayout.BeginHorizontal(); + { + bool newEnabled = GUILayout.Toggle(patch.IsEnabled, "", IMGUIUtils.LayoutOptionsExpandWidthFalse); + if (newEnabled != patch.IsEnabled) + TogglePatchDirect(patch, newEnabled); - GUI.color = Color.white; - GUILayout.EndVertical(); + if (GUILayout.Button( + new GUIContent($"Target: {patch.TargetType}.{patch.MethodName}", null, "Click to view IL and a full list of patches applied to this method. Right click for more options"), + GUI.skin.label, IMGUIUtils.LayoutOptionsExpandWidthTrue)) + { + if (IMGUIUtils.IsMouseRightClick()) + ContextMenu.Instance.Show(patch.TargetMethod, null, $"MethodInfo: {patch.TargetType}.{patch.MethodName}", null, null); + else + OpenILViewer(patch.TargetMethod); + } + } + GUILayout.EndHorizontal(); - GUILayout.BeginVertical(GUILayout.Width(80)); + var patchName = $"Patch: [{patch.PatchType}] {patch.Patch?.PatchMethod?.Name ?? "Not applied"}"; - bool newEnabled = GUILayout.Toggle(patch.IsEnabled, "Enabled"); - if (newEnabled != patch.IsEnabled) - { - TogglePatchDirect(patch, newEnabled); - SearchPatches(); - return; - } + if (patch.Patch == null) GUI.enabled = false; + if (GUILayout.Button(new GUIContent(patchName, null, "Click to see more information about the patch method. Right click for more options."), GUI.skin.label)) + { + if (IMGUIUtils.IsMouseRightClick()) + ContextMenu.Instance.Show(patch.Patch.PatchMethod, null, $"MethodInfo: {patch.Patch.PatchMethod?.DeclaringType?.Name}.{patch.Patch.PatchMethod?.Name}", null, null); + else + Inspector.Instance.Push(new InstanceStackEntry(patch.Patch, patchName), true); + } - if (GUILayout.Button("View IL", GUILayout.Height(25))) - { - OpenILViewer(patch.TargetMethod); - } + GUI.enabled = true; + if (GUILayout.Button(new GUIContent($"Patcher: {patch.PatcherNamespace}", null, "Click to search for types in this namespace."), GUI.skin.label)) + Inspector.Instance.Push(new InstanceStackEntry(AccessTools.AllTypes().Where(x => x.Namespace == patch.PatcherNamespace).ToArray(), "Types in " + patch.PatcherNamespace), true); + + if (GUILayout.Button( + new GUIContent($"Assembly: {patch.PatcherAssembly}", null, + $"File: {patch.FilePath}\n\nClick to open explorer focused on this file. Right click for to inspect the Assembly instance."), GUI.skin.label)) + { + if (IMGUIUtils.IsMouseRightClick()) + ContextMenu.Instance.Show(AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == patch.PatcherAssembly), null, "Assembly: " + patch.PatcherAssembly, + null, null); + else + { + try + { + if (!System.IO.File.Exists(patch.FilePath)) + throw new Exception("File does not exist on disk: " + (patch.FilePath ?? "NULL")); + + // Start explorer focused on the dll + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{patch.FilePath}\""); + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, "Failed to open explorer: " + e.Message); + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); + } + } + } + } GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - GUILayout.EndVertical(); - GUILayout.Space(5); + GUI.color = prevColor; } GUILayout.EndScrollView(); } - else if (!string.IsNullOrEmpty(_searchInput)) + else if (!string.IsNullOrEmpty(SearchInput)) { - GUILayout.Label("No patches found."); + GUILayout.Space(10); + GUILayout.Label("No patches found.", IMGUIUtils.UpperCenterLabelStyle); + GUILayout.FlexibleSpace(); } else { - GUILayout.Label("Enter a method name, namespace, or type to search for patches."); + GUILayout.Space(10); + GUILayout.Label("Use the search box to search for currently applied Harmony patches by method, class, or namespace.\n\nExamples: 'OnClick', 'method:OnClick class:AddButtonCtrl', 'namespace:SimpleGame'", IMGUIUtils.UpperCenterLabelStyle); + GUILayout.FlexibleSpace(); } - - GUILayout.Space(10); - GUILayout.EndVertical(); } private void SearchPatches() { _foundPatches.Clear(); - string searchTerm = _searchInput?.Trim(); + string searchTerm = SearchInput.Trim(); if (string.IsNullOrEmpty(searchTerm)) return; @@ -203,7 +228,14 @@ private void SearchPatches() AddPatchesToList(patches.Finalizers.ToArray(), method, "Finalizer"); } - _foundPatches = _foundPatches.OrderBy(info => info.TargetType).ThenBy(info => info.MethodName).ToList(); + _foundPatches.Sort((p1, p2) => + { + var c1 = string.Compare(p1.TargetType, p2.TargetType, StringComparison.Ordinal); + if (c1 != 0) + return c1; + + return string.Compare(p1.MethodName, p2.MethodName, StringComparison.Ordinal); + }); } catch (Exception ex) { @@ -301,6 +333,7 @@ private void AddPatchesToList(Patch[] patches, MethodBase targetMethod, string p var patchInfo = new PatchInfo { + Patch = patch, MethodName = targetMethod.Name, TargetType = targetMethod.DeclaringType?.FullName ?? "Unknown", PatcherAssembly = assembly?.GetName().Name ?? "Unknown", @@ -308,7 +341,7 @@ private void AddPatchesToList(Patch[] patches, MethodBase targetMethod, string p FilePath = GetAssemblyFilePath(assembly), PatcherNamespace = patchMethod.DeclaringType?.Namespace ?? "Unknown", TargetMethod = targetMethod, - IsEnabled = true + IsEnabled = true, }; _foundPatches.Add(patchInfo); diff --git a/RuntimeUnityEditor.Core/Utils/IMGUIUtils.cs b/RuntimeUnityEditor.Core/Utils/IMGUIUtils.cs index c9bf358..bcc365e 100644 --- a/RuntimeUnityEditor.Core/Utils/IMGUIUtils.cs +++ b/RuntimeUnityEditor.Core/Utils/IMGUIUtils.cs @@ -12,7 +12,7 @@ public static class IMGUIUtils /// Options with GUILayout.ExpandWidth(true). Useful for avoiding allocations. /// public static readonly GUILayoutOption[] LayoutOptionsExpandWidthTrue = { GUILayoutShim.ExpandWidth(true) }; - + ///

/// Options with GUILayout.ExpandWidth(false). Useful for avoiding allocations. /// @@ -20,6 +20,48 @@ public static class IMGUIUtils private static Texture2D SolidBoxTex { get; set; } + /// + /// Gets a Label configured to display text in MiddleCenter and stretch horizontally + vertically with wrapping. + /// + public static GUIStyle MiddleCenterLabelStyle + { + get + { + if (_middleCenterLabelStyle == null) + { + _middleCenterLabelStyle = GUI.skin.label.CreateCopy(); + _middleCenterLabelStyle.alignment = TextAnchor.MiddleCenter; + _middleCenterLabelStyle.stretchWidth = true; + _middleCenterLabelStyle.stretchHeight = true; + _middleCenterLabelStyle.wordWrap = true; + _middleCenterLabelStyle.richText = false; + } + return _middleCenterLabelStyle; + } + } + private static GUIStyle _middleCenterLabelStyle; + + /// + /// Gets a Label configured to display text in UpperCenter and stretch only horizontally with wrapping. + /// + public static GUIStyle UpperCenterLabelStyle + { + get + { + if (_upperCenterLabelStyle == null) + { + _upperCenterLabelStyle = GUI.skin.label.CreateCopy(); + _upperCenterLabelStyle.alignment = TextAnchor.UpperCenter; + _upperCenterLabelStyle.stretchWidth = true; + _upperCenterLabelStyle.stretchHeight = false; + _upperCenterLabelStyle.wordWrap = true; + _upperCenterLabelStyle.richText = false; + } + return _upperCenterLabelStyle; + } + } + private static GUIStyle _upperCenterLabelStyle; + /// /// Draw a gray non-transparent GUI.Box at the specified rect. Use before a GUI.Window or other controls to get rid of /// the default transparency and make the GUI easier to read. @@ -160,7 +202,7 @@ public static bool DrawButtonWithShadow(Rect r, GUIContent content, GUIStyle sty return result; } - + /// /// Draw a button with a shadow /// @@ -258,7 +300,7 @@ public static bool IsMouseWheelClick() { return Event.current.button == 2; } - + /// /// For use inside OnGUI to check if a GUI.Button was clicked with right mouse button. /// From 98aab7dd4e91bd2fa59fce7962e9e4dfda0a7c37 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:58:16 +0200 Subject: [PATCH 3/3] Clean up and improve ILViewerWindow --- .../PatchInspector/ILViewMode.cs | 8 - .../PatchInspector/ILViewerWindow.cs | 299 +++++++----------- .../PatchInspector/PatchInfo.cs | 2 +- .../PatchInspector/PatchInspector.cs | 97 +++++- RuntimeUnityEditor.Core/WindowManager.cs | 5 +- 5 files changed, 197 insertions(+), 214 deletions(-) delete mode 100644 RuntimeUnityEditor.Bepin5/PatchInspector/ILViewMode.cs diff --git a/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewMode.cs b/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewMode.cs deleted file mode 100644 index 6460397..0000000 --- a/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace RuntimeUnityEditor.Bepin5.PatchInspector -{ - internal enum ILViewMode - { - Original, - PatchMethods - } -} \ No newline at end of file diff --git a/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs b/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs index 54ef8e7..33c83a2 100644 --- a/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs +++ b/RuntimeUnityEditor.Bepin5/PatchInspector/ILViewerWindow.cs @@ -5,7 +5,11 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using RuntimeUnityEditor.Core.Inspector; +using RuntimeUnityEditor.Core.Inspector.Entries; +using RuntimeUnityEditor.Core.Utils; using UnityEngine; +using ContextMenu = RuntimeUnityEditor.Core.ContextMenu; namespace RuntimeUnityEditor.Bepin5.PatchInspector { @@ -17,10 +21,10 @@ internal sealed class ILViewerWindow : Window private readonly string _originalIL; private readonly List _patchMethods; - private Vector2 _scrollPosition; - private Vector2 _patchListScrollPosition; - private ILViewMode _currentView = ILViewMode.Original; - private int _selectedPatchIndex = -1; + private readonly GUIContent _headingGc; + + private Vector2 _patchesScrollPosition, _ilScrollPosition; + private int _selectedPatchIndex = -2; public ILViewerWindow(int windowId, MethodBase method, string originalIL, List patchMethods) { @@ -33,6 +37,8 @@ public ILViewerWindow(int windowId, MethodBase method, string originalIL, List

= 0 && _selectedPatchIndex < _patchMethods.Count) - { - var selectedPatch = _patchMethods[_selectedPatchIndex]; - GUILayout.Label($"IL Code for: {selectedPatch.PatchType} - {selectedPatch.PatchMethod.DeclaringType?.Name}.{selectedPatch.PatchMethod.Name}", GUI.skin.label); - - _scrollPosition = GUILayout.BeginScrollView(_scrollPosition, GUILayoutShim.ExpandHeight(true)); - - GUILayout.TextArea(selectedPatch.ILCode); - GUILayout.EndScrollView(); - } - else - { - GUILayout.Label("Select a patch from the list to view its IL code.", GUI.skin.label); - } - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - } + GUILayout.Space(5); + GUILayout.BeginVertical(GUI.skin.box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); + if (_selectedPatchIndex == -1) + { + GUILayout.Label("IL Code for the Original Method (target being patched)"); + _ilScrollPosition = GUILayout.BeginScrollView(_ilScrollPosition, GUILayoutShim.ExpandHeight(true)); + GUILayout.TextArea(_originalIL); + GUILayout.EndScrollView(); + } + else if (_selectedPatchIndex >= 0 && _selectedPatchIndex < _patchMethods.Count) + { + var selectedPatch = _patchMethods[_selectedPatchIndex]; - private void TogglePatch(int patchIndex, bool enable) - { - if (patchIndex < 0 || patchIndex >= _patchMethods.Count) - return; + GUILayout.BeginHorizontal(); + { + GUILayout.Space(3); - var patch = _patchMethods[patchIndex]; + var patchName = $"[{selectedPatch.PatchType}] {selectedPatch.PatchMethod.DeclaringType?.Name}.{selectedPatch.PatchMethod.Name}"; + GUILayout.Label($"IL Code for: {patchName}", IMGUIUtils.LayoutOptionsExpandWidthTrue); - try - { - if (enable && !patch.IsEnabled) - { - var harmony = new Harmony(patch.HarmonyId ?? "harmony.patch.inspector.temp"); + if (GUILayout.Button("Inspect", IMGUIUtils.LayoutOptionsExpandWidthFalse)) + Inspector.Instance.Push(new InstanceStackEntry(selectedPatch, patchName), true); - switch (patch.PatchType) - { - case "Prefix": - harmony.Patch(_method, prefix: patch.HarmonyPatch); - break; - case "Postfix": - harmony.Patch(_method, postfix: patch.HarmonyPatch); - break; - case "Transpiler": - harmony.Patch(_method, transpiler: patch.HarmonyPatch); - break; - case "Finalizer": - harmony.Patch(_method, finalizer: patch.HarmonyPatch); - break; + GUILayout.Space(3); } + GUILayout.EndHorizontal(); + + _ilScrollPosition = GUILayout.BeginScrollView(_ilScrollPosition, GUILayoutShim.ExpandHeight(true)); + + GUILayout.TextArea(selectedPatch.ILCode); - patch.IsEnabled = true; + GUILayout.EndScrollView(); } - else if (!enable && patch.IsEnabled) + else { - var harmony = new Harmony(patch.HarmonyId ?? "harmony.patch.inspector.temp"); - harmony.Unpatch(_method, patch.PatchMethod as MethodInfo); - patch.IsEnabled = false; + GUILayout.Label("Select a method from the list on the left to view its IL code.", IMGUIUtils.MiddleCenterLabelStyle); } + GUILayout.EndVertical(); } - catch (Exception e) - { - //patch.IsEnabled = !enable; - RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, $"Failed to {(enable ? "enable" : "disable")} Harmony patch {patch.HarmonyId ?? ""}:{patch.HarmonyPatch?.methodName}"); - RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); - } + GUILayout.EndHorizontal(); } private void RefreshPatchList() @@ -250,36 +202,5 @@ private void RefreshPatchList() RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); } } - - private static string GetMethodParameters(MethodBase method) - { - try - { - var parameters = method.GetParameters(); - if (parameters.Length == 0) - return "()"; - - var paramStrings = parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"); - return $"({string.Join(", ", paramStrings.ToArray())})"; - } - catch - { - return "(unknown)"; - } - } - - private static string GetReturnType(MethodBase method) - { - try - { - if (method is MethodInfo methodInfo) - return methodInfo.ReturnType.Name; - return "void"; - } - catch - { - return "Unknown"; - } - } } } \ No newline at end of file diff --git a/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInfo.cs b/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInfo.cs index 343a5d4..d3eaa35 100644 --- a/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInfo.cs +++ b/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInfo.cs @@ -6,7 +6,7 @@ namespace RuntimeUnityEditor.Bepin5.PatchInspector internal class PatchInfo { public Patch Patch; - public string MethodName; + public string TargetMethodName; public string TargetType; public string PatcherAssembly; public string PatcherNamespace; diff --git a/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInspector.cs b/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInspector.cs index 9aea9ca..4afbf50 100644 --- a/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInspector.cs +++ b/RuntimeUnityEditor.Bepin5/PatchInspector/PatchInspector.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using BepInEx; using HarmonyLib; using RuntimeUnityEditor.Core; @@ -9,6 +5,10 @@ using RuntimeUnityEditor.Core.Inspector.Entries; using RuntimeUnityEditor.Core.Utils; using RuntimeUnityEditor.Core.Utils.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using UnityEngine; using ContextMenu = RuntimeUnityEditor.Core.ContextMenu; @@ -46,11 +46,7 @@ protected override void Initialize(InitSettings initSettings) protected override void VisibleChanged(bool visible) { if (visible) - { - //_searchInput = string.Empty; - //_foundPatches.Clear(); SearchPatches(); - } foreach (var window in _ilViewerWindows) { @@ -74,6 +70,7 @@ protected override void OnGUI() else { _ilViewerWindows.RemoveAt(i); + WindowManager.AdditionalWindows.Remove(window); i--; } } @@ -84,7 +81,6 @@ protected override void OnGUI() /// protected override void DrawContents() { - //GUILayout.BeginVertical(); GUILayout.BeginHorizontal(GUI.skin.box); { GUILayout.Label("Search: ", IMGUIUtils.LayoutOptionsExpandWidthFalse); @@ -126,11 +122,11 @@ protected override void DrawContents() TogglePatchDirect(patch, newEnabled); if (GUILayout.Button( - new GUIContent($"Target: {patch.TargetType}.{patch.MethodName}", null, "Click to view IL and a full list of patches applied to this method. Right click for more options"), + new GUIContent($"Target: {patch.TargetType}.{patch.TargetMethodName}", null, "Click to view IL and a full list of patches applied to this method. Right click for more options"), GUI.skin.label, IMGUIUtils.LayoutOptionsExpandWidthTrue)) { if (IMGUIUtils.IsMouseRightClick()) - ContextMenu.Instance.Show(patch.TargetMethod, null, $"MethodInfo: {patch.TargetType}.{patch.MethodName}", null, null); + ContextMenu.Instance.Show(patch.TargetMethod, null, $"MethodInfo: {patch.TargetType}.{patch.TargetMethodName}", null, null); else OpenILViewer(patch.TargetMethod); } @@ -234,7 +230,7 @@ private void SearchPatches() if (c1 != 0) return c1; - return string.Compare(p1.MethodName, p2.MethodName, StringComparison.Ordinal); + return string.Compare(p1.TargetMethodName, p2.TargetMethodName, StringComparison.Ordinal); }); } catch (Exception ex) @@ -334,7 +330,7 @@ private void AddPatchesToList(Patch[] patches, MethodBase targetMethod, string p var patchInfo = new PatchInfo { Patch = patch, - MethodName = targetMethod.Name, + TargetMethodName = targetMethod.Name, TargetType = targetMethod.DeclaringType?.FullName ?? "Unknown", PatcherAssembly = assembly?.GetName().Name ?? "Unknown", PatchType = patchType, @@ -361,7 +357,7 @@ private void AddPatchesToList(Patch[] patches, MethodBase targetMethod, string p var assembly = storedPatch.PatchMethod.DeclaringType?.Assembly; var patchInfo = new PatchInfo { - MethodName = targetMethod.Name, + TargetMethodName = targetMethod.Name, TargetType = targetMethod.DeclaringType?.FullName ?? "Unknown", PatcherAssembly = assembly?.GetName().Name ?? "Unknown", PatchType = patchType, @@ -399,8 +395,12 @@ private void OpenILViewer(MethodBase method) { if (method == null) return; - if (_ilViewerWindows.Any(w => w.Method == method)) + var existing = _ilViewerWindows.Find(w => w.Method == method); + if (existing != null) + { + GUI.BringWindowToFront(existing.WindowId); return; + } try { @@ -498,6 +498,7 @@ private void OpenILViewer(MethodBase method) var window = new ILViewerWindow(_nextWindowId++, method, opIL, patchMethods); _ilViewerWindows.Add(window); + WindowManager.AdditionalWindows.Add(window); } catch (Exception e) { @@ -605,6 +606,72 @@ private static void AddPatchHInfo(Patches patchInfo, List patch } } + internal void TogglePatch(MethodBase targetMethod, PatchMethodInfo patch, bool enable) + { + try + { + string methodKey = GetMethodSignature(targetMethod); + + if (!_opPatchStates.ContainsKey(methodKey)) + { + var patchMethods = new List(); + var harmonyPatchInfo = Harmony.GetPatchInfo(targetMethod); + + if (harmonyPatchInfo != null) + { + AddPatchHInfo(harmonyPatchInfo, patchMethods); + } + + _opPatchStates[methodKey] = patchMethods; + } + + if (enable && !patch.IsEnabled) + { + var harmony = new Harmony(patch.HarmonyId ?? "harmony.patch.inspector.temp"); + + switch (patch.PatchType) + { + case "Prefix": + harmony.Patch(targetMethod, prefix: patch.HarmonyPatch); + break; + case "Postfix": + harmony.Patch(targetMethod, postfix: patch.HarmonyPatch); + break; + case "Transpiler": + harmony.Patch(targetMethod, transpiler: patch.HarmonyPatch); + break; + case "Finalizer": + harmony.Patch(targetMethod, finalizer: patch.HarmonyPatch); + break; + } + + patch.IsEnabled = true; + } + else if (!enable && patch.IsEnabled) + { + var harmony = new Harmony(patch.HarmonyId ?? "harmony.patch.inspector.temp"); + harmony.Unpatch(targetMethod, patch.PatchMethod as MethodInfo); + patch.IsEnabled = false; + } + } + catch (Exception e) + { + //patch.IsEnabled = !enable; + RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, $"Failed to {(enable ? "enable" : "disable")} Harmony patch {patch.HarmonyId ?? ""}:{patch.HarmonyPatch?.methodName}"); + RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e); + } + + var existing = _foundPatches.Find(fp => fp.TargetMethod == targetMethod && fp.PatchType == patch.PatchType && fp.PatcherNamespace == patch.PatcherNamespace); + if (existing != null) + { + existing.IsEnabled = patch.IsEnabled; + } + else + { + SearchPatches(); + } + } + private void TogglePatchDirect(PatchInfo patch, bool enable) { try diff --git a/RuntimeUnityEditor.Core/WindowManager.cs b/RuntimeUnityEditor.Core/WindowManager.cs index 496258e..a5cae3e 100644 --- a/RuntimeUnityEditor.Core/WindowManager.cs +++ b/RuntimeUnityEditor.Core/WindowManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using RuntimeUnityEditor.Core.REPL; using UnityEngine; @@ -76,10 +77,12 @@ public static Rect MakeDefaultWindowRect(Rect screenClientRect, ScreenPartition } } + internal static readonly List AdditionalWindows = new List(); + private static Rect EnsureVisible(Rect rect) { var result = rect; - var allWindows = RuntimeUnityEditorCore.Instance.InitializedFeatures.OfType(); + var allWindows = RuntimeUnityEditorCore.Instance.InitializedFeatures.OfType().Concat(AdditionalWindows); var allWindowsRects = allWindows.Select(w => w.WindowRect).ToList(); // Check if any window near this position, move the rect until it's not near any other window while (allWindowsRects.Any(r => Mathf.Abs(r.x - result.x) < 7 && Mathf.Abs(r.y - result.y) < 7))