Compare commits

..

20 Commits
4.7.2 ... 4.7.5

Author SHA1 Message Date
08f2c6035e Bump version 2022-04-20 18:47:23 +10:00
475e24a66a Update editor package 2022-04-20 18:47:19 +10:00
c62b93535d Update position input when Reset button clicked 2022-04-20 18:47:12 +10:00
374d0b3bae Bump UniverseLib 2022-04-20 18:46:48 +10:00
1f87e89b97 Add some more functionality to Freecam
- Option to use Game's camera
- Button to reset position back to original cam pos
- Free cam position will be stored between stopping/starting free cam
2022-04-19 19:37:05 +10:00
495bc41a8d Bump Editor package 2022-04-19 18:06:16 +10:00
9cf62c3250 Bump UniverseLib, fix EventSystem issue 2022-04-19 18:05:55 +10:00
4c9b3115cd Update UnityEditorPackage 2022-04-19 01:23:11 +10:00
ebdce70418 Move instructions 2022-04-19 01:22:25 +10:00
7c0b37440b Update UnityEditorPackage 2022-04-19 01:19:04 +10:00
0d8ab8bf14 Allow FreeCam to run when menu closed 2022-04-19 01:12:33 +10:00
5a3cad9be2 Fix weird Behaviour inheritance issue 2022-04-19 00:57:13 +10:00
70d67f1dad Bump version 2022-04-18 21:39:50 +10:00
469484d129 Add FreeCam panel 2022-04-18 21:39:17 +10:00
c5e262d1c3 Fix UEPanel save data issue 2022-04-18 21:39:11 +10:00
53c8dfcb6d Bump UniverseLib 2022-04-18 21:38:49 +10:00
ae3ac21992 Add test for Il2CppSystem.Array 2022-04-18 19:12:42 +10:00
7765b64748 Make MouseInspector panel just a PanelBase 2022-04-18 19:12:21 +10:00
a5023d03f4 Update to UniverseLib 1.3.4 2022-04-18 19:11:39 +10:00
a5e6b65dee Add UniverseLib to C# Console default usings 2022-04-18 19:10:24 +10:00
18 changed files with 459 additions and 727 deletions

View File

