* Fix for games where patching Cursor methods fails.
* Added backup attempt for loading Cursor module if not present.
* HashSet collections are now supported by CacheList
* try/catch for loading Mod Config
This commit is contained in:
sinaioutlander
2020-09-11 18:53:17 +10:00
parent 835a81765e
commit 8d648fec43
20 changed files with 292 additions and 167 deletions

238
src/Menu/CursorControl.cs Normal file
View File

@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Harmony;
using MelonLoader;
using System.Reflection;
namespace Explorer
{
public class CursorControl
{
public static bool ForceUnlockMouse
{
get => m_forceUnlock;
set => SetForceUnlock(value);
}
private static bool m_forceUnlock;
private static CursorLockMode m_lastLockMode;
private static bool m_lastVisibleState;
private static bool m_currentlySettingCursor = false;
public static bool ShouldForceMouse => CppExplorer.ShowMenu && ForceUnlockMouse;
public static void Init()
{
try
{
// Check if Cursor class is loaded
if (ReflectionHelpers.GetTypeByName("UnityEngine.Cursor") == null)
{
MelonLogger.Log("Trying to manually load Cursor module...");
if (ReflectionHelpers.LoadModule("UnityEngine.CoreModule"))
{
MelonLogger.Log("Ok!");
}
else
{
throw new Exception("Could not load UnityEngine.Cursor module!");
}
}
// Get current cursor state and enable cursor
m_lastLockMode = Cursor.lockState;
m_lastVisibleState = Cursor.visible;
TryPatch("lockState", new HarmonyMethod(typeof(CursorControl).GetMethod(nameof(Prefix_set_lockState))), false, false);
TryPatch("visible", new HarmonyMethod(typeof(CursorControl).GetMethod(nameof(Prefix_set_visible))), false, false);
TryPatch("lockState", new HarmonyMethod(typeof(CursorControl).GetMethod(nameof(Postfix_get_lockState))), true, true);
TryPatch("visible", new HarmonyMethod(typeof(CursorControl).GetMethod(nameof(Postfix_get_visible))), true, true);
}
catch (Exception e)
{
MelonLogger.Log($"Exception on CursorControl.Init! {e.GetType()}, {e.Message}");
}
// Enable ShowMenu and ForceUnlockMouse
// (set m_showMenu directly to not call UpdateCursorState twice)
CppExplorer.m_showMenu = true;
ForceUnlockMouse = true;
}
private static void TryPatch(string property, HarmonyMethod patch, bool getter = true, bool postfix = false)
{
// Setup Harmony Patches
try
{
var harmony = CppExplorer.Instance.harmonyInstance;
var prop = typeof(Cursor).GetProperty(property);
harmony.Patch(getter ? prop.GetGetMethod() : prop.GetSetMethod(),
postfix ? null : patch,
postfix ? patch : null);
}
catch (Exception e)
{
MelonLogger.Log($"[NON-FATAL] Couldn't patch a method: {e.Message}");
}
}
private static void SetForceUnlock(bool unlock)
{
m_forceUnlock = unlock;
UpdateCursorControl();
}
public static void Update()
{
// Check Force-Unlock input
if (InputHelper.GetKeyDown(KeyCode.LeftAlt))
{
ForceUnlockMouse = !ForceUnlockMouse;
}
}
public static void UpdateCursorControl()
{
try
{
m_currentlySettingCursor = true;
if (ShouldForceMouse)
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
else
{
Cursor.lockState = m_lastLockMode;
Cursor.visible = m_lastVisibleState;
}
m_currentlySettingCursor = false;
}
catch (Exception e)
{
MelonLogger.Log($"Exception setting Cursor state: {e.GetType()}, {e.Message}");
}
}
// Force mouse to stay unlocked and visible while UnlockMouse and ShowMenu are true.
// Also keep track of when anything else tries to set Cursor state, this will be the
// value that we set back to when we close the menu or disable force-unlock.
[HarmonyPrefix]
public static void Prefix_set_lockState(ref CursorLockMode value)
{
if (!m_currentlySettingCursor)
{
m_lastLockMode = value;
if (ShouldForceMouse)
{
value = CursorLockMode.None;
}
}
}
[HarmonyPrefix]
public static void Prefix_set_visible(ref bool value)
{
if (!m_currentlySettingCursor)
{
m_lastVisibleState = value;
if (ShouldForceMouse)
{
value = true;
}
}
}
[HarmonyPrefix]
public static void Postfix_get_lockState(ref CursorLockMode __result)
{
if (ShouldForceMouse)
{
__result = m_lastLockMode;
}
}
[HarmonyPrefix]
public static void Postfix_get_visible(ref bool __result)
{
if (ShouldForceMouse)
{
__result = m_lastVisibleState;
}
}
//[HarmonyPatch(typeof(Cursor), nameof(Cursor.lockState), MethodType.Setter)]
//public class Cursor_set_lockState
//{
// [HarmonyPrefix]
// public static void Prefix(ref CursorLockMode value)
// {
// if (!m_currentlySettingCursor)
// {
// m_lastLockMode = value;
// if (ShouldForceMouse)
// {
// value = CursorLockMode.None;
// }
// }
// }
//}
//[HarmonyPatch(typeof(Cursor), nameof(Cursor.visible), MethodType.Setter)]
//public class Cursor_set_visible
//{
// [HarmonyPrefix]
// public static void Prefix(ref bool value)
// {
// if (!m_currentlySettingCursor)
// {
// m_lastVisibleState = value;
// if (ShouldForceMouse)
// {
// value = true;
// }
// }
// }
//}
//// Make it appear as though UnlockMouse is disabled to the rest of the application.
//[HarmonyPatch(typeof(Cursor), nameof(Cursor.lockState), MethodType.Getter)]
//public class Cursor_get_lockState
//{
// [HarmonyPostfix]
// public static void Postfix(ref CursorLockMode __result)
// {
// if (ShouldForceMouse)
// {
// __result = m_lastLockMode;
// }
// }
//}
//[HarmonyPatch(typeof(Cursor), nameof(Cursor.visible), MethodType.Getter)]
//public class Cursor_get_visible
//{
// [HarmonyPostfix]
// public static void Postfix(ref bool __result)
// {
// if (ShouldForceMouse)
// {
// __result = m_lastVisibleState;
// }
// }
//}
}
}

View File

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace Explorer
{
public class InspectUnderMouse
{
public static bool EnableInspect { get; set; } = false;
private static string m_objUnderMouseName = "";
public static void Update()
{
if (CppExplorer.ShowMenu)
{
if (InputHelper.GetKey(KeyCode.LeftShift) && InputHelper.GetMouseButtonDown(1))
{
EnableInspect = !EnableInspect;
}
if (EnableInspect)
{
InspectRaycast();
}
}
else if (EnableInspect)
{
EnableInspect = false;
}
}
public static void InspectRaycast()
{
if (!UnityHelpers.MainCamera)
return;
var ray = UnityHelpers.MainCamera.ScreenPointToRay(InputHelper.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 1000f))
{
var obj = hit.transform.gameObject;
m_objUnderMouseName = obj.transform.GetGameObjectPath();
if (InputHelper.GetMouseButtonDown(0))
{
EnableInspect = false;
m_objUnderMouseName = "";
WindowManager.InspectObject(obj, out _);
}
}
else
{
m_objUnderMouseName = "";
}
}
public static void OnGUI()
{
if (EnableInspect)
{
if (m_objUnderMouseName != "")
{
var pos = InputHelper.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, $"<color=black>{m_objUnderMouseName}</color>");
//white text
GUI.Label(new Rect(rect.x - 1, rect.y + 1, rect.width, rect.height), m_objUnderMouseName);
GUI.skin.label.alignment = origAlign;
}
}
}
}
}

View File

@ -0,0 +1,117 @@
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 = 5000;
public static Rect MainRect = new Rect(5,5, ModConfig.Instance.Default_Window_Size.x,ModConfig.Instance.Default_Window_Size.y);
private static readonly List<WindowPage> Pages = new List<WindowPage>();
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()
{
MainRect = GUI.Window(MainWindowID, MainRect, (GUI.WindowFunction)MainWindow, CppExplorer.NAME);
}
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 ({ModConfig.Instance.Main_Menu_Toggle})"))
{
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 = GUIUnstrip.BeginScrollView(page.scroll);
page.DrawWindow();
GUIUnstrip.EndScrollView();
MainRect = ResizeDrag.ResizeWindow(MainRect, MainWindowID);
GUILayout.EndArea();
}
private void MainHeader()
{
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.BeginHorizontal(null);
GUI.color = Color.white;
InspectUnderMouse.EnableInspect = GUILayout.Toggle(InspectUnderMouse.EnableInspect, "Inspect Under Mouse (Shift + RMB)", null);
bool mouseState = CursorControl.ForceUnlockMouse;
bool setMouse = GUILayout.Toggle(mouseState, "Force Unlock Mouse (Left Alt)", null);
if (setMouse != mouseState) CursorControl.ForceUnlockMouse = setMouse;
WindowManager.TabView = GUILayout.Toggle(WindowManager.TabView, "Tab View", null);
GUILayout.EndHorizontal();
GUILayout.Space(10);
GUI.color = Color.white;
}
}
}

