Compare commits

..

23 Commits
1.4.1 ... 1.5.0

Author SHA1 Message Date
916bdea59b 1.5.0 2020-08-31 23:28:44 +10:00
d8688193d5 1.4.7
* Added support for Il2Cpp IList objects
* Improved support for Lists in general, they should now work better.
2020-08-31 18:23:19 +10:00
30b48b1f1f 1.4.6
* Fix a bug with the Scene Explorer Search feature (not Object search)
* Simplified parsing of primitive values to a better method
2020-08-31 16:27:14 +10:00
0fd382c1f6 1.4.5 finalize and release 2020-08-30 23:29:37 +10:00
fd20a1120b Merge branch 'master' of https://github.com/sinai-dev/CppExplorer 2020-08-30 17:32:28 +10:00
abcb548706 1.4.5 finalize
Will be pushed when MelonLoader releases 0.2.7.
2020-08-30 17:32:25 +10:00
b056644385 Update CacheMethod.cs 2020-08-30 16:55:14 +10:00
71f72e8f36 Update README.md 2020-08-30 16:51:03 +10:00
1ab41f5a30 Update README.md 2020-08-30 16:49:44 +10:00
7dc58ea02c 1.4.5 (pre-release)
* Added support for MethodInfos with only primitive arguments on reflection window
* Added backup resize mode incase resizing experiences an exception
2020-08-30 07:01:13 +10:00
68eeee353e 1.4.5 (pre-release) - Implement Tab View
* Implemented Tab View
2020-08-30 01:08:48 +10:00
92fe1dc704 1.4.5 (pre-release)
* Added MethodInfo support for basic methods with no arguments.
* Added support for missing primitive types (char, short, byte)
* Added CacheDictionary class (currently unsupported)
* Cleaned up some stuff, using System.Reflection.MemberType instead of a custom enum.
2020-08-29 21:15:54 +10:00
6e644b4f50 1.4.5 (pre-release)
* Windows now display the gameobject name or the object type in the header
* Added "Set DontDestroyOnLoad" button to Gameobject controls.
* Added dynamic input field size and more intelligent auto-wrap for primitive values
* Resize Drag will now disable itself on Exceptions, and log the error (affects VRChat)
* Various misc UI improvements
* Various small fixes
2020-08-28 00:45:34 +10:00
c47974115b WindowManager.cs, restore 2 lines accidentally deleted 2020-08-27 20:15:38 +10:00
535e88be9a 1.4.5 (pre-release)
* Pre-release. Will be released once MelonLoader bumps to Unhollower 0.4.9.0
* Added global "Force Unlock Mouse" option, should work on almost all games. Has smart behaviour and will maintain the previous value (or the value which should be set).
* Improve performacne of CacheList casting List ->IEnumerable
* Fix a bug causing some Components to not show the GameObject button in the Reflection Window (top-right corner).
* Fix a bug making the Window Manager think that two of the same Il2Cpp Object are not ReferenceEquals.
* Added logging when C# Console fails to compile anything
* Improve display of Reflection Window member name label, now expands with window resize.
2020-08-27 18:05:55 +10:00
e567c16221 Update README.md 2020-08-24 01:53:59 +10:00
d13af7548e Update README.md 2020-08-24 01:52:19 +10:00
5d750aec77 Update README.md 2020-08-24 01:50:03 +10:00
45b5ce0ef8 1.4.2
* Fixed a bug on the Reflection window which would prevent primitive values from being applied
* Improved some parts of the Scene Explorer and the Reflection Window interfaces
* Scene Explorer now has "page view" like other lists
* Various minor cleanups and refactorings
2020-08-24 01:42:19 +10:00
e3d1add090 Update README.md 2020-08-23 03:00:19 +10:00
a59bcc95e4 Update README.md 2020-08-23 03:00:12 +10:00
ac4414ca86 Update README.md 2020-08-23 02:50:59 +10:00
19263092fe Update CacheObject.cs 2020-08-22 19:36:06 +10:00
25 changed files with 2126 additions and 974 deletions

View File