@ -1,6 +1,6 @@
{
"name": "com.sinai-dev.unityexplorer",
"version": "4.7.0",
"version": "4.7.3",
"displayName": "UnityExplorer",
"description": "An in-game UI for exploring, debugging and modifying Unity games.",
"unity": "2017.1",

View File

@ -52,6 +52,7 @@ namespace UnityExplorer.CSConsole
"System.Collections",
"System.Collections.Generic",
"UnityEngine",
"UniverseLib",
#if CPP
"UnhollowerBaseLib",
"UnhollowerRuntimeLib",
@ -60,8 +61,6 @@ namespace UnityExplorer.CSConsole
public static void Init()
{
InitEventSystemPropertyHandlers();
// Make sure console is supported on this platform
try
{
@ -385,77 +384,17 @@ namespace UnityExplorer.CSConsole
RuntimeHelper.StartCoroutine(SetCaretCoroutine(caretPosition));
}
static void InitEventSystemPropertyHandlers()
{
try
{
foreach (MemberInfo member in typeof(EventSystem).GetMembers(AccessTools.all))
{
if (member.Name == "m_CurrentSelected")
{
Type backingType;
if (member.MemberType == MemberTypes.Property)
backingType = (member as PropertyInfo).PropertyType;
else
backingType = (member as FieldInfo).FieldType;
usingEventSystemDictionaryMembers = ReflectionUtility.IsDictionary(backingType);
break;
}
}
}
catch (Exception ex)
{
ExplorerCore.LogWarning($"Exception checking EventSystem property backing type: {ex}");
}
}
static bool usingEventSystemDictionaryMembers;
static readonly AmbiguousMemberHandler<EventSystem, GameObject> m_CurrentSelected_Handler_Normal
= new(true, true, "m_CurrentSelected", "m_currentSelected");
static readonly AmbiguousMemberHandler<EventSystem, Dictionary<int, GameObject>> m_CurrentSelected_Handler_Dictionary
= new(true, true, "m_CurrentSelected", "m_currentSelected");
static readonly AmbiguousMemberHandler<EventSystem, bool> m_SelectionGuard_Handler_Normal
= new(true, true, "m_SelectionGuard", "m_selectionGuard");
static readonly AmbiguousMemberHandler<EventSystem, Dictionary<int, bool>> m_SelectionGuard_Handler_Dictionary
= new(true, true, "m_SelectionGuard", "m_selectionGuard");
static void SetCurrentSelectedGameObject(EventSystem instance, GameObject value)
{
instance.SetSelectedGameObject(value);
if (usingEventSystemDictionaryMembers)
m_CurrentSelected_Handler_Dictionary.GetValue(instance)[0] = value;
else
m_CurrentSelected_Handler_Normal.SetValue(instance, value);
}
static void SetSelectionGuard(EventSystem instance, bool value)
{
if (usingEventSystemDictionaryMembers)
m_SelectionGuard_Handler_Dictionary.GetValue(instance)[0] = value;
else
m_SelectionGuard_Handler_Normal.SetValue(instance, value);
}
private static IEnumerator SetCaretCoroutine(int caretPosition)
{
Color color = Input.Component.selectionColor;
color.a = 0f;
Input.Component.selectionColor = color;
try { SetCurrentSelectedGameObject(CursorUnlocker.CurrentEventSystem, null); }
catch (Exception ex) { ExplorerCore.Log($"Failed removing selected object: {ex}"); }
EventSystemHelper.SetSelectedGameObject(null);
yield return null; // ~~~~~~~ YIELD FRAME ~~~~~~~~~
try { SetSelectionGuard(CursorUnlocker.CurrentEventSystem, false); }
catch (Exception ex) { ExplorerCore.Log($"Failed setting selection guard: {ex}"); }
try { SetCurrentSelectedGameObject(CursorUnlocker.CurrentEventSystem, Input.GameObject); }
catch (Exception ex) { ExplorerCore.Log($"Failed setting selected gameobject: {ex}"); }
EventSystemHelper.SetSelectedGameObject(null);
yield return null; // ~~~~~~~ YIELD FRAME ~~~~~~~~~
@ -704,7 +643,7 @@ Doorstop example:
// It is recommended to use the Log panel (or a console log window) while using this tool.
// Use the Help dropdown to see detailed examples of how to use the console.
// To execute a script automatically on startup, put the script at 'UnityExplorer\Scripts\startup.cs'</color>";
// To execute a script automatically on startup, put the script at 'sinai-dev-UnityExplorer\Scripts\startup.cs'</color>";
internal const string HELP_USINGS = @"// You can add a using directive to any namespace, but you must compile for it to take effect.
// It will remain in effect until you Reset the console.

View File

@ -48,6 +48,9 @@ namespace UnityExplorer.Config
TomlDocument document = TomlParser.ParseFile(CONFIG_PATH);
foreach (string key in document.Keys)
{
if (!Enum.IsDefined(typeof(UIManager.Panels), key))
continue;
UIManager.Panels panelKey = (UIManager.Panels)Enum.Parse(typeof(UIManager.Panels), key);
ConfigManager.GetPanelSaveData(panelKey).Value = document.GetString(key);
}

View File

@ -14,7 +14,7 @@ namespace UnityExplorer
public static class ExplorerCore
{
public const string NAME = "UnityExplorer";
public const string VERSION = "4.7.2";
public const string VERSION = "4.7.5";
public const string AUTHOR = "Sinai";
public const string GUID = "com.sinai.unityexplorer";

View File

@ -74,7 +74,7 @@ namespace UnityExplorer.Inspectors
InspectorManager.ReleaseInspector(this);
}
public void ChangeTarget(GameObject newTarget)
public void OnTransformCellClicked(GameObject newTarget)
{
this.Target = newTarget;
GOControls.UpdateGameObjectInfo(true, true);
@ -193,7 +193,8 @@ namespace UnityExplorer.Inspectors
compInstanceIDs.Clear();
foreach (Component comp in comps)
{
if (!comp) continue;
if (!comp)
continue;
componentEntries.Add(comp);
compInstanceIDs.Add(comp.GetInstanceID());
}
@ -202,8 +203,23 @@ namespace UnityExplorer.Inspectors
behaviourEnabledStates.Clear();
foreach (Behaviour behaviour in behaviours)
{
if (!behaviour) continue;
behaviourEntries.Add(behaviour);
if (!behaviour)
continue;
// Don't ask me how, but in some games this can be true for certain components.
// They get picked up from GetComponents<Behaviour>, but they are not actually Behaviour...?
if (!typeof(Behaviour).IsAssignableFrom(behaviour.GetType()))
continue;
try
{
behaviourEntries.Add(behaviour);
}
catch (Exception ex)
{
ExplorerCore.LogWarning(ex);
}
behaviourEnabledStates.Add(behaviour.enabled);
}
@ -293,12 +309,8 @@ namespace UnityExplorer.Inspectors
transformScroll = UIFactory.CreateScrollPool<TransformCell>(leftGroup, "TransformTree", out GameObject transformObj,
out GameObject transformContent, new Color(0.11f, 0.11f, 0.11f));
UIFactory.SetLayoutElement(transformObj, flexibleHeight: 9999);
UIFactory.SetLayoutElement(transformContent, flexibleHeight: 9999);
TransformTree = new TransformTree(transformScroll, GetTransformEntries);
TransformTree.Init();
TransformTree.OnClickOverrideHandler = ChangeTarget;
TransformTree = new TransformTree(transformScroll, GetTransformEntries, OnTransformCellClicked);
// Right group (Components)

View File

@ -148,7 +148,7 @@ namespace UnityExplorer.Inspectors
{
if (this.GOTarget && this.GOTarget.transform.parent)
{
Parent.ChangeTarget(this.GOTarget.transform.parent.gameObject);
Parent.OnTransformCellClicked(this.GOTarget.transform.parent.gameObject);
}
}

View File

@ -17,7 +17,7 @@ namespace UnityExplorer.Inspectors
UI
}
public class MouseInspector : UEPanel
public class MouseInspector : PanelBase
{
public static MouseInspector Instance { get; private set; }
@ -38,19 +38,15 @@ namespace UnityExplorer.Inspectors
// UIPanel
internal static readonly string UIBaseGUID = $"{ExplorerCore.GUID}.MouseInspector";
private UIBase inspectorUIBase;
internal static UIBase inspectorUIBase;
public override string Name => "Inspect Under Mouse";
public override UIManager.Panels PanelType => UIManager.Panels.MouseInspector;
public override int MinWidth => -1;
public override int MinHeight => -1;
public override Vector2 DefaultAnchorMin => Vector2.zero;
public override Vector2 DefaultAnchorMax => Vector2.zero;
public override bool CanDragAndResize => false;
public override bool NavButtonWanted => false;
public override bool ShouldSaveActiveState => false;
public override bool ShowByDefault => false;
internal Text objNameLabel;
internal Text objPathLabel;
@ -225,11 +221,10 @@ namespace UnityExplorer.Inspectors
UIRoot.SetActive(false);
// Create a new canvas for this panel to live on.
// It needs to always be shown on the main display, other panels can move displays.
inspectorUIBase = UniversalUI.RegisterUI(UIBaseGUID, null);
UIRoot.transform.SetParent(inspectorUIBase.RootObject.transform);
//// Create a new canvas for this panel to live on.
//// It needs to always be shown on the main display, other panels can move displays.
//
//UIRoot.transform.SetParent(inspectorUIBase.RootObject.transform);
}
}
}

View File

@ -13,6 +13,7 @@ using UniverseLib;
using UniverseLib.UI;
using UniverseLib.UI.Models;
using UniverseLib.Utility;
using UniverseLib.UI.Widgets;
namespace UnityExplorer.ObjectExplorer
{
@ -156,7 +157,7 @@ namespace UnityExplorer.ObjectExplorer
{
if ((!string.IsNullOrEmpty(input) && !Tree.Filtering) || (string.IsNullOrEmpty(input) && Tree.Filtering))
{
Tree.cachedTransforms.Clear();
Tree.Clear();
}
Tree.CurrentFilter = input;
@ -259,8 +260,7 @@ namespace UnityExplorer.ObjectExplorer
UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999);
UIFactory.SetLayoutElement(scrollContent, flexibleHeight: 9999);
Tree = new TransformTree(scrollPool, GetRootEntries);
Tree.Init();
Tree = new TransformTree(scrollPool, GetRootEntries, OnCellClicked);
Tree.RefreshData(true, true, true, false);
//scrollPool.Viewport.GetComponent<Mask>().enabled = false;
//UIRoot.GetComponent<Mask>().enabled = false;
@ -272,6 +272,8 @@ namespace UnityExplorer.ObjectExplorer
RuntimeHelper.StartCoroutine(TempFixCoro());
}
void OnCellClicked(GameObject obj) => InspectorManager.Inspect(obj);
// To "fix" a strange FPS drop issue with MelonLoader.
private IEnumerator TempFixCoro()
{

View File

@ -174,6 +174,7 @@ namespace UnityExplorer.Tests
public static Il2CppSystem.Collections.IDictionary IL2CPP_IDict;
public static Il2CppSystem.Collections.IList IL2CPP_IList;
public static Dictionary<Il2CppSystem.Object, Il2CppSystem.Object> IL2CPP_BoxedDict;
public static Il2CppSystem.Array IL2CPP_NonGenericArray;
public static Il2CppSystem.Object IL2CPP_BoxedInt;
public static Il2CppSystem.Int32 IL2CPP_Int;
@ -187,6 +188,9 @@ namespace UnityExplorer.Tests
private static void Init_IL2CPP()
{
ExplorerCore.Log("IL2CPP 0: Non-generic array");
IL2CPP_NonGenericArray = new Il2CppStructArray<int>(5).TryCast<Il2CppSystem.Array>();
ExplorerCore.Log($"IL2CPP 1: Il2Cpp Dictionary<string, string>");
IL2CPP_Dict = new Il2CppSystem.Collections.Generic.Dictionary<string, string>();
IL2CPP_Dict.Add("key1", "value1");

View File

@ -0,0 +1,394 @@
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UniverseLib;
using UniverseLib.Input;
using UniverseLib.UI;
using UniverseLib.UI.Models;
using UniverseLib.Utility;
namespace UnityExplorer.UI.Panels
{
internal class FreeCamPanel : UEPanel
{
public FreeCamPanel(UIBase owner) : base(owner)
{
}
public override string Name => "Freecam";
public override UIManager.Panels PanelType => UIManager.Panels.Freecam;
public override int MinWidth => 400;
public override int MinHeight => 320;
public override Vector2 DefaultAnchorMin => new(0.4f, 0.4f);
public override Vector2 DefaultAnchorMax => new(0.6f, 0.6f);
public override bool NavButtonWanted => true;
public override bool ShouldSaveActiveState => true;
internal static bool inFreeCamMode;
internal static bool usingGameCamera;
internal static Camera ourCamera;
internal static Camera lastMainCamera;
internal static FreeCamBehaviour freeCamScript;
internal static float desiredMoveSpeed = 10f;
internal static Vector3 originalCameraPosition;
internal static Quaternion originalCameraRotation;
internal static Vector3? currentUserCameraPosition;
internal static Quaternion? currentUserCameraRotation;
internal static Vector3 previousMousePosition;
internal static Vector3 lastSetCameraPosition;
static ButtonRef startStopButton;
static Toggle useGameCameraToggle;
static InputFieldRef positionInput;
static InputFieldRef moveSpeedInput;
static ButtonRef inspectButton;
internal static void BeginFreecam()
{
inFreeCamMode = true;
previousMousePosition = InputManager.MousePosition;
CacheMainCamera();
SetupFreeCamera();
inspectButton.GameObject.SetActive(true);
}
static void CacheMainCamera()
{
Camera currentMain = Camera.main;
if (currentMain)
{
lastMainCamera = currentMain;
originalCameraPosition = currentMain.transform.position;
originalCameraRotation = currentMain.transform.rotation;
if (currentUserCameraPosition == null)
{
currentUserCameraPosition = currentMain.transform.position;
currentUserCameraRotation = currentMain.transform.rotation;
}
}
else
originalCameraRotation = Quaternion.identity;
}
static void SetupFreeCamera()
{
if (useGameCameraToggle.isOn)
{
if (!lastMainCamera)
{
ExplorerCore.LogWarning($"There is no previous Camera found, reverting to default Free Cam.");
useGameCameraToggle.isOn = false;
}
else
{
usingGameCamera = true;
ourCamera = lastMainCamera;
}
}
if (!useGameCameraToggle.isOn)
{
usingGameCamera = false;
if (lastMainCamera)
lastMainCamera.enabled = false;
}
if (!ourCamera)
{
ourCamera = new GameObject("UE_Freecam").AddComponent<Camera>();
ourCamera.gameObject.tag = "MainCamera";
GameObject.DontDestroyOnLoad(ourCamera.gameObject);
ourCamera.gameObject.hideFlags = HideFlags.HideAndDontSave;
}
if (!freeCamScript)
freeCamScript = ourCamera.gameObject.AddComponent<FreeCamBehaviour>();
ourCamera.transform.position = (Vector3)currentUserCameraPosition;
ourCamera.transform.rotation = (Quaternion)currentUserCameraRotation;
ourCamera.gameObject.SetActive(true);
ourCamera.enabled = true;
}
internal static void EndFreecam()
{
inFreeCamMode = false;
if (usingGameCamera)
{
ourCamera = null;
if (lastMainCamera)
{
lastMainCamera.transform.position = originalCameraPosition;
lastMainCamera.transform.rotation = originalCameraRotation;
}
}
if (ourCamera)
ourCamera.gameObject.SetActive(false);
else
inspectButton.GameObject.SetActive(false);
if (freeCamScript)
{
GameObject.Destroy(freeCamScript);
freeCamScript = null;
}
if (lastMainCamera)
lastMainCamera.enabled = true;
}
static void SetCameraPosition(Vector3 pos)
{
if (!ourCamera || lastSetCameraPosition == pos)
return;
ourCamera.transform.position = pos;
lastSetCameraPosition = pos;
}
internal static void UpdatePositionInput()
{
if (!ourCamera)
return;
lastSetCameraPosition = ourCamera.transform.position;
positionInput.Text = ParseUtility.ToStringForInput<Vector3>(lastSetCameraPosition);
}
// ~~~~~~~~ UI construction / callbacks ~~~~~~~~
protected override void ConstructPanelContent()
{
startStopButton = UIFactory.CreateButton(ContentRoot, "ToggleButton", "Freecam");
UIFactory.SetLayoutElement(startStopButton.GameObject, minWidth: 150, minHeight: 25, flexibleWidth: 9999);
startStopButton.OnClick += StartStopButton_OnClick;
SetToggleButtonState();
AddSpacer(5);
GameObject toggleObj = UIFactory.CreateToggle(ContentRoot, "UseGameCameraToggle", out useGameCameraToggle, out Text toggleText);
UIFactory.SetLayoutElement(toggleObj, minHeight: 25, flexibleWidth: 9999);
useGameCameraToggle.onValueChanged.AddListener(OnUseGameCameraToggled);
useGameCameraToggle.isOn = false;
toggleText.text = "Use Game Camera?";
AddSpacer(5);
GameObject posRow = AddInputField("Position", "Freecam Pos:", "eg. 0 0 0", out positionInput, PositionInput_OnEndEdit);
ButtonRef resetPosButton = UIFactory.CreateButton(posRow, "ResetButton", "Reset");
UIFactory.SetLayoutElement(resetPosButton.GameObject, minWidth: 70, minHeight: 25);
resetPosButton.OnClick += OnResetPosButtonClicked;
AddSpacer(5);
AddInputField("MoveSpeed", "Move Speed:", "Default: 1", out moveSpeedInput, MoveSpeedInput_OnEndEdit);
moveSpeedInput.Text = desiredMoveSpeed.ToString();
AddSpacer(5);
string instructions = @"Controls:
- WASD / Arrows: Movement
- Space / PgUp: Move up
- LeftCtrl / PgDown: Move down
- Right Mouse Button: Free look
- Shift: Super speed";
Text instructionsText = UIFactory.CreateLabel(ContentRoot, "Instructions", instructions, TextAnchor.UpperLeft);
UIFactory.SetLayoutElement(instructionsText.gameObject, flexibleWidth: 9999, flexibleHeight: 9999);
AddSpacer(5);
inspectButton = UIFactory.CreateButton(ContentRoot, "InspectButton", "Inspect Free Camera");
UIFactory.SetLayoutElement(inspectButton.GameObject, flexibleWidth: 9999, minHeight: 25);
inspectButton.OnClick += () => { InspectorManager.Inspect(ourCamera); };
inspectButton.GameObject.SetActive(false);
AddSpacer(5);
}
void AddSpacer(int height)
{
GameObject obj = UIFactory.CreateUIObject("Spacer", ContentRoot);
UIFactory.SetLayoutElement(obj, minHeight: height, flexibleHeight: 0);
}
GameObject AddInputField(string name, string labelText, string placeHolder, out InputFieldRef inputField, Action<string> onInputEndEdit)
{
GameObject row = UIFactory.CreateHorizontalGroup(ContentRoot, $"{name}_Group", false, false, true, true, 3, default, new(1, 1, 1, 0));
Text posLabel = UIFactory.CreateLabel(row, $"{name}_Label", labelText);
UIFactory.SetLayoutElement(posLabel.gameObject, minWidth: 100, minHeight: 25);
inputField = UIFactory.CreateInputField(row, $"{name}_Input", placeHolder);
UIFactory.SetLayoutElement(inputField.GameObject, minWidth: 125, minHeight: 25, flexibleWidth: 9999);
inputField.Component.GetOnEndEdit().AddListener(onInputEndEdit);
return row;
}
void StartStopButton_OnClick()
{
EventSystemHelper.SetSelectedGameObject(null);
if (inFreeCamMode)
EndFreecam();
else
BeginFreecam();
SetToggleButtonState();
}
void SetToggleButtonState()
{
if (inFreeCamMode)
{
RuntimeHelper.SetColorBlockAuto(startStopButton.Component, new(0.4f, 0.2f, 0.2f));
startStopButton.ButtonText.text = "End Freecam";
}
else
{
RuntimeHelper.SetColorBlockAuto(startStopButton.Component, new(0.2f, 0.4f, 0.2f));
startStopButton.ButtonText.text = "Begin Freecam";
}
}
void OnUseGameCameraToggled(bool value)
{
EventSystemHelper.SetSelectedGameObject(null);
if (!inFreeCamMode)
return;
EndFreecam();
BeginFreecam();
}
void OnResetPosButtonClicked()
{
currentUserCameraPosition = originalCameraPosition;
currentUserCameraRotation = originalCameraRotation;
if (inFreeCamMode && ourCamera)
{
ourCamera.transform.position = (Vector3)currentUserCameraPosition;
ourCamera.transform.rotation = (Quaternion)currentUserCameraRotation;
}
positionInput.Text = ParseUtility.ToStringForInput<Vector3>(originalCameraPosition);
}
void PositionInput_OnEndEdit(string input)
{
EventSystemHelper.SetSelectedGameObject(null);
if (!ParseUtility.TryParse(input, out Vector3 parsed, out Exception parseEx))
{
ExplorerCore.LogWarning($"Could not parse position to Vector3: {parseEx.ReflectionExToString()}");
UpdatePositionInput();
return;
}
SetCameraPosition(parsed);
}
void MoveSpeedInput_OnEndEdit(string input)
{
EventSystemHelper.SetSelectedGameObject(null);
if (!ParseUtility.TryParse(input, out float parsed, out Exception parseEx))
{
ExplorerCore.LogWarning($"Could not parse value: {parseEx.ReflectionExToString()}");
moveSpeedInput.Text = desiredMoveSpeed.ToString();
return;
}
desiredMoveSpeed = parsed;
}
}
internal class FreeCamBehaviour : MonoBehaviour
{
#if CPP
static FreeCamBehaviour()
{
UnhollowerRuntimeLib.ClassInjector.RegisterTypeInIl2Cpp<FreeCamBehaviour>();
}
public FreeCamBehaviour(IntPtr ptr) : base(ptr) { }
#endif
internal void Update()
{
if (FreeCamPanel.inFreeCamMode)
{
if (!FreeCamPanel.ourCamera)
{
FreeCamPanel.EndFreecam();
return;
}
Transform transform = FreeCamPanel.ourCamera.transform;
FreeCamPanel.currentUserCameraPosition = transform.position;
FreeCamPanel.currentUserCameraRotation = transform.rotation;
float moveSpeed = FreeCamPanel.desiredMoveSpeed * Time.deltaTime;
if (InputManager.GetKey(KeyCode.LeftShift) || InputManager.GetKey(KeyCode.RightShift))
moveSpeed *= 10f;
if (InputManager.GetKey(KeyCode.LeftArrow) || InputManager.GetKey(KeyCode.A))
transform.position += transform.right * -1 * moveSpeed;
if (InputManager.GetKey(KeyCode.RightArrow) || InputManager.GetKey(KeyCode.D))
transform.position += transform.right * moveSpeed;
if (InputManager.GetKey(KeyCode.UpArrow) || InputManager.GetKey(KeyCode.W))
transform.position += transform.forward * moveSpeed;
if (InputManager.GetKey(KeyCode.DownArrow) || InputManager.GetKey(KeyCode.S))
transform.position += transform.forward * -1 * moveSpeed;
if (InputManager.GetKey(KeyCode.Space) || InputManager.GetKey(KeyCode.PageUp))
transform.position += transform.up * moveSpeed;
if (InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.PageDown))
transform.position += transform.up * -1 * moveSpeed;
if (InputManager.GetMouseButton(1))
{
Vector3 mouseDelta = InputManager.MousePosition - FreeCamPanel.previousMousePosition;
float newRotationX = transform.localEulerAngles.y + mouseDelta.x * 0.3f;
float newRotationY = transform.localEulerAngles.x - mouseDelta.y * 0.3f;
transform.localEulerAngles = new Vector3(newRotationY, newRotationX, 0f);
}
FreeCamPanel.UpdatePositionInput();
FreeCamPanel.previousMousePosition = InputManager.MousePosition;
}
}
}
}

View File

@ -75,13 +75,14 @@ namespace UnityExplorer.UI.Panels
public void SaveInternalData()
{
if (UIManager.Initializing)
if (UIManager.Initializing || ApplyingSaveData)
return;
SetSaveDataToConfigValue();
}
private void SetSaveDataToConfigValue() => ConfigManager.GetPanelSaveData(this.PanelType).Value = this.ToSaveData();
private void SetSaveDataToConfigValue()
=> ConfigManager.GetPanelSaveData(this.PanelType).Value = this.ToSaveData();
public virtual string ToSaveData()
{
@ -156,9 +157,10 @@ namespace UnityExplorer.UI.Panels
protected override void LateConstructUI()
{
ApplyingSaveData = true;
base.LateConstructUI();
ApplyingSaveData = true;
// apply panel save data or revert to default
try
{
@ -177,6 +179,8 @@ namespace UnityExplorer.UI.Panels
};
ApplyingSaveData = false;
Dragger.OnEndResize();
}
}

View File

@ -26,10 +26,11 @@ namespace UnityExplorer.UI
Options,
ConsoleLog,
AutoCompleter,
MouseInspector,
//MouseInspector,
UIInspectorResults,
HookManager,
Clipboard
Clipboard,
Freecam
}
public enum VerticalAnchor
@ -99,11 +100,14 @@ namespace UnityExplorer.UI
UIPanels.Add(Panels.Inspector, new InspectorPanel(UiBase));
UIPanels.Add(Panels.CSConsole, new CSConsolePanel(UiBase));
UIPanels.Add(Panels.HookManager, new HookManagerPanel(UiBase));
UIPanels.Add(Panels.Freecam, new FreeCamPanel(UiBase));
UIPanels.Add(Panels.Clipboard, new ClipboardPanel(UiBase));
UIPanels.Add(Panels.ConsoleLog, new LogPanel(UiBase));
UIPanels.Add(Panels.Options, new OptionsPanel(UiBase));
UIPanels.Add(Panels.UIInspectorResults, new MouseInspectorResultsPanel(UiBase));
UIPanels.Add(Panels.MouseInspector, new MouseInspector(UiBase));
MouseInspector.inspectorUIBase = UniversalUI.RegisterUI(MouseInspector.UIBaseGUID, null);
new MouseInspector(MouseInspector.inspectorUIBase);
// Call some initialize methods
Notification.Init();
@ -285,9 +289,9 @@ namespace UnityExplorer.UI
// UnityExplorer title
string titleTxt = $"{ExplorerCore.NAME} <i><color=grey>{ExplorerCore.VERSION}</color></i>";
Text title = UIFactory.CreateLabel(navbarPanel, "Title", titleTxt, TextAnchor.MiddleLeft, default, true, 17);
UIFactory.SetLayoutElement(title.gameObject, minWidth: 170, flexibleWidth: 0);
string titleTxt = $"UE <i><color=grey>{ExplorerCore.VERSION}</color></i>";
Text title = UIFactory.CreateLabel(navbarPanel, "Title", titleTxt, TextAnchor.MiddleCenter, default, true, 14);
UIFactory.SetLayoutElement(title.gameObject, minWidth: 75, flexibleWidth: 0);
// panel tabs

View File

@ -1,55 +0,0 @@
using UnityEngine;
namespace UnityExplorer.UI.Widgets
{
public class CachedTransform
{
public TransformTree Tree { get; }
public Transform Value { get; private set; }
public int InstanceID { get; }
public CachedTransform Parent { get; internal set; }
public int Depth { get; internal set; }
public int ChildCount { get; internal set; }
public string Name { get; internal set; }
public bool Enabled { get; internal set; }
public int SiblingIndex { get; internal set; }
public bool Expanded => Tree.IsTransformExpanded(InstanceID);
public CachedTransform(TransformTree tree, Transform transform, int depth, CachedTransform parent = null)
{
InstanceID = transform.GetInstanceID();
Tree = tree;
Value = transform;
Parent = parent;
SiblingIndex = transform.GetSiblingIndex();
Update(transform, depth);
}
public bool Update(Transform transform, int depth)
{
bool changed = false;
if (Value != transform
|| depth != Depth
|| ChildCount != transform.childCount
|| Name != transform.name
|| Enabled != transform.gameObject.activeSelf
|| SiblingIndex != transform.GetSiblingIndex())
{
changed = true;
Value = transform;
Depth = depth;
ChildCount = transform.childCount;
Name = transform.name;
Enabled = transform.gameObject.activeSelf;
SiblingIndex = transform.GetSiblingIndex();
}
return changed;
}
}
}

View File

@ -1,201 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using UniverseLib;
using UniverseLib.UI;
using UniverseLib.UI.Models;
using UniverseLib.UI.Widgets.ScrollView;
using UniverseLib.Utility;
namespace UnityExplorer.UI.Widgets
{
public class TransformCell : ICell
{
public float DefaultHeight => 25f;
public bool Enabled => enabled;
private bool enabled;
public Action<CachedTransform> OnExpandToggled;
public Action<CachedTransform> OnEnableToggled;
public Action<GameObject> OnGameObjectClicked;
public CachedTransform cachedTransform;
public GameObject UIRoot { get; set; }
public RectTransform Rect { get; set; }
public ButtonRef ExpandButton;
public ButtonRef NameButton;
public Toggle EnabledToggle;
public InputFieldRef SiblingIndex;
public LayoutElement spacer;
public void Enable()
{
enabled = true;
UIRoot.SetActive(true);
}
public void Disable()
{
enabled = false;
UIRoot.SetActive(false);
}
public void ConfigureCell(CachedTransform cached)
{
if (cached == null)
{
ExplorerCore.LogWarning("Setting TransformTree cell but the CachedTransform is null!");
return;
}
if (!Enabled)
Enable();
cachedTransform = cached;
spacer.minWidth = cached.Depth * 15;
if (cached.Value)
{
string name = cached.Value.name?.Trim();
if (string.IsNullOrEmpty(name))
name = "<i><color=grey>untitled</color></i>";
NameButton.ButtonText.text = name;
NameButton.ButtonText.color = cached.Value.gameObject.activeSelf ? Color.white : Color.grey;
EnabledToggle.Set(cached.Value.gameObject.activeSelf, false);
if (!cached.Value.parent)
SiblingIndex.GameObject.SetActive(false);
else
{
SiblingIndex.GameObject.SetActive(true);
if (!SiblingIndex.Component.isFocused)
SiblingIndex.Text = cached.Value.GetSiblingIndex().ToString();
}
int childCount = cached.Value.childCount;
if (childCount > 0)
{
NameButton.ButtonText.text = $"<color=grey>[{childCount}]</color> {NameButton.ButtonText.text}";
ExpandButton.Component.interactable = true;
ExpandButton.ButtonText.text = cached.Expanded ? "▼" : "►";
ExpandButton.ButtonText.color = cached.Expanded ? new Color(0.5f, 0.5f, 0.5f) : new Color(0.3f, 0.3f, 0.3f);
}
else
{
ExpandButton.Component.interactable = false;
ExpandButton.ButtonText.text = "▪";
ExpandButton.ButtonText.color = new Color(0.3f, 0.3f, 0.3f);
}
}
else
{
NameButton.ButtonText.text = $"[Destroyed]";
NameButton.ButtonText.color = Color.red;
SiblingIndex.GameObject.SetActive(false);
}
}
public void OnMainButtonClicked()
{
if (cachedTransform.Value)
OnGameObjectClicked?.Invoke(cachedTransform.Value.gameObject);
else
ExplorerCore.LogWarning("The object was destroyed!");
}
public void OnExpandClicked()
{
OnExpandToggled?.Invoke(cachedTransform);
}
private void OnEnableClicked(bool value)
{
OnEnableToggled?.Invoke(cachedTransform);
}
private void OnSiblingIndexEndEdit(string input)
{
if (this.cachedTransform == null || !this.cachedTransform.Value)
return;
if (int.TryParse(input.Trim(), out int index))
this.cachedTransform.Value.SetSiblingIndex(index);
this.SiblingIndex.Text = this.cachedTransform.Value.GetSiblingIndex().ToString();
}
public GameObject CreateContent(GameObject parent)
{
UIRoot = UIFactory.CreateUIObject("TransformCell", parent);
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(UIRoot, false, false, true, true, 2, childAlignment: TextAnchor.MiddleCenter);
Rect = UIRoot.GetComponent<RectTransform>();
Rect.anchorMin = new Vector2(0, 1);
Rect.anchorMax = new Vector2(0, 1);
Rect.pivot = new Vector2(0.5f, 1);
Rect.sizeDelta = new Vector2(25, 25);
UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
GameObject spacerObj = UIFactory.CreateUIObject("Spacer", UIRoot, new Vector2(0, 0));
UIFactory.SetLayoutElement(spacerObj, minWidth: 0, flexibleWidth: 0, minHeight: 0, flexibleHeight: 0);
this.spacer = spacerObj.GetComponent<LayoutElement>();
// Expand arrow
ExpandButton = UIFactory.CreateButton(this.UIRoot, "ExpandButton", "►");
UIFactory.SetLayoutElement(ExpandButton.Component.gameObject, minWidth: 15, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0);
// Enabled toggle
GameObject toggleObj = UIFactory.CreateToggle(UIRoot, "BehaviourToggle", out EnabledToggle, out Text behavText, default, 17, 17);
UIFactory.SetLayoutElement(toggleObj, minHeight: 17, flexibleHeight: 0, minWidth: 17);
EnabledToggle.onValueChanged.AddListener(OnEnableClicked);
// Name button
GameObject nameBtnHolder = UIFactory.CreateHorizontalGroup(this.UIRoot, "NameButtonHolder",
false, false, true, true, childAlignment: TextAnchor.MiddleLeft);
UIFactory.SetLayoutElement(nameBtnHolder, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
nameBtnHolder.AddComponent<Mask>().showMaskGraphic = false;
NameButton = UIFactory.CreateButton(nameBtnHolder, "NameButton", "Name", null);
UIFactory.SetLayoutElement(NameButton.Component.gameObject, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
Text nameLabel = NameButton.Component.GetComponentInChildren<Text>();
nameLabel.horizontalOverflow = HorizontalWrapMode.Overflow;
nameLabel.alignment = TextAnchor.MiddleLeft;
// Sibling index input
SiblingIndex = UIFactory.CreateInputField(this.UIRoot, "SiblingIndexInput", string.Empty);
SiblingIndex.Component.textComponent.fontSize = 11;
SiblingIndex.Component.textComponent.alignment = TextAnchor.MiddleRight;
Image siblingImage = SiblingIndex.GameObject.GetComponent<Image>();
siblingImage.color = new(0f, 0f, 0f, 0.25f);
UIFactory.SetLayoutElement(SiblingIndex.GameObject, 35, 20, 0, 0);
SiblingIndex.Component.GetOnEndEdit().AddListener(OnSiblingIndexEndEdit);
// Setup selectables
Color normal = new(0.11f, 0.11f, 0.11f);
Color highlight = new(0.25f, 0.25f, 0.25f);
Color pressed = new(0.05f, 0.05f, 0.05f);
Color disabled = new(1, 1, 1, 0);
RuntimeHelper.SetColorBlock(ExpandButton.Component, normal, highlight, pressed, disabled);
RuntimeHelper.SetColorBlock(NameButton.Component, normal, highlight, pressed, disabled);
NameButton.OnClick += OnMainButtonClicked;
ExpandButton.OnClick += OnExpandClicked;
UIRoot.SetActive(false);
return this.UIRoot;
}
}
}

View File

@ -1,369 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using UnityEngine;
using UniverseLib;
using UniverseLib.UI.Widgets.ScrollView;
using UniverseLib.Utility;
namespace UnityExplorer.UI.Widgets
{
public class TransformTree : ICellPoolDataSource<TransformCell>
{
public Func<IEnumerable<GameObject>> GetRootEntriesMethod;
public Action<GameObject> OnClickOverrideHandler;
public ScrollPool<TransformCell> ScrollPool;
// IMPORTANT CAVEAT WITH OrderedDictionary:
// While the performance is mostly good, there are two methods we should NEVER use:
// - Remove(object)
// - set_Item[object]
// These two methods have extremely bad performance due to using IndexOfKey(), which iterates the whole dictionary.
// Currently we do not use either of these methods, so everything should be constant time lookups.
// We DO make use of get_Item[object], get_Item[index], Add, Insert, Contains and RemoveAt, which OrderedDictionary meets our needs for.
/// <summary>
/// Key: UnityEngine.Transform instance ID<br/>
/// Value: CachedTransform
/// </summary>
internal readonly OrderedDictionary cachedTransforms = new();
// for keeping track of which actual transforms are expanded or not, outside of the cache data.
private readonly HashSet<int> expandedInstanceIDs = new();
private readonly HashSet<int> autoExpandedIDs = new();
// state for Traverse parse
private readonly HashSet<int> visited = new();
private bool needRefreshUI;
private int displayIndex;
int prevDisplayIndex;
private Coroutine refreshCoroutine;
private readonly Stopwatch traversedThisFrame = new();
// ScrollPool item count. PrevDisplayIndex is the highest index + 1 from our last traverse.
public int ItemCount => prevDisplayIndex;
// Search filter
public bool Filtering => !string.IsNullOrEmpty(currentFilter);
private bool wasFiltering;
public string CurrentFilter
{
get => currentFilter;
set
{
currentFilter = value ?? "";
if (!wasFiltering && Filtering)
wasFiltering = true;
else if (wasFiltering && !Filtering)
{
wasFiltering = false;
autoExpandedIDs.Clear();
}
}
}
private string currentFilter;
public TransformTree(ScrollPool<TransformCell> scrollPool, Func<IEnumerable<GameObject>> getRootEntriesMethod)
{
ScrollPool = scrollPool;
GetRootEntriesMethod = getRootEntriesMethod;
}
// Initialize and reset
// Must be called externally from owner of this TransformTree
public void Init()
{
ScrollPool.Initialize(this);
}
// Called to completely reset the tree, ie. switching inspected GameObject
public void Rebuild()
{
autoExpandedIDs.Clear();
expandedInstanceIDs.Clear();
RefreshData(true, true, true, false);
}
// Called to completely wipe our data (ie, GameObject inspector returning to pool)
public void Clear()
{
this.cachedTransforms.Clear();
displayIndex = 0;
autoExpandedIDs.Clear();
expandedInstanceIDs.Clear();
this.ScrollPool.Refresh(true, true);
if (refreshCoroutine != null)
{
RuntimeHelper.StopCoroutine(refreshCoroutine);
refreshCoroutine = null;
}
}
// Checks if the given Instance ID is expanded or not
public bool IsTransformExpanded(int instanceID)
{
return Filtering ? autoExpandedIDs.Contains(instanceID)
: expandedInstanceIDs.Contains(instanceID);
}
// Jumps to a specific Transform in the tree and highlights it.
public void JumpAndExpandToTransform(Transform transform)
{
// make sure all parents of the object are expanded
Transform parent = transform.parent;
while (parent)
{
int pid = parent.GetInstanceID();
if (!expandedInstanceIDs.Contains(pid))
expandedInstanceIDs.Add(pid);
parent = parent.parent;
}
// Refresh cached transforms (no UI rebuild yet).
// Stop existing coroutine and do it oneshot.
RefreshData(false, false, true, true);
int transformID = transform.GetInstanceID();
// find the index of our transform in the list and jump to it
int idx;
for (idx = 0; idx < cachedTransforms.Count; idx++)
{
CachedTransform cache = (CachedTransform)cachedTransforms[idx];
if (cache.InstanceID == transformID)
break;
}
ScrollPool.JumpToIndex(idx, OnCellJumpedTo);
}
private void OnCellJumpedTo(TransformCell cell)
{
RuntimeHelper.StartCoroutine(HighlightCellCoroutine(cell));
}
private IEnumerator HighlightCellCoroutine(TransformCell cell)
{
UnityEngine.UI.Button button = cell.NameButton.Component;
button.StartColorTween(new Color(0.2f, 0.3f, 0.2f), false);
float start = Time.realtimeSinceStartup;
while (Time.realtimeSinceStartup - start < 1.5f)
yield return null;
button.OnDeselect(null);
}
// Perform a Traverse and optionally refresh the ScrollPool as well.
// If oneShot, then this happens instantly with no yield.
public void RefreshData(bool andRefreshUI, bool jumpToTop, bool stopExistingCoroutine, bool oneShot)
{
if (refreshCoroutine != null)
{
if (stopExistingCoroutine)
{
RuntimeHelper.StopCoroutine(refreshCoroutine);
refreshCoroutine = null;
}
else
return;
}
visited.Clear();
displayIndex = 0;
needRefreshUI = false;
traversedThisFrame.Reset();
traversedThisFrame.Start();
refreshCoroutine = RuntimeHelper.StartCoroutine(RefreshCoroutine(andRefreshUI, jumpToTop, oneShot));
}
IEnumerator RefreshCoroutine(bool andRefreshUI, bool jumpToTop, bool oneShot)
{
// Instead of doing string.IsNullOrEmpty(CurrentFilter) many times, let's just do it once per update.
bool filtering = Filtering;
IEnumerable<GameObject> rootObjects = GetRootEntriesMethod();
foreach (GameObject gameObj in rootObjects)
{
if (!gameObj)
continue;
IEnumerator enumerator = Traverse(gameObj.transform, null, 0, oneShot, filtering);
while (enumerator.MoveNext())
{
if (!oneShot)
yield return enumerator.Current;
}
}
// Prune displayed transforms that we didnt visit in that traverse
for (int i = cachedTransforms.Count - 1; i >= 0; i--)
{
if (traversedThisFrame.ElapsedMilliseconds > 2)
{
yield return null;
traversedThisFrame.Reset();
traversedThisFrame.Start();
}
CachedTransform cached = (CachedTransform)cachedTransforms[i];
if (!visited.Contains(cached.InstanceID))
{
cachedTransforms.RemoveAt(i);
needRefreshUI = true;
}
}
if (andRefreshUI && needRefreshUI)
ScrollPool.Refresh(true, jumpToTop);
prevDisplayIndex = displayIndex;
refreshCoroutine = null;
}
// Recursive method to check a Transform and its children (if expanded).
// Parent and depth can be null/default.
private IEnumerator Traverse(Transform transform, CachedTransform parent, int depth, bool oneShot, bool filtering)
{
if (traversedThisFrame.ElapsedMilliseconds > 2)
{
yield return null;
traversedThisFrame.Reset();
traversedThisFrame.Start();
}
int instanceID = transform.GetInstanceID();
// Unlikely, but since this method is async it could theoretically happen in extremely rare circumstances
if (visited.Contains(instanceID))
yield break;
if (filtering)
{
if (!FilterHierarchy(transform))
yield break;
if (!autoExpandedIDs.Contains(instanceID))
autoExpandedIDs.Add(instanceID);
}
visited.Add(instanceID);
CachedTransform cached;
if (cachedTransforms.Contains(instanceID))
{
cached = (CachedTransform)cachedTransforms[(object)instanceID];
int prevSiblingIdx = cached.SiblingIndex;
if (cached.Update(transform, depth))
{
needRefreshUI = true;
// If the sibling index changed, we need to shuffle it in our cached transforms list.
if (prevSiblingIdx != cached.SiblingIndex)
{
cachedTransforms.Remove(instanceID);
if (cachedTransforms.Count <= displayIndex)
cachedTransforms.Add(instanceID, cached);
else
cachedTransforms.Insert(displayIndex, instanceID, cached);
}
}
}
else
{
needRefreshUI = true;
cached = new CachedTransform(this, transform, depth, parent);
if (cachedTransforms.Count <= displayIndex)
cachedTransforms.Add(instanceID, cached);
else
cachedTransforms.Insert(displayIndex, instanceID, cached);
}
displayIndex++;
if (IsTransformExpanded(instanceID) && cached.Value.childCount > 0)
{
for (int i = 0; i < transform.childCount; i++)
{
IEnumerator enumerator = Traverse(transform.GetChild(i), cached, depth + 1, oneShot, filtering);
while (enumerator.MoveNext())
{
if (!oneShot)
yield return enumerator.Current;
}
}
}
}
private bool FilterHierarchy(Transform obj)
{
if (obj.name.ContainsIgnoreCase(currentFilter))
return true;
if (obj.childCount <= 0)
return false;
for (int i = 0; i < obj.childCount; i++)
if (FilterHierarchy(obj.GetChild(i)))
return true;
return false;
}
public void SetCell(TransformCell cell, int index)
{
if (index < cachedTransforms.Count)
{
cell.ConfigureCell((CachedTransform)cachedTransforms[index]);
if (Filtering)
{
if (cell.cachedTransform.Name.ContainsIgnoreCase(currentFilter))
cell.NameButton.ButtonText.color = Color.green;
}
}
else
cell.Disable();
}
public void OnCellBorrowed(TransformCell cell)
{
cell.OnExpandToggled += OnCellExpandToggled;
cell.OnGameObjectClicked += OnGameObjectClicked;
cell.OnEnableToggled += OnCellEnableToggled;
}
private void OnGameObjectClicked(GameObject obj)
{
if (OnClickOverrideHandler != null)
OnClickOverrideHandler.Invoke(obj);
else
InspectorManager.Inspect(obj);
}
public void OnCellExpandToggled(CachedTransform cache)
{
int instanceID = cache.InstanceID;
if (expandedInstanceIDs.Contains(instanceID))
expandedInstanceIDs.Remove(instanceID);
else
expandedInstanceIDs.Add(instanceID);
RefreshData(true, false, true, true);
}
public void OnCellEnableToggled(CachedTransform cache)
{
cache.Value.gameObject.SetActive(!cache.Value.gameObject.activeSelf);
RefreshData(true, false, true, true);
}
}
}

View File

@ -79,11 +79,11 @@
<!-- il2cpp nuget -->
<ItemGroup Condition="'$(Configuration)'=='ML_Cpp_net6' or '$(Configuration)'=='ML_Cpp_net472' or '$(Configuration)'=='STANDALONE_Cpp' or '$(Configuration)'=='BIE_Cpp'">
<PackageReference Include="Il2CppAssemblyUnhollower.BaseLib" Version="0.4.22" IncludeAssets="compile" />
<PackageReference Include="UniverseLib.IL2CPP" Version="1.3.3" />
<PackageReference Include="UniverseLib.IL2CPP" Version="1.3.7" />
</ItemGroup>
<!-- mono nuget -->
<ItemGroup Condition="'$(Configuration)'=='BIE6_Mono' or '$(Configuration)'=='BIE5_Mono' or '$(Configuration)'=='ML_Mono' or '$(Configuration)'=='STANDALONE_Mono'">
<PackageReference Include="UniverseLib.Mono" Version="1.3.3" />
<PackageReference Include="UniverseLib.Mono" Version="1.3.7" />
</ItemGroup>
<!-- ~~~~~ ASSEMBLY REFERENCES ~~~~~ -->