From 240a2484a4ce031d664a2bce5b6113b5f956a9e8 Mon Sep 17 00:00:00 2001 From: sinaioutlander <49360850+sinaioutlander@users.noreply.github.com> Date: Fri, 7 Aug 2020 22:19:03 +1000 Subject: [PATCH] init --- src/CppExplorer.cs | 238 +++++++++++ src/CppExplorer.csproj | 131 ++++++ src/CppExplorer.sln | 25 ++ src/ILBehaviour.cs | 33 ++ src/Menu/Inspectors/GameObjectWindow.cs | 448 ++++++++++++++++++++ src/Menu/Inspectors/ReflectionWindow.cs | 542 ++++++++++++++++++++++++ src/Menu/MainMenu.cs | 136 ++++++ src/Menu/MainMenu/Console/REPL.cs | 131 ++++++ src/Menu/MainMenu/Console/REPLHelper.cs | 29 ++ src/Menu/MainMenu/ConsolePage.cs | 224 ++++++++++ src/Menu/MainMenu/ScenePage.cs | 219 ++++++++++ src/Menu/MainMenu/SearchPage.cs | 434 +++++++++++++++++++ src/Menu/UIStyles.cs | 357 ++++++++++++++++ src/Properties/AssemblyInfo.cs | 41 ++ src/WindowManager.cs | 258 +++++++++++ src/utils/AccessTools.cs | 65 +++ 16 files changed, 3311 insertions(+) create mode 100644 src/CppExplorer.cs create mode 100644 src/CppExplorer.csproj create mode 100644 src/CppExplorer.sln create mode 100644 src/ILBehaviour.cs create mode 100644 src/Menu/Inspectors/GameObjectWindow.cs create mode 100644 src/Menu/Inspectors/ReflectionWindow.cs create mode 100644 src/Menu/MainMenu.cs create mode 100644 src/Menu/MainMenu/Console/REPL.cs create mode 100644 src/Menu/MainMenu/Console/REPLHelper.cs create mode 100644 src/Menu/MainMenu/ConsolePage.cs create mode 100644 src/Menu/MainMenu/ScenePage.cs create mode 100644 src/Menu/MainMenu/SearchPage.cs create mode 100644 src/Menu/UIStyles.cs create mode 100644 src/Properties/AssemblyInfo.cs create mode 100644 src/WindowManager.cs create mode 100644 src/utils/AccessTools.cs diff --git a/src/CppExplorer.cs b/src/CppExplorer.cs new file mode 100644 index 0000000..4379e39 --- /dev/null +++ b/src/CppExplorer.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using System.IO; +using System.Reflection; +using MelonLoader; +using Harmony; +using UnhollowerBaseLib.Runtime; +using UnhollowerRuntimeLib; +using UnhollowerBaseLib; +using System.Runtime.CompilerServices; +using UnhollowerBaseLib.Attributes; + +namespace Explorer +{ + public class CppExplorer : MelonMod + { + // consts + + public const string ID = "com.sinai.explorer"; + public const string NAME = "IL2CPP Runtime Explorer"; + public const string VERSION = "0.91"; + public const string AUTHOR = "Sinai"; + + // fields + + public static CppExplorer Instance; + private string m_objUnderMouseName = ""; + private Camera m_main; + + // props + + public static bool ShowMenu { get; set; } = false; + public static int ArrayLimit { get; set; } = 100; + public bool MouseInspect { get; set; } = false; + + public static string ActiveSceneName + { + get + { + return UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; + } + } + + public Camera MainCamera + { + get + { + if (m_main == null) + { + m_main = Camera.main; + } + return m_main; + } + } + + // methods + + public override void OnApplicationStart() + { + base.OnApplicationStart(); + + Instance = this; + + new MainMenu(); + new WindowManager(); + + //LoadMCS(); + + //// init debugging hooks + //var harmony = HarmonyInstance.Create(ID); + //harmony.PatchAll(); + + // done init + ShowMenu = true; + } + + //private void LoadMCS() + //{ + // var mcsPath = @"Mods\mcs.dll"; + // if (File.Exists(mcsPath)) + // { + // Assembly.Load(File.ReadAllBytes(mcsPath)); + // MelonLogger.Log("Loaded mcs.dll"); + // } + // else + // { + // MelonLogger.LogError("Could not find mcs.dll!"); + // } + //} + + public override void OnLevelWasLoaded(int level) + { + if (ScenePage.Instance != null) + { + ScenePage.Instance.OnSceneChange(); + SearchPage.Instance.OnSceneChange(); + } + } + + public override void OnUpdate() + { + if (Input.GetKeyDown(KeyCode.F7)) + { + ShowMenu = !ShowMenu; + } + + if (ShowMenu) + { + MainMenu.Instance.Update(); + WindowManager.Instance.Update(); + + if (Input.GetKey(KeyCode.LeftShift) && Input.GetMouseButtonDown(1)) + { + MouseInspect = !MouseInspect; + } + + if (MouseInspect) + { + InspectUnderMouse(); + } + } + else if (MouseInspect) + { + MouseInspect = false; + } + } + + private void InspectUnderMouse() + { + Ray ray = MainCamera.ScreenPointToRay(Input.mousePosition); + + if (Physics.Raycast(ray, out RaycastHit hit, 1000f)) + { + var obj = hit.transform.gameObject; + + m_objUnderMouseName = GetGameObjectPath(obj.transform); + + if (Input.GetMouseButtonDown(0)) + { + MouseInspect = false; + m_objUnderMouseName = ""; + + WindowManager.InspectObject(obj, out _); + } + } + else + { + m_objUnderMouseName = ""; + } + } + + public override void OnGUI() + { + base.OnGUI(); + + MainMenu.Instance.OnGUI(); + WindowManager.Instance.OnGUI(); + + if (MouseInspect) + { + if (m_objUnderMouseName != "") + { + var pos = Input.mousePosition; + var rect = new Rect( + pos.x - (Screen.width / 2), // x + Screen.height - pos.y - 50, // y + Screen.width, // w + 50 // h + ); + + var origAlign = GUI.skin.label.alignment; + GUI.skin.label.alignment = TextAnchor.MiddleCenter; + + //shadow text + GUI.Label(rect, $"{m_objUnderMouseName}"); + //white text + GUI.Label(new Rect(rect.x - 1, rect.y + 1, rect.width, rect.height), m_objUnderMouseName); + + GUI.skin.label.alignment = origAlign; + } + } + } + + // ************** public helpers ************** + + public static object Il2CppCast(object obj, Type castTo) + { + var method = typeof(Il2CppObjectBase).GetMethod("TryCast"); + var generic = method.MakeGenericMethod(castTo); + return generic.Invoke(obj, null); + } + + public static string GetGameObjectPath(Transform _transform) + { + return GetGameObjectPath(_transform, true); + } + + public static string GetGameObjectPath(Transform _transform, bool _includeItemName) + { + string text = _includeItemName ? ("/" + _transform.name) : ""; + GameObject gameObject = _transform.gameObject; + while (gameObject.transform.parent != null) + { + gameObject = gameObject.transform.parent.gameObject; + text = "/" + gameObject.name + text; + } + return text; + } + + public static Type GetType(string _type) + { + try + { + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + if (asm.GetType(_type) is Type type) + { + return type; + } + } + catch { } + } + + return null; + } + catch + { + return null; + } + } + } +} diff --git a/src/CppExplorer.csproj b/src/CppExplorer.csproj new file mode 100644 index 0000000..570dcfe --- /dev/null +++ b/src/CppExplorer.csproj @@ -0,0 +1,131 @@ + + + + + Debug + AnyCPU + {B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D} + Library + Properties + Explorer + HPExplorer + v4.7.2 + 512 + true + + + + false + none + false + ..\Release\ + + + prompt + 4 + x64 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\Il2Cppmscorlib.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\Il2CppSystem.Core.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\MelonLoader.ModHandler.dll + False + + + + + + + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnhollowerBaseLib.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnhollowerRuntimeLib.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\Unity.TextMeshPro.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.CoreModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.IMGUIModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.InputLegacyModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.InputModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.ParticleSystemModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.Physics2DModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.PhysicsModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.TextRenderingModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.UI.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.UIElementsModule.dll + False + + + ..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.UIModule.dll + False + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/CppExplorer.sln b/src/CppExplorer.sln new file mode 100644 index 0000000..5845c60 --- /dev/null +++ b/src/CppExplorer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30128.74 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CppExplorer", "CppExplorer.csproj", "{B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DD5C0A5D-03F1-4CC3-8B4D-E10834908C5A} + EndGlobalSection +EndGlobal diff --git a/src/ILBehaviour.cs b/src/ILBehaviour.cs new file mode 100644 index 0000000..a00d88d --- /dev/null +++ b/src/ILBehaviour.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using MelonLoader; +using UnhollowerRuntimeLib; + +namespace Explorer +{ + //public class ILBehaviour : MonoBehaviour + //{ + // public ILBehaviour(IntPtr intPtr) : base(intPtr) { } + + // public static T AddToGameObject(GameObject _go) where T : ILBehaviour + // { + // Il2CppSystem.Type ilType = UnhollowerRuntimeLib.Il2CppType.Of(); + + // if (ilType == null) + // { + // MelonLogger.Log("Error - could not get MB as ilType"); + // return null; + // } + + // var obj = typeof(T) + // .GetConstructor(new Type[] { typeof(IntPtr) }) + // .Invoke(new object[] { _go.AddComponent(UnhollowerRuntimeLib.Il2CppType.Of()).Pointer }); + + // return (T)obj; + // } + //} +} diff --git a/src/Menu/Inspectors/GameObjectWindow.cs b/src/Menu/Inspectors/GameObjectWindow.cs new file mode 100644 index 0000000..16d265e --- /dev/null +++ b/src/Menu/Inspectors/GameObjectWindow.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MelonLoader; +using UnhollowerRuntimeLib; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Explorer +{ + public class GameObjectWindow : WindowManager.UIWindow + { + public override Il2CppSystem.String Name { get => "GameObject Inspector"; set => Name = value; } + + public GameObject m_object; + + // gui element holders + private string m_name; + private string m_scene; + + private Vector2 m_transformScroll = Vector2.zero; + private Transform[] m_children; + + private Vector2 m_compScroll = Vector2.zero; + //private Component[] m_components; + + private float m_translateAmount = 0.3f; + private float m_rotateAmount = 50f; + private float m_scaleAmount = 0.1f; + + List m_cachedDestroyList = new List(); + //private string m_addComponentInput = ""; + + private string m_setParentInput = ""; + + public bool GetObjectAsGameObject() + { + if (Target == null) + { + MelonLogger.Log("Target is null!"); + return false; + } + + var targetType = Target.GetType(); + + if (targetType == typeof(GameObject)) + { + m_object = Target as GameObject; + return true; + } + else if (targetType == typeof(Transform)) + { + m_object = (Target as Transform).gameObject; + return true; + } + + MelonLogger.Log("Error: Target is null or not a GameObject/Transform!"); + DestroyWindow(); + return false; + } + + public override void Init() + { + if (!GetObjectAsGameObject()) + { + return; + } + + m_name = m_object.name; + m_scene = m_object.scene == null ? "null" : m_object.scene.name; + + //var listComps = new Il2CppSystem.Collections.Generic.List(); + //m_object.GetComponents(listComps); + //m_components = listComps.ToArray(); + + var list = new List(); + for (int i = 0; i < m_object.transform.childCount; i++) + { + list.Add(m_object.transform.GetChild(i)); + } + m_children = list.ToArray(); + } + + public override void Update() + { + if (!m_object && !GetObjectAsGameObject()) + { + MelonLogger.Log("Object is null! Destroying window..."); + DestroyWindow(); + } + } + + private void InspectGameObject(GameObject obj) + { + var window = WindowManager.InspectObject(obj, out bool created); + + if (created) + { + window.m_rect = new Rect(this.m_rect.x, this.m_rect.y, this.m_rect.width, this.m_rect.height); + DestroyWindow(); + } + } + + private void ReflectObject(Il2CppSystem.Object obj) + { + var window = WindowManager.InspectObject(obj, out bool created); + + if (created) + { + if (this.m_rect.x <= (Screen.width - this.m_rect.width - 100)) + { + window.m_rect = new Rect( + this.m_rect.x + this.m_rect.width + 20, + this.m_rect.y, + 550, + 700); + } + else + { + window.m_rect = new Rect(this.m_rect.x + 50, this.m_rect.y + 50, 550, 700); + } + } + } + + public override void WindowFunction(int windowID) + { + Header(); + + GUILayout.BeginArea(new Rect(5, 25, m_rect.width - 10, m_rect.height - 35), GUI.skin.box); + + scroll = GUILayout.BeginScrollView(scroll, GUI.skin.scrollView); + + GUILayout.BeginHorizontal(null); + GUILayout.Label("Scene: " + (m_scene == "" ? "n/a" : m_scene) + "", null); + if (m_scene == CppExplorer.ActiveSceneName) + { + if (GUILayout.Button("< View in Scene Explorer", new GUILayoutOption[] { GUILayout.Width(230) })) + { + ScenePage.Instance.SetTransformTarget(m_object); + MainMenu.SetCurrentPage(0); + } + } + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(null); + GUILayout.Label("Path:", new GUILayoutOption[] { GUILayout.Width(50) }); + string pathlabel = CppExplorer.GetGameObjectPath(m_object.transform); + if (m_object.transform.parent != null) + { + if (GUILayout.Button("<-", new GUILayoutOption[] { GUILayout.Width(35) })) + { + InspectGameObject(m_object.transform.parent.gameObject); + } + } + GUILayout.TextArea(pathlabel, null); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(null); + GUILayout.Label("Name:", new GUILayoutOption[] { GUILayout.Width(50) }); + GUILayout.TextArea(m_name, null); + GUILayout.EndHorizontal(); + + // --- Horizontal Columns section --- + GUILayout.BeginHorizontal(null); + + GUILayout.BeginVertical(new GUILayoutOption[] { GUILayout.Width(m_rect.width / 2 - 17) }); + TransformList(); + GUILayout.EndVertical(); + + GUILayout.BeginVertical(new GUILayoutOption[] { GUILayout.Width(m_rect.width / 2 - 17) }); + ComponentList(); + GUILayout.EndVertical(); + + GUILayout.EndHorizontal(); // end horiz columns + + GameObjectControls(); + + GUILayout.EndScrollView(); + + m_rect = WindowManager.ResizeWindow(m_rect, windowID); + + GUILayout.EndArea(); + } + + private void TransformList() + { + GUILayout.BeginVertical(GUI.skin.box, new GUILayoutOption[] { GUILayout.Height(250) }); + m_transformScroll = GUILayout.BeginScrollView(m_transformScroll, GUI.skin.scrollView); + + GUILayout.Label("Children:", null); + if (m_children != null && m_children.Length > 0) + { + foreach (var obj in m_children.Where(x => x.childCount > 0)) + { + if (!obj) + { + GUILayout.Label("null", null); + continue; + } + UIStyles.GameobjButton(obj.gameObject, InspectGameObject, false, this.m_rect.width / 2 - 60); + } + foreach (var obj in m_children.Where(x => x.childCount == 0)) + { + if (!obj) + { + GUILayout.Label("null", null); + continue; + } + UIStyles.GameobjButton(obj.gameObject, InspectGameObject, false, this.m_rect.width / 2 - 60); + } + } + else + { + GUILayout.Label("None", null); + } + + GUILayout.EndScrollView(); + GUILayout.EndVertical(); + } + + + private void ComponentList() + { + GUILayout.BeginVertical(GUI.skin.box, new GUILayoutOption[] { GUILayout.Height(250) }); + m_compScroll = GUILayout.BeginScrollView(m_compScroll, GUI.skin.scrollView); + GUILayout.Label("Components", null); + + GUI.skin.button.alignment = TextAnchor.MiddleLeft; + if (m_cachedDestroyList.Count > 0) + { + m_cachedDestroyList.Clear(); + } + + var m_components = new Il2CppSystem.Collections.Generic.List(); + m_object.GetComponentsInternal(Il2CppType.Of(), false, false, true, false, m_components); + + var ilTypeOfTransform = Il2CppType.Of(); + var ilTypeOfBehaviour = Il2CppType.Of(); + foreach (var component in m_components) + { + var ilType = component.GetIl2CppType(); + if (ilType == ilTypeOfTransform) + { + continue; + } + + GUILayout.BeginHorizontal(null); + if (ilTypeOfBehaviour.IsAssignableFrom(ilType)) + { + BehaviourEnabledBtn(component.TryCast()); + } + if (GUILayout.Button("" + ilType.Name + "", new GUILayoutOption[] { GUILayout.Width(m_rect.width / 2 - 90) })) + { + ReflectObject(component); + } + if (GUILayout.Button("-", new GUILayoutOption[] { GUILayout.Width(20) })) + { + m_cachedDestroyList.Add(component); + } + GUILayout.EndHorizontal(); + } + + + GUI.skin.button.alignment = TextAnchor.MiddleCenter; + if (m_cachedDestroyList.Count > 0) + { + for (int i = m_cachedDestroyList.Count - 1; i >= 0; i--) + { + var comp = m_cachedDestroyList[i]; + GameObject.Destroy(comp); + } + } + + GUILayout.EndScrollView(); + + //GUILayout.BeginHorizontal(null); + //m_addComponentInput = GUILayout.TextField(m_addComponentInput, new GUILayoutOption[] { GUILayout.Width(m_rect.width / 2 - 150) }); + //if (GUILayout.Button("Add Component", new GUILayoutOption[] { GUILayout.Width(120) })) + //{ + // if (HPExplorer.GetType(m_addComponentInput) is Type type && typeof(Component).IsAssignableFrom(type)) + // { + // var comp = m_object.AddComponent(type); + // var list = m_components.ToList(); + // list.Add(comp); + // m_components = list.ToArray(); + // } + // else + // { + // MelonLogger.LogWarning($"Could not get type '{m_addComponentInput}'. If it's not a typo, try the fully qualified name."); + // } + //} + //GUILayout.EndHorizontal(); + + GUILayout.EndVertical(); + } + + private void BehaviourEnabledBtn(Behaviour obj) + { + var _col = GUI.color; + bool _enabled = obj.enabled; + if (_enabled) + { + GUI.color = Color.green; + } + else + { + GUI.color = Color.red; + } + + // ------ toggle active button ------ + + _enabled = GUILayout.Toggle(_enabled, "", new GUILayoutOption[] { GUILayout.Width(18) }); + if (obj.enabled != _enabled) + { + obj.enabled = _enabled; + } + GUI.color = _col; + } + + private void GameObjectControls() + { + GUILayout.BeginVertical(GUI.skin.box, new GUILayoutOption[] { GUILayout.Width(530) }); + GUILayout.Label("GameObject Controls", null); + + GUILayout.BeginHorizontal(null); + bool m_active = m_object.activeSelf; + m_active = GUILayout.Toggle(m_active, (m_active ? "Enabled " : "Disabled") + "", + new GUILayoutOption[] { GUILayout.Width(80) }); + if (m_object.activeSelf != m_active) { m_object.SetActive(m_active); } + + UIStyles.InstantiateButton(m_object, 100); + + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(null); + + if (GUILayout.Button("Remove from parent", new GUILayoutOption[] { GUILayout.Width(160) })) + { + m_object.transform.parent = null; + } + m_setParentInput = GUILayout.TextField(m_setParentInput, new GUILayoutOption[] { GUILayout.Width(m_rect.width - 280) }); + if (GUILayout.Button("Set Parent", new GUILayoutOption[] { GUILayout.Width(80) })) + { + if (GameObject.Find(m_setParentInput) is GameObject newparent) + { + m_object.transform.parent = newparent.transform; + } + else + { + MelonLogger.LogWarning($"Could not find gameobject '{m_setParentInput}'"); + } + } + GUILayout.EndHorizontal(); + + GUILayout.BeginVertical(GUI.skin.box, null); + + var t = m_object.transform; + TranslateControl(t, TranslateType.Position, ref m_translateAmount, false); + TranslateControl(t, TranslateType.Rotation, ref m_rotateAmount, true); + TranslateControl(t, TranslateType.Scale, ref m_scaleAmount, false); + + GUILayout.EndVertical(); + + if (GUILayout.Button("Destroy", null)) + { + GameObject.Destroy(m_object); + DestroyWindow(); + return; + } + + GUILayout.EndVertical(); + } + + public enum TranslateType + { + Position, + Rotation, + Scale + } + + private void TranslateControl(Transform transform, TranslateType mode, ref float amount, bool multByTime) + { + GUILayout.BeginHorizontal(null); + GUILayout.Label("" + mode + ":", new GUILayoutOption[] { GUILayout.Width(65) }); + + Vector3 vector = Vector3.zero; + switch (mode) + { + case TranslateType.Position: vector = transform.localPosition; break; + case TranslateType.Rotation: vector = transform.localRotation.eulerAngles; break; + case TranslateType.Scale: vector = transform.localScale; break; + } + GUILayout.Label(vector.ToString(), new GUILayoutOption[] { GUILayout.Width(250) }); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(null); + GUI.skin.label.alignment = TextAnchor.MiddleRight; + + GUILayout.Label("X:", new GUILayoutOption[] { GUILayout.Width(20) }); + PlusMinusFloat(ref vector.x, amount, multByTime); + + GUILayout.Label("Y:", new GUILayoutOption[] { GUILayout.Width(20) }); + PlusMinusFloat(ref vector.y, amount, multByTime); + + GUILayout.Label("Z:", new GUILayoutOption[] { GUILayout.Width(20) }); + PlusMinusFloat(ref vector.z, amount, multByTime); + + switch (mode) + { + case TranslateType.Position: transform.localPosition = vector; break; + case TranslateType.Rotation: transform.localRotation = Quaternion.Euler(vector); break; + case TranslateType.Scale: transform.localScale = vector; break; + } + + GUILayout.Label("+/-:", new GUILayoutOption[] { GUILayout.Width(30) }); + var input = amount.ToString("F3"); + input = GUILayout.TextField(input, new GUILayoutOption[] { GUILayout.Width(40) }); + if (float.TryParse(input, out float f)) + { + amount = f; + } + + GUI.skin.label.alignment = TextAnchor.UpperLeft; + GUILayout.EndHorizontal(); + } + + private void PlusMinusFloat(ref float f, float amount, bool multByTime) + { + string s = f.ToString("F3"); + s = GUILayout.TextField(s, new GUILayoutOption[] { GUILayout.Width(60) }); + if (float.TryParse(s, out float f2)) + { + f = f2; + } + if (GUILayout.RepeatButton("-", new GUILayoutOption[] { GUILayout.Width(20) })) + { + f -= multByTime ? amount * Time.deltaTime : amount; + } + if (GUILayout.RepeatButton("+", new GUILayoutOption[] { GUILayout.Width(20) })) + { + f += multByTime ? amount * Time.deltaTime : amount; + } + } + + + } +} diff --git a/src/Menu/Inspectors/ReflectionWindow.cs b/src/Menu/Inspectors/ReflectionWindow.cs new file mode 100644 index 0000000..3a8bbff --- /dev/null +++ b/src/Menu/Inspectors/ReflectionWindow.cs @@ -0,0 +1,542 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using MelonLoader; +using UnhollowerBaseLib; +using UnityEngine; + +namespace Explorer +{ + public class ReflectionWindow : WindowManager.UIWindow + { + public override Il2CppSystem.String Name { get => "Object Reflection"; set => Name = value; } + + public Type m_objectType; + public object m_object; + + private List m_FieldInfos; + private List m_PropertyInfos; + + private bool m_autoUpdate = false; + private string m_search = ""; + public MemberFilter m_filter = MemberFilter.Property; + + public enum MemberFilter + { + Both, + Property, + Field + } + + public Type GetActualType(object m_object) + { + if (m_object is Il2CppSystem.Object ilObject) + { + var iltype = ilObject.GetIl2CppType(); + return Type.GetType(iltype.AssemblyQualifiedName); + } + else + { + return m_object.GetType(); + } + } + + public Type[] GetAllBaseTypes(object m_object) + { + var list = new List(); + + if (m_object is Il2CppSystem.Object ilObject) + { + var ilType = ilObject.GetIl2CppType(); + if (Type.GetType(ilType.AssemblyQualifiedName) is Type ilTypeToManaged) + { + list.Add(ilTypeToManaged); + + while (ilType.BaseType != null) + { + ilType = ilType.BaseType; + if (Type.GetType(ilType.AssemblyQualifiedName) is Type ilBaseTypeToManaged) + { + list.Add(ilBaseTypeToManaged); + } + } + } + } + else + { + var type = m_object.GetType(); + list.Add(type); + while (type.BaseType != null) + { + type = type.BaseType; + list.Add(type); + } + } + + return list.ToArray(); + } + + public override void Init() + { + m_object = Target; + + m_FieldInfos = new List(); + m_PropertyInfos = new List(); + + var type = GetActualType(m_object); + if (type == null) + { + MelonLogger.Log("could not get underlying type for object. ToString(): " + m_object.ToString()); + return; + } + + m_objectType = type; + GetFields(m_object); + GetProperties(m_object); + + UpdateValues(); + } + + public override void Update() + { + if (m_autoUpdate) + { + UpdateValues(); + } + } + + private void UpdateValues() + { + if (m_filter == MemberFilter.Both || m_filter == MemberFilter.Field) + { + foreach (var holder in this.m_FieldInfos) + { + if (m_search == "" || holder.fieldInfo.Name.ToLower().Contains(m_search.ToLower())) + { + holder.UpdateValue(m_object); + } + } + } + + if (m_filter == MemberFilter.Both || m_filter == MemberFilter.Property) + { + foreach (var holder in this.m_PropertyInfos) + { + if (m_search == "" || holder.propInfo.Name.ToLower().Contains(m_search.ToLower())) + { + holder.UpdateValue(m_object); + } + } + } + } + + public override void WindowFunction(int windowID) + { + try + { + Header(); + + GUILayout.BeginArea(new Rect(5, 25, m_rect.width - 10, m_rect.height - 35), GUI.skin.box); + + GUILayout.BeginHorizontal(null); + GUILayout.Label("Type: " + m_objectType.Name + "", null); + + bool unityObj = m_object is UnityEngine.Object; + + if (unityObj) + { + GUILayout.Label("Name: " + (m_object as UnityEngine.Object).name, null); + } + GUILayout.EndHorizontal(); + + if (unityObj) + { + GUILayout.BeginHorizontal(null); + + GUILayout.Label("Tools:", new GUILayoutOption[] { GUILayout.Width(80) }); + + UIStyles.InstantiateButton((UnityEngine.Object)m_object); + + if (m_object is Component comp && comp.gameObject is GameObject obj) + { + GUI.skin.label.alignment = TextAnchor.MiddleRight; + GUILayout.Label("GameObject:", null); + if (GUILayout.Button("" + obj.name + "", new GUILayoutOption[] { GUILayout.MaxWidth(m_rect.width - 350) })) + { + WindowManager.InspectObject(obj, out bool _); + } + GUI.skin.label.alignment = TextAnchor.UpperLeft; + } + + GUILayout.EndHorizontal(); + } + + UIStyles.HorizontalLine(Color.grey); + + GUILayout.BeginHorizontal(null); + GUILayout.Label("Search:", new GUILayoutOption[] { GUILayout.Width(75) }); + m_search = GUILayout.TextField(m_search, null); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(null); + GUILayout.Label("Filter:", new GUILayoutOption[] { GUILayout.Width(75) }); + FilterToggle(MemberFilter.Both, "Both"); + FilterToggle(MemberFilter.Property, "Properties"); + FilterToggle(MemberFilter.Field, "Fields"); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(null); + GUILayout.Label("Values:", new GUILayoutOption[] { GUILayout.Width(75) }); + if (GUILayout.Button("Update", new GUILayoutOption[] { GUILayout.Width(100) })) + { + UpdateValues(); + } + GUI.color = m_autoUpdate ? Color.green : Color.red; + m_autoUpdate = GUILayout.Toggle(m_autoUpdate, "Auto-update?", new GUILayoutOption[] { GUILayout.Width(100) }); + GUI.color = Color.white; + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + + scroll = GUILayout.BeginScrollView(scroll, GUI.skin.scrollView); + + GUILayout.Space(10); + + if (m_filter == MemberFilter.Both || m_filter == MemberFilter.Field) + { + UIStyles.HorizontalLine(Color.grey); + + GUILayout.Label("Fields", null); + + foreach (var holder in this.m_FieldInfos) + { + if (m_search != "" && !holder.fieldInfo.Name.ToLower().Contains(m_search.ToLower())) + { + continue; + } + + GUILayout.BeginHorizontal(new GUILayoutOption[] { GUILayout.Height(25) }); + holder.Draw(this); + GUILayout.EndHorizontal(); + } + } + + if (m_filter == MemberFilter.Both || m_filter == MemberFilter.Property) + { + UIStyles.HorizontalLine(Color.grey); + + GUILayout.Label("Properties", null); + + foreach (var holder in this.m_PropertyInfos) + { + if (m_search != "" && !holder.propInfo.Name.ToLower().Contains(m_search.ToLower())) + { + continue; + } + + GUILayout.BeginHorizontal(new GUILayoutOption[] { GUILayout.Height(25) }); + holder.Draw(this); + GUILayout.EndHorizontal(); + } + } + + GUILayout.EndScrollView(); + + m_rect = WindowManager.ResizeWindow(m_rect, windowID); + + GUILayout.EndArea(); + } + catch (Exception e) + { + MelonLogger.LogWarning("Exception on window draw. Message: " + e.Message); + DestroyWindow(); + return; + } + } + + private void FilterToggle(MemberFilter mode, string label) + { + if (m_filter == mode) + { + GUI.color = Color.green; + } + else + { + GUI.color = Color.white; + } + if (GUILayout.Button(label, new GUILayoutOption[] { GUILayout.Width(100) })) + { + m_filter = mode; + } + GUI.color = Color.white; + } + + public static bool IsList(Type t) + { + return t.IsGenericType && t.GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>)); + } + + + private void GetProperties(object m_object, List names = null) + { + if (names == null) + { + names = new List(); + } + + var types = GetAllBaseTypes(m_object); + + foreach (var type in types) + { + foreach (var pi in type.GetProperties(At.flags)) + { + if (names.Contains(pi.Name)) + { + continue; + } + names.Add(pi.Name); + + var piHolder = new PropertyInfoHolder(type, pi); + m_PropertyInfos.Add(piHolder); + } + } + } + + private void GetFields(object m_object, List names = null) + { + if (names == null) + { + names = new List(); + } + + var types = GetAllBaseTypes(m_object); + + foreach (var type in types) + { + foreach (var fi in type.GetFields(At.flags)) + { + if (names.Contains(fi.Name)) + { + continue; + } + names.Add(fi.Name); + + var fiHolder = new FieldInfoHolder(type, fi); + m_FieldInfos.Add(fiHolder); + } + } + } + + + /* ********************* + * PROPERTYINFO HOLDER + */ + + public class PropertyInfoHolder + { + public Type classType; + public PropertyInfo propInfo; + public object m_value; + + public PropertyInfoHolder(Type _type, PropertyInfo _propInfo) + { + classType = _type; + propInfo = _propInfo; + } + + public void Draw(ReflectionWindow window) + { + if (propInfo.CanWrite) + { + UIStyles.DrawMember(ref m_value, propInfo.PropertyType.Name, propInfo.Name, window.m_rect, window.m_object, SetValue); + } + else + { + UIStyles.DrawMember(ref m_value, propInfo.PropertyType.Name, propInfo.Name, window.m_rect, window.m_object); + } + } + + public void UpdateValue(object obj) + { + try + { + if (obj is Il2CppSystem.Object ilObject) + { + var declaringType = this.propInfo.DeclaringType; + if (declaringType == typeof(Il2CppObjectBase)) + { + m_value = ilObject.Pointer; + } + else + { + var cast = CppExplorer.Il2CppCast(obj, declaringType); + m_value = this.propInfo.GetValue(cast, null); + } + } + else + { + m_value = this.propInfo.GetValue(obj, null); + } + } + catch (Exception e) + { + MelonLogger.Log("Exception on PropertyInfoHolder.UpdateValue, Name: " + this.propInfo.Name); + MelonLogger.Log(e.GetType() + ", " + e.Message); + + var inner = e.InnerException; + while (inner != null) + { + MelonLogger.Log("inner: " + inner.GetType() + ", " + inner.Message); + inner = inner.InnerException; + } + + m_value = null; + } + } + + public void SetValue(object obj) + { + try + { + if (propInfo.PropertyType.IsEnum) + { + if (Enum.Parse(propInfo.PropertyType, m_value.ToString()) is object enumValue && enumValue != null) + { + m_value = enumValue; + } + } + else if (propInfo.PropertyType.IsPrimitive) + { + if (propInfo.PropertyType == typeof(float)) + { + if (float.TryParse(m_value.ToString(), out float f)) + { + m_value = f; + } + else + { + MelonLogger.LogWarning("Cannot parse " + m_value.ToString() + " to a float!"); + } + } + else if (propInfo.PropertyType == typeof(double)) + { + if (double.TryParse(m_value.ToString(), out double d)) + { + m_value = d; + } + else + { + MelonLogger.LogWarning("Cannot parse " + m_value.ToString() + " to a double!"); + } + } + else if (propInfo.PropertyType != typeof(bool)) + { + if (int.TryParse(m_value.ToString(), out int i)) + { + m_value = i; + } + else + { + MelonLogger.LogWarning("Cannot parse " + m_value.ToString() + " to an integer! type: " + propInfo.PropertyType); + } + } + } + + propInfo.SetValue(propInfo.GetAccessors()[0].IsStatic ? null : obj, m_value, null); + } + catch + { + MelonLogger.Log("Exception trying to set property " + this.propInfo.Name); + } + } + } + + + /* ********************* + * FIELDINFO HOLDER + */ + + public class FieldInfoHolder + { + public Type classType; + public FieldInfo fieldInfo; + public object m_value; + + public FieldInfoHolder(Type _type, FieldInfo _fieldInfo) + { + classType = _type; + fieldInfo = _fieldInfo; + } + + public void UpdateValue(object obj) + { + m_value = fieldInfo.GetValue(fieldInfo.IsStatic ? null : obj); + } + + public void Draw(ReflectionWindow window) + { + bool canSet = !(fieldInfo.IsLiteral && !fieldInfo.IsInitOnly); + + if (canSet) + { + UIStyles.DrawMember(ref m_value, fieldInfo.FieldType.Name, fieldInfo.Name, window.m_rect, window.m_object, SetValue); + } + else + { + UIStyles.DrawMember(ref m_value, fieldInfo.FieldType.Name, fieldInfo.Name, window.m_rect, window.m_object); + } + } + + public void SetValue(object obj) + { + if (fieldInfo.FieldType.IsEnum) + { + if (Enum.Parse(fieldInfo.FieldType, m_value.ToString()) is object enumValue && enumValue != null) + { + m_value = enumValue; + } + } + else if (fieldInfo.FieldType.IsPrimitive) + { + if (fieldInfo.FieldType == typeof(float)) + { + if (float.TryParse(m_value.ToString(), out float f)) + { + m_value = f; + } + else + { + MelonLogger.LogWarning("Cannot parse " + m_value.ToString() + " to a float!"); + } + } + else if (fieldInfo.FieldType == typeof(double)) + { + if (double.TryParse(m_value.ToString(), out double d)) + { + m_value = d; + } + else + { + MelonLogger.LogWarning("Cannot parse " + m_value.ToString() + " to a double!"); + } + } + else if (fieldInfo.FieldType != typeof(bool)) + { + if (int.TryParse(m_value.ToString(), out int i)) + { + m_value = i; + } + else + { + MelonLogger.LogWarning("Cannot parse " + m_value.ToString() + " to an integer! type: " + fieldInfo.FieldType); + } + } + } + + fieldInfo.SetValue(fieldInfo.IsStatic ? null : obj, m_value); + } + } + } +} diff --git a/src/Menu/MainMenu.cs b/src/Menu/MainMenu.cs new file mode 100644 index 0000000..b655866 --- /dev/null +++ b/src/Menu/MainMenu.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.EventSystems; +using MelonLoader; + +namespace Explorer +{ + public class MainMenu + { + public static MainMenu Instance; + + public MainMenu() + { + Instance = this; + + Pages.Add(new ScenePage()); + Pages.Add(new SearchPage()); + //Pages.Add(new ConsolePage()); + + foreach (var page in Pages) + { + page.Init(); + } + } + + public const int MainWindowID = 10; + public static Rect MainRect = new Rect(5, 5, 550, 700); + private static readonly List Pages = new List(); + private static int m_currentPage = 0; + + public static void SetCurrentPage(int index) + { + if (index < 0 || Pages.Count <= index) + { + MelonLogger.Log("cannot set page " + index); + return; + } + m_currentPage = index; + GUI.BringWindowToFront(MainWindowID); + GUI.FocusWindow(MainWindowID); + } + + public void Update() + { + Pages[m_currentPage].Update(); + } + + public void OnGUI() + { + if (CppExplorer.ShowMenu) + { + var origSkin = GUI.skin; + GUI.skin = UIStyles.WindowSkin; + + MainRect = GUI.Window(MainWindowID, MainRect, (GUI.WindowFunction)MainWindow, "IL2CPP Runtime Explorer"); + + GUI.skin = origSkin; + } + } + + private void MainWindow(int id) + { + GUI.DragWindow(new Rect(0, 0, MainRect.width - 90, 20)); + + if (GUI.Button(new Rect(MainRect.width - 90, 2, 80, 20), "Hide (F7)")) + { + CppExplorer.ShowMenu = false; + return; + } + + GUILayout.BeginArea(new Rect(5, 25, MainRect.width - 10, MainRect.height - 35), GUI.skin.box); + + MainHeader(); + + var page = Pages[m_currentPage]; + page.scroll = GUILayout.BeginScrollView(page.scroll, GUI.skin.scrollView); + page.DrawWindow(); + GUILayout.EndScrollView(); + + MainRect = WindowManager.ResizeWindow(MainRect, MainWindowID); + + GUILayout.EndArea(); + } + + private void MainHeader() + { + GUILayout.BeginHorizontal(null); + GUILayout.Label("Options:", new GUILayoutOption[] { GUILayout.Width(70) }); + GUI.skin.label.alignment = TextAnchor.MiddleRight; + GUILayout.Label("Array Limit:", new GUILayoutOption[] { GUILayout.Width(70) }); + GUI.skin.label.alignment = TextAnchor.UpperLeft; + var _input = GUILayout.TextField(CppExplorer.ArrayLimit.ToString(), new GUILayoutOption[] { GUILayout.Width(60) }); + if (int.TryParse(_input, out int _lim)) + { + CppExplorer.ArrayLimit = _lim; + } + CppExplorer.Instance.MouseInspect = GUILayout.Toggle(CppExplorer.Instance.MouseInspect, "Inspect Under Mouse (Shift + RMB)", null); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(null); + for (int i = 0; i < Pages.Count; i++) + { + if (m_currentPage == i) + GUI.color = Color.green; + else + GUI.color = Color.white; + + if (GUILayout.Button(Pages[i].Name, null)) + { + m_currentPage = i; + } + } + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + GUI.color = Color.white; + } + + public abstract class WindowPage + { + public virtual string Name { get; set; } + + public Vector2 scroll = Vector2.zero; + + public abstract void Init(); + + public abstract void DrawWindow(); + + public abstract void Update(); + } + } +} diff --git a/src/Menu/MainMenu/Console/REPL.cs b/src/Menu/MainMenu/Console/REPL.cs new file mode 100644 index 0000000..37e25e4 --- /dev/null +++ b/src/Menu/MainMenu/Console/REPL.cs @@ -0,0 +1,131 @@ +//using System; +//using System.Collections; +//using System.Collections.Generic; +//using System.Linq; +//using System.Reflection; +//using System.Text; +//using Mono.CSharp; +//using UnityEngine; +//using Attribute = System.Attribute; +//using Object = UnityEngine.Object; + +//namespace Explorer +//{ +// public class REPL : InteractiveBase +// { +// static REPL() +// { +// var go = new GameObject("UnityREPL"); +// GameObject.DontDestroyOnLoad(go); +// //go.transform.parent = HPExplorer.Instance.transform; +// MB = go.AddComponent(); +// } + +// [Documentation("MB - A dummy MonoBehaviour for accessing Unity.")] +// public static ReplHelper MB { get; } + +// [Documentation("find() - find a UnityEngine.Object of type T.")] +// public static T find() where T : Object +// { +// return MB.Find(); +// } + +// [Documentation("findAll() - find all UnityEngine.Object of type T.")] +// public static T[] findAll() where T : Object +// { +// return MB.FindAll(); +// } + +// [Documentation("runCoroutine(enumerator) - runs an IEnumerator as a Unity coroutine.")] +// public static object runCoroutine(IEnumerator i) +// { +// return MB.RunCoroutine(i); +// } + +// [Documentation("endCoroutine(co) - ends a Unity coroutine.")] +// public static void endCoroutine(Coroutine c) +// { +// MB.EndCoroutine(c); +// } + +// ////[Documentation("type() - obtain type info about a type T. Provides some Reflection helpers.")] +// ////public static TypeHelper type() +// ////{ +// //// return new TypeHelper(typeof(T)); +// ////} + +// ////[Documentation("type(obj) - obtain type info about object obj. Provides some Reflection helpers.")] +// ////public static TypeHelper type(object instance) +// ////{ +// //// return new TypeHelper(instance); +// ////} + +// //[Documentation("dir(obj) - lists all available methods and fiels of a given obj.")] +// //public static string dir(object instance) +// //{ +// // return type(instance).info(); +// //} + +// //[Documentation("dir() - lists all available methods and fields of type T.")] +// //public static string dir() +// //{ +// // return type().info(); +// //} + +// //[Documentation("findrefs(obj) - find references to the object in currently loaded components.")] +// //public static Component[] findrefs(object obj) +// //{ +// // if (obj == null) throw new ArgumentNullException(nameof(obj)); + +// // var results = new List(); +// // foreach (var component in Object.FindObjectsOfType()) +// // { +// // var type = component.GetType(); + +// // var nameBlacklist = new[] { "parent", "parentInternal", "root", "transform", "gameObject" }; +// // var typeBlacklist = new[] { typeof(bool) }; + +// // foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) +// // .Where(x => x.CanRead && !nameBlacklist.Contains(x.Name) && !typeBlacklist.Contains(x.PropertyType))) +// // { +// // try +// // { +// // if (Equals(prop.GetValue(component, null), obj)) +// // { +// // results.Add(component); +// // goto finish; +// // } +// // } +// // catch { } +// // } +// // foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) +// // .Where(x => !nameBlacklist.Contains(x.Name) && !typeBlacklist.Contains(x.FieldType))) +// // { +// // try +// // { +// // if (Equals(field.GetValue(component), obj)) +// // { +// // results.Add(component); +// // goto finish; +// // } +// // } +// // catch { } +// // } +// // finish:; +// // } + +// // return results.ToArray(); +// //} + +// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +// private class DocumentationAttribute : Attribute +// { +// public DocumentationAttribute(string doc) +// { +// Docs = doc; +// } + +// public string Docs { get; } +// } +// } +//} \ No newline at end of file diff --git a/src/Menu/MainMenu/Console/REPLHelper.cs b/src/Menu/MainMenu/Console/REPLHelper.cs new file mode 100644 index 0000000..fe97c45 --- /dev/null +++ b/src/Menu/MainMenu/Console/REPLHelper.cs @@ -0,0 +1,29 @@ +//using System.Collections; +//using MelonLoader; +//using UnityEngine; + +//namespace Explorer +//{ +// public class ReplHelper : MonoBehaviour +// { +// public T Find() where T : Object +// { +// return FindObjectOfType(); +// } + +// public T[] FindAll() where T : Object +// { +// return FindObjectsOfType(); +// } + +// public object RunCoroutine(IEnumerator enumerator) +// { +// return MelonCoroutines.Start(enumerator); +// } + +// public void EndCoroutine(Coroutine c) +// { +// StopCoroutine(c); +// } +// } +//} \ No newline at end of file diff --git a/src/Menu/MainMenu/ConsolePage.cs b/src/Menu/MainMenu/ConsolePage.cs new file mode 100644 index 0000000..12d939e --- /dev/null +++ b/src/Menu/MainMenu/ConsolePage.cs @@ -0,0 +1,224 @@ +//using System; +//using System.Collections; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using System.Threading.Tasks; +//using UnityEngine; +//using System.Reflection; +//using Mono.CSharp; +//using System.IO; +//using MelonLoader; + +//namespace Explorer +//{ +// public class ConsolePage : MainMenu.WindowPage +// { +// public override string Name { get => "Console"; set => base.Name = value; } + +// private ScriptEvaluator _evaluator; +// private readonly StringBuilder _sb = new StringBuilder(); + +// private string MethodInput = ""; +// private string UsingInput = ""; + +// public static List UsingDirectives; + +// private static readonly string[] m_defaultUsing = new string[] +// { +// "System", +// "UnityEngine", +// "System.Linq", +// "System.Collections", +// "System.Collections.Generic", +// "System.Reflection" +// }; + +// public override void Init() +// { +// try +// { +// MethodInput = @"// This is a basic REPL console used to execute a method. +//// Some common directives are added by default, you can add more below. +//// If you want to return some output, Debug.Log() it. + +//Debug.Log(""hello world"");"; + +// ResetConsole(); +// } +// catch (Exception e) +// { +// MelonLogger.Log($"Error setting up console!\r\nMessage: {e.Message}\r\nStack: {e.StackTrace}"); +// } +// } + +// public void ResetConsole() +// { +// if (_evaluator != null) +// { +// _evaluator.Dispose(); +// } + +// _evaluator = new ScriptEvaluator(new StringWriter(_sb)) { InteractiveBaseClass = typeof(REPL) }; + +// UsingDirectives = new List(); +// UsingDirectives.AddRange(m_defaultUsing); +// foreach (string asm in UsingDirectives) +// { +// Evaluate(AsmToUsing(asm)); +// } +// } + +// public string AsmToUsing(string asm, bool richtext = false) +// { +// if (richtext) +// { +// return $"using {asm};"; +// } +// return $"using {asm};"; +// } + +// public void AddUsing(string asm) +// { +// if (!UsingDirectives.Contains(asm)) +// { +// UsingDirectives.Add(asm); +// Evaluate(AsmToUsing(asm)); +// } +// } + +// public object Evaluate(string str) +// { +// object ret = VoidType.Value; + +// _evaluator.Compile(str, out var compiled); + +// try +// { +// compiled?.Invoke(ref ret); +// } +// catch (Exception e) +// { +// MelonLogger.LogWarning(e.ToString()); +// } + +// return ret; +// } + + +// public override void DrawWindow() +// { +// GUILayout.Label("REPL Console", null); + +// GUILayout.Label("Method:", null); +// MethodInput = GUILayout.TextArea(MethodInput, new GUILayoutOption[] { GUILayout.Height(300) }); + +// if (GUILayout.Button("Execute", null)) +// { +// try +// { +// MethodInput = MethodInput.Trim(); + +// if (!string.IsNullOrEmpty(MethodInput)) +// { +// var result = Evaluate(MethodInput); + +// if (result != null && !Equals(result, VoidType.Value)) +// { +// MelonLogger.Log("[Console Output]\r\n" + result.ToString()); +// } +// } +// } +// catch (Exception e) +// { +// MelonLogger.LogError("Exception compiling!\r\nMessage: " + e.Message + "\r\nStack: " + e.StackTrace); +// } +// } + +// GUILayout.Label("Using directives:", null); +// foreach (var asm in UsingDirectives) +// { +// GUILayout.Label(AsmToUsing(asm, true), null); +// } +// GUILayout.BeginHorizontal(null); +// GUILayout.Label("Add namespace:", new GUILayoutOption[] { GUILayout.Width(110) }); +// UsingInput = GUILayout.TextField(UsingInput, new GUILayoutOption[] { GUILayout.Width(150) }); +// if (GUILayout.Button("Add", new GUILayoutOption[] { GUILayout.Width(50) })) +// { +// AddUsing(UsingInput); +// } +// if (GUILayout.Button("Reset", null)) +// { +// ResetConsole(); +// } +// GUILayout.EndHorizontal(); +// } + +// public override void Update() { } + +// private class VoidType +// { +// public static readonly VoidType Value = new VoidType(); +// private VoidType() { } +// } + +// } + +// internal class ScriptEvaluator : Evaluator, IDisposable +// { +// private static readonly HashSet StdLib = +// new HashSet(StringComparer.InvariantCultureIgnoreCase) { "mscorlib", "System.Core", "System", "System.Xml" }; + +// private readonly TextWriter _logger; + +// public ScriptEvaluator(TextWriter logger) : base(BuildContext(logger)) +// { +// _logger = logger; + +// ImportAppdomainAssemblies(ReferenceAssembly); +// AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad; +// } + +// public void Dispose() +// { +// AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad; +// _logger.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); + +// var settings = new CompilerSettings +// { +// Version = LanguageVersion.Experimental, +// GenerateDebugInfo = false, +// StdLib = true, +// Target = Target.Library, +// WarningLevel = 0, +// EnhancedWarnings = false +// }; + +// return new CompilerContext(settings, reporter); +// } + +// private static void ImportAppdomainAssemblies(Action import) +// { +// foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) +// { +// string name = assembly.GetName().Name; +// if (StdLib.Contains(name)) +// continue; +// import(assembly); +// } +// } +// } +//} diff --git a/src/Menu/MainMenu/ScenePage.cs b/src/Menu/MainMenu/ScenePage.cs new file mode 100644 index 0000000..59fc7c7 --- /dev/null +++ b/src/Menu/MainMenu/ScenePage.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MelonLoader; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Explorer +{ + public class ScenePage : MainMenu.WindowPage + { + public static ScenePage Instance; + + public override string Name { get => "Scene Explorer"; set => base.Name = value; } + + // ----- Holders for GUI elements ----- // + + private string m_currentScene = ""; + + // gameobject list + private Transform m_currentTransform; + private List m_objectList = new List(); + + // search bar + private bool m_searching = false; + private string m_searchInput = ""; + private List m_searchResults = new List(); + + // ------------ Init and Update ------------ // + + public override void Init() + { + Instance = this; + } + + public void OnSceneChange() + { + m_currentScene = CppExplorer.ActiveSceneName; + + m_currentTransform = null; + CancelSearch(); + + } + + public override void Update() + { + if (!m_searching) + { + m_objectList = new List(); + if (m_currentTransform) + { + var noChildren = new List(); + for (int i = 0; i < m_currentTransform.childCount; i++) + { + var child = m_currentTransform.GetChild(i); + + if (child) + { + if (child.childCount > 0) + m_objectList.Add(child.gameObject); + else + noChildren.Add(child.gameObject); + } + } + m_objectList.AddRange(noChildren); + noChildren = null; + } + else + { + var scene = SceneManager.GetActiveScene(); + var rootObjects = scene.GetRootGameObjects(); + + // add objects with children first + foreach (var obj in rootObjects.Where(x => x.transform.childCount > 0)) + { + m_objectList.Add(obj); + } + foreach (var obj in rootObjects.Where(x => x.transform.childCount == 0)) + { + m_objectList.Add(obj); + } + } + } + } + + // --------- GUI Draw Functions --------- // + + public override void DrawWindow() + { + try + { + // Current Scene label + GUILayout.Label("Current Scene: " + m_currentScene + "", null); + + // ----- GameObject Search ----- + GUILayout.BeginHorizontal(GUI.skin.box, null); + GUILayout.Label("Search Scene:", new GUILayoutOption[] { GUILayout.Width(100) }); + m_searchInput = GUILayout.TextField(m_searchInput, null); + if (GUILayout.Button("Search", new GUILayoutOption[] { GUILayout.Width(80) })) + { + Search(); + } + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + + // ************** GameObject list *************** + + // ----- main explorer ------ + if (!m_searching) + { + if (m_currentTransform != null) + { + GUILayout.BeginHorizontal(null); + if (GUILayout.Button("<-", new GUILayoutOption[] { GUILayout.Width(35) })) + { + TraverseUp(); + } + else + { + GUILayout.Label(CppExplorer.GetGameObjectPath(m_currentTransform), null); + } + GUILayout.EndHorizontal(); + } + else + { + GUILayout.Label("Scene Root GameObjects:", null); + } + + if (m_objectList.Count > 0) + { + foreach (var obj in m_objectList) + { + UIStyles.GameobjButton(obj, SetTransformTarget, true, MainMenu.MainRect.width - 170); + } + } + else + { + // if m_currentTransform != null ... + } + } + else // ------ Scene Search results ------ + { + if (GUILayout.Button("<-", new GUILayoutOption[] { GUILayout.Width(35) })) + { + CancelSearch(); + } + + GUILayout.Label("Search Results:", null); + + if (m_searchResults.Count > 0) + { + foreach (var obj in m_searchResults) + { + UIStyles.GameobjButton(obj, SetTransformTarget, true, MainMenu.MainRect.width - 170); + } + } + else + { + GUILayout.Label("No results found!", null); + } + } + } + catch + { + m_currentTransform = null; + } + } + + + + // -------- Actual Methods (not drawing GUI) ---------- // + + public void SetTransformTarget(GameObject obj) + { + m_currentTransform = obj.transform; + CancelSearch(); + } + + public void TraverseUp() + { + if (m_currentTransform.parent != null) + { + m_currentTransform = m_currentTransform.parent; + } + else + { + m_currentTransform = null; + } + } + + public void Search() + { + m_searchResults = SearchSceneObjects(m_searchInput); + m_searching = true; + } + + public void CancelSearch() + { + m_searching = false; + } + + public List SearchSceneObjects(string _search) + { + var matches = new List(); + + foreach (var obj in Resources.FindObjectsOfTypeAll()) + { + if (obj.name.ToLower().Contains(_search.ToLower()) && obj.scene.name == CppExplorer.ActiveSceneName) + { + matches.Add(obj); + } + } + + return matches; + } + } +} diff --git a/src/Menu/MainMenu/SearchPage.cs b/src/Menu/MainMenu/SearchPage.cs new file mode 100644 index 0000000..603de38 --- /dev/null +++ b/src/Menu/MainMenu/SearchPage.cs @@ -0,0 +1,434 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using System.Reflection; +using UnityEngine.SceneManagement; +using Object = UnityEngine.Object; +using UnhollowerRuntimeLib; +using MelonLoader; +using UnhollowerBaseLib; + +namespace Explorer +{ + public class SearchPage : MainMenu.WindowPage + { + public static SearchPage Instance; + + public override string Name { get => "Advanced Search"; set => base.Name = value; } + + private string m_searchInput = ""; + private string m_typeInput = ""; + private int m_limit = 100; + + public SceneFilter SceneMode = SceneFilter.Any; + public TypeFilter TypeMode = TypeFilter.Object; + + public enum SceneFilter + { + Any, + This, + DontDestroy, + None + } + + public enum TypeFilter + { + Object, + GameObject, + Component, + Custom + } + + private List m_searchResults = new List(); + private Vector2 resultsScroll = Vector2.zero; + + public override void Init() + { + Instance = this; + } + + public void OnSceneChange() + { + m_searchResults.Clear(); + } + + public override void Update() + { + } + + public override void DrawWindow() + { + try + { + // helpers + GUILayout.BeginHorizontal(GUI.skin.box, null); + GUILayout.Label("Helpers", new GUILayoutOption[] { GUILayout.Width(70) }); + if (GUILayout.Button("Find Static Instances", new GUILayoutOption[] { GUILayout.Width(180) })) + { + m_searchResults = GetInstanceClassScanner().ToList(); + } + GUILayout.EndHorizontal(); + + // search box + SearchBox(); + + // results + GUILayout.BeginVertical(GUI.skin.box, null); + + GUI.skin.label.alignment = TextAnchor.MiddleCenter; + GUILayout.Label("Results", null); + GUI.skin.label.alignment = TextAnchor.UpperLeft; + + resultsScroll = GUILayout.BeginScrollView(resultsScroll, GUI.skin.scrollView); + + var _temprect = new Rect(MainMenu.MainRect.x, MainMenu.MainRect.y, MainMenu.MainRect.width + 160, MainMenu.MainRect.height); + + if (m_searchResults.Count > 0) + { + for (int i = 0; i < m_searchResults.Count; i++) + { + var obj = m_searchResults[i]; + + UIStyles.DrawValue(ref obj, _temprect); + } + } + else + { + GUILayout.Label("No results found!", null); + } + + GUILayout.EndScrollView(); + + GUILayout.EndVertical(); + } + catch + { + m_searchResults.Clear(); + } + } + + private void SearchBox() + { + GUILayout.BeginVertical(GUI.skin.box, null); + + // ----- GameObject Search ----- + GUI.skin.label.alignment = TextAnchor.MiddleCenter; + GUILayout.Label("Search", null); + GUI.skin.label.alignment = TextAnchor.UpperLeft; + + GUILayout.BeginHorizontal(null); + + GUILayout.Label("Name Contains:", new GUILayoutOption[] { GUILayout.Width(100) }); + m_searchInput = GUILayout.TextField(m_searchInput, new GUILayoutOption[] { GUILayout.Width(200) }); + + GUI.skin.label.alignment = TextAnchor.MiddleRight; + GUILayout.Label("Result limit:", new GUILayoutOption[] { GUILayout.Width(100) }); + var resultinput = m_limit.ToString(); + resultinput = GUILayout.TextField(resultinput, new GUILayoutOption[] { GUILayout.Width(55) }); + if (int.TryParse(resultinput, out int _i) && _i > 0) + { + m_limit = _i; + } + GUI.skin.label.alignment = TextAnchor.UpperLeft; + + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(null); + + GUILayout.Label("Class Filter:", new GUILayoutOption[] { GUILayout.Width(100) }); + ClassFilterToggle(TypeFilter.Object, "Object"); + ClassFilterToggle(TypeFilter.GameObject, "GameObject"); + ClassFilterToggle(TypeFilter.Component, "Component"); + ClassFilterToggle(TypeFilter.Custom, "Custom"); + GUILayout.EndHorizontal(); + if (TypeMode == TypeFilter.Custom) + { + GUILayout.BeginHorizontal(null); + GUI.skin.label.alignment = TextAnchor.MiddleRight; + GUILayout.Label("Custom Class:", new GUILayoutOption[] { GUILayout.Width(250) }); + GUI.skin.label.alignment = TextAnchor.UpperLeft; + m_typeInput = GUILayout.TextField(m_typeInput, new GUILayoutOption[] { GUILayout.Width(250) }); + GUILayout.EndHorizontal(); + } + + GUILayout.BeginHorizontal(null); + GUILayout.Label("Scene Filter:", new GUILayoutOption[] { GUILayout.Width(100) }); + SceneFilterToggle(SceneFilter.Any, "Any", 60); + SceneFilterToggle(SceneFilter.This, "This Scene", 100); + SceneFilterToggle(SceneFilter.DontDestroy, "DontDestroyOnLoad", 140); + SceneFilterToggle(SceneFilter.None, "No Scene", 80); + GUILayout.EndHorizontal(); + + if (GUILayout.Button("Search", null)) + { + Search(); + } + + GUILayout.EndVertical(); + } + + private void ClassFilterToggle(TypeFilter mode, string label) + { + if (TypeMode == mode) + { + GUI.color = Color.green; + } + else + { + GUI.color = Color.white; + } + if (GUILayout.Button(label, new GUILayoutOption[] { GUILayout.Width(100) })) + { + TypeMode = mode; + } + GUI.color = Color.white; + } + + private void SceneFilterToggle(SceneFilter mode, string label, float width) + { + if (SceneMode == mode) + { + GUI.color = Color.green; + } + else + { + GUI.color = Color.white; + } + if (GUILayout.Button(label, new GUILayoutOption[] { GUILayout.Width(width) })) + { + SceneMode = mode; + } + GUI.color = Color.white; + } + + + // -------------- ACTUAL METHODS (not Gui draw) ----------------- // + + // credit: ManlyMarco (RuntimeUnityEditor) + public static IEnumerable GetInstanceClassScanner() + { + var query = AppDomain.CurrentDomain.GetAssemblies() + .Where(x => !x.FullName.StartsWith("Mono")) + .SelectMany(GetTypesSafe) + .Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters); + + foreach (var type in query) + { + object obj = null; + try + { + obj = type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)?.GetValue(null, null); + } + catch + { + try + { + obj = type.GetField("Instance", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)?.GetValue(null); + } + catch + { + } + } + if (obj != null && !obj.ToString().StartsWith("Mono")) + { + yield return obj; + } + } + } + + public static IEnumerable GetTypesSafe(Assembly asm) + { + try { return asm.GetTypes(); } + catch (ReflectionTypeLoadException e) { return e.Types.Where(x => x != null); } + catch { return Enumerable.Empty(); } + } + + // ======= search functions ======= + + private void Search() + { + m_searchResults = FindAllObjectsOfType(m_searchInput, m_typeInput); + } + + private List FindAllObjectsOfType(string _search, string _type) + { + Il2CppSystem.Type type = null; + + if (TypeMode == TypeFilter.Custom) + { + try + { + var findType = CppExplorer.GetType(_type); + type = Il2CppSystem.Type.GetType(findType.AssemblyQualifiedName); + MelonLogger.Log("Got type: " + type.AssemblyQualifiedName); + } + catch (Exception e) + { + MelonLogger.Log("Exception: " + e.GetType() + ", " + e.Message + "\r\n" + e.StackTrace); + } + } + else if (TypeMode == TypeFilter.Object) + { + type = Il2CppType.Of(); + } + else if (TypeMode == TypeFilter.GameObject) + { + type = Il2CppType.Of(); + } + else if (TypeMode == TypeFilter.Component) + { + type = Il2CppType.Of(); + } + + if (!Il2CppType.Of().IsAssignableFrom(type)) + { + MelonLogger.LogError("Your Class Type must inherit from UnityEngine.Object! Leave blank to default to UnityEngine.Object"); + return new List(); + } + + var matches = new List(); + int added = 0; + + //MelonLogger.Log("Trying to get IL Type. ASM name: " + type.Assembly.GetName().Name + ", Namespace: " + type.Namespace + ", name: " + type.Name); + + //var asmName = type.Assembly.GetName().Name; + //if (asmName.Contains("UnityEngine")) + //{ + // asmName = "UnityEngine"; + //} + + //var intPtr = IL2CPP.GetIl2CppClass(asmName, type.Namespace, type.Name); + //var ilType = Il2CppType.TypeFromPointer(intPtr); + + foreach (var obj in Resources.FindObjectsOfTypeAll(type)) + { + if (added == m_limit) + { + break; + } + + if (_search != "" && !obj.name.ToLower().Contains(_search.ToLower())) + { + continue; + } + + if (SceneMode != SceneFilter.Any) + { + if (SceneMode == SceneFilter.None) + { + if (!NoSceneFilter(obj, obj.GetType())) + { + continue; + } + } + else + { + GameObject go; + + var objtype = obj.GetType(); + if (objtype == typeof(GameObject)) + { + go = obj as GameObject; + } + else if (typeof(Component).IsAssignableFrom(objtype)) + { + go = (obj as Component).gameObject; + } + else { continue; } + + if (!go) { continue; } + + if (SceneMode == SceneFilter.This) + { + if (go.scene.name != CppExplorer.ActiveSceneName || go.scene.name == "DontDestroyOnLoad") + { + continue; + } + } + else if (SceneMode == SceneFilter.DontDestroy) + { + if (go.scene.name != "DontDestroyOnLoad") + { + continue; + } + } + } + } + + if (!matches.Contains(obj)) + { + matches.Add(obj); + added++; + } + } + + return matches; + } + + public static bool ThisSceneFilter(object obj, Type type) + { + if (type == typeof(GameObject) || typeof(Component).IsAssignableFrom(type)) + { + var go = obj as GameObject ?? (obj as Component).gameObject; + + if (go != null && go.scene.name == CppExplorer.ActiveSceneName && go.scene.name != "DontDestroyOnLoad") + { + return true; + } + } + + return false; + } + + public static bool DontDestroyFilter(object obj, Type type) + { + if (type == typeof(GameObject) || typeof(Component).IsAssignableFrom(type)) + { + var go = obj as GameObject ?? (obj as Component).gameObject; + + if (go != null && go.scene.name == "DontDestroyOnLoad") + { + return true; + } + } + + return false; + } + + public static bool NoSceneFilter(object obj, Type type) + { + if (type == typeof(GameObject)) + { + var go = obj as GameObject; + + if (go.scene.name != CppExplorer.ActiveSceneName && go.scene.name != "DontDestroyOnLoad") + { + return true; + } + else + { + return false; + } + } + else if (typeof(Component).IsAssignableFrom(type)) + { + var go = (obj as Component).gameObject; + + if (go == null || (go.scene.name != CppExplorer.ActiveSceneName && go.scene.name != "DontDestroyOnLoad")) + { + return true; + } + else + { + return false; + } + } + else + { + return true; + } + } + } +} diff --git a/src/Menu/UIStyles.cs b/src/Menu/UIStyles.cs new file mode 100644 index 0000000..72f04d1 --- /dev/null +++ b/src/Menu/UIStyles.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Explorer +{ + public class UIStyles + { + public static GUISkin WindowSkin + { + get + { + if (_customSkin == null) + { + try + { + _customSkin = CreateWindowSkin(); + } + catch + { + _customSkin = GUI.skin; + } + } + + return _customSkin; + } + } + + public static void HorizontalLine(Color color) + { + var c = GUI.color; + GUI.color = color; + GUILayout.Box(GUIContent.none, HorizontalBar, null); + GUI.color = c; + } + + private static GUISkin _customSkin; + + public static Texture2D m_nofocusTex; + public static Texture2D m_focusTex; + + private static GUIStyle _horizBarStyle; + + private static GUIStyle HorizontalBar + { + get + { + if (_horizBarStyle == null) + { + _horizBarStyle = new GUIStyle(); + _horizBarStyle.normal.background = Texture2D.whiteTexture; + _horizBarStyle.margin = new RectOffset(0, 0, 4, 4); + _horizBarStyle.fixedHeight = 2; + } + + return _horizBarStyle; + } + } + + private static GUISkin CreateWindowSkin() + { + var newSkin = Object.Instantiate(GUI.skin); + Object.DontDestroyOnLoad(newSkin); + + m_nofocusTex = MakeTex(550, 700, new Color(0.1f, 0.1f, 0.1f, 0.7f)); + m_focusTex = MakeTex(550, 700, new Color(0.3f, 0.3f, 0.3f, 1f)); + + newSkin.window.normal.background = m_nofocusTex; + newSkin.window.onNormal.background = m_focusTex; + + newSkin.box.normal.textColor = Color.white; + newSkin.window.normal.textColor = Color.white; + newSkin.button.normal.textColor = Color.white; + newSkin.textField.normal.textColor = Color.white; + newSkin.label.normal.textColor = Color.white; + + return newSkin; + } + + public static Texture2D MakeTex(int width, int height, Color col) + { + Color[] pix = new Color[width * height]; + for (int i = 0; i < pix.Length; ++i) + { + pix[i] = col; + } + Texture2D result = new Texture2D(width, height); + result.SetPixels(pix); + result.Apply(); + return result; + } + + // *********************************** METHODS FOR DRAWING VALUES IN GUI ************************************ + + // helper for "Instantiate" button on UnityEngine.Objects + public static void InstantiateButton(Object obj, float width = 100) + { + if (GUILayout.Button("Instantiate", new GUILayoutOption[] { GUILayout.Width(width) })) + { + var newobj = Object.Instantiate(obj); + + WindowManager.InspectObject(newobj, out _); + } + } + + // helper for drawing a styled button for a GameObject or Transform + public static void GameobjButton(GameObject obj, Action specialInspectMethod = null, bool showSmallInspectBtn = true, float width = 380) + { + if (obj == null) + { + GUILayout.Label("null", null); + return; + } + + bool enabled = obj.activeSelf; + bool children = obj.transform.childCount > 0; + + GUILayout.BeginHorizontal(null); + GUI.skin.button.alignment = TextAnchor.UpperLeft; + + // ------ build name ------ + + string label = children ? "[" + obj.transform.childCount + " children] " : ""; + label += obj.name; + + // ------ Color ------- + + if (enabled) + { + if (children) + { + GUI.color = Color.green; + } + else + { + GUI.color = new Color(Color.green.r - 0.3f, Color.green.g - 0.3f, Color.green.b - 0.3f); + } + } + else + { + GUI.color = Color.red; + } + + // ------ toggle active button ------ + + enabled = GUILayout.Toggle(enabled, "", new GUILayoutOption[] { GUILayout.Width(18) }); + if (obj.activeSelf != enabled) + { + obj.SetActive(enabled); + } + + // ------- actual button --------- + + if (GUILayout.Button(label, new GUILayoutOption[] { GUILayout.Height(22), GUILayout.Width(width) })) + { + if (specialInspectMethod != null) + { + specialInspectMethod(obj); + } + else + { + WindowManager.InspectObject(obj, out bool _); + } + } + + // ------ small "Inspect" button on the right ------ + + GUI.skin.button.alignment = TextAnchor.MiddleCenter; + GUI.color = Color.white; + + if (showSmallInspectBtn) + { + if (GUILayout.Button("Inspect", null)) + { + WindowManager.InspectObject(obj, out bool _); + } + } + + GUILayout.EndHorizontal(); + } + + public static void DrawMember(ref object value, string valueType, string memberName, Rect rect, object setTarget = null, Action setAction = null, float labelWidth = 180, bool autoSet = false) + { + GUILayout.Label("" + memberName + ":", new GUILayoutOption[] { GUILayout.Width(labelWidth) }); + + DrawValue(ref value, rect, valueType, memberName, setTarget, setAction, autoSet); + } + + public static void DrawValue(ref object value, Rect rect, string nullValueType = null, string memberName = null, object setTarget = null, Action setAction = null, bool autoSet = false) + { + if (value == null) + { + GUILayout.Label("null (" + nullValueType + ")", null); + } + else + { + var valueType = value.GetType(); + if (valueType.IsPrimitive || value.GetType() == typeof(string)) + { + DrawPrimitive(ref value, rect, setTarget, setAction); + } + else if (valueType == typeof(GameObject) || valueType == typeof(Transform)) + { + GameObject go; + if (value.GetType() == typeof(Transform)) + { + go = (value as Transform).gameObject; + } + else + { + go = (value as GameObject); + } + + UIStyles.GameobjButton(go, null, false, rect.width - 250); + } + else if (valueType.IsEnum) + { + if (setAction != null) + { + if (GUILayout.Button("<", new GUILayoutOption[] { GUILayout.Width(25) })) + { + SetEnum(ref value, -1); + setAction.Invoke(setTarget); + } + if (GUILayout.Button(">", new GUILayoutOption[] { GUILayout.Width(25) })) + { + SetEnum(ref value, 1); + setAction.Invoke(setTarget); + } + } + + GUILayout.Label(value.ToString(), null); + + } + else if (valueType.IsArray || ReflectionWindow.IsList(valueType)) + { + object[] m_array; + if (valueType.IsArray) + { + m_array = (value as Array).Cast().ToArray(); + } + else + { + m_array = (value as IEnumerable).Cast().ToArray(); + } + + GUI.skin.button.alignment = TextAnchor.MiddleLeft; + if (GUILayout.Button("[" + m_array.Length + "] " + valueType + "", new GUILayoutOption[] { GUILayout.MaxWidth(rect.width - 230) })) + { + WindowManager.InspectObject(value, out bool _); + } + GUI.skin.button.alignment = TextAnchor.MiddleCenter; + + for (int i = 0; i < m_array.Length; i++) + { + var obj = m_array[i]; + + // collapsing the BeginHorizontal called from ReflectionWindow.WindowFunction or previous array entry + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(null); + GUILayout.Space(190); + + if (i > CppExplorer.ArrayLimit) + { + GUILayout.Label($"{m_array.Length - CppExplorer.ArrayLimit} results omitted, array is too long!", null); + break; + } + + if (obj == null) + { + GUILayout.Label("null", null); + } + else + { + var type = obj.GetType(); + DrawMember(ref obj, type.ToString(), i.ToString(), rect, setTarget, setAction, 25, true); + } + } + } + else + { + var label = value.ToString(); + + if (valueType == typeof(Object)) + { + label = (value as Object).name; + } + + GUI.skin.button.alignment = TextAnchor.MiddleLeft; + if (GUILayout.Button("" + label + "", new GUILayoutOption[] { GUILayout.MaxWidth(rect.width - 230) })) + { + WindowManager.InspectObject(value, out bool _); + } + GUI.skin.button.alignment = TextAnchor.MiddleCenter; + } + } + } + + // Helper for drawing primitive values (with Apply button) + + public static void DrawPrimitive(ref object value, Rect m_rect, object setTarget = null, Action setAction = null, bool autoSet = false) + { + bool allowSet = setAction != null; + + if (value.GetType() == typeof(bool)) + { + bool b = (bool)value; + var color = "" : "red>"); + + if (allowSet) + { + value = GUILayout.Toggle((bool)value, color + value.ToString() + "", null); + + if (b != (bool)value) + { + setAction.Invoke(setTarget); + } + } + } + else + { + if (value.ToString().Length > 37) + { + value = GUILayout.TextArea(value.ToString(), new GUILayoutOption[] { GUILayout.MaxWidth(m_rect.width - 260) }); + } + else + { + value = GUILayout.TextField(value.ToString(), new GUILayoutOption[] { GUILayout.MaxWidth(m_rect.width - 260) }); + } + + if (autoSet || (allowSet && GUILayout.Button("Apply", new GUILayoutOption[] { GUILayout.Width(60) }))) + { + setAction.Invoke(setTarget); + } + } + } + + // Helper for setting an enum + + public static void SetEnum(ref object value, int change) + { + var type = value.GetType(); + var names = Enum.GetNames(type).ToList(); + + int newindex = names.IndexOf(value.ToString()) + change; + + if ((change < 0 && newindex >= 0) || (change > 0 && newindex < names.Count)) + { + value = Enum.Parse(type, names[newindex]); + } + } + } +} diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9033bec --- /dev/null +++ b/src/Properties/AssemblyInfo.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Explorer; +using MelonLoader; + +[assembly: MelonInfo(typeof(CppExplorer), CppExplorer.NAME, CppExplorer.VERSION, CppExplorer.AUTHOR)] +[assembly: MelonGame(null, null)] + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CppExplorer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CppExplorer")] +[assembly: AssemblyCopyright("By Sinai")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b21dbde3-5d6f-4726-93ab-cc3cc68bae7d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/WindowManager.cs b/src/WindowManager.cs new file mode 100644 index 0000000..6fd8bec --- /dev/null +++ b/src/WindowManager.cs @@ -0,0 +1,258 @@ +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Harmony; +using MelonLoader; +using UnhollowerBaseLib; +using UnhollowerRuntimeLib; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.EventSystems; + +namespace Explorer +{ + public class WindowManager + { + public static WindowManager Instance; + + public static List Windows = new List(); + public static int CurrentWindowID { get; set; } = 500000; + private static Rect m_lastWindowRect; + + public WindowManager() + { + Instance = this; + } + + public void Update() + { + foreach (var window in Windows) + { + window.Update(); + } + } + + public void OnGUI() + { + foreach (var window in Windows) + { + window.OnGUI(); + } + } + + // ========= Public Helpers ========= + + public static bool IsMouseInWindow + { + get + { + if (!CppExplorer.ShowMenu) + { + return false; + } + + foreach (var window in Windows) + { + if (RectContainsMouse(window.m_rect)) + { + return true; + } + } + return RectContainsMouse(MainMenu.MainRect); + } + } + + private static bool RectContainsMouse(Rect rect) + { + return rect.Contains(new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); + } + + public static int NextWindowID() + { + return CurrentWindowID++; + } + + public static Rect GetNewWindowRect() + { + return GetNewWindowRect(ref m_lastWindowRect); + } + + public static Rect GetNewWindowRect(ref Rect lastRect) + { + Rect rect = new Rect(0, 0, 550, 700); + + var mainrect = MainMenu.MainRect; + if (mainrect.x <= (Screen.width - mainrect.width - 100)) + { + rect = new Rect(mainrect.x + mainrect.width + 20, mainrect.y, rect.width, rect.height); + } + + if (lastRect.x == rect.x) + { + rect = new Rect(rect.x + 25, rect.y + 25, rect.width, rect.height); + } + + lastRect = rect; + + return rect; + } + + public static UIWindow InspectObject(object obj, out bool createdNew) + { + createdNew = false; + + foreach (var window in Windows) + { + if (obj == window.Target) + { + GUI.BringWindowToFront(window.windowID); + GUI.FocusWindow(window.windowID); + return window; + } + } + + createdNew = true; + if (obj is GameObject || obj is Transform) + { + return InspectGameObject(obj as GameObject ?? (obj as Transform).gameObject); + } + else + { + return InspectReflection(obj); + } + } + + private static UIWindow InspectGameObject(GameObject obj) + { + var new_window = UIWindow.CreateWindow(obj); + GUI.FocusWindow(new_window.windowID); + + return new_window; + } + + public static UIWindow InspectReflection(object obj) + { + var new_window = UIWindow.CreateWindow(obj); + GUI.FocusWindow(new_window.windowID); + + return new_window; + } + + // ============= Resize Window Helper ============ + + static readonly GUIContent gcDrag = new GUIContent("<->", "drag to resize"); + + private static bool isResizing = false; + private static Rect m_currentResize; + private static int m_currentWindow; + + public static Rect ResizeWindow(Rect _rect, int ID) + { + GUILayout.BeginHorizontal(null); + GUILayout.Space(_rect.width - 35); + + GUILayout.Button(gcDrag, GUI.skin.label, new GUILayoutOption[] { GUILayout.Width(25), GUILayout.Height(25) }); + + var r = GUILayoutUtility.GetLastRect(); + + Vector2 mouse = GUIUtility.ScreenToGUIPoint(new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)); + + if (r.Contains(mouse) && Input.GetMouseButtonDown(0)) + { + isResizing = true; + m_currentWindow = ID; + m_currentResize = new Rect(mouse.x, mouse.y, _rect.width, _rect.height); + } + else if (!Input.GetMouseButton(0)) + { + isResizing = false; + } + + if (isResizing && ID == m_currentWindow) + { + _rect.width = Mathf.Max(100, m_currentResize.width + (mouse.x - m_currentResize.x)); + _rect.height = Mathf.Max(100, m_currentResize.height + (mouse.y - m_currentResize.y)); + _rect.xMax = Mathf.Min(Screen.width, _rect.xMax); // modifying xMax affects width, not x + _rect.yMax = Mathf.Min(Screen.height, _rect.yMax); // modifying yMax affects height, not y + } + + GUILayout.EndHorizontal(); + + return _rect; + } + + // ============ GENERATED WINDOW HOLDER ============ + + public abstract class UIWindow + { + public abstract Il2CppSystem.String Name { get; set; } + + public object Target; + + public int windowID; + public Rect m_rect = new Rect(0, 0, 550, 700); + + public Vector2 scroll = Vector2.zero; + + public static UIWindow CreateWindow(object target) where T: UIWindow + { + //var component = (UIWindow)AddToGameObject(Instance.gameObject); + var component = Activator.CreateInstance(); + + component.Target = target; + component.windowID = NextWindowID(); + component.m_rect = GetNewWindowRect(); + + Windows.Add(component); + + component.Init(); + + return component; + } + + public void DestroyWindow() + { + try + { + Windows.Remove(this); + } + catch (Exception e) + { + MelonLogger.Log("Exception removing Window from WindowManager.Windows list!"); + MelonLogger.Log($"{e.GetType()} : {e.Message}\r\n{e.StackTrace}"); + } + //Destroy(this); + } + + public abstract void Init(); + public abstract void WindowFunction(int windowID); + public abstract void Update(); + + public void OnGUI() + { + if (CppExplorer.ShowMenu) + { + var origSkin = GUI.skin; + + GUI.skin = UIStyles.WindowSkin; + m_rect = GUI.Window(windowID, m_rect, (GUI.WindowFunction)WindowFunction, Name); + + GUI.skin = origSkin; + } + } + + public void Header() + { + GUI.DragWindow(new Rect(0, 0, m_rect.width - 90, 20)); + + if (GUI.Button(new Rect(m_rect.width - 90, 2, 80, 20), "X")) + { + DestroyWindow(); + return; + } + } + } + } +} diff --git a/src/utils/AccessTools.cs b/src/utils/AccessTools.cs new file mode 100644 index 0000000..3d6ef49 --- /dev/null +++ b/src/utils/AccessTools.cs @@ -0,0 +1,65 @@ +using System; +using System.Reflection; + +namespace Explorer +{ + /// + /// AccessTools + /// Some helpers for Reflection (GetValue, SetValue, Call, InheritBaseValues) + /// + public static class At + { + public static Il2CppSystem.Reflection.BindingFlags ilFlags = Il2CppSystem.Reflection.BindingFlags.Public + | Il2CppSystem.Reflection.BindingFlags.NonPublic + | Il2CppSystem.Reflection.BindingFlags.Instance + | Il2CppSystem.Reflection.BindingFlags.Static; + + public static BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static; + + //reflection call + public static object Call(object obj, string method, params object[] args) + { + var methodInfo = obj.GetType().GetMethod(method, flags); + if (methodInfo != null) + { + return methodInfo.Invoke(obj, args); + } + return null; + } + + // set value + public static void SetValue(T value, Type type, object obj, string field) + { + FieldInfo fieldInfo = type.GetField(field, flags); + if (fieldInfo != null) + { + fieldInfo.SetValue(obj, value); + } + } + + // get value + public static object GetValue(Type type, object obj, string value) + { + FieldInfo fieldInfo = type.GetField(value, flags); + if (fieldInfo != null) + { + return fieldInfo.GetValue(obj); + } + else + { + return null; + } + } + + // inherit base values + public static void InheritBaseValues(object _derived, object _base) + { + foreach (FieldInfo fi in _base.GetType().GetFields(flags)) + { + try { _derived.GetType().GetField(fi.Name).SetValue(_derived, fi.GetValue(_base)); } catch { } + } + + return; + } + } +}