mirror of
https://github.com/GrahamKracker/UnityExplorer.git
synced 2025-07-04 20:42:22 +08:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
ec8c88ab9b | |||
0eae78eeb2 | |||
a07ead2142 | |||
b93567c7ea | |||
957d80c7ec | |||
d530d10798 | |||
66daabf770 | |||
9fe998aa22 | |||
28b6db80f9 | |||
22effbb399 | |||
50f0c31e98 | |||
911522457e | |||
d5d1841109 | |||
239534e09c | |||
bc5d16051f | |||
f81822f219 | |||
f159cf5ea7 | |||
1e6bacb32b | |||
a80cef4c1d | |||
450bb77f2e | |||
0479102db6 | |||
93181f02be | |||
371054d6df | |||
427f23b80a | |||
a0d5ab8792 | |||
297034e38b |
21
README.md
21
README.md
@ -23,7 +23,6 @@
|
|||||||
| BIE 5.X | ✖️ n/a | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx5.Mono.zip) |
|
| BIE 5.X | ✖️ n/a | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx5.Mono.zip) |
|
||||||
|
|
||||||
1. Take the `UnityExplorer.BIE.[version].dll` file and put it in `BepInEx\plugins\`
|
1. Take the `UnityExplorer.BIE.[version].dll` file and put it in `BepInEx\plugins\`
|
||||||
2. In IL2CPP, you will need to download the [Unity libs](https://github.com/LavaGang/Unity-Runtime-Libraries) for the game's Unity version, create a folder `BepInEx\unity-libs\`, then extract the Unity libs into this folder.
|
|
||||||
|
|
||||||
<i>Note: BepInEx 6 is obtainable via [BepisBuilds](https://builds.bepis.io/projects/bepinex_be)</i>
|
<i>Note: BepInEx 6 is obtainable via [BepisBuilds](https://builds.bepis.io/projects/bepinex_be)</i>
|
||||||
|
|
||||||
@ -48,6 +47,24 @@ The standalone release can be used with any injector or loader of your choice, b
|
|||||||
3. Create an instance of Unity Explorer with `UnityExplorer.ExplorerStandalone.CreateInstance();`
|
3. Create an instance of Unity Explorer with `UnityExplorer.ExplorerStandalone.CreateInstance();`
|
||||||
4. Optionally subscribe to the `ExplorerStandalone.OnLog` event to handle logging if you wish
|
4. Optionally subscribe to the `ExplorerStandalone.OnLog` event to handle logging if you wish
|
||||||
|
|
||||||
|
# Common issues
|
||||||
|
|
||||||
|
These are some common fixes to issues which are present in some games, please create an issue in this repository if these fixes don't work.
|
||||||
|
|
||||||
|
### Input not working properly
|
||||||
|
|
||||||
|
This can be caused by a number of issues, but most commonly by the Event System.
|
||||||
|
|
||||||
|
1. Open the UnityExplorer config file. See the "Settings" section below if you're unsure where it is.
|
||||||
|
2. For the "Disable EventSystem Override" option, set the value to `true`
|
||||||
|
3. Restart the game.
|
||||||
|
|
||||||
|
### UI not appearing or gets destroyed during startup
|
||||||
|
|
||||||
|
1. Open the UnityExplorer config file. See the "Settings" section below if you're unsure where it is.
|
||||||
|
2. For the "Startup Delay" option, set the value to something higher, at least as long as it takes to reach the main menu of the game.
|
||||||
|
3. Restart the game.
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -88,7 +105,7 @@ The inspector is used to see detailed information on objects of any type and man
|
|||||||
|
|
||||||
* The Hooks panel allows you to hook methods at the click of a button for debugging purposes.
|
* The Hooks panel allows you to hook methods at the click of a button for debugging purposes.
|
||||||
* Simply enter any class (generic types not yet supported) and hook the methods you want from the menu.
|
* Simply enter any class (generic types not yet supported) and hook the methods you want from the menu.
|
||||||
* To quickly copy the hook into your own project for further development, use the "Log Hook Source" button.
|
* You can edit the source code of the generated hook with the "Edit Hook Source" button. Accepted method names are `Prefix` (which can return `bool` or `void`), `Postfix`, `Finalizer` (which can return `Exception` or `void`), and `Transpiler` (which must return `IEnumerable<HarmonyLib.CodeInstruction>`). You can define multiple patches if you wish.
|
||||||
|
|
||||||
### Mouse-Inspect
|
### Mouse-Inspect
|
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/unhollowed/UnityEngine.AssetBundleModule.dll
Normal file
BIN
lib/unhollowed/UnityEngine.AssetBundleModule.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -104,7 +104,6 @@ namespace UnityExplorer.CSConsole
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#region UI Listeners and options
|
#region UI Listeners and options
|
||||||
|
|
||||||
// TODO save
|
// TODO save
|
||||||
@ -659,7 +658,7 @@ var x = 5;
|
|||||||
// You can soft-overwrite a class by compiling it again with the same name. The old class will still technically exist in memory.
|
// You can soft-overwrite a class by compiling it again with the same name. The old class will still technically exist in memory.
|
||||||
|
|
||||||
// Compiled classes can be accessed from both inside and outside this console.
|
// Compiled classes can be accessed from both inside and outside this console.
|
||||||
// Note: in IL2CPP, injecting these classes with ClassInjector may crash the game!
|
// Note: in IL2CPP, you must declare a Namespace to inject these classes with ClassInjector or it will crash the game.
|
||||||
|
|
||||||
public class HelloWorld
|
public class HelloWorld
|
||||||
{
|
{
|
||||||
|
@ -80,7 +80,7 @@ namespace UnityExplorer.CacheObject.IValues
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = IOUtility.EnsureValidDirectory(SaveFilePath.Text);
|
var path = IOUtility.EnsureValidFilePath(SaveFilePath.Text);
|
||||||
|
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
File.Delete(path);
|
File.Delete(path);
|
||||||
|
@ -113,9 +113,6 @@ namespace UnityExplorer.Core.Input
|
|||||||
|
|
||||||
public static void SetEventSystem()
|
public static void SetEventSystem()
|
||||||
{
|
{
|
||||||
if (InputManager.CurrentType == InputType.InputSystem)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (EventSystem.current && EventSystem.current != UIManager.EventSys)
|
if (EventSystem.current && EventSystem.current != UIManager.EventSys)
|
||||||
{
|
{
|
||||||
lastEventSystem = EventSystem.current;
|
lastEventSystem = EventSystem.current;
|
||||||
@ -132,14 +129,12 @@ namespace UnityExplorer.Core.Input
|
|||||||
|
|
||||||
public static void ReleaseEventSystem()
|
public static void ReleaseEventSystem()
|
||||||
{
|
{
|
||||||
if (InputManager.CurrentType == InputType.InputSystem)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (lastEventSystem && lastEventSystem.gameObject.activeSelf)
|
if (lastEventSystem && lastEventSystem.gameObject.activeSelf)
|
||||||
{
|
{
|
||||||
lastEventSystem.enabled = true;
|
lastEventSystem.enabled = true;
|
||||||
|
|
||||||
settingEventSystem = true;
|
settingEventSystem = true;
|
||||||
|
UIManager.EventSys.enabled = false;
|
||||||
EventSystem.current = lastEventSystem;
|
EventSystem.current = lastEventSystem;
|
||||||
lastInputModule?.ActivateModule();
|
lastInputModule?.ActivateModule();
|
||||||
settingEventSystem = false;
|
settingEventSystem = false;
|
||||||
|
@ -14,7 +14,7 @@ namespace UnityExplorer.Core.Input
|
|||||||
bool GetMouseButtonDown(int btn);
|
bool GetMouseButtonDown(int btn);
|
||||||
bool GetMouseButton(int btn);
|
bool GetMouseButton(int btn);
|
||||||
|
|
||||||
BaseInputModule UIModule { get; }
|
BaseInputModule UIInputModule { get; }
|
||||||
|
|
||||||
void AddUIInputModule();
|
void AddUIInputModule();
|
||||||
void ActivateModule();
|
void ActivateModule();
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
|
using UnityExplorer.UI;
|
||||||
|
|
||||||
namespace UnityExplorer.Core.Input
|
namespace UnityExplorer.Core.Input
|
||||||
{
|
{
|
||||||
@ -16,39 +17,43 @@ namespace UnityExplorer.Core.Input
|
|||||||
{
|
{
|
||||||
public static InputType CurrentType { get; private set; }
|
public static InputType CurrentType { get; private set; }
|
||||||
|
|
||||||
private static IHandleInput m_inputModule;
|
private static IHandleInput m_inputHandler;
|
||||||
|
|
||||||
public static Vector3 MousePosition => m_inputModule.MousePosition;
|
public static Vector3 MousePosition => m_inputHandler.MousePosition;
|
||||||
|
|
||||||
public static bool GetKeyDown(KeyCode key)
|
public static bool GetKeyDown(KeyCode key)
|
||||||
{
|
{
|
||||||
if (key == KeyCode.None)
|
if (key == KeyCode.None)
|
||||||
return false;
|
return false;
|
||||||
return m_inputModule.GetKeyDown(key);
|
return m_inputHandler.GetKeyDown(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool GetKey(KeyCode key)
|
public static bool GetKey(KeyCode key)
|
||||||
{
|
{
|
||||||
if (key == KeyCode.None)
|
if (key == KeyCode.None)
|
||||||
return false;
|
return false;
|
||||||
return m_inputModule.GetKey(key);
|
return m_inputHandler.GetKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool GetMouseButtonDown(int btn) => m_inputModule.GetMouseButtonDown(btn);
|
public static bool GetMouseButtonDown(int btn) => m_inputHandler.GetMouseButtonDown(btn);
|
||||||
public static bool GetMouseButton(int btn) => m_inputModule.GetMouseButton(btn);
|
public static bool GetMouseButton(int btn) => m_inputHandler.GetMouseButton(btn);
|
||||||
|
|
||||||
public static BaseInputModule UIInput => m_inputModule.UIModule;
|
public static BaseInputModule UIInput => m_inputHandler.UIInputModule;
|
||||||
|
|
||||||
public static Vector2 MouseScrollDelta => m_inputModule.MouseScrollDelta;
|
public static Vector2 MouseScrollDelta => m_inputHandler.MouseScrollDelta;
|
||||||
|
|
||||||
public static void ActivateUIModule() => m_inputModule.ActivateModule();
|
|
||||||
|
|
||||||
public static void AddUIModule()
|
public static void AddUIModule()
|
||||||
{
|
{
|
||||||
m_inputModule.AddUIInputModule();
|
m_inputHandler.AddUIInputModule();
|
||||||
ActivateUIModule();
|
ActivateUIModule();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ActivateUIModule()
|
||||||
|
{
|
||||||
|
UIManager.EventSys.m_CurrentInputModule = UIInput;
|
||||||
|
m_inputHandler.ActivateModule();
|
||||||
|
}
|
||||||
|
|
||||||
public static void Init()
|
public static void Init()
|
||||||
{
|
{
|
||||||
InitHandler();
|
InitHandler();
|
||||||
@ -65,7 +70,7 @@ namespace UnityExplorer.Core.Input
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
m_inputModule = new LegacyInput();
|
m_inputHandler = new LegacyInput();
|
||||||
CurrentType = InputType.Legacy;
|
CurrentType = InputType.Legacy;
|
||||||
|
|
||||||
// make sure its working
|
// make sure its working
|
||||||
@ -84,7 +89,7 @@ namespace UnityExplorer.Core.Input
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
m_inputModule = new InputSystem();
|
m_inputHandler = new InputSystem();
|
||||||
CurrentType = InputType.InputSystem;
|
CurrentType = InputType.InputSystem;
|
||||||
ExplorerCore.Log("Initialized new InputSystem support.");
|
ExplorerCore.Log("Initialized new InputSystem support.");
|
||||||
return;
|
return;
|
||||||
@ -96,7 +101,7 @@ namespace UnityExplorer.Core.Input
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExplorerCore.LogWarning("Could not find any Input Module Type!");
|
ExplorerCore.LogWarning("Could not find any Input Module Type!");
|
||||||
m_inputModule = new NoInput();
|
m_inputHandler = new NoInput();
|
||||||
CurrentType = InputType.None;
|
CurrentType = InputType.None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ namespace UnityExplorer.Core.Input
|
|||||||
?? (m_tUIInputModule = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.UI.InputSystemUIInputModule"));
|
?? (m_tUIInputModule = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.UI.InputSystemUIInputModule"));
|
||||||
internal Type m_tUIInputModule;
|
internal Type m_tUIInputModule;
|
||||||
|
|
||||||
public BaseInputModule UIModule => m_newInputModule;
|
public BaseInputModule UIInputModule => m_newInputModule;
|
||||||
internal BaseInputModule m_newInputModule;
|
internal BaseInputModule m_newInputModule;
|
||||||
|
|
||||||
public void AddUIInputModule()
|
public void AddUIInputModule()
|
||||||
@ -239,6 +239,9 @@ namespace UnityExplorer.Core.Input
|
|||||||
|
|
||||||
private void CreateAction(object map, string actionName, string[] bindings, string propertyName)
|
private void CreateAction(object map, string actionName, string[] bindings, string propertyName)
|
||||||
{
|
{
|
||||||
|
var disable = map.GetType().GetMethod("Disable");
|
||||||
|
disable.Invoke(map, ArgumentUtility.EmptyArgs);
|
||||||
|
|
||||||
var inputActionType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputAction");
|
var inputActionType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputAction");
|
||||||
var addAction = inputExtensions.GetMethod("AddAction");
|
var addAction = inputExtensions.GetMethod("AddAction");
|
||||||
var action = addAction.Invoke(null, new object[] { map, actionName, default, null, null, null, null, null })
|
var action = addAction.Invoke(null, new object[] { map, actionName, default, null, null, null, null, null })
|
||||||
@ -262,8 +265,16 @@ namespace UnityExplorer.Core.Input
|
|||||||
|
|
||||||
public void ActivateModule()
|
public void ActivateModule()
|
||||||
{
|
{
|
||||||
m_newInputModule.ActivateModule();
|
try
|
||||||
UI_Enable.Invoke(UI_ActionMap, ArgumentUtility.EmptyArgs);
|
{
|
||||||
|
m_newInputModule.m_EventSystem = UIManager.EventSys;
|
||||||
|
m_newInputModule.ActivateModule();
|
||||||
|
UI_Enable.Invoke(UI_ActionMap, ArgumentUtility.EmptyArgs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExplorerCore.LogWarning("Exception enabling InputSystem UI Input Module: " + ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -42,17 +42,25 @@ namespace UnityExplorer.Core.Input
|
|||||||
|
|
||||||
// UI Input module
|
// UI Input module
|
||||||
|
|
||||||
public BaseInputModule UIModule => m_inputModule;
|
public BaseInputModule UIInputModule => m_inputModule;
|
||||||
internal StandaloneInputModule m_inputModule;
|
internal StandaloneInputModule m_inputModule;
|
||||||
|
|
||||||
public void AddUIInputModule()
|
public void AddUIInputModule()
|
||||||
{
|
{
|
||||||
m_inputModule = UIManager.CanvasRoot.gameObject.AddComponent<StandaloneInputModule>();
|
m_inputModule = UIManager.CanvasRoot.gameObject.AddComponent<StandaloneInputModule>();
|
||||||
|
m_inputModule.m_EventSystem = UIManager.EventSys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ActivateModule()
|
public void ActivateModule()
|
||||||
{
|
{
|
||||||
m_inputModule.ActivateModule();
|
try
|
||||||
|
{
|
||||||
|
m_inputModule.ActivateModule();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExplorerCore.LogWarning($"Exception enabling StandaloneInputModule: {ex}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ namespace UnityExplorer.Core.Input
|
|||||||
public bool GetMouseButton(int btn) => false;
|
public bool GetMouseButton(int btn) => false;
|
||||||
public bool GetMouseButtonDown(int btn) => false;
|
public bool GetMouseButtonDown(int btn) => false;
|
||||||
|
|
||||||
public BaseInputModule UIModule => null;
|
public BaseInputModule UIInputModule => null;
|
||||||
public void ActivateModule() { }
|
public void ActivateModule() { }
|
||||||
public void AddUIInputModule() { }
|
public void AddUIInputModule() { }
|
||||||
}
|
}
|
||||||
|
@ -4,48 +4,69 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using UnhollowerBaseLib;
|
using UnhollowerBaseLib;
|
||||||
|
using UnhollowerBaseLib.Attributes;
|
||||||
using UnhollowerRuntimeLib;
|
using UnhollowerRuntimeLib;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityExplorer.Core.Runtime.Il2Cpp;
|
using UnityExplorer.Core.Runtime.Il2Cpp;
|
||||||
|
|
||||||
namespace UnityExplorer
|
namespace UnityExplorer
|
||||||
{
|
{
|
||||||
public class AssetBundle
|
public class AssetBundle : UnityEngine.Object
|
||||||
{
|
{
|
||||||
|
static AssetBundle()
|
||||||
|
{
|
||||||
|
ClassInjector.RegisterTypeInIl2Cpp<AssetBundle>();
|
||||||
|
}
|
||||||
|
|
||||||
// ~~~~~~~~~~~~ Static ~~~~~~~~~~~~
|
// ~~~~~~~~~~~~ Static ~~~~~~~~~~~~
|
||||||
|
|
||||||
internal delegate IntPtr d_LoadFromFile(IntPtr path, uint crc, ulong offset);
|
internal delegate IntPtr d_LoadFromFile(IntPtr path, uint crc, ulong offset);
|
||||||
|
|
||||||
|
[HideFromIl2Cpp]
|
||||||
public static AssetBundle LoadFromFile(string path)
|
public static AssetBundle LoadFromFile(string path)
|
||||||
{
|
{
|
||||||
var iCall = ICallManager.GetICall<d_LoadFromFile>("UnityEngine.AssetBundle::LoadFromFile_Internal");
|
var iCall = ICallManager.GetICall<d_LoadFromFile>("UnityEngine.AssetBundle::LoadFromFile_Internal");
|
||||||
var ptr = iCall.Invoke(IL2CPP.ManagedStringToIl2Cpp(path), 0u, 0UL);
|
var ptr = iCall(IL2CPP.ManagedStringToIl2Cpp(path), 0u, 0UL);
|
||||||
return new AssetBundle(ptr);
|
return new AssetBundle(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate IntPtr d_LoadFromMemory(IntPtr binary, uint crc);
|
private delegate IntPtr d_LoadFromMemory(IntPtr binary, uint crc);
|
||||||
|
|
||||||
|
[HideFromIl2Cpp]
|
||||||
public static AssetBundle LoadFromMemory(byte[] binary, uint crc = 0)
|
public static AssetBundle LoadFromMemory(byte[] binary, uint crc = 0)
|
||||||
{
|
{
|
||||||
var iCall = ICallManager.GetICall<d_LoadFromMemory>("UnityEngine.AssetBundle::LoadFromMemory_Internal");
|
var iCall = ICallManager.GetICall<d_LoadFromMemory>("UnityEngine.AssetBundle::LoadFromMemory_Internal");
|
||||||
var ptr = iCall(((Il2CppStructArray<byte>) binary).Pointer, crc);
|
var ptr = iCall(((Il2CppStructArray<byte>)binary).Pointer, crc);
|
||||||
return new AssetBundle(ptr);
|
return new AssetBundle(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public delegate IntPtr d_GetAllLoadedAssetBundles_Native();
|
||||||
|
|
||||||
|
[HideFromIl2Cpp]
|
||||||
|
public static AssetBundle[] GetAllLoadedAssetBundles()
|
||||||
|
{
|
||||||
|
var iCall = ICallManager.GetICall<d_GetAllLoadedAssetBundles_Native>("UnityEngine.AssetBundle::GetAllLoadedAssetBundles_Native");
|
||||||
|
var ptr = iCall();
|
||||||
|
if (ptr == IntPtr.Zero)
|
||||||
|
return null;
|
||||||
|
return (AssetBundle[])new Il2CppReferenceArray<AssetBundle>(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
// ~~~~~~~~~~~~ Instance ~~~~~~~~~~~~
|
// ~~~~~~~~~~~~ Instance ~~~~~~~~~~~~
|
||||||
|
|
||||||
private readonly IntPtr m_bundlePtr = IntPtr.Zero;
|
public readonly IntPtr m_bundlePtr = IntPtr.Zero;
|
||||||
|
|
||||||
public AssetBundle(IntPtr ptr) { m_bundlePtr = ptr; }
|
public AssetBundle(IntPtr ptr) : base(ptr) { m_bundlePtr = ptr; }
|
||||||
|
|
||||||
// LoadAllAssets()
|
// LoadAllAssets()
|
||||||
|
|
||||||
internal delegate IntPtr d_LoadAssetWithSubAssets_Internal(IntPtr _this, IntPtr name, IntPtr type);
|
internal delegate IntPtr d_LoadAssetWithSubAssets_Internal(IntPtr _this, IntPtr name, IntPtr type);
|
||||||
|
|
||||||
|
[HideFromIl2Cpp]
|
||||||
public UnityEngine.Object[] LoadAllAssets()
|
public UnityEngine.Object[] LoadAllAssets()
|
||||||
{
|
{
|
||||||
var iCall = ICallManager.GetICall<d_LoadAssetWithSubAssets_Internal>("UnityEngine.AssetBundle::LoadAssetWithSubAssets_Internal");
|
var iCall = ICallManager.GetICall<d_LoadAssetWithSubAssets_Internal>("UnityEngine.AssetBundle::LoadAssetWithSubAssets_Internal");
|
||||||
var ptr = iCall.Invoke(m_bundlePtr, IL2CPP.ManagedStringToIl2Cpp(""), Il2CppType.Of<UnityEngine.Object>().Pointer);
|
var ptr = iCall.Invoke(m_bundlePtr, IL2CPP.ManagedStringToIl2Cpp(""), UnhollowerRuntimeLib.Il2CppType.Of<UnityEngine.Object>().Pointer);
|
||||||
|
|
||||||
if (ptr == IntPtr.Zero)
|
if (ptr == IntPtr.Zero)
|
||||||
return new UnityEngine.Object[0];
|
return new UnityEngine.Object[0];
|
||||||
@ -57,10 +78,11 @@ namespace UnityExplorer
|
|||||||
|
|
||||||
internal delegate IntPtr d_LoadAsset_Internal(IntPtr _this, IntPtr name, IntPtr type);
|
internal delegate IntPtr d_LoadAsset_Internal(IntPtr _this, IntPtr name, IntPtr type);
|
||||||
|
|
||||||
|
[HideFromIl2Cpp]
|
||||||
public T LoadAsset<T>(string name) where T : UnityEngine.Object
|
public T LoadAsset<T>(string name) where T : UnityEngine.Object
|
||||||
{
|
{
|
||||||
var iCall = ICallManager.GetICall<d_LoadAsset_Internal>("UnityEngine.AssetBundle::LoadAsset_Internal");
|
var iCall = ICallManager.GetICall<d_LoadAsset_Internal>("UnityEngine.AssetBundle::LoadAsset_Internal");
|
||||||
var ptr = iCall.Invoke(m_bundlePtr, IL2CPP.ManagedStringToIl2Cpp(name), Il2CppType.Of<T>().Pointer);
|
var ptr = iCall.Invoke(m_bundlePtr, IL2CPP.ManagedStringToIl2Cpp(name), UnhollowerRuntimeLib.Il2CppType.Of<T>().Pointer);
|
||||||
|
|
||||||
if (ptr == IntPtr.Zero)
|
if (ptr == IntPtr.Zero)
|
||||||
return null;
|
return null;
|
||||||
@ -72,10 +94,11 @@ namespace UnityExplorer
|
|||||||
|
|
||||||
internal delegate void d_Unload(IntPtr _this, bool unloadAllLoadedObjects);
|
internal delegate void d_Unload(IntPtr _this, bool unloadAllLoadedObjects);
|
||||||
|
|
||||||
public void Unload(bool unloadAssets = true)
|
[HideFromIl2Cpp]
|
||||||
|
public void Unload(bool unloadAllLoadedObjects)
|
||||||
{
|
{
|
||||||
var iCall = ICallManager.GetICall<d_Unload>("UnityEngine.AssetBundle::Unload");
|
var iCall = ICallManager.GetICall<d_Unload>("UnityEngine.AssetBundle::Unload");
|
||||||
iCall.Invoke(this.m_bundlePtr, unloadAssets);
|
iCall.Invoke(this.m_bundlePtr, unloadAllLoadedObjects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,15 @@ namespace UnityExplorer
|
|||||||
private static readonly char[] invalidDirectoryCharacters = Path.GetInvalidPathChars();
|
private static readonly char[] invalidDirectoryCharacters = Path.GetInvalidPathChars();
|
||||||
private static readonly char[] invalidFilenameCharacters = Path.GetInvalidFileNameChars();
|
private static readonly char[] invalidFilenameCharacters = Path.GetInvalidFileNameChars();
|
||||||
|
|
||||||
public static string EnsureValidDirectory(string path)
|
public static string EnsureValidFilePath(string fullPathWithFile)
|
||||||
{
|
{
|
||||||
path = string.Concat(path.Split(invalidDirectoryCharacters));
|
// Remove invalid path characters
|
||||||
|
fullPathWithFile = string.Concat(fullPathWithFile.Split(invalidDirectoryCharacters));
|
||||||
|
|
||||||
if (!Directory.Exists(path))
|
// Create directory (does nothing if it exists)
|
||||||
Directory.CreateDirectory(path);
|
Directory.CreateDirectory(Path.GetDirectoryName(fullPathWithFile));
|
||||||
|
|
||||||
return path;
|
return fullPathWithFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string EnsureValidFilename(string filename)
|
public static string EnsureValidFilename(string filename)
|
||||||
|
@ -9,7 +9,6 @@ using UnityEngine.Events;
|
|||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
// Project-wide namespace for accessibility
|
|
||||||
namespace UnityExplorer
|
namespace UnityExplorer
|
||||||
{
|
{
|
||||||
public static class UnityHelpers
|
public static class UnityHelpers
|
||||||
@ -32,25 +31,28 @@ namespace UnityExplorer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsNullOrDestroyed(this object obj, bool suppressWarning = true)
|
public static bool IsNullOrDestroyed(this object obj, bool suppressWarning = true)
|
||||||
{
|
{
|
||||||
var unityObj = obj as Object;
|
try
|
||||||
if (obj == null)
|
|
||||||
{
|
{
|
||||||
if (!suppressWarning)
|
if (obj == null)
|
||||||
ExplorerCore.LogWarning("The target instance is null!");
|
{
|
||||||
|
if (!suppressWarning)
|
||||||
|
ExplorerCore.LogWarning("The target instance is null!");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (obj is Object)
|
else if (obj is Object unityObj && !unityObj)
|
||||||
{
|
|
||||||
if (!unityObj)
|
|
||||||
{
|
{
|
||||||
if (!suppressWarning)
|
if (!suppressWarning)
|
||||||
ExplorerCore.LogWarning("The target UnityEngine.Object was destroyed!");
|
ExplorerCore.LogWarning("The target UnityEngine.Object was destroyed!");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -20,7 +20,7 @@ namespace UnityExplorer
|
|||||||
public static class ExplorerCore
|
public static class ExplorerCore
|
||||||
{
|
{
|
||||||
public const string NAME = "UnityExplorer";
|
public const string NAME = "UnityExplorer";
|
||||||
public const string VERSION = "4.3.0";
|
public const string VERSION = "4.3.3";
|
||||||
public const string AUTHOR = "Sinai";
|
public const string AUTHOR = "Sinai";
|
||||||
public const string GUID = "com.sinai.unityexplorer";
|
public const string GUID = "com.sinai.unityexplorer";
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ namespace UnityExplorer.Hooks
|
|||||||
UIFactory.SetLayoutElement(DeleteButton.Component.gameObject, minHeight: 25, minWidth: 100);
|
UIFactory.SetLayoutElement(DeleteButton.Component.gameObject, minHeight: 25, minWidth: 100);
|
||||||
DeleteButton.OnClick += OnDeleteClicked;
|
DeleteButton.OnClick += OnDeleteClicked;
|
||||||
|
|
||||||
EditPatchButton = UIFactory.CreateButton(UIRoot, "EditButton", "Log Hook Source", new Color(0.15f, 0.15f, 0.15f));
|
EditPatchButton = UIFactory.CreateButton(UIRoot, "EditButton", "Edit Hook Source", new Color(0.15f, 0.15f, 0.15f));
|
||||||
UIFactory.SetLayoutElement(EditPatchButton.Component.gameObject, minHeight: 25, minWidth: 150);
|
UIFactory.SetLayoutElement(EditPatchButton.Component.gameObject, minHeight: 25, minWidth: 150);
|
||||||
EditPatchButton.OnClick += OnEditPatchClicked;
|
EditPatchButton.OnClick += OnEditPatchClicked;
|
||||||
|
|
||||||
|
@ -5,85 +5,119 @@ using System.Linq;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using Microsoft.CSharp;
|
using Mono.CSharp;
|
||||||
using UnityExplorer.CSConsole;
|
using UnityExplorer.CSConsole;
|
||||||
|
|
||||||
namespace UnityExplorer.Hooks
|
namespace UnityExplorer.Hooks
|
||||||
{
|
{
|
||||||
public class HookInstance
|
public class HookInstance
|
||||||
{
|
{
|
||||||
|
// Static
|
||||||
|
|
||||||
private static readonly StringBuilder evalOutput = new StringBuilder();
|
private static readonly StringBuilder evalOutput = new StringBuilder();
|
||||||
private static readonly ScriptEvaluator scriptEvaluator = new ScriptEvaluator(new StringWriter(evalOutput));
|
private static readonly ScriptEvaluator scriptEvaluator = new ScriptEvaluator(new StringWriter(evalOutput));
|
||||||
|
|
||||||
|
static HookInstance()
|
||||||
|
{
|
||||||
|
scriptEvaluator.Run("using System;");
|
||||||
|
scriptEvaluator.Run("using System.Reflection;");
|
||||||
|
scriptEvaluator.Run("using System.Collections;");
|
||||||
|
scriptEvaluator.Run("using System.Collections.Generic;");
|
||||||
|
}
|
||||||
|
|
||||||
// Instance
|
// Instance
|
||||||
|
|
||||||
public bool Enabled;
|
public bool Enabled;
|
||||||
public MethodInfo TargetMethod;
|
public MethodInfo TargetMethod;
|
||||||
public string GeneratedSource;
|
public string PatchSourceCode;
|
||||||
|
|
||||||
private string shortSignature;
|
private readonly string shortSignature;
|
||||||
private PatchProcessor patchProcessor;
|
private PatchProcessor patchProcessor;
|
||||||
private HarmonyMethod patchDelegate;
|
|
||||||
private MethodInfo patchDelegateMethodInfo;
|
private MethodInfo postfix;
|
||||||
|
private MethodInfo prefix;
|
||||||
|
private MethodInfo finalizer;
|
||||||
|
private MethodInfo transpiler;
|
||||||
|
|
||||||
public HookInstance(MethodInfo targetMethod)
|
public HookInstance(MethodInfo targetMethod)
|
||||||
{
|
{
|
||||||
this.TargetMethod = targetMethod;
|
this.TargetMethod = targetMethod;
|
||||||
this.shortSignature = $"{targetMethod.DeclaringType.Name}.{targetMethod.Name}";
|
this.shortSignature = $"{targetMethod.DeclaringType.Name}.{targetMethod.Name}";
|
||||||
GenerateProcessorAndDelegate();
|
|
||||||
Patch();
|
GenerateDefaultPatchSourceCode(targetMethod);
|
||||||
|
|
||||||
|
if (CompileAndGenerateProcessor(PatchSourceCode))
|
||||||
|
Patch();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GenerateProcessorAndDelegate()
|
// Evaluator.source_file
|
||||||
|
private static readonly FieldInfo fi_sourceFile = ReflectionUtility.GetFieldInfo(typeof(Evaluator), "source_file");
|
||||||
|
// TypeDefinition.Definition
|
||||||
|
private static readonly PropertyInfo pi_Definition = ReflectionUtility.GetPropertyInfo(typeof(TypeDefinition), "Definition");
|
||||||
|
|
||||||
|
public bool CompileAndGenerateProcessor(string patchSource)
|
||||||
{
|
{
|
||||||
|
Unpatch();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
patchProcessor = ExplorerCore.Harmony.CreateProcessor(TargetMethod);
|
patchProcessor = ExplorerCore.Harmony.CreateProcessor(TargetMethod);
|
||||||
|
|
||||||
// Dynamically compile the patch method
|
// Dynamically compile the patch method
|
||||||
|
|
||||||
scriptEvaluator.Run(GeneratePatchSourceCode(TargetMethod));
|
var codeBuilder = new StringBuilder();
|
||||||
|
|
||||||
// Get the compiled method and check for errors
|
codeBuilder.AppendLine($"public class DynamicPatch_{DateTime.Now.Ticks}");
|
||||||
|
codeBuilder.AppendLine("{");
|
||||||
|
codeBuilder.AppendLine(patchSource);
|
||||||
|
codeBuilder.AppendLine("}");
|
||||||
|
|
||||||
|
scriptEvaluator.Run(codeBuilder.ToString());
|
||||||
|
|
||||||
string output = scriptEvaluator._textWriter.ToString();
|
|
||||||
var outputSplit = output.Split('\n');
|
|
||||||
if (outputSplit.Length >= 2)
|
|
||||||
output = outputSplit[outputSplit.Length - 2];
|
|
||||||
evalOutput.Clear();
|
|
||||||
if (ScriptEvaluator._reportPrinter.ErrorsCount > 0)
|
if (ScriptEvaluator._reportPrinter.ErrorsCount > 0)
|
||||||
throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}");
|
throw new FormatException($"Unable to compile the generated patch!");
|
||||||
|
|
||||||
// Could publicize MCS to avoid this reflection, but not bothering for now
|
// TODO: Publicize MCS to avoid this reflection
|
||||||
var source = (Mono.CSharp.CompilationSourceFile)ReflectionUtility.GetFieldInfo(typeof(Mono.CSharp.Evaluator), "source_file")
|
// Get the most recent Patch type in the source file
|
||||||
.GetValue(scriptEvaluator);
|
var typeContainer = ((CompilationSourceFile)fi_sourceFile.GetValue(scriptEvaluator))
|
||||||
var type = (Mono.CSharp.Class)source.Containers.Last();
|
.Containers
|
||||||
var systemType = ((Mono.CSharp.TypeSpec)ReflectionUtility.GetPropertyInfo(typeof(Mono.CSharp.TypeDefinition), "Definition")
|
.Last(it => it.MemberName.Name.StartsWith("DynamicPatch_"));
|
||||||
.GetValue(type, null))
|
// Get the TypeSpec from the TypeDefinition, then get its "MetaInfo" (System.Type)
|
||||||
.GetMetaInfo();
|
var patchClass = ((TypeSpec)pi_Definition.GetValue((Class)typeContainer, null)).GetMetaInfo();
|
||||||
|
|
||||||
this.patchDelegateMethodInfo = systemType.GetMethod("Patch", ReflectionUtility.FLAGS);
|
// Create the harmony patches as defined
|
||||||
|
|
||||||
// Actually create the harmony patch
|
postfix = patchClass.GetMethod("Postfix", ReflectionUtility.FLAGS);
|
||||||
this.patchDelegate = new HarmonyMethod(patchDelegateMethodInfo);
|
if (postfix != null)
|
||||||
patchProcessor.AddPostfix(patchDelegate);
|
patchProcessor.AddPostfix(new HarmonyMethod(postfix));
|
||||||
|
|
||||||
|
prefix = patchClass.GetMethod("Prefix", ReflectionUtility.FLAGS);
|
||||||
|
if (prefix != null)
|
||||||
|
patchProcessor.AddPrefix(new HarmonyMethod(prefix));
|
||||||
|
|
||||||
|
finalizer = patchClass.GetMethod("Finalizer", ReflectionUtility.FLAGS);
|
||||||
|
if (finalizer != null)
|
||||||
|
patchProcessor.AddFinalizer(new HarmonyMethod(finalizer));
|
||||||
|
|
||||||
|
transpiler = patchClass.GetMethod("Transpiler", ReflectionUtility.FLAGS);
|
||||||
|
if (transpiler != null)
|
||||||
|
patchProcessor.AddTranspiler(new HarmonyMethod(transpiler));
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
ExplorerCore.LogWarning($"Exception creating patch processor for target method {TargetMethod.FullDescription()}!\r\n{ex}");
|
ExplorerCore.LogWarning($"Exception creating patch processor for target method {TargetMethod.FullDescription()}!\r\n{ex}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GeneratePatchSourceCode(MethodInfo targetMethod)
|
private string GenerateDefaultPatchSourceCode(MethodInfo targetMethod)
|
||||||
{
|
{
|
||||||
var codeBuilder = new StringBuilder();
|
var codeBuilder = new StringBuilder();
|
||||||
|
|
||||||
codeBuilder.AppendLine($"public class DynamicPatch_{DateTime.Now.Ticks}");
|
|
||||||
codeBuilder.AppendLine("{");
|
|
||||||
|
|
||||||
// Arguments
|
// Arguments
|
||||||
|
|
||||||
codeBuilder.Append(" public static void Patch(System.Reflection.MethodBase __originalMethod");
|
codeBuilder.Append("public static void Postfix(System.Reflection.MethodBase __originalMethod");
|
||||||
|
|
||||||
if (!targetMethod.IsStatic)
|
if (!targetMethod.IsStatic)
|
||||||
codeBuilder.Append($", {targetMethod.DeclaringType.FullName} __instance");
|
codeBuilder.Append($", {targetMethod.DeclaringType.FullName} __instance");
|
||||||
@ -91,11 +125,12 @@ namespace UnityExplorer.Hooks
|
|||||||
if (targetMethod.ReturnType != typeof(void))
|
if (targetMethod.ReturnType != typeof(void))
|
||||||
codeBuilder.Append($", {targetMethod.ReturnType.FullName} __result");
|
codeBuilder.Append($", {targetMethod.ReturnType.FullName} __result");
|
||||||
|
|
||||||
int paramIdx = 0;
|
|
||||||
var parameters = targetMethod.GetParameters();
|
var parameters = targetMethod.GetParameters();
|
||||||
|
|
||||||
|
int paramIdx = 0;
|
||||||
foreach (var param in parameters)
|
foreach (var param in parameters)
|
||||||
{
|
{
|
||||||
codeBuilder.Append($", {param.ParameterType.FullName} __{paramIdx}");
|
codeBuilder.Append($", {param.ParameterType.FullDescription().Replace("&", "")} __{paramIdx}");
|
||||||
paramIdx++;
|
paramIdx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,59 +138,60 @@ namespace UnityExplorer.Hooks
|
|||||||
|
|
||||||
// Patch body
|
// Patch body
|
||||||
|
|
||||||
codeBuilder.AppendLine(" {");
|
codeBuilder.AppendLine("{");
|
||||||
|
|
||||||
codeBuilder.AppendLine(" try {");
|
codeBuilder.AppendLine(" try {");
|
||||||
|
|
||||||
// Log message
|
// Log message
|
||||||
|
|
||||||
var logMessage = new StringBuilder();
|
var logMessage = new StringBuilder();
|
||||||
logMessage.AppendLine($"$@\"Patch called: {shortSignature}");
|
logMessage.Append($"Patch called: {shortSignature}\\n");
|
||||||
|
|
||||||
if (!targetMethod.IsStatic)
|
if (!targetMethod.IsStatic)
|
||||||
logMessage.AppendLine("__instance: {__instance.ToString()}");
|
logMessage.Append("__instance: {__instance.ToString()}\\n");
|
||||||
|
|
||||||
paramIdx = 0;
|
paramIdx = 0;
|
||||||
foreach (var param in parameters)
|
foreach (var param in parameters)
|
||||||
{
|
{
|
||||||
if (param.ParameterType.IsValueType)
|
logMessage.Append($"Parameter {paramIdx} {param.Name}: ");
|
||||||
logMessage.AppendLine($"Parameter {paramIdx}: {{__{paramIdx}.ToString()}}");
|
Type pType = param.ParameterType;
|
||||||
|
if (pType.IsByRef) pType = pType.GetElementType();
|
||||||
|
if (pType.IsValueType)
|
||||||
|
logMessage.Append($"{{__{paramIdx}.ToString()}}");
|
||||||
else
|
else
|
||||||
logMessage.AppendLine($"Parameter {paramIdx}: {{__{paramIdx}?.ToString() ?? \"null\"}}");
|
logMessage.Append($"{{__{paramIdx}?.ToString() ?? \"null\"}}");
|
||||||
|
logMessage.Append("\\n");
|
||||||
paramIdx++;
|
paramIdx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetMethod.ReturnType != typeof(void))
|
if (targetMethod.ReturnType != typeof(void))
|
||||||
{
|
{
|
||||||
|
logMessage.Append("Return value: ");
|
||||||
if (targetMethod.ReturnType.IsValueType)
|
if (targetMethod.ReturnType.IsValueType)
|
||||||
logMessage.AppendLine("Return value: {__result.ToString()}");
|
logMessage.Append("{__result.ToString()}");
|
||||||
else
|
else
|
||||||
logMessage.AppendLine("Return value: {__result?.ToString() ?? \"null\"}");
|
logMessage.Append("{__result?.ToString() ?? \"null\"}");
|
||||||
|
logMessage.Append("\\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
logMessage.Append('"');
|
codeBuilder.AppendLine($" UnityExplorer.ExplorerCore.Log($\"{logMessage}\");");
|
||||||
|
codeBuilder.AppendLine(" }");
|
||||||
codeBuilder.AppendLine($" UnityExplorer.ExplorerCore.Log({logMessage});");
|
codeBuilder.AppendLine(" catch (System.Exception ex) {");
|
||||||
codeBuilder.AppendLine(" }");
|
codeBuilder.AppendLine($" UnityExplorer.ExplorerCore.LogWarning($\"Exception in patch of {shortSignature}:\\n{{ex}}\");");
|
||||||
codeBuilder.AppendLine(" catch (System.Exception ex) {");
|
codeBuilder.AppendLine(" }");
|
||||||
codeBuilder.AppendLine($" UnityExplorer.ExplorerCore.LogWarning($\"Exception in patch of {shortSignature}:\\n{{ex}}\");");
|
|
||||||
codeBuilder.AppendLine(" }");
|
|
||||||
|
|
||||||
// End patch body
|
// End patch body
|
||||||
|
|
||||||
codeBuilder.AppendLine(" }");
|
|
||||||
|
|
||||||
// End class
|
|
||||||
|
|
||||||
codeBuilder.AppendLine("}");
|
codeBuilder.AppendLine("}");
|
||||||
|
|
||||||
return GeneratedSource = codeBuilder.ToString();
|
//ExplorerCore.Log(codeBuilder.ToString());
|
||||||
|
|
||||||
|
return PatchSourceCode = codeBuilder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TogglePatch()
|
public void TogglePatch()
|
||||||
{
|
{
|
||||||
Enabled = !Enabled;
|
if (!Enabled)
|
||||||
if (Enabled)
|
|
||||||
Patch();
|
Patch();
|
||||||
else
|
else
|
||||||
Unpatch();
|
Unpatch();
|
||||||
@ -166,6 +202,7 @@ namespace UnityExplorer.Hooks
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
patchProcessor.Patch();
|
patchProcessor.Patch();
|
||||||
|
|
||||||
Enabled = true;
|
Enabled = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -176,12 +213,17 @@ namespace UnityExplorer.Hooks
|
|||||||
|
|
||||||
public void Unpatch()
|
public void Unpatch()
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.patchProcessor.Unpatch(patchDelegateMethodInfo);
|
if (prefix != null)
|
||||||
|
patchProcessor.Unpatch(prefix);
|
||||||
|
if (postfix != null)
|
||||||
|
patchProcessor.Unpatch(postfix);
|
||||||
|
if (finalizer != null)
|
||||||
|
patchProcessor.Unpatch(finalizer);
|
||||||
|
if (transpiler != null)
|
||||||
|
patchProcessor.Unpatch(transpiler);
|
||||||
|
|
||||||
Enabled = false;
|
Enabled = false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -6,6 +6,7 @@ using System.Reflection;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityExplorer.CSConsole;
|
||||||
using UnityExplorer.UI;
|
using UnityExplorer.UI;
|
||||||
using UnityExplorer.UI.Panels;
|
using UnityExplorer.UI.Panels;
|
||||||
using UnityExplorer.UI.Widgets;
|
using UnityExplorer.UI.Widgets;
|
||||||
@ -19,77 +20,25 @@ namespace UnityExplorer.Hooks
|
|||||||
|
|
||||||
public HookManagerPanel Panel => UIManager.GetPanel<HookManagerPanel>(UIManager.Panels.HookManager);
|
public HookManagerPanel Panel => UIManager.GetPanel<HookManagerPanel>(UIManager.Panels.HookManager);
|
||||||
|
|
||||||
public int ItemCount => addingMethods ? currentFilteredMethods.Count : currentHooks.Count;
|
// This class acts as the data source for both current hooks and eligable methods when adding hooks.
|
||||||
|
// 'isAddingMethods' keeps track of which pool is currently the displayed one, so our ItemCount reflects the
|
||||||
|
// correct pool cells.
|
||||||
|
private bool isAddingMethods;
|
||||||
|
public int ItemCount => isAddingMethods ? filteredEligableMethods.Count : currentHooks.Count;
|
||||||
|
|
||||||
private bool addingMethods;
|
// current hooks
|
||||||
private HashSet<string> hookedSignatures = new HashSet<string>();
|
private readonly HashSet<string> hookedSignatures = new HashSet<string>();
|
||||||
private readonly OrderedDictionary currentHooks = new OrderedDictionary();
|
private readonly OrderedDictionary currentHooks = new OrderedDictionary();
|
||||||
|
|
||||||
|
// adding hooks
|
||||||
private readonly List<MethodInfo> currentAddEligableMethods = new List<MethodInfo>();
|
private readonly List<MethodInfo> currentAddEligableMethods = new List<MethodInfo>();
|
||||||
private readonly List<MethodInfo> currentFilteredMethods = new List<MethodInfo>();
|
private readonly List<MethodInfo> filteredEligableMethods = new List<MethodInfo>();
|
||||||
|
|
||||||
public void OnClassSelectedForHooks(string typeFullName)
|
// hook editor
|
||||||
{
|
private readonly LexerBuilder Lexer = new LexerBuilder();
|
||||||
var type = ReflectionUtility.GetTypeByName(typeFullName);
|
private HookInstance currentEditedHook;
|
||||||
if (type == null)
|
|
||||||
{
|
|
||||||
ExplorerCore.LogWarning($"Could not find any type by name {typeFullName}!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Panel.SetAddHooksLabelType(SignatureHighlighter.Parse(type, true));
|
// ~~~~~~~~~~~ Main Current Hooks window ~~~~~~~~~~~
|
||||||
|
|
||||||
Panel.ResetMethodFilter();
|
|
||||||
currentFilteredMethods.Clear();
|
|
||||||
currentAddEligableMethods.Clear();
|
|
||||||
foreach (var method in type.GetMethods(ReflectionUtility.FLAGS))
|
|
||||||
{
|
|
||||||
if (method.IsGenericMethod || method.IsAbstract || ReflectionUtility.IsBlacklisted(method))
|
|
||||||
continue;
|
|
||||||
currentAddEligableMethods.Add(method);
|
|
||||||
currentFilteredMethods.Add(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
addingMethods = true;
|
|
||||||
Panel.SetAddPanelActive(true);
|
|
||||||
Panel.MethodResultsScrollPool.Refresh(true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseAddHooks()
|
|
||||||
{
|
|
||||||
addingMethods = false;
|
|
||||||
Panel.SetAddPanelActive(false);
|
|
||||||
Panel.HooksScrollPool.Refresh(true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnHookAllClicked()
|
|
||||||
{
|
|
||||||
foreach (var method in currentAddEligableMethods)
|
|
||||||
AddHook(method);
|
|
||||||
Panel.MethodResultsScrollPool.Refresh(true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddHookClicked(int index)
|
|
||||||
{
|
|
||||||
if (index >= this.currentFilteredMethods.Count)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AddHook(currentFilteredMethods[index]);
|
|
||||||
Panel.MethodResultsScrollPool.Refresh(true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddHook(MethodInfo method)
|
|
||||||
{
|
|
||||||
var sig = method.FullDescription();
|
|
||||||
if (hookedSignatures.Contains(sig))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var hook = new HookInstance(method);
|
|
||||||
if (hook.Enabled)
|
|
||||||
{
|
|
||||||
hookedSignatures.Add(sig);
|
|
||||||
currentHooks.Add(sig, hook);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnableOrDisableHookClicked(int index)
|
public void EnableOrDisableHookClicked(int index)
|
||||||
{
|
{
|
||||||
@ -111,34 +60,16 @@ namespace UnityExplorer.Hooks
|
|||||||
|
|
||||||
public void EditPatchClicked(int index)
|
public void EditPatchClicked(int index)
|
||||||
{
|
{
|
||||||
|
Panel.SetPage(HookManagerPanel.Pages.HookSourceEditor);
|
||||||
var hook = (HookInstance)currentHooks[index];
|
var hook = (HookInstance)currentHooks[index];
|
||||||
ExplorerCore.Log(hook.GeneratedSource);
|
currentEditedHook = hook;
|
||||||
|
Panel.EditorInput.Text = hook.PatchSourceCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnAddHookFilterInputChanged(string input)
|
|
||||||
{
|
|
||||||
currentFilteredMethods.Clear();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(input))
|
|
||||||
currentFilteredMethods.AddRange(currentAddEligableMethods);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var method in currentAddEligableMethods)
|
|
||||||
{
|
|
||||||
if (method.Name.ContainsIgnoreCase(input))
|
|
||||||
currentFilteredMethods.Add(method);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Panel.MethodResultsScrollPool.Refresh(true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnBorrow methods not needed
|
|
||||||
public void OnCellBorrowed(HookCell cell) { }
|
|
||||||
public void OnCellBorrowed(AddHookCell cell) { }
|
|
||||||
|
|
||||||
// Set current hook cell
|
// Set current hook cell
|
||||||
|
|
||||||
|
public void OnCellBorrowed(HookCell cell) { }
|
||||||
|
|
||||||
public void SetCell(HookCell cell, int index)
|
public void SetCell(HookCell cell, int index)
|
||||||
{
|
{
|
||||||
if (index >= this.currentHooks.Count)
|
if (index >= this.currentHooks.Count)
|
||||||
@ -157,18 +88,97 @@ namespace UnityExplorer.Hooks
|
|||||||
hook.Enabled ? new Color(0.15f, 0.2f, 0.15f) : new Color(0.2f, 0.2f, 0.15f));
|
hook.Enabled ? new Color(0.15f, 0.2f, 0.15f) : new Color(0.2f, 0.2f, 0.15f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ~~~~~~~~~~~ Add Hooks window ~~~~~~~~~~~
|
||||||
|
|
||||||
|
public void OnClassSelectedForHooks(string typeFullName)
|
||||||
|
{
|
||||||
|
var type = ReflectionUtility.GetTypeByName(typeFullName);
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
ExplorerCore.LogWarning($"Could not find any type by name {typeFullName}!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel.SetAddHooksLabelType(SignatureHighlighter.Parse(type, true));
|
||||||
|
|
||||||
|
Panel.ResetMethodFilter();
|
||||||
|
filteredEligableMethods.Clear();
|
||||||
|
currentAddEligableMethods.Clear();
|
||||||
|
foreach (var method in type.GetMethods(ReflectionUtility.FLAGS))
|
||||||
|
{
|
||||||
|
if (method.IsGenericMethod /* || method.IsAbstract */ || ReflectionUtility.IsBlacklisted(method))
|
||||||
|
continue;
|
||||||
|
currentAddEligableMethods.Add(method);
|
||||||
|
filteredEligableMethods.Add(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAddingMethods = true;
|
||||||
|
Panel.SetPage(HookManagerPanel.Pages.ClassMethodSelector);
|
||||||
|
Panel.AddHooksScrollPool.Refresh(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DoneAddingHooks()
|
||||||
|
{
|
||||||
|
isAddingMethods = false;
|
||||||
|
Panel.SetPage(HookManagerPanel.Pages.CurrentHooks);
|
||||||
|
Panel.HooksScrollPool.Refresh(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddHookClicked(int index)
|
||||||
|
{
|
||||||
|
if (index >= this.filteredEligableMethods.Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AddHook(filteredEligableMethods[index]);
|
||||||
|
Panel.AddHooksScrollPool.Refresh(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddHook(MethodInfo method)
|
||||||
|
{
|
||||||
|
var sig = method.FullDescription();
|
||||||
|
if (hookedSignatures.Contains(sig))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var hook = new HookInstance(method);
|
||||||
|
if (hook.Enabled)
|
||||||
|
{
|
||||||
|
hookedSignatures.Add(sig);
|
||||||
|
currentHooks.Add(sig, hook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnAddHookFilterInputChanged(string input)
|
||||||
|
{
|
||||||
|
filteredEligableMethods.Clear();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
filteredEligableMethods.AddRange(currentAddEligableMethods);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var method in currentAddEligableMethods)
|
||||||
|
{
|
||||||
|
if (method.Name.ContainsIgnoreCase(input))
|
||||||
|
filteredEligableMethods.Add(method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel.AddHooksScrollPool.Refresh(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Set eligable method cell
|
// Set eligable method cell
|
||||||
|
|
||||||
|
public void OnCellBorrowed(AddHookCell cell) { }
|
||||||
|
|
||||||
public void SetCell(AddHookCell cell, int index)
|
public void SetCell(AddHookCell cell, int index)
|
||||||
{
|
{
|
||||||
if (index >= this.currentFilteredMethods.Count)
|
if (index >= this.filteredEligableMethods.Count)
|
||||||
{
|
{
|
||||||
cell.Disable();
|
cell.Disable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cell.CurrentDisplayedIndex = index;
|
cell.CurrentDisplayedIndex = index;
|
||||||
var method = this.currentFilteredMethods[index];
|
var method = this.filteredEligableMethods[index];
|
||||||
|
|
||||||
cell.MethodNameLabel.text = HighlightMethod(method);
|
cell.MethodNameLabel.text = HighlightMethod(method);
|
||||||
|
|
||||||
@ -185,7 +195,35 @@ namespace UnityExplorer.Hooks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private static readonly string VOID_HIGHLIGHT = $"<color=#{SignatureHighlighter.keywordBlueHex}>void</color> ";
|
// ~~~~~~~~~~~ Hook source editor window ~~~~~~~~~~~
|
||||||
|
|
||||||
|
public void OnEditorInputChanged(string value)
|
||||||
|
{
|
||||||
|
Panel.EditorHighlightText.text = Lexer.BuildHighlightedString(value, 0, value.Length - 1, 0,
|
||||||
|
Panel.EditorInput.Component.caretPosition, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditorInputCancel()
|
||||||
|
{
|
||||||
|
currentEditedHook = null;
|
||||||
|
Panel.SetPage(HookManagerPanel.Pages.CurrentHooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditorInputSave()
|
||||||
|
{
|
||||||
|
var input = Panel.EditorInput.Text;
|
||||||
|
bool wasEnabled = currentEditedHook.Enabled;
|
||||||
|
if (currentEditedHook.CompileAndGenerateProcessor(input))
|
||||||
|
{
|
||||||
|
if (wasEnabled)
|
||||||
|
currentEditedHook.Patch();
|
||||||
|
currentEditedHook.PatchSourceCode = input;
|
||||||
|
currentEditedHook = null;
|
||||||
|
Panel.SetPage(HookManagerPanel.Pages.CurrentHooks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~~~~~~~~~~ Method syntax highlighting
|
||||||
|
|
||||||
private static readonly Dictionary<string, string> highlightedMethods = new Dictionary<string, string>();
|
private static readonly Dictionary<string, string> highlightedMethods = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
<ReferenceFolders Include="..\lib\BepInEx.6.Mono\" />
|
<ReferenceFolders Include="..\lib\BepInEx.6.Mono\" />
|
||||||
<ReferenceFolders Include="..\lib\BepInEx.5\" />
|
<ReferenceFolders Include="..\lib\BepInEx.5\" />
|
||||||
<ReferenceFolders Include="..\lib\MelonLoader\" />
|
<ReferenceFolders Include="..\lib\MelonLoader\" />
|
||||||
|
<ReferenceFolders Include="..\lib\Il2CppAssemblyUnhollower\UnhollowerBaseLib\bin\Release\net4.7.2\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ILRepack
|
<ILRepack
|
||||||
|
@ -644,7 +644,7 @@ namespace UnityExplorer.Inspectors
|
|||||||
var fitter = imageObj.AddComponent<ContentSizeFitter>();
|
var fitter = imageObj.AddComponent<ContentSizeFitter>();
|
||||||
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||||
textureImage = imageObj.AddComponent<Image>();
|
textureImage = imageObj.AddComponent<Image>();
|
||||||
textureImageLayout = UIFactory.SetLayoutElement(imageObj, flexibleWidth: 9999, flexibleHeight: 9999);
|
textureImageLayout = UIFactory.SetLayoutElement(imageObj, flexibleWidth: 1, flexibleHeight: 1);
|
||||||
|
|
||||||
textureViewer.SetActive(false);
|
textureViewer.SetActive(false);
|
||||||
}
|
}
|
||||||
@ -664,6 +664,7 @@ namespace UnityExplorer.Inspectors
|
|||||||
textureImage.sprite = sprite;
|
textureImage.sprite = sprite;
|
||||||
|
|
||||||
textureImageLayout.preferredHeight = sprite.rect.height;
|
textureImageLayout.preferredHeight = sprite.rect.height;
|
||||||
|
// not really working, its always stretched horizontally for some reason.
|
||||||
textureImageLayout.preferredWidth = sprite.rect.width;
|
textureImageLayout.preferredWidth = sprite.rect.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,7 +689,7 @@ namespace UnityExplorer.Inspectors
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
path = IOUtility.EnsureValidDirectory(path);
|
path = IOUtility.EnsureValidFilePath(path);
|
||||||
|
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
File.Delete(path);
|
File.Delete(path);
|
||||||
@ -699,7 +700,6 @@ namespace UnityExplorer.Inspectors
|
|||||||
tex = TextureUtilProvider.ForceReadTexture(tex);
|
tex = TextureUtilProvider.ForceReadTexture(tex);
|
||||||
|
|
||||||
byte[] data = TextureUtilProvider.Instance.EncodeToPNG(tex);
|
byte[] data = TextureUtilProvider.Instance.EncodeToPNG(tex);
|
||||||
|
|
||||||
File.WriteAllBytes(path, data);
|
File.WriteAllBytes(path, data);
|
||||||
|
|
||||||
if (tex != TextureRef)
|
if (tex != TextureRef)
|
||||||
|
@ -34,7 +34,7 @@ namespace UnityExplorer.Loader.ML
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This wrapper exists to handle the arbitrary "LemonAction" delegates which ML now uses in 0.4.4+.
|
// This wrapper exists to handle the "LemonAction" delegates which ML now uses in 0.4.4+.
|
||||||
// Reflection is required since the delegate type changed between 0.4.3 and 0.4.4.
|
// Reflection is required since the delegate type changed between 0.4.3 and 0.4.4.
|
||||||
// A wrapper class is required to link the MelonPreferences_Entry and the delegate instance.
|
// A wrapper class is required to link the MelonPreferences_Entry and the delegate instance.
|
||||||
public class EntryDelegateWrapper<T>
|
public class EntryDelegateWrapper<T>
|
||||||
|
@ -13,6 +13,13 @@ namespace UnityExplorer.UI.Panels
|
|||||||
{
|
{
|
||||||
public class HookManagerPanel : UIPanel
|
public class HookManagerPanel : UIPanel
|
||||||
{
|
{
|
||||||
|
public enum Pages
|
||||||
|
{
|
||||||
|
CurrentHooks,
|
||||||
|
ClassMethodSelector,
|
||||||
|
HookSourceEditor
|
||||||
|
}
|
||||||
|
|
||||||
public override UIManager.Panels PanelType => UIManager.Panels.HookManager;
|
public override UIManager.Panels PanelType => UIManager.Panels.HookManager;
|
||||||
|
|
||||||
public override string Name => "Hooks";
|
public override string Name => "Hooks";
|
||||||
@ -20,14 +27,22 @@ namespace UnityExplorer.UI.Panels
|
|||||||
public override int MinHeight => 600;
|
public override int MinHeight => 600;
|
||||||
public override bool ShowByDefault => false;
|
public override bool ShowByDefault => false;
|
||||||
|
|
||||||
|
public Pages CurrentPage { get; private set; } = Pages.CurrentHooks;
|
||||||
|
|
||||||
|
private GameObject currentHooksPanel;
|
||||||
public ScrollPool<HookCell> HooksScrollPool;
|
public ScrollPool<HookCell> HooksScrollPool;
|
||||||
public ScrollPool<AddHookCell> MethodResultsScrollPool;
|
private InputFieldRef classSelectorInputField;
|
||||||
|
|
||||||
private GameObject addHooksPanel;
|
private GameObject addHooksPanel;
|
||||||
private GameObject currentHooksPanel;
|
public ScrollPool<AddHookCell> AddHooksScrollPool;
|
||||||
private InputFieldRef classInputField;
|
|
||||||
private Text addHooksLabel;
|
private Text addHooksLabel;
|
||||||
private InputFieldRef methodFilterInput;
|
private InputFieldRef AddHooksMethodFilterInput;
|
||||||
|
|
||||||
|
private GameObject editorPanel;
|
||||||
|
public InputFieldScroller EditorInputScroller { get; private set; }
|
||||||
|
public InputFieldRef EditorInput => EditorInputScroller.InputField;
|
||||||
|
public Text EditorInputText { get; private set; }
|
||||||
|
public Text EditorHighlightText { get; private set; }
|
||||||
|
|
||||||
public override string GetSaveDataFromConfigManager() => ConfigManager.HookManagerData.Value;
|
public override string GetSaveDataFromConfigManager() => ConfigManager.HookManagerData.Value;
|
||||||
|
|
||||||
@ -35,22 +50,38 @@ namespace UnityExplorer.UI.Panels
|
|||||||
|
|
||||||
private void OnClassInputAddClicked()
|
private void OnClassInputAddClicked()
|
||||||
{
|
{
|
||||||
HookManager.Instance.OnClassSelectedForHooks(this.classInputField.Text);
|
HookManager.Instance.OnClassSelectedForHooks(this.classSelectorInputField.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetAddHooksLabelType(string typeText) => addHooksLabel.text = $"Adding hooks to: {typeText}";
|
public void SetAddHooksLabelType(string typeText) => addHooksLabel.text = $"Adding hooks to: {typeText}";
|
||||||
|
|
||||||
public void SetAddPanelActive(bool show)
|
public void SetPage(Pages page)
|
||||||
{
|
{
|
||||||
addHooksPanel.SetActive(show);
|
switch (page)
|
||||||
currentHooksPanel.SetActive(!show);
|
{
|
||||||
|
case Pages.CurrentHooks:
|
||||||
|
currentHooksPanel.SetActive(true);
|
||||||
|
addHooksPanel.SetActive(false);
|
||||||
|
editorPanel.SetActive(false);
|
||||||
|
break;
|
||||||
|
case Pages.ClassMethodSelector:
|
||||||
|
currentHooksPanel.SetActive(false);
|
||||||
|
addHooksPanel.SetActive(true);
|
||||||
|
editorPanel.SetActive(false);
|
||||||
|
break;
|
||||||
|
case Pages.HookSourceEditor:
|
||||||
|
currentHooksPanel.SetActive(false);
|
||||||
|
addHooksPanel.SetActive(false);
|
||||||
|
editorPanel.SetActive(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetMethodFilter() => methodFilterInput.Text = string.Empty;
|
public void ResetMethodFilter() => AddHooksMethodFilterInput.Text = string.Empty;
|
||||||
|
|
||||||
public override void ConstructPanelContent()
|
public override void ConstructPanelContent()
|
||||||
{
|
{
|
||||||
// Active hooks scroll pool
|
// ~~~~~~~~~ Active hooks scroll pool
|
||||||
|
|
||||||
currentHooksPanel = UIFactory.CreateUIObject("CurrentHooksPanel", this.content);
|
currentHooksPanel = UIFactory.CreateUIObject("CurrentHooksPanel", this.content);
|
||||||
UIFactory.SetLayoutElement(currentHooksPanel, flexibleHeight: 9999, flexibleWidth: 9999);
|
UIFactory.SetLayoutElement(currentHooksPanel, flexibleHeight: 9999, flexibleWidth: 9999);
|
||||||
@ -60,9 +91,9 @@ namespace UnityExplorer.UI.Panels
|
|||||||
new Vector4(2, 2, 2, 2), new Color(0.2f, 0.2f, 0.2f));
|
new Vector4(2, 2, 2, 2), new Color(0.2f, 0.2f, 0.2f));
|
||||||
UIFactory.SetLayoutElement(addRow, minHeight: 30, flexibleWidth: 9999);
|
UIFactory.SetLayoutElement(addRow, minHeight: 30, flexibleWidth: 9999);
|
||||||
|
|
||||||
classInputField = UIFactory.CreateInputField(addRow, "ClassInput", "Enter a class to add hooks to...");
|
classSelectorInputField = UIFactory.CreateInputField(addRow, "ClassInput", "Enter a class to add hooks to...");
|
||||||
UIFactory.SetLayoutElement(classInputField.Component.gameObject, flexibleWidth: 9999);
|
UIFactory.SetLayoutElement(classSelectorInputField.Component.gameObject, flexibleWidth: 9999);
|
||||||
new TypeCompleter(typeof(object), classInputField, false, false);
|
new TypeCompleter(typeof(object), classSelectorInputField, true, false);
|
||||||
|
|
||||||
var addButton = UIFactory.CreateButton(addRow, "AddButton", "Add Hooks");
|
var addButton = UIFactory.CreateButton(addRow, "AddButton", "Add Hooks");
|
||||||
UIFactory.SetLayoutElement(addButton.Component.gameObject, minWidth: 100, minHeight: 25);
|
UIFactory.SetLayoutElement(addButton.Component.gameObject, minWidth: 100, minHeight: 25);
|
||||||
@ -76,7 +107,7 @@ namespace UnityExplorer.UI.Panels
|
|||||||
UIFactory.SetLayoutElement(hooksScroll, flexibleHeight: 9999);
|
UIFactory.SetLayoutElement(hooksScroll, flexibleHeight: 9999);
|
||||||
HooksScrollPool.Initialize(HookManager.Instance);
|
HooksScrollPool.Initialize(HookManager.Instance);
|
||||||
|
|
||||||
// Add hooks panel
|
// ~~~~~~~~~ Add hooks panel
|
||||||
|
|
||||||
addHooksPanel = UIFactory.CreateUIObject("AddHooksPanel", this.content);
|
addHooksPanel = UIFactory.CreateUIObject("AddHooksPanel", this.content);
|
||||||
UIFactory.SetLayoutElement(addHooksPanel, flexibleHeight: 9999, flexibleWidth: 9999);
|
UIFactory.SetLayoutElement(addHooksPanel, flexibleHeight: 9999, flexibleWidth: 9999);
|
||||||
@ -90,22 +121,75 @@ namespace UnityExplorer.UI.Panels
|
|||||||
|
|
||||||
var doneButton = UIFactory.CreateButton(buttonRow, "DoneButton", "Done", new Color(0.2f, 0.3f, 0.2f));
|
var doneButton = UIFactory.CreateButton(buttonRow, "DoneButton", "Done", new Color(0.2f, 0.3f, 0.2f));
|
||||||
UIFactory.SetLayoutElement(doneButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999);
|
UIFactory.SetLayoutElement(doneButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999);
|
||||||
doneButton.OnClick += HookManager.Instance.CloseAddHooks;
|
doneButton.OnClick += HookManager.Instance.DoneAddingHooks;
|
||||||
|
|
||||||
var patchAllButton = UIFactory.CreateButton(buttonRow, "PatchAllButton", "Hook ALL methods in class", new Color(0.3f, 0.3f, 0.2f));
|
AddHooksMethodFilterInput = UIFactory.CreateInputField(addHooksPanel, "FilterInputField", "Filter method names...");
|
||||||
UIFactory.SetLayoutElement(patchAllButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999);
|
UIFactory.SetLayoutElement(AddHooksMethodFilterInput.Component.gameObject, minHeight: 30, flexibleWidth: 9999);
|
||||||
patchAllButton.OnClick += HookManager.Instance.OnHookAllClicked;
|
AddHooksMethodFilterInput.OnValueChanged += HookManager.Instance.OnAddHookFilterInputChanged;
|
||||||
|
|
||||||
methodFilterInput = UIFactory.CreateInputField(addHooksPanel, "FilterInputField", "Filter method names...");
|
AddHooksScrollPool = UIFactory.CreateScrollPool<AddHookCell>(addHooksPanel, "MethodAddScrollPool",
|
||||||
UIFactory.SetLayoutElement(methodFilterInput.Component.gameObject, minHeight: 30, flexibleWidth: 9999);
|
|
||||||
methodFilterInput.OnValueChanged += HookManager.Instance.OnAddHookFilterInputChanged;
|
|
||||||
|
|
||||||
MethodResultsScrollPool = UIFactory.CreateScrollPool<AddHookCell>(addHooksPanel, "MethodAddScrollPool",
|
|
||||||
out GameObject addScrollRoot, out GameObject addContent);
|
out GameObject addScrollRoot, out GameObject addContent);
|
||||||
UIFactory.SetLayoutElement(addScrollRoot, flexibleHeight: 9999);
|
UIFactory.SetLayoutElement(addScrollRoot, flexibleHeight: 9999);
|
||||||
MethodResultsScrollPool.Initialize(HookManager.Instance);
|
AddHooksScrollPool.Initialize(HookManager.Instance);
|
||||||
|
|
||||||
addHooksPanel.gameObject.SetActive(false);
|
addHooksPanel.gameObject.SetActive(false);
|
||||||
|
|
||||||
|
// ~~~~~~~~~ Hook source editor panel
|
||||||
|
|
||||||
|
editorPanel = UIFactory.CreateUIObject("HookSourceEditor", this.content);
|
||||||
|
UIFactory.SetLayoutElement(editorPanel, flexibleHeight: 9999, flexibleWidth: 9999);
|
||||||
|
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(editorPanel, true, true, true, true);
|
||||||
|
|
||||||
|
var editorLabel = UIFactory.CreateLabel(editorPanel,
|
||||||
|
"EditorLabel",
|
||||||
|
"Edit Harmony patch source as desired. Accepted method names are Prefix, Postfix, Finalizer and Transpiler (can define multiple).\n\n" +
|
||||||
|
"Hooks are temporary! Please copy the source into your IDE to avoid losing work if you wish to keep it!",
|
||||||
|
TextAnchor.MiddleLeft);
|
||||||
|
UIFactory.SetLayoutElement(editorLabel.gameObject, minHeight: 25, flexibleWidth: 9999);
|
||||||
|
|
||||||
|
var editorButtonRow = UIFactory.CreateHorizontalGroup(editorPanel, "ButtonRow", false, false, true, true, 5);
|
||||||
|
UIFactory.SetLayoutElement(editorButtonRow, minHeight: 25, flexibleWidth: 9999);
|
||||||
|
|
||||||
|
var editorSaveButton = UIFactory.CreateButton(editorButtonRow, "DoneButton", "Save and Return", new Color(0.2f, 0.3f, 0.2f));
|
||||||
|
UIFactory.SetLayoutElement(editorSaveButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999);
|
||||||
|
editorSaveButton.OnClick += HookManager.Instance.EditorInputSave;
|
||||||
|
|
||||||
|
var editorDoneButton = UIFactory.CreateButton(editorButtonRow, "DoneButton", "Cancel and Return", new Color(0.2f, 0.2f, 0.2f));
|
||||||
|
UIFactory.SetLayoutElement(editorDoneButton.Component.gameObject, minHeight: 25, flexibleWidth: 9999);
|
||||||
|
editorDoneButton.OnClick += HookManager.Instance.EditorInputCancel;
|
||||||
|
|
||||||
|
int fontSize = 16;
|
||||||
|
var inputObj = UIFactory.CreateScrollInputField(editorPanel, "EditorInput", "", out var inputScroller, fontSize);
|
||||||
|
EditorInputScroller = inputScroller;
|
||||||
|
EditorInput.OnValueChanged += HookManager.Instance.OnEditorInputChanged;
|
||||||
|
|
||||||
|
EditorInputText = EditorInput.Component.textComponent;
|
||||||
|
EditorInputText.supportRichText = false;
|
||||||
|
EditorInputText.color = Color.clear;
|
||||||
|
EditorInput.Component.customCaretColor = true;
|
||||||
|
EditorInput.Component.caretColor = Color.white;
|
||||||
|
EditorInput.PlaceholderText.fontSize = fontSize;
|
||||||
|
|
||||||
|
// Lexer highlight text overlay
|
||||||
|
var highlightTextObj = UIFactory.CreateUIObject("HighlightText", EditorInputText.gameObject);
|
||||||
|
var highlightTextRect = highlightTextObj.GetComponent<RectTransform>();
|
||||||
|
highlightTextRect.pivot = new Vector2(0, 1);
|
||||||
|
highlightTextRect.anchorMin = Vector2.zero;
|
||||||
|
highlightTextRect.anchorMax = Vector2.one;
|
||||||
|
highlightTextRect.offsetMin = Vector2.zero;
|
||||||
|
highlightTextRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
EditorHighlightText = highlightTextObj.AddComponent<Text>();
|
||||||
|
EditorHighlightText.color = Color.white;
|
||||||
|
EditorHighlightText.supportRichText = true;
|
||||||
|
EditorHighlightText.fontSize = fontSize;
|
||||||
|
|
||||||
|
// Set fonts
|
||||||
|
EditorInputText.font = UIManager.ConsoleFont;
|
||||||
|
EditorInput.PlaceholderText.font = UIManager.ConsoleFont;
|
||||||
|
EditorHighlightText.font = UIManager.ConsoleFont;
|
||||||
|
|
||||||
|
editorPanel.SetActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected internal override void DoSetDefaultPosAndAnchors()
|
protected internal override void DoSetDefaultPosAndAnchors()
|
||||||
@ -115,6 +199,5 @@ namespace UnityExplorer.UI.Panels
|
|||||||
this.Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, MinWidth);
|
this.Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, MinWidth);
|
||||||
this.Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, MinHeight);
|
this.Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, MinHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,8 +61,10 @@ namespace UnityExplorer.UI.Panels
|
|||||||
|
|
||||||
private void SetupIO()
|
private void SetupIO()
|
||||||
{
|
{
|
||||||
|
var fileName = $"UnityExplorer {DateTime.Now:u}.txt";
|
||||||
|
fileName = IOUtility.EnsureValidFilename(fileName);
|
||||||
var path = Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Logs");
|
var path = Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Logs");
|
||||||
path = IOUtility.EnsureValidDirectory(path);
|
CurrentStreamPath = IOUtility.EnsureValidFilePath(Path.Combine(path, fileName));
|
||||||
|
|
||||||
// clean old log(s)
|
// clean old log(s)
|
||||||
var files = Directory.GetFiles(path);
|
var files = Directory.GetFiles(path);
|
||||||
@ -75,11 +77,6 @@ namespace UnityExplorer.UI.Panels
|
|||||||
File.Delete(files[i]);
|
File.Delete(files[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileName = $"UnityExplorer {DateTime.Now:u}.txt";
|
|
||||||
fileName = IOUtility.EnsureValidFilename(fileName);
|
|
||||||
|
|
||||||
CurrentStreamPath = Path.Combine(path, fileName);
|
|
||||||
|
|
||||||
File.WriteAllLines(CurrentStreamPath, Logs.Select(it => it.message).ToArray());
|
File.WriteAllLines(CurrentStreamPath, Logs.Select(it => it.message).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
using System;
|
using HarmonyLib;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
@ -40,18 +43,15 @@ namespace UnityExplorer.UI
|
|||||||
|
|
||||||
public static bool Initializing { get; internal set; } = true;
|
public static bool Initializing { get; internal set; } = true;
|
||||||
|
|
||||||
private static readonly Dictionary<Panels, UIPanel> UIPanels = new Dictionary<Panels, UIPanel>();
|
|
||||||
|
|
||||||
public static VerticalAnchor NavbarAnchor = VerticalAnchor.Top;
|
public static VerticalAnchor NavbarAnchor = VerticalAnchor.Top;
|
||||||
|
|
||||||
// References
|
|
||||||
public static GameObject CanvasRoot { get; private set; }
|
public static GameObject CanvasRoot { get; private set; }
|
||||||
public static Canvas Canvas { get; private set; }
|
public static Canvas Canvas { get; private set; }
|
||||||
public static EventSystem EventSys { get; private set; }
|
public static EventSystem EventSys { get; private set; }
|
||||||
|
|
||||||
internal static GameObject PoolHolder { get; private set; }
|
internal static GameObject PoolHolder { get; private set; }
|
||||||
|
|
||||||
internal static GameObject PanelHolder { get; private set; }
|
internal static GameObject PanelHolder { get; private set; }
|
||||||
|
private static readonly Dictionary<Panels, UIPanel> UIPanels = new Dictionary<Panels, UIPanel>();
|
||||||
|
|
||||||
internal static Font ConsoleFont { get; private set; }
|
internal static Font ConsoleFont { get; private set; }
|
||||||
internal static Font DefaultFont { get; private set; }
|
internal static Font DefaultFont { get; private set; }
|
||||||
@ -196,15 +196,9 @@ namespace UnityExplorer.UI
|
|||||||
|
|
||||||
// Panels
|
// Panels
|
||||||
|
|
||||||
public static UIPanel GetPanel(Panels panel)
|
public static UIPanel GetPanel(Panels panel) => UIPanels[panel];
|
||||||
{
|
|
||||||
return UIPanels[panel];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T GetPanel<T>(Panels panel) where T : UIPanel
|
public static T GetPanel<T>(Panels panel) where T : UIPanel => (T)UIPanels[panel];
|
||||||
{
|
|
||||||
return (T)UIPanels[panel];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TogglePanel(Panels panel)
|
public static void TogglePanel(Panels panel)
|
||||||
{
|
{
|
||||||
@ -325,9 +319,14 @@ namespace UnityExplorer.UI
|
|||||||
CanvasRoot.layer = 5;
|
CanvasRoot.layer = 5;
|
||||||
CanvasRoot.transform.position = new Vector3(0f, 0f, 1f);
|
CanvasRoot.transform.position = new Vector3(0f, 0f, 1f);
|
||||||
|
|
||||||
|
CanvasRoot.SetActive(false);
|
||||||
|
|
||||||
EventSys = CanvasRoot.AddComponent<EventSystem>();
|
EventSys = CanvasRoot.AddComponent<EventSystem>();
|
||||||
InputManager.AddUIModule();
|
InputManager.AddUIModule();
|
||||||
|
|
||||||
|
EventSys.enabled = false;
|
||||||
|
CanvasRoot.SetActive(true);
|
||||||
|
|
||||||
Canvas = CanvasRoot.AddComponent<Canvas>();
|
Canvas = CanvasRoot.AddComponent<Canvas>();
|
||||||
Canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
Canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||||
Canvas.referencePixelsPerUnit = 100;
|
Canvas.referencePixelsPerUnit = 100;
|
||||||
@ -415,9 +414,12 @@ namespace UnityExplorer.UI
|
|||||||
|
|
||||||
// UI AssetBundle
|
// UI AssetBundle
|
||||||
|
|
||||||
|
internal static AssetBundle ExplorerBundle;
|
||||||
|
|
||||||
private static void LoadBundle()
|
private static void LoadBundle()
|
||||||
{
|
{
|
||||||
AssetBundle bundle;
|
SetupAssetBundlePatches();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get the Major and Minor of the Unity version
|
// Get the Major and Minor of the Unity version
|
||||||
@ -428,18 +430,18 @@ namespace UnityExplorer.UI
|
|||||||
// Use appropriate AssetBundle for Unity version
|
// Use appropriate AssetBundle for Unity version
|
||||||
// >= 2017
|
// >= 2017
|
||||||
if (major >= 2017)
|
if (major >= 2017)
|
||||||
bundle = LoadBundle("modern");
|
ExplorerBundle = LoadBundle("modern");
|
||||||
// 5.6.0 to <2017
|
// 5.6.0 to <2017
|
||||||
else if (major == 5 && minor >= 6)
|
else if (major == 5 && minor >= 6)
|
||||||
bundle = LoadBundle("legacy.5.6");
|
ExplorerBundle = LoadBundle("legacy.5.6");
|
||||||
// < 5.6.0
|
// < 5.6.0
|
||||||
else
|
else
|
||||||
bundle = LoadBundle("legacy");
|
ExplorerBundle = LoadBundle("legacy");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
ExplorerCore.LogWarning($"Exception parsing Unity version, falling back to old AssetBundle load method...");
|
ExplorerCore.LogWarning($"Exception parsing Unity version, falling back to old AssetBundle load method...");
|
||||||
bundle = LoadBundle("modern") ?? LoadBundle("legacy.5.6") ?? LoadBundle("legacy");
|
ExplorerBundle = LoadBundle("modern") ?? LoadBundle("legacy.5.6") ?? LoadBundle("legacy");
|
||||||
}
|
}
|
||||||
|
|
||||||
AssetBundle LoadBundle(string id)
|
AssetBundle LoadBundle(string id)
|
||||||
@ -451,19 +453,26 @@ namespace UnityExplorer.UI
|
|||||||
.GetManifestResourceStream($"UnityExplorer.Resources.{id}.bundle")));
|
.GetManifestResourceStream($"UnityExplorer.Resources.{id}.bundle")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bundle == null)
|
if (ExplorerBundle == null)
|
||||||
{
|
{
|
||||||
ExplorerCore.LogWarning("Could not load the ExplorerUI Bundle!");
|
ExplorerCore.LogWarning("Could not load the ExplorerUI Bundle!");
|
||||||
ConsoleFont = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
DefaultFont = ConsoleFont = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||||
DefaultFont = ConsoleFont;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bundle loaded
|
// Bundle loaded
|
||||||
ConsoleFont = bundle.LoadAsset<Font>("CONSOLA");
|
|
||||||
DefaultFont = bundle.LoadAsset<Font>("arial");
|
|
||||||
|
|
||||||
BackupShader = bundle.LoadAsset<Shader>("DefaultUI");
|
ConsoleFont = ExplorerBundle.LoadAsset<Font>("CONSOLA");
|
||||||
|
ConsoleFont.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(ConsoleFont);
|
||||||
|
|
||||||
|
DefaultFont = ExplorerBundle.LoadAsset<Font>("arial");
|
||||||
|
DefaultFont.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(DefaultFont);
|
||||||
|
|
||||||
|
BackupShader = ExplorerBundle.LoadAsset<Shader>("DefaultUI");
|
||||||
|
BackupShader.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(BackupShader);
|
||||||
// Fix for games which don't ship with 'UI/Default' shader.
|
// Fix for games which don't ship with 'UI/Default' shader.
|
||||||
if (Graphic.defaultGraphicMaterial.shader?.name != "UI/Default")
|
if (Graphic.defaultGraphicMaterial.shader?.name != "UI/Default")
|
||||||
{
|
{
|
||||||
@ -472,7 +481,6 @@ namespace UnityExplorer.UI
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
BackupShader = Graphic.defaultGraphicMaterial.shader;
|
BackupShader = Graphic.defaultGraphicMaterial.shader;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] ReadFully(Stream input)
|
private static byte[] ReadFully(Stream input)
|
||||||
@ -486,5 +494,52 @@ namespace UnityExplorer.UI
|
|||||||
return ms.ToArray();
|
return ms.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssetBundle patch
|
||||||
|
|
||||||
|
private static Type TypeofAssetBundle => ReflectionUtility.GetTypeByName("UnityEngine.AssetBundle");
|
||||||
|
|
||||||
|
private static void SetupAssetBundlePatches()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (TypeofAssetBundle.GetMethod("UnloadAllAssetBundles", AccessTools.all) is MethodInfo unloadAllBundles)
|
||||||
|
{
|
||||||
|
var processor = ExplorerCore.Harmony.CreateProcessor(unloadAllBundles);
|
||||||
|
var prefix = new HarmonyMethod(typeof(UIManager).GetMethod(nameof(Prefix_UnloadAllAssetBundles), AccessTools.all));
|
||||||
|
processor.AddPrefix(prefix);
|
||||||
|
processor.Patch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExplorerCore.LogWarning($"Exception setting up AssetBundle.UnloadAllAssetBundles patch: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool Prefix_UnloadAllAssetBundles(bool unloadAllObjects)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = typeof(AssetBundle).GetMethod("GetAllLoadedAssetBundles", AccessTools.all);
|
||||||
|
if (method == null)
|
||||||
|
return true;
|
||||||
|
var bundles = method.Invoke(null, ArgumentUtility.EmptyArgs) as AssetBundle[];
|
||||||
|
foreach (var obj in bundles)
|
||||||
|
{
|
||||||
|
if (obj.m_CachedPtr == ExplorerBundle.m_CachedPtr)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
obj.Unload(unloadAllObjects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ExplorerCore.LogWarning($"Exception unloading AssetBundles: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user