Compare commits

...

26 Commits
1.6.1 ... 1.6.9

Author SHA1 Message Date
8d648fec43 1.6.9
* Fix for games where patching Cursor methods fails.
* Added backup attempt for loading Cursor module if not present.
* HashSet collections are now supported by CacheList
* try/catch for loading Mod Config
2020-09-11 18:53:17 +10:00
835a81765e Add support for Hashset, add try/catch for loading settings 2020-09-11 00:17:13 +10:00
51ed936e30 Revert PR changes 2020-09-10 22:40:23 +10:00
ac16587cdc Merge pull request #11 from PsymoNBond/master
Fixed Rect init
2020-09-10 22:38:16 +10:00
1e1eaa6c38 Fixed Rect init 2020-09-10 14:16:25 +03:00
af17ae82c6 Update README.md 2020-09-10 20:47:13 +10:00
e23341c2b1 Update README.md 2020-09-10 20:36:24 +10:00
080bde4a63 Update README.md 2020-09-10 20:35:41 +10:00
1d739a1936 1.6.8
* Added a ModConfig, allowing you to define the main menu toggle keybind and the default window size (so far).
* Made the parsing of arguments more intelligent, should now behave as expected for null or empty arguments.
2020-09-10 20:31:09 +10:00
a927b5ed21 1.6.7
* Parameters (in Methods or Properties) with default values will now show these default values in the Inspector, and if you don't provide any input then this default value will be used as the argument.
* Removed an unnecessary update of cached members when you open a Reflection Inspector, should be a bit faster now.
* When entering arguments, the name of the argument is now white instead of cyan to avoid confusion with the Type name.
* A few clean ups
2020-09-10 18:02:41 +10:00
642c97812c fix img 2020-09-09 19:20:52 +10:00
e43d3579de Update img.png 2020-09-09 19:18:39 +10:00
6ea435deee 1.6.6
* Added better support for Properties with index parameters, can now support multiple parameters and non-int parameters.
* Parameters are now formatted in a more expected fashion (in the `(Type arg0, Type arg1)` format).
* Got rid of all the ugly yellow text.
* Cleaned up some minor GUI display / layout issues.
* Refactored some of CacheMethod into CacheObjectBase
2020-09-09 19:15:47 +10:00
94f749342d Update .gitignore 2020-09-09 00:53:31 +10:00
0769b7ef23 1.6.5
* Add expander to Unity struct inspectors, collapsed by default
* `UnityEngine.Color` labels in Reflection Inspector are now the same color as the value for convenience
* Cleaned up InputHelper
2020-09-08 23:47:17 +10:00
5086dcc82b Update README.md 2020-09-08 20:21:34 +10:00
56abd38e92 Add build instructions to Readme 2020-09-08 20:18:37 +10:00
a7e6ae87df Update README.md 2020-09-08 19:47:07 +10:00
b5c584bb02 Update README.md 2020-09-08 19:46:54 +10:00
c8a3aecdf4 Update README.md 2020-09-08 17:09:23 +10:00
33c2378f41 Update README.md 2020-09-08 17:09:12 +10:00
38aafa7e5b 1.6.4
* Fix for games which do not load InputModule on startup. CppExplorer will now try to load the module itself.
* Cleanups
2020-09-08 17:07:10 +10:00
4bb0811b2c 1.6.3
* Merged the two builds into one, there is now only one release. Using Reflection for UnityEngine.Input
* A few small fixes and cleanups
2020-09-08 06:21:45 +10:00
4aefe1c5a3 a few tidy ups 2020-09-08 04:33:27 +10:00
c228d29707 Update CppExplorer.cs 2020-09-07 20:28:43 +10:00
56d1507aff 1.6.2
* Fix for a crash that can occur when inspecting unsupported Dictionaries
* Added a scroll bar to the REPL console input area, fixes the issue of the code just being cut off when it goes too long.
2020-09-07 20:28:33 +10:00
41 changed files with 1338 additions and 726 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
*.user
*.userosscache
*.sln.docstates
*/mcs-unity*
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

View File

