From 2107df70ad1891e945914a05c09a224d3e7c3827 Mon Sep 17 00:00:00 2001 From: Sinai Date: Thu, 25 Mar 2021 18:39:35 +1100 Subject: [PATCH] Improvements to CS Console * Errors are now logged properly. * Can now define classes, methods, etc - no longer has to be an expression body. * Added `StartCoroutine(IEnumerator routine)` helper method to easily run a Coroutine * Disabling suggestions now properly stops Explorer trying to update suggestion cache instead of just not showing them. In the rare cases that suggestions cause a crash, disabling them will now prevent those crashes. * Various other misc improvements behind the scenes --- src/Core/CSharp/DummyBehaviour.cs | 29 ++++ src/Core/CSharp/ScriptEvaluator.cs | 14 +- src/Core/CSharp/ScriptInteraction.cs | 12 +- src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs | 156 ++++++++++++++++++++ src/Core/Runtime/Il2Cpp/Il2CppProvider.cs | 6 + src/Core/Runtime/Mono/MonoProvider.cs | 15 ++ src/Core/Runtime/RuntimeProvider.cs | 3 + src/UI/Main/CSConsole/AutoCompleter.cs | 2 +- src/UI/Main/CSConsole/CSLexerHighlighter.cs | 14 +- src/UI/Main/CSConsole/CSharpConsole.cs | 92 +++++++----- src/UnityExplorer.csproj | 2 + 11 files changed, 289 insertions(+), 56 deletions(-) create mode 100644 src/Core/CSharp/DummyBehaviour.cs create mode 100644 src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs diff --git a/src/Core/CSharp/DummyBehaviour.cs b/src/Core/CSharp/DummyBehaviour.cs new file mode 100644 index 0000000..3aed5ea --- /dev/null +++ b/src/Core/CSharp/DummyBehaviour.cs @@ -0,0 +1,29 @@ +#if MONO +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace UnityExplorer.Core.CSharp +{ + public class DummyBehaviour : MonoBehaviour + { + public static DummyBehaviour Instance; + + public static void Setup() + { + var obj = new GameObject("Explorer_DummyBehaviour"); + DontDestroyOnLoad(obj); + obj.hideFlags |= HideFlags.HideAndDontSave; + + obj.AddComponent(); + } + + internal void Awake() + { + Instance = this; + } + } +} +#endif \ No newline at end of file diff --git a/src/Core/CSharp/ScriptEvaluator.cs b/src/Core/CSharp/ScriptEvaluator.cs index 0127140..39f5ac4 100644 --- a/src/Core/CSharp/ScriptEvaluator.cs +++ b/src/Core/CSharp/ScriptEvaluator.cs @@ -15,11 +15,12 @@ namespace UnityExplorer.Core.CSharp "mscorlib", "System.Core", "System", "System.Xml" }; - private readonly TextWriter tw; + internal static TextWriter _textWriter; + internal static StreamReportPrinter _reportPrinter; public ScriptEvaluator(TextWriter tw) : base(BuildContext(tw)) { - this.tw = tw; + _textWriter = tw; ImportAppdomainAssemblies(ReferenceAssembly); AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad; @@ -28,23 +29,22 @@ namespace UnityExplorer.Core.CSharp public void Dispose() { AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad; - tw.Dispose(); + _textWriter.Dispose(); } private void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args) { string name = args.LoadedAssembly.GetName().Name; + if (StdLib.Contains(name)) - { return; - } ReferenceAssembly(args.LoadedAssembly); } private static CompilerContext BuildContext(TextWriter tw) { - var reporter = new StreamReportPrinter(tw); + _reportPrinter = new StreamReportPrinter(tw); var settings = new CompilerSettings { @@ -56,7 +56,7 @@ namespace UnityExplorer.Core.CSharp EnhancedWarnings = false }; - return new CompilerContext(settings, reporter); + return new CompilerContext(settings, _reportPrinter); } private static void ImportAppdomainAssemblies(Action import) diff --git a/src/Core/CSharp/ScriptInteraction.cs b/src/Core/CSharp/ScriptInteraction.cs index 5172520..2823b93 100644 --- a/src/Core/CSharp/ScriptInteraction.cs +++ b/src/Core/CSharp/ScriptInteraction.cs @@ -4,6 +4,11 @@ using UnityExplorer.UI; using UnityExplorer.UI.Main; using UnityExplorer.Core.Inspectors; using UnityExplorer.UI.Main.CSConsole; +using System.Collections; +using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using UnityExplorer.Core.Runtime; namespace UnityExplorer.Core.CSharp { @@ -14,6 +19,11 @@ namespace UnityExplorer.Core.CSharp ExplorerCore.Log(message); } + public static void StartCoroutine(IEnumerator ienumerator) + { + RuntimeProvider.Instance.StartConsoleCoroutine(ienumerator); + } + public static void AddUsing(string directive) { CSharpConsole.Instance.AddUsing(directive); @@ -21,7 +31,7 @@ namespace UnityExplorer.Core.CSharp public static void GetUsing() { - ExplorerCore.Log(CSharpConsole.Instance.m_evaluator.GetUsing()); + ExplorerCore.Log(CSharpConsole.Instance.Evaluator.GetUsing()); } public static void Reset() diff --git a/src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs b/src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs new file mode 100644 index 0000000..8f33ba7 --- /dev/null +++ b/src/Core/Runtime/Il2Cpp/Il2CppCoroutine.cs @@ -0,0 +1,156 @@ +#if CPP +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnhollowerBaseLib; +using UnityEngine; + +// CREDIT HerpDerpenstine +// https://github.com/LavaGang/MelonLoader/blob/master/MelonLoader.Support.Il2Cpp/MelonCoroutines.cs + +namespace UnityExplorer.Core.Runtime.Il2Cpp +{ + public static class Il2CppCoroutine + { + private struct CoroTuple + { + public object WaitCondition; + public IEnumerator Coroutine; + } + private static readonly List ourCoroutinesStore = new List(); + private static readonly List ourNextFrameCoroutines = new List(); + private static readonly List ourWaitForFixedUpdateCoroutines = new List(); + private static readonly List ourWaitForEndOfFrameCoroutines = new List(); + + private static readonly List tempList = new List(); + + internal static object Start(IEnumerator routine) + { + if (routine != null) ProcessNextOfCoroutine(routine); + return routine; + } + + internal static void Stop(IEnumerator enumerator) + { + if (ourNextFrameCoroutines.Contains(enumerator)) // the coroutine is running itself + ourNextFrameCoroutines.Remove(enumerator); + else + { + int coroTupleIndex = ourCoroutinesStore.FindIndex(c => c.Coroutine == enumerator); + if (coroTupleIndex != -1) // the coroutine is waiting for a subroutine + { + object waitCondition = ourCoroutinesStore[coroTupleIndex].WaitCondition; + if (waitCondition is IEnumerator waitEnumerator) + Stop(waitEnumerator); + + ourCoroutinesStore.RemoveAt(coroTupleIndex); + } + } + } + + private static void ProcessCoroList(List target) + { + if (target.Count == 0) return; + + // use a temp list to make sure waits made during processing are not handled by same processing invocation + // additionally, a temp list reduces allocations compared to an array + tempList.AddRange(target); + target.Clear(); + foreach (var enumerator in tempList) ProcessNextOfCoroutine(enumerator); + tempList.Clear(); + } + + internal static void Process() + { + for (var i = ourCoroutinesStore.Count - 1; i >= 0; i--) + { + var tuple = ourCoroutinesStore[i]; + if (tuple.WaitCondition is WaitForSeconds waitForSeconds) + { + if ((waitForSeconds.m_Seconds -= Time.deltaTime) <= 0) + { + ourCoroutinesStore.RemoveAt(i); + ProcessNextOfCoroutine(tuple.Coroutine); + } + } + } + + ProcessCoroList(ourNextFrameCoroutines); + } + + internal static void ProcessWaitForFixedUpdate() => ProcessCoroList(ourWaitForFixedUpdateCoroutines); + + internal static void ProcessWaitForEndOfFrame() => ProcessCoroList(ourWaitForEndOfFrameCoroutines); + + private static void ProcessNextOfCoroutine(IEnumerator enumerator) + { + try + { + if (!enumerator.MoveNext()) // Run the next step of the coroutine. If it's done, restore the parent routine + { + var indices = ourCoroutinesStore.Select((it, idx) => (idx, it)).Where(it => it.it.WaitCondition == enumerator).Select(it => it.idx).ToList(); + for (var i = indices.Count - 1; i >= 0; i--) + { + var index = indices[i]; + ourNextFrameCoroutines.Add(ourCoroutinesStore[index].Coroutine); + ourCoroutinesStore.RemoveAt(index); + } + return; + } + } + catch (Exception e) + { + ExplorerCore.LogError(e.ToString()); + Stop(FindOriginalCoro(enumerator)); // We want the entire coroutine hierachy to stop when an error happen + } + + var next = enumerator.Current; + switch (next) + { + case null: + ourNextFrameCoroutines.Add(enumerator); + return; + case WaitForFixedUpdate _: + ourWaitForFixedUpdateCoroutines.Add(enumerator); + return; + case WaitForEndOfFrame _: + ourWaitForEndOfFrameCoroutines.Add(enumerator); + return; + case WaitForSeconds _: + break; // do nothing, this one is supported in Process + case Il2CppObjectBase il2CppObjectBase: + var nextAsEnumerator = il2CppObjectBase.TryCast(); + if (nextAsEnumerator != null) // il2cpp IEnumerator also handles CustomYieldInstruction + next = new Il2CppEnumeratorWrapper(nextAsEnumerator); + else + ExplorerCore.LogWarning($"Unknown coroutine yield object of type {il2CppObjectBase} for coroutine {enumerator}"); + break; + } + + ourCoroutinesStore.Add(new CoroTuple { WaitCondition = next, Coroutine = enumerator }); + + if (next is IEnumerator nextCoro) + ProcessNextOfCoroutine(nextCoro); + } + + private static IEnumerator FindOriginalCoro(IEnumerator enumerator) + { + int index = ourCoroutinesStore.FindIndex(ct => ct.WaitCondition == enumerator); + if (index == -1) + return enumerator; + return FindOriginalCoro(ourCoroutinesStore[index].Coroutine); + } + + private class Il2CppEnumeratorWrapper : IEnumerator + { + private readonly Il2CppSystem.Collections.IEnumerator il2cppEnumerator; + + public Il2CppEnumeratorWrapper(Il2CppSystem.Collections.IEnumerator il2CppEnumerator) => il2cppEnumerator = il2CppEnumerator; + public bool MoveNext() => il2cppEnumerator.MoveNext(); + public void Reset() => il2cppEnumerator.Reset(); + public object Current => il2cppEnumerator.Current; + } + } +} +#endif \ No newline at end of file diff --git a/src/Core/Runtime/Il2Cpp/Il2CppProvider.cs b/src/Core/Runtime/Il2Cpp/Il2CppProvider.cs index 10e7bc3..6ff5101 100644 --- a/src/Core/Runtime/Il2Cpp/Il2CppProvider.cs +++ b/src/Core/Runtime/Il2Cpp/Il2CppProvider.cs @@ -10,6 +10,7 @@ using UnhollowerRuntimeLib; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; +using System.Collections; namespace UnityExplorer.Core.Runtime.Il2Cpp { @@ -41,6 +42,11 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp } } + public override void StartConsoleCoroutine(IEnumerator routine) + { + Il2CppCoroutine.Start(routine); + } + internal delegate IntPtr d_LayerToName(int layer); public override string LayerToName(int layer) diff --git a/src/Core/Runtime/Mono/MonoProvider.cs b/src/Core/Runtime/Mono/MonoProvider.cs index 11f559f..c4c61d5 100644 --- a/src/Core/Runtime/Mono/MonoProvider.cs +++ b/src/Core/Runtime/Mono/MonoProvider.cs @@ -1,5 +1,6 @@ #if MONO using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -7,6 +8,7 @@ using System.Text; using UnityEngine; using UnityEngine.SceneManagement; using UnityExplorer.Core; +using UnityExplorer.Core.CSharp; namespace UnityExplorer.Core.Runtime.Mono { @@ -25,6 +27,11 @@ namespace UnityExplorer.Core.Runtime.Mono //SceneManager.activeSceneChanged += ExplorerCore.Instance.OnSceneLoaded2; } + public override void StartConsoleCoroutine(IEnumerator routine) + { + DummyBehaviour.Instance.StartCoroutine(routine); + } + public override string LayerToName(int layer) => LayerMask.LayerToName(layer); @@ -50,4 +57,12 @@ namespace UnityExplorer.Core.Runtime.Mono } } +public static class MonoExtensions +{ + public static void Clear(this StringBuilder sb) + { + sb.Remove(0, sb.Length); + } +} + #endif \ No newline at end of file diff --git a/src/Core/Runtime/RuntimeProvider.cs b/src/Core/Runtime/RuntimeProvider.cs index f28a477..16be287 100644 --- a/src/Core/Runtime/RuntimeProvider.cs +++ b/src/Core/Runtime/RuntimeProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -36,6 +37,8 @@ namespace UnityExplorer.Core.Runtime public abstract void SetupEvents(); + public abstract void StartConsoleCoroutine(IEnumerator routine); + // Unity API handlers public abstract string LayerToName(int layer); diff --git a/src/UI/Main/CSConsole/AutoCompleter.cs b/src/UI/Main/CSConsole/AutoCompleter.cs index 633a60a..31a74ab 100644 --- a/src/UI/Main/CSConsole/AutoCompleter.cs +++ b/src/UI/Main/CSConsole/AutoCompleter.cs @@ -205,7 +205,7 @@ namespace UnityExplorer.UI.Main.CSConsole { // Credit ManylMarco CSharpConsole.AutoCompletes.Clear(); - string[] completions = CSharpConsole.Instance.m_evaluator.GetCompletions(input, out string prefix); + string[] completions = CSharpConsole.Instance.Evaluator.GetCompletions(input, out string prefix); if (completions != null) { if (prefix == null) diff --git a/src/UI/Main/CSConsole/CSLexerHighlighter.cs b/src/UI/Main/CSConsole/CSLexerHighlighter.cs index e591268..c6896a0 100644 --- a/src/UI/Main/CSConsole/CSLexerHighlighter.cs +++ b/src/UI/Main/CSConsole/CSLexerHighlighter.cs @@ -53,15 +53,10 @@ namespace UnityExplorer.UI.Main.CSConsole "else", "equals", "false", "finally", "float", "for", "foreach", "from", "global", "goto", "group", "if", "in", "int", "into", "is", "join", "let", "lock", "long", "new", "null", "object", "on", "orderby", "out", "ref", "remove", "return", "sbyte", "select", "short", "sizeof", "stackalloc", "string", -"switch", "throw", "true", "try", "typeof", "uint", "ulong", "ushort", "var", "where", "while", "yield" } - }; - - public static KeywordMatch invalidKeywordMatcher = new KeywordMatch() - { - highlightColor = new Color(0.95f, 0.10f, 0.10f, 1.0f), - Keywords = new[] { "abstract", "async", "base", "class", "delegate", "enum", "explicit", "extern", "fixed", "get", -"implicit", "interface", "internal", "namespace", "operator", "override", "params", "private", "protected", "public", -"using", "partial", "readonly", "sealed", "set", "static", "struct", "this", "unchecked", "unsafe", "value", "virtual", "volatile", "void" } +"switch", "throw", "true", "try", "typeof", "uint", "ulong", "ushort", "var", "where", "while", "yield", +"abstract", "async", "base", "class", "delegate", "enum", "explicit", "extern", "fixed", "get", +"implicit", "interface", "internal", "namespace", "operator", "override", "params", "private", "protected", "public", +"using", "partial", "readonly", "sealed", "set", "static", "struct", "this", "unchecked", "unsafe", "value", "virtual", "volatile", "void"} }; // ~~~~~~~ ctor ~~~~~~~ @@ -78,7 +73,6 @@ namespace UnityExplorer.UI.Main.CSConsole numberMatcher, stringMatcher, validKeywordMatcher, - invalidKeywordMatcher, }; foreach (Matcher lexer in matchers) diff --git a/src/UI/Main/CSConsole/CSharpConsole.cs b/src/UI/Main/CSConsole/CSharpConsole.cs index 0cc5766..79190b7 100644 --- a/src/UI/Main/CSConsole/CSharpConsole.cs +++ b/src/UI/Main/CSConsole/CSharpConsole.cs @@ -12,6 +12,9 @@ using UnityEngine.UI; using UnityExplorer.UI.Reusable; using UnityExplorer.UI.Main.CSConsole; using UnityExplorer.Core; +#if CPP +using UnityExplorer.Core.Runtime.Il2Cpp; +#endif namespace UnityExplorer.UI.Main.CSConsole { @@ -21,8 +24,8 @@ namespace UnityExplorer.UI.Main.CSConsole public static CSharpConsole Instance { get; private set; } - //public UI.CSConsole.CSharpConsole m_codeEditor; - public ScriptEvaluator m_evaluator; + public ScriptEvaluator Evaluator; + internal StringBuilder m_evalLogBuilder; public static List UsingDirectives; @@ -49,11 +52,13 @@ namespace UnityExplorer.UI.Main.CSConsole InitConsole(); AutoCompleter.Init(); +#if MONO + DummyBehaviour.Setup(); +#endif ResetConsole(); - // Make sure compiler is supported on this platform - m_evaluator.Compile(""); + Evaluator.Compile(""); foreach (string use in DefaultUsing) AddUsing(use); @@ -74,10 +79,27 @@ namespace UnityExplorer.UI.Main.CSConsole } } + public void ResetConsole() + { + if (Evaluator != null) + Evaluator.Dispose(); + + m_evalLogBuilder = new StringBuilder(); + + Evaluator = new ScriptEvaluator(new StringWriter(m_evalLogBuilder)) { InteractiveBaseClass = typeof(ScriptInteraction) }; + + UsingDirectives = new List(); + } + public override void Update() { UpdateConsole(); + AutoCompleter.Update(); + +#if CPP + Il2CppCoroutine.Process(); +#endif } public void AddUsing(string asm) @@ -89,40 +111,34 @@ namespace UnityExplorer.UI.Main.CSConsole } } - public void Evaluate(string code, bool suppressWarning = false) + public void Evaluate(string code, bool supressLog = false) { - m_evaluator.Compile(code, out Mono.CSharp.CompiledMethod compiled); - - if (compiled == null) + try { - if (!suppressWarning) - ExplorerCore.LogWarning("Unable to compile the code!"); + Evaluator.Run(code); + + string output = ScriptEvaluator._textWriter.ToString(); + var outputSplit = output.Split('\n'); + if (outputSplit.Length >= 2) + output = outputSplit[outputSplit.Length - 2]; + m_evalLogBuilder.Clear(); + + if (ScriptEvaluator._reportPrinter.ErrorsCount > 0) + throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}"); + + if (!supressLog) + ExplorerCore.Log("Code executed successfully."); } - else + catch (FormatException fex) { - try - { - object ret = VoidType.Value; - compiled.Invoke(ref ret); - } - catch (Exception e) - { - if (!suppressWarning) - ExplorerCore.LogWarning($"Exception executing code: {e.GetType()}, {e.Message}\r\n{e.StackTrace}"); - } + if (!supressLog) + ExplorerCore.LogWarning(fex.Message); } - } - - public void ResetConsole() - { - if (m_evaluator != null) + catch (Exception ex) { - m_evaluator.Dispose(); + if (!supressLog) + ExplorerCore.LogWarning(ex); } - - m_evaluator = new ScriptEvaluator(new StringWriter(new StringBuilder())) { InteractiveBaseClass = typeof(ScriptInteraction) }; - - UsingDirectives = new List(); } // ================================================================================================= @@ -160,6 +176,8 @@ The following helper methods are available: * Log(""message"") logs a message to the debug console +* StartCoroutine(IEnumerator routine) start the IEnumerator as a UnityEngine.Coroutine + * CurrentTarget() returns the currently inspected target on the Home page * AllTargets() returns an object[] array containing all inspected instances @@ -447,7 +465,7 @@ The following helper methods are available: mainGroup.childForceExpandHeight = true; mainGroup.childForceExpandWidth = true; - #region TOP BAR +#region TOP BAR // Main group object @@ -523,9 +541,9 @@ The following helper methods are available: autoIndentLayout.flexibleWidth = 0; autoIndentLayout.minHeight = 25; - #endregion +#endregion - #region CONSOLE INPUT +#region CONSOLE INPUT int fontSize = 16; @@ -554,9 +572,9 @@ The following helper methods are available: highlightTextInput.supportRichText = true; highlightTextInput.fontSize = fontSize; - #endregion +#endregion - #region COMPILE BUTTON +#region COMPILE BUTTON var compileBtnObj = UIFactory.CreateButton(Content); var compileBtnLayout = compileBtnObj.AddComponent(); @@ -583,7 +601,7 @@ The following helper methods are available: } } - #endregion +#endregion //mainTextInput.supportRichText = false; diff --git a/src/UnityExplorer.csproj b/src/UnityExplorer.csproj index 2e9070f..e67e0f4 100644 --- a/src/UnityExplorer.csproj +++ b/src/UnityExplorer.csproj @@ -261,6 +261,8 @@ + +