@ -1,7 +1,7 @@
# CppExplorer [![Version](https://img.shields.io/badge/MelonLoader-0.2.6-green.svg)]()
# CppExplorer [![Version](https://img.shields.io/badge/MelonLoader-0.2.7.1-green.svg)]()
<p align="center">
<img align="center" src="https://i.imgur.com/1ZoZemW.png">
<img align="center" src="https://sinai-dev.github.io/images/thumbs/02.png">
</p>
<p align="center">
@ -14,8 +14,10 @@
<img src="https://img.shields.io/github/downloads/sinai-dev/CppExplorer/total.svg" />
</p>
### Note
Most games running on Unity 2017 to 2019 should be supported. If you find that the GUI does not display properly and you get errors in the MelonLoader console about it, then this is likely due to a bug with Il2CppAssemblyUnhollower's unstripping. This bug is known by the developer of the tool and they will fix it as soon as they are able to.
### Known issue
Some games are experiencing `MissingMethodException`s or exceptions about failed unstripping, which prevent the CppExplorer menu from showing properly or at all. This is a bug with [Il2CppAssemblyUnhollower](https://github.com/knah/Il2CppAssemblyUnhollower) and there isn't much I can do about it myself.
If you're familiar with C# and Unity, one possibility for now is making a fork of this repo and manually fixing all the broken methods to ones which aren't broken (if possible). There may be another overload of the same method which wasn't stripped or was unstripped successfully, which you can use instead.
## Features
* Scene hierarchy explorer
@ -36,34 +38,63 @@ Requires [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) to be ins
## How to use
* Press F7 to show or hide the menu.
* Simply browse through the scene, search for objects, etc, it's pretty self-explanatory.
* Simply browse through the scene, search for objects, etc, most of it is pretty self-explanatory.
### Help! I can't use the mouse!
### Scene Explorer
It is fairly common for games to override mouse control with their own mouse behaviour. Unfortunately, it's not feasible for CppExplorer to handle this due to how differently every game will go about it.
* A simple menu which allows you to traverse the Transform heirarchy of the scene.
* Click on a GameObject to set it as the current path, or <b>Inspect</b> it to send it to an Inspector Window.
In order to fix this problem, you can:
* Use [VRCExplorerMouseControl](https://github.com/sinaioutlander/VRCExplorerMouseControl) (for VRChat)
* Use [HPExplorerMouseControl](https://github.com/sinaioutlander/Hellpoint-Mods/tree/master/HPExplorerMouseControl/HPExplorerMouseControl) (for Hellpoint)
* In general, pressing Escape (to open a menu) will usually give you temporary control over the mouse.
* Create your own mini-plugin using one of the two plugins above as an example. Usually only 1 or 2 simple Harmony patches are needed to fix the problem.
[![](https://i.imgur.com/2b0q0jL.png)](https://i.imgur.com/2b0q0jL.png)
## Images
### Inspectors
<i>Note: images may be slightly outdated, taken from version 1.2</i>.
CppExplorer has two main inspector modes: <b>GameObject Inspector</b>, and <b>Reflection Inspector</b>.
Scene explorer, and inspection of a MonoBehaviour object:
<b>Tip:</b> when in Tab View, GameObjects are denoted by a [G] prefix, and Reflection objects are denoted by a [R] prefix.
[![](https://i.imgur.com/Yxizwcz.png)](https://i.imgur.com/Yxizwcz.png)
### GameObject Inspector
Search feature:
* Allows you to see the children and components on a GameObject.
* Can use some basic GameObject Controls such as translating and rotating the object, destroy it, clone it, etc.
[![](https://i.imgur.com/F9ZfMvz.png)](https://i.imgur.com/F9ZfMvz.png)
[![](https://i.imgur.com/JTxqlx4.png)](https://i.imgur.com/JTxqlx4.png)
### Reflection Inspector
C# REPL console:
* The Reflection Inspector is used for all other supported objects.
* Allows you to inspect Properties, Fields and basic Methods, as well as set primitive values and evaluate primitive methods.
* Can search and filter members for the ones you are interested in.
[![](https://i.imgur.com/14Dbtf8.png)](https://i.imgur.com/14Dbtf8.png)
[![](https://i.imgur.com/iq92m0l.png)](https://i.imgur.com/iq92m0l.png)
### Object Search
* You can search for an `UnityEngine.Object` with the Object Search feature.
* Filter by name, type, etc.
* For GameObjects and Transforms you can filter which scene they are found in too.
[![](https://i.imgur.com/lK2RthM.png)](https://i.imgur.com/lK2RthM.png)
### C# REPL console
* A simple C# REPL console, allows you to execute a method body on the fly.
[![](https://i.imgur.com/5U4D1a8.png)](https://i.imgur.com/5U4D1a8.png)
### Inspect-under-mouse
* Press Shift+RMB (Right Mouse Button) while the CppExplorer menu is open to begin Inspect-Under-Mouse.
* Hover over your desired object, if you see the name appear then you can click on it to inspect it.
* Only objects with Colliders are supported.
### Mouse Control
CppExplorer can force the mouse to be visible and unlocked when the menu is open, if you have enabled "Force Unlock Mouse" (Left-Alt toggle). However, you may also want to prevent the mouse clicking-through onto the game behind CppExplorer, this is possible but it requires specific patches for that game.
* For VRChat, use [VRCExplorerMouseControl](https://github.com/sinai-dev/VRCExplorerMouseControl)
* For Hellpoint, use [HPExplorerMouseControl](https://github.com/sinai-dev/Hellpoint-Mods/tree/master/HPExplorerMouseControl/HPExplorerMouseControl)
* You can create your own plugin using one of the two plugins above as an example. Usually only a few simple Harmony patches are needed to fix the problem.
## Credits

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MelonLoader;
using UnityEngine;
namespace Explorer
{
public class CacheDictionary : CacheObjectBase
{
public override void Init()
{
//base.Init();
Value = "Unsupported";
}
public override void UpdateValue()
{
//base.UpdateValue();
}
public override void DrawValue(Rect window, float width)
{
GUILayout.Label("<color=red>Dictionary (unsupported)</color>", null);
}
}
}

View File

@ -8,23 +8,20 @@ using UnityEngine;
namespace Explorer
{
public class CacheEnum : CacheObject
public class CacheEnum : CacheObjectBase
{
private readonly Type m_enumType;
private readonly string[] m_names;
public Type EnumType;
public string[] EnumNames;
public CacheEnum(object obj)
public override void Init()
{
if (obj != null)
{
m_enumType = obj.GetType();
m_names = Enum.GetNames(m_enumType);
}
EnumType = Value.GetType();
EnumNames = Enum.GetNames(EnumType);
}
public override void DrawValue(Rect window, float width)
{
if (MemberInfo != null)
if (CanWrite)
{
if (GUILayout.Button("<", new GUILayoutOption[] { GUILayout.Width(25) }))
{
@ -38,34 +35,18 @@ namespace Explorer
}
}
GUILayout.Label(Value.ToString(), null);
}
public override void SetValue()
{
if (MemberInfo == null)
{
MelonLogger.Log("Trying to SetValue but the MemberInfo is null!");
return;
}
if (Enum.Parse(m_enumType, Value.ToString()) is object enumValue && enumValue != null)
{
Value = enumValue;
}
SetValue(Value, MemberInfo, DeclaringInstance);
GUILayout.Label(Value.ToString(), null);// + "<color=yellow><i> (" + ValueType + ")</i></color>", null);
}
public void SetEnum(ref object value, int change)
{
var names = m_names.ToList();
var names = EnumNames.ToList();
int newindex = names.IndexOf(value.ToString()) + change;
if ((change < 0 && newindex >= 0) || (change > 0 && newindex < names.Count))
{
value = Enum.Parse(m_enumType, names[newindex]);
value = Enum.Parse(EnumType, names[newindex]);
}
}
}

View File

@ -8,46 +8,39 @@ using UnityEngine;
namespace Explorer
{
public class CacheGameObject : CacheObject
public class CacheGameObject : CacheObjectBase
{
private GameObject m_gameObject;
public CacheGameObject(object obj)
private GameObject GameObj
{
if (obj != null)
m_gameObject = GetGameObject(obj);
}
private GameObject GetGameObject(object obj)
{
if (obj is Il2CppSystem.Object ilObj)
get
{
var ilType = ilObj.GetIl2CppType();
if (ilType == ReflectionHelpers.GameObjectType || ilType == ReflectionHelpers.TransformType)
if (m_gameObject == null)
{
return ilObj.TryCast<GameObject>() ?? ilObj.TryCast<Transform>()?.gameObject;
}
}
if (Value is Il2CppSystem.Object ilObj)
{
var ilType = ilObj.GetIl2CppType();
return null;
if (ilType == ReflectionHelpers.GameObjectType || ilType == ReflectionHelpers.TransformType)
{
m_gameObject = ilObj.TryCast<GameObject>() ?? ilObj.TryCast<Transform>()?.gameObject;
}
}
}
return m_gameObject;
}
}
private GameObject m_gameObject;
public override void DrawValue(Rect window, float width)
{
UIHelpers.GameobjButton(m_gameObject, null, false, width);
}
public override void SetValue()
{
throw new NotImplementedException("TODO");
UIHelpers.GameobjButton(GameObj, null, false, width);
}
public override void UpdateValue()
{
base.UpdateValue();
m_gameObject = GetGameObject(Value);
}
}
}

View File

@ -3,71 +3,230 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Mono.CSharp;
using MelonLoader;
using UnityEngine;
namespace Explorer
{
public partial class CacheList : CacheObject
public class CacheList : CacheObjectBase
{
public bool IsExpanded { get; set; }
public int ArrayOffset { get; set; }
public int ArrayLimit { get; set; } = 20;
public float WhiteSpace = 215f;
public float ButtonWidthOffset = 290f;
private CacheObjectBase[] m_cachedEntries;
// Type of Entries in the Array
public Type EntryType
{
get
{
if (m_entryType == null)
{
m_entryType = Value?.GetType().GetGenericArguments()[0];
}
return m_entryType;
}
set
{
m_entryType = value;
}
{
get => GetEntryType();
set => m_entryType = value;
}
private Type m_entryType;
// Cached IEnumerable object
public IEnumerable Enumerable
{
get
{
if (m_enumerable == null && Value != null)
{
m_enumerable = Value as IEnumerable ?? CppListToEnumerable(Value);
}
return m_enumerable;
}
get => GetEnumerable();
}
private IEnumerable m_enumerable;
private CacheObject[] m_cachedEntries;
public CacheList(object obj)
// Generic Type Definition for Lists
public Type GenericTypeDef
{
if (obj != null)
get => GetGenericTypeDef();
}
private Type m_genericTypeDef;
// Cached ToArray method for Lists
public MethodInfo GenericToArrayMethod
{
get => GetGenericToArrayMethod();
}
private MethodInfo m_genericToArray;
// Cached Item Property for ILists
public PropertyInfo ItemProperty
{
get => GetItemProperty();
}
private PropertyInfo m_itemProperty;
// ========== Methods ==========
private IEnumerable GetEnumerable()
{
if (m_enumerable == null && Value != null)
{
Value = obj;
EntryType = obj.GetType().GetGenericArguments()[0];
m_enumerable = Value as IEnumerable ?? GetEnumerableFromIl2CppList();
}
return m_enumerable;
}
private Type GetGenericTypeDef()
{
if (m_genericTypeDef == null && Value != null)
{
var type = Value.GetType();
if (type.IsGenericType)
{
m_genericTypeDef = type.GetGenericTypeDefinition();
}
}
return m_genericTypeDef;
}
private MethodInfo GetGenericToArrayMethod()
{
if (GenericTypeDef == null) return null;
if (m_genericToArray == null)
{
m_genericToArray = GenericTypeDef
.MakeGenericType(new Type[] { this.EntryType })
.GetMethod("ToArray");
}
return m_genericToArray;
}
private PropertyInfo GetItemProperty()
{
if (m_itemProperty == null)
{
m_itemProperty = Value?.GetType().GetProperty("Item");
}
return m_itemProperty;
}
private IEnumerable GetEnumerableFromIl2CppList()
{
if (Value == null) return null;
if (GenericTypeDef == typeof(Il2CppSystem.Collections.Generic.List<>))
{
return (IEnumerable)GenericToArrayMethod?.Invoke(Value, new object[0]);
}
else
{
return ConvertIListToMono();
}
}
private IEnumerable CppListToEnumerable(object list)
private IList ConvertIListToMono()
{
if (EntryType == null) return null;
try
{
var genericType = typeof(List<>).MakeGenericType(new Type[] { this.EntryType });
var list = (IList)Activator.CreateInstance(genericType);
return (IEnumerable)typeof(Il2CppSystem.Collections.Generic.List<>)
.MakeGenericType(new Type[] { EntryType })
.GetMethod("ToArray")
.Invoke(list, new object[0]);
for (int i = 0; ; i++)
{
try
{
var itm = ItemProperty.GetValue(Value, new object[] { i });
list.Add(itm);
}
catch { break; }
}
return list;
}
catch (Exception e)
{
MelonLogger.Log("Exception converting Il2Cpp IList to Mono IList: " + e.GetType() + ", " + e.Message);
return null;
}
}
private Type GetEntryType()
{
if (m_entryType == null)
{
if (this.MemberInfo != null)
{
Type memberType = null;
switch (this.MemberInfo.MemberType)
{
case MemberTypes.Field:
memberType = (MemberInfo as FieldInfo).FieldType;
break;
case MemberTypes.Property:
memberType = (MemberInfo as PropertyInfo).PropertyType;
break;
}
if (memberType != null && memberType.IsGenericType)
{
m_entryType = memberType.GetGenericArguments()[0];
}
}
else if (Value != null)
{
var type = Value.GetType();
if (type.IsGenericType)
{
m_entryType = type.GetGenericArguments()[0];
}
}
}
// IList probably won't be able to get any EntryType.
if (m_entryType == null)
{
m_entryType = typeof(object);
}
return m_entryType;
}
public override void UpdateValue()
{
base.UpdateValue();
if (Value == null || Enumerable == null)
{
return;
}
var enumerator = Enumerable.GetEnumerator();
if (enumerator == null)
{
return;
}
var list = new List<CacheObjectBase>();
while (enumerator.MoveNext())
{
var obj = enumerator.Current;
var type = ReflectionHelpers.GetActualType(obj);
if (obj is Il2CppSystem.Object iObj)
{
obj = iObj.Il2CppCast(type);
}
var cached = GetCacheObject(obj, null, null, type);
cached.UpdateValue();
list.Add(cached);
}
m_cachedEntries = list.ToArray();
}
// ============= GUI Draw =============
public override void DrawValue(Rect window, float width)
{
if (m_cachedEntries == null)
{
GUILayout.Label("m_cachedEntries is null!", null);
return;
}
int count = m_cachedEntries.Length;
if (!IsExpanded)
@ -87,84 +246,84 @@ namespace Explorer
GUI.skin.button.alignment = TextAnchor.MiddleLeft;
string btnLabel = "<color=yellow>[" + count + "] " + EntryType + "</color>";
if (GUILayout.Button(btnLabel, new GUILayoutOption[] { GUILayout.MaxWidth(window.width - 260) }))
if (GUILayout.Button(btnLabel, new GUILayoutOption[] { GUILayout.MaxWidth(window.width - ButtonWidthOffset) }))
{
WindowManager.InspectObject(Value, out bool _);
}
GUI.skin.button.alignment = TextAnchor.MiddleCenter;
GUILayout.Space(5);
if (IsExpanded)
{
if (count > CppExplorer.ArrayLimit)
float whitespace = WhiteSpace;
if (whitespace > 0)
{
ClampLabelWidth(window, ref whitespace);
}
if (count > ArrayLimit)
{
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Space(190);
int maxOffset = (int)Mathf.Ceil((float)(count / (decimal)CppExplorer.ArrayLimit)) - 1;
GUILayout.Space(whitespace);
int maxOffset = (int)Mathf.Ceil((float)(count / (decimal)ArrayLimit)) - 1;
GUILayout.Label($"Page {ArrayOffset + 1}/{maxOffset + 1}", new GUILayoutOption[] { GUILayout.Width(80) });
// prev/next page buttons
if (GUILayout.Button("< Prev", null))
if (GUILayout.Button("< Prev", new GUILayoutOption[] { GUILayout.Width(60) }))
{
if (ArrayOffset > 0) ArrayOffset--;
}
if (GUILayout.Button("Next >", null))
if (GUILayout.Button("Next >", new GUILayoutOption[] { GUILayout.Width(60) }))
{
if (ArrayOffset < maxOffset) ArrayOffset++;
}
GUILayout.Label("Limit: ", new GUILayoutOption[] { GUILayout.Width(50) });
var limit = this.ArrayLimit.ToString();
limit = GUILayout.TextField(limit, new GUILayoutOption[] { GUILayout.Width(50) });
if (limit != ArrayLimit.ToString() && int.TryParse(limit, out int i))
{
ArrayLimit = i;
}
GUILayout.Space(5);
}
int offset = ArrayOffset * CppExplorer.ArrayLimit;
int offset = ArrayOffset * ArrayLimit;
if (offset >= count) offset = 0;
if (offset >= count)
{
offset = 0;
ArrayOffset = 0;
}
for (int i = offset; i < offset + CppExplorer.ArrayLimit && i < count; i++)
for (int i = offset; i < offset + ArrayLimit && i < count; i++)
{
var entry = m_cachedEntries[i];
//collapsing the BeginHorizontal called from ReflectionWindow.WindowFunction or previous array entry
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Space(190);
GUILayout.Space(whitespace);
if (entry.Value == null)
{
GUILayout.Label("<i><color=grey>null</color></i>", null);
GUILayout.Label(i + "<i><color=grey> (null)</color></i>", null);
}
else
{
GUILayout.Label(i.ToString(), new GUILayoutOption[] { GUILayout.Width(30) });
entry.DrawValue(window, window.width - 250);
GUI.skin.label.alignment = TextAnchor.MiddleCenter;
GUILayout.Label($"[{i}]", new GUILayoutOption[] { GUILayout.Width(30) });
entry.DrawValue(window, window.width - (whitespace + 85));
}
}
GUI.skin.label.alignment = TextAnchor.UpperLeft;
}
}
public override void SetValue()
{
throw new NotImplementedException("TODO");
}
/// <summary>
/// Called when the user presses the "Update" button, or if AutoUpdate is on.
/// </summary>
public override void UpdateValue()
{
base.UpdateValue();
if (Value == null) return;
var enumerator = Enumerable?.GetEnumerator();
if (enumerator == null) return;
var list = new List<CacheObject>();
while (enumerator.MoveNext())
{
list.Add(GetCacheObject(enumerator.Current));
}
m_cachedEntries = list.ToArray();
}
}
}

View File

@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using UnityEngine;
using MelonLoader;
namespace Explorer
{
public class CacheMethod : CacheObjectBase
{
private bool m_evaluated = false;
private CacheObjectBase m_cachedReturnValue;
private bool m_isEvaluating;
private ParameterInfo[] m_arguments;
private string[] m_argumentInput;
public bool HasParameters
{
get
{
if (m_hasParams == null)
{
m_hasParams = (MemberInfo as MethodInfo).GetParameters().Length > 0;
}
return (bool)m_hasParams;
}
}
private bool? m_hasParams;
public static bool CanEvaluate(MethodInfo mi)
{
// generic type args not supported yet
if (mi.GetGenericArguments().Length > 0)
{
return false;
}
// only primitive and string args supported
foreach (var param in mi.GetParameters())
{
if (!param.ParameterType.IsPrimitive && param.ParameterType != typeof(string))
{
return false;
}
}
return true;
}
public override void Init()
{
base.Init();
var mi = MemberInfo as MethodInfo;
m_arguments = mi.GetParameters();
m_argumentInput = new string[m_arguments.Length];
}
public override void UpdateValue()
{
//base.UpdateValue();
}
public override void DrawValue(Rect window, float width)
{
GUILayout.BeginVertical(null);
string evaluateLabel = "<color=lime>Evaluate</color>";
if (HasParameters)
{
if (m_isEvaluating)
{
for (int i = 0; i < m_arguments.Length; i++)
{
var name = m_arguments[i].Name;
var input = m_argumentInput[i];
var type = m_arguments[i].ParameterType.Name;
GUILayout.BeginHorizontal(null);
m_argumentInput[i] = GUILayout.TextField(input, new GUILayoutOption[] { GUILayout.Width(150) });
GUILayout.Label(i + ": <color=cyan>" + name + "</color> <color=yellow>(" + type + ")</color>", null);
GUILayout.EndHorizontal();
}
GUILayout.BeginHorizontal(null);
if (GUILayout.Button(evaluateLabel, new GUILayoutOption[] { GUILayout.Width(70) }))
{
Evaluate();
m_isEvaluating = false;
}
if (GUILayout.Button("Cancel", new GUILayoutOption[] { GUILayout.Width(70) }))
{
m_isEvaluating = false;
}
}
else
{
GUILayout.BeginHorizontal(null);
if (GUILayout.Button($"Evaluate ({m_arguments.Length} params)", new GUILayoutOption[] { GUILayout.Width(150) }))
{
m_isEvaluating = true;
}
}
}
else
{
GUILayout.BeginHorizontal(null);
if (GUILayout.Button(evaluateLabel, new GUILayoutOption[] { GUILayout.Width(70) }))
{
Evaluate();
}
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
if (m_evaluated)
{
if (m_cachedReturnValue != null)
{
try
{
m_cachedReturnValue.DrawValue(window, width);
}
catch (Exception e)
{
MelonLogger.Log("Exception drawing m_cachedReturnValue!");
MelonLogger.Log(e.ToString());
}
}
else
{
GUILayout.Label($"null (<color=yellow>{ValueType}</color>)", null);
}
}
else
{
GUILayout.Label($"<color=grey><i>Not yet evaluated</i></color> (<color=yellow>{ValueType}</color>)", null);
}
GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
private void Evaluate()
{
var mi = MemberInfo as MethodInfo;
object ret = null;
if (!HasParameters)
{
ret = mi.Invoke(mi.IsStatic ? null : DeclaringInstance, new object[0]);
m_evaluated = true;
}
else
{
var arguments = new List<object>();
for (int i = 0; i < m_arguments.Length; i++)
{
var input = m_argumentInput[i];
var type = m_arguments[i].ParameterType;
if (type == typeof(string))
{
arguments.Add(input);
}
else
{
try
{
if (type.GetMethod("Parse", new Type[] { typeof(string) }).Invoke(null, new object[] { input }) is object parsed)
{
arguments.Add(parsed);
}
else
{
throw new Exception();
}
}
catch
{
MelonLogger.Log($"Unable to parse '{input}' to type '{type.Name}'");
break;
}
}
}
if (arguments.Count == m_arguments.Length)
{
ret = mi.Invoke(mi.IsStatic ? null : DeclaringInstance, arguments.ToArray());
m_evaluated = true;
}
else
{
MelonLogger.Log($"Did not invoke because {m_arguments.Length - arguments.Count} arguments could not be parsed!");
}
}
if (ret != null)
{
m_cachedReturnValue = GetCacheObject(ret);
if (m_cachedReturnValue is CacheList cacheList)
{
cacheList.WhiteSpace = 0f;
cacheList.ButtonWidthOffset += 70f;
}
m_cachedReturnValue.UpdateValue();
}
else
{
m_cachedReturnValue = null;
}
}
}
}

View File

@ -1,194 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using MelonLoader;
using UnhollowerBaseLib;
using UnityEngine;
namespace Explorer
{
public abstract class CacheObject
{
public object Value;
public string ValueType;
// Reflection window only
public MemberInfo MemberInfo { get; set; }
public ReflectionWindow.MemberInfoType MemberInfoType { get; set; }
public Type DeclaringType { get; set; }
public object DeclaringInstance { get; set; }
public string FullName => $"{MemberInfo.DeclaringType.Name}.{MemberInfo.Name}";
public string ReflectionException;
// methods
public abstract void DrawValue(Rect window, float width);
public abstract void SetValue();
public static CacheObject GetCacheObject(object obj)
{
return GetCacheObject(obj, null, null);
}
/// <summary>
/// Gets the CacheObject subclass for an object or MemberInfo
/// </summary>
/// <param name="obj">The current value (can be null if memberInfo is not null)</param>
/// <param name="memberInfo">The MemberInfo (can be null if obj is not null)</param>
/// <param name="declaringInstance">If MemberInfo is not null, the declaring class instance. Can be null if static.</param>
/// <returns></returns>
public static CacheObject GetCacheObject(object obj, MemberInfo memberInfo, object declaringInstance)
{
CacheObject holder;
var type = ReflectionHelpers.GetActualType(obj) ?? (memberInfo as FieldInfo)?.FieldType ?? (memberInfo as PropertyInfo)?.PropertyType;
if ((obj is Il2CppSystem.Object || typeof(Il2CppSystem.Object).IsAssignableFrom(type))
&& (type.FullName.Contains("UnityEngine.GameObject") || type.FullName.Contains("UnityEngine.Transform")))
{
holder = new CacheGameObject(obj);
}
else if (type.IsPrimitive || type == typeof(string))
{
holder = new CachePrimitive(obj);
}
else if (type.IsEnum)
{
holder = new CacheEnum(obj);
}
else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(type) || ReflectionHelpers.IsList(type))
{
holder = new CacheList(obj);
}
else
{
holder = new CacheOther();
}
if (memberInfo != null)
{
holder.MemberInfo = memberInfo;
holder.DeclaringType = memberInfo.DeclaringType;
holder.DeclaringInstance = declaringInstance;
//if (declaringInstance is Il2CppSystem.Object ilInstance && ilInstance.GetType() != memberInfo.DeclaringType)
//{
// try
// {
// holder.DeclaringInstance = ilInstance.Il2CppCast(holder.DeclaringType);
// }
// catch (Exception e)
// {
// holder.ReflectionException = ReflectionHelpers.ExceptionToString(e);
// holder.DeclaringInstance = declaringInstance;
// }
//}
//else
//{
// holder.DeclaringInstance = declaringInstance;
//}
if (memberInfo.MemberType == MemberTypes.Field)
{
holder.MemberInfoType = ReflectionWindow.MemberInfoType.Field;
}
else if (memberInfo.MemberType == MemberTypes.Property)
{
holder.MemberInfoType = ReflectionWindow.MemberInfoType.Property;
}
else if (memberInfo.MemberType == MemberTypes.Method)
{
holder.MemberInfoType = ReflectionWindow.MemberInfoType.Method;
}
}
holder.Value = obj;
holder.ValueType = type.FullName;
return holder;
}
public void Draw(Rect window, float labelWidth = 180f)
{
if (MemberInfo != null)
{
GUILayout.Label("<color=cyan>" + FullName + ":</color>", new GUILayoutOption[] { GUILayout.Width(labelWidth) });
}
else
{
GUILayout.Space(labelWidth);
}
if (!string.IsNullOrEmpty(ReflectionException))
{
GUILayout.Label("<color=red>Reflection failed!</color> (" + ReflectionException + ")", null);
}
else if (Value == null)
{
GUILayout.Label("<i>null (" + this.ValueType + ")</i>", null);
}
else
{
DrawValue(window, window.width - labelWidth - 90);
}
}
public virtual void UpdateValue()
{
if (MemberInfo == null || !string.IsNullOrEmpty(ReflectionException))
{
return;
}
try
{
if (MemberInfo.MemberType == MemberTypes.Field)
{
var fi = MemberInfo as FieldInfo;
Value = fi.GetValue(fi.IsStatic ? null : DeclaringInstance);
}
else if (MemberInfo.MemberType == MemberTypes.Property)
{
var pi = MemberInfo as PropertyInfo;
bool isStatic = pi.GetAccessors()[0].IsStatic;
var target = isStatic ? null : DeclaringInstance;
Value = pi.GetValue(target, null);
}
//ReflectionException = null;
}
catch (Exception e)
{
ReflectionException = ReflectionHelpers.ExceptionToString(e);
}
}
public void SetValue(object value, MemberInfo memberInfo, object declaringInstance)
{
try
{
if (memberInfo.MemberType == MemberTypes.Field)
{
var fi = memberInfo as FieldInfo;
if (!(fi.IsLiteral && !fi.IsInitOnly))
{
fi.SetValue(fi.IsStatic ? null : declaringInstance, value);
}
}
else if (memberInfo.MemberType == MemberTypes.Property)
{
var pi = memberInfo as PropertyInfo;
if (pi.CanWrite)
{
pi.SetValue(pi.GetAccessors()[0].IsStatic ? null : declaringInstance, value);
}
}
}
catch (Exception e)
{
MelonLogger.LogWarning($"Error setting value: {e.GetType()}, {e.Message}");
}
}
}
}

View File

@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using MelonLoader;
using UnhollowerBaseLib;
using UnityEngine;
namespace Explorer
{
public abstract class CacheObjectBase
{
public object Value;
public string ValueType;
// Reflection window only
public MemberInfo MemberInfo { get; set; }
public Type DeclaringType { get; set; }
public object DeclaringInstance { get; set; }
public string RichTextName
{
get
{
if (m_richTextName == null)
{
GetRichTextName();
}
return m_richTextName;
}
}
private string m_richTextName;
public string ReflectionException;
public bool CanWrite
{
get
{
if (MemberInfo is FieldInfo fi)
{
return !(fi.IsLiteral && !fi.IsInitOnly);
}
else if (MemberInfo is PropertyInfo pi)
{
return pi.CanWrite;
}
else
{
return false;
}
}
}
// methods
public virtual void Init() { }
public abstract void DrawValue(Rect window, float width);
public static CacheObjectBase GetCacheObject(object obj)
{
return GetCacheObject(obj, null, null);
}
/// <summary>
/// Gets the CacheObject subclass for an object or MemberInfo
/// </summary>
/// <param name="obj">The current value (can be null if memberInfo is not null)</param>
/// <param name="memberInfo">The MemberInfo (can be null if obj is not null)</param>
/// <param name="declaringInstance">If MemberInfo is not null, the declaring class instance. Can be null if static.</param>
/// <returns></returns>
public static CacheObjectBase GetCacheObject(object obj, MemberInfo memberInfo, object declaringInstance)
{
//var type = ReflectionHelpers.GetActualType(obj) ?? (memberInfo as FieldInfo)?.FieldType ?? (memberInfo as PropertyInfo)?.PropertyType;
Type type = null;
if (obj != null)
{
type = ReflectionHelpers.GetActualType(obj);
}
else if (memberInfo != null)
{
if (memberInfo is FieldInfo fi)
{
type = fi.FieldType;
}
else if (memberInfo is PropertyInfo pi)
{
type = pi.PropertyType;
}
else if (memberInfo is MethodInfo mi)
{
type = mi.ReturnType;
}
}
if (type == null)
{
return null;
}
return GetCacheObject(obj, memberInfo, declaringInstance, type);
}
/// <summary>
/// Gets the CacheObject subclass for an object or MemberInfo
/// </summary>
/// <param name="obj">The current value (can be null if memberInfo is not null)</param>
/// <param name="memberInfo">The MemberInfo (can be null if obj is not null)</param>
/// <param name="declaringInstance">If MemberInfo is not null, the declaring class instance. Can be null if static.</param>
/// <param name="valueType">The type of the object or MemberInfo value.</param>
/// <returns></returns>
public static CacheObjectBase GetCacheObject(object obj, MemberInfo memberInfo, object declaringInstance, Type valueType)
{
CacheObjectBase holder;
if (memberInfo is MethodInfo mi)
{
if (CacheMethod.CanEvaluate(mi))
{
holder = new CacheMethod();
}
else
{
return null;
}
}
else if (valueType == typeof(GameObject) || valueType == typeof(Transform))
{
holder = new CacheGameObject();
}
else if (valueType.IsPrimitive || valueType == typeof(string))
{
holder = new CachePrimitive();
}
else if (valueType.IsEnum)
{
holder = new CacheEnum();
}
else if (ReflectionHelpers.IsArray(valueType) || ReflectionHelpers.IsList(valueType))
{
holder = new CacheList();
}
else if (ReflectionHelpers.IsDictionary(valueType))
{
holder = new CacheDictionary();
}
else
{
holder = new CacheOther();
}
holder.Value = obj;
holder.ValueType = valueType.FullName;
if (memberInfo != null)
{
holder.MemberInfo = memberInfo;
holder.DeclaringType = memberInfo.DeclaringType;
holder.DeclaringInstance = declaringInstance;
holder.UpdateValue();
}
holder.Init();
return holder;
}
// ======== Updating and Setting Value (memberinfo only) =========
public virtual void UpdateValue()
{
if (MemberInfo == null || !string.IsNullOrEmpty(ReflectionException))
{
return;
}
try
{
if (MemberInfo.MemberType == MemberTypes.Field)
{
var fi = MemberInfo as FieldInfo;
Value = fi.GetValue(fi.IsStatic ? null : DeclaringInstance);
}
else if (MemberInfo.MemberType == MemberTypes.Property)
{
var pi = MemberInfo as PropertyInfo;
bool isStatic = pi.GetAccessors()[0].IsStatic;
var target = isStatic ? null : DeclaringInstance;
Value = pi.GetValue(target, null);
}
//ReflectionException = null;
}
catch (Exception e)
{
ReflectionException = ReflectionHelpers.ExceptionToString(e);
}
}
public void SetValue()
{
try
{
if (MemberInfo.MemberType == MemberTypes.Field)
{
var fi = MemberInfo as FieldInfo;
fi.SetValue(fi.IsStatic ? null : DeclaringInstance, Value);
}
else if (MemberInfo.MemberType == MemberTypes.Property)
{
var pi = MemberInfo as PropertyInfo;
pi.SetValue(pi.GetAccessors()[0].IsStatic ? null : DeclaringInstance, Value);
}
}
catch (Exception e)
{
MelonLogger.LogWarning($"Error setting value: {e.GetType()}, {e.Message}");
}
}
// ========= Gui Draw ==========
public const float MAX_LABEL_WIDTH = 400f;
public static void ClampLabelWidth(Rect window, ref float labelWidth)
{
float min = window.width * 0.37f;
if (min > MAX_LABEL_WIDTH) min = MAX_LABEL_WIDTH;
labelWidth = Mathf.Clamp(labelWidth, min, MAX_LABEL_WIDTH);
}
public void Draw(Rect window, float labelWidth = 215f)
{
if (labelWidth > 0)
{
ClampLabelWidth(window, ref labelWidth);
}
if (MemberInfo != null)
{
GUILayout.Label(RichTextName, new GUILayoutOption[] { GUILayout.Width(labelWidth) });
}
else
{
GUILayout.Space(labelWidth);
}
if (!string.IsNullOrEmpty(ReflectionException))
{
GUILayout.Label("<color=red>Reflection failed!</color> (" + ReflectionException + ")", null);
}
else if (Value == null && MemberInfo?.MemberType != MemberTypes.Method)
{
GUILayout.Label("<i>null (" + ValueType + ")</i>", null);
}
else
{
DrawValue(window, window.width - labelWidth - 90);
}
}
private void GetRichTextName()
{
string memberColor = "";
switch (MemberInfo.MemberType)
{
case MemberTypes.Field:
memberColor = "#c266ff"; break;
case MemberTypes.Property:
memberColor = "#72a6a6"; break;
case MemberTypes.Method:
memberColor = "#ff8000"; break;
};
m_richTextName = $"<color=#2df7b2>{MemberInfo.DeclaringType.Name}</color>.<color={memberColor}>{MemberInfo.Name}</color>";
if (MemberInfo is MethodInfo mi)
{
m_richTextName += "(";
var _params = "";
foreach (var param in mi.GetParameters())
{
if (_params != "") _params += ", ";
_params += $"<color=#a6e9e9>{param.Name}</color>";
}
m_richTextName += _params;
m_richTextName += ")";
}
}
}
}

View File

@ -9,7 +9,7 @@ using UnityEngine;
namespace Explorer
{
public class CacheOther : CacheObject
public class CacheOther : CacheObjectBase
{
private MethodInfo m_toStringMethod;
private bool m_triedToGetMethod;
@ -58,21 +58,11 @@ namespace Explorer
}
GUI.skin.button.alignment = TextAnchor.MiddleLeft;
if (GUILayout.Button("<color=yellow>" + label + "</color>", new GUILayoutOption[] { GUILayout.MaxWidth(width) }))
if (GUILayout.Button("<color=yellow>" + label + "</color>", new GUILayoutOption[] { GUILayout.MaxWidth(width + 40) }))
{
WindowManager.InspectObject(Value, out bool _);
}
GUI.skin.button.alignment = TextAnchor.MiddleCenter;
}
public override void SetValue()
{
throw new NotImplementedException("TODO");
}
//public override void UpdateValue(object obj)
//{
//}
}
}

View File

@ -1,87 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using MelonLoader;
using UnityEngine;
namespace Explorer
{
public class CachePrimitive : CacheObject
public class CachePrimitive : CacheObjectBase
{
public enum PrimitiveType
public enum PrimitiveTypes
{
Bool,
Double,
Float,
Int,
String
String,
Char
}
private readonly PrimitiveType m_primitiveType;
private string m_valueToString;
public CachePrimitive(object obj)
public PrimitiveTypes PrimitiveType;
public MethodInfo ParseMethod
{
if (obj == null) return;
get
{
if (m_parseMethod == null)
{
m_parseMethod = Value.GetType().GetMethod("Parse", new Type[] { typeof(string) });
}
return m_parseMethod;
}
}
if (obj is bool)
private MethodInfo m_parseMethod;
public override void Init()
{
if (Value == null)
{
m_primitiveType = PrimitiveType.Bool;
// this must mean it is a string. No other primitive type should be nullable.
PrimitiveType = PrimitiveTypes.String;
return;
}
else if (obj is double)
m_valueToString = Value.ToString();
var type = Value.GetType();
if (type == typeof(bool))
{
m_primitiveType = PrimitiveType.Double;
PrimitiveType = PrimitiveTypes.Bool;
}
else if (obj is float)
else if (type == typeof(double))
{
m_primitiveType = PrimitiveType.Float;
PrimitiveType = PrimitiveTypes.Double;
}
else if (obj is int || obj is IntPtr || obj is uint)
else if (type == typeof(float))
{
m_primitiveType = PrimitiveType.Int;
PrimitiveType = PrimitiveTypes.Float;
}
else if (obj is string)
else if (IsInteger(type))
{
m_primitiveType = PrimitiveType.String;
PrimitiveType = PrimitiveTypes.Int;
}
else if (type == typeof(char))
{
PrimitiveType = PrimitiveTypes.Char;
}
else
{
PrimitiveType = PrimitiveTypes.String;
}
}
private static bool IsInteger(Type type)
{
// For our purposes, all types of int can be treated the same, including IntPtr.
return _integerTypes.Contains(type);
}
private static readonly HashSet<Type> _integerTypes = new HashSet<Type>
{
typeof(int),
typeof(uint),
typeof(short),
typeof(ushort),
typeof(long),
typeof(ulong),
typeof(byte),
typeof(sbyte),
typeof(IntPtr)
};
public override void UpdateValue()
{
base.UpdateValue();
m_valueToString = Value?.ToString();
}
public override void DrawValue(Rect window, float width)
{
if (m_primitiveType == PrimitiveType.Bool && Value is bool b)
if (PrimitiveType == PrimitiveTypes.Bool)
{
var color = "<color=" + (b ? "lime>" : "red>");
Value = GUILayout.Toggle((bool)Value, color + Value.ToString() + "</color>", null);
var b = (bool)Value;
var color = $"<color={(b ? "lime>" : "red>")}";
var label = $"{color}{b}</color>";
if (b != (bool)Value)
if (CanWrite)
{
SetValue();
b = GUILayout.Toggle(b, label, null);
if (b != (bool)Value)
{
SetValueFromInput(b.ToString());
}
}
else
{
GUILayout.Label(label, null);
}
}
else
{
var toString = Value.ToString();
if (toString.Length > 37)
GUILayout.Label("<color=yellow><i>" + PrimitiveType + "</i></color>", new GUILayoutOption[] { GUILayout.Width(50) });
int dynSize = 25 + (m_valueToString.Length * 15);
var maxwidth = window.width - 300f;
if (CanWrite) maxwidth -= 60;
if (dynSize > maxwidth)
{
Value = GUILayout.TextArea(toString, new GUILayoutOption[] { GUILayout.MaxWidth(window.width - 260) });
m_valueToString = GUILayout.TextArea(m_valueToString, new GUILayoutOption[] { GUILayout.MaxWidth(maxwidth) });
}
else
{
Value = GUILayout.TextField(toString, new GUILayoutOption[] { GUILayout.MaxWidth(window.width - 260) });
m_valueToString = GUILayout.TextField(m_valueToString, new GUILayoutOption[] { GUILayout.MaxWidth(dynSize) });
}
if (MemberInfo != null)
if (CanWrite)
{
if (GUILayout.Button("<color=#00FF00>Apply</color>", new GUILayoutOption[] { GUILayout.Width(60) }))
{
SetValue();
SetValueFromInput(m_valueToString);
}
}
GUILayout.Space(5);
}
}
public override void SetValue()
public void SetValueFromInput(string value)
{
if (MemberInfo == null)
{
@ -89,19 +158,24 @@ namespace Explorer
return;
}
switch (m_primitiveType)
if (PrimitiveType == PrimitiveTypes.String)
{
case PrimitiveType.Bool:
SetValue(bool.Parse(Value.ToString()), MemberInfo, DeclaringInstance); return;
case PrimitiveType.Double:
SetValue(double.Parse(Value.ToString()), MemberInfo, DeclaringInstance); return;
case PrimitiveType.Float:
SetValue(float.Parse(Value.ToString()), MemberInfo, DeclaringInstance); return;
case PrimitiveType.Int:
SetValue(int.Parse(Value.ToString()), MemberInfo, DeclaringInstance); return;
case PrimitiveType.String:
SetValue(Value.ToString(), MemberInfo, DeclaringInstance); return;
Value = value;
}
else
{
try
{
var val = ParseMethod.Invoke(null, new object[] { value });
Value = val;
}
catch (Exception e)
{
MelonLogger.Log("Exception parsing value: " + e.GetType() + ", " + e.Message);
}
}
SetValue();
}
}
}

View File

@ -1,19 +1,18 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using System.Reflection;
using Harmony;
using MelonLoader;
using UnhollowerBaseLib;
using UnityEngine;
namespace Explorer
{
public class CppExplorer : MelonMod
{
// consts
public const string ID = "com.sinai.cppexplorer";
public const string VERSION = "1.4.1";
public const string GUID = "com.sinai.cppexplorer";
public const string VERSION = "1.5.0";
public const string AUTHOR = "Sinai";
public const string NAME = "CppExplorer"
@ -22,27 +21,52 @@ namespace Explorer
#endif
;
// fields
public static CppExplorer Instance { get; private set; }
public static CppExplorer Instance;
public static bool ShowMenu
{
get => m_showMenu;
set => SetShowMenu(value);
}
private static bool m_showMenu;
// props
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 ShowMenu { get; set; } = false;
public static int ArrayLimit { get; set; } = 20;
public static bool ShouldForceMouse => ShowMenu && ForceUnlockMouse;
// methods
private static void SetShowMenu(bool show)
{
m_showMenu = show;
UpdateCursorControl();
}
// ========== MonoBehaviour methods ==========
public override void OnApplicationStart()
{
base.OnApplicationStart();
Instance = this;
new MainMenu();
new WindowManager();
ShowMenu = true;
// Get current cursor state and enable cursor
m_lastLockMode = Cursor.lockState;
m_lastVisibleState = Cursor.visible;
// Enable ShowMenu and ForceUnlockMouse
// (set m_showMenu to not call UpdateCursorState twice)
m_showMenu = true;
SetForceUnlock(true);
MelonLogger.Log($"CppExplorer {VERSION} initialized.");
}
public override void OnLevelWasLoaded(int level)
@ -53,6 +77,7 @@ namespace Explorer
public override void OnUpdate()
{
// Check main toggle key input
if (Input.GetKeyDown(KeyCode.F7))
{
ShowMenu = !ShowMenu;
@ -60,27 +85,115 @@ namespace Explorer
if (ShowMenu)
{
if (!Cursor.visible)
// Check Force-Unlock input
if (Input.GetKeyDown(KeyCode.LeftAlt))
{
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
ForceUnlockMouse = !ForceUnlockMouse;
}
MainMenu.Instance.Update();
WindowManager.Instance.Update();
InspectUnderMouse.Update();
}
}
public override void OnGUI()
{
base.OnGUI();
MainMenu.Instance.OnGUI();
WindowManager.Instance.OnGUI();
InspectUnderMouse.OnGUI();
}
// =========== Cursor control ===========
private static void SetForceUnlock(bool unlock)
{
m_forceUnlock = unlock;
UpdateCursorControl();
}
private static void UpdateCursorControl()
{
m_currentlySettingCursor = true;
if (ShouldForceMouse)
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
else
{
Cursor.lockState = m_lastLockMode;
Cursor.visible = m_lastVisibleState;
}
m_currentlySettingCursor = false;
}
// 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.
[HarmonyPatch(typeof(Cursor), nameof(Cursor.lockState), MethodType.Setter)]
public class Cursor_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.visible), MethodType.Getter)]
public class Cursor_get_visible
{
[HarmonyPostfix]
public static void Postfix(ref bool __result)
{
if (ShouldForceMouse)
{
__result = m_lastVisibleState;
}
}
}
[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;
}
}
}
}
}

View File

@ -42,10 +42,6 @@
<HintPath>..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\Il2Cppmscorlib.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Il2CppSystem.Core">
<HintPath>..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\Il2CppSystem.Core.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="mcs">
<HintPath>..\lib\mcs.dll</HintPath>
<Private>False</Private>
@ -64,10 +60,6 @@
<HintPath>..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnhollowerBaseLib.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnhollowerRuntimeLib">
<HintPath>..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnhollowerRuntimeLib.dll</HintPath>
<Private>False</Private>
</Reference>
<!-- Unity 2019 build (InputLegacyModule.dll) -->
<Reference Include="UnityEngine" Condition="'$(Configuration)'=='Debug'">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.dll</HintPath>
@ -97,10 +89,6 @@
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.UI.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.UIElementsModule" Condition="'$(Configuration)'=='Debug'">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.UIElementsModule.dll</HintPath>
<Private>False</Private>
</Reference>
<!-- Unity 2018 build (InputModule.dll) -->
<Reference Include="UnityEngine" Condition="'$(Configuration)'=='Release_Unity2018'">
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.dll</HintPath>
@ -130,17 +118,15 @@
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.UI.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.UIElementsModule" Condition="'$(Configuration)'=='Release_Unity2018'">
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.UIElementsModule.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="CachedObjects\CacheDictionary.cs" />
<Compile Include="CachedObjects\CacheEnum.cs" />
<Compile Include="CachedObjects\CacheGameObject.cs" />
<Compile Include="CachedObjects\CacheList.cs" />
<Compile Include="CachedObjects\CachePrimitive.cs" />
<Compile Include="CachedObjects\CacheOther.cs" />
<Compile Include="CachedObjects\CacheMethod.cs" />
<Compile Include="CppExplorer.cs" />
<Compile Include="Extensions\ReflectionExtensions.cs" />
<Compile Include="Extensions\UnityExtensions.cs" />
@ -148,7 +134,9 @@
<Compile Include="Helpers\UIHelpers.cs" />
<Compile Include="Helpers\UnityHelpers.cs" />
<Compile Include="MainMenu\InspectUnderMouse.cs" />
<Compile Include="CachedObjects\CacheObject.cs" />
<Compile Include="CachedObjects\CacheObjectBase.cs" />
<Compile Include="Windows\ResizeDrag.cs" />
<Compile Include="Windows\TabViewWindow.cs" />
<Compile Include="Windows\UIWindow.cs" />
<Compile Include="MainMenu\Pages\ConsolePage.cs" />
<Compile Include="MainMenu\Pages\Console\REPL.cs" />

View File

@ -8,20 +8,18 @@ using UnhollowerBaseLib;
using UnhollowerRuntimeLib;
using UnityEngine;
using BF = System.Reflection.BindingFlags;
using ILBF = Il2CppSystem.Reflection.BindingFlags;
using MelonLoader;
namespace Explorer
{
public class ReflectionHelpers
{
public static BF CommonFlags = BF.Public | BF.Instance | BF.NonPublic | BF.Static;
public static ILBF CommonFlags_IL = ILBF.Public | ILBF.NonPublic | ILBF.Instance | ILBF.Static;
public static Il2CppSystem.Type GameObjectType => Il2CppType.Of<GameObject>();
public static Il2CppSystem.Type TransformType => Il2CppType.Of<Transform>();
public static Il2CppSystem.Type ObjectType => Il2CppType.Of<UnityEngine.Object>();
public static Il2CppSystem.Type ComponentType => Il2CppType.Of<Component>();
public static Il2CppSystem.Type BehaviourType => Il2CppType.Of<Behaviour>();
private static readonly MethodInfo m_tryCastMethodInfo = typeof(Il2CppObjectBase).GetMethod("TryCast");
@ -74,11 +72,30 @@ namespace Explorer
return false;
}
public static bool IsArray(Type t)
{
return typeof(System.Collections.IEnumerable).IsAssignableFrom(t);
}
public static bool IsList(Type t)
{
if (t.IsGenericType)
{
return t.GetGenericTypeDefinition() is Type typeDef
&& (typeDef.IsAssignableFrom(typeof(Il2CppSystem.Collections.Generic.List<>))
|| typeDef.IsAssignableFrom(typeof(Il2CppSystem.Collections.Generic.IList<>)));
}
else
{
return t.IsAssignableFrom(typeof(Il2CppSystem.Collections.IList));
}
}
public static bool IsDictionary(Type t)
{
return t.IsGenericType
&& t.GetGenericTypeDefinition() is Type typeDef
&& (typeDef.IsAssignableFrom(typeof(List<>)) || typeDef.IsAssignableFrom(typeof(Il2CppSystem.Collections.Generic.List<>)));
&& typeDef.IsAssignableFrom(typeof(Il2CppSystem.Collections.Generic.Dictionary<,>));
}
public static Type GetTypeByName(string typeName)
@ -105,7 +122,14 @@ namespace Explorer
if (m_object is Il2CppSystem.Object ilObject)
{
var iltype = ilObject.GetIl2CppType();
return Type.GetType(iltype.AssemblyQualifiedName);
if (Type.GetType(iltype.AssemblyQualifiedName) is Type type)
{
return type;
}
else
{
return ilObject.GetType();
}
}
else
{

View File

@ -22,7 +22,7 @@ namespace Explorer
}
// helper for drawing a styled button for a GameObject or Transform
public static void GameobjButton(GameObject obj, Action<GameObject> specialInspectMethod = null, bool showSmallInspectBtn = true, float width = 380)
public static void GameobjButton(GameObject obj, Action<Transform> specialInspectMethod = null, bool showSmallInspectBtn = true, float width = 380)
{
bool children = obj.transform.childCount > 0;
@ -52,7 +52,7 @@ namespace Explorer
FastGameobjButton(obj, color, label, obj.activeSelf, specialInspectMethod, showSmallInspectBtn, width);
}
public static void FastGameobjButton(GameObject obj, Color activeColor, string label, bool enabled, Action<GameObject> specialInspectMethod = null, bool showSmallInspectBtn = true, float width = 380)
public static void FastGameobjButton(GameObject obj, Color activeColor, string label, bool enabled, Action<Transform> specialInspectMethod = null, bool showSmallInspectBtn = true, float width = 380)
{
if (!obj)
{
@ -79,7 +79,7 @@ namespace Explorer
{
if (specialInspectMethod != null)
{
specialInspectMethod(obj);
specialInspectMethod(obj.transform);
}
else
{
@ -94,13 +94,18 @@ namespace Explorer
if (showSmallInspectBtn)
{
if (GUILayout.Button("Inspect", null))
{
WindowManager.InspectObject(obj, out bool _);
}
SmallInspectButton(obj);
}
GUILayout.EndHorizontal();
}
public static void SmallInspectButton(object obj)
{
if (GUILayout.Button("Inspect", null))
{
WindowManager.InspectObject(obj, out bool _);
}
}
}
}

View File

@ -69,8 +69,8 @@ namespace Explorer
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));
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;

View File

@ -81,26 +81,13 @@ namespace Explorer
page.DrawWindow();
GUILayout.EndScrollView();
MainRect = WindowManager.ResizeWindow(MainRect, MainWindowID);
MainRect = ResizeDrag.ResizeWindow(MainRect, MainWindowID);
GUILayout.EndArea();
}
private void MainHeader()
{
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Options:</b>", 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;
}
InspectUnderMouse.EnableInspect = GUILayout.Toggle(InspectUnderMouse.EnableInspect, "Inspect Under Mouse (Shift + RMB)", null);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
for (int i = 0; i < Pages.Count; i++)
{
@ -116,6 +103,17 @@ namespace Explorer
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUI.color = Color.white;
InspectUnderMouse.EnableInspect = GUILayout.Toggle(InspectUnderMouse.EnableInspect, "Inspect Under Mouse (Shift + RMB)", null);
bool mouseState = CppExplorer.ForceUnlockMouse;
bool setMouse = GUILayout.Toggle(mouseState, "Force Unlock Mouse (Left Alt)", null);
if (setMouse != mouseState) CppExplorer.ForceUnlockMouse = setMouse;
WindowManager.TabView = GUILayout.Toggle(WindowManager.TabView, "Tab View", null);
GUILayout.EndHorizontal();
GUILayout.Space(10);
GUI.color = Color.white;
}

View File

@ -86,11 +86,11 @@ MelonLogger.Log(""hello world"");";
if (!UsingDirectives.Contains(asm))
{
UsingDirectives.Add(asm);
Evaluate(AsmToUsing(asm));
Evaluate(AsmToUsing(asm), true);
}
}
public object Evaluate(string str)
public object Evaluate(string str, bool suppressWarning = false)
{
object ret = VoidType.Value;
@ -98,11 +98,19 @@ MelonLogger.Log(""hello world"");";
try
{
compiled?.Invoke(ref ret);
if (compiled == null)
{
throw new Exception("Mono.Csharp Service was unable to compile the code provided.");
}
compiled.Invoke(ref ret);
}
catch (Exception e)
{
MelonLogger.LogWarning(e.ToString());
if (!suppressWarning)
{
MelonLogger.LogWarning(e.GetType() + ", " + e.Message);
}
}
return ret;
@ -113,8 +121,10 @@ MelonLogger.Log(""hello world"");";
{
GUILayout.Label("<b><size=15><color=cyan>C# REPL Console</color></size></b>", null);
GUILayout.Label("Method:", null);
MethodInput = GUILayout.TextArea(MethodInput, new GUILayoutOption[] { GUILayout.Height(300) });
GUI.skin.label.alignment = TextAnchor.UpperLeft;
GUILayout.Label("Enter code here as though it is a method body:", null);
MethodInput = GUILayout.TextArea(MethodInput, new GUILayoutOption[] { GUILayout.Height(250) });
if (GUILayout.Button("<color=cyan><b>Execute</b></color>", null))
{
@ -139,12 +149,9 @@ MelonLogger.Log(""hello world"");";
}
GUILayout.Label("<b>Using directives:</b>", null);
foreach (var asm in UsingDirectives)
{
GUILayout.Label(AsmToUsing(asm, true), null);
}
GUILayout.BeginHorizontal(null);
GUILayout.Label("Add namespace:", new GUILayoutOption[] { GUILayout.Width(110) });
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) }))
{
@ -155,6 +162,11 @@ MelonLogger.Log(""hello world"");";
ResetConsole();
}
GUILayout.EndHorizontal();
foreach (var asm in UsingDirectives)
{
GUILayout.Label(AsmToUsing(asm, true), null);
}
}
public override void Update() { }

View File

@ -14,9 +14,15 @@ namespace Explorer
public override string Name { get => "Scene Explorer"; set => base.Name = value; }
// ----- Holders for GUI elements ----- //
private int m_pageOffset = 0;
private int m_limit = 20;
private int m_currentTotalCount = 0;
private string m_currentScene = "";
private float m_timeOfLastUpdate = -1f;
// ----- Holders for GUI elements ----- //
private string m_currentScene = "";
// gameobject list
private Transform m_currentTransform;
@ -37,190 +43,84 @@ namespace Explorer
public void OnSceneChange()
{
m_currentScene = UnityHelpers.ActiveSceneName;
SetTransformTarget(null);
}
m_currentTransform = null;
CancelSearch();
public void CheckOffset(ref int offset, int childCount)
{
if (offset >= childCount)
{
offset = 0;
m_pageOffset = 0;
}
}
public override void Update()
{
if (!m_searching)
if (m_searching) return;
if (Time.time - m_timeOfLastUpdate < 1f) return;
m_timeOfLastUpdate = Time.time;
m_objectList = new List<GameObjectCache>();
int offset = m_pageOffset * m_limit;
var allTransforms = new List<Transform>();
// get current list of all transforms (either scene root or our current transform children)
if (m_currentTransform)
{
m_objectList = new List<GameObjectCache>();
if (m_currentTransform)
for (int i = 0; i < m_currentTransform.childCount; i++)
{
var endAppend = new List<GameObjectCache>();
for (int i = 0; i < m_currentTransform.childCount; i++)
{
var child = m_currentTransform.GetChild(i);
if (child)
{
if (child.childCount > 0)
m_objectList.Add(new GameObjectCache(child.gameObject));
else
endAppend.Add(new GameObjectCache(child.gameObject));
}
}
m_objectList.AddRange(endAppend);
endAppend = null;
allTransforms.Add(m_currentTransform.GetChild(i));
}
else
}
else
{
var scene = SceneManager.GetSceneByName(m_currentScene);
var rootObjects = scene.GetRootGameObjects();
foreach (var obj in rootObjects)
{
var scene = SceneManager.GetSceneByName(m_currentScene);
var rootObjects = scene.GetRootGameObjects();
// add objects with children first
foreach (var obj in rootObjects.Where(x => x.transform.childCount > 0))
{
m_objectList.Add(new GameObjectCache(obj));
}
foreach (var obj in rootObjects.Where(x => x.transform.childCount == 0))
{
m_objectList.Add(new GameObjectCache(obj));
}
allTransforms.Add(obj.transform);
}
}
m_currentTotalCount = allTransforms.Count;
// make sure offset doesn't exceed count
CheckOffset(ref offset, m_currentTotalCount);
// sort by childcount
allTransforms.Sort((a, b) => b.childCount.CompareTo(a.childCount));
for (int i = offset; i < offset + m_limit && i < m_currentTotalCount; i++)
{
var child = allTransforms[i];
m_objectList.Add(new GameObjectCache(child.gameObject));
}
}
// --------- GUI Draw Functions --------- //
public override void DrawWindow()
public void SetTransformTarget(Transform t)
{
try
{
GUILayout.BeginHorizontal(null);
// Current Scene label
GUILayout.Label("Current Scene:", new GUILayoutOption[] { GUILayout.Width(120) });
try
{
// Need to do 'ToList()' so the object isn't cleaned up by Il2Cpp GC.
var scenes = SceneManager.GetAllScenes().ToList();
m_currentTransform = t;
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;
}
}
}
catch { }
GUILayout.Label("<color=cyan>" + m_currentScene + "</color>", null); //new GUILayoutOption[] { GUILayout.Width(250) });
if (m_searching)
CancelSearch();
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(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(m_currentTransform.GetGameObjectPath(), 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);
UIHelpers.FastGameobjButton(obj.RefGameObject, obj.EnabledColor, obj.Label, obj.RefGameObject.activeSelf, SetTransformTarget, true, MainMenu.MainRect.width - 170);
}
}
}
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);
UIHelpers.FastGameobjButton(obj.RefGameObject, obj.EnabledColor, obj.Label, obj.RefGameObject.activeSelf, SetTransformTarget, true, MainMenu.MainRect.width - 170);
}
}
else
{
GUILayout.Label("<color=red><i>No results found!</i></color>", null);
}
}
}
catch
{
m_currentTransform = null;
}
}
// -------- Actual Methods (not drawing GUI) ---------- //
public void SetTransformTarget(GameObject obj)
{
m_currentTransform = obj.transform;
CancelSearch();
m_timeOfLastUpdate = -1f;
Update();
}
public void TraverseUp()
{
if (m_currentTransform.parent != null)
{
m_currentTransform = m_currentTransform.parent;
SetTransformTarget(m_currentTransform.parent);
}
else
{
m_currentTransform = null;
SetTransformTarget(null);
}
}
@ -228,6 +128,7 @@ namespace Explorer
{
m_searchResults = SearchSceneObjects(m_searchInput);
m_searching = true;
m_currentTotalCount = m_searchResults.Count;
}
public void CancelSearch()
@ -249,6 +150,233 @@ namespace Explorer
return matches;
}
// --------- 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 (Exception e)
{
MelonLogger.Log("Exception drawing ScenePage! " + e.GetType() + ", " + e.Message);
MelonLogger.Log(e.StackTrace);
m_currentTransform = null;
}
}
private void DrawHeaderArea()
{
GUILayout.BeginHorizontal(null);
// Current Scene label
GUILayout.Label("Current Scene:", new GUILayoutOption[] { GUILayout.Width(120) });
try
{
// 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;
}
}
}
catch { }
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 DrawPageButtons()
{
GUILayout.BeginHorizontal(null);
GUILayout.Label("Limit per page: ", new GUILayoutOption[] { GUILayout.Width(100) });
var limit = m_limit.ToString();
limit = GUILayout.TextField(limit, new GUILayoutOption[] { GUILayout.Width(30) });
if (int.TryParse(limit, out int lim))
{
m_limit = lim;
}
// prev/next page buttons
if (m_currentTotalCount > m_limit)
{
int count = m_currentTotalCount;
int maxOffset = (int)Mathf.Ceil((float)(count / (decimal)m_limit)) - 1;
if (GUILayout.Button("< Prev", null))
{
if (m_pageOffset > 0) m_pageOffset--;
m_timeOfLastUpdate = -1f;
Update();
}
GUI.skin.label.alignment = TextAnchor.MiddleCenter;
GUILayout.Label($"Page {m_pageOffset + 1}/{maxOffset + 1}", new GUILayoutOption[] { GUILayout.Width(80) });
if (GUILayout.Button("Next >", null))
{
if (m_pageOffset < maxOffset) m_pageOffset++;
m_timeOfLastUpdate = -1f;
Update();
}
}
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_objectList.Count > 0)
{
foreach (var obj in m_objectList)
{
if (!obj.RefGameObject)
{
string label = "<color=red><i>null";
if (obj.RefGameObject != null)
{
label += " (Destroyed)";
}
label += "</i></color>";
GUILayout.Label(label, null);
}
else
{
UIHelpers.FastGameobjButton(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 = m_pageOffset * m_limit;
if (offset >= m_searchResults.Count)
{
offset = 0;
m_pageOffset = 0;
}
for (int i = offset; i < offset + m_limit && i < m_searchResults.Count; i++)
{
var obj = m_searchResults[i];
if (obj.RefGameObject)
{
UIHelpers.FastGameobjButton(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
{

View File

@ -26,7 +26,7 @@ namespace Explorer
//private List<object> m_searchResults = new List<object>();
private Vector2 resultsScroll = Vector2.zero;
private List<CacheObject> m_searchResults = new List<CacheObject>();
private List<CacheObjectBase> m_searchResults = new List<CacheObjectBase>();
public SceneFilter SceneMode = SceneFilter.Any;
public TypeFilter TypeMode = TypeFilter.Object;
@ -64,7 +64,7 @@ namespace Explorer
private void CacheResults(IEnumerable results)
{
m_searchResults = new List<CacheObject>();
m_searchResults = new List<CacheObjectBase>();
foreach (var obj in results)
{
@ -75,7 +75,7 @@ namespace Explorer
toCache = ilObject.TryCast<GameObject>() ?? ilObject.TryCast<Transform>()?.gameObject ?? ilObject;
}
var cache = CacheObject.GetCacheObject(toCache);
var cache = CacheObjectBase.GetCacheObject(toCache);
m_searchResults.Add(cache);
}
}
@ -135,7 +135,7 @@ namespace Explorer
int offset = m_pageOffset * this.m_limit;
if (offset >= count) m_pageOffset = 0;
for (int i = offset; i < offset + CppExplorer.ArrayLimit && i < count; i++)
for (int i = offset; i < offset + m_limit && i < count; i++)
{
m_searchResults[i].Draw(MainMenu.MainRect, 0f);
//m_searchResults[i].DrawValue(MainMenu.MainRect);

View File

@ -11,7 +11,9 @@ namespace Explorer
{
public class GameObjectWindow : UIWindow
{
public override string Name { get => "GameObject Inspector"; set => Name = value; }
public override string Title => WindowManager.TabView
? $"<color=cyan>[G]</color> {m_object.name}"
: $"GameObject Inspector ({m_object.name})";
public GameObject m_object;
@ -21,18 +23,18 @@ namespace Explorer
private Vector2 m_transformScroll = Vector2.zero;
private Transform[] m_children;
private Component[] m_components;
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<Component> m_cachedDestroyList = new List<Component>();
private readonly List<Component> m_cachedDestroyList = new List<Component>();
//private string m_addComponentInput = "";
private string m_setParentInput = "";
private string m_setParentInput = "Enter a GameObject name or path";
public bool GetObjectAsGameObject()
{
@ -68,11 +70,9 @@ namespace Explorer
}
m_name = m_object.name;
m_scene = m_object.scene == null ? "null" : m_object.scene.name;
//var listComps = new Il2CppSystem.Collections.Generic.List<Component>();
//m_object.GetComponents(listComps);
//m_components = listComps.ToArray();
m_scene = string.IsNullOrEmpty(m_object.scene.name)
? "None"
: m_object.scene.name;
var list = new List<Transform>();
for (int i = 0; i < m_object.transform.childCount; i++)
@ -84,14 +84,33 @@ namespace Explorer
public override void Update()
{
if (!m_object && !GetObjectAsGameObject())
try
{
MelonLogger.Log("Object is null! Destroying window...");
DestroyWindow();
if (!m_object && !GetObjectAsGameObject())
{
throw new Exception("Object is null!");
}
var list = new List<Component>();
foreach (var comp in m_object.GetComponents(ReflectionHelpers.ComponentType))
{
list.Add(comp);
}
m_components = list.ToArray();
}
catch (Exception e)
{
DestroyOnException(e);
}
}
private void InspectGameObject(GameObject obj)
private void DestroyOnException(Exception e)
{
MelonLogger.Log($"{e.GetType()}, {e.Message}");
DestroyWindow();
}
private void InspectGameObject(Transform obj)
{
var window = WindowManager.InspectObject(obj, out bool created);
@ -125,67 +144,81 @@ namespace Explorer
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: <color=cyan>" + (m_scene == "" ? "n/a" : m_scene) + "</color>", null);
if (m_scene == UnityHelpers.ActiveSceneName)
try
{
if (GUILayout.Button("<color=#00FF00>< View in Scene Explorer</color>", new GUILayoutOption[] { GUILayout.Width(230) }))
var rect = WindowManager.TabView ? TabViewWindow.Instance.m_rect : this.m_rect;
if (!WindowManager.TabView)
{
ScenePage.Instance.SetTransformTarget(m_object);
MainMenu.SetCurrentPage(0);
Header();
GUILayout.BeginArea(new Rect(5, 25, rect.width - 10, rect.height - 35), GUI.skin.box);
}
scroll = GUILayout.BeginScrollView(scroll, GUI.skin.scrollView);
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>< View in Scene Explorer</color>", new GUILayoutOption[] { GUILayout.Width(230) }))
{
ScenePage.Instance.SetTransformTarget(m_object.transform);
MainMenu.SetCurrentPage(0);
}
}
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();
GUILayout.EndScrollView();
if (!WindowManager.TabView)
{
m_rect = ResizeDrag.ResizeWindow(rect, windowID);
GUILayout.EndArea();
}
}
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)
catch (Exception e)
{
if (GUILayout.Button("<-", new GUILayoutOption[] { GUILayout.Width(35) }))
{
InspectGameObject(m_object.transform.parent.gameObject);
}
DestroyOnException(e);
}
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()
private void TransformList(Rect m_rect)
{
GUILayout.BeginVertical(GUI.skin.box, new GUILayoutOption[] { GUILayout.Height(250) });
GUILayout.BeginVertical(GUI.skin.box, null); // new GUILayoutOption[] { GUILayout.Height(250) });
m_transformScroll = GUILayout.BeginScrollView(m_transformScroll, GUI.skin.scrollView);
GUILayout.Label("<b>Children:</b>", null);
@ -198,7 +231,7 @@ namespace Explorer
GUILayout.Label("null", null);
continue;
}
UIHelpers.GameobjButton(obj.gameObject, InspectGameObject, false, this.m_rect.width / 2 - 60);
UIHelpers.GameobjButton(obj.gameObject, InspectGameObject, false, m_rect.width / 2 - 60);
}
foreach (var obj in m_children.Where(x => x.childCount == 0))
{
@ -207,7 +240,7 @@ namespace Explorer
GUILayout.Label("null", null);
continue;
}
UIHelpers.GameobjButton(obj.gameObject, InspectGameObject, false, this.m_rect.width / 2 - 60);
UIHelpers.GameobjButton(obj.gameObject, InspectGameObject, false, m_rect.width / 2 - 60);
}
}
else
@ -220,9 +253,9 @@ namespace Explorer
}
private void ComponentList()
private void ComponentList(Rect m_rect)
{
GUILayout.BeginVertical(GUI.skin.box, new GUILayoutOption[] { GUILayout.Height(250) });
GUILayout.BeginVertical(GUI.skin.box, null); // new GUILayoutOption[] { GUILayout.Height(250) });
m_compScroll = GUILayout.BeginScrollView(m_compScroll, GUI.skin.scrollView);
GUILayout.Label("<b><size=15>Components</size></b>", null);
@ -232,36 +265,39 @@ namespace Explorer
m_cachedDestroyList.Clear();
}
var m_components = new Il2CppSystem.Collections.Generic.List<Component>();
m_object.GetComponentsInternal(Il2CppType.Of<Component>(), false, false, true, false, m_components);
var ilTypeOfTransform = Il2CppType.Of<Transform>();
var ilTypeOfBehaviour = Il2CppType.Of<Behaviour>();
foreach (var component in m_components)
if (m_components != null)
{
var ilType = component.GetIl2CppType();
if (ilType == ilTypeOfTransform)
foreach (var component in m_components)
{
continue;
}
if (!component) continue;
GUILayout.BeginHorizontal(null);
if (ilTypeOfBehaviour.IsAssignableFrom(ilType))
{
BehaviourEnabledBtn(component.TryCast<Behaviour>());
var ilType = component.GetIl2CppType();
if (ilType == ReflectionHelpers.TransformType)
{
continue;
}
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 - 90) }))
{
ReflectObject(component);
}
if (GUILayout.Button("<color=red>-</color>", new GUILayoutOption[] { GUILayout.Width(20) }))
{
m_cachedDestroyList.Add(component);
}
GUILayout.EndHorizontal();
}
if (GUILayout.Button("<color=cyan>" + ilType.Name + "</color>", new GUILayoutOption[] { GUILayout.Width(m_rect.width / 2 - 90) }))
{
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)
{
@ -274,24 +310,6 @@ namespace Explorer
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();
}
@ -320,7 +338,7 @@ namespace Explorer
private void GameObjectControls()
{
GUILayout.BeginVertical(GUI.skin.box, new GUILayoutOption[] { GUILayout.Width(530) });
GUILayout.BeginVertical(GUI.skin.box, new GUILayoutOption[] { GUILayout.Width(520) });
GUILayout.Label("<b><size=15>GameObject Controls</size></b>", null);
GUILayout.BeginHorizontal(null);
@ -329,17 +347,18 @@ namespace Explorer
new GUILayoutOption[] { GUILayout.Width(80) });
if (m_object.activeSelf != m_active) { m_object.SetActive(m_active); }
UIHelpers.InstantiateButton(m_object, 100);
UIHelpers.InstantiateButton(m_object, 100);
if (GUILayout.Button("Set DontDestroyOnLoad", new GUILayoutOption[] { GUILayout.Width(170) }))
{
GameObject.DontDestroyOnLoad(m_object);
m_object.hideFlags |= HideFlags.DontUnloadUnusedAsset;
}
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) });
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)
@ -351,6 +370,11 @@ namespace Explorer
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);
@ -362,7 +386,7 @@ namespace Explorer
GUILayout.EndVertical();
if (GUILayout.Button("<color=red><b>Destroy</b></color>", null))
if (GUILayout.Button("<color=red><b>Destroy</b></color>", new GUILayoutOption[] { GUILayout.Width(120) }))
{
GameObject.Destroy(m_object);
DestroyWindow();

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MelonLoader;
using Mono.CSharp;
using UnhollowerBaseLib;
using UnityEngine;
@ -13,34 +12,33 @@ namespace Explorer
{
public class ReflectionWindow : UIWindow
{
public override string Name { get => "Object Reflection"; set => Name = value; }
public override string Title => WindowManager.TabView
? $"<color=cyan>[R]</color> {ObjectType.Name}"
: $"Reflection Inspector ({ObjectType.Name})";
public Type ObjectType;
private CacheObject[] m_cachedMembers;
private CacheObject[] m_cachedMemberFiltered;
private CacheObjectBase[] m_allCachedMembers;
private CacheObjectBase[] m_cachedMembersFiltered;
private int m_pageOffset;
private int m_limitPerPage = 20;
private bool m_autoUpdate = false;
private string m_search = "";
public MemberInfoType m_filter = MemberInfoType.Property;
private bool m_hideFailedReflection = true;
public MemberTypes m_filter = MemberTypes.Property;
private bool m_hideFailedReflection = false;
public enum MemberInfoType
{
Field,
Property,
Method,
All
}
// some extra caching
private UnityEngine.Object m_uObj;
private Component m_component;
public override void Init()
{
var type = ReflectionHelpers.GetActualType(Target);
if (type == null)
{
MelonLogger.Log("Could not get underlying type for object. ToString(): " + Target.ToString());
MelonLogger.Log($"Could not get underlying type for object! Type: {Target?.GetType().AssemblyQualifiedName}, Value ToString: {Target?.ToString()}");
DestroyWindow();
return;
}
@ -49,15 +47,32 @@ namespace Explorer
var types = ReflectionHelpers.GetAllBaseTypes(Target);
CacheMembers(types);
m_filter = MemberInfoType.All;
m_cachedMemberFiltered = m_cachedMembers.Where(x => ShouldProcessMember(x)).ToArray();
UpdateValues();
m_filter = MemberInfoType.Property;
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;
}
}
}
m_filter = MemberTypes.All;
m_autoUpdate = true;
Update();
m_autoUpdate = false;
m_filter = MemberTypes.Property;
}
public override void Update()
{
m_cachedMemberFiltered = m_cachedMembers.Where(x => ShouldProcessMember(x)).ToArray();
m_cachedMembersFiltered = m_allCachedMembers.Where(x => ShouldProcessMember(x)).ToArray();
if (m_autoUpdate)
{
@ -67,42 +82,34 @@ namespace Explorer
private void UpdateValues()
{
UpdateMembers();
}
private void UpdateMembers()
{
foreach (var member in m_cachedMemberFiltered)
foreach (var member in m_cachedMembersFiltered)
{
member.UpdateValue();
}
}
private bool ShouldProcessMember(CacheObject holder)
private bool ShouldProcessMember(CacheObjectBase holder)
{
if (m_filter != MemberInfoType.All && m_filter != holder.MemberInfoType) return false;
if (m_filter != MemberTypes.All && m_filter != holder.MemberInfo?.MemberType) return false;
if (!string.IsNullOrEmpty(holder.ReflectionException) && m_hideFailedReflection) return false;
if (m_search == "" || holder.MemberInfo == null) return true;
return holder.FullName
.ToLower()
.Contains(m_search.ToLower());
var name = holder.MemberInfo.DeclaringType.Name + "." + holder.MemberInfo.Name;
return name.ToLower().Contains(m_search.ToLower());
}
private void CacheMembers(Type[] types)
{
var list = new List<CacheObject>();
var list = new List<CacheObjectBase>();
var names = new List<string>();
foreach (var declaringType in types)
{
if (declaringType == typeof(Il2CppObjectBase)) continue;
MemberInfo[] infos;
string exception = null;
try
@ -115,8 +122,8 @@ namespace Explorer
continue;
}
//object value = null;
object target = Target;
if (target is Il2CppSystem.Object ilObject)
{
try
@ -131,22 +138,47 @@ namespace Explorer
foreach (var member in infos)
{
if (member.MemberType == MemberTypes.Field || member.MemberType == MemberTypes.Property)
if (member.MemberType == MemberTypes.Field || member.MemberType == MemberTypes.Property || member.MemberType == MemberTypes.Method)
{
if (member.Name == "Il2CppType") continue;
// ignore these
if (member.Name.Contains("Il2CppType") || member.Name.StartsWith("get_") || member.Name.StartsWith("set_"))
continue;
var name = member.DeclaringType.Name + "." + member.Name;
if (names.Contains(name)) continue;
names.Add(name);
if (member is MethodInfo mi)
{
name += " (";
foreach (var param in mi.GetParameters())
{
name += param.ParameterType.Name + ", ";
}
name += ")";
}
if (names.Contains(name))
{
continue;
}
var cached = CacheObject.GetCacheObject(null, member, target);
list.Add(cached);
cached.ReflectionException = exception;
try
{
var cached = CacheObjectBase.GetCacheObject(null, member, target);
if (cached != null)
{
names.Add(name);
list.Add(cached);
cached.ReflectionException = exception;
}
}
catch (Exception e)
{
MelonLogger.LogWarning($"Exception caching member {name}!");
MelonLogger.Log(e.ToString());
}
}
}
}
m_cachedMembers = list.ToArray();
m_allCachedMembers = list.ToArray();
}
// =========== GUI DRAW =========== //
@ -155,40 +187,42 @@ namespace Explorer
{
try
{
Header();
// ====== HEADER ======
GUILayout.BeginArea(new Rect(5, 25, m_rect.width - 10, m_rect.height - 35), GUI.skin.box);
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>" + ObjectType.Name + "</color>", null);
bool unityObj = Target is UnityEngine.Object;
if (unityObj)
GUILayout.Label("<b>Type:</b> <color=cyan>" + ObjectType.FullName + "</color>", new GUILayoutOption[] { GUILayout.Width(245f) });
if (m_uObj)
{
GUILayout.Label("Name: " + (Target as UnityEngine.Object).name, null);
GUILayout.Label("Name: " + m_uObj.name, null);
}
GUILayout.EndHorizontal();
if (unityObj)
if (m_uObj)
{
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Tools:</b>", new GUILayoutOption[] { GUILayout.Width(80) });
UIHelpers.InstantiateButton((UnityEngine.Object)Target);
if (Target is Component comp && comp.gameObject is GameObject obj)
UIHelpers.InstantiateButton(m_uObj);
if (m_component && m_component.gameObject is GameObject obj)
{
GUI.skin.label.alignment = TextAnchor.MiddleRight;
GUILayout.Label("GameObject:", null);
if (GUILayout.Button("<color=#00FF00>" + obj.name + "</color>", new GUILayoutOption[] { GUILayout.MaxWidth(m_rect.width - 350) }))
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();
}
@ -196,21 +230,15 @@ namespace Explorer
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Search:</b>", new GUILayoutOption[] { GUILayout.Width(75) });
m_search = GUILayout.TextField(m_search, null);
GUILayout.Label("<b>Limit per page:</b>", new GUILayoutOption[] { GUILayout.Width(125) });
var limitString = m_limitPerPage.ToString();
limitString = GUILayout.TextField(limitString, new GUILayoutOption[] { GUILayout.Width(60) });
if (int.TryParse(limitString, out int i))
{
m_limitPerPage = i;
}
m_search = GUILayout.TextField(m_search, null);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Filter:</b>", new GUILayoutOption[] { GUILayout.Width(75) });
FilterToggle(MemberInfoType.All, "All");
FilterToggle(MemberInfoType.Property, "Properties");
FilterToggle(MemberInfoType.Field, "Fields");
FilterToggle(MemberTypes.All, "All");
FilterToggle(MemberTypes.Property, "Properties");
FilterToggle(MemberTypes.Field, "Fields");
FilterToggle(MemberTypes.Method, "Methods");
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
@ -228,100 +256,104 @@ namespace Explorer
GUILayout.Space(10);
int count = m_cachedMemberFiltered.Length;
// prev/next page buttons
GUILayout.BeginHorizontal(null);
GUILayout.Label("<b>Limit per page:</b>", new GUILayoutOption[] { GUILayout.Width(125) });
var limitString = m_limitPerPage.ToString();
limitString = GUILayout.TextField(limitString, new GUILayoutOption[] { GUILayout.Width(60) });
if (int.TryParse(limitString, out int lim))
{
m_limitPerPage = lim;
}
int count = m_cachedMembersFiltered.Length;
if (count > m_limitPerPage)
{
// prev/next page buttons
GUILayout.BeginHorizontal(null);
int maxOffset = (int)Mathf.Ceil((float)(count / (decimal)m_limitPerPage)) - 1;
if (GUILayout.Button("< Prev", null))
if (GUILayout.Button("< Prev", new GUILayoutOption[] { GUILayout.Width(80) }))
{
if (m_pageOffset > 0) m_pageOffset--;
scroll = Vector2.zero;
}
GUI.skin.label.alignment = TextAnchor.MiddleCenter;
GUILayout.Label($"Page {m_pageOffset + 1}/{maxOffset + 1}", new GUILayoutOption[] { GUILayout.Width(80) });
GUI.skin.label.alignment = TextAnchor.UpperLeft;
if (GUILayout.Button("Next >", null))
if (GUILayout.Button("Next >", new GUILayoutOption[] { GUILayout.Width(80) }))
{
if (m_pageOffset < maxOffset) m_pageOffset++;
scroll = Vector2.zero;
}
GUILayout.EndHorizontal();
}
GUILayout.EndHorizontal();
// ====== BODY ======
scroll = GUILayout.BeginScrollView(scroll, GUI.skin.scrollView);
GUILayout.Space(10);
DrawMembers(this.m_cachedMemberFiltered);
UIStyles.HorizontalLine(Color.grey);
GUILayout.BeginVertical(GUI.skin.box, null);
int index = 0;
var members = this.m_cachedMembersFiltered;
int offsetIndex = (m_pageOffset * m_limitPerPage) + index;
if (offsetIndex >= count)
{
int maxOffset = (int)Mathf.Ceil((float)(m_cachedMembersFiltered.Length / (decimal)m_limitPerPage)) - 1;
if (m_pageOffset > maxOffset)
{
m_pageOffset = 0;
}
}
for (int j = offsetIndex; (j < offsetIndex + m_limitPerPage && j < members.Length); j++)
{
var holder = members[j];
GUILayout.BeginHorizontal(new GUILayoutOption[] { GUILayout.Height(25) });
try
{
holder.Draw(rect, 180f);
}
catch { }
GUILayout.EndHorizontal();
index++;
}
GUILayout.EndVertical();
GUILayout.EndScrollView();
m_rect = WindowManager.ResizeWindow(m_rect, windowID);
if (!WindowManager.TabView)
{
m_rect = ResizeDrag.ResizeWindow(rect, windowID);
GUILayout.EndArea();
GUILayout.EndArea();
}
}
catch (Il2CppException e)
{
if (!e.Message.Contains("in a group with only"))
{
throw;
}
}
catch (Exception e)
{
MelonLogger.LogWarning("Exception on window draw. Message: " + e.Message);
MelonLogger.LogWarning("Exception drawing ReflectionWindow: " + e.GetType() + ", " + e.Message);
DestroyWindow();
return;
}
}
private void DrawMembers(CacheObject[] members)
{
// todo pre-cache list based on current search, otherwise this doesnt work.
int i = 0;
DrawMembersInternal("Properties", MemberInfoType.Property, members, ref i);
DrawMembersInternal("Fields", MemberInfoType.Field, members, ref i);
}
private void DrawMembersInternal(string title, MemberInfoType filter, CacheObject[] members, ref int index)
{
if (m_filter != filter && m_filter != MemberInfoType.All)
{
return;
}
UIStyles.HorizontalLine(Color.grey);
GUILayout.Label($"<size=18><b><color=gold>{title}</color></b></size>", null);
int offset = (m_pageOffset * m_limitPerPage) + index;
if (offset >= m_cachedMemberFiltered.Length)
{
m_pageOffset = 0;
offset = 0;
}
for (int j = offset; j < offset + m_limitPerPage && j < members.Length; j++)
{
var holder = members[j];
if (holder.MemberInfoType != filter || !ShouldProcessMember(holder)) continue;
GUILayout.BeginHorizontal(new GUILayoutOption[] { GUILayout.Height(25) });
try
{
holder.Draw(this.m_rect, 180f);
}
catch // (Exception e)
{
//MelonLogger.Log("Exception drawing member " + holder.MemberInfo.Name);
//MelonLogger.Log(e.GetType() + ", " + e.Message);
//MelonLogger.Log(e.StackTrace);
}
GUILayout.EndHorizontal();
index++;
if (index >= m_limitPerPage) break;
}
}
private void FilterToggle(MemberInfoType mode, string label)
private void FilterToggle(MemberTypes mode, string label)
{
if (m_filter == mode)
{
@ -334,6 +366,7 @@ namespace Explorer
if (GUILayout.Button(label, new GUILayoutOption[] { GUILayout.Width(100) }))
{
m_filter = mode;
m_pageOffset = 0;
}
GUI.color = Color.white;
}

106
src/Windows/ResizeDrag.cs Normal file
View File

@ -0,0 +1,106 @@
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();
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();
}
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);
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,116 @@
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

@ -7,14 +7,12 @@ using MelonLoader;
using UnhollowerBaseLib;
using UnhollowerRuntimeLib;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
namespace Explorer
{
public abstract class UIWindow
{
public abstract string Name { get; set; }
public abstract string Title { get; }
public object Target;
@ -23,6 +21,8 @@ namespace Explorer
public Vector2 scroll = Vector2.zero;
public virtual bool IsTabViewWindow => false;
public abstract void Init();
public abstract void WindowFunction(int windowID);
public abstract void Update();
@ -44,15 +44,7 @@ namespace Explorer
public void DestroyWindow()
{
try
{
WindowManager.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}");
}
WindowManager.DestroyWindow(this);
}
public void OnGUI()
@ -62,7 +54,7 @@ namespace Explorer
var origSkin = GUI.skin;
GUI.skin = UIStyles.WindowSkin;
m_rect = GUI.Window(windowID, m_rect, (GUI.WindowFunction)WindowFunction, Name);
m_rect = GUI.Window(windowID, m_rect, (GUI.WindowFunction)WindowFunction, Title);
GUI.skin = origSkin;
}
@ -70,12 +62,15 @@ namespace Explorer
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), "<color=red><b>X</b></color>"))
if (!WindowManager.TabView)
{
DestroyWindow();
return;
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

@ -16,28 +16,71 @@ namespace Explorer
{
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()
{
foreach (var window in Windows)
if (m_windowsToDestroy.Count > 0)
{
window.Update();
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()
{
foreach (var window in Windows)
if (TabView)
{
window.OnGUI();
if (Windows.Count > 0)
{
TabViewWindow.Instance.OnGUI();
}
}
else
{
foreach (var window in Windows)
{
window.OnGUI();
}
}
}
@ -102,12 +145,24 @@ namespace Explorer
{
createdNew = false;
UnityEngine.Object uObj = null;
if (obj is UnityEngine.Object)
{
uObj = obj as UnityEngine.Object;
}
foreach (var window in Windows)
{
if (obj == window.Target)
bool equals = ReferenceEquals(obj, window.Target);
if (!equals && uObj != null && window.Target is UnityEngine.Object uTarget)
{
GUI.BringWindowToFront(window.windowID);
GUI.FocusWindow(window.windowID);
equals = uObj.m_CachedPtr == uTarget.m_CachedPtr;
}
if (equals)
{
FocusWindow(window);
return window;
}
}
@ -123,10 +178,23 @@ namespace Explorer
}
}
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);
GUI.FocusWindow(new_window.windowID);
FocusWindow(new_window);
return new_window;
}
@ -134,56 +202,9 @@ namespace Explorer
public static UIWindow InspectReflection(object obj)
{
var new_window = UIWindow.CreateWindow<ReflectionWindow>(obj);
GUI.FocusWindow(new_window.windowID);
FocusWindow(new_window);
return new_window;
}
// ============= Resize Window Helper ============
// static readonly GUIContent gcDrag = new GUIContent("<->", "drag to resize");
private static readonly GUIContent gcDrag = new GUIContent("<->");
private static bool isResizing = false;
private static Rect m_currentResize;
private static int m_currentWindow;
public static Rect ResizeWindow(Rect _rect, int ID)
{
try
{
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();
}
catch { }
return _rect;
}
}
}