diff --git a/src/Hooks/AddHookCell.cs b/src/Hooks/AddHookCell.cs index 390bca7..7c92bb9 100644 --- a/src/Hooks/AddHookCell.cs +++ b/src/Hooks/AddHookCell.cs @@ -16,14 +16,13 @@ namespace UnityExplorer.Hooks public float DefaultHeight => 30; public Text MethodNameLabel; - public Text HookedLabel; public ButtonRef HookButton; public int CurrentDisplayedIndex; private void OnHookClicked() { - HookManager.Instance.AddHookClicked(CurrentDisplayedIndex); + HookCreator.AddHookClicked(CurrentDisplayedIndex); } public void Enable() @@ -44,9 +43,6 @@ namespace UnityExplorer.Hooks UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 600); UIRoot.AddComponent().verticalFit = ContentSizeFitter.FitMode.PreferredSize; - HookedLabel = UIFactory.CreateLabel(UIRoot, "HookedLabel", "✓", TextAnchor.MiddleCenter, Color.green); - UIFactory.SetLayoutElement(HookedLabel.gameObject, minHeight: 25, minWidth: 100); - HookButton = UIFactory.CreateButton(UIRoot, "HookButton", "Hook", new Color(0.2f, 0.25f, 0.2f)); UIFactory.SetLayoutElement(HookButton.Component.gameObject, minHeight: 25, minWidth: 100); HookButton.OnClick += OnHookClicked; diff --git a/src/Hooks/GenericHookHandler.cs b/src/Hooks/GenericHookHandler.cs new file mode 100644 index 0000000..a0df2e6 --- /dev/null +++ b/src/Hooks/GenericHookHandler.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Widgets; +using UniverseLib.UI; +using UniverseLib.UI.Models; +using UniverseLib.UI.ObjectPool; +using UniverseLib.Utility; + +namespace UnityExplorer.Hooks +{ + public class GenericHookHandler + { + static GenericArgumentHandler[] handlers; + + static Type[] currentGenericParameters; + static Action currentOnSubmit; + static Action currentOnCancel; + + static Text Title; + static GameObject ArgsHolder; + + // UI + internal static GameObject UIRoot; + + public static void Show(Action onSubmit, Action onCancel, Type genericTypeDefinition) + { + Title.text = $"Setting generic arguments for {SignatureHighlighter.Parse(genericTypeDefinition, false)}..."; + + OnShow(onSubmit, onCancel, genericTypeDefinition.GetGenericArguments()); + } + + public static void Show(Action onSubmit, Action onCancel, MethodInfo genericMethodDefinition) + { + Title.text = $"Setting generic arguments for {SignatureHighlighter.HighlightMethod(genericMethodDefinition)}..."; + + OnShow(onSubmit, onCancel, genericMethodDefinition.GetGenericArguments()); + } + + static void OnShow(Action onSubmit, Action onCancel, Type[] genericParameters) + { + HookManagerPanel.Instance.SetPage(HookManagerPanel.Pages.GenericArgsSelector); + + currentOnSubmit = onSubmit; + currentOnCancel = onCancel; + + SetGenericParameters(genericParameters); + } + + static void SetGenericParameters(Type[] genericParameters) + { + currentGenericParameters = genericParameters; + + handlers = new GenericArgumentHandler[genericParameters.Length]; + for (int i = 0; i < genericParameters.Length; i++) + { + Type type = genericParameters[i]; + + GenericArgumentHandler holder = handlers[i] = Pool.Borrow(); + holder.UIRoot.transform.SetParent(ArgsHolder.transform, false); + holder.OnBorrowed(type); + } + } + + public static void TrySubmit() + { + Type[] args = new Type[currentGenericParameters.Length]; + + for (int i = 0; i < args.Length; i++) + { + GenericArgumentHandler handler = handlers[i]; + Type arg = handler.Evaluate(); + if (arg == null) + { + ExplorerCore.LogWarning($"Generic argument '{handler.inputField.Text}' is not a valid type."); + return; + } + args[i] = arg; + } + + OnClose(); + currentOnSubmit(args); + } + + public static void Cancel() + { + OnClose(); + + currentOnCancel(); + } + + static void OnClose() + { + foreach (GenericArgumentHandler widget in handlers) + { + widget.OnReturned(); + Pool.Return(widget); + } + handlers = null; + } + + // UI Construction + + internal void ConstructUI(GameObject parent) + { + UIRoot = UIFactory.CreateVerticalGroup(parent, "GenericArgsHandler", false, false, true, true, 5, new Vector4(5, 5, 5, 5), + childAlignment: TextAnchor.MiddleCenter); + UIFactory.SetLayoutElement(UIRoot, flexibleWidth: 9999, flexibleHeight: 9999); + + ButtonRef submitButton = UIFactory.CreateButton(UIRoot, "SubmitButton", "Submit", new Color(0.2f, 0.3f, 0.2f)); + UIFactory.SetLayoutElement(submitButton.GameObject, minHeight: 25, minWidth: 200); + submitButton.OnClick += TrySubmit; + + ButtonRef cancelButton = UIFactory.CreateButton(UIRoot, "CancelButton", "Cancel", new Color(0.3f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(cancelButton.GameObject, minHeight: 25, minWidth: 200); + cancelButton.OnClick += Cancel; + + Title = UIFactory.CreateLabel(UIRoot, "Title", "Generic Arguments", TextAnchor.MiddleCenter); + UIFactory.SetLayoutElement(Title.gameObject, minHeight: 25, flexibleWidth: 9999); + + GameObject scrollview = UIFactory.CreateScrollView(UIRoot, "GenericArgsScrollView", out ArgsHolder, out _, new(0.1f, 0.1f, 0.1f)); + UIFactory.SetLayoutElement(scrollview, flexibleWidth: 9999, flexibleHeight: 9999); + UIFactory.SetLayoutGroup(ArgsHolder, padTop: 5, padLeft: 5, padBottom: 5, padRight: 5); + } + } +} diff --git a/src/Hooks/HookCell.cs b/src/Hooks/HookCell.cs index f38d7ea..af558ef 100644 --- a/src/Hooks/HookCell.cs +++ b/src/Hooks/HookCell.cs @@ -1,5 +1,6 @@ using UnityEngine; using UnityEngine.UI; +using UnityExplorer.UI.Panels; using UniverseLib.UI; using UniverseLib.UI.Models; using UniverseLib.UI.Widgets.ScrollView; @@ -24,17 +25,18 @@ namespace UnityExplorer.Hooks private void OnToggleActiveClicked() { - HookManager.Instance.EnableOrDisableHookClicked(CurrentDisplayedIndex); + HookList.EnableOrDisableHookClicked(CurrentDisplayedIndex); } private void OnDeleteClicked() { - HookManager.Instance.DeleteHookClicked(CurrentDisplayedIndex); + HookList.DeleteHookClicked(CurrentDisplayedIndex); + HookCreator.AddHooksScrollPool.Refresh(true, false); } private void OnEditPatchClicked() { - HookManager.Instance.EditPatchClicked(CurrentDisplayedIndex); + HookList.EditPatchClicked(CurrentDisplayedIndex); } public GameObject CreateContent(GameObject parent) @@ -48,18 +50,18 @@ namespace UnityExplorer.Hooks MethodNameLabel = UIFactory.CreateLabel(UIRoot, "MethodName", "NOT SET", TextAnchor.MiddleLeft); UIFactory.SetLayoutElement(MethodNameLabel.gameObject, minHeight: 25, flexibleWidth: 9999); - ToggleActiveButton = UIFactory.CreateButton(UIRoot, "ToggleActiveBtn", "Enabled", new Color(0.15f, 0.2f, 0.15f)); - UIFactory.SetLayoutElement(ToggleActiveButton.Component.gameObject, minHeight: 25, minWidth: 100); + ToggleActiveButton = UIFactory.CreateButton(UIRoot, "ToggleActiveBtn", "On", new Color(0.15f, 0.2f, 0.15f)); + UIFactory.SetLayoutElement(ToggleActiveButton.Component.gameObject, minHeight: 25, minWidth: 35); ToggleActiveButton.OnClick += OnToggleActiveClicked; - DeleteButton = UIFactory.CreateButton(UIRoot, "DeleteButton", "Delete", new Color(0.2f, 0.15f, 0.15f)); - UIFactory.SetLayoutElement(DeleteButton.Component.gameObject, minHeight: 25, minWidth: 100); - DeleteButton.OnClick += OnDeleteClicked; - - EditPatchButton = UIFactory.CreateButton(UIRoot, "EditButton", "Edit Hook Source", new Color(0.15f, 0.15f, 0.15f)); - UIFactory.SetLayoutElement(EditPatchButton.Component.gameObject, minHeight: 25, minWidth: 150); + EditPatchButton = UIFactory.CreateButton(UIRoot, "EditButton", "Edit", new Color(0.15f, 0.15f, 0.15f)); + UIFactory.SetLayoutElement(EditPatchButton.Component.gameObject, minHeight: 25, minWidth: 35); EditPatchButton.OnClick += OnEditPatchClicked; + DeleteButton = UIFactory.CreateButton(UIRoot, "DeleteButton", "X", new Color(0.2f, 0.15f, 0.15f)); + UIFactory.SetLayoutElement(DeleteButton.Component.gameObject, minHeight: 25, minWidth: 35); + DeleteButton.OnClick += OnDeleteClicked; + return UIRoot; } diff --git a/src/Hooks/HookCreator.cs b/src/Hooks/HookCreator.cs new file mode 100644 index 0000000..0694e07 --- /dev/null +++ b/src/Hooks/HookCreator.cs @@ -0,0 +1,331 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.CSConsole; +using UnityExplorer.Runtime; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Widgets.AutoComplete; +using UniverseLib; +using UniverseLib.UI; +using UniverseLib.UI.Models; +using UniverseLib.UI.Widgets; +using UniverseLib.UI.Widgets.ScrollView; +using UniverseLib.Utility; + +namespace UnityExplorer.Hooks +{ + public class HookCreator : ICellPoolDataSource + { + public int ItemCount => filteredEligableMethods.Count; + + static readonly List currentAddEligableMethods = new(); + static readonly List filteredEligableMethods = new(); + + // hook editor + static readonly LexerBuilder Lexer = new(); + internal static HookInstance CurrentEditedHook; + + // Add Hooks UI + internal static GameObject AddHooksRoot; + internal static ScrollPool AddHooksScrollPool; + internal static Text AddHooksLabel; + internal static InputFieldRef AddHooksMethodFilterInput; + internal static InputFieldRef ClassSelectorInputField; + internal static Type pendingGenericDefinition; + internal static MethodInfo pendingGenericMethod; + + // Hook Source Editor UI + public static GameObject EditorRoot { get; private set; } + public static Text EditingHookLabel { get; private set; } + public static InputFieldScroller EditorInputScroller { get; private set; } + public static InputFieldRef EditorInput => EditorInputScroller.InputField; + public static Text EditorInputText { get; private set; } + public static Text EditorHighlightText { get; private set; } + + // ~~~~~~ New hook method selector ~~~~~~~ + + public void OnClassSelectedForHooks(string typeFullName) + { + Type type = ReflectionUtility.GetTypeByName(typeFullName); + if (type == null) + { + ExplorerCore.LogWarning($"Could not find any type by name {typeFullName}!"); + return; + } + if (type.IsGenericType) + { + pendingGenericDefinition = type; + GenericHookHandler.Show(OnGenericClassChosen, OnGenericClassCancel, type); + return; + } + + ShowMethodsForType(type); + } + + void ShowMethodsForType(Type type) + { + SetAddHooksLabelType(SignatureHighlighter.Parse(type, true)); + + AddHooksMethodFilterInput.Text = string.Empty; + + filteredEligableMethods.Clear(); + currentAddEligableMethods.Clear(); + foreach (MethodInfo method in type.GetMethods(ReflectionUtility.FLAGS)) + { + if (UERuntimeHelper.IsBlacklisted(method)) + continue; + currentAddEligableMethods.Add(method); + filteredEligableMethods.Add(method); + } + + AddHooksScrollPool.Refresh(true, true); + } + + void OnGenericClassChosen(Type[] genericArgs) + { + Type generic = pendingGenericDefinition.MakeGenericType(genericArgs); + ShowMethodsForType(generic); + HookManagerPanel.Instance.SetPage(HookManagerPanel.Pages.ClassMethodSelector); + } + + void OnGenericClassCancel() + { + pendingGenericDefinition = null; + HookManagerPanel.Instance.SetPage(HookManagerPanel.Pages.ClassMethodSelector); + } + + public void SetAddHooksLabelType(string typeText) + { + AddHooksLabel.text = $"Adding hooks to: {typeText}"; + + AddHooksMethodFilterInput.GameObject.SetActive(true); + AddHooksScrollPool.UIRoot.SetActive(true); + } + + public static void AddHookClicked(int index) + { + if (index >= filteredEligableMethods.Count) + return; + + MethodInfo method = filteredEligableMethods[index]; + if (!method.IsGenericMethod && HookList.hookedSignatures.Contains(method.FullDescription())) + { + ExplorerCore.Log($"Non-generic methods can only be hooked once."); + return; + } + else if (method.IsGenericMethod) + { + pendingGenericMethod = method; + GenericHookHandler.Show(OnGenericMethodChosen, OnGenericMethodCancel, method); + return; + } + + AddHook(filteredEligableMethods[index]); + } + + static void OnGenericMethodChosen(Type[] arguments) + { + MethodInfo generic = pendingGenericMethod.MakeGenericMethod(arguments); + AddHook(generic); + } + + static void OnGenericMethodCancel() + { + pendingGenericMethod = null; + HookManagerPanel.Instance.SetPage(HookManagerPanel.Pages.ClassMethodSelector); + } + + public static void AddHook(MethodInfo method) + { + HookManagerPanel.Instance.SetPage(HookManagerPanel.Pages.ClassMethodSelector); + + string sig = method.FullDescription(); + if (HookList.hookedSignatures.Contains(sig)) + { + ExplorerCore.LogWarning($"Method is already hooked!"); + return; + } + + HookInstance hook = new(method); + if (hook.Enabled) + { + HookList.hookedSignatures.Add(sig); + HookList.currentHooks.Add(sig, hook); + } + + AddHooksScrollPool.Refresh(true, false); + HookList.HooksScrollPool.Refresh(true, false); + } + + public void OnAddHookFilterInputChanged(string input) + { + filteredEligableMethods.Clear(); + + if (string.IsNullOrEmpty(input)) + filteredEligableMethods.AddRange(currentAddEligableMethods); + else + { + foreach (MethodInfo method in currentAddEligableMethods) + { + if (method.Name.ContainsIgnoreCase(input)) + filteredEligableMethods.Add(method); + } + } + + AddHooksScrollPool.Refresh(true, true); + } + + // Set eligable method cell + + public void OnCellBorrowed(AddHookCell cell) { } + + public void SetCell(AddHookCell cell, int index) + { + if (index >= filteredEligableMethods.Count) + { + cell.Disable(); + return; + } + + cell.CurrentDisplayedIndex = index; + MethodInfo method = filteredEligableMethods[index]; + + cell.MethodNameLabel.text = SignatureHighlighter.HighlightMethod(method); + } + + // ~~~~~~~~ Hook source editor ~~~~~~~~ + + internal static void SetEditedHook(HookInstance hook) + { + CurrentEditedHook = hook; + EditingHookLabel.text = $"Editing: {SignatureHighlighter.Parse(hook.TargetMethod.DeclaringType, false, hook.TargetMethod)}"; + EditorInput.Text = hook.PatchSourceCode; + } + + internal static void OnEditorInputChanged(string value) + { + EditorHighlightText.text = Lexer.BuildHighlightedString(value, 0, value.Length - 1, 0, EditorInput.Component.caretPosition, out _); + } + + internal static void EditorInputCancel() + { + CurrentEditedHook = null; + HookManagerPanel.Instance.SetPage(HookManagerPanel.Pages.ClassMethodSelector); + } + + internal static void EditorInputSave() + { + string input = EditorInput.Text; + bool wasEnabled = CurrentEditedHook.Enabled; + if (CurrentEditedHook.CompileAndGenerateProcessor(input)) + { + if (wasEnabled) + CurrentEditedHook.Patch(); + + CurrentEditedHook.PatchSourceCode = input; + CurrentEditedHook = null; + HookManagerPanel.Instance.SetPage(HookManagerPanel.Pages.ClassMethodSelector); + } + } + + // UI Construction + + internal void ConstructAddHooksView(GameObject rightGroup) + { + AddHooksRoot = UIFactory.CreateUIObject("AddHooksPanel", rightGroup); + UIFactory.SetLayoutElement(AddHooksRoot, flexibleHeight: 9999, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(AddHooksRoot, false, false, true, true); + + GameObject addRow = UIFactory.CreateHorizontalGroup(AddHooksRoot, "AddRow", false, true, true, true, 4, + new Vector4(2, 2, 2, 2), new Color(0.2f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(addRow, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + ClassSelectorInputField = UIFactory.CreateInputField(addRow, "ClassInput", "Enter a class to add hooks to..."); + UIFactory.SetLayoutElement(ClassSelectorInputField.Component.gameObject, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0); + new TypeCompleter(typeof(object), ClassSelectorInputField, true, false, Universe.Context != UniverseLib.Runtime.RuntimeContext.IL2CPP); + + ButtonRef addButton = UIFactory.CreateButton(addRow, "AddButton", "View Methods"); + UIFactory.SetLayoutElement(addButton.Component.gameObject, minWidth: 110, minHeight: 25); + addButton.OnClick += () => { OnClassSelectedForHooks(ClassSelectorInputField.Text); }; + + AddHooksLabel = UIFactory.CreateLabel(AddHooksRoot, "AddLabel", "Choose a class to begin...", TextAnchor.MiddleCenter); + UIFactory.SetLayoutElement(AddHooksLabel.gameObject, minHeight: 30, minWidth: 100, flexibleWidth: 9999); + + AddHooksMethodFilterInput = UIFactory.CreateInputField(AddHooksRoot, "FilterInputField", "Filter method names..."); + UIFactory.SetLayoutElement(AddHooksMethodFilterInput.Component.gameObject, minHeight: 30, flexibleWidth: 9999); + AddHooksMethodFilterInput.OnValueChanged += OnAddHookFilterInputChanged; + + AddHooksScrollPool = UIFactory.CreateScrollPool(AddHooksRoot, "MethodAddScrollPool", + out GameObject addScrollRoot, out GameObject addContent); + UIFactory.SetLayoutElement(addScrollRoot, flexibleHeight: 9999); + AddHooksScrollPool.Initialize(this); + + AddHooksMethodFilterInput.GameObject.SetActive(false); + AddHooksScrollPool.UIRoot.SetActive(false); + } + + public void ConstructEditor(GameObject parent) + { + EditorRoot = UIFactory.CreateUIObject("HookSourceEditor", parent); + UIFactory.SetLayoutElement(EditorRoot, flexibleHeight: 9999, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(EditorRoot, true, true, true, true, 2, 3, 3, 3, 3); + + EditingHookLabel = UIFactory.CreateLabel(EditorRoot, "EditingHookLabel", "NOT SET", TextAnchor.MiddleCenter); + EditingHookLabel.fontStyle = FontStyle.Bold; + UIFactory.SetLayoutElement(EditingHookLabel.gameObject, flexibleWidth: 9999, minHeight: 25); + + Text editorLabel = UIFactory.CreateLabel(EditorRoot, + "EditorLabel", + "* Accepted method names are Prefix, Postfix, Finalizer and Transpiler (can define multiple).\n" + + "* Your patch methods must be static.\n" + + "* Hooks are temporary! Copy the source into your IDE to avoid losing work if you wish to keep it!", + TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(editorLabel.gameObject, minHeight: 25, flexibleWidth: 9999); + + GameObject editorButtonRow = UIFactory.CreateHorizontalGroup(EditorRoot, "ButtonRow", false, false, true, true, 5); + UIFactory.SetLayoutElement(editorButtonRow, minHeight: 25, flexibleWidth: 9999); + + ButtonRef editorSaveButton = UIFactory.CreateButton(editorButtonRow, "DoneButton", "Save and Return", new Color(0.2f, 0.3f, 0.2f)); + UIFactory.SetLayoutElement(editorSaveButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999); + editorSaveButton.OnClick += EditorInputSave; + + ButtonRef editorDoneButton = UIFactory.CreateButton(editorButtonRow, "DoneButton", "Cancel and Return", new Color(0.2f, 0.2f, 0.2f)); + UIFactory.SetLayoutElement(editorDoneButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999); + editorDoneButton.OnClick += EditorInputCancel; + + int fontSize = 16; + GameObject inputObj = UIFactory.CreateScrollInputField(EditorRoot, "EditorInput", "", out InputFieldScroller inputScroller, fontSize); + EditorInputScroller = inputScroller; + EditorInput.OnValueChanged += OnEditorInputChanged; + + EditorInputText = EditorInput.Component.textComponent; + EditorInputText.supportRichText = false; + EditorInputText.color = Color.clear; + EditorInput.Component.customCaretColor = true; + EditorInput.Component.caretColor = Color.white; + EditorInput.PlaceholderText.fontSize = fontSize; + + // Lexer highlight text overlay + GameObject highlightTextObj = UIFactory.CreateUIObject("HighlightText", EditorInputText.gameObject); + RectTransform highlightTextRect = highlightTextObj.GetComponent(); + highlightTextRect.pivot = new Vector2(0, 1); + highlightTextRect.anchorMin = Vector2.zero; + highlightTextRect.anchorMax = Vector2.one; + highlightTextRect.offsetMin = Vector2.zero; + highlightTextRect.offsetMax = Vector2.zero; + + EditorHighlightText = highlightTextObj.AddComponent(); + EditorHighlightText.color = Color.white; + EditorHighlightText.supportRichText = true; + EditorHighlightText.fontSize = fontSize; + + // Set fonts + EditorInputText.font = UniversalUI.ConsoleFont; + EditorInput.PlaceholderText.font = UniversalUI.ConsoleFont; + EditorHighlightText.font = UniversalUI.ConsoleFont; + } + } +} diff --git a/src/Hooks/HookList.cs b/src/Hooks/HookList.cs new file mode 100644 index 0000000..bbb2500 --- /dev/null +++ b/src/Hooks/HookList.cs @@ -0,0 +1,94 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.UI.Panels; +using UniverseLib; +using UniverseLib.UI; +using UniverseLib.UI.Models; +using UniverseLib.UI.Widgets.ScrollView; +using UniverseLib.Utility; + +namespace UnityExplorer.Hooks +{ + public class HookList : ICellPoolDataSource + { + public int ItemCount => currentHooks.Count; + + internal static readonly HashSet hookedSignatures = new(); + internal static readonly OrderedDictionary currentHooks = new(); + + internal static GameObject UIRoot; + internal static ScrollPool HooksScrollPool; + + public static void EnableOrDisableHookClicked(int index) + { + HookInstance hook = (HookInstance)currentHooks[index]; + hook.TogglePatch(); + + HooksScrollPool.Refresh(true, false); + } + + public static void DeleteHookClicked(int index) + { + HookInstance hook = (HookInstance)currentHooks[index]; + + if (HookCreator.CurrentEditedHook == hook) + HookCreator.EditorInputCancel(); + + hook.Unpatch(); + currentHooks.RemoveAt(index); + hookedSignatures.Remove(hook.TargetMethod.FullDescription()); + + HooksScrollPool.Refresh(true, false); + } + + public static void EditPatchClicked(int index) + { + HookManagerPanel.Instance.SetPage(HookManagerPanel.Pages.HookSourceEditor); + HookInstance hook = (HookInstance)currentHooks[index]; + HookCreator.SetEditedHook(hook); + } + + // Set current hook cell + + public void OnCellBorrowed(HookCell cell) { } + + public void SetCell(HookCell cell, int index) + { + if (index >= currentHooks.Count) + { + cell.Disable(); + return; + } + + cell.CurrentDisplayedIndex = index; + HookInstance hook = (HookInstance)currentHooks[index]; + + cell.MethodNameLabel.text = SignatureHighlighter.HighlightMethod(hook.TargetMethod); + + cell.ToggleActiveButton.ButtonText.text = hook.Enabled ? "On" : "Off"; + RuntimeHelper.SetColorBlockAuto(cell.ToggleActiveButton.Component, + hook.Enabled ? new Color(0.15f, 0.2f, 0.15f) : new Color(0.2f, 0.2f, 0.15f)); + } + + // UI + + internal void ConstructUI(GameObject leftGroup) + { + UIRoot = UIFactory.CreateUIObject("CurrentHooksPanel", leftGroup); + UIFactory.SetLayoutElement(UIRoot, flexibleHeight: 9999, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(UIRoot, true, true, true, true); + + Text hooksLabel = UIFactory.CreateLabel(UIRoot, "HooksLabel", "Current Hooks", TextAnchor.MiddleCenter); + UIFactory.SetLayoutElement(hooksLabel.gameObject, minHeight: 30, flexibleWidth: 9999); + + HooksScrollPool = UIFactory.CreateScrollPool(UIRoot, "HooksScrollPool", + out GameObject hooksScroll, out GameObject hooksContent); + UIFactory.SetLayoutElement(hooksScroll, flexibleHeight: 9999); + HooksScrollPool.Initialize(this); + } + } +} diff --git a/src/Hooks/HookManager.cs b/src/Hooks/HookManager.cs deleted file mode 100644 index c3f291c..0000000 --- a/src/Hooks/HookManager.cs +++ /dev/null @@ -1,227 +0,0 @@ -using HarmonyLib; -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Reflection; -using UnityEngine; -using UnityExplorer.CSConsole; -using UnityExplorer.Runtime; -using UnityExplorer.UI; -using UnityExplorer.UI.Panels; -using UniverseLib; -using UniverseLib.UI.Widgets.ScrollView; -using UniverseLib.Utility; - -namespace UnityExplorer.Hooks -{ - public class HookManager : ICellPoolDataSource, ICellPoolDataSource - { - private static HookManager s_instance; - public static HookManager Instance => s_instance ?? (s_instance = new HookManager()); - - public HookManagerPanel Panel => UIManager.GetPanel(UIManager.Panels.HookManager); - - // This class acts as the data source for both current hooks and eligable methods when adding hooks. - // 'isAddingMethods' keeps track of which pool is currently the displayed one, so our ItemCount reflects the - // correct pool cells. - private bool isAddingMethods; - public int ItemCount => isAddingMethods ? filteredEligableMethods.Count : currentHooks.Count; - - // current hooks - private readonly HashSet hookedSignatures = new(); - private readonly OrderedDictionary currentHooks = new(); - - // adding hooks - private readonly List currentAddEligableMethods = new(); - private readonly List filteredEligableMethods = new(); - - // hook editor - private readonly LexerBuilder Lexer = new(); - private HookInstance currentEditedHook; - - // ~~~~~~~~~~~ Main Current Hooks window ~~~~~~~~~~~ - - public void EnableOrDisableHookClicked(int index) - { - HookInstance hook = (HookInstance)currentHooks[index]; - hook.TogglePatch(); - - Panel.HooksScrollPool.Refresh(true, false); - } - - public void DeleteHookClicked(int index) - { - HookInstance hook = (HookInstance)currentHooks[index]; - hook.Unpatch(); - currentHooks.RemoveAt(index); - hookedSignatures.Remove(hook.TargetMethod.FullDescription()); - - Panel.HooksScrollPool.Refresh(true, false); - } - - public void EditPatchClicked(int index) - { - Panel.SetPage(HookManagerPanel.Pages.HookSourceEditor); - HookInstance hook = (HookInstance)currentHooks[index]; - currentEditedHook = hook; - Panel.EditorInput.Text = hook.PatchSourceCode; - } - - // Set current hook cell - - public void OnCellBorrowed(HookCell cell) { } - - public void SetCell(HookCell cell, int index) - { - if (index >= this.currentHooks.Count) - { - cell.Disable(); - return; - } - - cell.CurrentDisplayedIndex = index; - HookInstance hook = (HookInstance)this.currentHooks[index]; - - cell.MethodNameLabel.text = SignatureHighlighter.HighlightMethod(hook.TargetMethod); - - cell.ToggleActiveButton.ButtonText.text = hook.Enabled ? "Enabled" : "Disabled"; - RuntimeHelper.SetColorBlockAuto(cell.ToggleActiveButton.Component, - hook.Enabled ? new Color(0.15f, 0.2f, 0.15f) : new Color(0.2f, 0.2f, 0.15f)); - } - - // ~~~~~~~~~~~ Add Hooks window ~~~~~~~~~~~ - - public void OnClassSelectedForHooks(string typeFullName) - { - Type type = ReflectionUtility.GetTypeByName(typeFullName); - if (type == null) - { - ExplorerCore.LogWarning($"Could not find any type by name {typeFullName}!"); - return; - } - - Panel.SetAddHooksLabelType(SignatureHighlighter.Parse(type, true)); - - Panel.ResetMethodFilter(); - filteredEligableMethods.Clear(); - currentAddEligableMethods.Clear(); - foreach (MethodInfo method in type.GetMethods(ReflectionUtility.FLAGS)) - { - if (method.IsGenericMethod || UERuntimeHelper.IsBlacklisted(method)) - continue; - currentAddEligableMethods.Add(method); - filteredEligableMethods.Add(method); - } - - isAddingMethods = true; - Panel.SetPage(HookManagerPanel.Pages.ClassMethodSelector); - Panel.AddHooksScrollPool.Refresh(true, true); - } - - public void DoneAddingHooks() - { - isAddingMethods = false; - Panel.SetPage(HookManagerPanel.Pages.CurrentHooks); - Panel.HooksScrollPool.Refresh(true, false); - } - - public void AddHookClicked(int index) - { - if (index >= this.filteredEligableMethods.Count) - return; - - AddHook(filteredEligableMethods[index]); - Panel.AddHooksScrollPool.Refresh(true, false); - } - - public void AddHook(MethodInfo method) - { - string sig = method.FullDescription(); - if (hookedSignatures.Contains(sig)) - return; - - HookInstance hook = new(method); - if (hook.Enabled) - { - hookedSignatures.Add(sig); - currentHooks.Add(sig, hook); - } - } - - public void OnAddHookFilterInputChanged(string input) - { - filteredEligableMethods.Clear(); - - if (string.IsNullOrEmpty(input)) - filteredEligableMethods.AddRange(currentAddEligableMethods); - else - { - foreach (MethodInfo method in currentAddEligableMethods) - { - if (method.Name.ContainsIgnoreCase(input)) - filteredEligableMethods.Add(method); - } - } - - Panel.AddHooksScrollPool.Refresh(true, true); - } - - // Set eligable method cell - - public void OnCellBorrowed(AddHookCell cell) { } - - public void SetCell(AddHookCell cell, int index) - { - if (index >= this.filteredEligableMethods.Count) - { - cell.Disable(); - return; - } - - cell.CurrentDisplayedIndex = index; - MethodInfo method = this.filteredEligableMethods[index]; - - cell.MethodNameLabel.text = SignatureHighlighter.HighlightMethod(method); - - string sig = method.FullDescription(); - if (hookedSignatures.Contains(sig)) - { - cell.HookButton.Component.gameObject.SetActive(false); - cell.HookedLabel.gameObject.SetActive(true); - } - else - { - cell.HookButton.Component.gameObject.SetActive(true); - cell.HookedLabel.gameObject.SetActive(false); - } - } - - // ~~~~~~~~~~~ Hook source editor window ~~~~~~~~~~~ - - public void OnEditorInputChanged(string value) - { - Panel.EditorHighlightText.text = Lexer.BuildHighlightedString(value, 0, value.Length - 1, 0, - Panel.EditorInput.Component.caretPosition, out _); - } - - public void EditorInputCancel() - { - currentEditedHook = null; - Panel.SetPage(HookManagerPanel.Pages.CurrentHooks); - } - - public void EditorInputSave() - { - string input = Panel.EditorInput.Text; - bool wasEnabled = currentEditedHook.Enabled; - if (currentEditedHook.CompileAndGenerateProcessor(input)) - { - if (wasEnabled) - currentEditedHook.Patch(); - currentEditedHook.PatchSourceCode = input; - currentEditedHook = null; - Panel.SetPage(HookManagerPanel.Pages.CurrentHooks); - } - } - } -} diff --git a/src/UI/Panels/HookManagerPanel.cs b/src/UI/Panels/HookManagerPanel.cs index 5716ead..d2d3d5d 100644 --- a/src/UI/Panels/HookManagerPanel.cs +++ b/src/UI/Panels/HookManagerPanel.cs @@ -1,7 +1,9 @@ -using UnityEngine; +using System; +using UnityEngine; using UnityEngine.UI; using UnityExplorer.Hooks; using UnityExplorer.UI.Widgets.AutoComplete; +using UniverseLib; using UniverseLib.UI; using UniverseLib.UI.Models; using UniverseLib.UI.Widgets; @@ -11,76 +13,58 @@ namespace UnityExplorer.UI.Panels { public class HookManagerPanel : UEPanel { + public static HookManagerPanel Instance { get; private set; } + public enum Pages { - CurrentHooks, ClassMethodSelector, - HookSourceEditor + HookSourceEditor, + GenericArgsSelector, } - public override UIManager.Panels PanelType => UIManager.Panels.HookManager; + internal static HookCreator hookCreator; + internal static HookList hookList; + internal static GenericHookHandler genericArgsHandler; + // Panel + public override UIManager.Panels PanelType => UIManager.Panels.HookManager; public override string Name => "Hooks"; public override bool ShowByDefault => false; - - public override int MinWidth => 500; - public override int MinHeight => 600; + public override int MinWidth => 750; + public override int MinHeight => 400; public override Vector2 DefaultAnchorMin => new(0.5f, 0.5f); public override Vector2 DefaultAnchorMax => new(0.5f, 0.5f); - public Pages CurrentPage { get; private set; } = Pages.CurrentHooks; - - private GameObject currentHooksPanel; - public ScrollPool HooksScrollPool; - private InputFieldRef classSelectorInputField; - - private GameObject addHooksPanel; - public ScrollPool AddHooksScrollPool; - private Text addHooksLabel; - private InputFieldRef AddHooksMethodFilterInput; - - private GameObject editorPanel; - - public InputFieldScroller EditorInputScroller { get; private set; } - public InputFieldRef EditorInput => EditorInputScroller.InputField; - public Text EditorInputText { get; private set; } - public Text EditorHighlightText { get; private set; } + public Pages CurrentPage { get; private set; } = Pages.ClassMethodSelector; public HookManagerPanel(UIBase owner) : base(owner) { } - private void OnClassInputAddClicked() - { - HookManager.Instance.OnClassSelectedForHooks(this.classSelectorInputField.Text); - } - - public void SetAddHooksLabelType(string typeText) => addHooksLabel.text = $"Adding hooks to: {typeText}"; - public void SetPage(Pages page) { switch (page) { - case Pages.CurrentHooks: - currentHooksPanel.SetActive(true); - addHooksPanel.SetActive(false); - editorPanel.SetActive(false); - break; case Pages.ClassMethodSelector: - currentHooksPanel.SetActive(false); - addHooksPanel.SetActive(true); - editorPanel.SetActive(false); + HookCreator.AddHooksRoot.SetActive(true); + HookCreator.EditorRoot.SetActive(false); + GenericHookHandler.UIRoot.SetActive(false); break; + case Pages.HookSourceEditor: - currentHooksPanel.SetActive(false); - addHooksPanel.SetActive(false); - editorPanel.SetActive(true); + HookCreator.AddHooksRoot.SetActive(false); + HookCreator.EditorRoot.SetActive(true); + GenericHookHandler.UIRoot.SetActive(false); + break; + + case Pages.GenericArgsSelector: + HookCreator.AddHooksRoot.SetActive(false); + HookCreator.EditorRoot.SetActive(false); + GenericHookHandler.UIRoot.SetActive(true); break; } } - public void ResetMethodFilter() => AddHooksMethodFilterInput.Text = string.Empty; - public override void SetDefaultSizeAndPosition() { base.SetDefaultSizeAndPosition(); @@ -91,115 +75,33 @@ namespace UnityExplorer.UI.Panels protected override void ConstructPanelContent() { - // ~~~~~~~~~ Active hooks scroll pool + Instance = this; + hookList = new(); + hookCreator = new(); + genericArgsHandler = new(); - currentHooksPanel = UIFactory.CreateUIObject("CurrentHooksPanel", this.ContentRoot); - UIFactory.SetLayoutElement(currentHooksPanel, flexibleHeight: 9999, flexibleWidth: 9999); - UIFactory.SetLayoutGroup(currentHooksPanel, true, true, true, true); + GameObject baseHoriGroup = UIFactory.CreateHorizontalGroup(ContentRoot, "HoriGroup", true, true, true, true); + UIFactory.SetLayoutElement(baseHoriGroup, flexibleWidth: 9999, flexibleHeight: 9999); - GameObject addRow = UIFactory.CreateHorizontalGroup(currentHooksPanel, "AddRow", false, true, true, true, 4, - new Vector4(2, 2, 2, 2), new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(addRow, minHeight: 30, flexibleWidth: 9999); + // Left Group - classSelectorInputField = UIFactory.CreateInputField(addRow, "ClassInput", "Enter a class to add hooks to..."); - UIFactory.SetLayoutElement(classSelectorInputField.Component.gameObject, flexibleWidth: 9999); - new TypeCompleter(typeof(object), classSelectorInputField, true, false); + GameObject leftGroup = UIFactory.CreateVerticalGroup(baseHoriGroup, "LeftGroup", true, true, true, true); + UIFactory.SetLayoutElement(leftGroup.gameObject, minWidth: 300, flexibleWidth: 9999, flexibleHeight: 9999); - ButtonRef addButton = UIFactory.CreateButton(addRow, "AddButton", "Add Hooks"); - UIFactory.SetLayoutElement(addButton.Component.gameObject, minWidth: 100, minHeight: 25); - addButton.OnClick += OnClassInputAddClicked; + hookList.ConstructUI(leftGroup); - Text hooksLabel = UIFactory.CreateLabel(currentHooksPanel, "HooksLabel", "Current Hooks", TextAnchor.MiddleCenter); - UIFactory.SetLayoutElement(hooksLabel.gameObject, minHeight: 30, flexibleWidth: 9999); + // Right Group - HooksScrollPool = UIFactory.CreateScrollPool(currentHooksPanel, "HooksScrollPool", - out GameObject hooksScroll, out GameObject hooksContent); - UIFactory.SetLayoutElement(hooksScroll, flexibleHeight: 9999); - HooksScrollPool.Initialize(HookManager.Instance); + GameObject rightGroup = UIFactory.CreateVerticalGroup(baseHoriGroup, "RightGroup", true, true, true, true); + UIFactory.SetLayoutElement(rightGroup, minWidth: 300, flexibleWidth: 9999, flexibleHeight: 9999); - // ~~~~~~~~~ Add hooks panel + hookCreator.ConstructAddHooksView(rightGroup); - addHooksPanel = UIFactory.CreateUIObject("AddHooksPanel", this.ContentRoot); - UIFactory.SetLayoutElement(addHooksPanel, flexibleHeight: 9999, flexibleWidth: 9999); - UIFactory.SetLayoutGroup(addHooksPanel, true, true, true, true); + hookCreator.ConstructEditor(rightGroup); + HookCreator.EditorRoot.SetActive(false); - addHooksLabel = UIFactory.CreateLabel(addHooksPanel, "AddLabel", "NOT SET", TextAnchor.MiddleCenter); - UIFactory.SetLayoutElement(addHooksLabel.gameObject, minHeight: 30, minWidth: 100, flexibleWidth: 9999); - - GameObject buttonRow = UIFactory.CreateHorizontalGroup(addHooksPanel, "ButtonRow", false, false, true, true, 5); - UIFactory.SetLayoutElement(buttonRow, minHeight: 25, flexibleWidth: 9999); - - ButtonRef doneButton = UIFactory.CreateButton(buttonRow, "DoneButton", "Done", new Color(0.2f, 0.3f, 0.2f)); - UIFactory.SetLayoutElement(doneButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999); - doneButton.OnClick += HookManager.Instance.DoneAddingHooks; - - AddHooksMethodFilterInput = UIFactory.CreateInputField(addHooksPanel, "FilterInputField", "Filter method names..."); - UIFactory.SetLayoutElement(AddHooksMethodFilterInput.Component.gameObject, minHeight: 30, flexibleWidth: 9999); - AddHooksMethodFilterInput.OnValueChanged += HookManager.Instance.OnAddHookFilterInputChanged; - - AddHooksScrollPool = UIFactory.CreateScrollPool(addHooksPanel, "MethodAddScrollPool", - out GameObject addScrollRoot, out GameObject addContent); - UIFactory.SetLayoutElement(addScrollRoot, flexibleHeight: 9999); - AddHooksScrollPool.Initialize(HookManager.Instance); - - addHooksPanel.gameObject.SetActive(false); - - // ~~~~~~~~~ Hook source editor panel - - editorPanel = UIFactory.CreateUIObject("HookSourceEditor", this.ContentRoot); - UIFactory.SetLayoutElement(editorPanel, flexibleHeight: 9999, flexibleWidth: 9999); - UIFactory.SetLayoutGroup(editorPanel, true, true, true, true); - - Text editorLabel = UIFactory.CreateLabel(editorPanel, - "EditorLabel", - "Edit Harmony patch source as desired. Accepted method names are Prefix, Postfix, Finalizer and Transpiler (can define multiple).\n\n" + - "Hooks are temporary! Please copy the source into your IDE to avoid losing work if you wish to keep it!", - TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(editorLabel.gameObject, minHeight: 25, flexibleWidth: 9999); - - GameObject editorButtonRow = UIFactory.CreateHorizontalGroup(editorPanel, "ButtonRow", false, false, true, true, 5); - UIFactory.SetLayoutElement(editorButtonRow, minHeight: 25, flexibleWidth: 9999); - - ButtonRef editorSaveButton = UIFactory.CreateButton(editorButtonRow, "DoneButton", "Save and Return", new Color(0.2f, 0.3f, 0.2f)); - UIFactory.SetLayoutElement(editorSaveButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999); - editorSaveButton.OnClick += HookManager.Instance.EditorInputSave; - - ButtonRef editorDoneButton = UIFactory.CreateButton(editorButtonRow, "DoneButton", "Cancel and Return", new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(editorDoneButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999); - editorDoneButton.OnClick += HookManager.Instance.EditorInputCancel; - - int fontSize = 16; - GameObject inputObj = UIFactory.CreateScrollInputField(editorPanel, "EditorInput", "", out InputFieldScroller inputScroller, fontSize); - EditorInputScroller = inputScroller; - EditorInput.OnValueChanged += HookManager.Instance.OnEditorInputChanged; - - EditorInputText = EditorInput.Component.textComponent; - EditorInputText.supportRichText = false; - EditorInputText.color = Color.clear; - EditorInput.Component.customCaretColor = true; - EditorInput.Component.caretColor = Color.white; - EditorInput.PlaceholderText.fontSize = fontSize; - - // Lexer highlight text overlay - GameObject highlightTextObj = UIFactory.CreateUIObject("HighlightText", EditorInputText.gameObject); - RectTransform highlightTextRect = highlightTextObj.GetComponent(); - highlightTextRect.pivot = new Vector2(0, 1); - highlightTextRect.anchorMin = Vector2.zero; - highlightTextRect.anchorMax = Vector2.one; - highlightTextRect.offsetMin = Vector2.zero; - highlightTextRect.offsetMax = Vector2.zero; - - EditorHighlightText = highlightTextObj.AddComponent(); - EditorHighlightText.color = Color.white; - EditorHighlightText.supportRichText = true; - EditorHighlightText.fontSize = fontSize; - - // Set fonts - EditorInputText.font = UniversalUI.ConsoleFont; - EditorInput.PlaceholderText.font = UniversalUI.ConsoleFont; - EditorHighlightText.font = UniversalUI.ConsoleFont; - - editorPanel.SetActive(false); + genericArgsHandler.ConstructUI(rightGroup); + GenericHookHandler.UIRoot.SetActive(false); } } }