View File

@ -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<ReplHelper>();
}
[Documentation("MB - A dummy MonoBehaviour for accessing Unity.")]
public static ReplHelper MB { get; }
[Documentation("find<T>() - find a UnityEngine.Object of type T.")]
public static T find<T>() where T : Object
{
return MB.Find<T>();
}
[Documentation("findAll<T>() - find all UnityEngine.Object of type T.")]
public static T[] findAll<T>() where T : Object
{
return MB.FindAll<T>();
}
//[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<T>() - obtain type info about a type T. Provides some Reflection helpers.")]
////public static TypeHelper type<T>()
////{
//// 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<T>() - lists all available methods and fields of type T.")]
//public static string dir<T>()
//{
// return type<T>().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<Component>();
// foreach (var component in Object.FindObjectsOfType<Component>())
// {
// 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; }
}
}
}

View File

@ -0,0 +1,34 @@
using System.Collections;
//using Il2CppSystem;
using MelonLoader;
using UnityEngine;
using System;
using Object = UnityEngine.Object;
namespace Explorer
{
public class ReplHelper : MonoBehaviour
{
public ReplHelper(IntPtr intPtr) : base(intPtr) { }
public T Find<T>() where T : Object
{
return FindObjectOfType<T>();
}
public T[] FindAll<T>() where T : Object
{
return FindObjectsOfType<T>();
}
//public object RunCoroutine(IEnumerator enumerator)
//{
// return MelonCoroutines.Start(enumerator);
//}
//public void EndCoroutine(Coroutine c)
//{
// StopCoroutine(c);
//}
}
}

View File

@ -0,0 +1,246 @@
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 : WindowPage
{
public override string Name { get => "C# Console"; }
private ScriptEvaluator _evaluator;
private readonly StringBuilder _sb = new StringBuilder();
private Vector2 inputAreaScroll;
private string MethodInput = "";
private string UsingInput = "";
public static List<string> UsingDirectives;
private static readonly string[] m_defaultUsing = new string[]
{
"System",
"UnityEngine",
"System.Linq",
"System.Collections",
"System.Collections.Generic",
"System.Reflection",
"MelonLoader"
};
public override void Init()
{
UnhollowerRuntimeLib.ClassInjector.RegisterTypeInIl2Cpp<ReplHelper>();
try
{
MethodInput = @"// This is a basic C# REPL console.
// Some common using directives are added by default, you can add more below.
// If you want to return some output, MelonLogger.Log() it.
MelonLogger.Log(""hello world"");";
ResetConsole();
foreach (var use in m_defaultUsing)
{
AddUsing(use);
}
}
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<string>();
}
public string AsmToUsing(string asm, bool richtext = false)
{
if (richtext)
{
return $"<color=#569cd6>using</color> {asm};";
}
return $"using {asm};";
}
public void AddUsing(string asm)
{
if (!UsingDirectives.Contains(asm))
{
UsingDirectives.Add(asm);
Evaluate(AsmToUsing(asm), true);
}
}
public object Evaluate(string str, bool suppressWarning = false)
{
object ret = VoidType.Value;
_evaluator.Compile(str, out var compiled);
try
{
if (compiled == null)
{
throw new Exception("Mono.Csharp Service was unable to compile the code provided.");
}
compiled.Invoke(ref ret);
}
catch (Exception e)
{
if (!suppressWarning)
{
MelonLogger.LogWarning(e.GetType() + ", " + e.Message);
}
}
return ret;
}
public override void DrawWindow()
{
GUILayout.Label("<b><size=15><color=cyan>C# REPL Console</color></size></b>", null);
GUI.skin.label.alignment = TextAnchor.UpperLeft;
GUILayout.Label("Enter code here as though it is a method body:", null);
inputAreaScroll = GUIUnstrip.BeginScrollView(inputAreaScroll, new GUILayoutOption[] { GUILayout.Height(250) });
MethodInput = GUILayout.TextArea(MethodInput, new GUILayoutOption[] { GUILayout.ExpandHeight(true) });
GUIUnstrip.EndScrollView();
if (GUILayout.Button("<color=cyan><b>Execute</b></color>", 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("<b>Using directives:</b>", null);
GUILayout.BeginHorizontal(null);
GUILayout.Label("Add namespace:", new GUILayoutOption[] { GUILayout.Width(105) });
UsingInput = GUILayout.TextField(UsingInput, new GUILayoutOption[] { GUILayout.Width(150) });
if (GUILayout.Button("<b><color=lime>Add</color></b>", new GUILayoutOption[] { GUILayout.Width(120) }))
{
AddUsing(UsingInput);
}
if (GUILayout.Button("<b><color=red>Clear All</color></b>", new GUILayoutOption[] { GUILayout.Width(120) }))
{
ResetConsole();
}
GUILayout.EndHorizontal();
foreach (var asm in UsingDirectives)
{
GUILayout.Label(AsmToUsing(asm, true), null);
}
}
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<string> StdLib =
new HashSet<string>(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<Assembly> import)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
string name = assembly.GetName().Name;
if (StdLib.Contains(name))
continue;
import(assembly);
}
}
}
}

View File