@ -16,16 +16,9 @@
### Known issues
* CppExplorer may experience a `MissingMethodException` when trying to use certain UnityEngine methods. If you experience this, please open an issue and I will do my best to fix it.
* Reflection may fail with certain types (eg. `Nullable<T>`, some Dictionary types, etc). Please see [Il2CppAssemblyUnhollower](https://github.com/knah/Il2CppAssemblyUnhollower#known-issues) for more details.
* Scrolling with mouse wheel in the CppExplorer menu may not work on all games at the moment.
## Features
* Scene hierarchy explorer
* Search loaded assets with filters
* Traverse and manipulate GameObjects
* Generic Reflection inspector
* C# REPL Console
* Inspect-under-mouse
## How to install
Requires [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) to be installed for your game.
@ -37,15 +30,35 @@ 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, most of it is pretty self-explanatory.
* Use the Scene Explorer or the Object Search to start Exploring, or the C# Console to test some code.
* See below for more specific details.
### Mod Config
There is a simple Mod Config for the CppExplorer, which is generated the first time you run it.
This config is generated to `Mods\CppExplorer\config.xml`. Edit the config while the game is closed if you wish to change it.
`Main_Menu_Toggle` (KeyCode)
* Sets the keybinding for the Main Menu toggle (show/hide all CppExplorer windows)
* See [this article](https://docs.unity3d.com/ScriptReference/KeyCode.html) for a full list of all accepted KeyCodes.
* Default: `F7`
`Default_Window_Size` (Vector2)
* Sets the default width and height for all CppExplorer windows when created.
* `x` is width, `y` is height.
* Default: `<x>550</x> <y>700</y>`
## Features
[![](overview.png)](overview.png)
<i>An overview of the different CppExplorer menus.</i>
### Scene Explorer
* 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.
[![](https://i.imgur.com/BzTOCvp.png)](https://i.imgur.com/BzTOCvp.png)
### Inspectors
CppExplorer has two main inspector modes: <b>GameObject Inspector</b>, and <b>Reflection Inspector</b>.
@ -59,30 +72,22 @@ CppExplorer has two main inspector modes: <b>GameObject Inspector</b>, and <b>Re
* 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/DiDvl0Q.png)](https://i.imgur.com/DiDvl0Q.png)
### Reflection Inspector
* 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/Pq127XD.png)](https://i.imgur.com/Pq127XD.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.
@ -97,10 +102,21 @@ CppExplorer can force the mouse to be visible and unlocked when the menu is open
* 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.
## Building
If you'd like to build this yourself, everything you need (other than MelonLoader) is included with this repository, there is no need for recursive cloning etc.
1. Install MelonLoader for your game.
2. Open the `src\CppExplorer.csproj` file in a text editor.
3. Scroll down until you see the `<ItemGroup>` containing the References.
4. Fix all of the paths in the `..\Steam\` directory for your game (use the full path if you need to).
5. Open the `src\CppExplorer.sln` project and build it.
6. The dll is built to the `Release\` folder in the root of the repository.
## Credits
Written by Sinai.
Thanks to:
* [ManlyMarco](https://github.com/ManlyMarco) for their [Runtime Unity Editor](https://github.com/ManlyMarco/RuntimeUnityEditor), which I used for the REPL Console and the "Find instances" snippet, and the UI style.
* [denikson](https://github.com/denikson) for [mcs-unity](https://github.com/denikson/mcs-unity). I commented out the `SkipVisibilityExt` constructor in `mcs.dll` since it was causing an exception with the Hook it attempted.
* [denikson](https://github.com/denikson) for [mcs-unity](https://github.com/denikson/mcs-unity). I commented out the `SkipVisibilityExt` constructor since it was causing an exception with the Hook it attempted.

BIN
overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

View File

@ -19,8 +19,12 @@ namespace Explorer
public MemberInfo MemInfo { get; set; }
public Type DeclaringType { get; set; }
public object DeclaringInstance { get; set; }
public int PropertyIndex { get; private set; }
private string m_propertyIndexInput = "0";
public bool HasParameters => m_arguments != null && m_arguments.Length > 0;
public bool m_evaluated = false;
public bool m_isEvaluating;
public ParameterInfo[] m_arguments = new ParameterInfo[0];
public string[] m_argumentInput = new string[0];
public string ReflectionException { get; set; }
@ -40,14 +44,10 @@ namespace Explorer
}
}
// ===== Abstract/Virtual Methods ===== //
public virtual void Init() { }
public abstract void DrawValue(Rect window, float width);
// ===== Static Methods ===== //
/// <summary>
/// Get CacheObject from only an object instance
/// Calls GetCacheObject(obj, memberInfo, declaringInstance) with (obj, null, null)</summary>
@ -114,12 +114,21 @@ namespace Explorer
{
CacheObjectBase holder;
var pi = memberInfo as PropertyInfo;
var mi = memberInfo as MethodInfo;
// if PropertyInfo, check if can process args
if (pi != null && !CanProcessArgs(pi.GetIndexParameters()))
{
return null;
}
// This is pretty ugly, could probably make a cleaner implementation.
// However, the only cleaner ways I can think of are slower and probably not worth it.
// Note: the order is somewhat important.
if (memberInfo is MethodInfo mi)
if (mi != null)
{
if (CacheMethod.CanEvaluate(mi))
{
@ -163,7 +172,7 @@ namespace Explorer
{
holder = new CacheDictionary();
}
else if (ReflectionHelpers.IsEnumerable(valueType) || ReflectionHelpers.IsCppList(valueType))
else if (ReflectionHelpers.IsEnumerable(valueType) || ReflectionHelpers.IsCppEnumerable(valueType))
{
holder = new CacheList();
}
@ -181,24 +190,113 @@ namespace Explorer
holder.MemInfo = memberInfo;
holder.DeclaringType = memberInfo.DeclaringType;
holder.DeclaringInstance = declaringInstance;
holder.UpdateValue();
}
if (pi != null)
{
holder.m_arguments = pi.GetIndexParameters();
}
else if (mi != null)
{
holder.m_arguments = mi.GetParameters();
}
holder.m_argumentInput = new string[holder.m_arguments.Length];
holder.UpdateValue();
holder.Init();
return holder;
}
// ======== Instance Methods =========
public static bool CanProcessArgs(ParameterInfo[] parameters)
{
foreach (var param in parameters)
{
if (!param.ParameterType.IsPrimitive && param.ParameterType != typeof(string))
{
return false;
}
}
return true;
}
public float CalcWhitespace(Rect window)
{
if (!(this is IExpandHeight)) return 0f;
float whitespace = (this as IExpandHeight).WhiteSpace;
if (whitespace > 0)
{
ClampLabelWidth(window, ref whitespace);
}
return whitespace;
}
public object[] ParseArguments()
{
var parsedArgs = new List<object>();
for (int i = 0; i < m_arguments.Length; i++)
{
var input = m_argumentInput[i];
var type = m_arguments[i].ParameterType;
// First, try parse the input and use that.
if (!string.IsNullOrEmpty(input))
{
// strings can obviously just be used directly
if (type == typeof(string))
{
parsedArgs.Add(input);
continue;
}
else
{
// try to invoke the parse method and use that.
try
{
parsedArgs.Add(type.GetMethod("Parse", new Type[] { typeof(string) })
.Invoke(null, new object[] { input }));
continue;
}
catch
{
MelonLogger.Log($"Argument #{i} '{m_arguments[i].Name}' ({type.Name}), could not parse input '{input}'.");
}
}
}
// Didn't use input, see if there is a default value.
if (m_arguments[i].HasDefaultValue)
{
parsedArgs.Add(m_arguments[i].DefaultValue);
continue;
}
// Try add a null arg I guess
parsedArgs.Add(null);
}
return parsedArgs.ToArray();
}
public virtual void UpdateValue()
{
if (MemInfo == null || !string.IsNullOrEmpty(ReflectionException))
if (MemInfo == null)
{
return;
}
if (HasParameters && !m_isEvaluating)
{
// Need to enter parameters first
return;
}
try
{
if (MemInfo.MemberType == MemberTypes.Field)
@ -209,13 +307,11 @@ namespace Explorer
else if (MemInfo.MemberType == MemberTypes.Property)
{
var pi = MemInfo as PropertyInfo;
bool isStatic = pi.GetAccessors()[0].IsStatic;
var target = isStatic ? null : DeclaringInstance;
var target = pi.GetAccessors()[0].IsStatic ? null : DeclaringInstance;
if (pi.GetIndexParameters().Length > 0)
if (HasParameters)
{
var indexes = new object[] { PropertyIndex };
Value = pi.GetValue(target, indexes);
Value = pi.GetValue(target, ParseArguments());
}
else
{
@ -224,6 +320,8 @@ namespace Explorer
}
ReflectionException = null;
m_evaluated = true;
m_isEvaluating = false;
}
catch (Exception e)
{
@ -244,10 +342,9 @@ namespace Explorer
{
var pi = MemInfo as PropertyInfo;
if (pi.GetIndexParameters().Length > 0)
if (HasParameters)
{
var indexes = new object[] { PropertyIndex };
pi.SetValue(pi.GetAccessors()[0].IsStatic ? null : DeclaringInstance, Value, indexes);
pi.SetValue(pi.GetAccessors()[0].IsStatic ? null : DeclaringInstance, Value, ParseArguments());
}
else
{
@ -261,9 +358,10 @@ namespace Explorer
}
}
// ========= Instance Gui Draw ==========
// ========= Gui Draw ==========
public const float MAX_LABEL_WIDTH = 400f;
public const string EVALUATE_LABEL = "<color=lime>Evaluate</color>";
public static void ClampLabelWidth(Rect window, ref float labelWidth)
{
@ -282,53 +380,108 @@ namespace Explorer
if (MemInfo != null)
{
var name = RichTextName;
if (MemInfo is PropertyInfo pi && pi.GetIndexParameters().Length > 0)
{
name += $"[{PropertyIndex}]";
}
GUILayout.Label(name, new GUILayoutOption[] { GUILayout.Width(labelWidth) });
GUILayout.Label(RichTextName, new GUILayoutOption[] { GUILayout.Width(labelWidth) });
}
else
{
GUILayout.Space(labelWidth);
}
var cm = this as CacheMethod;
if (HasParameters)
{
GUILayout.BeginVertical(null);
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;
var label = "<color=#2df7b2>" + type + "</color> <color=#a6e9e9>" + name + "</color>";
if (m_arguments[i].HasDefaultValue)
{
label = $"<i>[{label} = {m_arguments[i].DefaultValue}]</i>";
}
GUILayout.BeginHorizontal(null);
GUILayout.Label(i.ToString(), new GUILayoutOption[] { GUILayout.Width(20) });
m_argumentInput[i] = GUILayout.TextField(input, new GUILayoutOption[] { GUILayout.Width(150) });
GUILayout.Label(label, null);
GUILayout.EndHorizontal();
}
GUILayout.BeginHorizontal(null);
if (cm != null)
{
if (GUILayout.Button(EVALUATE_LABEL, new GUILayoutOption[] { GUILayout.Width(70) }))
{
cm.Evaluate();
}
}
else
{
if (GUILayout.Button(EVALUATE_LABEL, new GUILayoutOption[] { GUILayout.Width(70) }))
{
UpdateValue();
}
}
if (GUILayout.Button("Cancel", new GUILayoutOption[] { GUILayout.Width(70) }))
{
m_isEvaluating = false;
}
GUILayout.EndHorizontal();
}
else
{
if (GUILayout.Button($"Evaluate ({m_arguments.Length} params)", new GUILayoutOption[] { GUILayout.Width(150) }))
{
m_isEvaluating = true;
}
}
GUILayout.EndVertical();
// new line and space
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Space(labelWidth);
}
else if (cm != null)
{
//GUILayout.BeginHorizontal(null);
if (GUILayout.Button(EVALUATE_LABEL, new GUILayoutOption[] { GUILayout.Width(70) }))
{
cm.Evaluate();
}
// new line and space
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Space(labelWidth);
}
if (!string.IsNullOrEmpty(ReflectionException))
{
GUILayout.Label("<color=red>Reflection failed!</color> (" + ReflectionException + ")", null);
}
else if (Value == null && MemInfo?.MemberType != MemberTypes.Method)
else if ((HasParameters || this is CacheMethod) && !m_evaluated)
{
GUILayout.Label($"<color=grey><i>Not yet evaluated</i></color> (<color=#2df7b2>{ValueTypeName}</color>)", null);
}
else if (Value == null && !(this is CacheMethod))
{
GUILayout.Label("<i>null (" + ValueTypeName + ")</i>", null);
}
else
{
if (MemInfo is PropertyInfo pi && pi.GetIndexParameters().Length > 0)
{
GUILayout.Label("index:", new GUILayoutOption[] { GUILayout.Width(50) });
m_propertyIndexInput = GUILayout.TextField(m_propertyIndexInput, new GUILayoutOption[] { GUILayout.Width(100) });
if (GUILayout.Button("Set", new GUILayoutOption[] { GUILayout.Width(60) }))
{
if (int.TryParse(m_propertyIndexInput, out int i))
{
PropertyIndex = i;
UpdateValue();
}
else
{
MelonLogger.Log($"Could not parse '{m_propertyIndexInput}' to an int!");
}
}
// new line and space
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(null);
GUILayout.Space(labelWidth);
}
DrawValue(window, window.width - labelWidth - 90);
}
}
@ -348,15 +501,15 @@ namespace Explorer
m_richTextName = $"<color=#2df7b2>{MemInfo.DeclaringType.Name}</color>.<color={memberColor}>{MemInfo.Name}</color>";
if (MemInfo is MethodInfo mi)
if (m_arguments.Length > 0 || this is CacheMethod)
{
m_richTextName += "(";
var _params = "";
foreach (var param in mi.GetParameters())
foreach (var param in m_arguments)
{
if (_params != "") _params += ", ";
_params += $"<color=#a6e9e9>{param.Name}</color>";
_params += $"<color=#2df7b2>{param.ParameterType.Name}</color> <color=#a6e9e9>{param.Name}</color>";
}
m_richTextName += _params;
m_richTextName += ")";

View File

@ -9,8 +9,6 @@ namespace Explorer
interface IExpandHeight
{
bool IsExpanded { get; set; }
float WhiteSpace { get; set; }
float ButtonWidthOffset { get; set; }
float WhiteSpace { get; set; }
}
}

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using MelonLoader;
using UnityEngine;
using System.Reflection;
using UnhollowerBaseLib;
namespace Explorer
{
@ -14,7 +15,6 @@ namespace Explorer
{
public bool IsExpanded { get; set; }
public float WhiteSpace { get; set; } = 215f;
public float ButtonWidthOffset { get; set; } = 290f;
public PageHelper Pages = new PageHelper();
@ -55,10 +55,6 @@ namespace Explorer
{
// note: "ValueType" is the Dictionary itself, TypeOfValues is the 'Dictionary.Values' type.
// make generic dictionary from key and value type
var dict = (IDictionary)Activator.CreateInstance(typeof(Dictionary<,>)
.MakeGenericType(TypeOfKeys, TypeOfValues));
// get keys and values
var keys = ValueType.GetProperty("Keys") .GetValue(Value);
var values = ValueType.GetProperty("Values").GetValue(Value);
@ -67,27 +63,15 @@ namespace Explorer
var keyList = new List<object>();
var valueList = new List<object>();
// get keys enumerator and store keys
var keyEnumerator = keys.GetType().GetMethod("GetEnumerator").Invoke(keys, null);
var keyCollectionType = keyEnumerator.GetType();
var keyMoveNext = keyCollectionType.GetMethod("MoveNext");
var keyCurrent = keyCollectionType.GetProperty("Current");
while ((bool)keyMoveNext.Invoke(keyEnumerator, null))
{
keyList.Add(keyCurrent.GetValue(keyEnumerator));
}
// store entries with reflection
EnumerateWithReflection(keys, keyList);
EnumerateWithReflection(values, valueList);
// get values enumerator and store values
var valueEnumerator = values.GetType().GetMethod("GetEnumerator").Invoke(values, null);
var valueCollectionType = valueEnumerator.GetType();
var valueMoveNext = valueCollectionType.GetMethod("MoveNext");
var valueCurrent = valueCollectionType.GetProperty("Current");
while ((bool)valueMoveNext.Invoke(valueEnumerator, null))
{
valueList.Add(valueCurrent.GetValue(valueEnumerator));
}
// make actual mono dictionary
var dict = (IDictionary)Activator.CreateInstance(typeof(Dictionary<,>)
.MakeGenericType(TypeOfKeys, TypeOfValues));
// finally iterate into actual dictionary
// finally iterate into dictionary
for (int i = 0; i < keyList.Count; i++)
{
dict.Add(keyList[i], valueList[i]);
@ -96,6 +80,22 @@ namespace Explorer
return dict;
}
private void EnumerateWithReflection(object collection, List<object> list)
{
// invoke GetEnumerator
var enumerator = collection.GetType().GetMethod("GetEnumerator").Invoke(collection, null);
// get the type of it
var enumeratorType = enumerator.GetType();
// reflect MoveNext and Current
var moveNext = enumeratorType.GetMethod("MoveNext");
var current = enumeratorType.GetProperty("Current");
// iterate
while ((bool)moveNext.Invoke(enumerator, null))
{
list.Add(current.GetValue(enumerator));
}
}
private void GetGenericArguments()
{
if (this.MemInfo != null)
@ -125,11 +125,22 @@ namespace Explorer
m_keysType = type.GetGenericArguments()[0];
m_valuesType = type.GetGenericArguments()[1];
}
else
{
MelonLogger.Log("TODO? Dictionary is of type: " + Value.GetType().FullName);
}
}
}
public override void UpdateValue()
{
// first make sure we won't run into a TypeInitializationException.
if (!EnsureDictionaryIsSupported())
{
ReflectionException = "Dictionary Type not supported with Reflection!";
return;
}
base.UpdateValue();
// reset
@ -144,7 +155,6 @@ namespace Explorer
foreach (var key in IDict.Keys)
{
var cache = GetCacheObject(key, TypeOfKeys);
cache.UpdateValue();
keys.Add(cache);
}
@ -152,7 +162,6 @@ namespace Explorer
foreach (var val in IDict.Values)
{
var cache = GetCacheObject(val, TypeOfValues);
cache.UpdateValue();
values.Add(cache);
}
@ -160,6 +169,33 @@ namespace Explorer
m_cachedValues = values.ToArray();
}
private bool EnsureDictionaryIsSupported()
{
if (typeof(IDictionary).IsAssignableFrom(ValueType))
{
return true;
}
try
{
return Check(TypeOfKeys) && Check(TypeOfValues);
bool Check(Type type)
{
var ptr = (IntPtr)typeof(Il2CppClassPointerStore<>)
.MakeGenericType(type)
.GetField("NativeClassPtr")
.GetValue(null);
return Il2CppSystem.Type.internal_from_handle(IL2CPP.il2cpp_class_get_type(ptr)) is Il2CppSystem.Type;
}
}
catch
{
return false;
}
}
// ============= GUI Draw =============
public override void DrawValue(Rect window, float width)
@ -170,6 +206,8 @@ namespace Explorer
return;
}
var whitespace = CalcWhitespace(window);
int count = m_cachedKeys.Length;
if (!IsExpanded)
@ -187,9 +225,11 @@ namespace Explorer
}
}
var negativeWhitespace = window.width - (whitespace + 100f);
GUI.skin.button.alignment = TextAnchor.MiddleLeft;
string btnLabel = $"<color=yellow>[{count}] Dictionary<{TypeOfKeys.FullName}, {TypeOfValues.FullName}></color>";
if (GUILayout.Button(btnLabel, new GUILayoutOption[] { GUILayout.MaxWidth(window.width - ButtonWidthOffset) }))
string btnLabel = $"[{count}] <color=#2df7b2>Dictionary<{TypeOfKeys.FullName}, {TypeOfValues.FullName}></color>";
if (GUILayout.Button(btnLabel, new GUILayoutOption[] { GUILayout.Width(negativeWhitespace) }))
{
WindowManager.InspectObject(Value, out bool _);
}
@ -199,12 +239,6 @@ namespace Explorer
if (IsExpanded)
{
float whitespace = WhiteSpace;
if (whitespace > 0)
{
ClampLabelWidth(window, ref whitespace);
}
Pages.ItemCount = count;
if (count > Pages.ItemsPerPage)

View File

@ -12,7 +12,6 @@ namespace Explorer
{
public bool IsExpanded { get; set; }
public float WhiteSpace { get; set; } = 215f;
public float ButtonWidthOffset { get; set; } = 290f;
public PageHelper Pages = new PageHelper();
@ -41,7 +40,7 @@ namespace Explorer
private Type m_genericTypeDef;
// Cached ToArray method for Lists
public MethodInfo GenericToArrayMethod
public MethodInfo CppListToArrayMethod
{
get => GetGenericToArrayMethod();
}
@ -61,7 +60,7 @@ namespace Explorer
{
if (m_enumerable == null && Value != null)
{
m_enumerable = Value as IEnumerable ?? GetEnumerableFromIl2CppList();
m_enumerable = Value as IEnumerable ?? EnumerateWithReflection();
}
return m_enumerable;
}
@ -101,21 +100,45 @@ namespace Explorer
return m_itemProperty;
}
private IEnumerable GetEnumerableFromIl2CppList()
private IEnumerable EnumerateWithReflection()
{
if (Value == null) return null;
if (GenericTypeDef == typeof(Il2CppSystem.Collections.Generic.List<>))
{
return (IEnumerable)GenericToArrayMethod?.Invoke(Value, new object[0]);
return (IEnumerable)CppListToArrayMethod?.Invoke(Value, new object[0]);
}
else if (GenericTypeDef == typeof(Il2CppSystem.Collections.Generic.HashSet<>))
{
return CppHashSetToMono();
}
else
{
return ConvertIListToMono();
return CppIListToMono();
}
}
private IList ConvertIListToMono()
private IEnumerable CppHashSetToMono()
{
var set = new HashSet<object>();
// invoke GetEnumerator
var enumerator = Value.GetType().GetMethod("GetEnumerator").Invoke(Value, null);
// get the type of it
var enumeratorType = enumerator.GetType();
// reflect MoveNext and Current
var moveNext = enumeratorType.GetMethod("MoveNext");
var current = enumeratorType.GetProperty("Current");
// iterate
while ((bool)moveNext.Invoke(enumerator, null))
{
set.Add(current.GetValue(enumerator));
}
return set;
}
private IList CppIListToMono()
{
try
{
@ -219,7 +242,6 @@ namespace Explorer
if (GetCacheObject(obj, t) is CacheObjectBase cached)
{
cached.UpdateValue();
list.Add(cached);
}
else
@ -246,6 +268,8 @@ namespace Explorer
return;
}
var whitespace = CalcWhitespace(window);
int count = m_cachedEntries.Length;
if (!IsExpanded)
@ -263,9 +287,11 @@ namespace Explorer
}
}
var negativeWhitespace = window.width - (whitespace + 100f);
GUI.skin.button.alignment = TextAnchor.MiddleLeft;
string btnLabel = "<color=yellow>[" + count + "] " + EntryType.FullName + "</color>";
if (GUILayout.Button(btnLabel, new GUILayoutOption[] { GUILayout.MaxWidth(window.width - ButtonWidthOffset) }))
string btnLabel = $"[{count}] <color=#2df7b2>{EntryType.FullName}</color>";
if (GUILayout.Button(btnLabel, new GUILayoutOption[] { GUILayout.MaxWidth(negativeWhitespace) }))
{
WindowManager.InspectObject(Value, out bool _);
}
@ -275,12 +301,6 @@ namespace Explorer
if (IsExpanded)
{
float whitespace = WhiteSpace;
if (whitespace > 0)
{
ClampLabelWidth(window, ref whitespace);
}
Pages.ItemCount = count;
if (count > Pages.ItemsPerPage)

View File

@ -11,15 +11,8 @@ 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 => m_arguments != null && m_arguments.Length > 0;
public static bool CanEvaluate(MethodInfo mi)
{
// TODO generic args
@ -29,25 +22,7 @@ namespace Explorer
}
// 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 = MemInfo as MethodInfo;
m_arguments = mi.GetParameters();
m_argumentInput = new string[m_arguments.Length];
return CanProcessArgs(mi.GetParameters());
}
public override void UpdateValue()
@ -55,10 +30,11 @@ namespace Explorer
//base.UpdateValue();
}
private void Evaluate()
public void Evaluate()
{
var mi = MemInfo as MethodInfo;
m_isEvaluating = false;
var mi = MemInfo as MethodInfo;
object ret = null;
if (!HasParameters)
@ -68,42 +44,9 @@ namespace Explorer
}
else
{
var parsedArgs = 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))
{
parsedArgs.Add(input);
}
else
{
try
{
if (type.GetMethod("Parse", new Type[] { typeof(string) }).Invoke(null, new object[] { input }) is object parsed)
{
parsedArgs.Add(parsed);
}
else
{
// try add a null arg i guess
parsedArgs.Add(null);
}
}
catch
{
MelonLogger.Log($"Unable to parse '{input}' to type '{type.Name}'");
break;
}
}
}
try
{
ret = mi.Invoke(mi.IsStatic ? null : DeclaringInstance, parsedArgs.ToArray());
ret = mi.Invoke(mi.IsStatic ? null : DeclaringInstance, ParseArguments());
m_evaluated = true;
}
catch (Exception e)
@ -115,13 +58,6 @@ namespace Explorer
if (ret != null)
{
m_cachedReturnValue = GetCacheObject(ret);
if (m_cachedReturnValue is IExpandHeight expander)
{
expander.WhiteSpace = 0f;
expander.ButtonWidthOffset += 70f;
}
m_cachedReturnValue.UpdateValue();
}
else
@ -134,58 +70,6 @@ namespace Explorer
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)
@ -194,16 +78,13 @@ namespace Explorer
}
else
{
GUILayout.Label($"null (<color=yellow>{ValueTypeName}</color>)", null);
GUILayout.Label($"null (<color=#2df7b2>{ValueTypeName}</color>)", null);
}
}
else
{
GUILayout.Label($"<color=grey><i>Not yet evaluated</i></color> (<color=yellow>{ValueTypeName}</color>)", null);
GUILayout.Label($"<color=grey><i>Not yet evaluated</i></color> (<color=#2df7b2>{ValueTypeName}</color>)", null);
}
GUILayout.EndHorizontal();
GUILayout.EndVertical();
}
}
}

View File

@ -42,15 +42,20 @@ namespace Explorer
if (!label.Contains(ValueTypeName))
{
label += $" ({ValueTypeName})";
label += $" (<color=#2df7b2>{ValueTypeName}</color>)";
}
else
{
label = label.Replace(ValueTypeName, $"<color=#2df7b2>{ValueTypeName}</color>");
}
if (Value is UnityEngine.Object unityObj && !label.Contains(unityObj.name))
{
label = unityObj.name + " | " + label;
}
GUI.skin.button.alignment = TextAnchor.MiddleLeft;
if (GUILayout.Button("<color=yellow>" + label + "</color>", new GUILayoutOption[] { GUILayout.Width(width) }))
if (GUILayout.Button(label, new GUILayoutOption[] { GUILayout.Width(width - 15) }))
{
WindowManager.InspectObject(Value, out bool _);
}

View File

@ -7,13 +7,16 @@ using UnityEngine;
namespace Explorer
{
public class CacheColor : CacheObjectBase
public class CacheColor : CacheObjectBase, IExpandHeight
{
private string r = "0";
private string g = "0";
private string b = "0";
private string a = "0";
public bool IsExpanded { get; set; }
public float WhiteSpace { get; set; } = 215f;
public override void UpdateValue()
{
base.UpdateValue();
@ -28,12 +31,34 @@ namespace Explorer
public override void DrawValue(Rect window, float width)
{
GUILayout.Label($"<color=yellow>Color</color>: {((Color)Value).ToString()}", null);
if (CanWrite)
{
if (!IsExpanded)
{
if (GUILayout.Button("v", new GUILayoutOption[] { GUILayout.Width(25) }))
{
IsExpanded = true;
}
}
else
{
if (GUILayout.Button("^", new GUILayoutOption[] { GUILayout.Width(25) }))
{
IsExpanded = false;
}
}
}
//var c = (Color)Value;
//GUI.color = c;
GUILayout.Label($"<color=#2df7b2>Color:</color> {((Color)Value).ToString()}", null);
//GUI.color = Color.white;
if (CanWrite && IsExpanded)
{
GUILayout.EndHorizontal();
var whitespace = window.width - width - 90;
var whitespace = CalcWhitespace(window);
GUILayout.BeginHorizontal(null);
GUILayout.Space(whitespace);

View File

@ -51,7 +51,7 @@ namespace Explorer
}
}
GUILayout.Label(Value.ToString() + "<color=yellow><i> (" + ValueType + ")</i></color>", null);
GUILayout.Label(Value.ToString() + "<color=#2df7b2><i> (" + ValueType + ")</i></color>", null);
}
public void SetEnum(ref object value, int change)

View File

@ -69,10 +69,10 @@ namespace Explorer
else
{
// using ValueType.Name instead of ValueTypeName, because we only want the short name.
GUILayout.Label("<color=yellow><i>" + ValueType.Name + "</i></color>", new GUILayoutOption[] { GUILayout.Width(50) });
GUILayout.Label("<color=#2df7b2><i>" + ValueType.Name + "</i></color>", new GUILayoutOption[] { GUILayout.Width(50) });
int dynSize = 25 + (m_valueToString.Length * 15);
var maxwidth = window.width - 300f;
var maxwidth = window.width - 310f;
if (CanWrite) maxwidth -= 60;
if (dynSize > maxwidth)
@ -92,7 +92,7 @@ namespace Explorer
}
}
GUILayout.Space(5);
GUILayout.Space(10);
}
}

View File

@ -7,12 +7,15 @@ using UnityEngine;
namespace Explorer
{
public class CacheQuaternion : CacheObjectBase
public class CacheQuaternion : CacheObjectBase, IExpandHeight
{
private string x = "0";
private string y = "0";
private string z = "0";
public bool IsExpanded { get; set; }
public float WhiteSpace { get; set; } = 215f;
public override void UpdateValue()
{
base.UpdateValue();
@ -26,12 +29,31 @@ namespace Explorer
public override void DrawValue(Rect window, float width)
{
GUILayout.Label($"<color=yellow>Quaternion</color>: {((Quaternion)Value).eulerAngles.ToString()}", null);
if (CanWrite)
{
if (!IsExpanded)
{
if (GUILayout.Button("v", new GUILayoutOption[] { GUILayout.Width(25) }))
{
IsExpanded = true;
}
}
else
{
if (GUILayout.Button("^", new GUILayoutOption[] { GUILayout.Width(25) }))
{
IsExpanded = false;
}
}
}
GUILayout.Label($"<color=#2df7b2>Quaternion</color>: {((Quaternion)Value).eulerAngles.ToString()}", null);
if (CanWrite && IsExpanded)
{
GUILayout.EndHorizontal();
var whitespace = window.width - width - 90;
var whitespace = CalcWhitespace(window);
GUILayout.BeginHorizontal(null);
GUILayout.Space(whitespace);

View File

@ -7,13 +7,16 @@ using UnityEngine;
namespace Explorer
{
public class CacheRect : CacheObjectBase
public class CacheRect : CacheObjectBase, IExpandHeight
{
private string x = "0";
private string y = "0";
private string w = "0";
private string h = "0";
public bool IsExpanded { get; set; }
public float WhiteSpace { get; set; } = 215f;
public override void UpdateValue()
{
base.UpdateValue();
@ -28,12 +31,31 @@ namespace Explorer
public override void DrawValue(Rect window, float width)
{
GUILayout.Label($"<color=yellow>Rect</color>: {((Rect)Value).ToString()}", null);
if (CanWrite)
{
if (!IsExpanded)
{
if (GUILayout.Button("v", new GUILayoutOption[] { GUILayout.Width(25) }))
{
IsExpanded = true;
}
}
else
{
if (GUILayout.Button("^", new GUILayoutOption[] { GUILayout.Width(25) }))
{
IsExpanded = false;
}
}
}
GUILayout.Label($"<color=#2df7b2>Rect</color>: {((Rect)Value).ToString()}", null);
if (CanWrite && IsExpanded)
{
GUILayout.EndHorizontal();
var whitespace = window.width - width - 90;
var whitespace = CalcWhitespace(window);
GUILayout.BeginHorizontal(null);
GUILayout.Space(whitespace);

View File

@ -8,7 +8,7 @@ using UnityEngine;
namespace Explorer
{
public class CacheVector : CacheObjectBase
public class CacheVector : CacheObjectBase, IExpandHeight
{
public int VectorSize = 2;
@ -19,6 +19,9 @@ namespace Explorer
private MethodInfo m_toStringMethod;
public bool IsExpanded { get; set; }
public float WhiteSpace { get; set; } = 215f;
public override void Init()
{
if (Value is Vector2)
@ -63,12 +66,31 @@ namespace Explorer
public override void DrawValue(Rect window, float width)
{
GUILayout.Label($"<color=yellow>Vector{VectorSize}</color>: {(string)m_toStringMethod.Invoke(Value, new object[0])}", null);
if (CanWrite)
{
if (!IsExpanded)
{
if (GUILayout.Button("v", new GUILayoutOption[] { GUILayout.Width(25) }))
{
IsExpanded = true;
}
}
else
{
if (GUILayout.Button("^", new GUILayoutOption[] { GUILayout.Width(25) }))
{
IsExpanded = false;
}
}
}
GUILayout.Label($"<color=#2df7b2>Vector{VectorSize}</color>: {(string)m_toStringMethod.Invoke(Value, new object[0])}", null);
if (CanWrite && IsExpanded)
{
GUILayout.EndHorizontal();
var whitespace = window.width - width - 90;
var whitespace = CalcWhitespace(window);
// always draw x and y
GUILayout.BeginHorizontal(null);

69
src/Config/ModConfig.cs Normal file
View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using UnityEngine;
namespace Explorer
{
public class ModConfig
{
[XmlIgnore] public static readonly XmlSerializer Serializer = new XmlSerializer(typeof(ModConfig));
[XmlIgnore] private const string EXPLORER_FOLDER = @"Mods\CppExplorer";
[XmlIgnore] private const string SETTINGS_PATH = EXPLORER_FOLDER + @"\config.xml";
[XmlIgnore] public static ModConfig Instance;
public KeyCode Main_Menu_Toggle = KeyCode.F7;
public Vector2 Default_Window_Size = new Vector2(550, 700);
public static void OnLoad()
{
if (!Directory.Exists(EXPLORER_FOLDER))
{
Directory.CreateDirectory(EXPLORER_FOLDER);
}
if (LoadSettings()) return;
Instance = new ModConfig();
SaveSettings();
}
// returns true if settings successfully loaded
public static bool LoadSettings(bool checkExist = true)
{
if (checkExist && !File.Exists(SETTINGS_PATH))
return false;
try
{
using (var file = File.OpenRead(SETTINGS_PATH))
{
Instance = (ModConfig)Serializer.Deserialize(file);
}
}
catch
{
return false;
}
return Instance != null;
}
public static void SaveSettings(bool checkExist = true)
{
if (checkExist && File.Exists(SETTINGS_PATH))
File.Delete(SETTINGS_PATH);
using (var file = File.Create(SETTINGS_PATH))
{
Serializer.Serialize(file, Instance);
}
}
}
}

View File

@ -12,15 +12,10 @@ namespace Explorer
{
public class CppExplorer : MelonMod
{
public const string GUID = "com.sinai.cppexplorer";
public const string VERSION = "1.6.1";
public const string AUTHOR = "Sinai";
public const string NAME = "CppExplorer"
#if Release_Unity2018
+ " (Unity 2018)"
#endif
;
public const string NAME = "CppExplorer";
public const string VERSION = "1.6.9";
public const string AUTHOR = "Sinai";
public const string GUID = "com.sinai.cppexplorer";
public static CppExplorer Instance { get; private set; }
@ -29,43 +24,30 @@ namespace Explorer
get => m_showMenu;
set => SetShowMenu(value);
}
private static bool m_showMenu;
public static bool ForceUnlockMouse
{
get => m_forceUnlock;
set => SetForceUnlock(value);
}
private static bool m_forceUnlock;
private static CursorLockMode m_lastLockMode;
private static bool m_lastVisibleState;
private static bool m_currentlySettingCursor = false;
public static bool ShouldForceMouse => ShowMenu && ForceUnlockMouse;
public static bool m_showMenu;
private static void SetShowMenu(bool show)
{
m_showMenu = show;
UpdateCursorControl();
CursorControl.UpdateCursorControl();
}
// ========== MonoBehaviour methods ==========
public override void OnApplicationStart()
{
Instance = this;
// First, load config
ModConfig.OnLoad();
// Setup InputHelper class (UnityEngine.Input)
InputHelper.Init();
// Create CppExplorer modules
new MainMenu();
new WindowManager();
// 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);
// Init cursor control
CursorControl.Init();
MelonLogger.Log($"CppExplorer {VERSION} initialized.");
}
@ -79,22 +61,18 @@ namespace Explorer
public override void OnUpdate()
{
// Check main toggle key input
if (Input.GetKeyDown(KeyCode.F7))
if (InputHelper.GetKeyDown(ModConfig.Instance.Main_Menu_Toggle))
{
ShowMenu = !ShowMenu;
}
if (ShowMenu)
{
// Check Force-Unlock input
if (Input.GetKeyDown(KeyCode.LeftAlt))
{
ForceUnlockMouse = !ForceUnlockMouse;
}
CursorControl.Update();
InspectUnderMouse.Update();
MainMenu.Instance.Update();
WindowManager.Instance.Update();
InspectUnderMouse.Update();
}
}
@ -102,101 +80,14 @@ namespace Explorer
{
if (!ShowMenu) return;
var origSkin = GUI.skin;
GUI.skin = UIStyles.WindowSkin;
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_set_lockState
{
[HarmonyPrefix]
public static void Prefix(ref CursorLockMode value)
{
if (!m_currentlySettingCursor)
{
m_lastLockMode = value;
if (ShouldForceMouse)
{
value = CursorLockMode.None;
}
}
}
}
[HarmonyPatch(typeof(Cursor), nameof(Cursor.visible), MethodType.Setter)]
public class Cursor_set_visible
{
[HarmonyPrefix]
public static void Prefix(ref bool value)
{
if (!m_currentlySettingCursor)
{
m_lastVisibleState = value;
if (ShouldForceMouse)
{
value = true;
}
}
}
}
// Make it appear as though UnlockMouse is disabled to the rest of the application.
[HarmonyPatch(typeof(Cursor), nameof(Cursor.lockState), MethodType.Getter)]
public class Cursor_get_lockState
{
[HarmonyPostfix]
public static void Postfix(ref CursorLockMode __result)
{
if (ShouldForceMouse)
{
__result = m_lastLockMode;
}
}
}
[HarmonyPatch(typeof(Cursor), nameof(Cursor.visible), MethodType.Getter)]
public class Cursor_get_visible
{
[HarmonyPostfix]
public static void Postfix(ref bool __result)
{
if (ShouldForceMouse)
{
__result = m_lastVisibleState;
}
}
GUI.skin = origSkin;
}
}
}

View File

@ -12,36 +12,27 @@
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<AssemblyName>CppExplorer</AssemblyName>
<DebugSymbols>false</DebugSymbols>
<DebugType>none</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Release\2019\</OutputPath>
<DefineConstants>Release_2019</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_Unity2018|AnyCPU' ">
<AssemblyName>CppExplorer_Unity2018</AssemblyName>
<DebugSymbols>false</DebugSymbols>
<DebugType>none</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Release\2018\</OutputPath>
<DefineConstants>Release_Unity2018</DefineConstants>
<OutputPath>..\Release\</OutputPath>
<DefineConstants />
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<!-- Replace the '..\Steam\..` references with ones from your game (make sure to use the 'MelonLoader\' folder) -->
<Reference Include="Il2Cppmscorlib">
<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>True</Private>
@ -60,67 +51,33 @@
<HintPath>..\..\..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnhollowerBaseLib.dll</HintPath>
<Private>False</Private>
</Reference>
<!-- Unity 2019 build (InputLegacyModule.dll) -->
<Reference Include="UnityEngine" Condition="'$(Configuration)'=='Debug'">
<Reference Include="UnityEngine">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.CoreModule" Condition="'$(Configuration)'=='Debug'">
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.CoreModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.IMGUIModule" Condition="'$(Configuration)'=='Debug'">
<Reference Include="UnityEngine.IMGUIModule">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.IMGUIModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.InputModule" Condition="'$(Configuration)'=='Debug'">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.InputLegacyModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.PhysicsModule" Condition="'$(Configuration)'=='Debug'">
<Reference Include="UnityEngine.PhysicsModule">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.PhysicsModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.TextRenderingModule" Condition="'$(Configuration)'=='Debug'">
<Reference Include="UnityEngine.TextRenderingModule">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.TextRenderingModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.UI" Condition="'$(Configuration)'=='Debug'">
<Reference Include="UnityEngine.UI">
<HintPath>..\..\..\Steam\steamapps\common\Hellpoint\MelonLoader\Managed\UnityEngine.UI.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>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.CoreModule" Condition="'$(Configuration)'=='Release_Unity2018'">
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.CoreModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.IMGUIModule" Condition="'$(Configuration)'=='Release_Unity2018'">
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.IMGUIModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.InputModule" Condition="'$(Configuration)'=='Release_Unity2018'">
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.InputModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.PhysicsModule" Condition="'$(Configuration)'=='Release_Unity2018'">
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.PhysicsModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.TextRenderingModule" Condition="'$(Configuration)'=='Release_Unity2018'">
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.TextRenderingModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.UI" Condition="'$(Configuration)'=='Release_Unity2018'">
<HintPath>..\..\..\Steam\steamapps\common\VRChat\MelonLoader\Managed\UnityEngine.UI.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Helpers\IExpandHeight.cs" />
<Compile Include="CachedObjects\IExpandHeight.cs" />
<Compile Include="CachedObjects\Struct\CacheColor.cs" />
<Compile Include="CachedObjects\Object\CacheDictionary.cs" />
<Compile Include="CachedObjects\Struct\CacheEnum.cs" />
@ -132,8 +89,12 @@
<Compile Include="CachedObjects\Struct\CacheQuaternion.cs" />
<Compile Include="CachedObjects\Struct\CacheVector.cs" />
<Compile Include="CachedObjects\Struct\CacheRect.cs" />
<Compile Include="Config\ModConfig.cs" />
<Compile Include="CppExplorer.cs" />
<Compile Include="Extensions\ReflectionExtensions.cs" />
<Compile Include="Helpers\InputHelper.cs" />
<Compile Include="Menu\CursorControl.cs" />
<Compile Include="Tests\TestClass.cs" />
<Compile Include="UnstripFixes\GUIUnstrip.cs" />
<Compile Include="UnstripFixes\ScrollViewStateUnstrip.cs" />
<Compile Include="Extensions\UnityExtensions.cs" />
@ -141,24 +102,24 @@
<Compile Include="Helpers\ReflectionHelpers.cs" />
<Compile Include="Helpers\UIHelpers.cs" />
<Compile Include="Helpers\UnityHelpers.cs" />
<Compile Include="MainMenu\InspectUnderMouse.cs" />
<Compile Include="Menu\MainMenu\InspectUnderMouse.cs" />
<Compile Include="CachedObjects\CacheObjectBase.cs" />
<Compile Include="UnstripFixes\SliderHandlerUnstrip.cs" />
<Compile Include="UnstripFixes\UnstripExtensions.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" />
<Compile Include="MainMenu\Pages\Console\REPLHelper.cs" />
<Compile Include="MainMenu\Pages\WindowPage.cs" />
<Compile Include="Windows\WindowManager.cs" />
<Compile Include="MainMenu\MainMenu.cs" />
<Compile Include="Windows\GameObjectWindow.cs" />
<Compile Include="Windows\ReflectionWindow.cs" />
<Compile Include="MainMenu\Pages\ScenePage.cs" />
<Compile Include="MainMenu\Pages\SearchPage.cs" />
<Compile Include="Helpers\UIStyles.cs" />
<Compile Include="Menu\Windows\ResizeDrag.cs" />
<Compile Include="Menu\Windows\TabViewWindow.cs" />
<Compile Include="Menu\Windows\UIWindow.cs" />
<Compile Include="Menu\MainMenu\Pages\ConsolePage.cs" />
<Compile Include="Menu\MainMenu\Pages\Console\REPL.cs" />
<Compile Include="Menu\MainMenu\Pages\Console\REPLHelper.cs" />
<Compile Include="Menu\MainMenu\Pages\WindowPage.cs" />
<Compile Include="Menu\Windows\WindowManager.cs" />
<Compile Include="Menu\MainMenu\MainMenu.cs" />
<Compile Include="Menu\Windows\GameObjectWindow.cs" />
<Compile Include="Menu\Windows\ReflectionWindow.cs" />
<Compile Include="Menu\MainMenu\Pages\ScenePage.cs" />
<Compile Include="Menu\MainMenu\Pages\SearchPage.cs" />
<Compile Include="Menu\UIStyles.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@ -7,14 +7,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CppExplorer", "CppExplorer.
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release_Unity2018|Any CPU = Release_Unity2018|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Release_Unity2018|Any CPU.ActiveCfg = Release_Unity2018|Any CPU
{B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Release_Unity2018|Any CPU.Build.0 = Release_Unity2018|Any CPU
{B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{B21DBDE3-5D6F-4726-93AB-CC3CC68BAE7D}.Release|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -3,14 +3,38 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
namespace Explorer
{
public static class ReflectionExtensions
{
/// <summary>
/// Extension to allow for easy, non-generic Il2Cpp casting.
/// The extension is on System.Object, but only Il2Cpp objects would be a valid target.
/// </summary>
public static object Il2CppCast(this object obj, Type castTo)
{
return ReflectionHelpers.Il2CppCast(obj, castTo);
}
/// <summary>
/// Extension to safely try to get all Types from an Assembly, with a fallback for ReflectionTypeLoadException.
/// </summary>
public static IEnumerable<Type> TryGetTypes(this Assembly asm)
{
try
{
return asm.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
return e.Types.Where(t => t != null);
}
catch
{
return Enumerable.Empty<Type>();
}
}
}
}

104
src/Helpers/InputHelper.cs Normal file
View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using UnityEngine;
using MelonLoader;
namespace Explorer
{
/// <summary>
/// Version-agnostic UnityEngine Input module using Reflection.
/// </summary>
public static class InputHelper
{
// If Input module failed to load at all
public static bool NO_INPUT;
// Base UnityEngine.Input class
private static Type Input => _input ?? (_input = ReflectionHelpers.GetTypeByName("UnityEngine.Input"));
private static Type _input;
// Cached member infos
private static PropertyInfo _mousePosition;
private static MethodInfo _getKey;
private static MethodInfo _getKeyDown;
private static MethodInfo _getMouseButton;
private static MethodInfo _getMouseButtonDown;
public static void Init()
{
if (Input == null && !TryManuallyLoadInput())
{
NO_INPUT = true;
return;
}
// Cache reflection now that we know Input is loaded
_mousePosition = Input.GetProperty("mousePosition");
_getKey = Input.GetMethod("GetKey", new Type[] { typeof(KeyCode) });
_getKeyDown = Input.GetMethod("GetKeyDown", new Type[] { typeof(KeyCode) });
_getMouseButton = Input.GetMethod("GetMouseButton", new Type[] { typeof(int) });
_getMouseButtonDown = Input.GetMethod("GetMouseButtonDown", new Type[] { typeof(int) });
}
#pragma warning disable IDE1006 // Camel-case property (Unity style)
public static Vector3 mousePosition
{
get
{
if (NO_INPUT) return Vector3.zero;
return (Vector3)_mousePosition.GetValue(null);
}
}
#pragma warning restore IDE1006
public static bool GetKeyDown(KeyCode key)
{
if (NO_INPUT) return false;
return (bool)_getKeyDown.Invoke(null, new object[] { key });
}
public static bool GetKey(KeyCode key)
{
if (NO_INPUT) return false;
return (bool)_getKey.Invoke(null, new object[] { key });
}
/// <param name="btn">1 = left, 2 = middle, 3 = right, etc</param>
public static bool GetMouseButtonDown(int btn)
{
if (NO_INPUT) return false;
return (bool)_getMouseButtonDown.Invoke(null, new object[] { btn });
}
/// <param name="btn">1 = left, 2 = middle, 3 = right, etc</param>
public static bool GetMouseButton(int btn)
{
if (NO_INPUT) return false;
return (bool)_getMouseButton.Invoke(null, new object[] { btn });
}
private static bool TryManuallyLoadInput()
{
MelonLogger.Log("UnityEngine.Input is null, trying to load manually....");
if ((ReflectionHelpers.LoadModule("UnityEngine.InputLegacyModule.dll") || ReflectionHelpers.LoadModule("UnityEngine.CoreModule.dll"))
&& Input != null)
{
MelonLogger.Log("Ok!");
return true;
}
else
{
MelonLogger.Log("Could not load Input module!");
return false;
}
}
}
}

View File

@ -1,16 +1,15 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.IO;
using MelonLoader;
using UnhollowerBaseLib;
using UnhollowerRuntimeLib;
using UnityEngine;
using BF = System.Reflection.BindingFlags;
using MelonLoader;
using System.Collections;
using Mono.CSharp;
using ILType = Il2CppSystem.Type;
namespace Explorer
{
@ -18,11 +17,11 @@ namespace Explorer
{
public static BF CommonFlags = BF.Public | BF.Instance | BF.NonPublic | BF.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>();
public static ILType GameObjectType => Il2CppType.Of<GameObject>();
public static ILType TransformType => Il2CppType.Of<Transform>();
public static ILType ObjectType => Il2CppType.Of<UnityEngine.Object>();
public static ILType ComponentType => Il2CppType.Of<Component>();
public static ILType BehaviourType => Il2CppType.Of<Behaviour>();
private static readonly MethodInfo m_tryCastMethodInfo = typeof(Il2CppObjectBase).GetMethod("TryCast");
@ -35,6 +34,113 @@ namespace Explorer
.Invoke(obj, null);
}
public static bool IsEnumerable(Type t)
{
return typeof(IEnumerable).IsAssignableFrom(t);
}
// Checks for Il2Cpp List or HashSet.
public static bool IsCppEnumerable(Type t)
{
if (t.IsGenericType && t.GetGenericTypeDefinition() is Type g)
{
return typeof(Il2CppSystem.Collections.Generic.List<>).IsAssignableFrom(g)
|| typeof(Il2CppSystem.Collections.Generic.IList<>).IsAssignableFrom(g)
|| typeof(Il2CppSystem.Collections.Generic.HashSet<>).IsAssignableFrom(g);
}
else
{
return typeof(Il2CppSystem.Collections.IList).IsAssignableFrom(t);
}
}
public static bool IsDictionary(Type t)
{
if (typeof(IDictionary).IsAssignableFrom(t))
{
return true;
}
if (t.IsGenericType && t.GetGenericTypeDefinition() is Type g)
{
return typeof(Il2CppSystem.Collections.Generic.Dictionary<,>).IsAssignableFrom(g)
|| typeof(Il2CppSystem.Collections.Generic.IDictionary<,>).IsAssignableFrom(g);
}
else
{
return typeof(Il2CppSystem.Collections.IDictionary).IsAssignableFrom(t);
}
}
public static Type GetTypeByName(string fullName)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in asm.TryGetTypes())
{
if (type.FullName == fullName)
{
return type;
}
}
}
return null;
}
public static Type GetActualType(object obj)
{
if (obj == null) return null;
// Need to use GetIl2CppType for Il2CppSystem Objects
if (obj is Il2CppSystem.Object ilObject)
{
// Prevent weird behaviour when inspecting an Il2CppSystem.Type object.
if (ilObject is ILType)
{
return typeof(ILType);
}
// Get the System.Type using the qualified name, or fallback to GetType.
return Type.GetType(ilObject.GetIl2CppType().AssemblyQualifiedName) ?? obj.GetType();
}
// It's a normal object, this is fine
return obj.GetType();
}
public static Type[] GetAllBaseTypes(object obj)
{
var list = new List<Type>();
var type = GetActualType(obj);
while (type != null)
{
list.Add(type);
type = type.BaseType;
}
return list.ToArray();
}
public static bool LoadModule(string module)
{
var path = $@"MelonLoader\Managed\{module}";
if (!File.Exists(path)) return false;
try
{
Assembly.Load(File.ReadAllBytes(path));
return true;
}
catch (Exception e)
{
MelonLogger.Log(e.GetType() + ", " + e.Message);
return false;
}
}
public static string ExceptionToString(Exception e)
{
if (IsFailedGeneric(e))
@ -75,100 +181,5 @@ namespace Explorer
else
return false;
}
public static bool IsEnumerable(Type t)
{
return typeof(IEnumerable).IsAssignableFrom(t);
}
// Only Il2Cpp List needs this check. C# List is IEnumerable.
public static bool IsCppList(Type t)
{
if (t.IsGenericType && t.GetGenericTypeDefinition() is Type g)
{
return typeof(Il2CppSystem.Collections.Generic.List<>).IsAssignableFrom(g)
|| typeof(Il2CppSystem.Collections.Generic.IList<>).IsAssignableFrom(g);
}
else
{
return typeof(Il2CppSystem.Collections.IList).IsAssignableFrom(t);
}
}
public static bool IsDictionary(Type t)
{
if (typeof(IDictionary).IsAssignableFrom(t))
{
return true;
}
if (t.IsGenericType && t.GetGenericTypeDefinition() is Type g)
{
return typeof(Il2CppSystem.Collections.Generic.Dictionary<,>).IsAssignableFrom(g)
|| typeof(Il2CppSystem.Collections.Generic.IDictionary<,>).IsAssignableFrom(g);
}
else
{
return typeof(Il2CppSystem.Collections.IDictionary).IsAssignableFrom(t);
}
}
public static Type GetTypeByName(string typeName)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in GetTypesSafe(asm))
{
if (type.FullName == typeName)
{
return type;
}
}
}
return null;
}
public static Type GetActualType(object obj)
{
if (obj == null) return null;
if (obj is Il2CppSystem.Object ilObject)
{
var ilTypeName = ilObject.GetIl2CppType().AssemblyQualifiedName;
if (Type.GetType(ilTypeName) is Type t && !t.FullName.Contains("System.RuntimeType"))
{
return t;
}
return ilObject.GetType();
}
return obj.GetType();
}
public static IEnumerable<Type> GetTypesSafe(Assembly asm)
{
try { return asm.GetTypes(); }
catch (ReflectionTypeLoadException e) { return e.Types.Where(x => x != null); }
catch { return Enumerable.Empty<Type>(); }
}
public static Type[] GetAllBaseTypes(object obj)
{
var list = new List<Type>();
var type = GetActualType(obj);
list.Add(type);
while (type.BaseType != null)
{
type = type.BaseType;
list.Add(type);
}
return list.ToArray();
}
}
}

