diff --git a/README.md b/README.md index c444e56..74b60dc 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@

- An in-game explorer and a suite of debugging tools for IL2CPP and Mono Unity games, using MelonLoader and BepInEx.

- + An in-game explorer and a suite of debugging tools for IL2CPP and Mono Unity games, to aid with modding development. +

+

@@ -22,20 +23,19 @@ ## Releases -| Mod Loader | Il2Cpp | Mono | +| Mod Loader | IL2CPP | Mono | | ----------- | ------ | ---- | | [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) | ✔️ [link](https://github.com/sinai-dev/Explorer/releases/latest/download/Explorer.MelonLoader.Il2Cpp.zip) | ✔️ [link](https://github.com/sinai-dev/Explorer/releases/latest/download/Explorer.MelonLoader.Mono.zip) | | [BepInEx](https://github.com/BepInEx/BepInEx) | ❔ [link](https://github.com/sinai-dev/Explorer/releases/latest/download/Explorer.BepInEx.Il2Cpp.zip) | ✔️ [link](https://github.com/sinai-dev/Explorer/releases/latest/download/Explorer.BepInEx.Mono.zip) | -Il2Cpp Issues: +IL2CPP Issues: * Some methods may still fail with a `MissingMethodException`, please let me know if you experience this (with full debug log please). -* Reflection may fail with certain types, see [here](https://github.com/knah/Il2CppAssemblyUnhollower#known-issues) for more details. -* Scrolling with mouse wheel in the Explorer menu may not work on all games at the moment. +* Reflection may fail with certain types, see [here](https://github.com/knah/IL2CPPAssemblyUnhollower#known-issues) for more details. ## Features

- +

