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
This commit is contained in:
Sinai 2021-03-25 18:39:35 +11:00
parent a9fbea7c96
commit 2107df70ad
11 changed files with 289 additions and 56 deletions

View File

@ -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<DummyBehaviour>();
}
internal void Awake()
{
Instance = this;
}
}
}
#endif

View File

@ -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<Assembly> import)

View File

@ -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()

View File

@ -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<CoroTuple> ourCoroutinesStore = new List<CoroTuple>();
private static readonly List<IEnumerator> ourNextFrameCoroutines = new List<IEnumerator>();
private static readonly List<IEnumerator> ourWaitForFixedUpdateCoroutines = new List<IEnumerator>();
private static readonly List<IEnumerator> ourWaitForEndOfFrameCoroutines = new List<IEnumerator>();
private static readonly List<IEnumerator> tempList = new List<IEnumerator>();
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<IEnumerator> 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<Il2CppSystem.Collections.IEnumerator>();
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

View File

@ -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)

View File

@ -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

View File

@ -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);

View File

@ -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)

View File

@ -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)

View File

@ -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<string> 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<string>();
}
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<string>();
}
// =================================================================================================
@ -160,6 +176,8 @@ The following helper methods are available:
* <color=#add490>Log(""message"")</color> logs a message to the debug console
* <color=#add490>StartCoroutine(IEnumerator routine)</color> start the IEnumerator as a UnityEngine.Coroutine
* <color=#add490>CurrentTarget()</color> returns the currently inspected target on the Home page
* <color=#add490>AllTargets()</color> 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<LayoutElement>();
@ -583,7 +601,7 @@ The following helper methods are available:
}
}
#endregion
#endregion
//mainTextInput.supportRichText = false;

View File

@ -261,6 +261,8 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Core\CSharp\DummyBehaviour.cs" />
<Compile Include="Core\Runtime\Il2Cpp\Il2CppCoroutine.cs" />
<Compile Include="Loader\ExplorerBepIn6Plugin.cs" />
<Compile Include="Loader\ExplorerStandalone.cs" />
<Compile Include="Core\Runtime\Il2Cpp\Il2CppReflection.cs" />