@ -0,0 +1,454 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MelonLoader;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Explorer
{
public class ScenePage : WindowPage
{
public static ScenePage Instance;
public override string Name { get => "Scene Explorer"; }
public PageHelper Pages = new PageHelper();
private float m_timeOfLastUpdate = -1f;
private const int PASSIVE_UPDATE_INTERVAL = 1;
private static bool m_getRootObjectsFailed;
private static string m_currentScene = "";
// gameobject list
private Transform m_currentTransform;
private readonly List<GameObjectCache> m_objectList = new List<GameObjectCache>();
// search bar
private bool m_searching = false;
private string m_searchInput = "";
private List<GameObjectCache> m_searchResults = new List<GameObjectCache>();
public override void Init()
{
Instance = this;
}
public void OnSceneChange()
{
m_currentScene = UnityHelpers.ActiveSceneName;
SetTransformTarget(null);
}
public void SetTransformTarget(Transform t)
{
m_currentTransform = t;
if (m_searching)
CancelSearch();
Update_Impl(true);
}
public void TraverseUp()
{
if (m_currentTransform.parent != null)
{
SetTransformTarget(m_currentTransform.parent);
}
else
{
SetTransformTarget(null);
}
}
public void Search()
{
m_searchResults = SearchSceneObjects(m_searchInput);
m_searching = true;
Pages.ItemCount = m_searchResults.Count;
}
public void CancelSearch()
{
m_searching = false;
if (m_getRootObjectsFailed && !m_currentTransform)
{
GetRootObjectsManual_Impl();
}
}
public List<GameObjectCache> SearchSceneObjects(string _search)
{
var matches = new List<GameObjectCache>();
foreach (var obj in Resources.FindObjectsOfTypeAll(ReflectionHelpers.GameObjectType))
{
var go = obj.TryCast<GameObject>();
if (go.name.ToLower().Contains(_search.ToLower()) && go.scene.name == m_currentScene)
{
matches.Add(new GameObjectCache(go));
}
}
return matches;
}
public override void Update()
{
if (m_searching) return;
if (Time.time - m_timeOfLastUpdate < PASSIVE_UPDATE_INTERVAL) return;
m_timeOfLastUpdate = Time.time;
Update_Impl();
}
private void Update_Impl(bool manual = false)
{
List<Transform> allTransforms = new List<Transform>();
// get current list of all transforms (either scene root or our current transform children)
if (m_currentTransform)
{
for (int i = 0; i < m_currentTransform.childCount; i++)
{
allTransforms.Add(m_currentTransform.GetChild(i));
}
}
else
{
if (!m_getRootObjectsFailed)
{
try
{
var scene = SceneManager.GetSceneByName(m_currentScene);
allTransforms.AddRange(scene.GetRootGameObjects()
.Select(it => it.transform));
}
catch
{
MelonLogger.Log("Exception getting root scene objects, falling back to backup method...");
m_getRootObjectsFailed = true;
allTransforms.AddRange(GetRootObjectsManual_Impl());
}
}
else
{
if (!manual)
{
return;
}
allTransforms.AddRange(GetRootObjectsManual_Impl());
}
}
Pages.ItemCount = allTransforms.Count;
int offset = Pages.CalculateOffsetIndex();
// sort by childcount
allTransforms.Sort((a, b) => b.childCount.CompareTo(a.childCount));
m_objectList.Clear();
for (int i = offset; i < offset + Pages.ItemsPerPage && i < Pages.ItemCount; i++)
{
var child = allTransforms[i];
m_objectList.Add(new GameObjectCache(child.gameObject));
}
}
private IEnumerable<Transform> GetRootObjectsManual_Impl()
{
try
{
var array = Resources.FindObjectsOfTypeAll(ReflectionHelpers.TransformType);
var list = new List<Transform>();
foreach (var obj in array)
{
var transform = obj.TryCast<Transform>();
if (transform.parent == null && transform.gameObject.scene.name == m_currentScene)
{
list.Add(transform);
}
}
return list;
}
catch (Exception e)
{
MelonLogger.Log("Exception getting root scene objects (manual): "
+ e.GetType() + ", " + e.Message + "\r\n"
+ e.StackTrace);
return new Transform[0];
}
}
// --------- GUI Draw Function --------- //
public override void DrawWindow()
{
try
{
DrawHeaderArea();
GUILayout.BeginVertical(GUI.skin.box, null);
DrawPageButtons();
if (!m_searching)
{
DrawGameObjectList();
}
else
{
DrawSearchResultsList();
}
GUILayout.EndVertical();
}
catch
{
// supress
}
}
private void DrawHeaderArea()
{
GUILayout.BeginHorizontal(null);
// Current Scene label
GUILayout.Label("Current Scene:", new GUILayoutOption[] { GUILayout.Width(120) });
SceneChangeButtons();
GUILayout.Label("<color=cyan>" + m_currentScene + "</color>", null); //new GUILayoutOption[] { GUILayout.Width(250) });
GUILayout.EndHorizontal();
// ----- GameObject Search -----
GUILayout.BeginHorizontal(GUI.skin.box, null);
GUILayout.Label("<b>Search Scene:</b>", 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(5);
}
private void SceneChangeButtons()
{
// Need to do 'ToList()' so the object isn't cleaned up by Il2Cpp GC.
var scenes = SceneManager.GetAllScenes().ToList();
if (scenes.Count > 1)
{
int changeWanted = 0;
if (GUILayout.Button("<", new GUILayoutOption[] { GUILayout.Width(30) }))
{
changeWanted = -1;
}
if (GUILayout.Button(">", new GUILayoutOption[] { GUILayout.Width(30) }))
{
changeWanted = 1;
}
if (changeWanted != 0)
{
int index = scenes.IndexOf(SceneManager.GetSceneByName(m_currentScene));
index += changeWanted;
if (index > scenes.Count - 1)
{
index = 0;
}
else if (index < 0)
{
index = scenes.Count - 1;
}
m_currentScene = scenes[index].name;
}
}
}
private void DrawPageButtons()
{
GUILayout.BeginHorizontal(null);
Pages.DrawLimitInputArea();
if (Pages.ItemCount > Pages.ItemsPerPage)
{
if (GUILayout.Button("< Prev", new GUILayoutOption[] { GUILayout.Width(80) }))
{
Pages.TurnPage(Turn.Left, ref this.scroll);
Update_Impl(true);
}
Pages.CurrentPageLabel();
if (GUILayout.Button("Next >", new GUILayoutOption[] { GUILayout.Width(80) }))
{
Pages.TurnPage(Turn.Right, ref this.scroll);
Update_Impl(true);
}
}
GUILayout.EndHorizontal();
GUI.skin.label.alignment = TextAnchor.UpperLeft;
}
private void DrawGameObjectList()
{
if (m_currentTransform != null)
{
GUILayout.BeginHorizontal(null);
if (GUILayout.Button("<-", new GUILayoutOption[] { GUILayout.Width(35) }))
{
TraverseUp();
}
else
{
GUILayout.Label("<color=cyan>" + m_currentTransform.GetGameObjectPath() + "</color>",
new GUILayoutOption[] { GUILayout.Width(MainMenu.MainRect.width - 187f) });
}
UIHelpers.SmallInspectButton(m_currentTransform);
GUILayout.EndHorizontal();
}
else
{
GUILayout.Label("Scene Root GameObjects:", null);
if (m_getRootObjectsFailed)
{
if (GUILayout.Button("Update Root Object List (auto-update failed!)", null))
{
Update_Impl(true);
}
}
}
if (m_objectList.Count > 0)
{
for (int i = 0; i < m_objectList.Count; i++)
{
var obj = m_objectList[i];
if (obj == null) continue;
if (!obj.RefGameObject)
{
string label = "<color=red><i>null";
if (obj.RefGameObject != null)
{
label += " (Destroyed)";
}
label += "</i></color>";
GUILayout.Label(label, null);
}
else
{
UIHelpers.GOButton_Impl(obj.RefGameObject,
obj.EnabledColor,
obj.Label,
obj.RefGameObject.activeSelf,
SetTransformTarget,
true,
MainMenu.MainRect.width - 170);
}
}
}
}
private void DrawSearchResultsList()
{
if (GUILayout.Button("<- Cancel Search", new GUILayoutOption[] { GUILayout.Width(150) }))
{
CancelSearch();
}
GUILayout.Label("Search Results:", null);
if (m_searchResults.Count > 0)
{
int offset = Pages.CalculateOffsetIndex();
for (int i = offset; i < offset + Pages.ItemsPerPage && i < m_searchResults.Count; i++)
{
var obj = m_searchResults[i];
if (obj.RefGameObject)
{
UIHelpers.GOButton_Impl(obj.RefGameObject,
obj.EnabledColor,
obj.Label,
obj.RefGameObject.activeSelf,
SetTransformTarget,
true,
MainMenu.MainRect.width - 170);
}
else
{
GUILayout.Label("<i><color=red>Null or destroyed!</color></i>", null);
}
}
}
else
{
GUILayout.Label("<color=red><i>No results found!</i></color>", null);
}
}
// -------- Mini GameObjectCache class ---------- //
public class GameObjectCache
{
public GameObject RefGameObject;
public string Label;
public Color EnabledColor;
public int ChildCount;
public GameObjectCache(GameObject obj)
{
RefGameObject = obj;
ChildCount = obj.transform.childCount;
Label = (ChildCount > 0) ? "[" + obj.transform.childCount + " children] " : "";
Label += obj.name;
bool enabled = obj.activeSelf;
int childCount = obj.transform.childCount;
if (enabled)
{
if (childCount > 0)
{
EnabledColor = Color.green;
}
else
{
EnabledColor = UIStyles.LightGreen;
}
}
else
{
EnabledColor = Color.red;
}
}
}
}
}

View File

@ -0,0 +1,441 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using System.Reflection;
using MelonLoader;
namespace Explorer
{
public class SearchPage : WindowPage
{
public static SearchPage Instance;
public override string Name { get => "Object Search"; }
private string m_searchInput = "";
private string m_typeInput = "";
private Vector2 resultsScroll = Vector2.zero;
public PageHelper Pages = new PageHelper();
private List<CacheObjectBase> m_searchResults = new List<CacheObjectBase>();
public SceneFilter SceneMode = SceneFilter.Any;
public TypeFilter TypeMode = TypeFilter.Object;
public enum SceneFilter
{
Any,
This,
DontDestroy,
None
}
public enum TypeFilter
{
Object,
GameObject,
Component,
Custom
}
public override void Init()
{
Instance = this;
}
public void OnSceneChange()
{
m_searchResults.Clear();
Pages.PageOffset = 0;
}
public override void Update()
{
}
private void CacheResults(IEnumerable results)
{
m_searchResults = new List<CacheObjectBase>();
foreach (var obj in results)
{
var toCache = obj;
if (toCache is Il2CppSystem.Object ilObject)
{
toCache = ilObject.TryCast<GameObject>() ?? ilObject.TryCast<Transform>()?.gameObject ?? ilObject;
}
var cache = CacheObjectBase.GetCacheObject(toCache);
m_searchResults.Add(cache);
}
Pages.ItemCount = m_searchResults.Count;
Pages.PageOffset = 0;
}
public override void DrawWindow()
{
try
{
// helpers
GUILayout.BeginHorizontal(GUI.skin.box, null);
GUILayout.Label("<b><color=orange>Helpers</color></b>", new GUILayoutOption[] { GUILayout.Width(70) });
if (GUILayout.Button("Find Static Instances", new GUILayoutOption[] { GUILayout.Width(180) }))
{
//m_searchResults = GetInstanceClassScanner().ToList();
CacheResults(GetInstanceClassScanner());
}
GUILayout.EndHorizontal();
// search box
SearchBox();
// results
GUILayout.BeginVertical(GUI.skin.box, null);
GUI.skin.label.alignment = TextAnchor.MiddleCenter;
GUILayout.Label("<b><color=orange>Results </color></b>" + " (" + m_searchResults.Count + ")", null);
GUI.skin.label.alignment = TextAnchor.UpperLeft;
int count = m_searchResults.Count;
GUILayout.BeginHorizontal(null);
Pages.DrawLimitInputArea();
if (count > Pages.ItemsPerPage)
{
// prev/next page buttons
if (Pages.ItemCount > Pages.ItemsPerPage)
{
if (GUILayout.Button("< Prev", new GUILayoutOption[] { GUILayout.Width(80) }))
{
Pages.TurnPage(Turn.Left, ref this.resultsScroll);
}
Pages.CurrentPageLabel();
if (GUILayout.Button("Next >", new GUILayoutOption[] { GUILayout.Width(80) }))
{
Pages.TurnPage(Turn.Right, ref this.resultsScroll);
}
}
}
GUILayout.EndHorizontal();
resultsScroll = GUIUnstrip.BeginScrollView(resultsScroll);
var _temprect = new Rect(MainMenu.MainRect.x, MainMenu.MainRect.y, MainMenu.MainRect.width + 160, MainMenu.MainRect.height);
if (m_searchResults.Count > 0)
{
int offset = Pages.CalculateOffsetIndex();
for (int i = offset; i < offset + Pages.ItemsPerPage && i < count; i++)
{
m_searchResults[i].Draw(MainMenu.MainRect, 0f);
}
}
else
{
GUILayout.Label("<color=red><i>No results found!</i></color>", null);
}
GUIUnstrip.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("<b><color=orange>Search</color></b>", 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) });
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("<b><color=cyan>Search</color></b>", 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) ----------------- //
// ======= search functions =======
private void Search()
{
Pages.PageOffset = 0;
CacheResults(FindAllObjectsOfType(m_searchInput, m_typeInput));
}
private List<object> FindAllObjectsOfType(string searchQuery, string typeName)
{
Il2CppSystem.Type searchType = null;
if (TypeMode == TypeFilter.Custom)
{
try
{
if (ReflectionHelpers.GetTypeByName(typeName) is Type t)
{
searchType = Il2CppSystem.Type.GetType(t.AssemblyQualifiedName);
}
else
{
throw new Exception($"Could not find a Type by the name of '{typeName}'!");
}
}
catch (Exception e)
{
MelonLogger.Log("Exception getting Search Type: " + e.GetType() + ", " + e.Message);
}
}
else if (TypeMode == TypeFilter.Object)
{
searchType = ReflectionHelpers.ObjectType;
}
else if (TypeMode == TypeFilter.GameObject)
{
searchType = ReflectionHelpers.GameObjectType;
}
else if (TypeMode == TypeFilter.Component)
{
searchType = ReflectionHelpers.ComponentType;
}
if (!ReflectionHelpers.ObjectType.IsAssignableFrom(searchType))
{
if (searchType != null)
{
MelonLogger.LogWarning("Your Custom Class Type must inherit from UnityEngine.Object!");
}
return new List<object>();
}
var matches = new List<object>();
var allObjectsOfType = Resources.FindObjectsOfTypeAll(searchType);
//MelonLogger.Log("Found count: " + allObjectsOfType.Length);
int i = 0;
foreach (var obj in allObjectsOfType)
{
if (i >= 2000) break;
if (searchQuery != "" && !obj.name.ToLower().Contains(searchQuery.ToLower()))
{
continue;
}
if (searchType.FullName == ReflectionHelpers.ComponentType.FullName
&& ReflectionHelpers.TransformType.IsAssignableFrom(obj.GetIl2CppType()))
{
// Transforms shouldn't really be counted as Components, skip them.
// They're more akin to GameObjects.
continue;
}
if (SceneMode != SceneFilter.Any && !FilterScene(obj, this.SceneMode))
{
continue;
}
if (!matches.Contains(obj))
{
matches.Add(obj);
}
i++;
}
return matches;
}
public static bool FilterScene(object obj, SceneFilter filter)
{
GameObject go;
if (obj is Il2CppSystem.Object ilObject)
{
go = ilObject.TryCast<GameObject>() ?? ilObject.TryCast<Component>().gameObject;
}
else
{
go = (obj as GameObject) ?? (obj as Component).gameObject;
}
if (!go)
{
// object is not on a GameObject, cannot perform scene filter operation.
return false;
}
if (filter == SceneFilter.None)
{
return string.IsNullOrEmpty(go.scene.name);
}
else if (filter == SceneFilter.This)
{
return go.scene.name == UnityHelpers.ActiveSceneName;
}
else if (filter == SceneFilter.DontDestroy)
{
return go.scene.name == "DontDestroyOnLoad";
}
return false;
}
// ====== other ========
private static bool FilterName(string name)
{
// Don't really want these instances.
return !name.StartsWith("Mono")
&& !name.StartsWith("System")
&& !name.StartsWith("Il2CppSystem")
&& !name.StartsWith("Iced");
}
// credit: ManlyMarco (RuntimeUnityEditor)
public static IEnumerable<object> GetInstanceClassScanner()
{
var query = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.TryGetTypes())
.Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters);
var flags = BindingFlags.Public | BindingFlags.Static;
var flatFlags = flags | BindingFlags.FlattenHierarchy;
foreach (var type in query)
{
object obj = null;
try
{
var pi = type.GetProperty("Instance", flags);
if (pi == null)
{
pi = type.GetProperty("Instance", flatFlags);
}
if (pi != null)
{
obj = pi.GetValue(null);
}
else
{
var fi = type.GetField("Instance", flags);
if (fi == null)
{
fi = type.GetField("Instance", flatFlags);
}
if (fi != null)
{
obj = fi.GetValue(null);
}
}
}
catch { }
if (obj != null)
{
var t = ReflectionHelpers.GetActualType(obj);
if (!FilterName(t.FullName) || ReflectionHelpers.IsEnumerable(t) || ReflectionHelpers.IsCppEnumerable(t))
{
continue;
}
yield return obj;
}
}
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace Explorer
{
public abstract class WindowPage
{
public virtual string Name { get; }
public Vector2 scroll = Vector2.zero;
public abstract void Init();
public abstract void DrawWindow();
public abstract void Update();
}
}

124
src/Menu/UIStyles.cs Normal file
View File

@ -0,0 +1,124 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Explorer
{
public class UIStyles
{
public static Color LightGreen = new Color(Color.green.r - 0.3f, Color.green.g - 0.3f, Color.green.b - 0.3f);
public static GUISkin WindowSkin
{
get
{
if (_customSkin == null)
{
try
{
_customSkin = CreateWindowSkin();
}
catch
{
_customSkin = GUI.skin;
}
}
return _customSkin;
}
}
public static void HorizontalLine(Color _color, bool small = false)
{
var orig = GUI.color;
GUI.color = _color;
GUILayout.Box(GUIContent.none, !small ? HorizontalBar : HorizontalBarSmall, null);
GUI.color = orig;
}
private static GUISkin _customSkin;
public static Texture2D m_nofocusTex;
public static Texture2D m_focusTex;
private static GUIStyle HorizontalBar
{
get
{
if (_horizBarStyle == null)
{
_horizBarStyle = new GUIStyle();
_horizBarStyle.normal.background = Texture2D.whiteTexture;
var rectOffset = new RectOffset();
rectOffset.top = 4;
rectOffset.bottom = 4;
_horizBarStyle.margin = rectOffset;
_horizBarStyle.fixedHeight = 2;
}
return _horizBarStyle;
}
}
private static GUIStyle _horizBarStyle;
private static GUIStyle HorizontalBarSmall
{
get
{
if (_horizBarSmallStyle == null)
{
_horizBarSmallStyle = new GUIStyle();
_horizBarSmallStyle.normal.background = Texture2D.whiteTexture;
var rectOffset = new RectOffset();
rectOffset.top = 2;
rectOffset.bottom = 2;
_horizBarSmallStyle.margin = rectOffset;
_horizBarSmallStyle.fixedHeight = 1;
}
return _horizBarSmallStyle;
}
}
private static GUIStyle _horizBarSmallStyle;
private static GUISkin CreateWindowSkin()
{
var newSkin = Object.Instantiate(GUI.skin);
Object.DontDestroyOnLoad(newSkin);
m_nofocusTex = MakeTex(1, 1, new Color(0.1f, 0.1f, 0.1f, 0.7f));
m_focusTex = MakeTex(1, 1, 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;
}
}
}

View File

@ -0,0 +1,652 @@
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 : UIWindow
{
public override string Title => WindowManager.TabView
? $"<color=cyan>[G]</color> {m_object.name}"
: $"GameObject Inspector ({m_object.name})";
public GameObject m_object;
// gui element holders
private string m_name;
private string m_scene;
private Transform[] m_children;
private Vector2 m_transformScroll = Vector2.zero;
private PageHelper ChildPages = new PageHelper();
private Component[] m_components;
private Vector2 m_compScroll = Vector2.zero;
private PageHelper CompPages = new PageHelper();
private readonly Vector3[] m_cachedInput = new Vector3[3];
private float m_translateAmount = 0.3f;
private float m_rotateAmount = 50f;
private float m_scaleAmount = 0.1f;
private bool m_freeze;
private Vector3 m_frozenPosition;
private Quaternion m_frozenRotation;
private Vector3 m_frozenScale;
private bool m_autoApplyTransform;
private bool m_autoUpdateTransform;
private bool m_localContext;
private readonly List<Component> m_cachedDestroyList = new List<Component>();
//private string m_addComponentInput = "";
private string m_setParentInput = "Enter a GameObject name or path";
public bool GetObjectAsGameObject()
{
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 = string.IsNullOrEmpty(m_object.scene.name)
? "None (Asset/Resource)"
: m_object.scene.name;
CacheTransformValues();
Update();
}
private void CacheTransformValues()
{
if (m_localContext)
{
m_cachedInput[0] = m_object.transform.localPosition;
m_cachedInput[1] = m_object.transform.localEulerAngles;
}
else
{
m_cachedInput[0] = m_object.transform.position;
m_cachedInput[1] = m_object.transform.eulerAngles;
}
m_cachedInput[2] = m_object.transform.localScale;
}
public override void Update()
{
try
{
if (Target == null)
{
MelonLogger.Log("Target is null!");
DestroyWindow();
return;
}
else if (Target is UnityEngine.Object uObj)
{
if (!uObj)
{
MelonLogger.Log("Target was destroyed!");
DestroyWindow();
return;
}
}
if (!m_object && !GetObjectAsGameObject())
{
throw new Exception("Object is null!");
}
if (m_freeze)
{
if (m_localContext)
{
m_object.transform.localPosition = m_frozenPosition;
m_object.transform.localRotation = m_frozenRotation;
}
else
{
m_object.transform.position = m_frozenPosition;
m_object.transform.rotation = m_frozenRotation;
}
m_object.transform.localScale = m_frozenScale;
}
// update child objects
var childList = new List<Transform>();
for (int i = 0; i < m_object.transform.childCount; i++)
{
childList.Add(m_object.transform.GetChild(i));
}
childList.Sort((a, b) => b.childCount.CompareTo(a.childCount));
m_children = childList.ToArray();
ChildPages.ItemCount = m_children.Length;
// update components
var compList = new Il2CppSystem.Collections.Generic.List<Component>();
m_object.GetComponentsInternal(ReflectionHelpers.ComponentType, true, false, true, false, compList);
m_components = compList.ToArray();
CompPages.ItemCount = m_components.Length;
}
catch (Exception e)
{
DestroyOnException(e);
}
}
private void DestroyOnException(Exception e)
{
MelonLogger.Log($"Exception drawing GameObject Window: {e.GetType()}, {e.Message}");
DestroyWindow();
}
private void InspectGameObject(Transform 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)
{
try
{
var rect = WindowManager.TabView ? TabViewWindow.Instance.m_rect : this.m_rect;
if (!WindowManager.TabView)
{
Header();
GUILayout.BeginArea(new Rect(5, 25, rect.width - 10, rect.height - 35), GUI.skin.box);
}
scroll = GUIUnstrip.BeginScrollView(scroll);
GUILayout.BeginHorizontal(null);
GUILayout.Label("Scene: <color=cyan>" + (m_scene == "" ? "n/a" : m_scene) + "</color>", null);
if (m_scene == UnityHelpers.ActiveSceneName)
{
if (GUILayout.Button("<color=#00FF00>Send to Scene View</color>", new GUILayoutOption[] { GUILayout.Width(150) }))
{
ScenePage.Instance.SetTransformTarget(m_object.transform);
MainMenu.SetCurrentPage(0);
}
}
if (GUILayout.Button("Reflection Inspect", new GUILayoutOption[] { GUILayout.Width(150) }))
{
WindowManager.InspectObject(Target, out _, true);
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Label("Path:", new GUILayoutOption[] { GUILayout.Width(50) });
string pathlabel = m_object.transform.GetGameObjectPath();
if (m_object.transform.parent != null)
{
if (GUILayout.Button("<-", new GUILayoutOption[] { GUILayout.Width(35) }))
{
InspectGameObject(m_object.transform.parent);
}
}
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(rect.width / 2 - 17) });
TransformList(rect);
GUILayout.EndVertical();
GUILayout.BeginVertical(new GUILayoutOption[] { GUILayout.Width(rect.width / 2 - 17) });
ComponentList(rect);
GUILayout.EndVertical();
GUILayout.EndHorizontal(); // end horiz columns
GameObjectControls();
GUIUnstrip.EndScrollView();
if (!WindowManager.TabView)
{
m_rect = ResizeDrag.ResizeWindow(rect, windowID);
GUILayout.EndArea();
}
}
catch (Exception e)
{
DestroyOnException(e);
}
}
private void TransformList(Rect m_rect)
{
GUILayout.BeginVertical(GUI.skin.box, null);
m_transformScroll = GUIUnstrip.BeginScrollView(m_transformScroll);
GUILayout.Label("<b><size=15>Children</size></b>", null);
GUILayout.BeginHorizontal(null);
ChildPages.DrawLimitInputArea();
if (ChildPages.ItemCount > ChildPages.ItemsPerPage)
{
ChildPages.CurrentPageLabel();
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
if (GUILayout.Button("< Prev", new GUILayoutOption[] { GUILayout.Width(80) }))
{
ChildPages.TurnPage(Turn.Left, ref this.m_transformScroll);
}
if (GUILayout.Button("Next >", new GUILayoutOption[] { GUILayout.Width(80) }))
{
ChildPages.TurnPage(Turn.Right, ref this.m_transformScroll);
}
}
GUILayout.EndHorizontal();
if (m_children != null && m_children.Length > 0)
{
int start = ChildPages.CalculateOffsetIndex();
for (int j = start; (j < start + ChildPages.ItemsPerPage && j < ChildPages.ItemCount); j++)
{
var obj = m_children[j];
if (!obj)
{
GUILayout.Label("null", null);
continue;
}
UIHelpers.GOButton(obj.gameObject, InspectGameObject, false, m_rect.width / 2 - 80);
}
}
else
{
GUILayout.Label("<i>None</i>", null);
}
GUIUnstrip.EndScrollView();
GUILayout.EndVertical();
}
private void ComponentList(Rect m_rect)
{
GUILayout.BeginVertical(GUI.skin.box, null);
m_compScroll = GUIUnstrip.BeginScrollView(m_compScroll);
GUILayout.Label("<b><size=15>Components</size></b>", null);
GUILayout.BeginHorizontal(null);
CompPages.DrawLimitInputArea();
if (CompPages.ItemCount > CompPages.ItemsPerPage)
{
CompPages.CurrentPageLabel();
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
if (GUILayout.Button("< Prev", new GUILayoutOption[] { GUILayout.Width(80) }))
{
CompPages.TurnPage(Turn.Left, ref this.m_compScroll);
}
if (GUILayout.Button("Next >", new GUILayoutOption[] { GUILayout.Width(80) }))
{
CompPages.TurnPage(Turn.Right, ref this.m_compScroll);
}
}
GUILayout.EndHorizontal();
GUI.skin.button.alignment = TextAnchor.MiddleLeft;
if (m_cachedDestroyList.Count > 0)
{
m_cachedDestroyList.Clear();
}
if (m_components != null)
{
int start = CompPages.CalculateOffsetIndex();
for (int j = start; (j < start + CompPages.ItemsPerPage && j < CompPages.ItemCount); j++)
{
var component = m_components[j];
if (!component) continue;
var ilType = component.GetIl2CppType();
GUILayout.BeginHorizontal(null);
if (ReflectionHelpers.BehaviourType.IsAssignableFrom(ilType))
{
BehaviourEnabledBtn(component.TryCast<Behaviour>());
}
else
{
GUILayout.Space(26);
}
if (GUILayout.Button("<color=cyan>" + ilType.Name + "</color>", new GUILayoutOption[] { GUILayout.Width(m_rect.width / 2 - 100) }))
{
ReflectObject(component);
}
if (GUILayout.Button("<color=red>-</color>", 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);
}
}
GUIUnstrip.EndScrollView();
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(520) });
GUILayout.Label("<b><size=15>GameObject Controls</size></b>", null);
GUILayout.BeginHorizontal(null);
bool m_active = m_object.activeSelf;
m_active = GUILayout.Toggle(m_active, (m_active ? "<color=lime>Enabled " : "<color=red>Disabled") + "</color>",
new GUILayoutOption[] { GUILayout.Width(80) });
if (m_object.activeSelf != m_active) { m_object.SetActive(m_active); }
UIHelpers.InstantiateButton(m_object, 100);
if (GUILayout.Button("Set DontDestroyOnLoad", new GUILayoutOption[] { GUILayout.Width(170) }))
{
GameObject.DontDestroyOnLoad(m_object);
m_object.hideFlags |= HideFlags.DontUnloadUnusedAsset;
}
var lbl = m_freeze ? "<color=lime>Unfreeze</color>" : "<color=orange>Freeze Pos/Rot</color>";
if (GUILayout.Button(lbl, new GUILayoutOption[] { GUILayout.Width(110) }))
{
m_freeze = !m_freeze;
if (m_freeze)
{
UpdateFreeze();
}
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
m_setParentInput = GUILayout.TextField(m_setParentInput, null);
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}'");
}
}
if (GUILayout.Button("Detach from parent", new GUILayoutOption[] { GUILayout.Width(160) }))
{
m_object.transform.parent = null;
}
GUILayout.EndHorizontal();
GUILayout.BeginVertical(GUI.skin.box, null);
m_cachedInput[0] = TranslateControl(TranslateType.Position, ref m_translateAmount, false);
m_cachedInput[1] = TranslateControl(TranslateType.Rotation, ref m_rotateAmount, true);
m_cachedInput[2] = TranslateControl(TranslateType.Scale, ref m_scaleAmount, false);
GUILayout.BeginHorizontal(null);
if (GUILayout.Button("<color=lime>Apply to Transform</color>", null) || m_autoApplyTransform)
{
if (m_localContext)
{
m_object.transform.localPosition = m_cachedInput[0];
m_object.transform.localEulerAngles = m_cachedInput[1];
}
else
{
m_object.transform.position = m_cachedInput[0];
m_object.transform.eulerAngles = m_cachedInput[1];
}
m_object.transform.localScale = m_cachedInput[2];
if (m_freeze)
{
UpdateFreeze();
}
}
if (GUILayout.Button("<color=lime>Update from Transform</color>", null) || m_autoUpdateTransform)
{
CacheTransformValues();
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
BoolToggle(ref m_autoApplyTransform, "Auto-apply to Transform?");
BoolToggle(ref m_autoUpdateTransform, "Auto-update from transform?");
GUILayout.EndHorizontal();
bool b = m_localContext;
b = GUILayout.Toggle(b, "<color=" + (b ? "lime" : "red") + ">Use local transform values?</color>", null);
if (b != m_localContext)
{
m_localContext = b;
CacheTransformValues();
if (m_freeze)
{
UpdateFreeze();
}
}
GUILayout.EndVertical();
if (GUILayout.Button("<color=red><b>Destroy</b></color>", new GUILayoutOption[] { GUILayout.Width(120) }))
{
GameObject.Destroy(m_object);
DestroyWindow();
return;
}
GUILayout.EndVertical();
}
private void UpdateFreeze()
{
if (m_localContext)
{
m_frozenPosition = m_object.transform.localPosition;
m_frozenRotation = m_object.transform.localRotation;
}
else
{
m_frozenPosition = m_object.transform.position;
m_frozenRotation = m_object.transform.rotation;
}
m_frozenScale = m_object.transform.localScale;
}
private void BoolToggle(ref bool value, string message)
{
string lbl = "<color=";
lbl += value ? "lime" : "red";
lbl += $">{message}</color>";
value = GUILayout.Toggle(value, lbl, null);
}
public enum TranslateType
{
Position,
Rotation,
Scale
}
private Vector3 TranslateControl(TranslateType mode, ref float amount, bool multByTime)
{
GUILayout.BeginHorizontal(null);
GUILayout.Label($"<color=cyan><b>{(m_localContext ? "Local " : "")}{mode}</b></color>:",
new GUILayoutOption[] { GUILayout.Width(m_localContext ? 110 : 65) });
var transform = m_object.transform;
switch (mode)
{
case TranslateType.Position:
var pos = m_localContext ? transform.localPosition : transform.position;
GUILayout.Label(pos.ToString(), new GUILayoutOption[] { GUILayout.Width(250) });
break;
case TranslateType.Rotation:
var rot = m_localContext ? transform.localEulerAngles : transform.eulerAngles;
GUILayout.Label(rot.ToString(), new GUILayoutOption[] { GUILayout.Width(250) });
break;
case TranslateType.Scale:
GUILayout.Label(transform.localScale.ToString(), new GUILayoutOption[] { GUILayout.Width(250) });
break;
}
GUILayout.EndHorizontal();
Vector3 input = m_cachedInput[(int)mode];
GUILayout.BeginHorizontal(null);
GUI.skin.label.alignment = TextAnchor.MiddleRight;
GUILayout.Label("<color=cyan>X:</color>", new GUILayoutOption[] { GUILayout.Width(20) });
PlusMinusFloat(ref input.x, amount, multByTime);
GUILayout.Label("<color=cyan>Y:</color>", new GUILayoutOption[] { GUILayout.Width(20) });
PlusMinusFloat(ref input.y, amount, multByTime);
GUILayout.Label("<color=cyan>Z:</color>", new GUILayoutOption[] { GUILayout.Width(20) });
PlusMinusFloat(ref input.z, amount, multByTime);
GUILayout.Label("+/-:", new GUILayoutOption[] { GUILayout.Width(30) });
var amountInput = amount.ToString("F3");
amountInput = GUILayout.TextField(amountInput, new GUILayoutOption[] { GUILayout.Width(60) });
if (float.TryParse(amountInput, out float f))
{
amount = f;
}
GUI.skin.label.alignment = TextAnchor.UpperLeft;
GUILayout.EndHorizontal();
return input;
}
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;
}
}
}
}