* Scene Explorer: Simple menu to traverse the Transform heirarchy of the scene. @@ -47,79 +47,54 @@ ## How to install -### MelonLoader -Requires [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) to be installed for your game. - -1. Download the relevant release from above. -2. Unzip the file into the `Mods` folder in your game's installation directory, created by MelonLoader. -3. Make sure it's not in a sub-folder, `Explorer.dll` should be directly in the `Mods\` folder. - ### BepInEx -Requires [BepInEx](https://github.com/BepInEx/BepInEx) to be installed for your game. -1. Download the relevant release from above. -2. Unzip the file into the `BepInEx\plugins\` folder in your game's installation directory, created by BepInEx. -3. Make sure it's not in a sub-folder, `Explorer.dll` should be directly in the `plugins\` folder. +0. Install [BepInEx](https://github.com/BepInEx/BepInEx) for your game. +1. Download the UnityExplorer release for BepInEx IL2CPP or Mono above. +2. Take the `UnityExplorer.dll` file and put it in `[GameFolder]\BepInEx\plugins\` +3. Take the `UnityExplorer\` folder (with `explorerui.bundle`) and put it in `[GameFolder]\Mods\`, so it looks like `[GameFolder]\Mods\UnityExplorer\explorerui.bundle`. +4. In IL2CPP, it is highly recommended to get the base Unity libs for the game's Unity version and put them in the `BepInEx\unhollowed\base\` folder. + +### MelonLoader + +0. Install [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) for your game. +1. Download the UnityExplorer release for MelonLoader IL2CPP or Mono above. +2. Take the contents of the release and put it in the `[GameFolder]\Mods\` folder. It should look like `[GameFolder]\Mods\UnityExplorer.dll` and `[GameFolder]\Mods\UnityExplorer\explorerui.bundle`. ## Mod Config -There is a simple Mod Config for the Explorer. You can access the settings via the "Options" page of the main menu. +You can access the settings via the "Options" page of the main menu, or directly from the config at `Mods\UnityExplorer\config.xml` (generated after first launch). -`Main Menu Toggle` (KeyCode) | Default: `F7` +`Main Menu Toggle` (KeyCode) +* Default: `F7` * See [this article](https://docs.unity3d.com/ScriptReference/KeyCode.html) for a full list of all accepted KeyCodes. -`Default Window Size` (Vector2) | Default: `x: 550, y: 700` -* Sets the default width and height for all Explorer windows when created. +`Force Unlock Mouse` (bool) +* Default: `true` +* Forces the cursor to be unlocked and visible while the UnityExplorer menu is open, and prevents anything else taking control. -`Default Items per Page` (int) | Default: `20` +`Default Page Limit` (int) +* Default: `25` * Sets the default items per page when viewing lists or search results. +* Requires a restart to take effect, apart from Reflection Inspector tabs. -`Enable Bitwise Editing` (bool) | Default: `false` -* Whether or not to show the Bitwise Editing helper when inspecting integers - -`Enable Tab View` (bool) | Default: `true` -* Whether or not all inspector windows a grouped into a single window with tabs. - -`Default Output Path` (string) | Default: `Mods\Explorer` +`Default Output Path` (string) +* Default: `Mods\Explorer` * Where output is generated to, by default (for Texture PNG saving, etc). -## Mouse Control - -Explorer can force the mouse to be visible and unlocked when the menu is open, if you have enabled "Force Unlock Mouse" (Left-Alt toggle). Explorer also attempts to prevent clicking-through onto the game behind the Explorer menu. - -If you need more mouse control: - -* For VRChat, use [VRCExplorerMouseControl](https://github.com/sinai-dev/VRCExplorerMouseControl) -* For Hellpoint, use [HPExplorerMouseControl](https://github.com/sinai-dev/Hellpoint-Mods/tree/master/HPExplorerMouseControl/HPExplorerMouseControl) -* You can create your own plugin using one of the two plugins above as an example. Usually only a few simple Harmony patches are needed to fix the problem. - -For example: -```csharp -using Explorer; -using Harmony; // or 'using HarmonyLib;' for BepInEx -// ... -// You will need to figure out the relevant Class and Method for your game using dnSpy. -[HarmonyPatch(typeof(MyGame.InputManager), nameof(MyGame.InputManager.Update))] -public class InputManager_Update -{ - [HarmonyPrefix] - public static bool Prefix() - { - // prevent method running if menu open, let it run if not. - return !ExplorerCore.ShowMenu; - } -} -``` +`Log Unity Debug` (bool) +* Default: `false` +* Listens for Unity `Debug.Log` messages and prints them to UnityExplorer's log. ## Building -If you'd like to build this yourself, you will need to have installed BepInEx and/or MelonLoader for at least one Unity game. If you want to build all 4 versions, you will need at least one Il2Cpp and one Mono game, with BepInEx and MelonLoader installed for both. +If you'd like to build this yourself, you will need to have installed BepInEx and/or MelonLoader for at least one Unity game. If you want to build all 4 versions, you will need at least one IL2CPP and one Mono game, with BepInEx and MelonLoader installed for both. 1. Install MelonLoader or BepInEx for your game. 2. Open the `src\Explorer.csproj` file in a text editor. -3. Set the relevant `GameFolder` values for the versions you want to build, eg. set `MLCppGameFolder` if you want to build for a MelonLoader Il2Cpp game. +3. Set the relevant `GameFolder` values for the versions you want to build, eg. set `MLCppGameFolder` if you want to build for a MelonLoader IL2CPP game. 4. Open the `src\Explorer.sln` project. -5. Select `Solution 'Explorer' (1 of 1 project)` in the Solution Explorer panel, and set the Active config property to the version you want to build, then build it. +5. Select `Solution 'UnityExplorer' (1 of 1 project)` in the Solution Explorer panel, and set the Active config property to the version you want to build, then build it. 5. The DLLs are built to the `Release\` folder in the root of the repository. 6. If ILRepack fails or is missing, use the NuGet package manager to re-install `ILRepack.Lib.MSBuild.Task`, then re-build. @@ -128,5 +103,5 @@ If you'd like to build this yourself, you will need to have installed BepInEx an Written by Sinai. Thanks to: -* [ManlyMarco](https://github.com/ManlyMarco) for their [Runtime Unity Editor](https://github.com/ManlyMarco/RuntimeUnityEditor), which I used for the REPL Console and the "Find instances" snippet, and the UI style. -* [denikson](https://github.com/denikson) for [mcs-unity](https://github.com/denikson/mcs-unity). I commented out the `SkipVisibilityExt` constructor since it was causing an exception with the Hook it attempted. +* [ManlyMarco](https://github.com/ManlyMarco) for their [Runtime Unity Editor](https://github.com/ManlyMarco/RuntimeUnityEditor), which I used for some aspects of the C# Console and Auto-Complete features. +* [denikson](https://github.com/denikson) (aka Horse) for [mcs-unity](https://github.com/denikson/mcs-unity). I commented out the `SkipVisibilityExt` constructor since it was causing an exception with the Hook it attempted in IL2CPP. diff --git a/icon.png b/icon.png index ae791aa..edb16bc 100644 Binary files a/icon.png and b/icon.png differ diff --git a/lib/UnityEngine.UI.dll b/lib/UnityEngine.UI.dll new file mode 100644 index 0000000..aeee3d8 Binary files /dev/null and b/lib/UnityEngine.UI.dll differ diff --git a/overview.png b/overview.png index f099f68..6491f9a 100644 Binary files a/overview.png and b/overview.png differ diff --git a/resources/explorerui.bundle b/resources/explorerui.bundle new file mode 100644 index 0000000..c6acc12 Binary files /dev/null and b/resources/explorerui.bundle differ diff --git a/src/CSConsole/AutoCompleter.cs b/src/CSConsole/AutoCompleter.cs new file mode 100644 index 0000000..42dade8 --- /dev/null +++ b/src/CSConsole/AutoCompleter.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using UnityExplorer.Helpers; +using UnityExplorer.UI; +using UnityExplorer.UI.Modules; + +namespace UnityExplorer.CSConsole +{ + public class AutoCompleter + { + public static AutoCompleter Instance; + + public const int MAX_LABELS = 500; + private const int UPDATES_PER_BATCH = 100; + + public static GameObject m_mainObj; + //private static RectTransform m_thisRect; + + private static readonly List m_suggestionButtons = new List(); + private static readonly List m_suggestionTexts = new List(); + private static readonly List m_hiddenSuggestionTexts = new List(); + + private static bool m_suggestionsDirty; + private static Suggestion[] m_suggestions = new Suggestion[0]; + private static int m_lastBatchIndex; + + private static string m_prevInput = "NULL"; + private static int m_lastCaretPos; + + public static void Init() + { + ConstructUI(); + + m_mainObj.SetActive(false); + } + + public static void Update() + { + if (!m_mainObj) + { + return; + } + + if (!CodeEditor.EnableAutocompletes) + { + if (m_mainObj.activeSelf) + { + m_mainObj.SetActive(false); + } + + return; + } + + RefreshButtons(); + + UpdatePosition(); + } + + public static void SetSuggestions(Suggestion[] suggestions) + { + m_suggestions = suggestions; + + m_suggestionsDirty = true; + m_lastBatchIndex = 0; + } + + private static void RefreshButtons() + { + if (!m_suggestionsDirty) + { + return; + } + + if (m_suggestions.Length < 1) + { + if (m_mainObj.activeSelf) + { + m_mainObj?.SetActive(false); + } + return; + } + + if (!m_mainObj.activeSelf) + { + m_mainObj.SetActive(true); + } + + if (m_suggestions.Length < 1 || m_lastBatchIndex >= MAX_LABELS) + { + m_suggestionsDirty = false; + return; + } + + int end = m_lastBatchIndex + UPDATES_PER_BATCH; + for (int i = m_lastBatchIndex; i < end && i < MAX_LABELS; i++) + { + if (i >= m_suggestions.Length) + { + if (m_suggestionButtons[i].activeSelf) + { + m_suggestionButtons[i].SetActive(false); + } + } + else + { + if (!m_suggestionButtons[i].activeSelf) + { + m_suggestionButtons[i].SetActive(true); + } + + var suggestion = m_suggestions[i]; + var label = m_suggestionTexts[i]; + var hiddenLabel = m_hiddenSuggestionTexts[i]; + + label.text = suggestion.Full; + hiddenLabel.text = suggestion.Addition; + + label.color = suggestion.TextColor; + } + + m_lastBatchIndex = i; + } + + m_lastBatchIndex++; + } + + private static void UpdatePosition() + { + try + { + var editor = CSConsolePage.Instance.m_codeEditor; + + if (!editor.InputField.isFocused) + return; + + var textGen = editor.InputText.cachedTextGenerator; + int caretPos = editor.m_lastCaretPos; + + if (caretPos == m_lastCaretPos) + return; + + m_lastCaretPos = caretPos; + + if (caretPos >= 1) + caretPos--; + + var pos = textGen.characters[caretPos].cursorPos; + + pos = editor.InputField.transform.TransformPoint(pos); + + m_mainObj.transform.position = new Vector3(pos.x + 10, pos.y - 20, 0); + } + catch //(Exception e) + { + //ExplorerCore.Log(e.ToString()); + } + } + + private static readonly char[] splitChars = new[] { '{', '}', ',', ';', '<', '>', '(', ')', '[', ']', '=', '|', '&', '?' }; + + public static void CheckAutocomplete() + { + var m_codeEditor = CSConsolePage.Instance.m_codeEditor; + string input = m_codeEditor.InputField.text; + int caretIndex = m_codeEditor.InputField.caretPosition; + + if (!string.IsNullOrEmpty(input)) + { + try + { + int start = caretIndex <= 0 ? 0 : input.LastIndexOfAny(splitChars, caretIndex - 1) + 1; + input = input.Substring(start, caretIndex - start).Trim(); + } + catch (ArgumentException) { } + } + + if (!string.IsNullOrEmpty(input) && input != m_prevInput) + { + GetAutocompletes(input); + } + else + { + ClearAutocompletes(); + } + + m_prevInput = input; + } + + public static void ClearAutocompletes() + { + if (CodeEditor.AutoCompletes.Any()) + { + CodeEditor.AutoCompletes.Clear(); + } + } + + public static void GetAutocompletes(string input) + { + try + { + // Credit ManylMarco + CodeEditor.AutoCompletes.Clear(); + string[] completions = CSConsolePage.Instance.m_evaluator.GetCompletions(input, out string prefix); + if (completions != null) + { + if (prefix == null) + { + prefix = input; + } + + CodeEditor.AutoCompletes.AddRange(completions + .Where(x => !string.IsNullOrEmpty(x)) + .Select(x => new Suggestion(x, prefix, Suggestion.Contexts.Other)) + ); + } + + string trimmed = input.Trim(); + if (trimmed.StartsWith("using")) + { + trimmed = trimmed.Remove(0, 5).Trim(); + } + + IEnumerable namespaces = Suggestion.Namespaces + .Where(x => x.StartsWith(trimmed) && x.Length > trimmed.Length) + .Select(x => new Suggestion( + x.Substring(trimmed.Length), + x.Substring(0, trimmed.Length), + Suggestion.Contexts.Namespace)); + + CodeEditor.AutoCompletes.AddRange(namespaces); + + IEnumerable keywords = Suggestion.Keywords + .Where(x => x.StartsWith(trimmed) && x.Length > trimmed.Length) + .Select(x => new Suggestion( + x.Substring(trimmed.Length), + x.Substring(0, trimmed.Length), + Suggestion.Contexts.Keyword)); + + CodeEditor.AutoCompletes.AddRange(keywords); + } + catch (Exception ex) + { + ExplorerCore.Log("Autocomplete error:\r\n" + ex.ToString()); + ClearAutocompletes(); + } + } + + #region UI Construction + + private static void ConstructUI() + { + var parent = UIManager.CanvasRoot; + + var obj = UIFactory.CreateScrollView(parent, out GameObject content, out _, new Color(0.1f, 0.1f, 0.1f, 0.95f)); + + m_mainObj = obj; + + var mainRect = obj.GetComponent(); + //m_thisRect = mainRect; + mainRect.pivot = new Vector2(0f, 1f); + mainRect.anchorMin = new Vector2(0.45f, 0.45f); + mainRect.anchorMax = new Vector2(0.65f, 0.6f); + mainRect.offsetMin = Vector2.zero; + mainRect.offsetMax = Vector2.zero; + + var mainGroup = content.GetComponent(); + mainGroup.childControlHeight = false; + mainGroup.childControlWidth = true; + mainGroup.childForceExpandHeight = false; + mainGroup.childForceExpandWidth = true; + + for (int i = 0; i < MAX_LABELS; i++) + { + var buttonObj = UIFactory.CreateButton(content); + Button btn = buttonObj.GetComponent