From 371054d6df4a7cff166bf4dc6dc6aba40fa32271 Mon Sep 17 00:00:00 2001 From: Sinai <49360850+sinai-dev@users.noreply.github.com> Date: Tue, 7 Sep 2021 17:23:20 +1000 Subject: [PATCH] Add hook source editor for custom dynamic hooks --- src/Hooks/HookCell.cs | 2 +- src/Hooks/HookInstance.cs | 121 +++++++++------- src/Hooks/HookManager.cs | 225 ++++++++++++++++++------------ src/UI/Panels/HookManagerPanel.cs | 133 ++++++++++++++---- 4 files changed, 315 insertions(+), 166 deletions(-) diff --git a/src/Hooks/HookCell.cs b/src/Hooks/HookCell.cs index 7e1667c..4fab2b0 100644 --- a/src/Hooks/HookCell.cs +++ b/src/Hooks/HookCell.cs @@ -59,7 +59,7 @@ namespace UnityExplorer.Hooks UIFactory.SetLayoutElement(DeleteButton.Component.gameObject, minHeight: 25, minWidth: 100); DeleteButton.OnClick += OnDeleteClicked; - EditPatchButton = UIFactory.CreateButton(UIRoot, "EditButton", "Log Hook Source", new Color(0.15f, 0.15f, 0.15f)); + 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.OnClick += OnEditPatchClicked; diff --git a/src/Hooks/HookInstance.cs b/src/Hooks/HookInstance.cs index db4055d..a886bd4 100644 --- a/src/Hooks/HookInstance.cs +++ b/src/Hooks/HookInstance.cs @@ -21,18 +21,24 @@ namespace UnityExplorer.Hooks public bool Enabled; public MethodInfo TargetMethod; - public string GeneratedSource; + public string PatchSourceCode; - private string shortSignature; + private readonly string shortSignature; private PatchProcessor patchProcessor; - private HarmonyMethod patchDelegate; - private MethodInfo patchDelegateMethodInfo; + + private MethodInfo postfix; + private MethodInfo prefix; + private MethodInfo finalizer; + private MethodInfo transpiler; public HookInstance(MethodInfo targetMethod) { this.TargetMethod = targetMethod; this.shortSignature = $"{targetMethod.DeclaringType.Name}.{targetMethod.Name}"; - if (GenerateProcessorAndDelegate()) + + GenerateDefaultPatchSourceCode(targetMethod); + + if (CompileAndGenerateProcessor(PatchSourceCode)) Patch(); } @@ -41,30 +47,53 @@ namespace UnityExplorer.Hooks // TypeDefinition.Definition private static readonly PropertyInfo pi_Definition = ReflectionUtility.GetPropertyInfo(typeof(TypeDefinition), "Definition"); - private bool GenerateProcessorAndDelegate() + public bool CompileAndGenerateProcessor(string patchSource) { + Unpatch(); + try { patchProcessor = ExplorerCore.Harmony.CreateProcessor(TargetMethod); // Dynamically compile the patch method - scriptEvaluator.Run(GeneratePatchSourceCode(TargetMethod)); + var codeBuilder = new StringBuilder(); + + codeBuilder.AppendLine($"public class DynamicPatch_{DateTime.Now.Ticks}"); + codeBuilder.AppendLine("{"); + codeBuilder.AppendLine(patchSource); + codeBuilder.AppendLine("}"); + + scriptEvaluator.Run(codeBuilder.ToString()); if (ScriptEvaluator._reportPrinter.ErrorsCount > 0) throw new FormatException($"Unable to compile the generated patch!"); // TODO: Publicize MCS to avoid this reflection - // Get the last defined type in the source file - var typeContainer = ((CompilationSourceFile)fi_sourceFile.GetValue(scriptEvaluator)).Containers.Last(); - // Get the TypeSpec from the TypeDefinition, then get its "MetaInfo" (System.Type), then get the method called Patch. - this.patchDelegateMethodInfo = ((TypeSpec)pi_Definition.GetValue((Class)typeContainer, null)) - .GetMetaInfo() - .GetMethod("Patch", ReflectionUtility.FLAGS); + // Get the most recent Patch type in the source file + var typeContainer = ((CompilationSourceFile)fi_sourceFile.GetValue(scriptEvaluator)) + .Containers + .Last(it => it.MemberName.Name.StartsWith("DynamicPatch_")); + // Get the TypeSpec from the TypeDefinition, then get its "MetaInfo" (System.Type) + var patchClass = ((TypeSpec)pi_Definition.GetValue((Class)typeContainer, null)).GetMetaInfo(); - // Actually create the harmony patch - this.patchDelegate = new HarmonyMethod(patchDelegateMethodInfo); - patchProcessor.AddPostfix(patchDelegate); + // Create the harmony patches as defined + + postfix = patchClass.GetMethod("Postfix", ReflectionUtility.FLAGS); + if (postfix != null) + patchProcessor.AddPostfix(new HarmonyMethod(postfix)); + + prefix = patchClass.GetMethod("Prefix", ReflectionUtility.FLAGS); + if (prefix != null) + patchProcessor.AddPrefix(new HarmonyMethod(prefix)); + + finalizer = patchClass.GetMethod("Finalizer", ReflectionUtility.FLAGS); + if (finalizer != null) + patchProcessor.AddFinalizer(new HarmonyMethod(finalizer)); + + transpiler = patchClass.GetMethod("Transpiler", ReflectionUtility.FLAGS); + if (transpiler != null) + patchProcessor.AddTranspiler(new HarmonyMethod(transpiler)); return true; } @@ -75,16 +104,12 @@ namespace UnityExplorer.Hooks } } - private string GeneratePatchSourceCode(MethodInfo targetMethod) + private string GenerateDefaultPatchSourceCode(MethodInfo targetMethod) { var codeBuilder = new StringBuilder(); - - codeBuilder.AppendLine($"public class DynamicPatch_{DateTime.Now.Ticks}"); - codeBuilder.AppendLine("{"); - // Arguments - codeBuilder.Append(" public static void Patch(System.Reflection.MethodBase __originalMethod"); + codeBuilder.Append("public static void Postfix(System.Reflection.MethodBase __originalMethod"); if (!targetMethod.IsStatic) codeBuilder.Append($", {targetMethod.DeclaringType.FullName} __instance"); @@ -106,55 +131,53 @@ namespace UnityExplorer.Hooks // Patch body - codeBuilder.AppendLine(" {"); + codeBuilder.AppendLine("{"); - codeBuilder.AppendLine(" try {"); + codeBuilder.AppendLine(" try {"); // Log message var logMessage = new StringBuilder(); - logMessage.AppendLine($"$@\"Patch called: {shortSignature}"); - + logMessage.Append($"Patch called: {shortSignature}\\n"); + if (!targetMethod.IsStatic) - logMessage.AppendLine("__instance: {__instance.ToString()}"); + logMessage.Append("__instance: {__instance.ToString()}\\n"); paramIdx = 0; foreach (var param in parameters) { + logMessage.Append($"Parameter {paramIdx} {param.Name}: "); Type pType = param.ParameterType; if (pType.IsByRef) pType = pType.GetElementType(); if (pType.IsValueType) - logMessage.AppendLine($"Parameter {paramIdx} {param.Name}: {{__{paramIdx}.ToString()}}"); + logMessage.Append($"{{__{paramIdx}.ToString()}}"); else - logMessage.AppendLine($"Parameter {paramIdx} {param.Name}: {{__{paramIdx}?.ToString() ?? \"null\"}}"); + logMessage.Append($"{{__{paramIdx}?.ToString() ?? \"null\"}}"); + logMessage.Append("\\n"); paramIdx++; } if (targetMethod.ReturnType != typeof(void)) { + logMessage.Append("Return value: "); if (targetMethod.ReturnType.IsValueType) - logMessage.AppendLine("Return value: {__result.ToString()}"); + logMessage.Append("{__result.ToString()}"); else - logMessage.AppendLine("Return value: {__result?.ToString() ?? \"null\"}"); + logMessage.Append("{__result?.ToString() ?? \"null\"}"); + logMessage.Append("\\n"); } - logMessage.Append('"'); - - codeBuilder.AppendLine($" UnityExplorer.ExplorerCore.Log({logMessage});"); - codeBuilder.AppendLine(" }"); - codeBuilder.AppendLine(" catch (System.Exception ex) {"); - codeBuilder.AppendLine($" UnityExplorer.ExplorerCore.LogWarning($\"Exception in patch of {shortSignature}:\\n{{ex}}\");"); - codeBuilder.AppendLine(" }"); + codeBuilder.AppendLine($" UnityExplorer.ExplorerCore.Log($\"{logMessage}\");"); + codeBuilder.AppendLine(" }"); + codeBuilder.AppendLine(" catch (System.Exception ex) {"); + codeBuilder.AppendLine($" UnityExplorer.ExplorerCore.LogWarning($\"Exception in patch of {shortSignature}:\\n{{ex}}\");"); + codeBuilder.AppendLine(" }"); // End patch body - codeBuilder.AppendLine(" }"); - - // End class - codeBuilder.AppendLine("}"); - return GeneratedSource = codeBuilder.ToString(); + return PatchSourceCode = codeBuilder.ToString(); } public void TogglePatch() @@ -181,12 +204,16 @@ namespace UnityExplorer.Hooks public void Unpatch() { - if (!Enabled) - return; - try { - this.patchProcessor.Unpatch(patchDelegateMethodInfo); + if (prefix != null) + patchProcessor.Unpatch(prefix); + if (postfix != null) + patchProcessor.Unpatch(postfix); + if (finalizer != null) + patchProcessor.Unpatch(finalizer); + if (transpiler != null) + patchProcessor.Unpatch(transpiler); Enabled = false; } catch (Exception ex) diff --git a/src/Hooks/HookManager.cs b/src/Hooks/HookManager.cs index 25a80ad..96b3e1c 100644 --- a/src/Hooks/HookManager.cs +++ b/src/Hooks/HookManager.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Text; using HarmonyLib; using UnityEngine; +using UnityExplorer.CSConsole; using UnityExplorer.UI; using UnityExplorer.UI.Panels; using UnityExplorer.UI.Widgets; @@ -19,77 +20,25 @@ namespace UnityExplorer.Hooks public HookManagerPanel Panel => UIManager.GetPanel(UIManager.Panels.HookManager); - public int ItemCount => addingMethods ? currentFilteredMethods.Count : currentHooks.Count; + // 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; - private bool addingMethods; - private HashSet hookedSignatures = new HashSet(); + // current hooks + private readonly HashSet hookedSignatures = new HashSet(); private readonly OrderedDictionary currentHooks = new OrderedDictionary(); + + // adding hooks private readonly List currentAddEligableMethods = new List(); - private readonly List currentFilteredMethods = new List(); + private readonly List filteredEligableMethods = new List(); - public void OnClassSelectedForHooks(string typeFullName) - { - var type = ReflectionUtility.GetTypeByName(typeFullName); - if (type == null) - { - ExplorerCore.LogWarning($"Could not find any type by name {typeFullName}!"); - return; - } + // hook editor + private readonly LexerBuilder Lexer = new LexerBuilder(); + private HookInstance currentEditedHook; - Panel.SetAddHooksLabelType(SignatureHighlighter.Parse(type, true)); - - Panel.ResetMethodFilter(); - currentFilteredMethods.Clear(); - currentAddEligableMethods.Clear(); - foreach (var method in type.GetMethods(ReflectionUtility.FLAGS)) - { - if (method.IsGenericMethod || method.IsAbstract || ReflectionUtility.IsBlacklisted(method)) - continue; - currentAddEligableMethods.Add(method); - currentFilteredMethods.Add(method); - } - - addingMethods = true; - Panel.SetAddPanelActive(true); - Panel.MethodResultsScrollPool.Refresh(true, true); - } - - public void CloseAddHooks() - { - addingMethods = false; - Panel.SetAddPanelActive(false); - Panel.HooksScrollPool.Refresh(true, false); - } - - public void OnHookAllClicked() - { - foreach (var method in currentAddEligableMethods) - AddHook(method); - Panel.MethodResultsScrollPool.Refresh(true, false); - } - - public void AddHookClicked(int index) - { - if (index >= this.currentFilteredMethods.Count) - return; - - AddHook(currentFilteredMethods[index]); - Panel.MethodResultsScrollPool.Refresh(true, false); - } - - public void AddHook(MethodInfo method) - { - var sig = method.FullDescription(); - if (hookedSignatures.Contains(sig)) - return; - - var hook = new HookInstance(method); - if (hook.Enabled) - { - hookedSignatures.Add(sig); - currentHooks.Add(sig, hook); - } - } + // ~~~~~~~~~~~ Main Current Hooks window ~~~~~~~~~~~ public void EnableOrDisableHookClicked(int index) { @@ -111,32 +60,12 @@ namespace UnityExplorer.Hooks public void EditPatchClicked(int index) { + Panel.SetPage(HookManagerPanel.Pages.HookSourceEditor); var hook = (HookInstance)currentHooks[index]; - ExplorerCore.Log(hook.GeneratedSource); + currentEditedHook = hook; + Panel.EditorInput.Text = hook.PatchSourceCode; } - public void OnAddHookFilterInputChanged(string input) - { - currentFilteredMethods.Clear(); - - if (string.IsNullOrEmpty(input)) - currentFilteredMethods.AddRange(currentAddEligableMethods); - else - { - foreach (var method in currentAddEligableMethods) - { - if (method.Name.ContainsIgnoreCase(input)) - currentFilteredMethods.Add(method); - } - } - - Panel.MethodResultsScrollPool.Refresh(true, true); - } - - // OnBorrow methods not needed - public void OnCellBorrowed(HookCell cell) { } - public void OnCellBorrowed(AddHookCell cell) { } - // Set current hook cell public void SetCell(HookCell cell, int index) @@ -149,26 +78,136 @@ namespace UnityExplorer.Hooks cell.CurrentDisplayedIndex = index; var hook = (HookInstance)this.currentHooks[index]; - + cell.MethodNameLabel.text = HighlightMethod(hook.TargetMethod); cell.ToggleActiveButton.ButtonText.text = hook.Enabled ? "Enabled" : "Disabled"; - RuntimeProvider.Instance.SetColorBlockAuto(cell.ToggleActiveButton.Component, + RuntimeProvider.Instance.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) + { + var 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 (var method in type.GetMethods(ReflectionUtility.FLAGS)) + { + if (method.IsGenericMethod /* || method.IsAbstract */ || ReflectionUtility.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) + { + var sig = method.FullDescription(); + if (hookedSignatures.Contains(sig)) + return; + + var hook = new HookInstance(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 (var method in currentAddEligableMethods) + { + if (method.Name.ContainsIgnoreCase(input)) + filteredEligableMethods.Add(method); + } + } + + Panel.AddHooksScrollPool.Refresh(true, true); + } + + // ~~~~~~~~~~~ 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() + { + var 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); + } + } + + // OnBorrow methods not needed + public void OnCellBorrowed(HookCell cell) { } + public void OnCellBorrowed(AddHookCell cell) { } + + // Set eligable method cell public void SetCell(AddHookCell cell, int index) { - if (index >= this.currentFilteredMethods.Count) + if (index >= this.filteredEligableMethods.Count) { cell.Disable(); return; } cell.CurrentDisplayedIndex = index; - var method = this.currentFilteredMethods[index]; + var method = this.filteredEligableMethods[index]; cell.MethodNameLabel.text = HighlightMethod(method); diff --git a/src/UI/Panels/HookManagerPanel.cs b/src/UI/Panels/HookManagerPanel.cs index d6fd5db..d2160d5 100644 --- a/src/UI/Panels/HookManagerPanel.cs +++ b/src/UI/Panels/HookManagerPanel.cs @@ -13,6 +13,13 @@ namespace UnityExplorer.UI.Panels { public class HookManagerPanel : UIPanel { + public enum Pages + { + CurrentHooks, + ClassMethodSelector, + HookSourceEditor + } + public override UIManager.Panels PanelType => UIManager.Panels.HookManager; public override string Name => "Hooks"; @@ -20,14 +27,22 @@ namespace UnityExplorer.UI.Panels public override int MinHeight => 600; public override bool ShowByDefault => false; + public Pages CurrentPage { get; private set; } = Pages.CurrentHooks; + + private GameObject currentHooksPanel; public ScrollPool HooksScrollPool; - public ScrollPool MethodResultsScrollPool; + private InputFieldRef classSelectorInputField; private GameObject addHooksPanel; - private GameObject currentHooksPanel; - private InputFieldRef classInputField; + public ScrollPool AddHooksScrollPool; private Text addHooksLabel; - private InputFieldRef methodFilterInput; + 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 override string GetSaveDataFromConfigManager() => ConfigManager.HookManagerData.Value; @@ -35,22 +50,38 @@ namespace UnityExplorer.UI.Panels private void OnClassInputAddClicked() { - HookManager.Instance.OnClassSelectedForHooks(this.classInputField.Text); + HookManager.Instance.OnClassSelectedForHooks(this.classSelectorInputField.Text); } public void SetAddHooksLabelType(string typeText) => addHooksLabel.text = $"Adding hooks to: {typeText}"; - public void SetAddPanelActive(bool show) + public void SetPage(Pages page) { - addHooksPanel.SetActive(show); - currentHooksPanel.SetActive(!show); + 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); + break; + case Pages.HookSourceEditor: + currentHooksPanel.SetActive(false); + addHooksPanel.SetActive(false); + editorPanel.SetActive(true); + break; + } } - public void ResetMethodFilter() => methodFilterInput.Text = string.Empty; + public void ResetMethodFilter() => AddHooksMethodFilterInput.Text = string.Empty; public override void ConstructPanelContent() { - // Active hooks scroll pool + // ~~~~~~~~~ Active hooks scroll pool currentHooksPanel = UIFactory.CreateUIObject("CurrentHooksPanel", this.content); UIFactory.SetLayoutElement(currentHooksPanel, flexibleHeight: 9999, flexibleWidth: 9999); @@ -60,9 +91,9 @@ namespace UnityExplorer.UI.Panels new Vector4(2, 2, 2, 2), new Color(0.2f, 0.2f, 0.2f)); UIFactory.SetLayoutElement(addRow, minHeight: 30, flexibleWidth: 9999); - classInputField = UIFactory.CreateInputField(addRow, "ClassInput", "Enter a class to add hooks to..."); - UIFactory.SetLayoutElement(classInputField.Component.gameObject, flexibleWidth: 9999); - new TypeCompleter(typeof(object), classInputField, false, false); + 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); var addButton = UIFactory.CreateButton(addRow, "AddButton", "Add Hooks"); UIFactory.SetLayoutElement(addButton.Component.gameObject, minWidth: 100, minHeight: 25); @@ -76,7 +107,7 @@ namespace UnityExplorer.UI.Panels UIFactory.SetLayoutElement(hooksScroll, flexibleHeight: 9999); HooksScrollPool.Initialize(HookManager.Instance); - // Add hooks panel + // ~~~~~~~~~ Add hooks panel addHooksPanel = UIFactory.CreateUIObject("AddHooksPanel", this.content); UIFactory.SetLayoutElement(addHooksPanel, flexibleHeight: 9999, flexibleWidth: 9999); @@ -90,22 +121,75 @@ namespace UnityExplorer.UI.Panels var 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.CloseAddHooks; + doneButton.OnClick += HookManager.Instance.DoneAddingHooks; - var patchAllButton = UIFactory.CreateButton(buttonRow, "PatchAllButton", "Hook ALL methods in class", new Color(0.3f, 0.3f, 0.2f)); - UIFactory.SetLayoutElement(patchAllButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999); - patchAllButton.OnClick += HookManager.Instance.OnHookAllClicked; + AddHooksMethodFilterInput = UIFactory.CreateInputField(addHooksPanel, "FilterInputField", "Filter method names..."); + UIFactory.SetLayoutElement(AddHooksMethodFilterInput.Component.gameObject, minHeight: 30, flexibleWidth: 9999); + AddHooksMethodFilterInput.OnValueChanged += HookManager.Instance.OnAddHookFilterInputChanged; - methodFilterInput = UIFactory.CreateInputField(addHooksPanel, "FilterInputField", "Filter method names..."); - UIFactory.SetLayoutElement(methodFilterInput.Component.gameObject, minHeight: 30, flexibleWidth: 9999); - methodFilterInput.OnValueChanged += HookManager.Instance.OnAddHookFilterInputChanged; - - MethodResultsScrollPool = UIFactory.CreateScrollPool(addHooksPanel, "MethodAddScrollPool", + AddHooksScrollPool = UIFactory.CreateScrollPool(addHooksPanel, "MethodAddScrollPool", out GameObject addScrollRoot, out GameObject addContent); UIFactory.SetLayoutElement(addScrollRoot, flexibleHeight: 9999); - MethodResultsScrollPool.Initialize(HookManager.Instance); + AddHooksScrollPool.Initialize(HookManager.Instance); addHooksPanel.gameObject.SetActive(false); + + // ~~~~~~~~~ Hook source editor panel + + editorPanel = UIFactory.CreateUIObject("HookSourceEditor", this.content); + UIFactory.SetLayoutElement(editorPanel, flexibleHeight: 9999, flexibleWidth: 9999); + UIFactory.SetLayoutGroup(editorPanel, true, true, true, true); + + var 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); + + var editorButtonRow = UIFactory.CreateHorizontalGroup(editorPanel, "ButtonRow", false, false, true, true, 5); + UIFactory.SetLayoutElement(editorButtonRow, minHeight: 25, flexibleWidth: 9999); + + var 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; + + var 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; + var inputObj = UIFactory.CreateScrollInputField(editorPanel, "EditorInput", "", out var 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 + var highlightTextObj = UIFactory.CreateUIObject("HighlightText", EditorInputText.gameObject); + var 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 = UIManager.ConsoleFont; + EditorInput.PlaceholderText.font = UIManager.ConsoleFont; + EditorHighlightText.font = UIManager.ConsoleFont; + + editorPanel.SetActive(false); } protected internal override void DoSetDefaultPosAndAnchors() @@ -115,6 +199,5 @@ namespace UnityExplorer.UI.Panels this.Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, MinWidth); this.Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, MinHeight); } - } }