View File

@ -0,0 +1,394 @@
using System;
using System.CodeDom;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MelonLoader;
using UnhollowerBaseLib;
using UnityEngine;
namespace Explorer
{
public class ReflectionWindow : UIWindow
{
public override string Title => WindowManager.TabView
? $"<color=cyan>[R]</color> {TargetType.Name}"
: $"Reflection Inspector ({TargetType.Name})";
public Type TargetType;
private CacheObjectBase[] m_allCachedMembers;
private CacheObjectBase[] m_cachedMembersFiltered;
public PageHelper Pages = new PageHelper();
private bool m_autoUpdate = false;
private string m_search = "";
public MemberTypes m_filter = MemberTypes.Property;
private bool m_hideFailedReflection = false;
// some extra cast-caching
private UnityEngine.Object m_uObj;
private Component m_component;
private static readonly HashSet<string> _typeAndMemberBlacklist = new HashSet<string>
{
// Causes a crash
"Type.DeclaringMethod",
};
private static readonly HashSet<string> _methodStartsWithBlacklist = new HashSet<string>
{
// Pointless (handled by Properties)
"get_",
"set_"
};
public override void Init()
{
TargetType = ReflectionHelpers.GetActualType(Target);
CacheMembers(ReflectionHelpers.GetAllBaseTypes(Target));
// cache the extra cast-caching
if (Target is Il2CppSystem.Object ilObject)
{
var unityObj = ilObject.TryCast<UnityEngine.Object>();
if (unityObj)
{
m_uObj = unityObj;
var component = ilObject.TryCast<Component>();
if (component)
{
m_component = component;
}
}
}
}
public override void Update()
{
if (Target == null)
{
DestroyWindow();
return;
}
else if (Target is UnityEngine.Object uObj)
{
if (!uObj)
{
DestroyWindow();
return;
}
}
m_cachedMembersFiltered = m_allCachedMembers.Where(x => ShouldProcessMember(x)).ToArray();
if (m_autoUpdate)
{
UpdateValues();
}
}
private void UpdateValues()
{
foreach (var member in m_cachedMembersFiltered)
{
member.UpdateValue();
}
}
private bool ShouldProcessMember(CacheObjectBase holder)
{
// check MemberTypes filter
if (m_filter != MemberTypes.All && m_filter != holder.MemInfo?.MemberType)
return false;
// hide failed reflection
if (!string.IsNullOrEmpty(holder.ReflectionException) && m_hideFailedReflection)
return false;
// see if we should do name search
if (m_search == "" || holder.MemInfo == null)
return true;
// ok do name search
return (holder.MemInfo.DeclaringType.Name + "." + holder.MemInfo.Name)
.ToLower()
.Contains(m_search.ToLower());
}
private void CacheMembers(Type[] types)
{
var list = new List<CacheObjectBase>();
var cachedSigs = new List<string>();
foreach (var declaringType in types)
{
MemberInfo[] infos;
try
{
infos = declaringType.GetMembers(ReflectionHelpers.CommonFlags);
}
catch
{
MelonLogger.Log($"Exception getting members for type: {declaringType.FullName}");
continue;
}
object target = Target;
string exception = null;
if (target is Il2CppSystem.Object ilObject)
{
try
{
target = ilObject.Il2CppCast(declaringType);
}
catch (Exception e)
{
exception = ReflectionHelpers.ExceptionToString(e);
}
}
foreach (var member in infos)
{
// make sure member type is Field, Method of Property (4 / 8 / 16)
int m = (int)member.MemberType;
if (m < 4 || m > 16)
continue;
// check blacklisted members
var name = member.DeclaringType.Name + "." + member.Name;
if (_typeAndMemberBlacklist.Any(it => it == name))
continue;
if (_methodStartsWithBlacklist.Any(it => member.Name.StartsWith(it)))
continue;
// compare signature to already cached members
var signature = $"{member.DeclaringType.Name}.{member.Name}";
if (member is MethodInfo mi)
{
AppendParams(mi.GetParameters());
}
else if (member is PropertyInfo pi)
{
AppendParams(pi.GetIndexParameters());
}
void AppendParams(ParameterInfo[] _args)
{
signature += " (";
foreach (var param in _args)
{
signature += $"{param.ParameterType.Name} {param.Name}, ";
}
signature += ")";
}
if (cachedSigs.Contains(signature))
{
continue;
}
try
{
var cached = CacheObjectBase.GetCacheObject(member, target);
if (cached != null)
{
cachedSigs.Add(signature);
list.Add(cached);
cached.ReflectionException = exception;
}
}
catch (Exception e)
{
MelonLogger.LogWarning($"Exception caching member {signature}!");
MelonLogger.Log(e.ToString());
}
}
}
m_allCachedMembers = list.ToArray();
}
// =========== GUI DRAW =========== //
public override void WindowFunction(int windowID)
{
try
{
// ====== HEADER ======
var rect = WindowManager.TabView ? TabViewWindow.Instance.m_rect : this.m_rect;
if (!WindowManager.TabView)
{
Header();
GUILayout.BeginArea(new Rect(5, 25, rect.width - 10, rect.height - 35), GUI.skin.box);
}
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Type:</b> <color=cyan>" + TargetType.FullName + "</color>", new GUILayoutOption[] { GUILayout.Width(245f) });
if (m_uObj)
{
GUILayout.Label("Name: " + m_uObj.name, null);
}
GUILayout.EndHorizontal();
if (m_uObj)
{
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Tools:</b>", new GUILayoutOption[] { GUILayout.Width(80) });
UIHelpers.InstantiateButton(m_uObj);
if (m_component && m_component.gameObject is GameObject obj)
{
GUI.skin.label.alignment = TextAnchor.MiddleRight;
GUILayout.Label("GameObject:", new GUILayoutOption[] { GUILayout.Width(135) });
var charWidth = obj.name.Length * 15;
var maxWidth = rect.width - 350;
var labelWidth = charWidth < maxWidth ? charWidth : maxWidth;
if (GUILayout.Button("<color=#00FF00>" + obj.name + "</color>", new GUILayoutOption[] { GUILayout.Width(labelWidth) }))
{
WindowManager.InspectObject(obj, out bool _);
}
GUI.skin.label.alignment = TextAnchor.UpperLeft;
}
GUILayout.EndHorizontal();
}
UIStyles.HorizontalLine(Color.grey);
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Search:</b>", new GUILayoutOption[] { GUILayout.Width(75) });
m_search = GUILayout.TextField(m_search, null);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Filter:</b>", new GUILayoutOption[] { GUILayout.Width(75) });
FilterToggle(MemberTypes.All, "All");
FilterToggle(MemberTypes.Property, "Properties");
FilterToggle(MemberTypes.Field, "Fields");
FilterToggle(MemberTypes.Method, "Methods");
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Values:</b>", 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 = m_hideFailedReflection ? Color.green : Color.red;
m_hideFailedReflection = GUILayout.Toggle(m_hideFailedReflection, "Hide failed Reflection?", new GUILayoutOption[] { GUILayout.Width(150) });
GUI.color = Color.white;
GUILayout.EndHorizontal();
GUILayout.Space(10);
Pages.ItemCount = m_cachedMembersFiltered.Length;
// prev/next page buttons
GUILayout.BeginHorizontal(null);
Pages.DrawLimitInputArea();
if (Pages.ItemCount > Pages.ItemsPerPage)
{
if (GUILayout.Button("< Prev", new GUILayoutOption[] { GUILayout.Width(80) }))
{
Pages.TurnPage(Turn.Left, ref this.scroll);
}
Pages.CurrentPageLabel();
if (GUILayout.Button("Next >", new GUILayoutOption[] { GUILayout.Width(80) }))
{
Pages.TurnPage(Turn.Right, ref this.scroll);
}
}
GUILayout.EndHorizontal();
// ====== BODY ======
scroll = GUIUnstrip.BeginScrollView(scroll);
GUILayout.Space(10);
UIStyles.HorizontalLine(Color.grey);
GUILayout.BeginVertical(GUI.skin.box, null);
var members = this.m_cachedMembersFiltered;
int start = Pages.CalculateOffsetIndex();
for (int j = start; (j < start + Pages.ItemsPerPage && j < members.Length); j++)
{
var holder = members[j];
GUILayout.BeginHorizontal(new GUILayoutOption[] { GUILayout.Height(25) });
try
{
holder.Draw(rect, 180f);
}
catch
{
GUILayout.EndHorizontal();
continue;
}
GUILayout.EndHorizontal();
// if not last element
if (!(j == (start + Pages.ItemsPerPage - 1) || j == (members.Length - 1)))
UIStyles.HorizontalLine(new Color(0.07f, 0.07f, 0.07f), true);
}
GUILayout.EndVertical();
GUIUnstrip.EndScrollView();
if (!WindowManager.TabView)
{
m_rect = ResizeDrag.ResizeWindow(rect, windowID);
GUILayout.EndArea();
}
}
catch (Il2CppException e)
{
if (!e.Message.Contains("in a group with only"))
{
throw;
}
}
catch (Exception e)
{
MelonLogger.LogWarning("Exception drawing ReflectionWindow: " + e.GetType() + ", " + e.Message);
DestroyWindow();
return;
}
}
private void FilterToggle(MemberTypes 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;
Pages.PageOffset = 0;
scroll = Vector2.zero;
}
GUI.color = Color.white;
}
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using MelonLoader;
using UnhollowerBaseLib;
namespace Explorer
{
public class ResizeDrag
{
private static bool RESIZE_FAILED = false;
private 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)
{
if (!RESIZE_FAILED)
{
var origRect = _rect;
try
{
GUILayout.BeginHorizontal(GUI.skin.box, null);
GUI.skin.label.alignment = TextAnchor.MiddleCenter;
GUILayout.Button(gcDrag, GUI.skin.label, new GUILayoutOption[] { GUILayout.Height(15) });
//var r = GUILayoutUtility.GetLastRect();
var r = GUIUnstrip.GetLastRect();
var mousePos = InputHelper.mousePosition;
Vector2 mouse = GUIUtility.ScreenToGUIPoint(new Vector2(mousePos.x, Screen.height - mousePos.y));
if (r.Contains(mouse) && InputHelper.GetMouseButtonDown(0))
{
isResizing = true;
m_currentWindow = ID;
m_currentResize = new Rect(mouse.x, mouse.y, _rect.width, _rect.height);
}
else if (!InputHelper.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();
}
catch (Il2CppException e) when (e.Message.StartsWith("System.ArgumentException"))
{
// suppress
return origRect;
}
catch (Exception e)
{
RESIZE_FAILED = true;
MelonLogger.Log("Exception on GuiResize: " + e.GetType() + ", " + e.Message);
MelonLogger.Log(e.StackTrace);
return origRect;
}
GUI.skin.label.alignment = TextAnchor.UpperLeft;
}
else
{
GUILayout.BeginHorizontal(GUI.skin.box, null);
GUILayout.Label("Resize window:", new GUILayoutOption[] { GUILayout.Width(100) });
GUI.skin.label.alignment = TextAnchor.MiddleRight;
GUILayout.Label("<color=cyan>Width:</color>", new GUILayoutOption[] { GUILayout.Width(60) });
if (GUILayout.RepeatButton("-", new GUILayoutOption[] { GUILayout.Width(20) }))
{
_rect.width -= 5f;
}
if (GUILayout.RepeatButton("+", new GUILayoutOption[] { GUILayout.Width(20) }))
{
_rect.width += 5f;
}
GUILayout.Label("<color=cyan>Height:</color>", new GUILayoutOption[] { GUILayout.Width(60) });
if (GUILayout.RepeatButton("-", new GUILayoutOption[] { GUILayout.Width(20) }))
{
_rect.height -= 5f;
}
if (GUILayout.RepeatButton("+", new GUILayoutOption[] { GUILayout.Width(20) }))
{
_rect.height += 5f;
}
GUILayout.EndHorizontal();
GUI.skin.label.alignment = TextAnchor.UpperLeft;
}
return _rect;
}
}
}

View File

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MelonLoader;
using UnityEngine;
namespace Explorer
{
public class TabViewWindow : UIWindow
{
public override string Title => $"Tabs ({WindowManager.Windows.Count})";
public static TabViewWindow Instance => m_instance ?? (m_instance = new TabViewWindow());
private static TabViewWindow m_instance;
private UIWindow m_targetWindow;
public int TargetTabID = 0;
public override bool IsTabViewWindow => true;
public TabViewWindow()
{
m_rect = new Rect(570, 0, 550, 700);
}
public override void Init() { }
public override void Update()
{
while (TargetTabID >= WindowManager.Windows.Count)
{
TargetTabID--;
}
if (TargetTabID == -1 && WindowManager.Windows.Count > 0)
{
TargetTabID = 0;
}
if (TargetTabID >= 0)
{
m_targetWindow = WindowManager.Windows[TargetTabID];
}
else
{
m_targetWindow = null;
}
m_targetWindow?.Update();
}
public override void WindowFunction(int windowID)
{
try
{
GUI.DragWindow(new Rect(0, 0, m_rect.width - 90, 20));
if (GUI.Button(new Rect(m_rect.width - 90, 2, 80, 20), "<color=red>Close All</color>"))
{
foreach (var window in WindowManager.Windows)
{
window.DestroyWindow();
}
return;
}
GUILayout.BeginArea(new Rect(5, 25, m_rect.width - 10, m_rect.height - 35), GUI.skin.box);
GUILayout.BeginVertical(GUI.skin.box, null);
GUILayout.BeginHorizontal(null);
GUI.skin.button.alignment = TextAnchor.MiddleLeft;
int tabPerRow = Mathf.FloorToInt((float)((decimal)m_rect.width / 238));
int rowCount = 0;
for (int i = 0; i < WindowManager.Windows.Count; i++)
{
if (rowCount >= tabPerRow)
{
rowCount = 0;
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
}
rowCount++;
bool focused = i == TargetTabID;
string color = focused ? "<color=lime>" : "<color=orange>";
GUI.color = focused ? Color.green : Color.white;
var window = WindowManager.Windows[i];
if (GUILayout.Button(color + window.Title + "</color>", new GUILayoutOption[] { GUILayout.Width(200) }))
{
TargetTabID = i;
}
if (GUILayout.Button("<color=red><b>X</b></color>", new GUILayoutOption[] { GUILayout.Width(22) }))
{
window.DestroyWindow();
}
}
GUI.color = Color.white;
GUILayout.EndHorizontal();
GUILayout.EndVertical();
GUI.skin.button.alignment = TextAnchor.MiddleCenter;
m_targetWindow.WindowFunction(m_targetWindow.windowID);
try
{
m_rect = ResizeDrag.ResizeWindow(m_rect, windowID);
}
catch { }
GUILayout.EndArea();
}
catch { }
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Harmony;
using MelonLoader;
using UnhollowerBaseLib;
using UnhollowerRuntimeLib;
using UnityEngine;
namespace Explorer
{
public abstract class UIWindow
{
public abstract string Title { get; }
public object Target;
public int windowID;
public Rect m_rect = new Rect(0,0, ModConfig.Instance.Default_Window_Size.x,ModConfig.Instance.Default_Window_Size.y);
public Vector2 scroll = Vector2.zero;
public virtual bool IsTabViewWindow => false;
public abstract void Init();
public abstract void WindowFunction(int windowID);
public abstract void Update();
public static UIWindow CreateWindow<T>(object target) where T : UIWindow
{
var window = Activator.CreateInstance<T>();
window.Target = target;
window.windowID = WindowManager.NextWindowID();
window.m_rect = WindowManager.GetNewWindowRect();
WindowManager.Windows.Add(window);
window.Init();
return window;
}
public void DestroyWindow()
{
WindowManager.DestroyWindow(this);
}
public void OnGUI()
{
m_rect = GUI.Window(windowID, m_rect, (GUI.WindowFunction)WindowFunction, Title);
}
public void Header()
{
if (!WindowManager.TabView)
{
GUI.DragWindow(new Rect(0, 0, m_rect.width - 90, 20));
if (GUI.Button(new Rect(m_rect.width - 90, 2, 80, 20), "<color=red><b>X</b></color>"))
{
DestroyWindow();
return;
}
}
}
}
}

View File

@ -0,0 +1,229 @@
using System;
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 bool TabView = true;
public static List<UIWindow> Windows = new List<UIWindow>();
public static int CurrentWindowID { get; set; } = 500000;
private static Rect m_lastWindowRect;
private static readonly List<UIWindow> m_windowsToDestroy = new List<UIWindow>();
public WindowManager()
{
Instance = this;
}
public static void DestroyWindow(UIWindow window)
{
m_windowsToDestroy.Add(window);
}
public void Update()
{
if (m_windowsToDestroy.Count > 0)
{
foreach (var window in m_windowsToDestroy)
{
if (Windows.Contains(window))
{
Windows.Remove(window);
}
}
m_windowsToDestroy.Clear();
}
if (TabView)
{
TabViewWindow.Instance.Update();
}
else
{
for (int i = 0; i < Windows.Count; i++)
{
var window = Windows[i];
if (window != null)
{
window.Update();
}
}
}
}
public void OnGUI()
{
if (TabView)
{
if (Windows.Count > 0)
{
TabViewWindow.Instance.OnGUI();
}
}
else
{
foreach (var window in Windows)
{
window.OnGUI();
}
}
}
// ========= Public Helpers =========
public static UIWindow InspectObject(object obj, out bool createdNew, bool forceReflection = false)
{
createdNew = false;
if (InputHelper.GetKey(KeyCode.LeftShift))
{
forceReflection = true;
}
Il2CppSystem.Object iObj = null;
if (obj is Il2CppSystem.Object isObj)
{
iObj = isObj;
}
if (!forceReflection)
{
foreach (var window in Windows)
{
bool equals = ReferenceEquals(obj, window.Target);
if (!equals && iObj is Il2CppSystem.Object iCurrent && window.Target is Il2CppSystem.Object iTarget)
{
if (iCurrent.GetIl2CppType().FullName != iTarget.GetIl2CppType().FullName)
{
if (iCurrent is Transform transform)
{
iCurrent = transform.gameObject;
}
}
equals = iCurrent.Pointer == iTarget.Pointer;
}
if (equals)
{
FocusWindow(window);
return window;
}
}
}
createdNew = true;
if (!forceReflection && (obj is GameObject || obj is Transform))
{
return InspectGameObject(obj as GameObject ?? (obj as Transform).gameObject);
}
else
{
return InspectReflection(obj);
}
}
private static void FocusWindow(UIWindow window)
{
if (!TabView)
{
GUI.BringWindowToFront(window.windowID);
GUI.FocusWindow(window.windowID);
}
else
{
TabViewWindow.Instance.TargetTabID = Windows.IndexOf(window);
}
}
private static UIWindow InspectGameObject(GameObject obj)
{
var new_window = UIWindow.CreateWindow<GameObjectWindow>(obj);
FocusWindow(new_window);
return new_window;
}
private static UIWindow InspectReflection(object obj)
{
var new_window = UIWindow.CreateWindow<ReflectionWindow>(obj);
FocusWindow(new_window);
return new_window;
}
// === Misc 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)
{
var mousePos = InputHelper.mousePosition;
return rect.Contains(new Vector2(mousePos.x, Screen.height - mousePos.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;
}
}
}