mirror of
https://github.com/GrahamKracker/UnityExplorer.git
synced 2025-07-14 07:37:06 +08:00
Implement HookManager UI and logic
This commit is contained in:
63
src/Hooks/AddHookCell.cs
Normal file
63
src/Hooks/AddHookCell.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.Hooks
|
||||
{
|
||||
public class AddHookCell : ICell
|
||||
{
|
||||
public bool Enabled => UIRoot.activeSelf;
|
||||
|
||||
public RectTransform Rect { get; set; }
|
||||
public GameObject UIRoot { get; set; }
|
||||
|
||||
public float DefaultHeight => 30;
|
||||
|
||||
public Text MethodNameLabel;
|
||||
public Text HookedLabel;
|
||||
public ButtonRef HookButton;
|
||||
|
||||
public int CurrentDisplayedIndex;
|
||||
|
||||
private void OnHookClicked()
|
||||
{
|
||||
HookManager.Instance.AddHookClicked(CurrentDisplayedIndex);
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
this.UIRoot.SetActive(true);
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
this.UIRoot.SetActive(false);
|
||||
}
|
||||
|
||||
public GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateUIObject(this.GetType().Name, parent, new Vector2(100, 30));
|
||||
Rect = UIRoot.GetComponent<RectTransform>();
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(UIRoot, false, false, true, true, 5, childAlignment: TextAnchor.UpperLeft);
|
||||
UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 600);
|
||||
UIRoot.AddComponent<ContentSizeFitter>().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;
|
||||
|
||||
MethodNameLabel = UIFactory.CreateLabel(UIRoot, "MethodName", "NOT SET", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(MethodNameLabel.gameObject, minHeight: 25, flexibleWidth: 9999);
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
}
|
||||
}
|
79
src/Hooks/HookCell.cs
Normal file
79
src/Hooks/HookCell.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.Hooks
|
||||
{
|
||||
public class HookCell : ICell
|
||||
{
|
||||
public bool Enabled => UIRoot.activeSelf;
|
||||
|
||||
public RectTransform Rect { get; set; }
|
||||
public GameObject UIRoot { get; set; }
|
||||
|
||||
public float DefaultHeight => 30;
|
||||
|
||||
public Text MethodNameLabel;
|
||||
public ButtonRef EditPatchButton;
|
||||
public ButtonRef ToggleActiveButton;
|
||||
public ButtonRef DeleteButton;
|
||||
|
||||
public int CurrentDisplayedIndex;
|
||||
|
||||
private void OnToggleActiveClicked()
|
||||
{
|
||||
HookManager.Instance.EnableOrDisableHookClicked(CurrentDisplayedIndex);
|
||||
}
|
||||
|
||||
private void OnDeleteClicked()
|
||||
{
|
||||
HookManager.Instance.DeleteHookClicked(CurrentDisplayedIndex);
|
||||
}
|
||||
|
||||
private void OnEditPatchClicked()
|
||||
{
|
||||
HookManager.Instance.EditPatchClicked(CurrentDisplayedIndex);
|
||||
}
|
||||
|
||||
public GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateUIObject(this.GetType().Name, parent, new Vector2(100, 30));
|
||||
Rect = UIRoot.GetComponent<RectTransform>();
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(UIRoot, false, false, true, true, 4, childAlignment: TextAnchor.UpperLeft);
|
||||
UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 600);
|
||||
UIRoot.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
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.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", "Log Patch Source", new Color(0.15f, 0.15f, 0.15f));
|
||||
UIFactory.SetLayoutElement(EditPatchButton.Component.gameObject, minHeight: 25, minWidth: 150);
|
||||
EditPatchButton.OnClick += OnEditPatchClicked;
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
UIRoot.SetActive(false);
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
UIRoot.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
193
src/Hooks/HookInstance.cs
Normal file
193
src/Hooks/HookInstance.cs
Normal file
@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.CodeDom.Compiler;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using HarmonyLib;
|
||||
using Microsoft.CSharp;
|
||||
using UnityExplorer.CSConsole;
|
||||
|
||||
namespace UnityExplorer.Hooks
|
||||
{
|
||||
public class HookInstance
|
||||
{
|
||||
private static readonly StringBuilder evalOutput = new StringBuilder();
|
||||
private static readonly ScriptEvaluator scriptEvaluator = new ScriptEvaluator(new StringWriter(evalOutput));
|
||||
|
||||
// Instance
|
||||
|
||||
public bool Enabled;
|
||||
public MethodInfo TargetMethod;
|
||||
public string GeneratedSource;
|
||||
|
||||
private string shortSignature;
|
||||
private PatchProcessor patchProcessor;
|
||||
private HarmonyMethod patchDelegate;
|
||||
private MethodInfo patchDelegateMethodInfo;
|
||||
|
||||
public HookInstance(MethodInfo targetMethod)
|
||||
{
|
||||
this.TargetMethod = targetMethod;
|
||||
this.shortSignature = $"{targetMethod.DeclaringType.Name}.{targetMethod.Name}";
|
||||
GenerateProcessorAndDelegate();
|
||||
Patch();
|
||||
}
|
||||
|
||||
private void GenerateProcessorAndDelegate()
|
||||
{
|
||||
try
|
||||
{
|
||||
patchProcessor = ExplorerCore.Harmony.CreateProcessor(TargetMethod);
|
||||
|
||||
// Dynamically compile the patch method
|
||||
|
||||
scriptEvaluator.Run(GeneratePatchSourceCode(TargetMethod));
|
||||
|
||||
// Get the compiled method and check for errors
|
||||
|
||||
string output = scriptEvaluator._textWriter.ToString();
|
||||
var outputSplit = output.Split('\n');
|
||||
if (outputSplit.Length >= 2)
|
||||
output = outputSplit[outputSplit.Length - 2];
|
||||
evalOutput.Clear();
|
||||
if (ScriptEvaluator._reportPrinter.ErrorsCount > 0)
|
||||
throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}");
|
||||
|
||||
// Could publicize MCS to avoid this reflection, but not bothering for now
|
||||
var source = (Mono.CSharp.CompilationSourceFile)ReflectionUtility.GetFieldInfo(typeof(Mono.CSharp.Evaluator), "source_file")
|
||||
.GetValue(scriptEvaluator);
|
||||
var type = (Mono.CSharp.Class)source.Containers.Last();
|
||||
var systemType = ((Mono.CSharp.TypeSpec)ReflectionUtility.GetPropertyInfo(typeof(Mono.CSharp.TypeDefinition), "Definition")
|
||||
.GetValue(type, null))
|
||||
.GetMetaInfo();
|
||||
|
||||
this.patchDelegateMethodInfo = systemType.GetMethod("Patch", ReflectionUtility.FLAGS);
|
||||
|
||||
// Actually create the harmony patch
|
||||
this.patchDelegate = new HarmonyMethod(patchDelegateMethodInfo);
|
||||
patchProcessor.AddPostfix(patchDelegate);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception creating patch processor for target method {TargetMethod.FullDescription()}!\r\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private string GeneratePatchSourceCode(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");
|
||||
|
||||
if (!targetMethod.IsStatic)
|
||||
codeBuilder.Append($", {targetMethod.DeclaringType.FullName} __instance");
|
||||
|
||||
if (targetMethod.ReturnType != typeof(void))
|
||||
codeBuilder.Append($", {targetMethod.ReturnType.FullName} __result");
|
||||
|
||||
int paramIdx = 0;
|
||||
var parameters = targetMethod.GetParameters();
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
codeBuilder.Append($", {param.ParameterType.FullName} __{paramIdx}");
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
codeBuilder.Append(")\n");
|
||||
|
||||
// Patch body
|
||||
|
||||
codeBuilder.AppendLine(" {");
|
||||
|
||||
codeBuilder.AppendLine(" try {");
|
||||
|
||||
// Log message
|
||||
|
||||
var logMessage = new StringBuilder();
|
||||
logMessage.AppendLine($"$@\"Patch called: {shortSignature}");
|
||||
|
||||
if (!targetMethod.IsStatic)
|
||||
logMessage.AppendLine("__instance: {__instance.ToString()}");
|
||||
|
||||
paramIdx = 0;
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
if (param.ParameterType.IsValueType)
|
||||
logMessage.AppendLine($"Parameter {paramIdx}: {{__{paramIdx}.ToString()}}");
|
||||
else
|
||||
logMessage.AppendLine($"Parameter {paramIdx}: {{__{paramIdx}?.ToString() ?? \"null\"}}");
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (targetMethod.ReturnType != typeof(void))
|
||||
{
|
||||
if (targetMethod.ReturnType.IsValueType)
|
||||
logMessage.AppendLine("Return value: {__result.ToString()}");
|
||||
else
|
||||
logMessage.AppendLine("Return value: {__result?.ToString() ?? \"null\"}");
|
||||
}
|
||||
|
||||
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(" }");
|
||||
|
||||
// End patch body
|
||||
|
||||
codeBuilder.AppendLine(" }");
|
||||
|
||||
// End class
|
||||
|
||||
codeBuilder.AppendLine("}");
|
||||
|
||||
return GeneratedSource = codeBuilder.ToString();
|
||||
}
|
||||
|
||||
public void TogglePatch()
|
||||
{
|
||||
Enabled = !Enabled;
|
||||
if (Enabled)
|
||||
Patch();
|
||||
else
|
||||
Unpatch();
|
||||
}
|
||||
|
||||
public void Patch()
|
||||
{
|
||||
try
|
||||
{
|
||||
patchProcessor.Patch();
|
||||
Enabled = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception hooking method!\r\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Unpatch()
|
||||
{
|
||||
if (!Enabled)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
this.patchProcessor.Unpatch(patchDelegateMethodInfo);
|
||||
Enabled = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception unpatching method: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
233
src/Hooks/HookManager.cs
Normal file
233
src/Hooks/HookManager.cs
Normal file
@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Panels;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.Hooks
|
||||
{
|
||||
public class HookManager : ICellPoolDataSource<HookCell>, ICellPoolDataSource<AddHookCell>
|
||||
{
|
||||
private static HookManager s_instance;
|
||||
public static HookManager Instance => s_instance ?? (s_instance = new HookManager());
|
||||
|
||||
public HookManagerPanel Panel => UIManager.GetPanel<HookManagerPanel>(UIManager.Panels.HookManager);
|
||||
|
||||
public int ItemCount => addingMethods ? currentFilteredMethods.Count : currentHooks.Count;
|
||||
|
||||
private bool addingMethods;
|
||||
private HashSet<string> hookedSignatures = new HashSet<string>();
|
||||
private readonly OrderedDictionary currentHooks = new OrderedDictionary();
|
||||
private readonly List<MethodInfo> currentAddEligableMethods = new List<MethodInfo>();
|
||||
private readonly List<MethodInfo> currentFilteredMethods = new List<MethodInfo>();
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public void EnableOrDisableHookClicked(int index)
|
||||
{
|
||||
var hook = (HookInstance)currentHooks[index];
|
||||
hook.TogglePatch();
|
||||
|
||||
Panel.HooksScrollPool.Refresh(true, false);
|
||||
}
|
||||
|
||||
public void DeleteHookClicked(int index)
|
||||
{
|
||||
var hook = (HookInstance)currentHooks[index];
|
||||
hook.Unpatch();
|
||||
currentHooks.RemoveAt(index);
|
||||
hookedSignatures.Remove(hook.TargetMethod.FullDescription());
|
||||
|
||||
Panel.HooksScrollPool.Refresh(true, false);
|
||||
}
|
||||
|
||||
public void EditPatchClicked(int index)
|
||||
{
|
||||
var hook = (HookInstance)currentHooks[index];
|
||||
ExplorerCore.Log(hook.GeneratedSource);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (index >= this.currentHooks.Count)
|
||||
{
|
||||
cell.Disable();
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
hook.Enabled ? new Color(0.15f, 0.2f, 0.15f) : new Color(0.2f, 0.2f, 0.15f));
|
||||
}
|
||||
|
||||
// Set eligable method cell
|
||||
|
||||
public void SetCell(AddHookCell cell, int index)
|
||||
{
|
||||
if (index >= this.currentFilteredMethods.Count)
|
||||
{
|
||||
cell.Disable();
|
||||
return;
|
||||
}
|
||||
|
||||
cell.CurrentDisplayedIndex = index;
|
||||
var method = this.currentFilteredMethods[index];
|
||||
|
||||
cell.MethodNameLabel.text = HighlightMethod(method);
|
||||
|
||||
var 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);
|
||||
}
|
||||
}
|
||||
|
||||
// private static readonly string VOID_HIGHLIGHT = $"<color=#{SignatureHighlighter.keywordBlueHex}>void</color> ";
|
||||
|
||||
private static readonly Dictionary<string, string> highlightedMethods = new Dictionary<string, string>();
|
||||
|
||||
private string HighlightMethod(MethodInfo method)
|
||||
{
|
||||
var sig = method.FullDescription();
|
||||
if (highlightedMethods.ContainsKey(sig))
|
||||
return highlightedMethods[sig];
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// declaring type
|
||||
sb.Append(SignatureHighlighter.Parse(method.DeclaringType, false));
|
||||
sb.Append('.');
|
||||
|
||||
// method name
|
||||
var color = !method.IsStatic
|
||||
? SignatureHighlighter.METHOD_INSTANCE
|
||||
: SignatureHighlighter.METHOD_STATIC;
|
||||
sb.Append($"<color={color}>{method.Name}</color>");
|
||||
|
||||
// arguments
|
||||
sb.Append('(');
|
||||
var args = method.GetParameters();
|
||||
if (args != null && args.Any())
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var param in args)
|
||||
{
|
||||
sb.Append(SignatureHighlighter.Parse(param.ParameterType, false));
|
||||
sb.Append(' ');
|
||||
sb.Append($"<color={SignatureHighlighter.LOCAL_ARG}>{param.Name}</color>");
|
||||
i++;
|
||||
if (i < args.Length)
|
||||
sb.Append(", ");
|
||||
}
|
||||
}
|
||||
sb.Append(')');
|
||||
|
||||
var ret = sb.ToString();
|
||||
highlightedMethods.Add(sig, ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user