View File

@ -15,7 +15,7 @@ namespace Explorer
{
get
{
if (m_mainCamera == null)
if (!m_mainCamera)
{
m_mainCamera = Camera.main;
}

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

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

View File

@ -17,7 +17,7 @@ namespace Explorer
{
if (CppExplorer.ShowMenu)
{
if (Input.GetKey(KeyCode.LeftShift) && Input.GetMouseButtonDown(1))
if (InputHelper.GetKey(KeyCode.LeftShift) && InputHelper.GetMouseButtonDown(1))
{
EnableInspect = !EnableInspect;
}
@ -35,7 +35,10 @@ namespace Explorer
public static void InspectRaycast()
{
Ray ray = UnityHelpers.MainCamera.ScreenPointToRay(Input.mousePosition);
if (!UnityHelpers.MainCamera)
return;
var ray = UnityHelpers.MainCamera.ScreenPointToRay(InputHelper.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 1000f))
{
@ -43,7 +46,7 @@ namespace Explorer
m_objUnderMouseName = obj.transform.GetGameObjectPath();
if (Input.GetMouseButtonDown(0))
if (InputHelper.GetMouseButtonDown(0))
{
EnableInspect = false;
m_objUnderMouseName = "";
@ -63,7 +66,7 @@ namespace Explorer
{
if (m_objUnderMouseName != "")
{
var pos = Input.mousePosition;
var pos = InputHelper.mousePosition;
var rect = new Rect(
pos.x - (Screen.width / 2), // x
Screen.height - pos.y - 50, // y

View File

@ -27,8 +27,9 @@ namespace Explorer
}
}
public const int MainWindowID = 10;
public static Rect MainRect = new Rect(5, 5, 550, 700);
public const int MainWindowID = 5000;
public static Rect MainRect = new Rect(5,5, ModConfig.Instance.Default_Window_Size.x,ModConfig.Instance.Default_Window_Size.y);
private static readonly List<WindowPage> Pages = new List<WindowPage>();
private static int m_currentPage = 0;
@ -51,19 +52,14 @@ namespace Explorer
public void OnGUI()
{
var origSkin = GUI.skin;
GUI.skin = UIStyles.WindowSkin;
MainRect = GUI.Window(MainWindowID, MainRect, (GUI.WindowFunction)MainWindow, CppExplorer.NAME);
GUI.skin = origSkin;
}
private void MainWindow(int id)
{
GUI.DragWindow(new Rect(0, 0, MainRect.width - 90, 20));
if (GUI.Button(new Rect(MainRect.width - 90, 2, 80, 20), "Hide (F7)"))
if (GUI.Button(new Rect(MainRect.width - 90, 2, 80, 20), $"Hide ({ModConfig.Instance.Main_Menu_Toggle})"))
{
CppExplorer.ShowMenu = false;
return;
@ -107,9 +103,9 @@ namespace Explorer
GUI.color = Color.white;
InspectUnderMouse.EnableInspect = GUILayout.Toggle(InspectUnderMouse.EnableInspect, "Inspect Under Mouse (Shift + RMB)", null);
bool mouseState = CppExplorer.ForceUnlockMouse;
bool mouseState = CursorControl.ForceUnlockMouse;
bool setMouse = GUILayout.Toggle(mouseState, "Force Unlock Mouse (Left Alt)", null);
if (setMouse != mouseState) CppExplorer.ForceUnlockMouse = setMouse;
if (setMouse != mouseState) CursorControl.ForceUnlockMouse = setMouse;
WindowManager.TabView = GUILayout.Toggle(WindowManager.TabView, "Tab View", null);
GUILayout.EndHorizontal();

View File

@ -19,6 +19,8 @@ namespace Explorer
private ScriptEvaluator _evaluator;
private readonly StringBuilder _sb = new StringBuilder();
private Vector2 inputAreaScroll;
private string MethodInput = "";
private string UsingInput = "";
@ -124,7 +126,12 @@ MelonLogger.Log(""hello world"");";
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) });
inputAreaScroll = GUIUnstrip.BeginScrollView(inputAreaScroll, new GUILayoutOption[] { GUILayout.Height(250) });
MethodInput = GUILayout.TextArea(MethodInput, new GUILayoutOption[] { GUILayout.ExpandHeight(true) });
GUIUnstrip.EndScrollView();
if (GUILayout.Button("<color=cyan><b>Execute</b></color>", null))
{

View File

@ -192,7 +192,7 @@ namespace Explorer
}
}
// --------- GUI Draw Function --------- //
// --------- GUI Draw Function --------- //
public override void DrawWindow()
{
@ -215,11 +215,9 @@ namespace Explorer
GUILayout.EndVertical();
}
catch (Exception e)
catch
{
MelonLogger.Log("Exception drawing ScenePage! " + e.GetType() + ", " + e.Message);
MelonLogger.Log(e.StackTrace);
m_currentTransform = null;
// supress
}
}
@ -229,39 +227,7 @@ namespace Explorer
// 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 { }
SceneChangeButtons();
GUILayout.Label("<color=cyan>" + m_currentScene + "</color>", null); //new GUILayoutOption[] { GUILayout.Width(250) });
GUILayout.EndHorizontal();
@ -269,7 +235,9 @@ namespace Explorer
// ----- 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();
@ -279,6 +247,39 @@ namespace Explorer
GUILayout.Space(5);
}
private void SceneChangeButtons()
{
// Need to do 'ToList()' so the object isn't cleaned up by Il2Cpp GC.
var scenes = SceneManager.GetAllScenes().ToList();
if (scenes.Count > 1)
{
int changeWanted = 0;
if (GUILayout.Button("<", new GUILayoutOption[] { GUILayout.Width(30) }))
{
changeWanted = -1;
}
if (GUILayout.Button(">", new GUILayoutOption[] { GUILayout.Width(30) }))
{
changeWanted = 1;
}
if (changeWanted != 0)
{
int index = scenes.IndexOf(SceneManager.GetSceneByName(m_currentScene));
index += changeWanted;
if (index > scenes.Count - 1)
{
index = 0;
}
else if (index < 0)
{
index = scenes.Count - 1;
}
m_currentScene = scenes[index].name;
}
}
}
private void DrawPageButtons()
{
GUILayout.BeginHorizontal(null);

View File

@ -385,8 +385,8 @@ namespace Explorer
public static IEnumerable<object> GetInstanceClassScanner()
{
var query = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(ReflectionHelpers.GetTypesSafe)
.Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters);
.SelectMany(t => t.TryGetTypes())
.Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters);
var flags = BindingFlags.Public | BindingFlags.Static;
var flatFlags = flags | BindingFlags.FlattenHierarchy;
@ -428,7 +428,7 @@ namespace Explorer
{
var t = ReflectionHelpers.GetActualType(obj);
if (!FilterName(t.FullName) || ReflectionHelpers.IsEnumerable(t) || ReflectionHelpers.IsCppList(t))
if (!FilterName(t.FullName) || ReflectionHelpers.IsEnumerable(t) || ReflectionHelpers.IsCppEnumerable(t))
{
continue;
}

View File

@ -28,20 +28,30 @@ namespace Explorer
public MemberTypes m_filter = MemberTypes.Property;
private bool m_hideFailedReflection = false;
// some extra caching
// some extra cast-caching
private UnityEngine.Object m_uObj;
private Component m_component;
private static readonly HashSet<string> _typeAndMemberBlacklist = new HashSet<string>
{
// Causes a crash
"Type.DeclaringMethod",
};
private static readonly HashSet<string> _methodStartsWithBlacklist = new HashSet<string>
{
// Pointless (handled by Properties)
"get_",
"set_"
};
public override void Init()
{
var type = ReflectionHelpers.GetActualType(Target);
TargetType = ReflectionHelpers.GetActualType(Target);
TargetType = type;
var types = ReflectionHelpers.GetAllBaseTypes(Target);
CacheMembers(types);
CacheMembers(ReflectionHelpers.GetAllBaseTypes(Target));
// cache the extra cast-caching
if (Target is Il2CppSystem.Object ilObject)
{
var unityObj = ilObject.TryCast<UnityEngine.Object>();
@ -56,13 +66,6 @@ namespace Explorer
}
}
}
m_filter = MemberTypes.All;
m_autoUpdate = true;
Update();
m_autoUpdate = false;
m_filter = MemberTypes.Property;
}
public override void Update()
@ -99,28 +102,32 @@ namespace Explorer
private bool ShouldProcessMember(CacheObjectBase holder)
{
if (m_filter != MemberTypes.All && m_filter != holder.MemInfo?.MemberType) return false;
// check MemberTypes filter
if (m_filter != MemberTypes.All && m_filter != holder.MemInfo?.MemberType)
return false;
if (!string.IsNullOrEmpty(holder.ReflectionException) && m_hideFailedReflection) return false;
// hide failed reflection
if (!string.IsNullOrEmpty(holder.ReflectionException) && m_hideFailedReflection)
return false;
if (m_search == "" || holder.MemInfo == null) return true;
// see if we should do name search
if (m_search == "" || holder.MemInfo == null)
return true;
var name = holder.MemInfo.DeclaringType.Name + "." + holder.MemInfo.Name;
return name.ToLower().Contains(m_search.ToLower());
// ok do name search
return (holder.MemInfo.DeclaringType.Name + "." + holder.MemInfo.Name)
.ToLower()
.Contains(m_search.ToLower());
}
private void CacheMembers(Type[] types)
{
var list = new List<CacheObjectBase>();
var names = new List<string>();
var cachedSigs = new List<string>();
foreach (var declaringType in types)
{
MemberInfo[] infos;
string exception = null;
try
{
infos = declaringType.GetMembers(ReflectionHelpers.CommonFlags);
@ -132,6 +139,7 @@ namespace Explorer
}
object target = Target;
string exception = null;
if (target is Il2CppSystem.Object ilObject)
{
@ -147,46 +155,60 @@ namespace Explorer
foreach (var member in infos)
{
if (member.MemberType == MemberTypes.Field || member.MemberType == MemberTypes.Property || member.MemberType == MemberTypes.Method)
// make sure member type is Field, Method of Property (4 / 8 / 16)
int m = (int)member.MemberType;
if (m < 4 || m > 16)
continue;
// check blacklisted members
var name = member.DeclaringType.Name + "." + member.Name;
if (_typeAndMemberBlacklist.Any(it => it == name))
continue;
if (_methodStartsWithBlacklist.Any(it => member.Name.StartsWith(it)))
continue;
// compare signature to already cached members
var signature = $"{member.DeclaringType.Name}.{member.Name}";
if (member is MethodInfo mi)
{
var name = $"{member.DeclaringType.Name}.{member.Name}";
AppendParams(mi.GetParameters());
}
else if (member is PropertyInfo pi)
{
AppendParams(pi.GetIndexParameters());
}
// blacklist (should probably make a proper implementation)
if (name == "Type.DeclaringMethod" || member.Name.StartsWith("get_") || member.Name.StartsWith("set_")) //|| member.Name.Contains("Il2CppType")
void AppendParams(ParameterInfo[] _args)
{
signature += " (";
foreach (var param in _args)
{
continue;
signature += $"{param.ParameterType.Name} {param.Name}, ";
}
signature += ")";
}
if (member is MethodInfo mi)
{
name += " (";
foreach (var param in mi.GetParameters())
{
name += param.ParameterType.Name + ", ";
}
name += ")";
}
if (names.Contains(name))
{
continue;
}
if (cachedSigs.Contains(signature))
{
continue;
}
try
try
{
var cached = CacheObjectBase.GetCacheObject(member, target);
if (cached != null)
{
var cached = CacheObjectBase.GetCacheObject(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());
cachedSigs.Add(signature);
list.Add(cached);
cached.ReflectionException = exception;
}
}
catch (Exception e)
{
MelonLogger.LogWarning($"Exception caching member {signature}!");
MelonLogger.Log(e.ToString());
}
}
}

View File

@ -34,15 +34,17 @@ namespace Explorer
//var r = GUILayoutUtility.GetLastRect();
var r = GUIUnstrip.GetLastRect();
Vector2 mouse = GUIUtility.ScreenToGUIPoint(new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y));
var mousePos = InputHelper.mousePosition;
if (r.Contains(mouse) && Input.GetMouseButtonDown(0))
Vector2 mouse = GUIUtility.ScreenToGUIPoint(new Vector2(mousePos.x, Screen.height - mousePos.y));
if (r.Contains(mouse) && InputHelper.GetMouseButtonDown(0))
{
isResizing = true;
m_currentWindow = ID;
m_currentResize = new Rect(mouse.x, mouse.y, _rect.width, _rect.height);
}
else if (!Input.GetMouseButton(0))
else if (!InputHelper.GetMouseButton(0))
{
isResizing = false;
}

View File

@ -17,7 +17,7 @@ namespace Explorer
public object Target;
public int windowID;
public Rect m_rect = new Rect(0, 0, 550, 700);
public Rect m_rect = new Rect(0,0, ModConfig.Instance.Default_Window_Size.x,ModConfig.Instance.Default_Window_Size.y);
public Vector2 scroll = Vector2.zero;
@ -49,15 +49,7 @@ namespace Explorer
public void OnGUI()
{
if (CppExplorer.ShowMenu)
{
var origSkin = GUI.skin;
GUI.skin = UIStyles.WindowSkin;
m_rect = GUI.Window(windowID, m_rect, (GUI.WindowFunction)WindowFunction, Title);
GUI.skin = origSkin;
}
m_rect = GUI.Window(windowID, m_rect, (GUI.WindowFunction)WindowFunction, Title);
}
public void Header()

View File

@ -90,7 +90,7 @@ namespace Explorer
{
createdNew = false;
if (Input.GetKey(KeyCode.LeftShift))
if (InputHelper.GetKey(KeyCode.LeftShift))
{
forceReflection = true;
}
@ -192,7 +192,8 @@ namespace Explorer
private static bool RectContainsMouse(Rect rect)
{
return rect.Contains(new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y));
var mousePos = InputHelper.mousePosition;
return rect.Contains(new Vector2(mousePos.x, Screen.height - mousePos.y));
}
public static int NextWindowID()

100
src/Tests/TestClass.cs Normal file
View File

@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MelonLoader;
using UnityEngine;
namespace Explorer.Tests
{
public class TestClass
{
public static TestClass Instance => m_instance ?? (m_instance = new TestClass());
private static TestClass m_instance;
public TestClass()
{
ILHashSetTest = new Il2CppSystem.Collections.Generic.HashSet<string>();
ILHashSetTest.Add("1");
ILHashSetTest.Add("2");
ILHashSetTest.Add("3");
}
// test HashSets
public static HashSet<string> HashSetTest = new HashSet<string>
{
"One",
"Two",
"Three"
};
public static Il2CppSystem.Collections.Generic.HashSet<string> ILHashSetTest;
// Test indexed parameter
public string this[int arg0, string arg1]
{
get
{
return $"arg0: {arg0}, arg1: {arg1}";
}
}
// Test basic list
public static List<string> TestList = new List<string>
{
"1",
"2",
"3",
"etc..."
};
// Test a nested dictionary
public static Dictionary<int, Dictionary<string, int>> NestedDictionary = new Dictionary<int, Dictionary<string, int>>
{
{
1,
new Dictionary<string, int>
{
{
"Sub 1", 123
},
{
"Sub 2", 456
},
}
},
{
2,
new Dictionary<string, int>
{
{
"Sub 3", 789
},
{
"Sub 4", 000
},
}
},
};
// Test a basic method
public static Color TestMethod(float r, float g, float b, float a)
{
return new Color(r, g, b, a);
}
// A method with default arguments
public static Vector3 TestDefaultArgs(float arg0, float arg1, float arg2 = 5.0f)
{
return new Vector3(arg0, arg1, arg2);
}
}
}

View File

@ -41,8 +41,7 @@ namespace Explorer
}
}
private static PropertyInfo m_scrollViewStatesInfo;
// ======= public methods ======= //
public static Rect GetLastRect()
{
@ -73,7 +72,6 @@ namespace Explorer
catch
{
ScrollFailed = true;
return scroll;
}
}
@ -86,10 +84,8 @@ namespace Explorer
}
catch (Exception e)
{
MelonLogger.Log("Exception on GUIUnstrip.BeginScrollView_ImplLayout: " + e.GetType() + ", " + e.Message + "\r\n" + e.StackTrace);
MelonLogger.Log("Exception on manual BeginScrollView: " + e.GetType() + ", " + e.Message + "\r\n" + e.StackTrace);
ManualUnstripFailed = true;
return scroll;
}
}
@ -113,8 +109,6 @@ namespace Explorer
}
}
// ======= private methods ======= //
private static Vector2 BeginScrollView_ImplLayout(Vector2 scrollPosition, bool alwaysShowHorizontal, bool alwaysShowVertical,
GUIStyle horizontalScrollbar, GUIStyle verticalScrollbar, GUIStyle background, params GUILayoutOption[] options)
{