├── .gitattributes ├── lib ├── mono │ ├── UnityEngine.dll │ ├── UnityEngine.UI.dll │ ├── UnityEngine_publicized.dll │ └── UnityEngine.UI_publicized.dll ├── _AssemblyPublicizer.exe └── interop │ ├── UnityEngine.dll │ ├── Il2Cppmscorlib.dll │ ├── UnityEngine.UI.dll │ ├── Il2CppSystem.Core.dll │ ├── UnityEngine.UIModule.dll │ ├── UnityEngine.CoreModule.dll │ ├── UnityEngine.IMGUIModule.dll │ ├── UnityEngine.PhysicsModule.dll │ ├── UnityEngine.AssetBundleModule.dll │ └── UnityEngine.TextRenderingModule.dll ├── src ├── Resources │ ├── legacy.bundle │ ├── modern.bundle │ ├── legacy.5.6.bundle │ └── legacy.5.3.4.bundle ├── Input │ ├── InputType.cs │ ├── IHandleInput.cs │ ├── NoInput.cs │ ├── LegacyInput.cs │ ├── CursorUnlocker.cs │ └── InputManager.cs ├── UI │ ├── Panels │ │ ├── MouseState.cs │ │ ├── PanelBase.cs │ │ └── PanelManager.cs │ ├── ObjectPool │ │ ├── IPooledObject.cs │ │ └── Pool.cs │ ├── Widgets │ │ ├── ScrollView │ │ │ ├── ICell.cs │ │ │ ├── ICellPoolDataSource.cs │ │ │ ├── UIExtensions.cs │ │ │ └── DataHeightCache.cs │ │ ├── TransformTree │ │ │ ├── CachedTransform.cs │ │ │ └── TransformCell.cs │ │ ├── ButtonList │ │ │ ├── ButtonCell.cs │ │ │ └── ButtonListHandler.cs │ │ ├── AutoSliderScrollbar.cs │ │ └── InputFieldScroller.cs │ ├── Models │ │ ├── UIModel.cs │ │ ├── UIBehaviourModel.cs │ │ ├── ButtonRef.cs │ │ └── InputFieldRef.cs │ └── UIBase.cs ├── Runtime │ ├── RuntimeContext.cs │ ├── Il2Cpp │ │ ├── Il2CppThreadingHelper.cs │ │ ├── ICallManager.cs │ │ ├── Il2CppTextureHelper.cs │ │ ├── Il2CppManagedEnumerator.cs │ │ ├── AssetBundle.cs │ │ └── Il2CppProvider.cs │ ├── Mono │ │ ├── MonoTextureHelper.cs │ │ └── MonoProvider.cs │ └── AmbiguousMemberHandler.cs ├── .editorconfig ├── Utility │ ├── ArgumentUtility.cs │ ├── IOUtility.cs │ ├── MiscUtility.cs │ ├── UnityHelpers.cs │ └── ToStringUtility.cs ├── Reflection │ ├── Il2CppDictionary.cs │ ├── ReflectionPatches.cs │ ├── Il2CppEnumerator.cs │ ├── Il2CppTypeRedirector.cs │ └── Extensions.cs ├── Config │ ├── UniverseLibConfig.cs │ └── ConfigManager.cs ├── UniverseLib.sln ├── Properties │ └── AssemblyInfo.cs ├── UniversalBehaviour.cs ├── UniverseLib.csproj ├── RuntimeHelper.cs └── Universe.cs ├── README.md └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /lib/mono/UnityEngine.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/mono/UnityEngine.dll -------------------------------------------------------------------------------- /lib/_AssemblyPublicizer.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/_AssemblyPublicizer.exe -------------------------------------------------------------------------------- /lib/interop/UnityEngine.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/UnityEngine.dll -------------------------------------------------------------------------------- /lib/mono/UnityEngine.UI.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/mono/UnityEngine.UI.dll -------------------------------------------------------------------------------- /src/Resources/legacy.bundle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/src/Resources/legacy.bundle -------------------------------------------------------------------------------- /src/Resources/modern.bundle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/src/Resources/modern.bundle -------------------------------------------------------------------------------- /lib/interop/Il2Cppmscorlib.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/Il2Cppmscorlib.dll -------------------------------------------------------------------------------- /lib/interop/UnityEngine.UI.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/UnityEngine.UI.dll -------------------------------------------------------------------------------- /src/Resources/legacy.5.6.bundle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/src/Resources/legacy.5.6.bundle -------------------------------------------------------------------------------- /lib/interop/Il2CppSystem.Core.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/Il2CppSystem.Core.dll -------------------------------------------------------------------------------- /src/Resources/legacy.5.3.4.bundle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/src/Resources/legacy.5.3.4.bundle -------------------------------------------------------------------------------- /lib/interop/UnityEngine.UIModule.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/UnityEngine.UIModule.dll -------------------------------------------------------------------------------- /lib/mono/UnityEngine_publicized.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/mono/UnityEngine_publicized.dll -------------------------------------------------------------------------------- /lib/interop/UnityEngine.CoreModule.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/UnityEngine.CoreModule.dll -------------------------------------------------------------------------------- /lib/interop/UnityEngine.IMGUIModule.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/UnityEngine.IMGUIModule.dll -------------------------------------------------------------------------------- /lib/mono/UnityEngine.UI_publicized.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/mono/UnityEngine.UI_publicized.dll -------------------------------------------------------------------------------- /lib/interop/UnityEngine.PhysicsModule.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/UnityEngine.PhysicsModule.dll -------------------------------------------------------------------------------- /lib/interop/UnityEngine.AssetBundleModule.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/UnityEngine.AssetBundleModule.dll -------------------------------------------------------------------------------- /lib/interop/UnityEngine.TextRenderingModule.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/release/UniverseLib/main/lib/interop/UnityEngine.TextRenderingModule.dll -------------------------------------------------------------------------------- /src/Input/InputType.cs: -------------------------------------------------------------------------------- 1 | namespace UniverseLib.Input 2 | { 3 | public enum InputType 4 | { 5 | InputSystem, 6 | Legacy, 7 | None 8 | } 9 | } -------------------------------------------------------------------------------- /src/UI/Panels/MouseState.cs: -------------------------------------------------------------------------------- 1 | namespace UniverseLib.UI.Panels 2 | { 3 | public enum MouseState 4 | { 5 | Down, 6 | Held, 7 | NotPressed 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Runtime/RuntimeContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace UniverseLib.Runtime 7 | { 8 | public enum RuntimeContext 9 | { 10 | Mono, 11 | IL2CPP 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/UI/ObjectPool/IPooledObject.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UniverseLib.UI.ObjectPool 4 | { 5 | /// 6 | /// An object which can be pooled by a . 7 | /// 8 | public interface IPooledObject 9 | { 10 | GameObject UIRoot { get; set; } 11 | float DefaultHeight { get; } 12 | 13 | GameObject CreateContent(GameObject parent); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CS1591: Missing XML comment for publicly visible type or member 4 | dotnet_diagnostic.CS1591.severity = none 5 | 6 | # CS0419: Ambiguous reference in cref attribute 7 | dotnet_diagnostic.CS0419.severity = none 8 | 9 | # CS1584: XML comment has syntactically incorrect cref attribute 10 | dotnet_diagnostic.CS1584.severity = none 11 | 12 | # CS1573: Parameter has no matching param tag in the XML comment (but other parameters do) 13 | dotnet_diagnostic.CS1573.severity = none 14 | -------------------------------------------------------------------------------- /src/UI/Widgets/ScrollView/ICell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using UniverseLib.UI.Models; 7 | using UniverseLib.UI.ObjectPool; 8 | 9 | namespace UniverseLib.UI.Widgets.ScrollView 10 | { 11 | public interface ICell : IPooledObject 12 | { 13 | bool Enabled { get; } 14 | 15 | RectTransform Rect { get; set; } 16 | 17 | void Enable(); 18 | void Disable(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/UI/Widgets/ScrollView/ICellPoolDataSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | namespace UniverseLib.UI.Widgets.ScrollView 8 | { 9 | /// 10 | /// A data source for a ScrollPool. 11 | /// 12 | public interface ICellPoolDataSource where T : ICell 13 | { 14 | int ItemCount { get; } 15 | 16 | void OnCellBorrowed(T cell); 17 | 18 | void SetCell(T cell, int index); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Utility/ArgumentUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace UniverseLib.Utility 7 | { 8 | public static class ArgumentUtility 9 | { 10 | /// 11 | /// Equivalent to new Type[0] 12 | /// 13 | public static readonly Type[] EmptyTypes = new Type[0]; 14 | 15 | /// 16 | /// Equivalent to new object[0] 17 | /// 18 | public static readonly object[] EmptyArgs = new object[0]; 19 | 20 | /// 21 | /// Equivalent to new Type[] { typeof(string) } 22 | /// 23 | public static readonly Type[] ParseArgs = new Type[] { typeof(string) }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Input/IHandleInput.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.EventSystems; 3 | 4 | namespace UniverseLib.Input 5 | { 6 | /// 7 | /// Interface for handling Unity Input API. 8 | /// 9 | public interface IHandleInput 10 | { 11 | Vector2 MousePosition { get; } 12 | Vector2 MouseScrollDelta { get; } 13 | 14 | bool GetKeyDown(KeyCode key); 15 | bool GetKey(KeyCode key); 16 | bool GetKeyUp(KeyCode key); 17 | 18 | bool GetMouseButtonDown(int btn); 19 | bool GetMouseButton(int btn); 20 | bool GetMouseButtonUp(int btn); 21 | 22 | void ResetInputAxes(); 23 | 24 | BaseInputModule UIInputModule { get; } 25 | 26 | void AddUIInputModule(); 27 | void ActivateModule(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Input/NoInput.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.EventSystems; 3 | 4 | namespace UniverseLib.Input 5 | { 6 | // Just a stub for games where no Input module was able to load at all. 7 | 8 | public class NoInput : IHandleInput 9 | { 10 | public Vector2 MousePosition => Vector2.zero; 11 | public Vector2 MouseScrollDelta => Vector2.zero; 12 | 13 | public bool GetKey(KeyCode key) => false; 14 | public bool GetKeyDown(KeyCode key) => false; 15 | public bool GetKeyUp(KeyCode key) => false; 16 | 17 | public bool GetMouseButton(int btn) => false; 18 | public bool GetMouseButtonDown(int btn) => false; 19 | public bool GetMouseButtonUp(int btn) => false; 20 | 21 | public void ResetInputAxes() { } 22 | 23 | public BaseInputModule UIInputModule => null; 24 | public void ActivateModule() { } 25 | public void AddUIInputModule() { } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Reflection/Il2CppDictionary.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections; 5 | 6 | namespace UniverseLib 7 | { 8 | internal class Il2CppDictionary : IEnumerator 9 | { 10 | readonly Il2CppEnumerator keysEnumerator; 11 | readonly Il2CppEnumerator valuesEnumerator; 12 | 13 | public object Current => new DictionaryEntry(keysEnumerator.Current, valuesEnumerator.Current); 14 | 15 | DictionaryEntry IEnumerator.Current => new(keysEnumerator.Current, valuesEnumerator.Current); 16 | 17 | public Il2CppDictionary(Il2CppEnumerator keysEnumerator, Il2CppEnumerator valuesEnumerator) 18 | { 19 | this.keysEnumerator = keysEnumerator; 20 | this.valuesEnumerator = valuesEnumerator; 21 | } 22 | 23 | public bool MoveNext() 24 | { 25 | return keysEnumerator.MoveNext() && valuesEnumerator.MoveNext(); 26 | } 27 | 28 | public void Dispose() => throw new NotImplementedException(); 29 | public void Reset() => throw new NotImplementedException(); 30 | } 31 | } 32 | 33 | #endif -------------------------------------------------------------------------------- /src/Config/UniverseLibConfig.cs: -------------------------------------------------------------------------------- 1 | using UniverseLib.UI; 2 | 3 | namespace UniverseLib.Config 4 | { 5 | public struct UniverseLibConfig 6 | { 7 | /// If true, disables UniverseLib from overriding the EventSystem from the game when a UniversalUI is in use. 8 | public bool? Disable_EventSystem_Override; 9 | 10 | /// If true, attempts to force-unlock the mouse () when a UniversalUI is in use. 11 | public bool? Force_Unlock_Mouse; 12 | 13 | /// For IL2CPP games, this should be the full path to a folder containing the Unhollowed assemblies. 14 | public string Unhollowed_Modules_Folder; 15 | 16 | /// If the game does not use an EventSystem and you do not expect there to be any other EventSystems, set this to true. 17 | public bool? Disable_Fallback_EventSystem_Search; 18 | 19 | /// If true, GameObjects which are not a child to a can be selected as the selected GameObject by the EventSystem. 20 | public bool? Allow_UI_Selection_Outside_UIBase; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/UI/Widgets/ScrollView/UIExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | namespace UniverseLib.UI.Widgets.ScrollView 8 | { 9 | public static class UIExtension 10 | { 11 | public static void GetCorners(this RectTransform rect, Vector3[] corners) 12 | { 13 | Vector3 bottomLeft = new(rect.position.x, rect.position.y - rect.rect.height, 0); 14 | 15 | corners[0] = bottomLeft; 16 | corners[1] = bottomLeft + new Vector3(0, rect.rect.height, 0); 17 | corners[2] = bottomLeft + new Vector3(rect.rect.width, rect.rect.height, 0); 18 | corners[3] = bottomLeft + new Vector3(rect.rect.width, 0, 0); 19 | } 20 | 21 | // again, using position and rect instead of 22 | 23 | public static float MaxY(this RectTransform rect) => rect.position.y - rect.rect.height; 24 | 25 | public static float MinY(this RectTransform rect) => rect.position.y; 26 | 27 | public static float MaxX(this RectTransform rect) => rect.position.x + rect.rect.width; 28 | 29 | public static float MinX(this RectTransform rect) => rect.position.x; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/UI/Models/UIModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | namespace UniverseLib.UI.Models 8 | { 9 | /// 10 | /// An abstract UI object which does not exist as an actual UI Component, but which may be a reference to one. 11 | /// 12 | public abstract class UIModel 13 | { 14 | public abstract GameObject UIRoot { get; } 15 | 16 | public bool Enabled 17 | { 18 | get => UIRoot && UIRoot.activeInHierarchy; 19 | set 20 | { 21 | if (!UIRoot || Enabled == value) 22 | return; 23 | UIRoot.SetActive(value); 24 | OnToggleEnabled?.Invoke(value); 25 | } 26 | } 27 | 28 | public event Action OnToggleEnabled; 29 | 30 | public abstract void ConstructUI(GameObject parent); 31 | 32 | public virtual void Toggle() => SetActive(!Enabled); 33 | 34 | public virtual void SetActive(bool active) 35 | { 36 | UIRoot?.SetActive(active); 37 | } 38 | 39 | public virtual void Destroy() 40 | { 41 | if (UIRoot) 42 | GameObject.Destroy(UIRoot); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Runtime/Il2Cpp/Il2CppThreadingHelper.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using Il2CppInterop.Runtime; 7 | 8 | namespace UniverseLib.Runtime.Il2Cpp 9 | { 10 | public static class Il2CppThreadingHelper 11 | { 12 | /// 13 | /// Invokes your delegate on the main thread, necessary when using threads to work with certain Unity API, etc. 14 | /// 15 | public static void InvokeOnMainThread(Delegate method) 16 | { 17 | UniversalBehaviour.InvokeDelegate(method); 18 | } 19 | 20 | /// 21 | /// Start a new IL2CPP Thread with your entry point. 22 | /// 23 | public static Il2CppSystem.Threading.Thread StartThread(Action entryPoint) 24 | { 25 | if (entryPoint == null) 26 | throw new ArgumentNullException(nameof(entryPoint)); 27 | 28 | System.Threading.ThreadStart entry = new(entryPoint); 29 | Il2CppSystem.Threading.Thread thread 30 | = new(DelegateSupport.ConvertDelegate(entry)); 31 | thread.Start(); 32 | IL2CPP.il2cpp_thread_attach(thread.Pointer); 33 | return thread; 34 | } 35 | } 36 | } 37 | 38 | #endif -------------------------------------------------------------------------------- /src/Utility/IOUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace UniverseLib.Utility 8 | { 9 | public static class IOUtility 10 | { 11 | private static readonly char[] invalidDirectoryCharacters = Path.GetInvalidPathChars(); 12 | private static readonly char[] invalidFilenameCharacters = Path.GetInvalidFileNameChars(); 13 | 14 | /// 15 | /// Ensures the path contains no invalid characters and that the containing directory exists. 16 | /// 17 | public static string EnsureValidFilePath(string fullPathWithFile) 18 | { 19 | // Remove invalid path characters 20 | fullPathWithFile = string.Concat(fullPathWithFile.Split(invalidDirectoryCharacters)); 21 | 22 | // Create directory (does nothing if it exists) 23 | Directory.CreateDirectory(Path.GetDirectoryName(fullPathWithFile)); 24 | 25 | return fullPathWithFile; 26 | } 27 | 28 | /// 29 | /// Ensures the file name contains no invalid characters. 30 | /// 31 | public static string EnsureValidFilename(string filename) 32 | { 33 | return string.Concat(filename.Split(invalidFilenameCharacters)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/UniverseLib.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32328.378 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UniverseLib", "UniverseLib.csproj", "{49736F05-474F-49DA-ADB5-ED85BA079FFD}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{67C9CBA5-9CF3-44E7-AC88-588BACC86888}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Release_IL2CPP|Any CPU = Release_IL2CPP|Any CPU 16 | Release_Mono|Any CPU = Release_Mono|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {49736F05-474F-49DA-ADB5-ED85BA079FFD}.Release_IL2CPP|Any CPU.ActiveCfg = Release_IL2CPP|Any CPU 20 | {49736F05-474F-49DA-ADB5-ED85BA079FFD}.Release_IL2CPP|Any CPU.Build.0 = Release_IL2CPP|Any CPU 21 | {49736F05-474F-49DA-ADB5-ED85BA079FFD}.Release_Mono|Any CPU.ActiveCfg = Release_Mono|Any CPU 22 | {49736F05-474F-49DA-ADB5-ED85BA079FFD}.Release_Mono|Any CPU.Build.0 = Release_Mono|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {2DB1638C-C94D-4319-8B66-17F6F724766B} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle(UniverseLib.Universe.NAME)] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany(UniverseLib.Universe.AUTHOR)] 11 | [assembly: AssemblyProduct(UniverseLib.Universe.NAME)] 12 | [assembly: AssemblyCopyright("LGPL 2.1")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("b21dbde3-5d6f-4726-93ab-cc3cc68bae7d")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion(UniverseLib.Universe.VERSION)] 35 | [assembly: AssemblyFileVersion(UniverseLib.Universe.VERSION)] 36 | 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniverseLib 2 | 3 | UniverseLib is a library for making plugins which target IL2CPP and Mono Unity games, with a focus on UI-driven plugins. 4 | 5 | It was developed for personal use so that my [UnityExplorer](https://github.com/rainbowblood666/UnityExplorer) tool and my Config Manager plugins could use a shared environment without overwriting or conflicting with each other, but I made it available as a public tool cause why not. 6 | 7 | ## NuGet 8 | 9 | [![](https://img.shields.io/nuget/v/rainbowblood.UniverseLib.Mono?label=UniverseLib.Mono)](https://www.nuget.org/packages/rainbowblood.UniverseLib.Mono) 10 | 11 | [![](https://img.shields.io/nuget/v/rainbowblood.UniverseLib.IL2CPP?label=UniverseLib.IL2CPP)](https://www.nuget.org/packages/rainbowblood.UniverseLib.IL2CPP) 12 | 13 | ## Documentation 14 | 15 | Documentation and usage guides can currently be found on the [Wiki](https://github.com/rainbowblood666/UniverseLib/wiki). 16 | 17 | ## UniverseLib.Analyzers 18 | 19 | [![](https://img.shields.io/nuget/v/UniverseLib.Analyzers)](https://www.nuget.org/packages/UniverseLib.Analyzers) 20 | [![](https://img.shields.io/badge/-source-blue?logo=github)](https://github.com/rainbowblood666/UniverseLib.Analyzers) 21 | 22 | The Analyzers package contains IDE analyzers for using UniverseLib and avoiding common mistakes when making universal Unity mods and tools. 23 | 24 | ## Acknowledgements 25 | 26 | * [Geoffrey Horsington](https://github.com/ghorsington) and [BepInEx](https://github.com/BepInEx) for [ManagedIl2CppEnumerator](https://github.com/BepInEx/BepInEx/blob/master/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerator.cs) \[[license](https://github.com/BepInEx/BepInEx/blob/master/LICENSE)\], included for IL2CPP coroutine support. 27 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionPatches.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using HarmonyLib; 7 | using UniverseLib.Utility; 8 | 9 | namespace UniverseLib 10 | { 11 | internal static class ReflectionPatches 12 | { 13 | internal static void Init() 14 | { 15 | Universe.Patch(typeof(Assembly), 16 | nameof(Assembly.GetTypes), 17 | MethodType.Normal, 18 | new Type[0], 19 | finalizer: AccessTools.Method(typeof(ReflectionPatches), nameof(Finalizer_Assembly_GetTypes))); 20 | } 21 | 22 | public static Exception Finalizer_Assembly_GetTypes(Assembly __instance, Exception __exception, ref Type[] __result) 23 | { 24 | if (__exception != null) 25 | { 26 | if (__exception is ReflectionTypeLoadException rtle) 27 | { 28 | __result = ReflectionUtility.TryExtractTypesFromException(rtle); 29 | } 30 | else // It was some other exception, try use GetExportedTypes 31 | { 32 | try 33 | { 34 | __result = __instance.GetExportedTypes(); 35 | } 36 | catch (ReflectionTypeLoadException e) 37 | { 38 | __result = ReflectionUtility.TryExtractTypesFromException(e); 39 | } 40 | catch 41 | { 42 | __result = ArgumentUtility.EmptyTypes; 43 | } 44 | } 45 | } 46 | 47 | return null; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/UI/Models/UIBehaviourModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | namespace UniverseLib.UI.Models 8 | { 9 | /// 10 | /// A class which can be used as an abstract UI object, which does not exist as a Component but which can receive Update calls. 11 | /// 12 | public abstract class UIBehaviourModel : UIModel 13 | { 14 | // Static 15 | static readonly List Instances = new(); 16 | 17 | internal static void UpdateInstances() 18 | { 19 | if (!Instances.Any()) 20 | return; 21 | 22 | try 23 | { 24 | for (int i = Instances.Count - 1; i >= 0; i--) 25 | { 26 | UIBehaviourModel instance = Instances[i]; 27 | if (instance == null || !instance.UIRoot) 28 | { 29 | Instances.RemoveAt(i); 30 | continue; 31 | } 32 | if (instance.Enabled) 33 | instance.Update(); 34 | } 35 | } 36 | catch (Exception ex) 37 | { 38 | Universe.Log(ex); 39 | } 40 | } 41 | 42 | // Instance 43 | 44 | public UIBehaviourModel() 45 | { 46 | Instances.Add(this); 47 | } 48 | 49 | public virtual void Update() 50 | { 51 | } 52 | 53 | public override void Destroy() 54 | { 55 | if (Instances.Contains(this)) 56 | Instances.Remove(this); 57 | 58 | base.Destroy(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/UI/Models/ButtonRef.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using UnityEngine.UI; 7 | 8 | namespace UniverseLib.UI.Models 9 | { 10 | /// 11 | /// A simple helper class to handle a button's OnClick more effectively, along with some helpers. 12 | /// 13 | public class ButtonRef 14 | { 15 | /// 16 | /// Invoked when the Button is clicked. 17 | /// 18 | public Action OnClick; 19 | 20 | /// 21 | /// The actual Button component this object is a reference to. 22 | /// 23 | public Button Component { get; } 24 | 25 | /// 26 | /// The Text component on the button. 27 | /// 28 | public Text ButtonText { get; } 29 | 30 | /// 31 | /// The GameObject this Button is attached to. 32 | /// 33 | public GameObject GameObject => Component.gameObject; 34 | 35 | /// 36 | /// The RectTransform for this Button. 37 | /// 38 | public RectTransform Transform => Component.transform.TryCast(); 39 | 40 | /// 41 | /// Helper for Button.enabled. 42 | /// 43 | public bool Enabled 44 | { 45 | get => Component.enabled; 46 | set => Component.enabled = value; 47 | } 48 | 49 | public ButtonRef(Button button) 50 | { 51 | this.Component = button; 52 | this.ButtonText = button.GetComponentInChildren(); 53 | 54 | button.onClick.AddListener(() => { OnClick?.Invoke(); }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Utility/MiscUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace UniverseLib.Utility 8 | { 9 | public static class MiscUtility 10 | { 11 | /// 12 | /// Check if a string contains another string, case-insensitive. 13 | /// 14 | public static bool ContainsIgnoreCase(this string _this, string s) 15 | { 16 | return CultureInfo.CurrentCulture.CompareInfo.IndexOf(_this, s, CompareOptions.IgnoreCase) >= 0; 17 | } 18 | 19 | /// 20 | /// Just to allow Enum to do .HasFlag() in NET 3.5 21 | /// 22 | public static bool HasFlag(this Enum flags, Enum value) 23 | { 24 | try 25 | { 26 | ulong flag = Convert.ToUInt64(value); 27 | return (Convert.ToUInt64(flags) & flag) == flag; 28 | } 29 | catch 30 | { 31 | long flag = Convert.ToInt64(value); 32 | return (Convert.ToInt64(flags) & flag) == flag; 33 | } 34 | } 35 | 36 | /// 37 | /// Returns true if the StringBuilder ends with the provided string. 38 | /// 39 | public static bool EndsWith(this StringBuilder sb, string _string) 40 | { 41 | int len = _string.Length; 42 | 43 | if (sb.Length < len) 44 | return false; 45 | 46 | int stringpos = 0; 47 | for (int i = sb.Length - len; i < sb.Length; i++, stringpos++) 48 | { 49 | if (sb[i] != _string[stringpos]) 50 | return false; 51 | } 52 | return true; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/UniversalBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | #if IL2CPP 8 | using Il2CppInterop.Runtime.Injection; 9 | #endif 10 | 11 | namespace UniverseLib 12 | { 13 | /// 14 | /// Used for receiving Update events and starting Coroutines. 15 | /// 16 | internal class UniversalBehaviour : MonoBehaviour 17 | { 18 | internal static UniversalBehaviour Instance { get; private set; } 19 | 20 | internal static void Setup() 21 | { 22 | #if IL2CPP 23 | ClassInjector.RegisterTypeInIl2Cpp(); 24 | #endif 25 | 26 | GameObject obj = new("UniverseLibBehaviour"); 27 | GameObject.DontDestroyOnLoad(obj); 28 | obj.hideFlags |= HideFlags.HideAndDontSave; 29 | Instance = obj.AddComponent(); 30 | } 31 | 32 | internal void Update() 33 | { 34 | Universe.Update(); 35 | } 36 | 37 | #if IL2CPP 38 | public UniversalBehaviour(IntPtr ptr) : base(ptr) { } 39 | 40 | static Delegate queuedDelegate; 41 | 42 | internal static void InvokeDelegate(Delegate method) 43 | { 44 | queuedDelegate = method; 45 | Instance.Invoke(nameof(InvokeQueuedAction), 0f); 46 | } 47 | 48 | void InvokeQueuedAction() 49 | { 50 | try 51 | { 52 | Delegate method = queuedDelegate; 53 | queuedDelegate = null; 54 | method?.DynamicInvoke(); 55 | } 56 | catch (Exception ex) 57 | { 58 | Universe.LogWarning($"Exception invoking action from IL2CPP thread: {ex}"); 59 | } 60 | } 61 | #endif 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/UI/Widgets/TransformTree/CachedTransform.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UniverseLib.UI.Widgets 4 | { 5 | public class CachedTransform 6 | { 7 | public TransformTree Tree { get; } 8 | public Transform Value { get; private set; } 9 | public int InstanceID { get; } 10 | public CachedTransform Parent { get; internal set; } 11 | 12 | public int Depth { get; internal set; } 13 | public int ChildCount { get; internal set; } 14 | public string Name { get; internal set; } 15 | public bool Enabled { get; internal set; } 16 | public int SiblingIndex { get; internal set; } 17 | 18 | public bool Expanded => Tree.IsTransformExpanded(InstanceID); 19 | 20 | public CachedTransform(TransformTree tree, Transform transform, int depth, CachedTransform parent = null) 21 | { 22 | InstanceID = transform.GetInstanceID(); 23 | 24 | Tree = tree; 25 | Value = transform; 26 | Parent = parent; 27 | SiblingIndex = transform.GetSiblingIndex(); 28 | Update(transform, depth); 29 | } 30 | 31 | public bool Update(Transform transform, int depth) 32 | { 33 | bool changed = false; 34 | 35 | if (!Value.ReferenceEqual(transform) 36 | || depth != Depth 37 | || ChildCount != transform.childCount 38 | || Name != transform.name 39 | || Enabled != transform.gameObject.activeSelf 40 | || (transform.parent != null && SiblingIndex != transform.GetSiblingIndex())) 41 | { 42 | changed = true; 43 | 44 | Value = transform; 45 | Depth = depth; 46 | ChildCount = transform.childCount; 47 | Name = transform.name; 48 | Enabled = transform.gameObject.activeSelf; 49 | SiblingIndex = transform.GetSiblingIndex(); 50 | } 51 | 52 | return changed; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Reflection/Il2CppEnumerator.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System; 3 | using System.Reflection; 4 | using System.Collections; 5 | using UniverseLib.Utility; 6 | 7 | namespace UniverseLib 8 | { 9 | internal class Il2CppEnumerator : IEnumerator 10 | { 11 | readonly object enumerator; 12 | readonly MethodInfo m_GetEnumerator; 13 | 14 | readonly object instanceForMoveNext; 15 | readonly MethodInfo m_MoveNext; 16 | 17 | readonly object instanceForCurrent; 18 | readonly MethodInfo p_Current; 19 | 20 | public object Current => p_Current.Invoke(instanceForCurrent, null); 21 | 22 | public bool MoveNext() 23 | { 24 | return (bool)m_MoveNext.Invoke(instanceForMoveNext, null); 25 | } 26 | 27 | public void Reset() => throw new NotImplementedException(); 28 | 29 | public Il2CppEnumerator(object instance, Type type) 30 | { 31 | m_GetEnumerator = type.GetMethod("GetEnumerator") 32 | ?? type.GetMethod("System_Collections_IEnumerable_GetEnumerator", ReflectionUtility.FLAGS); 33 | 34 | enumerator = m_GetEnumerator.Invoke( 35 | instance.TryCast(m_GetEnumerator.DeclaringType), 36 | ArgumentUtility.EmptyArgs); 37 | 38 | if (enumerator == null) 39 | throw new Exception($"GetEnumerator returned null"); 40 | 41 | Type enumeratorType = enumerator.GetActualType(); 42 | 43 | m_MoveNext = enumeratorType.GetMethod("MoveNext") 44 | ?? enumeratorType.GetMethod("System_Collections_IEnumerator_MoveNext", ReflectionUtility.FLAGS); 45 | 46 | instanceForMoveNext = enumerator.TryCast(m_MoveNext.DeclaringType); 47 | 48 | p_Current = enumeratorType.GetProperty("Current")?.GetGetMethod() 49 | ?? enumeratorType.GetMethod("System_Collections_IEnumerator_get_Current", ReflectionUtility.FLAGS); 50 | 51 | instanceForCurrent = enumerator.TryCast(p_Current.DeclaringType); 52 | } 53 | } 54 | } 55 | 56 | #endif -------------------------------------------------------------------------------- /src/UI/Widgets/ButtonList/ButtonCell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using UnityEngine.UI; 7 | using UniverseLib.UI.Models; 8 | using UniverseLib.UI.Widgets.ScrollView; 9 | 10 | namespace UniverseLib.UI.Widgets.ButtonList 11 | { 12 | /// 13 | /// Represents the base cell used by a . 14 | /// 15 | public class ButtonCell : ICell 16 | { 17 | public Action OnClick; 18 | 19 | public int CurrentDataIndex { get; set; } 20 | public ButtonRef Button { get; private set; } 21 | 22 | // ICell 23 | public float DefaultHeight => 25f; 24 | public GameObject UIRoot { get; set; } 25 | public RectTransform Rect { get; set; } 26 | 27 | public bool Enabled => UIRoot.activeSelf; 28 | public void Enable() => UIRoot.SetActive(true); 29 | public void Disable() => UIRoot.SetActive(false); 30 | 31 | public virtual GameObject CreateContent(GameObject parent) 32 | { 33 | UIRoot = UIFactory.CreateHorizontalGroup(parent, "ButtonCell", true, false, true, true, 2, default, 34 | new Color(0.11f, 0.11f, 0.11f), TextAnchor.MiddleCenter); 35 | Rect = UIRoot.GetComponent(); 36 | Rect.anchorMin = new Vector2(0, 1); 37 | Rect.anchorMax = new Vector2(0, 1); 38 | Rect.pivot = new Vector2(0.5f, 1); 39 | Rect.sizeDelta = new Vector2(25, 25); 40 | UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0); 41 | 42 | UIRoot.SetActive(false); 43 | 44 | this.Button = UIFactory.CreateButton(UIRoot, "NameButton", "Name"); 45 | UIFactory.SetLayoutElement(Button.Component.gameObject, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0); 46 | Text buttonText = Button.Component.GetComponentInChildren(); 47 | buttonText.horizontalOverflow = HorizontalWrapMode.Overflow; 48 | buttonText.alignment = TextAnchor.MiddleLeft; 49 | 50 | Color normal = new(0.11f, 0.11f, 0.11f); 51 | Color highlight = new(0.16f, 0.16f, 0.16f); 52 | Color pressed = new(0.05f, 0.05f, 0.05f); 53 | Color disabled = new(1, 1, 1, 0); 54 | RuntimeHelper.Instance.Internal_SetColorBlock(Button.Component, normal, highlight, pressed, disabled); 55 | 56 | Button.OnClick += () => { OnClick?.Invoke(CurrentDataIndex); }; 57 | 58 | return UIRoot; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Config/ConfigManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UniverseLib.UI; 6 | 7 | namespace UniverseLib.Config 8 | { 9 | /// 10 | /// Contains properties used by UniverseLib to act as a "config", although it is not really a full config implementation. Changing 11 | /// property values in this class has a direct and immediate effect on UniverseLib. 12 | /// 13 | public static class ConfigManager 14 | { 15 | /// 16 | /// Applies the values from the provided which are not null. 17 | /// 18 | public static void LoadConfig(UniverseLibConfig config) 19 | { 20 | if (config.Disable_EventSystem_Override != null) 21 | Disable_EventSystem_Override = config.Disable_EventSystem_Override.Value; 22 | 23 | if (config.Force_Unlock_Mouse != null) 24 | Force_Unlock_Mouse = config.Force_Unlock_Mouse.Value; 25 | 26 | if (!string.IsNullOrEmpty(config.Unhollowed_Modules_Folder)) 27 | Unhollowed_Modules_Folder = config.Unhollowed_Modules_Folder; 28 | 29 | if (config.Disable_Fallback_EventSystem_Search != null) 30 | Disable_Fallback_EventSystem_Search = config.Disable_Fallback_EventSystem_Search.Value; 31 | 32 | if (config.Allow_UI_Selection_Outside_UIBase != null) 33 | Allow_UI_Selection_Outside_UIBase = config.Allow_UI_Selection_Outside_UIBase.Value; 34 | } 35 | 36 | /// If true, disables UniverseLib from overriding the EventSystem from the game when a UniversalUI is in use. 37 | public static bool Disable_EventSystem_Override { get; set; } 38 | 39 | /// If true, attempts to force-unlock the mouse () when a UniversalUI is in use. 40 | public static bool Force_Unlock_Mouse { get; set; } 41 | 42 | /// For IL2CPP games, this should be the full path to a folder containing the Unhollowed assemblies. 43 | /// This property is only used during the intial startup process. 44 | public static string Unhollowed_Modules_Folder { get; set; } 45 | 46 | /// If the game does not use an EventSystem and you do not expect there to be any other EventSystems, set this to true. 47 | public static bool Disable_Fallback_EventSystem_Search { get; set; } 48 | 49 | /// If true, GameObjects which are not a child to a can be selected as the selected GameObject by the EventSystem. 50 | public static bool Allow_UI_Selection_Outside_UIBase { get; set; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Runtime/Il2Cpp/ICallManager.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using Il2CppInterop.Runtime; 8 | 9 | namespace UniverseLib.Runtime.Il2Cpp 10 | { 11 | /// 12 | /// Helper class for using Unity ICalls (internal calls). 13 | /// 14 | public static class ICallManager 15 | { 16 | // cache used by GetICall 17 | private static readonly Dictionary iCallCache = new(); 18 | // cache used by GetICallUnreliable 19 | private static readonly Dictionary unreliableCache = new(); 20 | 21 | /// 22 | /// Helper to get and cache an iCall by providing the signature (eg. "UnityEngine.Resources::FindObjectsOfTypeAll"). 23 | /// 24 | /// The Type of Delegate to provide for the iCall. 25 | /// The signature of the iCall you want to get. 26 | /// The delegate if successful. 27 | /// 28 | public static T GetICall(string signature) where T : Delegate 29 | { 30 | if (iCallCache.ContainsKey(signature)) 31 | return (T)iCallCache[signature]; 32 | 33 | IntPtr ptr = IL2CPP.il2cpp_resolve_icall(signature); 34 | 35 | if (ptr == IntPtr.Zero) 36 | throw new MissingMethodException($"Could not find any iCall with the signature '{signature}'!"); 37 | 38 | Delegate iCall = Marshal.GetDelegateForFunctionPointer(ptr, typeof(T)); 39 | iCallCache.Add(signature, iCall); 40 | 41 | return (T)iCall; 42 | } 43 | 44 | /// 45 | /// Get an iCall which may be one of multiple different signatures (ie, the name changed in different Unity versions). 46 | /// Each possible signature must have the same Delegate type, it can only vary by name. 47 | /// 48 | public static T GetICallUnreliable(params string[] possibleSignatures) where T : Delegate 49 | { 50 | // use the first possible signature as the 'key'. 51 | string key = possibleSignatures.First(); 52 | 53 | if (unreliableCache.ContainsKey(key)) 54 | return (T)unreliableCache[key]; 55 | 56 | T iCall; 57 | IntPtr ptr; 58 | foreach (string sig in possibleSignatures) 59 | { 60 | ptr = IL2CPP.il2cpp_resolve_icall(sig); 61 | if (ptr != IntPtr.Zero) 62 | { 63 | iCall = (T)Marshal.GetDelegateForFunctionPointer(ptr, typeof(T)); 64 | unreliableCache.Add(key, iCall); 65 | return iCall; 66 | } 67 | } 68 | 69 | throw new MissingMethodException($"Could not find any iCall from list of provided signatures starting with '{key}'!"); 70 | } 71 | } 72 | } 73 | #endif -------------------------------------------------------------------------------- /src/Input/LegacyInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEngine; 4 | using UnityEngine.EventSystems; 5 | using UniverseLib.UI; 6 | using UniverseLib.Utility; 7 | 8 | namespace UniverseLib.Input 9 | { 10 | public class LegacyInput : IHandleInput 11 | { 12 | public LegacyInput() 13 | { 14 | p_mousePosition = TInput.GetProperty("mousePosition"); 15 | p_mouseDelta = TInput.GetProperty("mouseScrollDelta"); 16 | m_getKey = TInput.GetMethod("GetKey", new Type[] { typeof(KeyCode) }); 17 | m_getKeyDown = TInput.GetMethod("GetKeyDown", new Type[] { typeof(KeyCode) }); 18 | m_getKeyUp = TInput.GetMethod("GetKeyUp", new Type[] { typeof(KeyCode) }); 19 | m_getMouseButton = TInput.GetMethod("GetMouseButton", new Type[] { typeof(int) }); 20 | m_getMouseButtonDown = TInput.GetMethod("GetMouseButtonDown", new Type[] { typeof(int) }); 21 | m_getMouseButtonUp = TInput.GetMethod("GetMouseButtonUp", new Type[] { typeof(int) }); 22 | m_resetInputAxes = TInput.GetMethod("ResetInputAxes", ArgumentUtility.EmptyTypes); 23 | } 24 | 25 | public static Type TInput => t_Input ??= ReflectionUtility.GetTypeByName("UnityEngine.Input"); 26 | private static Type t_Input; 27 | 28 | private static PropertyInfo p_mousePosition; 29 | private static PropertyInfo p_mouseDelta; 30 | private static MethodInfo m_getKey; 31 | private static MethodInfo m_getKeyDown; 32 | private static MethodInfo m_getKeyUp; 33 | private static MethodInfo m_getMouseButton; 34 | private static MethodInfo m_getMouseButtonDown; 35 | private static MethodInfo m_getMouseButtonUp; 36 | private static MethodInfo m_resetInputAxes; 37 | 38 | public Vector2 MousePosition => (Vector3)p_mousePosition.GetValue(null, null); 39 | public Vector2 MouseScrollDelta => (Vector2)p_mouseDelta.GetValue(null, null); 40 | 41 | public bool GetKey(KeyCode key) => (bool)m_getKey.Invoke(null, new object[] { key }); 42 | public bool GetKeyDown(KeyCode key) => (bool)m_getKeyDown.Invoke(null, new object[] { key }); 43 | public bool GetKeyUp(KeyCode key) => (bool)m_getKeyUp.Invoke(null, new object[] { key }); 44 | 45 | public bool GetMouseButton(int btn) => (bool)m_getMouseButton.Invoke(null, new object[] { btn }); 46 | public bool GetMouseButtonDown(int btn) => (bool)m_getMouseButtonDown.Invoke(null, new object[] { btn }); 47 | public bool GetMouseButtonUp(int btn) => (bool)m_getMouseButtonUp.Invoke(null, new object[] { btn }); 48 | 49 | public void ResetInputAxes() => m_resetInputAxes.Invoke(null, ArgumentUtility.EmptyArgs); 50 | 51 | // UI Input module 52 | 53 | public BaseInputModule UIInputModule => inputModule; 54 | internal StandaloneInputModule inputModule; 55 | 56 | public void AddUIInputModule() 57 | { 58 | inputModule = UniversalUI.CanvasRoot.gameObject.AddComponent(); 59 | inputModule.m_EventSystem = UniversalUI.EventSys; 60 | } 61 | 62 | public void ActivateModule() 63 | { 64 | try 65 | { 66 | inputModule.ActivateModule(); 67 | } 68 | catch (Exception ex) 69 | { 70 | Universe.LogWarning($"Exception enabling StandaloneInputModule: {ex}"); 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/Runtime/Il2Cpp/Il2CppTextureHelper.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using UnityEngine; 8 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 9 | 10 | namespace UniverseLib.Runtime.Il2Cpp 11 | { 12 | internal class Il2CppTextureHelper : TextureHelper 13 | { 14 | internal delegate IntPtr d_EncodeToPNG(IntPtr tex); 15 | 16 | internal delegate void d_Blit2(IntPtr source, IntPtr dest); 17 | 18 | internal delegate IntPtr d_CreateSprite(IntPtr texture, ref Rect rect, ref Vector2 pivot, float pixelsPerUnit, 19 | uint extrude, int meshType, ref Vector4 border, bool generateFallbackPhysicsShape); 20 | 21 | internal delegate void d_CopyTexture_Region(IntPtr src, int srcElement, int srcMip, int srcX, int srcY, 22 | int srcWidth, int srcHeight, IntPtr dst, int dstElement, int dstMip, int dstX, int dstY); 23 | 24 | protected internal override Texture2D Internal_NewTexture2D(int width, int height) 25 | { 26 | return new(width, height, TextureFormat.RGBA32, 1, false, IntPtr.Zero); 27 | } 28 | 29 | protected internal override Texture2D Internal_NewTexture2D(int width, int height, TextureFormat textureFormat, bool mipChain) 30 | { 31 | return new(width, height, textureFormat, mipChain ? -1 : 1, false, IntPtr.Zero); 32 | } 33 | 34 | protected internal override void Internal_Blit(Texture tex, RenderTexture rt) 35 | { 36 | ICallManager.GetICall("UnityEngine.Graphics::Blit2") 37 | .Invoke(tex.Pointer, rt.Pointer); 38 | } 39 | 40 | protected internal override byte[] Internal_EncodeToPNG(Texture2D tex) 41 | { 42 | IntPtr arrayPtr = ICallManager.GetICall("UnityEngine.ImageConversion::EncodeToPNG") 43 | .Invoke(tex.Pointer); 44 | 45 | return arrayPtr == IntPtr.Zero ? null : new Il2CppStructArray(arrayPtr); 46 | } 47 | 48 | protected internal override Sprite Internal_CreateSprite(Texture2D texture) 49 | => CreateSpriteImpl(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero, 100f, 0u, Vector4.zero); 50 | 51 | protected internal override Sprite Internal_CreateSprite(Texture2D texture, Rect rect, Vector2 pivot, float pixelsPerUnit, uint extrude, Vector4 border) 52 | => CreateSpriteImpl(texture, rect, pivot, pixelsPerUnit, extrude, border); 53 | 54 | internal static Sprite CreateSpriteImpl(Texture texture, Rect rect, Vector2 pivot, float pixelsPerUnit, uint extrude, Vector4 border) 55 | { 56 | IntPtr spritePtr = ICallManager.GetICall("UnityEngine.Sprite::CreateSprite_Injected") 57 | .Invoke(texture.Pointer, ref rect, ref pivot, pixelsPerUnit, extrude, 1, ref border, false); 58 | 59 | return spritePtr == IntPtr.Zero ? null : new Sprite(spritePtr); 60 | } 61 | 62 | internal override bool Internal_CanForceReadCubemaps => true; 63 | 64 | internal override Texture Internal_CopyTexture(Texture src, int srcElement, int srcMip, int srcX, int srcY, 65 | int srcWidth, int srcHeight, Texture dst, int dstElement, int dstMip, int dstX, int dstY) 66 | { 67 | ICallManager.GetICall("UnityEngine.Graphics::CopyTexture_Region") 68 | .Invoke(src.Pointer, srcElement, srcMip, srcX, srcY, srcWidth, srcHeight, dst.Pointer, dstElement, dstMip, dstX, dstY); 69 | 70 | return dst; 71 | } 72 | } 73 | } 74 | #endif -------------------------------------------------------------------------------- /src/UI/Widgets/ButtonList/ButtonListHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using UnityEngine; 7 | using UniverseLib.UI.Widgets.ScrollView; 8 | 9 | namespace UniverseLib.UI.Widgets.ButtonList 10 | { 11 | /// 12 | /// A helper to create and handle a simple of Buttons, which can be backed by any data. 13 | /// 14 | public class ButtonListHandler : ICellPoolDataSource where TCell : ButtonCell 15 | { 16 | public ScrollPool ScrollPool { get; private set; } 17 | 18 | public int ItemCount => CurrentEntries.Count; 19 | public List CurrentEntries { get; } = new(); 20 | 21 | protected Func> GetEntries; 22 | protected Action SetICell; 23 | protected Func ShouldDisplay; 24 | protected Action OnCellClicked; 25 | 26 | public string CurrentFilter 27 | { 28 | get => currentFilter; 29 | set => currentFilter = value ?? ""; 30 | } 31 | private string currentFilter; 32 | 33 | /// 34 | /// Create a wrapper to handle your Button ScrollPool. 35 | /// 36 | /// The ScrollPool<ButtonCell> you have already created. 37 | /// A method which should return your current data values. 38 | /// A method which should set the data at the int index to the cell. 39 | /// A method which should determine if the data at the index should be displayed, with an optional string filter from CurrentFilter. 40 | /// A method invoked when a cell is clicked, containing the data index assigned to the cell. 41 | public ButtonListHandler(ScrollPool scrollPool, Func> getEntriesMethod, 42 | Action setICellMethod, Func shouldDisplayMethod, 43 | Action onCellClickedMethod) 44 | { 45 | ScrollPool = scrollPool; 46 | 47 | GetEntries = getEntriesMethod; 48 | SetICell = setICellMethod; 49 | ShouldDisplay = shouldDisplayMethod; 50 | OnCellClicked = onCellClickedMethod; 51 | } 52 | 53 | public void RefreshData() 54 | { 55 | List allEntries = GetEntries(); 56 | CurrentEntries.Clear(); 57 | 58 | foreach (TData entry in allEntries) 59 | { 60 | if (!string.IsNullOrEmpty(currentFilter)) 61 | { 62 | if (!ShouldDisplay(entry, currentFilter)) 63 | continue; 64 | 65 | CurrentEntries.Add(entry); 66 | } 67 | else 68 | CurrentEntries.Add(entry); 69 | } 70 | } 71 | 72 | public virtual void OnCellBorrowed(TCell cell) 73 | { 74 | cell.OnClick += OnCellClicked; 75 | } 76 | 77 | public virtual void SetCell(TCell cell, int index) 78 | { 79 | if (CurrentEntries == null) 80 | RefreshData(); 81 | 82 | if (index < 0 || index >= CurrentEntries.Count) 83 | cell.Disable(); 84 | else 85 | { 86 | cell.Enable(); 87 | cell.CurrentDataIndex = index; 88 | SetICell(cell, index); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/UI/Models/InputFieldRef.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using UnityEngine.UI; 7 | 8 | namespace UniverseLib.UI.Models 9 | { 10 | /// 11 | /// A simple wrapper class for working with InputFields, with some helpers and performance improvements. 12 | /// 13 | public class InputFieldRef : UIModel 14 | { 15 | // Static 16 | 17 | internal static readonly HashSet inputsPendingUpdate = new(); 18 | 19 | internal static void UpdateInstances() 20 | { 21 | while (inputsPendingUpdate.Any()) 22 | { 23 | InputFieldRef inputField = inputsPendingUpdate.First(); 24 | LayoutRebuilder.MarkLayoutForRebuild(inputField.Transform); 25 | inputField.OnValueChanged?.Invoke(inputField.Component.text); 26 | 27 | inputsPendingUpdate.Remove(inputField); 28 | } 29 | } 30 | 31 | // Instance 32 | 33 | /// 34 | /// Invoked at most once per frame, if the input was changed in the previous frame. 35 | /// 36 | public event Action OnValueChanged; 37 | 38 | /// 39 | /// The actual InputField component which this object is a reference to. 40 | /// 41 | public InputField Component { get; } 42 | 43 | /// 44 | /// The placeholder Text component. 45 | /// 46 | public Text PlaceholderText { get; } 47 | 48 | /// 49 | /// The GameObject which the InputField is attached to. 50 | /// 51 | public override GameObject UIRoot => Component.gameObject; 52 | 53 | /// 54 | /// The GameObject which the InputField is attached to. 55 | /// 56 | public GameObject GameObject => Component.gameObject; 57 | 58 | /// 59 | /// The RectTransform for this InputField. 60 | /// 61 | public RectTransform Transform { get; } 62 | 63 | /// 64 | /// The Text set to the InputField. 65 | /// 66 | public string Text 67 | { 68 | get => Component.text; 69 | set => Component.text = value; 70 | } 71 | 72 | /// 73 | /// A reference to the InputField's cachedInputTextGenerator. 74 | /// 75 | public TextGenerator TextGenerator => Component.cachedInputTextGenerator; 76 | 77 | /// 78 | /// Returns true if the InputField's vertex count has reached the limit. 79 | /// 80 | public bool ReachedMaxVerts => TextGenerator.vertexCount >= UniversalUI.MAX_TEXT_VERTS; 81 | 82 | public InputFieldRef(InputField component) 83 | { 84 | this.Component = component; 85 | Transform = component.GetComponent(); 86 | PlaceholderText = component.placeholder.TryCast(); 87 | component.onValueChanged.AddListener(OnInputChanged); 88 | } 89 | 90 | private void OnInputChanged(string value) 91 | { 92 | if (!inputsPendingUpdate.Contains(this)) 93 | inputsPendingUpdate.Add(this); 94 | } 95 | 96 | /// 97 | /// Not implemented. 98 | /// 99 | public override void ConstructUI(GameObject parent) => throw new NotImplementedException(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Runtime/Mono/MonoTextureHelper.cs: -------------------------------------------------------------------------------- 1 | #if MONO 2 | using HarmonyLib; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text; 9 | using UnityEngine; 10 | using UniverseLib; 11 | using UniverseLib.Utility; 12 | 13 | namespace UniverseLib.Runtime.Mono 14 | { 15 | internal class MonoTextureHelper : TextureHelper 16 | { 17 | static MethodInfo mi_EncodeToPNG; 18 | static MethodInfo mi_Graphics_CopyTexture; 19 | 20 | internal override bool Internal_CanForceReadCubemaps => mi_Graphics_CopyTexture != null; 21 | 22 | internal MonoTextureHelper() : base() 23 | { 24 | RuntimeHelper.StartCoroutine(InitCoro()); 25 | } 26 | 27 | static IEnumerator InitCoro() 28 | { 29 | while (ReflectionUtility.Initializing) 30 | yield return null; 31 | 32 | mi_Graphics_CopyTexture = AccessTools.Method( 33 | typeof(Graphics), 34 | "CopyTexture", 35 | new Type[] 36 | { 37 | typeof(Texture), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), 38 | typeof(Texture), typeof(int), typeof(int), typeof(int), typeof(int) 39 | }); 40 | 41 | if (ReflectionUtility.GetTypeByName("UnityEngine.ImageConversion") is Type imageConversion) 42 | mi_EncodeToPNG = imageConversion.GetMethod("EncodeToPNG", ReflectionUtility.FLAGS); 43 | else 44 | mi_EncodeToPNG = typeof(Texture2D).GetMethod("EncodeToPNG", ReflectionUtility.FLAGS); 45 | } 46 | 47 | protected internal override void Internal_Blit(Texture tex, RenderTexture rt) 48 | => Graphics.Blit(tex, rt); 49 | 50 | protected internal override Sprite Internal_CreateSprite(Texture2D texture) 51 | => Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero); 52 | 53 | protected internal override Sprite Internal_CreateSprite(Texture2D texture, Rect rect, Vector2 pivot, float pixelsPerUnit, uint extrude, Vector4 border) 54 | => Sprite.Create(texture, rect, pivot, pixelsPerUnit, extrude, SpriteMeshType.Tight, border); 55 | 56 | protected internal override Texture2D Internal_NewTexture2D(int width, int height) 57 | => new(width, height); 58 | 59 | protected internal override Texture2D Internal_NewTexture2D(int width, int height, TextureFormat textureFormat, bool mipChain) 60 | => new(width, height, textureFormat, mipChain); 61 | 62 | protected internal override byte[] Internal_EncodeToPNG(Texture2D tex) 63 | { 64 | if (mi_EncodeToPNG == null) 65 | throw new MissingMethodException("Could not find any Texture2D EncodeToPNG method!"); 66 | 67 | return mi_EncodeToPNG.IsStatic 68 | ? (byte[])mi_EncodeToPNG.Invoke(null, new object[] { tex }) 69 | : (byte[])mi_EncodeToPNG.Invoke(tex, ArgumentUtility.EmptyArgs); 70 | } 71 | 72 | internal override Texture Internal_CopyTexture(Texture src, int srcElement, int srcMip, int srcX, int srcY, 73 | int srcWidth, int srcHeight, Texture dst, int dstElement, int dstMip, int dstX, int dstY) 74 | { 75 | if (mi_Graphics_CopyTexture == null) 76 | throw new MissingMethodException("This game does not ship with the required method 'Graphics.CopyTexture'."); 77 | 78 | mi_Graphics_CopyTexture.Invoke(null, new object[] 79 | { 80 | src, srcElement, srcMip, srcX, srcY, srcWidth, srcHeight, dst, dstElement, dstMip, dstX, dstY 81 | }); 82 | 83 | return dst; 84 | } 85 | } 86 | } 87 | #endif -------------------------------------------------------------------------------- /src/Reflection/Il2CppTypeRedirector.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace UniverseLib.Reflection 9 | { 10 | // This class exists to fix a bug(?) with Unhollower, where "Il2CppSystem" types are returned as the equivalent "System" type. 11 | // Specifically, this is for Generic Types, as all other types should be handled by the GetUnhollowedType method. 12 | // It does not replace System.String or any System primitive types, since "fixing" those seems to be incorrect behaviour. 13 | internal static class Il2CppTypeRedirector 14 | { 15 | static readonly Dictionary redirectors = new(); 16 | 17 | public static string GetAssemblyQualifiedName(Il2CppSystem.Type type) 18 | { 19 | StringBuilder sb = new(); 20 | ProcessType(sb, type); 21 | return sb.ToString(); 22 | } 23 | 24 | static void ProcessType(StringBuilder sb, Il2CppSystem.Type type) 25 | { 26 | if (type.IsPrimitive || type.FullName == "System.String") 27 | { 28 | sb.Append(type.AssemblyQualifiedName); 29 | return; 30 | } 31 | 32 | if (!string.IsNullOrEmpty(type.Namespace)) 33 | { 34 | if (type.FullName.StartsWith("System.")) 35 | sb.Append("Il2Cpp"); 36 | 37 | sb.Append(type.Namespace) 38 | .Append('.'); 39 | } 40 | 41 | int start = sb.Length; 42 | Il2CppSystem.Type declaring = type.DeclaringType; 43 | while (declaring is not null) 44 | { 45 | sb.Insert(start, $"{declaring.Name}+"); 46 | declaring = declaring.DeclaringType; 47 | } 48 | 49 | sb.Append(type.Name); 50 | 51 | if (type.IsGenericType && !type.IsGenericTypeDefinition) 52 | { 53 | Il2CppSystem.Type[] genericArgs = type.GetGenericArguments(); 54 | 55 | // Process and append each type argument (recursive) 56 | sb.Append('['); 57 | int i = 0; 58 | foreach (Il2CppSystem.Type typeArg in genericArgs) 59 | { 60 | sb.Append('['); 61 | ProcessType(sb, typeArg); 62 | sb.Append(']'); 63 | i++; 64 | if (i < genericArgs.Length) 65 | sb.Append(','); 66 | } 67 | sb.Append(']'); 68 | } 69 | 70 | // Append the assembly signature 71 | sb.Append(", "); 72 | 73 | if (type.FullName.StartsWith("System.")) 74 | { 75 | if (!redirectors.ContainsKey(type.Assembly.FullName) && !TryRedirectSystemType(type)) 76 | // No redirect found for type? 77 | throw new TypeLoadException($"No Il2CppSystem redirect found for system type: {type.AssemblyQualifiedName}"); 78 | else 79 | // Type redirect was set up 80 | sb.Append(redirectors[type.Assembly.FullName]); 81 | 82 | } 83 | else // no redirect required 84 | sb.Append(type.Assembly.FullName); 85 | } 86 | 87 | static bool TryRedirectSystemType(Il2CppSystem.Type type) 88 | { 89 | if (type.IsGenericType && !type.IsGenericTypeDefinition) 90 | type = type.GetGenericTypeDefinition(); 91 | 92 | if (ReflectionUtility.AllTypes.TryGetValue($"Il2Cpp{type.FullName}", out Type il2cppType)) 93 | { 94 | redirectors.Add(type.Assembly.FullName, il2cppType.Assembly.FullName); 95 | return true; 96 | } 97 | 98 | return false; 99 | } 100 | } 101 | } 102 | 103 | #endif 104 | -------------------------------------------------------------------------------- /src/UI/UIBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using UnityEngine.UI; 7 | using UniverseLib.UI.Panels; 8 | 9 | namespace UniverseLib.UI 10 | { 11 | /// 12 | /// A simple wrapper to handle a UI created with . 13 | /// 14 | public class UIBase 15 | { 16 | public string ID { get; } 17 | public GameObject RootObject { get; } 18 | public RectTransform RootRect { get; } 19 | public Canvas Canvas { get; } 20 | public Action UpdateMethod { get; } 21 | 22 | public PanelManager Panels { get; } 23 | 24 | internal static readonly int TOP_SORTORDER = 30000; 25 | 26 | /// 27 | /// Whether this UI is currently being displayed or not. Disabled UIs will not receive Update calls. 28 | /// 29 | public bool Enabled 30 | { 31 | get => RootObject && RootObject.activeSelf; 32 | set => UniversalUI.SetUIActive(this.ID, value); 33 | } 34 | 35 | public UIBase(string id, Action updateMethod) 36 | { 37 | if (string.IsNullOrEmpty(id)) 38 | throw new ArgumentException("Cannot register a UI with a null or empty id!"); 39 | 40 | if (UniversalUI.registeredUIs.ContainsKey(id)) 41 | throw new ArgumentException($"A UI with the id '{id}' is already registered!"); 42 | 43 | ID = id; 44 | UpdateMethod = updateMethod; 45 | 46 | RootObject = UIFactory.CreateUIObject($"{id}_Root", UniversalUI.CanvasRoot); 47 | RootObject.SetActive(false); 48 | 49 | RootRect = RootObject.GetComponent(); 50 | 51 | this.Canvas = RootObject.AddComponent(); 52 | this.Canvas.renderMode = RenderMode.ScreenSpaceCamera; 53 | this.Canvas.referencePixelsPerUnit = 100; 54 | this.Canvas.sortingOrder = TOP_SORTORDER; 55 | this.Canvas.overrideSorting = true; 56 | 57 | CanvasScaler scaler = RootObject.AddComponent(); 58 | scaler.referenceResolution = new Vector2(1920, 1080); 59 | scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand; 60 | 61 | RootObject.AddComponent(); 62 | 63 | RectTransform uiRect = RootObject.GetComponent(); 64 | uiRect.anchorMin = Vector2.zero; 65 | uiRect.anchorMax = Vector2.one; 66 | uiRect.pivot = new Vector2(0.5f, 0.5f); 67 | 68 | Panels = CreatePanelManager(); 69 | 70 | RootObject.SetActive(true); 71 | 72 | UniversalUI.registeredUIs.Add(id, this); 73 | UniversalUI.uiBases.Add(this); 74 | } 75 | 76 | /// 77 | /// Can be overridden if you want a different type of PanelManager implementation. 78 | /// 79 | protected virtual PanelManager CreatePanelManager() => new(this); 80 | 81 | 82 | /// 83 | /// Set this UIBase to be on top of all others. 84 | /// 85 | public void SetOnTop() 86 | { 87 | RootObject.transform.SetAsLastSibling(); 88 | 89 | foreach (UIBase ui in UniversalUI.uiBases) 90 | { 91 | int offset = UniversalUI.CanvasRoot.transform.childCount - ui.RootRect.GetSiblingIndex(); 92 | ui.Canvas.sortingOrder = TOP_SORTORDER - offset; 93 | } 94 | 95 | // Sort UniversalUI dictionary so update order is correct 96 | UniversalUI.uiBases.Sort((a, b) => b.RootObject.transform.GetSiblingIndex().CompareTo(a.RootObject.transform.GetSiblingIndex())); 97 | } 98 | 99 | internal void Update() 100 | { 101 | try 102 | { 103 | Panels.Update(); 104 | 105 | UpdateMethod?.Invoke(); 106 | } 107 | catch (Exception ex) 108 | { 109 | Universe.LogWarning($"Exception invoking update method for {ID}: {ex}"); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Reflection/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | 7 | namespace UniverseLib 8 | { 9 | public static class ReflectionExtensions 10 | { 11 | /// 12 | /// Get the true underlying Type of the provided object. 13 | /// 14 | public static Type GetActualType(this object obj) 15 | => ReflectionUtility.Instance.Internal_GetActualType(obj); 16 | 17 | /// 18 | /// Attempt to cast the provided object to it's true underlying Type. 19 | /// 20 | public static object TryCast(this object obj) 21 | => ReflectionUtility.Instance.Internal_TryCast(obj, ReflectionUtility.Instance.Internal_GetActualType(obj)); 22 | 23 | /// 24 | /// Attempt to cast the provided object to the provided Type . 25 | /// 26 | public static object TryCast(this object obj, Type castTo) 27 | => ReflectionUtility.Instance.Internal_TryCast(obj, castTo); 28 | 29 | /// 30 | /// Attempt to cast the provided object to Type . 31 | /// 32 | public static T TryCast(this object obj) 33 | { 34 | try 35 | { 36 | return (T)ReflectionUtility.Instance.Internal_TryCast(obj, typeof(T)); 37 | } 38 | catch 39 | { 40 | return default; 41 | } 42 | } 43 | 44 | // ------- Misc extensions -------- 45 | 46 | [Obsolete("This method is no longer necessary, just use Assembly.GetTypes().", false)] 47 | public static IEnumerable TryGetTypes(this Assembly asm) 48 | { 49 | // This is redundant since we patch Assembly.GetTypes with a Finalizer anyway. 50 | // Let's just call the method and let our patch handle it should exceptions occur. 51 | return asm.GetTypes(); 52 | } 53 | 54 | /// 55 | /// Check if the two objects are reference-equal, including checking for UnityEngine.Object-equality and Il2CppSystem.Object-equality. 56 | /// 57 | public static bool ReferenceEqual(this object objA, object objB) 58 | { 59 | if (object.ReferenceEquals(objA, objB)) 60 | return true; 61 | 62 | if (objA is UnityEngine.Object unityA && objB is UnityEngine.Object unityB) 63 | { 64 | if (unityA && unityB && unityA.m_CachedPtr == unityB.m_CachedPtr) 65 | return true; 66 | } 67 | 68 | #if IL2CPP 69 | if (objA is Il2CppSystem.Object cppA && objB is Il2CppSystem.Object cppB 70 | && cppA.Pointer == cppB.Pointer) 71 | return true; 72 | #endif 73 | 74 | return false; 75 | } 76 | 77 | /// 78 | /// Helper to display a simple "{ExceptionType}: {Message}" of the exception, and optionally use the inner-most exception. 79 | /// 80 | public static string ReflectionExToString(this Exception e, bool innerMost = true) 81 | { 82 | if (e == null) 83 | return "The exception was null."; 84 | 85 | if (innerMost) 86 | e = e.GetInnerMostException(); 87 | 88 | return $"{e.GetType()}: {e.Message}"; 89 | } 90 | 91 | /// 92 | /// Get the inner-most exception from the provided exception, if there are any. This is recursive. 93 | /// 94 | /// 95 | /// 96 | public static Exception GetInnerMostException(this Exception e) 97 | { 98 | while (e != null) 99 | { 100 | if (e.InnerException == null) 101 | break; 102 | #if IL2CPP 103 | if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException) 104 | break; 105 | #endif 106 | e = e.InnerException; 107 | } 108 | 109 | return e; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Runtime/Il2Cpp/Il2CppManagedEnumerator.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Reflection.Emit; 5 | using HarmonyLib; 6 | using Il2CppSystem; 7 | using IntPtr = System.IntPtr; 8 | using Type = System.Type; 9 | using ArgumentNullException = System.ArgumentNullException; 10 | using NotSupportedException = System.NotSupportedException; 11 | using Il2CppIEnumerator = Il2CppSystem.Collections.IEnumerator; 12 | using Il2CppInterop.Runtime.Injection; 13 | using Il2CppInterop.Runtime; 14 | 15 | // Credit to Horse/BepInEx for this wrapper. 16 | // https://github.com/BepInEx/BepInEx/tree/master/BepInEx.IL2CPP/Utils/Collections 17 | 18 | namespace UniverseLib.Runtime.Il2Cpp 19 | { 20 | public static class CollectionExtensions 21 | { 22 | public static Il2CppIEnumerator WrapToIl2Cpp(this IEnumerator self) 23 | => new(new Il2CppManagedEnumerator(self).Pointer); 24 | } 25 | 26 | public class Il2CppManagedEnumerator : Object 27 | { 28 | private static readonly Dictionary> boxers = new(); 29 | 30 | private readonly IEnumerator enumerator; 31 | 32 | static Il2CppManagedEnumerator() 33 | { 34 | try 35 | { 36 | ClassInjector.RegisterTypeInIl2Cpp(new RegisterTypeOptions 37 | { 38 | Interfaces = new[] { typeof(Il2CppIEnumerator) } 39 | }); 40 | 41 | } 42 | catch (System.Exception ex) 43 | { 44 | Universe.LogWarning(ex); 45 | } 46 | } 47 | 48 | public Il2CppManagedEnumerator(IntPtr ptr) : base(ptr) { } 49 | 50 | public Il2CppManagedEnumerator(IEnumerator enumerator) 51 | : base(ClassInjector.DerivedConstructorPointer()) 52 | { 53 | this.enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator)); 54 | ClassInjector.DerivedConstructorBody(this); 55 | } 56 | 57 | public Object Current => enumerator.Current switch 58 | { 59 | Il2CppIEnumerator i => i.Cast(), 60 | IEnumerator e => new Il2CppManagedEnumerator(e), 61 | Object il2cppObj => il2cppObj, 62 | { } obj => ManagedToIl2CppObject(obj), 63 | null => null 64 | }; 65 | 66 | public bool MoveNext() => enumerator.MoveNext(); 67 | 68 | public void Reset() => enumerator.Reset(); 69 | 70 | private static Object ManagedToIl2CppObject(object obj) 71 | { 72 | Type t = obj.GetType(); 73 | if (obj is string s) 74 | return new Object(IL2CPP.ManagedStringToIl2Cpp(s)); 75 | if (t.IsPrimitive) 76 | return GetValueBoxer(t)(obj); 77 | throw new NotSupportedException($"Type {t} cannot be converted directly to an Il2Cpp object"); 78 | } 79 | 80 | private static System.Func GetValueBoxer(Type t) 81 | { 82 | if (boxers.TryGetValue(t, out System.Func conv)) 83 | return conv; 84 | 85 | DynamicMethod dm = new($"Il2CppUnbox_{t.FullDescription()}", typeof(Object), 86 | new[] { typeof(object) }); 87 | ILGenerator il = dm.GetILGenerator(); 88 | LocalBuilder loc = il.DeclareLocal(t); 89 | System.Reflection.FieldInfo classField = typeof(Il2CppClassPointerStore<>).MakeGenericType(t) 90 | .GetField(nameof(Il2CppClassPointerStore 91 | .NativeClassPtr)); 92 | il.Emit(OpCodes.Ldsfld, classField); 93 | il.Emit(OpCodes.Ldarg_0); 94 | il.Emit(OpCodes.Unbox_Any, t); 95 | il.Emit(OpCodes.Stloc, loc); 96 | il.Emit(OpCodes.Ldloca, loc); 97 | il.Emit(OpCodes.Call, 98 | typeof(IL2CPP).GetMethod(nameof(IL2CPP.il2cpp_value_box))); 99 | il.Emit(OpCodes.Newobj, typeof(Object).GetConstructor(new[] { typeof(IntPtr) })); 100 | il.Emit(OpCodes.Ret); 101 | 102 | System.Func converter = dm.CreateDelegate(typeof(System.Func)) as System.Func; 103 | boxers[t] = converter; 104 | return converter; 105 | } 106 | } 107 | } 108 | 109 | #endif -------------------------------------------------------------------------------- /src/Runtime/Il2Cpp/AssetBundle.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using UnityEngine; 7 | using UniverseLib.Runtime.Il2Cpp; 8 | using Il2CppInterop.Runtime; 9 | using Il2CppInterop.Runtime.Attributes; 10 | using Il2CppInterop.Runtime.Injection; 11 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 12 | using IL2CPPType = Il2CppInterop.Runtime.Il2CppType; 13 | 14 | namespace UniverseLib 15 | { 16 | /// 17 | /// Replacement class for AssetBundles in case they were stripped by the game. 18 | /// 19 | public class AssetBundle : UnityEngine.Object 20 | { 21 | static AssetBundle() 22 | { 23 | ClassInjector.RegisterTypeInIl2Cpp(); 24 | } 25 | 26 | // ~~~~~~~~~~~~ Static ~~~~~~~~~~~~ 27 | 28 | // AssetBundle.LoadFromFile(string path) 29 | 30 | internal delegate IntPtr d_LoadFromFile(IntPtr path, uint crc, ulong offset); 31 | 32 | [HideFromIl2Cpp] 33 | public static AssetBundle LoadFromFile(string path) 34 | { 35 | IntPtr ptr = ICallManager.GetICallUnreliable( 36 | "UnityEngine.AssetBundle::LoadFromFile_Internal", 37 | "UnityEngine.AssetBundle::LoadFromFile") 38 | .Invoke(IL2CPP.ManagedStringToIl2Cpp(path), 0u, 0UL); 39 | 40 | return ptr != IntPtr.Zero ? new AssetBundle(ptr) : null; 41 | } 42 | 43 | // AssetBundle.LoadFromMemory(byte[] binary) 44 | 45 | private delegate IntPtr d_LoadFromMemory(IntPtr binary, uint crc); 46 | 47 | [HideFromIl2Cpp] 48 | public static AssetBundle LoadFromMemory(byte[] binary, uint crc = 0) 49 | { 50 | IntPtr ptr = ICallManager.GetICallUnreliable( 51 | "UnityEngine.AssetBundle::LoadFromMemory_Internal", 52 | "UnityEngine.AssetBundle::LoadFromMemory") 53 | .Invoke(((Il2CppStructArray)binary).Pointer, crc); 54 | 55 | return ptr != IntPtr.Zero ? new AssetBundle(ptr) : null; 56 | } 57 | 58 | // AssetBundle.GetAllLoadedAssetBundles() 59 | 60 | public delegate IntPtr d_GetAllLoadedAssetBundles_Native(); 61 | 62 | [HideFromIl2Cpp] 63 | public static AssetBundle[] GetAllLoadedAssetBundles() 64 | { 65 | IntPtr ptr = ICallManager.GetICall("UnityEngine.AssetBundle::GetAllLoadedAssetBundles_Native") 66 | .Invoke(); 67 | 68 | return ptr != IntPtr.Zero ? (AssetBundle[])new Il2CppReferenceArray(ptr) : null; 69 | } 70 | 71 | // ~~~~~~~~~~~~ Instance ~~~~~~~~~~~~ 72 | 73 | public readonly IntPtr m_bundlePtr = IntPtr.Zero; 74 | 75 | public AssetBundle(IntPtr ptr) : base(ptr) { m_bundlePtr = ptr; } 76 | 77 | // LoadAllAssets() 78 | 79 | internal delegate IntPtr d_LoadAssetWithSubAssets_Internal(IntPtr _this, IntPtr name, IntPtr type); 80 | 81 | [HideFromIl2Cpp] 82 | public UnityEngine.Object[] LoadAllAssets() 83 | { 84 | IntPtr ptr = ICallManager.GetICall("UnityEngine.AssetBundle::LoadAssetWithSubAssets_Internal") 85 | .Invoke(m_bundlePtr, IL2CPP.ManagedStringToIl2Cpp(""), IL2CPPType.Of().Pointer); 86 | 87 | return ptr != IntPtr.Zero ? (UnityEngine.Object[])new Il2CppReferenceArray(ptr) : new UnityEngine.Object[0]; 88 | } 89 | 90 | // LoadAsset(string name, Type type) 91 | 92 | internal delegate IntPtr d_LoadAsset_Internal(IntPtr _this, IntPtr name, IntPtr type); 93 | 94 | [HideFromIl2Cpp] 95 | public T LoadAsset(string name) where T : UnityEngine.Object 96 | { 97 | IntPtr ptr = ICallManager.GetICall("UnityEngine.AssetBundle::LoadAsset_Internal") 98 | .Invoke(m_bundlePtr, IL2CPP.ManagedStringToIl2Cpp(name), IL2CPPType.Of().Pointer); 99 | 100 | return ptr != IntPtr.Zero ? new UnityEngine.Object(ptr).TryCast() : null; 101 | } 102 | 103 | // Unload(bool unloadAllLoadedObjects); 104 | 105 | internal delegate void d_Unload(IntPtr _this, bool unloadAllLoadedObjects); 106 | 107 | [HideFromIl2Cpp] 108 | public void Unload(bool unloadAllLoadedObjects) 109 | { 110 | ICallManager.GetICall("UnityEngine.AssetBundle::Unload") 111 | .Invoke(this.m_bundlePtr, unloadAllLoadedObjects); 112 | } 113 | } 114 | } 115 | #endif -------------------------------------------------------------------------------- /src/UI/Widgets/AutoSliderScrollbar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using UnityEngine.Events; 7 | using UnityEngine.UI; 8 | using UniverseLib; 9 | using UniverseLib.UI; 10 | using UniverseLib.UI.Models; 11 | 12 | namespace UniverseLib.UI.Widgets 13 | { 14 | /// 15 | /// A scrollbar which automatically resizes itself (and its handle) depending on the size of the content and viewport. 16 | /// 17 | public class AutoSliderScrollbar : UIBehaviourModel 18 | { 19 | public override GameObject UIRoot => Slider?.gameObject; 20 | 21 | public Slider Slider { get; } 22 | public Scrollbar Scrollbar { get; } 23 | public RectTransform ContentRect { get; } 24 | public RectTransform ViewportRect { get; } 25 | 26 | public AutoSliderScrollbar(Scrollbar scrollbar, Slider slider, RectTransform contentRect, RectTransform viewportRect) 27 | { 28 | this.Scrollbar = scrollbar; 29 | this.Slider = slider; 30 | this.ContentRect = contentRect; 31 | this.ViewportRect = viewportRect; 32 | 33 | this.Scrollbar.onValueChanged.AddListener(this.OnScrollbarValueChanged); 34 | this.Slider.onValueChanged.AddListener(this.OnSliderValueChanged); 35 | 36 | //this.RefreshVisibility(); 37 | this.Slider.Set(0f, false); 38 | 39 | UpdateSliderHandle(); 40 | } 41 | 42 | private float lastAnchorPosition; 43 | private float lastContentHeight; 44 | private float lastViewportHeight; 45 | private bool _refreshWanted; 46 | 47 | public override void Update() 48 | { 49 | if (!Enabled) 50 | return; 51 | 52 | _refreshWanted = false; 53 | if (ContentRect.localPosition.y != lastAnchorPosition) 54 | { 55 | lastAnchorPosition = ContentRect.localPosition.y; 56 | _refreshWanted = true; 57 | } 58 | if (ContentRect.rect.height != lastContentHeight) 59 | { 60 | lastContentHeight = ContentRect.rect.height; 61 | _refreshWanted = true; 62 | } 63 | if (ViewportRect.rect.height != lastViewportHeight) 64 | { 65 | lastViewportHeight = ViewportRect.rect.height; 66 | _refreshWanted = true; 67 | } 68 | 69 | if (_refreshWanted) 70 | UpdateSliderHandle(); 71 | } 72 | 73 | public void UpdateSliderHandle() 74 | { 75 | // calculate handle size based on viewport / total data height 76 | float totalHeight = ContentRect.rect.height; 77 | float viewportHeight = ViewportRect.rect.height; 78 | 79 | if (totalHeight <= viewportHeight) 80 | { 81 | Slider.handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 0f); 82 | Slider.value = 0f; 83 | Slider.interactable = false; 84 | return; 85 | } 86 | 87 | float handleHeight = viewportHeight * Math.Min(1, viewportHeight / totalHeight); 88 | handleHeight = Math.Max(15f, handleHeight); 89 | 90 | // resize the handle container area for the size of the handle (bigger handle = smaller container) 91 | RectTransform container = Slider.m_HandleContainerRect; 92 | container.offsetMax = new Vector2(container.offsetMax.x, -(handleHeight * 0.5f)); 93 | container.offsetMin = new Vector2(container.offsetMin.x, handleHeight * 0.5f); 94 | 95 | // set handle size 96 | Slider.handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, handleHeight); 97 | 98 | // if slider is 100% height then make it not interactable 99 | Slider.interactable = !Mathf.Approximately(handleHeight, viewportHeight); 100 | 101 | float val = 0f; 102 | if (totalHeight > 0f) 103 | val = (float)((decimal)ContentRect.localPosition.y / (decimal)(totalHeight - ViewportRect.rect.height)); 104 | 105 | Slider.value = val; 106 | } 107 | 108 | public void OnScrollbarValueChanged(float value) 109 | { 110 | value = 1f - value; 111 | if (this.Slider.value != value) 112 | this.Slider.Set(value, false); 113 | //OnValueChanged?.Invoke(value); 114 | } 115 | 116 | public void OnSliderValueChanged(float value) 117 | { 118 | value = 1f - value; 119 | this.Scrollbar.value = value; 120 | //OnValueChanged?.Invoke(value); 121 | } 122 | 123 | public override void ConstructUI(GameObject parent) 124 | { 125 | throw new NotImplementedException(); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /src/UI/Widgets/InputFieldScroller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using UnityEngine; 7 | using UnityEngine.Events; 8 | using UnityEngine.EventSystems; 9 | using UnityEngine.UI; 10 | using UniverseLib.UI.Models; 11 | #if IL2CPP 12 | using Il2CppInterop.Runtime; 13 | #endif 14 | 15 | namespace UniverseLib.UI.Widgets 16 | { 17 | /// 18 | /// A wrapper for a scrollable InputField created with .

19 | /// 20 | /// This is otherwise a normal InputField, but which handles scrolling more nicely than a vanilla one. 21 | ///
22 | public class InputFieldScroller : UIBehaviourModel 23 | { 24 | public override GameObject UIRoot => InputField?.UIRoot; 25 | 26 | /// 27 | /// Invoked whenever this InputField is scrolled through (ie, 28 | /// 29 | public Action OnScroll; 30 | 31 | public InputFieldRef InputField { get; } 32 | public AutoSliderScrollbar Slider { get; } 33 | public RectTransform ContentRect { get; } 34 | public RectTransform ViewportRect { get; } 35 | 36 | public static CanvasScaler RootScaler { get; private set; } 37 | 38 | internal string lastText; 39 | internal bool updateWanted; 40 | internal bool wantJumpToBottom; 41 | private float desiredContentHeight; 42 | private float lastContentPosition; 43 | private float lastViewportHeight; 44 | 45 | public InputFieldScroller(AutoSliderScrollbar sliderScroller, InputFieldRef inputField) 46 | { 47 | this.Slider = sliderScroller; 48 | this.InputField = inputField; 49 | 50 | inputField.OnValueChanged += OnTextChanged; 51 | 52 | ContentRect = inputField.UIRoot.GetComponent(); 53 | ViewportRect = ContentRect.transform.parent.GetComponent(); 54 | 55 | if (!RootScaler) 56 | #if IL2CPP 57 | RootScaler = inputField.Component.gameObject.GetComponentInParent(Il2CppType.Of()).TryCast(); 58 | #else 59 | RootScaler = inputField.Component.gameObject.GetComponentInParent(); 60 | #endif 61 | } 62 | 63 | public override void Update() 64 | { 65 | if (this.ContentRect.localPosition.y != lastContentPosition) 66 | { 67 | lastContentPosition = ContentRect.localPosition.y; 68 | OnScroll?.Invoke(); 69 | } 70 | 71 | if (ViewportRect.rect.height != lastViewportHeight) 72 | { 73 | lastViewportHeight = ViewportRect.rect.height; 74 | updateWanted = true; 75 | } 76 | 77 | if (updateWanted) 78 | { 79 | updateWanted = false; 80 | ProcessInputText(); 81 | 82 | float desiredHeight = Math.Max(desiredContentHeight, ViewportRect.rect.height); 83 | 84 | if (ContentRect.rect.height < desiredHeight) 85 | { 86 | ContentRect.sizeDelta = new Vector2(ContentRect.sizeDelta.x, desiredHeight); 87 | this.Slider.UpdateSliderHandle(); 88 | } 89 | else if (ContentRect.rect.height > desiredHeight) 90 | { 91 | ContentRect.sizeDelta = new Vector2(ContentRect.sizeDelta.x, desiredHeight); 92 | this.Slider.UpdateSliderHandle(); 93 | } 94 | } 95 | 96 | if (wantJumpToBottom) 97 | { 98 | Slider.Slider.value = 1f; 99 | wantJumpToBottom = false; 100 | } 101 | } 102 | 103 | internal void OnTextChanged(string text) 104 | { 105 | lastText = text; 106 | updateWanted = true; 107 | } 108 | 109 | internal void ProcessInputText() 110 | { 111 | Rect curInputRect = InputField.Component.textComponent.rectTransform.rect; 112 | float scaleFactor = RootScaler.scaleFactor; 113 | 114 | // Current text settings 115 | TextGenerationSettings texGenSettings = InputField.Component.textComponent.GetGenerationSettings(curInputRect.size); 116 | texGenSettings.generateOutOfBounds = false; 117 | texGenSettings.scaleFactor = scaleFactor; 118 | 119 | // Preferred text rect height 120 | TextGenerator textGen = InputField.Component.textComponent.cachedTextGeneratorForLayout; 121 | desiredContentHeight = textGen.GetPreferredHeight(lastText, texGenSettings) + 10; 122 | } 123 | 124 | public override void ConstructUI(GameObject parent) 125 | { 126 | throw new NotImplementedException(); 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /src/Runtime/Mono/MonoProvider.cs: -------------------------------------------------------------------------------- 1 | #if MONO 2 | using HarmonyLib; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text; 9 | using UnityEngine; 10 | using UnityEngine.Events; 11 | using UnityEngine.EventSystems; 12 | using UnityEngine.SceneManagement; 13 | using UnityEngine.UI; 14 | using UniverseLib; 15 | 16 | namespace UniverseLib.Runtime.Mono 17 | { 18 | internal class MonoProvider : RuntimeHelper 19 | { 20 | protected internal override void OnInitialize() 21 | { 22 | new MonoTextureHelper(); 23 | } 24 | 25 | /// 26 | protected internal override Coroutine Internal_StartCoroutine(IEnumerator routine) 27 | => UniversalBehaviour.Instance.StartCoroutine(routine); 28 | 29 | /// 30 | protected internal override void Internal_StopCoroutine(Coroutine coroutine) 31 | => UniversalBehaviour.Instance.StopCoroutine(coroutine); 32 | 33 | /// 34 | protected internal override T Internal_AddComponent(GameObject obj, Type type) 35 | => (T)obj.AddComponent(type); 36 | 37 | /// 38 | protected internal override ScriptableObject Internal_CreateScriptable(Type type) 39 | => ScriptableObject.CreateInstance(type); 40 | 41 | /// 42 | protected internal override void Internal_GraphicRaycast(GraphicRaycaster raycaster, PointerEventData data, List list) 43 | => raycaster.Raycast(data, list); 44 | 45 | /// 46 | protected internal override string Internal_LayerToName(int layer) 47 | => LayerMask.LayerToName(layer); 48 | 49 | /// 50 | protected internal override UnityEngine.Object[] Internal_FindObjectsOfTypeAll(Type type) 51 | => Resources.FindObjectsOfTypeAll(type); 52 | 53 | protected internal override T[] Internal_FindObjectsOfTypeAll() 54 | => Resources.FindObjectsOfTypeAll(); 55 | 56 | /// 57 | protected internal override GameObject[] Internal_GetRootGameObjects(Scene scene) 58 | => scene.isLoaded ? scene.GetRootGameObjects() : new GameObject[0]; 59 | 60 | /// 61 | protected internal override int Internal_GetRootCount(Scene scene) 62 | => scene.rootCount; 63 | 64 | /// 65 | protected internal override void Internal_SetColorBlock(Selectable selectable, ColorBlock colors) 66 | => selectable.colors = colors; 67 | 68 | /// 69 | protected internal override void Internal_SetColorBlock(Selectable selectable, Color? normal = null, Color? highlighted = null, Color? pressed = null, 70 | Color? disabled = null) 71 | { 72 | ColorBlock colors = selectable.colors; 73 | 74 | if (normal != null) 75 | colors.normalColor = (Color)normal; 76 | 77 | if (highlighted != null) 78 | colors.highlightedColor = (Color)highlighted; 79 | 80 | if (pressed != null) 81 | colors.pressedColor = (Color)pressed; 82 | 83 | if (disabled != null) 84 | colors.disabledColor = (Color)disabled; 85 | 86 | Internal_SetColorBlock(selectable, colors); 87 | } 88 | } 89 | } 90 | 91 | public static class MonoExtensions 92 | { 93 | // Helpers to use the same style of AddListener that IL2CPP uses. 94 | 95 | public static void AddListener(this UnityEvent _event, Action listener) 96 | => _event.AddListener(new UnityAction(listener)); 97 | 98 | public static void AddListener(this UnityEvent _event, Action listener) 99 | => _event.AddListener(new UnityAction(listener)); 100 | 101 | public static void RemoveListener(this UnityEvent _event, Action listener) 102 | => _event.RemoveListener(new UnityAction(listener)); 103 | 104 | public static void RemoveListener(this UnityEvent _event, Action listener) 105 | => _event.RemoveListener(new UnityAction(listener)); 106 | 107 | // Doesn't exist in NET 3.5 108 | 109 | public static void Clear(this StringBuilder sb) 110 | => sb.Remove(0, sb.Length); 111 | 112 | // These properties don't exist in some earlier games, so null check before trying to set them. 113 | 114 | static PropertyInfo p_childControlHeight = AccessTools.Property(typeof(HorizontalOrVerticalLayoutGroup), "childControlHeight"); 115 | static PropertyInfo p_childControlWidth = AccessTools.Property(typeof(HorizontalOrVerticalLayoutGroup), "childControlWidth"); 116 | 117 | public static void SetChildControlHeight(this HorizontalOrVerticalLayoutGroup group, bool value) 118 | => p_childControlHeight?.SetValue(group, value, null); 119 | 120 | public static void SetChildControlWidth(this HorizontalOrVerticalLayoutGroup group, bool value) 121 | => p_childControlWidth?.SetValue(group, value, null); 122 | } 123 | 124 | #endif -------------------------------------------------------------------------------- /src/Runtime/AmbiguousMemberHandler.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | 8 | namespace UniverseLib.Runtime 9 | { 10 | /// 11 | /// Handles getting/setting arbitrary field/property members which may have different names or member types in different Unity versions. 12 | /// 13 | /// The containing Type for the member. 14 | /// The Type of the value for the member. 15 | public class AmbiguousMemberHandler 16 | { 17 | public readonly MemberInfo member; 18 | public readonly MemberTypes memberType; 19 | 20 | public AmbiguousMemberHandler(bool canWrite, bool canRead, params string[] possibleNames) 21 | { 22 | foreach (string name in possibleNames) 23 | { 24 | if (typeof(TClass).GetProperty(name, AccessTools.all) is PropertyInfo pi 25 | && typeof(TValue).IsAssignableFrom(pi.PropertyType) 26 | && (!canWrite || pi.CanWrite) 27 | && (!canRead || pi.CanRead)) 28 | { 29 | member = pi; 30 | memberType = MemberTypes.Property; 31 | break; 32 | } 33 | if (typeof(TClass).GetField(name, AccessTools.all) is FieldInfo fi 34 | && typeof(TValue).IsAssignableFrom(fi.FieldType) 35 | && (!canWrite || !(fi.IsLiteral && !fi.IsInitOnly))) // (don't need to write or is not constant) 36 | { 37 | member = fi; 38 | memberType = MemberTypes.Field; 39 | break; 40 | } 41 | } 42 | 43 | //if (member == null) 44 | // Universe.LogWarning($"AmbigiousMemberHandler could not find any member on 45 | // {typeof(TClass).Name} from possibleNames: {string.Join(", ", possibleNames)}"); 46 | //else 47 | // Universe.Log($"Resolved AmbiguousMemberHandler: {member}"); 48 | } 49 | 50 | /// 51 | /// Gets the value of an instance member from the provided instance. 52 | /// 53 | /// The instance to get from. 54 | /// The value from the member, if successful. 55 | public TValue GetValue(object instance) 56 | => DoGetValue(instance); 57 | 58 | /// 59 | /// Gets the value of a static member. 60 | /// 61 | /// The value from the member, if successful. 62 | public TValue GetValue() 63 | => DoGetValue(null); 64 | 65 | private TValue DoGetValue(object instance) 66 | { 67 | if (member == null) 68 | return default; 69 | 70 | try 71 | { 72 | object value = memberType switch 73 | { 74 | MemberTypes.Property => (member as PropertyInfo).GetValue(instance, null), 75 | MemberTypes.Field => (member as FieldInfo).GetValue(instance), 76 | _ => throw new NotImplementedException() 77 | }; 78 | 79 | return value == null ? default : (TValue)value; 80 | } 81 | catch // (Exception ex) 82 | { 83 | // Universe.LogWarning($"Exception getting value from member {member}: {ex}"); 84 | return default; 85 | } 86 | } 87 | 88 | /// 89 | /// Sets the value of an instance member to the instance. 90 | /// 91 | /// The instance to set to. 92 | /// The value to set to the instance. 93 | public void SetValue(object instance, TValue value) 94 | => DoSetValue(instance, value); 95 | 96 | /// 97 | /// Sets the value of a static member. 98 | /// 99 | /// The value to set to the instance. 100 | public void SetValue(TValue value) 101 | => DoSetValue(null, value); 102 | 103 | void DoSetValue(object instance, TValue value) 104 | { 105 | if (member == null) 106 | return; 107 | 108 | try 109 | { 110 | switch (memberType) 111 | { 112 | case MemberTypes.Property: 113 | (member as PropertyInfo).SetValue(instance, value, null); 114 | break; 115 | 116 | case MemberTypes.Field: 117 | (member as FieldInfo).SetValue(instance, value); 118 | break; 119 | } 120 | } 121 | catch // (Exception ex) 122 | { 123 | // Universe.LogWarning($"Exception setting value '{value}' to member {member.DeclaringType.Name}.{member}: {ex}"); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Input/CursorUnlocker.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using System.Collections; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEngine; 7 | using UnityEngine.EventSystems; 8 | using UniverseLib; 9 | using UniverseLib.Config; 10 | using UniverseLib.Input; 11 | using UniverseLib.Runtime; 12 | using UniverseLib.UI; 13 | using UniverseLib.Utility; 14 | 15 | namespace UniverseLib.Input 16 | { 17 | /// 18 | /// Handles taking control of the mouse/cursor and EventSystem (depending on Config settings) when a UniversalUI is being used. 19 | /// 20 | public class CursorUnlocker 21 | { 22 | /// 23 | /// True if a UI is being displayed and is true. 24 | /// 25 | public static bool ShouldUnlock => ConfigManager.Force_Unlock_Mouse && UniversalUI.AnyUIShowing; 26 | 27 | private static bool currentlySettingCursor; 28 | private static CursorLockMode lastLockMode; 29 | private static bool lastVisibleState; 30 | 31 | private static WaitForEndOfFrame waitForEndOfFrame = new(); 32 | 33 | [Obsolete("Moved to EventSystemHelper")] 34 | public static EventSystem CurrentEventSystem => EventSystemHelper.CurrentEventSystem; 35 | 36 | internal static void Init() 37 | { 38 | lastLockMode = Cursor.lockState; 39 | lastVisibleState = Cursor.visible; 40 | 41 | InitPatches(); 42 | UpdateCursorControl(); 43 | 44 | try 45 | { 46 | RuntimeHelper.Instance.Internal_StartCoroutine(UnlockCoroutine()); 47 | } 48 | catch (Exception ex) 49 | { 50 | Universe.LogWarning($"Exception setting up Aggressive Mouse Unlock: {ex}"); 51 | } 52 | } 53 | 54 | /// 55 | /// Uses WaitForEndOfFrame in a Coroutine to aggressively set the Cursor state every frame. 56 | /// 57 | private static IEnumerator UnlockCoroutine() 58 | { 59 | while (true) 60 | { 61 | yield return waitForEndOfFrame ??= new WaitForEndOfFrame(); 62 | if (UniversalUI.AnyUIShowing || !EventSystemHelper.lastEventSystem) 63 | UpdateCursorControl(); 64 | } 65 | } 66 | 67 | /// 68 | /// Checks current ShouldUnlock state and sets the Cursor and EventSystem as required. 69 | /// 70 | internal static void UpdateCursorControl() 71 | { 72 | try 73 | { 74 | currentlySettingCursor = true; 75 | 76 | if (ShouldUnlock) 77 | { 78 | Cursor.lockState = CursorLockMode.None; 79 | Cursor.visible = true; 80 | 81 | if (!ConfigManager.Disable_EventSystem_Override) 82 | EventSystemHelper.EnableEventSystem(); 83 | } 84 | else 85 | { 86 | Cursor.lockState = lastLockMode; 87 | Cursor.visible = lastVisibleState; 88 | 89 | if (!ConfigManager.Disable_EventSystem_Override) 90 | EventSystemHelper.ReleaseEventSystem(); 91 | } 92 | 93 | currentlySettingCursor = false; 94 | } 95 | catch (Exception e) 96 | { 97 | Universe.Log($"Exception setting Cursor state: {e}"); 98 | } 99 | } 100 | 101 | // Patches 102 | 103 | internal static void InitPatches() 104 | { 105 | Universe.Patch(typeof(Cursor), 106 | "lockState", 107 | MethodType.Setter, 108 | prefix: AccessTools.Method(typeof(CursorUnlocker), nameof(Prefix_set_lockState))); 109 | 110 | Universe.Patch(typeof(Cursor), 111 | "visible", 112 | MethodType.Setter, 113 | prefix: AccessTools.Method(typeof(CursorUnlocker), nameof(Prefix_set_visible))); 114 | } 115 | 116 | // Force mouse to stay unlocked and visible while UnlockMouse and ShowMenu are true. 117 | // Also keep track of when anything else tries to set Cursor state, this will be the 118 | // value that we set back to when we close the menu or disable force-unlock. 119 | 120 | internal static void Prefix_set_lockState(ref CursorLockMode value) 121 | { 122 | try 123 | { 124 | if (!currentlySettingCursor) 125 | { 126 | lastLockMode = value; 127 | 128 | if (ShouldUnlock) 129 | value = CursorLockMode.None; 130 | } 131 | } 132 | catch { } 133 | } 134 | 135 | internal static void Prefix_set_visible(ref bool value) 136 | { 137 | try 138 | { 139 | if (!currentlySettingCursor) 140 | { 141 | lastVisibleState = value; 142 | 143 | if (ShouldUnlock) 144 | value = true; 145 | } 146 | } 147 | catch { } 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /src/Utility/UnityHelpers.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using UnityEngine; 9 | using UnityEngine.Events; 10 | using UnityEngine.UI; 11 | using Object = UnityEngine.Object; 12 | 13 | namespace UniverseLib.Utility 14 | { 15 | public static class UnityHelpers 16 | { 17 | // Time helpers, can't use Time.time since timeScale will affect it. 18 | 19 | /// 20 | /// Returns true if the provided occured more than 10ms before . 21 | /// 22 | /// Should be a value from which you stored earlier. 23 | public static bool OccuredEarlierThanDefault(this float time) 24 | { 25 | return Time.realtimeSinceStartup - 0.01f >= time; 26 | } 27 | 28 | /// 29 | /// Returns true if the provided occured at least before . 30 | /// 31 | /// Should be a value from which you stored earlier. 32 | public static bool OccuredEarlierThan(this float time, float secondsAgo) 33 | { 34 | return Time.realtimeSinceStartup - secondsAgo >= time; 35 | } 36 | 37 | /// 38 | /// Check if an object is null, and if it's a UnityEngine.Object then also check if it was destroyed. 39 | /// 40 | public static bool IsNullOrDestroyed(this object obj, bool suppressWarning = true) 41 | { 42 | try 43 | { 44 | if (obj == null) 45 | { 46 | if (!suppressWarning) 47 | Universe.LogWarning("The target instance is null!"); 48 | 49 | return true; 50 | } 51 | else if (obj is Object unityObj && !unityObj) 52 | { 53 | if (!suppressWarning) 54 | Universe.LogWarning("The target UnityEngine.Object was destroyed!"); 55 | 56 | return true; 57 | } 58 | return false; 59 | } 60 | catch 61 | { 62 | return true; 63 | } 64 | } 65 | 66 | /// 67 | /// Get the full Transform heirarchy path for this provided Transform. 68 | /// 69 | public static string GetTransformPath(this Transform transform, bool includeSelf = false) 70 | { 71 | StringBuilder sb = new(); 72 | if (includeSelf) 73 | sb.Append(transform.name); 74 | 75 | while (transform.parent) 76 | { 77 | transform = transform.parent; 78 | sb.Insert(0, '/'); 79 | sb.Insert(0, transform.name); 80 | } 81 | 82 | return sb.ToString(); 83 | } 84 | 85 | /// 86 | /// Converts Color to 6-digit RGB hex code (without # symbol). Eg, RGBA(1,0,0,1) -> FF0000 87 | /// 88 | public static string ToHex(this Color color) 89 | { 90 | byte r = (byte)Mathf.Clamp(Mathf.RoundToInt(color.r * 255f), 0, 255); 91 | byte g = (byte)Mathf.Clamp(Mathf.RoundToInt(color.g * 255f), 0, 255); 92 | byte b = (byte)Mathf.Clamp(Mathf.RoundToInt(color.b * 255f), 0, 255); 93 | 94 | return $"{r:X2}{g:X2}{b:X2}"; 95 | } 96 | 97 | /// 98 | /// Assumes the string is a 6-digit RGB Hex color code (with optional leading #) which it will parse into a UnityEngine.Color. 99 | /// Eg, FF0000 -> RGBA(1,0,0,1) 100 | /// 101 | public static Color ToColor(this string _string) 102 | { 103 | _string = _string.Replace("#", ""); 104 | 105 | if (_string.Length != 6) 106 | return Color.magenta; 107 | 108 | byte r = byte.Parse(_string.Substring(0, 2), NumberStyles.HexNumber); 109 | byte g = byte.Parse(_string.Substring(2, 2), NumberStyles.HexNumber); 110 | byte b = byte.Parse(_string.Substring(4, 2), NumberStyles.HexNumber); 111 | 112 | Color color = new() 113 | { 114 | r = (float)(r / (decimal)255), 115 | g = (float)(g / (decimal)255), 116 | b = (float)(b / (decimal)255), 117 | a = 1 118 | }; 119 | 120 | return color; 121 | } 122 | 123 | private static PropertyInfo onEndEdit; 124 | 125 | /// 126 | /// Returns the onEndEdit event as a for greater compatibility with all Unity versions. 127 | /// 128 | public static UnityEvent GetOnEndEdit(this InputField _this) 129 | { 130 | if (onEndEdit == null) 131 | onEndEdit = AccessTools.Property(typeof(InputField), "onEndEdit") 132 | ?? throw new Exception("Could not get InputField.onEndEdit property!"); 133 | 134 | return onEndEdit.GetValue(_this, null).TryCast>(); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/UniverseLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Release_IL2CPP 4 | Library 5 | 10.0 6 | 7 | 8 | false 9 | none 10 | AnyCPU 11 | true 12 | false 13 | false 14 | false 15 | none 16 | false 17 | Release_Mono;Release_IL2CPP 18 | 19 | 20 | 21 | net6 22 | ..\Release\UniverseLib.Il2Cpp\ 23 | IL2CPP 24 | UniverseLib.IL2CPP 25 | ..\Release\UniverseLib.Il2Cpp\UniverseLib.IL2CPP.xml 26 | 27 | 28 | net35 29 | ..\Release\UniverseLib.Mono\ 30 | MONO 31 | UniverseLib.Mono 32 | ..\Release\UniverseLib.Mono\UniverseLib.Mono.xml 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ..\lib\mono\UnityEngine_publicized.dll 47 | False 48 | 49 | 50 | ..\lib\mono\UnityEngine.UI_publicized.dll 51 | False 52 | 53 | 54 | 55 | 56 | 57 | ..\lib\interop\Il2Cppmscorlib.dll 58 | False 59 | 60 | 61 | ..\lib\interop\Il2CppSystem.Core.dll 62 | False 63 | 64 | 65 | ..\lib\interop\UnityEngine.dll 66 | False 67 | 68 | 69 | ..\lib\interop\UnityEngine.CoreModule.dll 70 | False 71 | 72 | 73 | ..\lib\interop\UnityEngine.PhysicsModule.dll 74 | False 75 | 76 | 77 | ..\lib\interop\UnityEngine.TextRenderingModule.dll 78 | False 79 | 80 | 81 | ..\lib\interop\UnityEngine.UI.dll 82 | False 83 | 84 | 85 | ..\lib\interop\UnityEngine.UIModule.dll 86 | False 87 | 88 | 89 | ..\lib\interop\UnityEngine.IMGUIModule.dll 90 | False 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/UI/ObjectPool/Pool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | namespace UniverseLib.UI.ObjectPool 8 | { 9 | /// 10 | /// Abstract base class to handle interfacing with a generic , without the generic parameter at compile time. 11 | /// 12 | public abstract class Pool 13 | { 14 | protected static readonly Dictionary pools = new(); 15 | 16 | public static Pool GetPool(Type type) 17 | { 18 | if (!pools.TryGetValue(type, out Pool pool)) 19 | pool = CreatePool(type); 20 | return pool; 21 | } 22 | 23 | protected static Pool CreatePool(Type type) 24 | { 25 | Pool pool = (Pool)Activator.CreateInstance(typeof(Pool<>).MakeGenericType(new[] { type })); 26 | pools.Add(type, pool); 27 | return pool; 28 | } 29 | 30 | /// 31 | /// Borrow an object from the pool, creating a new object if none are available. 32 | /// 33 | public static IPooledObject Borrow(Type type) 34 | => GetPool(type).DoBorrow(); 35 | 36 | /// 37 | /// Borrow an object from the pool, creating a new object if none are available. 38 | /// 39 | public static T Borrow() where T : IPooledObject 40 | => (T)GetPool(typeof(T)).DoBorrow(); 41 | 42 | /// 43 | /// Return the object to the pool. 44 | /// 45 | public static void Return(Type type, IPooledObject obj) 46 | => GetPool(type).DoReturn(obj); 47 | 48 | /// 49 | /// Return the object to the pool. 50 | /// 51 | public static void Return(T obj) where T : IPooledObject 52 | => GetPool(typeof(T)).DoReturn(obj); 53 | 54 | protected abstract IPooledObject DoBorrow(); 55 | protected abstract void DoReturn(IPooledObject obj); 56 | } 57 | 58 | /// 59 | /// Handles object pooling for all objects. Each has its own instance. 60 | /// 61 | public class Pool : Pool where T : IPooledObject 62 | { 63 | // Static 64 | 65 | public static Pool Instance => instance ?? (Pool)CreatePool(typeof(T)); 66 | private static Pool instance; 67 | 68 | public static Pool GetPool() => (Pool)GetPool(typeof(T)); 69 | 70 | /// 71 | /// Borrow an object from the pool, creating a new object if none are available. 72 | /// 73 | public static T Borrow() => Instance.BorrowObject(); 74 | 75 | /// 76 | /// Return the object to the pool. 77 | /// 78 | public static void Return(T obj) => Instance.ReturnObject(obj); 79 | 80 | // Instance 81 | 82 | /// 83 | /// Holds all returned objects in the pool. 84 | /// 85 | public GameObject InactiveHolder { get; } 86 | 87 | /// 88 | /// Optional default height for the object, necessary if this object can be used by a . 89 | /// 90 | public float DefaultHeight { get; } 91 | 92 | /// 93 | /// How many objects are available in the pool. 94 | /// 95 | public int AvailableCount => available.Count; 96 | 97 | private readonly HashSet available = new(); 98 | private readonly HashSet borrowed = new(); 99 | 100 | public Pool() 101 | { 102 | instance = this; 103 | 104 | //UniverseLib.Log($"Creating Pool<{typeof(T).Name}>"); 105 | 106 | InactiveHolder = new GameObject($"PoolHolder_{typeof(T).Name}"); 107 | InactiveHolder.transform.parent = UniversalUI.PoolHolder.transform; 108 | InactiveHolder.hideFlags |= HideFlags.HideAndDontSave; 109 | InactiveHolder.SetActive(false); 110 | 111 | // Create an instance (just the C# class, not content) to grab the default height 112 | T obj = (T)Activator.CreateInstance(typeof(T)); 113 | DefaultHeight = obj.DefaultHeight; 114 | } 115 | 116 | protected override IPooledObject DoBorrow() 117 | => BorrowObject(); 118 | 119 | /// 120 | /// Borrow an object from the pool, creating a new object if none are available. 121 | /// 122 | public T BorrowObject() 123 | { 124 | if (available.Count <= 0) 125 | IncrementPool(); 126 | 127 | T obj = available.First(); 128 | available.Remove(obj); 129 | borrowed.Add(obj); 130 | 131 | return obj; 132 | } 133 | 134 | private void IncrementPool() 135 | { 136 | T obj = (T)Activator.CreateInstance(typeof(T)); 137 | obj.CreateContent(InactiveHolder); 138 | available.Add(obj); 139 | } 140 | 141 | protected override void DoReturn(IPooledObject obj) 142 | => Return((T)obj); 143 | 144 | /// 145 | /// Return the object to the pool. 146 | /// 147 | public void ReturnObject(T obj) 148 | { 149 | if (!borrowed.Contains(obj)) 150 | Universe.LogWarning($"Returning an item to object pool ({typeof(T).Name}) but the item didn't exist in the borrowed list?"); 151 | else 152 | borrowed.Remove(obj); 153 | 154 | available.Add(obj); 155 | obj.UIRoot.transform.SetParent(InactiveHolder.transform, false); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/RuntimeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using UnityEngine; 7 | using UnityEngine.EventSystems; 8 | using UnityEngine.SceneManagement; 9 | using UnityEngine.UI; 10 | 11 | namespace UniverseLib 12 | { 13 | /// 14 | /// Class to help with some differences between Mono and Il2Cpp Runtimes. 15 | /// 16 | public abstract class RuntimeHelper 17 | { 18 | internal static RuntimeHelper Instance { get; private set; } 19 | 20 | internal static void Init() 21 | { 22 | #if IL2CPP 23 | Instance = new Runtime.Il2Cpp.Il2CppProvider(); 24 | #else 25 | Instance = new Runtime.Mono.MonoProvider(); 26 | #endif 27 | Instance.OnInitialize(); 28 | } 29 | 30 | protected internal abstract void OnInitialize(); 31 | 32 | /// 33 | /// Start any as a , handled by UniverseLib's Instance. 34 | /// 35 | public static Coroutine StartCoroutine(IEnumerator routine) 36 | => Instance.Internal_StartCoroutine(routine); 37 | 38 | protected internal abstract Coroutine Internal_StartCoroutine(IEnumerator routine); 39 | 40 | /// 41 | /// Stop a , which needs to have been started with . 42 | /// 43 | /// 44 | public static void StopCoroutine(Coroutine coroutine) 45 | => Instance.Internal_StopCoroutine(coroutine); 46 | 47 | protected internal abstract void Internal_StopCoroutine(Coroutine coroutine); 48 | 49 | /// 50 | /// Helper to add a component of Type , and return it as Type (provided is assignable from ). 51 | /// 52 | public static T AddComponent(GameObject obj, Type type) where T : Component 53 | => Instance.Internal_AddComponent(obj, type); 54 | 55 | protected internal abstract T Internal_AddComponent(GameObject obj, Type type) where T : Component; 56 | 57 | /// 58 | /// Helper to create an instance of the ScriptableObject of Type . 59 | /// 60 | public static ScriptableObject CreateScriptable(Type type) 61 | => Instance.Internal_CreateScriptable(type); 62 | 63 | protected internal abstract ScriptableObject Internal_CreateScriptable(Type type); 64 | 65 | /// 66 | /// Helper to invoke Unity's method. 67 | /// 68 | public static string LayerToName(int layer) 69 | => Instance.Internal_LayerToName(layer); 70 | 71 | protected internal abstract string Internal_LayerToName(int layer); 72 | 73 | /// 74 | /// Helper to invoke Unity's method. 75 | /// 76 | public static T[] FindObjectsOfTypeAll() where T : UnityEngine.Object 77 | => Instance.Internal_FindObjectsOfTypeAll(); 78 | 79 | /// 80 | /// Helper to invoke Unity's method. 81 | /// 82 | public static UnityEngine.Object[] FindObjectsOfTypeAll(Type type) 83 | => Instance.Internal_FindObjectsOfTypeAll(type); 84 | 85 | protected internal abstract T[] Internal_FindObjectsOfTypeAll() where T : UnityEngine.Object; 86 | 87 | protected internal abstract UnityEngine.Object[] Internal_FindObjectsOfTypeAll(Type type); 88 | 89 | /// 90 | /// Helper to invoke Unity's method. 91 | /// 92 | public static void GraphicRaycast(GraphicRaycaster raycaster, PointerEventData data, List list) 93 | => Instance.Internal_GraphicRaycast(raycaster, data, list); 94 | 95 | protected internal abstract void Internal_GraphicRaycast(GraphicRaycaster raycaster, PointerEventData data, List list); 96 | 97 | /// 98 | /// Helper to invoke Unity's method. 99 | /// 100 | public static GameObject[] GetRootGameObjects(Scene scene) 101 | => Instance.Internal_GetRootGameObjects(scene); 102 | 103 | protected internal abstract GameObject[] Internal_GetRootGameObjects(Scene scene); 104 | 105 | /// 106 | /// Helper to get the value of Unity's property. 107 | /// 108 | public static int GetRootCount(Scene scene) 109 | => Instance.Internal_GetRootCount(scene); 110 | 111 | protected internal abstract int Internal_GetRootCount(Scene scene); 112 | 113 | /// 114 | /// Automatically sets the base, highlighted and pressed values of the 's , 115 | /// with * 1.2f for the highlighted color and * 0.8f for the pressed color. 116 | /// 117 | public static void SetColorBlockAuto(Selectable selectable, Color baseColor) 118 | => Instance.Internal_SetColorBlock(selectable, baseColor, baseColor * 1.2f, baseColor * 0.8f); 119 | 120 | /// 121 | /// Sets the to the . 122 | /// 123 | public static void SetColorBlock(Selectable selectable, ColorBlock colors) 124 | => Instance.Internal_SetColorBlock(selectable, colors); 125 | 126 | protected internal abstract void Internal_SetColorBlock(Selectable selectable, ColorBlock colors); 127 | 128 | /// 129 | /// Sets the provided non- colors to the . 130 | /// 131 | public static void SetColorBlock(Selectable selectable, Color? normal = null, Color? highlighted = null, Color? pressed = null, Color? disabled = null) 132 | => Instance.Internal_SetColorBlock(selectable, normal, highlighted, pressed, disabled); 133 | 134 | protected internal abstract void Internal_SetColorBlock(Selectable selectable, Color? normal = null, Color? highlighted = null, Color? pressed = null, Color? disabled = null); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Utility/ToStringUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using UnityEngine; 8 | using UnityEngine.EventSystems; 9 | using UniverseLib.Runtime; 10 | 11 | namespace UniverseLib.Utility 12 | { 13 | /// 14 | /// Provides utility for displaying an object's ToString result in a more user-friendly format. 15 | /// 16 | public static class ToStringUtility 17 | { 18 | internal static Dictionary toStringMethods = new(); 19 | 20 | private const string nullString = "null"; 21 | private const string nullUnknown = nullString + " (?)"; 22 | private const string destroyedString = "Destroyed"; 23 | private const string untitledString = "untitled"; 24 | 25 | private const string eventSystemNamespace = "UnityEngine.EventSystem"; 26 | 27 | /// 28 | /// Constrains the provided string to a maximum length, and maximum number of lines. 29 | /// 30 | public static string PruneString(string s, int chars = 200, int lines = 5) 31 | { 32 | if (string.IsNullOrEmpty(s)) 33 | return s; 34 | 35 | StringBuilder sb = new(Math.Max(chars, s.Length)); 36 | int newlines = 0; 37 | for (int i = 0; i < s.Length; i++) 38 | { 39 | if (newlines >= lines || i >= chars) 40 | { 41 | sb.Append("..."); 42 | break; 43 | } 44 | char c = s[i]; 45 | if (c == '\r' || c == '\n') 46 | newlines++; 47 | sb.Append(c); 48 | } 49 | 50 | return sb.ToString(); 51 | } 52 | 53 | /// 54 | /// Returns the ToString result with a rich-text highlighted Type in trailing brackets. 55 | /// If the object does not implement ToString, then only the trailing highlighted Type will be returned. 56 | /// 57 | public static string ToStringWithType(object value, Type fallbackType, bool includeNamespace = true) 58 | { 59 | if (value.IsNullOrDestroyed() && fallbackType == null) 60 | return nullUnknown; 61 | 62 | Type type = value?.GetActualType() ?? fallbackType; 63 | 64 | string richType = SignatureHighlighter.Parse(type, includeNamespace); 65 | 66 | StringBuilder sb = new(); 67 | 68 | if (value.IsNullOrDestroyed()) 69 | { 70 | if (value == null) 71 | { 72 | sb.Append(nullString); 73 | AppendRichType(sb, richType); 74 | return sb.ToString(); 75 | } 76 | else // destroyed unity object 77 | { 78 | sb.Append(destroyedString); 79 | AppendRichType(sb, richType); 80 | return sb.ToString(); 81 | } 82 | } 83 | 84 | if (value is UnityEngine.Object obj) 85 | { 86 | if (string.IsNullOrEmpty(obj.name)) 87 | sb.Append(untitledString); 88 | else 89 | { 90 | sb.Append('"'); 91 | sb.Append(PruneString(obj.name, 50, 1)); 92 | sb.Append('"'); 93 | } 94 | 95 | AppendRichType(sb, richType); 96 | } 97 | else if (type.FullName.StartsWith(eventSystemNamespace)) 98 | { 99 | // UnityEngine.EventSystem classes can have some obnoxious ToString results with rich text. 100 | sb.Append(richType); 101 | } 102 | else 103 | { 104 | string toString = ToString(value); 105 | 106 | if (type.IsGenericType 107 | || toString == type.FullName 108 | || toString == $"{type.FullName} {type.FullName}" 109 | || toString == $"Il2Cpp{type.FullName}" || type.FullName == $"Il2Cpp{toString}") 110 | { 111 | sb.Append(richType); 112 | } 113 | else // the ToString contains some actual implementation, use that value. 114 | { 115 | sb.Append(PruneString(toString, 200, 5)); 116 | 117 | AppendRichType(sb, richType); 118 | } 119 | } 120 | 121 | return sb.ToString(); 122 | } 123 | 124 | private static void AppendRichType(StringBuilder sb, string richType) 125 | { 126 | sb.Append(' '); 127 | sb.Append('('); 128 | sb.Append(richType); 129 | sb.Append(')'); 130 | } 131 | 132 | private static string ToString(object value) 133 | { 134 | if (value.IsNullOrDestroyed()) 135 | { 136 | if (value == null) 137 | return nullString; 138 | else // destroyed unity object 139 | return destroyedString; 140 | } 141 | 142 | Type type = value.GetActualType(); 143 | 144 | // Find and cache the ToString method for this Type, if haven't already. 145 | 146 | if (!toStringMethods.ContainsKey(type.AssemblyQualifiedName)) 147 | { 148 | MethodInfo toStringMethod = type.GetMethod("ToString", ArgumentUtility.EmptyTypes); 149 | toStringMethods.Add(type.AssemblyQualifiedName, toStringMethod); 150 | } 151 | 152 | // Invoke the ToString method on the object 153 | 154 | value = value.TryCast(type); 155 | 156 | string toString; 157 | try 158 | { 159 | toString = (string)toStringMethods[type.AssemblyQualifiedName].Invoke(value, ArgumentUtility.EmptyArgs); 160 | } 161 | catch (Exception ex) 162 | { 163 | toString = ex.ReflectionExToString(); 164 | } 165 | 166 | toString = ReflectionUtility.ProcessTypeInString(type, toString); 167 | 168 | #if IL2CPP 169 | if (value is Il2CppSystem.Type cppType) 170 | { 171 | Type monoType = Il2CppReflection.GetUnhollowedType(cppType); 172 | if (monoType != null) 173 | toString = ReflectionUtility.ProcessTypeInString(monoType, toString); 174 | } 175 | #endif 176 | 177 | return toString; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/UI/Panels/PanelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using UnityEngine; 6 | using UnityEngine.UI; 7 | using UniverseLib.Input; 8 | using UniverseLib.UI.Models; 9 | 10 | namespace UniverseLib.UI.Panels 11 | { 12 | public abstract class PanelBase : UIBehaviourModel 13 | { 14 | public UIBase Owner { get; } 15 | 16 | public abstract string Name { get; } 17 | 18 | public abstract int MinWidth { get; } 19 | public abstract int MinHeight { get; } 20 | public abstract Vector2 DefaultAnchorMin { get; } 21 | public abstract Vector2 DefaultAnchorMax { get; } 22 | public virtual Vector2 DefaultPosition { get; } 23 | 24 | public virtual bool CanDragAndResize => true; 25 | public PanelDragger Dragger { get; internal set; } 26 | 27 | public override GameObject UIRoot => uiRoot; 28 | protected GameObject uiRoot; 29 | public RectTransform Rect { get; private set; } 30 | public GameObject ContentRoot { get; protected set; } 31 | 32 | public GameObject TitleBar { get; private set; } 33 | 34 | public PanelBase(UIBase owner) 35 | { 36 | Owner = owner; 37 | 38 | ConstructUI(); 39 | 40 | // Add to owner 41 | Owner.Panels.AddPanel(this); 42 | } 43 | 44 | public override void Destroy() 45 | { 46 | Owner.Panels.RemovePanel(this); 47 | base.Destroy(); 48 | } 49 | 50 | public virtual void OnFinishResize() 51 | { 52 | } 53 | 54 | public virtual void OnFinishDrag() 55 | { 56 | } 57 | 58 | public override void SetActive(bool active) 59 | { 60 | if (this.Enabled != active) 61 | base.SetActive(active); 62 | 63 | if (!active) 64 | this.Dragger.WasDragging = false; 65 | else 66 | { 67 | this.UIRoot.transform.SetAsLastSibling(); 68 | this.Owner.Panels.InvokeOnPanelsReordered(); 69 | } 70 | } 71 | 72 | protected virtual void OnClosePanelClicked() 73 | { 74 | this.SetActive(false); 75 | } 76 | 77 | // Setting size and position 78 | 79 | public virtual void SetDefaultSizeAndPosition() 80 | { 81 | Rect.localPosition = DefaultPosition; 82 | Rect.pivot = new Vector2(0f, 1f); 83 | 84 | Rect.anchorMin = DefaultAnchorMin; 85 | Rect.anchorMax = DefaultAnchorMax; 86 | 87 | LayoutRebuilder.ForceRebuildLayoutImmediate(this.Rect); 88 | 89 | EnsureValidPosition(); 90 | EnsureValidSize(); 91 | 92 | Dragger.OnEndResize(); 93 | } 94 | 95 | public virtual void EnsureValidSize() 96 | { 97 | if (Rect.rect.width < MinWidth) 98 | Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, MinWidth); 99 | 100 | if (Rect.rect.height < MinHeight) 101 | Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, MinHeight); 102 | 103 | Dragger.OnEndResize(); 104 | } 105 | 106 | public virtual void EnsureValidPosition() 107 | { 108 | // Prevent panel going oustide screen bounds 109 | 110 | Vector3 pos = this.Rect.localPosition; 111 | Vector2 dimensions = Owner.Panels.ScreenDimensions; 112 | 113 | float halfW = dimensions.x * 0.5f; 114 | float halfH = dimensions.y * 0.5f; 115 | 116 | pos.x = Math.Max(-halfW - this.Rect.rect.width + 50, Math.Min(pos.x, halfW - 50)); 117 | pos.y = Math.Max(-halfH + 50, Math.Min(pos.y, halfH)); 118 | 119 | this.Rect.localPosition = pos; 120 | } 121 | 122 | // UI Construction 123 | 124 | protected abstract void ConstructPanelContent(); 125 | 126 | protected virtual PanelDragger CreatePanelDragger() => new(this); 127 | 128 | public virtual void ConstructUI() 129 | { 130 | // create core canvas 131 | uiRoot = UIFactory.CreatePanel(Name, Owner.Panels.PanelHolder, out GameObject contentRoot); 132 | ContentRoot = contentRoot; 133 | Rect = this.uiRoot.GetComponent(); 134 | 135 | UIFactory.SetLayoutGroup(this.ContentRoot, false, false, true, true, 2, 2, 2, 2, 2, TextAnchor.UpperLeft); 136 | UIFactory.SetLayoutElement(ContentRoot, 0, 0, flexibleWidth: 9999, flexibleHeight: 9999); 137 | 138 | // Title bar 139 | TitleBar = UIFactory.CreateHorizontalGroup(ContentRoot, "TitleBar", false, true, true, true, 2, 140 | new Vector4(2, 2, 2, 2), new Color(0.06f, 0.06f, 0.06f)); 141 | UIFactory.SetLayoutElement(TitleBar, minHeight: 25, flexibleHeight: 0); 142 | 143 | 144 | // Title text 145 | 146 | Text titleTxt = UIFactory.CreateLabel(TitleBar, "TitleBar", Name, TextAnchor.MiddleLeft); 147 | UIFactory.SetLayoutElement(titleTxt.gameObject, 50, 25, 9999, 0); 148 | 149 | // close button 150 | 151 | GameObject closeHolder = UIFactory.CreateUIObject("CloseHolder", TitleBar); 152 | UIFactory.SetLayoutElement(closeHolder, minHeight: 25, flexibleHeight: 0, minWidth: 30, flexibleWidth: 9999); 153 | UIFactory.SetLayoutGroup(closeHolder, false, false, true, true, 3, childAlignment: TextAnchor.MiddleRight); 154 | ButtonRef closeBtn = UIFactory.CreateButton(closeHolder, "CloseButton", "—"); 155 | UIFactory.SetLayoutElement(closeBtn.Component.gameObject, minHeight: 25, minWidth: 25, flexibleWidth: 0); 156 | RuntimeHelper.SetColorBlock(closeBtn.Component, new Color(0.33f, 0.32f, 0.31f)); 157 | 158 | closeBtn.OnClick += () => 159 | { 160 | OnClosePanelClicked(); 161 | }; 162 | 163 | if (!CanDragAndResize) 164 | TitleBar.SetActive(false); 165 | 166 | // Panel dragger 167 | 168 | Dragger = CreatePanelDragger(); 169 | Dragger.OnFinishResize += OnFinishResize; 170 | Dragger.OnFinishDrag += OnFinishDrag; 171 | 172 | // content (abstract) 173 | 174 | ConstructPanelContent(); 175 | SetDefaultSizeAndPosition(); 176 | 177 | RuntimeHelper.StartCoroutine(LateSetupCoroutine()); 178 | } 179 | 180 | private IEnumerator LateSetupCoroutine() 181 | { 182 | yield return null; 183 | 184 | LateConstructUI(); 185 | } 186 | 187 | protected virtual void LateConstructUI() 188 | { 189 | SetDefaultSizeAndPosition(); 190 | } 191 | 192 | #pragma warning disable CS0809 // Obsolete member overrides non-obsolete member 193 | [Obsolete("Not used. Use ConstructUI() instead.")] 194 | public override void ConstructUI(GameObject parent) => ConstructUI(); 195 | #pragma warning restore CS0809 196 | 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # JustCode is a .NET coding add-in 131 | .JustCode 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | /src/UI/Widgets/InputFieldScroller.cs 355 | -------------------------------------------------------------------------------- /src/Runtime/Il2Cpp/Il2CppProvider.cs: -------------------------------------------------------------------------------- 1 | #if IL2CPP 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEngine; 7 | using UnityEngine.Events; 8 | using UnityEngine.SceneManagement; 9 | using System.Collections; 10 | using UnityEngine.UI; 11 | using UniverseLib.Input; 12 | using UnityEngine.EventSystems; 13 | using HarmonyLib; 14 | using UniverseLib.Utility; 15 | using Il2CppInterop.Runtime; 16 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 17 | 18 | namespace UniverseLib.Runtime.Il2Cpp 19 | { 20 | internal class Il2CppProvider : RuntimeHelper 21 | { 22 | readonly AmbiguousMemberHandler normalColor = new(true, true, "normalColor", "m_NormalColor"); 23 | readonly AmbiguousMemberHandler highlightedColor = new(true, true, "highlightedColor", "m_HighlightedColor"); 24 | readonly AmbiguousMemberHandler pressedColor = new(true, true, "pressedColor", "m_PressedColor"); 25 | readonly AmbiguousMemberHandler disabledColor = new(true, true, "disabledColor", "m_DisabledColor"); 26 | 27 | internal delegate IntPtr d_LayerToName(int layer); 28 | 29 | internal delegate IntPtr d_FindObjectsOfTypeAll(IntPtr type); 30 | 31 | internal delegate void d_GetRootGameObjects(int handle, IntPtr list); 32 | 33 | internal delegate int d_GetRootCountInternal(int handle); 34 | 35 | protected internal override void OnInitialize() 36 | { 37 | new Il2CppTextureHelper(); 38 | } 39 | 40 | /// 41 | protected internal override Coroutine Internal_StartCoroutine(IEnumerator routine) 42 | => UniversalBehaviour.Instance.StartCoroutine(routine.WrapToIl2Cpp()); 43 | 44 | /// 45 | protected internal override void Internal_StopCoroutine(Coroutine coroutine) 46 | => UniversalBehaviour.Instance.StopCoroutine(coroutine); 47 | 48 | /// 49 | protected internal override T Internal_AddComponent(GameObject obj, Type type) 50 | => obj.AddComponent(Il2CppType.From(type)).TryCast(); 51 | 52 | /// 53 | protected internal override ScriptableObject Internal_CreateScriptable(Type type) 54 | => ScriptableObject.CreateInstance(Il2CppType.From(type)); 55 | 56 | /// 57 | protected internal override void Internal_GraphicRaycast(GraphicRaycaster raycaster, PointerEventData data, List list) 58 | { 59 | Il2CppSystem.Collections.Generic.List il2cppList = new(); 60 | 61 | raycaster.Raycast(data, il2cppList); 62 | 63 | if (il2cppList.Count > 0) 64 | list.AddRange(il2cppList.ToArray()); 65 | } 66 | 67 | /// 68 | protected internal override string Internal_LayerToName(int layer) 69 | { 70 | d_LayerToName iCall = ICallManager.GetICall("UnityEngine.LayerMask::LayerToName"); 71 | return IL2CPP.Il2CppStringToManaged(iCall.Invoke(layer)); 72 | } 73 | 74 | /// 75 | protected internal override UnityEngine.Object[] Internal_FindObjectsOfTypeAll(Type type) 76 | { 77 | return new Il2CppReferenceArray( 78 | ICallManager.GetICallUnreliable( 79 | "UnityEngine.Resources::FindObjectsOfTypeAll", 80 | "UnityEngine.ResourcesAPIInternal::FindObjectsOfTypeAll") // Unity 2020+ updated to this 81 | .Invoke(Il2CppType.From(type).Pointer)); 82 | } 83 | 84 | /// 85 | protected internal override T[] Internal_FindObjectsOfTypeAll() 86 | { 87 | return new Il2CppReferenceArray( 88 | ICallManager.GetICallUnreliable( 89 | "UnityEngine.Resources::FindObjectsOfTypeAll", 90 | "UnityEngine.ResourcesAPIInternal::FindObjectsOfTypeAll") // Unity 2020+ updated to this 91 | .Invoke(Il2CppType.From(typeof(T)).Pointer)); 92 | } 93 | 94 | /// 95 | protected internal override GameObject[] Internal_GetRootGameObjects(Scene scene) 96 | { 97 | if (!scene.isLoaded || scene.handle == -1) 98 | return new GameObject[0]; 99 | 100 | int count = GetRootCount(scene.handle); 101 | if (count < 1) 102 | return new GameObject[0]; 103 | 104 | Il2CppSystem.Collections.Generic.List list = new(count); 105 | ICallManager.GetICall("UnityEngine.SceneManagement.Scene::GetRootGameObjectsInternal") 106 | .Invoke(scene.handle, list.Pointer); 107 | return list.ToArray(); 108 | } 109 | 110 | /// 111 | protected internal override int Internal_GetRootCount(Scene scene) => GetRootCount(scene.handle); 112 | 113 | /// 114 | /// Gets the for the provided scene handle. 115 | /// 116 | protected internal static int GetRootCount(int sceneHandle) 117 | { 118 | return ICallManager.GetICall("UnityEngine.SceneManagement.Scene::GetRootCountInternal") 119 | .Invoke(sceneHandle); 120 | } 121 | 122 | /// 123 | protected internal override void Internal_SetColorBlock(Selectable selectable, ColorBlock colorBlock) 124 | { 125 | try 126 | { 127 | AccessTools.Property(typeof(Selectable), "m_Colors") 128 | .SetValue(selectable, colorBlock, null); 129 | 130 | AccessTools.Method(typeof(Selectable), "OnSetProperty") 131 | .Invoke(selectable, ArgumentUtility.EmptyArgs); 132 | } 133 | catch (Exception ex) 134 | { 135 | Universe.LogWarning(ex); 136 | } 137 | } 138 | 139 | /// 140 | protected internal override void Internal_SetColorBlock(Selectable selectable, Color? normal = null, Color? highlighted = null, Color? pressed = null, Color? disabled = null) 141 | { 142 | ColorBlock colors = selectable.colors; 143 | colors.colorMultiplier = 1; 144 | 145 | object boxedColors = colors; 146 | 147 | if (normal != null) 148 | normalColor.SetValue(boxedColors, (Color)normal); 149 | 150 | if (highlighted != null) 151 | highlightedColor.SetValue(boxedColors, (Color)highlighted); 152 | 153 | if (pressed != null) 154 | pressedColor.SetValue(boxedColors, (Color)pressed); 155 | 156 | if (disabled != null) 157 | disabledColor.SetValue(boxedColors, (Color)disabled); 158 | 159 | SetColorBlock(selectable, (ColorBlock)boxedColors); 160 | } 161 | } 162 | } 163 | 164 | namespace UniverseLib 165 | { 166 | public static class Il2CppExtensions 167 | { 168 | public static void AddListener(this UnityEvent action, Action listener) 169 | { 170 | action.AddListener(listener); 171 | } 172 | 173 | public static void AddListener(this UnityEvent action, Action listener) 174 | { 175 | action.AddListener(listener); 176 | } 177 | 178 | public static void RemoveListener(this UnityEvent action, Action listener) 179 | { 180 | action.RemoveListener(listener); 181 | } 182 | 183 | public static void RemoveListener(this UnityEvent action, Action listener) 184 | { 185 | action.RemoveListener(listener); 186 | } 187 | 188 | public static void SetChildControlHeight(this HorizontalOrVerticalLayoutGroup group, bool value) => group.childControlHeight = value; 189 | public static void SetChildControlWidth(this HorizontalOrVerticalLayoutGroup group, bool value) => group.childControlWidth = value; 190 | } 191 | } 192 | 193 | #endif -------------------------------------------------------------------------------- /src/UI/Widgets/TransformTree/TransformCell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UI; 4 | using UniverseLib; 5 | using UniverseLib.UI; 6 | using UniverseLib.UI.Models; 7 | using UniverseLib.UI.Widgets.ScrollView; 8 | using UniverseLib.Utility; 9 | 10 | namespace UniverseLib.UI.Widgets 11 | { 12 | public class TransformCell : ICell 13 | { 14 | public float DefaultHeight => 25f; 15 | 16 | public bool Enabled => enabled; 17 | private bool enabled; 18 | 19 | public Action OnExpandToggled; 20 | public Action OnEnableToggled; 21 | public Action OnGameObjectClicked; 22 | 23 | public CachedTransform cachedTransform; 24 | 25 | public GameObject UIRoot { get; set; } 26 | public RectTransform Rect { get; set; } 27 | 28 | public ButtonRef ExpandButton; 29 | public ButtonRef NameButton; 30 | public Toggle EnabledToggle; 31 | public InputFieldRef SiblingIndex; 32 | 33 | public LayoutElement spacer; 34 | 35 | public void Enable() 36 | { 37 | enabled = true; 38 | UIRoot.SetActive(true); 39 | } 40 | 41 | public void Disable() 42 | { 43 | enabled = false; 44 | UIRoot.SetActive(false); 45 | } 46 | 47 | public void ConfigureCell(CachedTransform cached) 48 | { 49 | if (cached == null) 50 | { 51 | Universe.LogWarning("Setting TransformTree cell but the CachedTransform is null!"); 52 | return; 53 | } 54 | 55 | if (!Enabled) 56 | Enable(); 57 | 58 | cachedTransform = cached; 59 | 60 | spacer.minWidth = cached.Depth * 15; 61 | 62 | if (cached.Value) 63 | { 64 | string name = cached.Value.name?.Trim(); 65 | if (string.IsNullOrEmpty(name)) 66 | name = "untitled"; 67 | NameButton.ButtonText.text = name; 68 | NameButton.ButtonText.color = cached.Value.gameObject.activeSelf ? Color.white : Color.grey; 69 | 70 | EnabledToggle.Set(cached.Value.gameObject.activeSelf, false); 71 | 72 | if (!cached.Value.parent) 73 | SiblingIndex.GameObject.SetActive(false); 74 | else 75 | { 76 | SiblingIndex.GameObject.SetActive(true); 77 | if (!SiblingIndex.Component.isFocused) 78 | SiblingIndex.Text = cached.Value.GetSiblingIndex().ToString(); 79 | } 80 | 81 | int childCount = cached.Value.childCount; 82 | if (childCount > 0) 83 | { 84 | NameButton.ButtonText.text = $"[{childCount}] {NameButton.ButtonText.text}"; 85 | 86 | ExpandButton.Component.interactable = true; 87 | ExpandButton.ButtonText.text = cached.Expanded ? "▼" : "►"; 88 | ExpandButton.ButtonText.color = cached.Expanded ? new Color(0.5f, 0.5f, 0.5f) : new Color(0.3f, 0.3f, 0.3f); 89 | } 90 | else 91 | { 92 | ExpandButton.Component.interactable = false; 93 | ExpandButton.ButtonText.text = "▪"; 94 | ExpandButton.ButtonText.color = new Color(0.3f, 0.3f, 0.3f); 95 | } 96 | } 97 | else 98 | { 99 | NameButton.ButtonText.text = $"[Destroyed]"; 100 | NameButton.ButtonText.color = Color.red; 101 | 102 | SiblingIndex.GameObject.SetActive(false); 103 | } 104 | } 105 | 106 | public void OnMainButtonClicked() 107 | { 108 | if (cachedTransform.Value) 109 | OnGameObjectClicked?.Invoke(cachedTransform.Value.gameObject); 110 | else 111 | Universe.LogWarning("The object was destroyed!"); 112 | } 113 | 114 | public void OnExpandClicked() 115 | { 116 | OnExpandToggled?.Invoke(cachedTransform); 117 | } 118 | 119 | private void OnEnableClicked(bool value) 120 | { 121 | OnEnableToggled?.Invoke(cachedTransform); 122 | } 123 | 124 | private void OnSiblingIndexEndEdit(string input) 125 | { 126 | if (this.cachedTransform == null || !this.cachedTransform.Value) 127 | return; 128 | 129 | if (int.TryParse(input.Trim(), out int index)) 130 | this.cachedTransform.Value.SetSiblingIndex(index); 131 | 132 | this.SiblingIndex.Text = this.cachedTransform.Value.GetSiblingIndex().ToString(); 133 | } 134 | 135 | public GameObject CreateContent(GameObject parent) 136 | { 137 | UIRoot = UIFactory.CreateUIObject("TransformCell", parent); 138 | UIFactory.SetLayoutGroup(UIRoot, false, false, true, true, 2, childAlignment: TextAnchor.MiddleCenter); 139 | Rect = UIRoot.GetComponent(); 140 | Rect.anchorMin = new Vector2(0, 1); 141 | Rect.anchorMax = new Vector2(0, 1); 142 | Rect.pivot = new Vector2(0.5f, 1); 143 | Rect.sizeDelta = new Vector2(25, 25); 144 | UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0); 145 | 146 | GameObject spacerObj = UIFactory.CreateUIObject("Spacer", UIRoot, new Vector2(0, 0)); 147 | UIFactory.SetLayoutElement(spacerObj, minWidth: 0, flexibleWidth: 0, minHeight: 0, flexibleHeight: 0); 148 | this.spacer = spacerObj.GetComponent(); 149 | 150 | // Expand arrow 151 | 152 | ExpandButton = UIFactory.CreateButton(this.UIRoot, "ExpandButton", "►"); 153 | UIFactory.SetLayoutElement(ExpandButton.Component.gameObject, minWidth: 15, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0); 154 | 155 | // Enabled toggle 156 | 157 | GameObject toggleObj = UIFactory.CreateToggle(UIRoot, "BehaviourToggle", out EnabledToggle, out Text behavText, default, 17, 17); 158 | UIFactory.SetLayoutElement(toggleObj, minHeight: 17, flexibleHeight: 0, minWidth: 17); 159 | EnabledToggle.onValueChanged.AddListener(OnEnableClicked); 160 | 161 | // Name button 162 | 163 | GameObject nameBtnHolder = UIFactory.CreateHorizontalGroup(this.UIRoot, "NameButtonHolder", 164 | false, false, true, true, childAlignment: TextAnchor.MiddleLeft); 165 | UIFactory.SetLayoutElement(nameBtnHolder, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0); 166 | nameBtnHolder.AddComponent().showMaskGraphic = false; 167 | 168 | NameButton = UIFactory.CreateButton(nameBtnHolder, "NameButton", "Name", null); 169 | UIFactory.SetLayoutElement(NameButton.Component.gameObject, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0); 170 | Text nameLabel = NameButton.Component.GetComponentInChildren(); 171 | nameLabel.horizontalOverflow = HorizontalWrapMode.Overflow; 172 | nameLabel.alignment = TextAnchor.MiddleLeft; 173 | 174 | // Sibling index input 175 | 176 | SiblingIndex = UIFactory.CreateInputField(this.UIRoot, "SiblingIndexInput", string.Empty); 177 | SiblingIndex.Component.textComponent.fontSize = 11; 178 | SiblingIndex.Component.textComponent.alignment = TextAnchor.MiddleRight; 179 | Image siblingImage = SiblingIndex.GameObject.GetComponent(); 180 | siblingImage.color = new(0f, 0f, 0f, 0.25f); 181 | UIFactory.SetLayoutElement(SiblingIndex.GameObject, 35, 20, 0, 0); 182 | SiblingIndex.Component.GetOnEndEdit().AddListener(OnSiblingIndexEndEdit); 183 | 184 | // Setup selectables 185 | 186 | Color normal = new(0.11f, 0.11f, 0.11f); 187 | Color highlight = new(0.25f, 0.25f, 0.25f); 188 | Color pressed = new(0.05f, 0.05f, 0.05f); 189 | Color disabled = new(1, 1, 1, 0); 190 | RuntimeHelper.SetColorBlock(ExpandButton.Component, normal, highlight, pressed, disabled); 191 | RuntimeHelper.SetColorBlock(NameButton.Component, normal, highlight, pressed, disabled); 192 | 193 | NameButton.OnClick += OnMainButtonClicked; 194 | ExpandButton.OnClick += OnExpandClicked; 195 | 196 | UIRoot.SetActive(false); 197 | 198 | return this.UIRoot; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/UI/Panels/PanelManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using UnityEngine.UI; 7 | using UniverseLib.Input; 8 | 9 | namespace UniverseLib.UI.Panels 10 | { 11 | /// 12 | /// Handles updating, dragging and resizing all s for the parent . 13 | /// 14 | public class PanelManager 15 | { 16 | #region STATIC 17 | 18 | /// Is a panel currently being resized? 19 | public static bool Resizing { get; internal set; } 20 | 21 | /// Is the resize cursor being displayed? 22 | public static bool ResizePrompting => resizeCursor && resizeCursorUIBase.Enabled; 23 | 24 | protected internal static readonly List allDraggers = new(); 25 | 26 | protected internal static UIBase resizeCursorUIBase; 27 | protected internal static GameObject resizeCursor; 28 | 29 | protected internal static bool focusHandledThisFrame; 30 | protected internal static bool draggerHandledThisFrame; 31 | protected internal static bool wasAnyDragging; 32 | 33 | /// Force any current Resizing to immediately end. 34 | public static void ForceEndResize() 35 | { 36 | if (!resizeCursor || resizeCursorUIBase == null) 37 | return; 38 | 39 | resizeCursorUIBase.Enabled = false; 40 | resizeCursor.SetActive(false); 41 | wasAnyDragging = false; 42 | Resizing = false; 43 | 44 | foreach (PanelDragger instance in allDraggers) 45 | { 46 | instance.WasDragging = false; 47 | instance.WasResizing = false; 48 | } 49 | } 50 | 51 | protected static void CreateResizeCursorUI() 52 | { 53 | try 54 | { 55 | resizeCursorUIBase = UniversalUI.RegisterUI($"{Universe.GUID}.resizeCursor", null); 56 | GameObject parent = resizeCursorUIBase.RootObject; 57 | parent.transform.SetParent(UniversalUI.CanvasRoot.transform); 58 | 59 | Text text = UIFactory.CreateLabel(parent, "ResizeCursor", "↔", TextAnchor.MiddleCenter, Color.white, true, 35); 60 | resizeCursor = text.gameObject; 61 | 62 | Outline outline = text.gameObject.AddComponent(); 63 | outline.effectColor = Color.black; 64 | outline.effectDistance = new(1, 1); 65 | 66 | RectTransform rect = resizeCursor.GetComponent(); 67 | rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 64); 68 | rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 64); 69 | 70 | resizeCursorUIBase.Enabled = false; 71 | } 72 | catch (Exception e) 73 | { 74 | Universe.LogWarning("Exception creating Resize Cursor UI!\r\n" + e.ToString()); 75 | } 76 | } 77 | 78 | #endregion 79 | 80 | /// The UIBase which created this PanelManager. 81 | public UIBase Owner { get; } 82 | 83 | /// The GameObject which holds all of this PanelManager's Panels. 84 | public GameObject PanelHolder { get; } 85 | 86 | /// Invoked when the UIPanel heirarchy is reordered. 87 | public event Action OnPanelsReordered; 88 | 89 | /// Invoked when the user clicks outside of all panels. 90 | public event Action OnClickedOutsidePanels; 91 | 92 | protected readonly List panelInstances = new(); 93 | protected readonly Dictionary transformIDToUIPanel = new(); 94 | protected readonly List draggerInstances = new(); 95 | 96 | public PanelManager(UIBase owner) 97 | { 98 | Owner = owner; 99 | PanelHolder = UIFactory.CreateUIObject("PanelHolder", owner.RootObject); 100 | RectTransform rect = PanelHolder.GetComponent(); 101 | rect.anchorMin = Vector2.zero; 102 | rect.anchorMax = Vector2.one; 103 | } 104 | 105 | /// 106 | /// Determines if the PanelManager should update "focus" (ie. heirarchy order). 107 | /// By default, returns true if user is clicking. 108 | /// 109 | protected virtual bool ShouldUpdateFocus 110 | { 111 | get => MouseInTargetDisplay && (InputManager.GetMouseButtonDown(0) || InputManager.GetMouseButtonDown(1)); 112 | } 113 | 114 | /// 115 | /// The MousePosition which should be used for this PanelManager. By default, this is . 116 | /// 117 | protected internal virtual Vector3 MousePosition 118 | { 119 | get => InputManager.MousePosition; 120 | } 121 | 122 | /// 123 | /// The Screen dimensions which should be used for this PanelManager. By default, this is x . 124 | /// 125 | protected internal virtual Vector2 ScreenDimensions 126 | { 127 | get => new(Screen.width, Screen.height); 128 | } 129 | 130 | /// 131 | /// Determines if the mouse is currently in the Display used by this PanelManager. By default, this always returns true. 132 | /// 133 | protected virtual bool MouseInTargetDisplay => true; 134 | 135 | // invoked from UIPanel ctor 136 | internal protected virtual void AddPanel(PanelBase panel) 137 | { 138 | allDraggers.Add(panel.Dragger); 139 | this.draggerInstances.Add(panel.Dragger); 140 | 141 | this.panelInstances.Add(panel); 142 | this.transformIDToUIPanel.Add(panel.UIRoot.transform.GetInstanceID(), panel); 143 | } 144 | 145 | // invoked from UIPanel.Destroy 146 | internal protected virtual void RemovePanel(PanelBase panel) 147 | { 148 | allDraggers.Remove(panel.Dragger); 149 | this.draggerInstances.Remove(panel.Dragger); 150 | 151 | this.panelInstances.Remove(panel); 152 | this.transformIDToUIPanel.Remove(panel.UIRoot.transform.GetInstanceID()); 153 | } 154 | 155 | // invoked from UIPanel.Enable 156 | internal protected virtual void InvokeOnPanelsReordered() 157 | { 158 | Owner.SetOnTop(); 159 | SortDraggerHeirarchy(); 160 | OnPanelsReordered?.Invoke(); 161 | } 162 | 163 | // invoked from parent UIBase.Update 164 | internal protected virtual void Update() 165 | { 166 | if (!ResizePrompting && ShouldUpdateFocus) 167 | UpdateFocus(); 168 | 169 | if (!draggerHandledThisFrame) 170 | UpdateDraggers(); 171 | } 172 | 173 | protected virtual void UpdateFocus() 174 | { 175 | bool clickedInAny = false; 176 | 177 | // If another UIBase has already handled a user's click for focus, don't update it for this UIBase. 178 | if (!focusHandledThisFrame) 179 | { 180 | Vector3 mousePos = MousePosition; 181 | int count = PanelHolder.transform.childCount; 182 | 183 | for (int i = count - 1; i >= 0; i--) 184 | { 185 | // make sure this is a real recognized panel 186 | Transform transform = PanelHolder.transform.GetChild(i); 187 | if (!transformIDToUIPanel.TryGetValue(transform.GetInstanceID(), out PanelBase panel)) 188 | continue; 189 | 190 | // check if our mouse is clicking inside the panel 191 | Vector3 pos = panel.Rect.InverseTransformPoint(mousePos); 192 | if (!panel.Enabled || !panel.Rect.rect.Contains(pos)) 193 | continue; 194 | 195 | // Panel was clicked in. 196 | focusHandledThisFrame = true; 197 | clickedInAny = true; 198 | 199 | Owner.SetOnTop(); 200 | 201 | // if this is not the top panel, reorder and invoke the onchanged event 202 | if (transform.GetSiblingIndex() != count - 1) 203 | { 204 | // Set the clicked panel to be on top 205 | transform.SetAsLastSibling(); 206 | 207 | InvokeOnPanelsReordered(); 208 | } 209 | 210 | break; 211 | } 212 | } 213 | 214 | if (!clickedInAny) 215 | OnClickedOutsidePanels?.Invoke(); 216 | } 217 | 218 | // Resizing 219 | 220 | /// Invoked when panels are reordered. 221 | protected virtual void SortDraggerHeirarchy() 222 | { 223 | draggerInstances.Sort((a, b) => b.Rect.GetSiblingIndex().CompareTo(a.Rect.GetSiblingIndex())); 224 | } 225 | 226 | /// 227 | /// Updates all PanelDraggers owned by this PanelManager. 228 | /// 229 | internal protected virtual void UpdateDraggers() 230 | { 231 | if (!MouseInTargetDisplay) 232 | return; 233 | 234 | if (!resizeCursor) 235 | CreateResizeCursorUI(); 236 | 237 | MouseState state; 238 | if (InputManager.GetMouseButtonDown(0)) 239 | state = MouseState.Down; 240 | else if (InputManager.GetMouseButton(0)) 241 | state = MouseState.Held; 242 | else 243 | state = MouseState.NotPressed; 244 | 245 | Vector3 mousePos = MousePosition; 246 | 247 | foreach (PanelDragger instance in draggerInstances) 248 | { 249 | if (!instance.Rect.gameObject.activeSelf) 250 | continue; 251 | 252 | instance.Update(state, mousePos); 253 | 254 | if (draggerHandledThisFrame) 255 | break; 256 | } 257 | 258 | if (wasAnyDragging && state == MouseState.NotPressed) 259 | { 260 | foreach (PanelDragger instance in draggerInstances) 261 | instance.WasDragging = false; 262 | wasAnyDragging = false; 263 | } 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/Universe.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Reflection; 8 | using UnityEngine; 9 | using UniverseLib.Config; 10 | using UniverseLib.Input; 11 | using UniverseLib.Runtime; 12 | using UniverseLib.UI; 13 | 14 | #if IL2CPP 15 | using Il2CppInterop.Runtime; 16 | using IL2CPPUtils = Il2CppInterop.Common.Il2CppInteropUtils; 17 | #endif 18 | 19 | namespace UniverseLib 20 | { 21 | public class Universe 22 | { 23 | public enum GlobalState 24 | { 25 | WaitingToSetup, 26 | SettingUp, 27 | SetupCompleted 28 | } 29 | 30 | public const string NAME = "UniverseLib"; 31 | public const string VERSION = "2.0.1"; 32 | public const string AUTHOR = "Release"; 33 | public const string GUID = "release.universelib"; 34 | 35 | /// The current runtime context (Mono or IL2CPP). 36 | public static RuntimeContext Context { get; } = 37 | #if MONO 38 | RuntimeContext.Mono; 39 | #else 40 | RuntimeContext.IL2CPP; 41 | #endif 42 | 43 | /// The current setup state of UniverseLib. 44 | public static GlobalState CurrentGlobalState { get; private set; } 45 | 46 | internal static Harmony Harmony { get; } = new Harmony(GUID); 47 | 48 | static float startupDelay; 49 | static event Action OnInitialized; 50 | 51 | static Action logHandler; 52 | 53 | /// 54 | /// Initialize UniverseLib with default settings, if you don't require any finer control over the startup process. 55 | /// 56 | /// Invoked after the startupDelayand after UniverseLib has finished initializing. 57 | /// Should be used for printing UniverseLib's own internal logs. Your listener will only be used if no listener has 58 | /// yet been provided to handle it. It is not required to implement this but it may be useful to diagnose internal errors. 59 | public static void Init(Action onInitialized = null, Action logHandler = null) 60 | => Init(1f, onInitialized, logHandler, default); 61 | 62 | /// 63 | /// Initialize UniverseLib with the provided parameters. 64 | /// 65 | /// Will be used only if it is the highest value supplied to this method compared to other assemblies. 66 | /// If another assembly calls this Init method with a higher startupDelay, their value will be used instead. 67 | /// Invoked after the and after UniverseLib has finished initializing. 68 | /// Should be used for printing UniverseLib's own internal logs. Your listener will only be used if no listener has 69 | /// yet been provided to handle it. It is not required to implement this but it may be useful to diagnose internal errors. 70 | /// Can be used to set certain values of UniverseLib's configuration. Null config values will be ignored. 71 | public static void Init(float startupDelay, Action onInitialized, Action logHandler, UniverseLibConfig config) 72 | { 73 | // If already finished intializing, just return (and invoke onInitialized if supplied) 74 | if (CurrentGlobalState == GlobalState.SetupCompleted) 75 | { 76 | InvokeOnInitialized(onInitialized); 77 | return; 78 | } 79 | 80 | // Only use the provided startup delay if its higher than the current value 81 | if (startupDelay > Universe.startupDelay) 82 | Universe.startupDelay = startupDelay; 83 | 84 | ConfigManager.LoadConfig(config); 85 | 86 | OnInitialized += onInitialized; 87 | 88 | if (logHandler != null && Universe.logHandler == null) 89 | Universe.logHandler = logHandler; 90 | 91 | if (CurrentGlobalState == GlobalState.WaitingToSetup) 92 | { 93 | CurrentGlobalState = GlobalState.SettingUp; 94 | Log($"{NAME} {VERSION} initializing..."); 95 | 96 | // Run immediate setups which don't require any delay 97 | UniversalBehaviour.Setup(); 98 | ReflectionUtility.Init(); 99 | RuntimeHelper.Init(); 100 | 101 | // Begin the startup delay coroutine 102 | RuntimeHelper.Instance.Internal_StartCoroutine(SetupCoroutine()); 103 | 104 | Log($"Finished UniverseLib initial setup."); 105 | } 106 | } 107 | 108 | internal static void Update() 109 | { 110 | UniversalUI.Update(); 111 | } 112 | 113 | private static IEnumerator SetupCoroutine() 114 | { 115 | // Always yield at least one frame, otherwise if the first startupDelay is 0f this would run immediately and 116 | // not allow other Init calls to set a higher delay. 117 | yield return null; 118 | 119 | Stopwatch sw = new(); 120 | sw.Start(); 121 | while (ReflectionUtility.Initializing || sw.ElapsedMilliseconds * 0.001f < startupDelay) 122 | yield return null; 123 | 124 | // Initialize late startup processes 125 | InputManager.Init(); 126 | UniversalUI.Init(); 127 | 128 | Log($"{NAME} {VERSION} initialized."); 129 | CurrentGlobalState = GlobalState.SetupCompleted; 130 | 131 | InvokeOnInitialized(OnInitialized); 132 | } 133 | 134 | private static void InvokeOnInitialized(Action onInitialized) 135 | { 136 | if (onInitialized == null) 137 | return; 138 | 139 | foreach (Delegate listener in onInitialized.GetInvocationList()) 140 | { 141 | try 142 | { 143 | listener.DynamicInvoke(); 144 | } 145 | catch (Exception ex) 146 | { 147 | LogWarning($"Exception invoking onInitialized callback! {ex}"); 148 | } 149 | } 150 | } 151 | 152 | // UniverseLib internal logging. These are assumed to be handled by a logHandler supplied to Init(). 153 | // Not for external use. 154 | 155 | internal static void Log(object message) 156 | => Log(message, LogType.Log); 157 | 158 | internal static void LogWarning(object message) 159 | => Log(message, LogType.Warning); 160 | 161 | internal static void LogError(object message) 162 | => Log(message, LogType.Error); 163 | 164 | static void Log(object message, LogType logType) 165 | { 166 | if (logHandler == null) 167 | return; 168 | logHandler($"[UniverseLib] {message?.ToString() ?? string.Empty}", logType); 169 | } 170 | 171 | // Patching helpers 172 | 173 | internal static bool Patch(Type type, string methodName, MethodType methodType, Type[] arguments = null, 174 | MethodInfo prefix = null, MethodInfo postfix = null, MethodInfo finalizer = null) 175 | { 176 | try 177 | { 178 | string namePrefix = methodType switch 179 | { 180 | MethodType.Getter => "get_", 181 | MethodType.Setter => "set_", 182 | _ => string.Empty 183 | }; 184 | 185 | MethodInfo target; 186 | if (arguments != null) 187 | target = type.GetMethod($"{namePrefix}{methodName}", AccessTools.all, null, arguments, null); 188 | else 189 | target = type.GetMethod($"{namePrefix}{methodName}", AccessTools.all); 190 | 191 | if (target == null) 192 | { 193 | // LogWarning($"\t Couldn't find any method on type {type.FullName} called {methodName}!"); 194 | return false; 195 | } 196 | 197 | #if IL2CPP 198 | // if this is an IL2CPP type, ensure method wasn't stripped. 199 | if (Il2CppType.From(type, false) != null 200 | && IL2CPPUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(target) == null) 201 | { 202 | Log($"\t IL2CPP method has no corresponding pointer, aborting patch of {type.FullName}.{methodName}"); 203 | return false; 204 | } 205 | #endif 206 | 207 | PatchProcessor processor = Harmony.CreateProcessor(target); 208 | 209 | if (prefix != null) 210 | processor.AddPrefix(new HarmonyMethod(prefix)); 211 | if (postfix != null) 212 | processor.AddPostfix(new HarmonyMethod(postfix)); 213 | if (finalizer != null) 214 | processor.AddFinalizer(new HarmonyMethod(finalizer)); 215 | 216 | processor.Patch(); 217 | 218 | // Log($"\t Successfully patched {type.FullName}.{methodName}"); 219 | return true; 220 | } 221 | catch (Exception ex) 222 | { 223 | LogWarning($"\t Exception patching {type.FullName}.{methodName}: {ex}"); 224 | return false; 225 | } 226 | } 227 | 228 | internal static bool Patch(Type type, string[] possibleNames, MethodType methodType, Type[] arguments = null, 229 | MethodInfo prefix = null, MethodInfo postfix = null, MethodInfo finalizer = null) 230 | { 231 | foreach (var name in possibleNames) 232 | { 233 | if (Patch(type, name, methodType, arguments, prefix, postfix, finalizer)) 234 | return true; 235 | } 236 | return false; 237 | } 238 | 239 | internal static bool Patch(Type type, string[] possibleNames, MethodType methodType, Type[][] possibleArguments, 240 | MethodInfo prefix = null, MethodInfo postfix = null, MethodInfo finalizer = null) 241 | { 242 | foreach (var name in possibleNames) 243 | { 244 | foreach (var arguments in possibleArguments) 245 | { 246 | if (Patch(type, name, methodType, arguments, prefix, postfix, finalizer)) 247 | return true; 248 | } 249 | } 250 | return false; 251 | } 252 | 253 | internal static bool Patch(Type type, string methodName, MethodType methodType, Type[][] possibleArguments, 254 | MethodInfo prefix = null, MethodInfo postfix = null, MethodInfo finalizer = null) 255 | { 256 | foreach (var arguments in possibleArguments) 257 | { 258 | if (Patch(type, methodName, methodType, arguments, prefix, postfix, finalizer)) 259 | return true; 260 | } 261 | return false; 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/Input/InputManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using UnityEngine.EventSystems; 7 | using UniverseLib.UI; 8 | 9 | namespace UniverseLib.Input 10 | { 11 | /// 12 | /// A universal Input handler which works with both legacy Input and the new InputSystem. 13 | /// 14 | public static class InputManager 15 | { 16 | /// 17 | /// The current Input package which is being used by the game. 18 | /// 19 | public static InputType CurrentType { get; private set; } 20 | 21 | internal static IHandleInput inputHandler; 22 | 23 | #region Internal Init 24 | 25 | internal static void Init() 26 | { 27 | InitHandler(); 28 | InitKeycodes(); 29 | CursorUnlocker.Init(); 30 | EventSystemHelper.Init(); 31 | } 32 | 33 | private static void InitHandler() 34 | { 35 | // First, just try to use the legacy input, see if its working. 36 | // The InputSystem package may be present but not actually activated, so we can find out this way. 37 | 38 | // With BepInEx Il2CppInterop, for some reason InputLegacyModule may be loaded but our ReflectionUtility doesn't cache it? 39 | // No idea why or what is happening but this solves it for now. 40 | if (ReflectionUtility.GetTypeByName("UnityEngine.Input") == null) 41 | { 42 | foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) 43 | { 44 | if (asm.FullName.Contains("UnityEngine.InputLegacyModule")) 45 | { 46 | ReflectionUtility.CacheTypes(asm); 47 | break; 48 | } 49 | } 50 | } 51 | 52 | if (LegacyInput.TInput != null) 53 | { 54 | try 55 | { 56 | inputHandler = new LegacyInput(); 57 | CurrentType = InputType.Legacy; 58 | 59 | // make sure its working 60 | inputHandler.GetKeyDown(KeyCode.F5); 61 | 62 | Universe.Log("Initialized Legacy Input support"); 63 | return; 64 | } 65 | catch 66 | { 67 | // It's not working, we'll fall back to InputSystem. 68 | } 69 | } 70 | 71 | if (InputSystem.TKeyboard != null) 72 | { 73 | try 74 | { 75 | inputHandler = new InputSystem(); 76 | CurrentType = InputType.InputSystem; 77 | Universe.Log("Initialized new InputSystem support."); 78 | return; 79 | } 80 | catch (Exception ex) 81 | { 82 | Universe.Log(ex); 83 | } 84 | } 85 | 86 | Universe.LogWarning("Could not find any Input Module Type!"); 87 | inputHandler = new NoInput(); 88 | CurrentType = InputType.None; 89 | } 90 | 91 | private static void InitKeycodes() 92 | { 93 | // Cache keycodes for rebinding 94 | 95 | Array keycodes = Enum.GetValues(typeof(KeyCode)); 96 | List list = new(); 97 | foreach (KeyCode kc in keycodes) 98 | { 99 | string s = kc.ToString(); 100 | if (!s.Contains("Mouse") && !s.Contains("Joystick")) 101 | list.Add(kc); 102 | } 103 | allKeycodes = list.ToArray(); 104 | } 105 | 106 | #endregion 107 | 108 | // ~~~~~~ Main Input API ~~~~~~ 109 | 110 | /// 111 | /// The current user Cursor position, with (0,0) being the bottom-left of the game display window. 112 | /// 113 | public static Vector3 MousePosition 114 | { 115 | get 116 | { 117 | if (Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 118 | return Vector2.zero; 119 | 120 | return inputHandler.MousePosition; 121 | } 122 | } 123 | 124 | /// 125 | /// The current mouse scroll delta from this frame. x is horizontal, y is vertical. 126 | /// 127 | public static Vector2 MouseScrollDelta 128 | { 129 | get 130 | { 131 | if (Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 132 | return Vector2.zero; 133 | 134 | return inputHandler.MouseScrollDelta; 135 | } 136 | } 137 | 138 | /// 139 | /// Returns true if the provided KeyCode was pressed this frame. 140 | /// Translates KeyCodes into Key if InputSystem is being used. 141 | /// 142 | public static bool GetKeyDown(KeyCode key) 143 | { 144 | if (Rebinding || Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 145 | return false; 146 | 147 | if (key == KeyCode.None) 148 | return false; 149 | return inputHandler.GetKeyDown(key); 150 | } 151 | 152 | /// 153 | /// Returns true if the provided KeyCode is being held down (not necessarily just pressed). 154 | /// Translates KeyCodes into Key if InputSystem is being used. 155 | /// 156 | public static bool GetKey(KeyCode key) 157 | { 158 | if (Rebinding || Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 159 | return false; 160 | 161 | if (key == KeyCode.None) 162 | return false; 163 | return inputHandler.GetKey(key); 164 | } 165 | 166 | /// 167 | /// Returns true if the provided KeyCode was released this frame. 168 | /// Translates KeyCodes into Key if InputSystem is being used. 169 | /// 170 | public static bool GetKeyUp(KeyCode key) 171 | { 172 | if (Rebinding || Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 173 | return false; 174 | 175 | if (key == KeyCode.None) 176 | return false; 177 | return inputHandler.GetKeyUp(key); 178 | } 179 | 180 | /// 181 | /// Returns true if the provided mouse button was pressed this frame. 182 | ///
0 = left, 1 = right, 2 = middle, 3 = back, 4 = forward. 183 | ///
184 | public static bool GetMouseButtonDown(int btn) 185 | { 186 | if (Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 187 | return false; 188 | 189 | return inputHandler.GetMouseButtonDown(btn); 190 | } 191 | 192 | /// 193 | /// Returns true if the provided mouse button is being held down (not necessarily just pressed). 194 | ///
0 = left, 1 = right, 2 = middle, 3 = back, 4 = forward. 195 | ///
196 | public static bool GetMouseButton(int btn) 197 | { 198 | if (Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 199 | return false; 200 | 201 | return inputHandler.GetMouseButton(btn); 202 | } 203 | 204 | /// 205 | /// Returns true if the provided mouse button was released this frame. 206 | ///
0 = left, 1 = right, 2 = middle, 3 = back, 4 = forward. 207 | ///
208 | public static bool GetMouseButtonUp(int btn) 209 | { 210 | if (Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 211 | return false; 212 | 213 | return inputHandler.GetMouseButtonUp(btn); 214 | } 215 | 216 | /// 217 | /// Calls the equivalent method for the current to reset the Input axes. 218 | /// 219 | public static void ResetInputAxes() 220 | { 221 | if (Universe.CurrentGlobalState != Universe.GlobalState.SetupCompleted) 222 | return; 223 | 224 | inputHandler.ResetInputAxes(); 225 | } 226 | 227 | // ~~~~~~ Rebinding ~~~~~~ 228 | 229 | /// 230 | /// Whether anything is currently using the Rebinding feature. If no UI is showing, this will return false. 231 | /// 232 | public static bool Rebinding 233 | { 234 | get => isRebinding && UniversalUI.AnyUIShowing; 235 | set => isRebinding = value; 236 | } 237 | static bool isRebinding; 238 | 239 | /// 240 | /// The last pressed Key during rebinding. 241 | /// 242 | public static KeyCode? LastRebindKey { get; set; } 243 | 244 | internal static IEnumerable allKeycodes; 245 | internal static Action onRebindPressed; 246 | internal static Action onRebindFinished; 247 | 248 | internal static void Update() 249 | { 250 | if (Rebinding) 251 | { 252 | KeyCode? kc = GetCurrentKeyDown(); 253 | if (kc != null) 254 | { 255 | LastRebindKey = kc; 256 | onRebindPressed?.Invoke((KeyCode)kc); 257 | } 258 | } 259 | } 260 | 261 | internal static KeyCode? GetCurrentKeyDown() 262 | { 263 | foreach (KeyCode kc in allKeycodes) 264 | { 265 | if (inputHandler.GetKeyDown(kc)) 266 | return kc; 267 | } 268 | 269 | return null; 270 | } 271 | 272 | /// 273 | /// Begins the Rebinding process, keys pressed will be recorded. Call to finish rebinding. 274 | /// 275 | /// Will be invoked whenever any key is pressed, even if rebinding has not finished yet. 276 | /// Invoked when EndRebind is called. 277 | public static void BeginRebind(Action onSelection, Action onFinished) 278 | { 279 | if (Rebinding) 280 | return; 281 | 282 | onRebindPressed = onSelection; 283 | onRebindFinished = onFinished; 284 | 285 | Rebinding = true; 286 | LastRebindKey = null; 287 | } 288 | 289 | /// 290 | /// Call this to finish Rebinding. The onFinished Action supplied to will be invoked if we are currently Rebinding. 291 | /// 292 | public static void EndRebind() 293 | { 294 | if (!Rebinding) 295 | return; 296 | 297 | Rebinding = false; 298 | onRebindFinished?.Invoke(LastRebindKey); 299 | 300 | onRebindFinished = null; 301 | onRebindPressed = null; 302 | } 303 | } 304 | } -------------------------------------------------------------------------------- /src/UI/Widgets/ScrollView/DataHeightCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | namespace UniverseLib.UI.Widgets.ScrollView 8 | { 9 | /// 10 | /// Used to handle the underlying height data for a scroll pool, tracking which data values are at which position and how far they span.

11 | /// 12 | /// A DataHeightCache is created and managed automatically by a ScrollPool, you do not need to use this class yourself. 13 | ///
14 | public class DataHeightCache where T : ICell 15 | { 16 | private ScrollPool ScrollPool { get; } 17 | 18 | public DataHeightCache(ScrollPool scrollPool) 19 | { 20 | ScrollPool = scrollPool; 21 | } 22 | 23 | private readonly List heightCache = new(); 24 | 25 | public DataViewInfo this[int index] 26 | { 27 | get => heightCache[index]; 28 | set => SetIndex(index, value); 29 | } 30 | 31 | public int Count => heightCache.Count; 32 | 33 | public float TotalHeight => totalHeight; 34 | private float totalHeight; 35 | 36 | public float DefaultHeight => m_defaultHeight ?? (float)(m_defaultHeight = ScrollPool.PrototypeHeight); 37 | private float? m_defaultHeight; 38 | 39 | /// 40 | /// Lookup table for "which data index first appears at this position"
41 | /// Index: DefaultHeight * index from top of data
42 | /// Value: the first data index at this position
43 | ///
44 | private readonly List rangeCache = new(); 45 | 46 | /// Same as GetRangeIndexOfPosition, except this rounds up to the next division if there was remainder from the previous cell. 47 | private int GetRangeCeilingOfPosition(float position) => (int)Math.Ceiling((decimal)position / (decimal)DefaultHeight); 48 | 49 | /// Get the first range (division of DefaultHeight) which the position appears in. 50 | private int GetRangeFloorOfPosition(float position) => (int)Math.Floor((decimal)position / (decimal)DefaultHeight); 51 | 52 | public int GetFirstDataIndexAtPosition(float desiredHeight) 53 | { 54 | if (!heightCache.Any()) 55 | return 0; 56 | 57 | int rangeIndex = GetRangeFloorOfPosition(desiredHeight); 58 | 59 | // probably shouldnt happen but just in case 60 | if (rangeIndex < 0) 61 | return 0; 62 | if (rangeIndex >= rangeCache.Count) 63 | { 64 | int idx = ScrollPool.DataSource.ItemCount - 1; 65 | return idx; 66 | } 67 | 68 | int dataIndex = rangeCache[rangeIndex]; 69 | DataViewInfo cache = heightCache[dataIndex]; 70 | 71 | // if the DataViewInfo is outdated, need to rebuild 72 | int expectedMin = GetRangeCeilingOfPosition(cache.startPosition); 73 | int expectedMax = expectedMin + cache.normalizedSpread - 1; 74 | if (rangeIndex < expectedMin || rangeIndex > expectedMax) 75 | { 76 | RecalculateStartPositions(ScrollPool.DataSource.ItemCount - 1); 77 | 78 | rangeIndex = GetRangeFloorOfPosition(desiredHeight); 79 | dataIndex = rangeCache[rangeIndex]; 80 | } 81 | 82 | return dataIndex; 83 | } 84 | 85 | /// 86 | /// Get the spread of the height, starting from the start position.

87 | /// The "spread" begins at the start of the next interval of the DefaultHeight, then increases for 88 | /// every interval beyond that. 89 | ///
90 | private int GetRangeSpread(float startPosition, float height) 91 | { 92 | // get the remainder of the start position divided by min height 93 | float rem = startPosition % DefaultHeight; 94 | 95 | // if there is a remainder, this means the previous cell started in our first cell and 96 | // they take priority, so reduce our height by (minHeight - remainder) to account for that. 97 | // We need to fill that gap and reach the next cell before we take priority. 98 | if (rem != 0.0f) 99 | height -= (DefaultHeight - rem); 100 | 101 | return (int)Math.Ceiling((decimal)height / (decimal)DefaultHeight); 102 | } 103 | 104 | /// Append a data index to the cache with the provided height value. 105 | public void Add(float value) 106 | { 107 | value = Math.Max(DefaultHeight, value); 108 | 109 | int spread = GetRangeSpread(totalHeight, value); 110 | 111 | heightCache.Add(new DataViewInfo(heightCache.Count, value, totalHeight, spread)); 112 | 113 | int dataIdx = heightCache.Count - 1; 114 | for (int i = 0; i < spread; i++) 115 | rangeCache.Add(dataIdx); 116 | 117 | totalHeight += value; 118 | } 119 | 120 | /// Remove the last (highest count) index from the height cache. 121 | public void RemoveLast() 122 | { 123 | if (!heightCache.Any()) 124 | return; 125 | 126 | totalHeight -= heightCache[heightCache.Count - 1]; 127 | heightCache.RemoveAt(heightCache.Count - 1); 128 | 129 | int idx = heightCache.Count; 130 | while (rangeCache.Count > 0 && rangeCache[rangeCache.Count - 1] == idx) 131 | rangeCache.RemoveAt(rangeCache.Count - 1); 132 | } 133 | 134 | /// Set a given data index with the specified value. 135 | public void SetIndex(int dataIndex, float height) 136 | { 137 | height = (float)Math.Floor(height); 138 | height = Math.Max(DefaultHeight, height); 139 | 140 | // If the index being set is beyond the DataSource item count, prune and return. 141 | if (dataIndex >= ScrollPool.DataSource.ItemCount) 142 | { 143 | while (heightCache.Count > dataIndex) 144 | RemoveLast(); 145 | return; 146 | } 147 | 148 | // If the data index exceeds our cache count, fill the gap. 149 | // This is done by the ScrollPool when the DataSource sets its initial count, or the count increases. 150 | if (dataIndex >= heightCache.Count) 151 | { 152 | while (dataIndex > heightCache.Count) 153 | Add(DefaultHeight); 154 | Add(height); 155 | return; 156 | } 157 | 158 | // We are actually updating an index. First, update the height and the totalHeight. 159 | DataViewInfo cache = heightCache[dataIndex]; 160 | if (cache.height != height) 161 | { 162 | float diff = height - cache.height; 163 | totalHeight += diff; 164 | cache.height = height; 165 | } 166 | 167 | // update our start position using the previous cell (if it exists) 168 | if (dataIndex > 0) 169 | { 170 | DataViewInfo prev = heightCache[dataIndex - 1]; 171 | cache.startPosition = prev.startPosition + prev.height; 172 | } 173 | 174 | // Get the normalized range index (actually ceiling) and spread based on our start position and height 175 | int rangeIndex = GetRangeCeilingOfPosition(cache.startPosition); 176 | int spread = GetRangeSpread(cache.startPosition, height); 177 | 178 | // If the previous item in the range cache is not the previous data index, there is a gap. 179 | if (rangeCache[rangeIndex] != dataIndex) 180 | { 181 | // Recalculate start positions up to this index. The gap could be anywhere before here. 182 | RecalculateStartPositions(ScrollPool.DataSource.ItemCount - 1); 183 | // Get the range index and spread again after rebuilding 184 | rangeIndex = GetRangeCeilingOfPosition(cache.startPosition); 185 | spread = GetRangeSpread(cache.startPosition, height); 186 | } 187 | 188 | if (rangeCache[rangeIndex] != dataIndex) 189 | throw new IndexOutOfRangeException($"Trying to set dataIndex {dataIndex} at rangeIndex {rangeIndex}, but cache is corrupt or invalid!"); 190 | 191 | if (spread != cache.normalizedSpread) 192 | { 193 | int spreadDiff = spread - cache.normalizedSpread; 194 | cache.normalizedSpread = spread; 195 | 196 | UpdateSpread(dataIndex, rangeIndex, spreadDiff); 197 | } 198 | 199 | // set the struct back to the array 200 | heightCache[dataIndex] = cache; 201 | } 202 | 203 | private void UpdateSpread(int dataIndex, int rangeIndex, int spreadDiff) 204 | { 205 | if (spreadDiff > 0) 206 | { 207 | while (rangeCache[rangeIndex] == dataIndex && spreadDiff > 0) 208 | { 209 | rangeCache.Insert(rangeIndex, dataIndex); 210 | spreadDiff--; 211 | } 212 | } 213 | else 214 | { 215 | while (rangeCache[rangeIndex] == dataIndex && spreadDiff < 0) 216 | { 217 | rangeCache.RemoveAt(rangeIndex); 218 | spreadDiff++; 219 | } 220 | } 221 | } 222 | 223 | private void RecalculateStartPositions(int toIndex) 224 | { 225 | if (heightCache.Count <= 1) 226 | return; 227 | 228 | rangeCache.Clear(); 229 | 230 | DataViewInfo cache; 231 | DataViewInfo prev = DataViewInfo.None; 232 | for (int idx = 0; idx <= toIndex && idx < heightCache.Count; idx++) 233 | { 234 | cache = heightCache[idx]; 235 | 236 | if (!prev.Equals(DataViewInfo.None)) 237 | cache.startPosition = prev.startPosition + prev.height; 238 | else 239 | cache.startPosition = 0; 240 | 241 | cache.normalizedSpread = GetRangeSpread(cache.startPosition, cache.height); 242 | for (int i = 0; i < cache.normalizedSpread; i++) 243 | rangeCache.Add(cache.dataIndex); 244 | 245 | heightCache[idx] = cache; 246 | 247 | prev = cache; 248 | } 249 | } 250 | } 251 | 252 | public struct DataViewInfo 253 | { 254 | // static 255 | public static DataViewInfo None => s_default; 256 | private static DataViewInfo s_default = default; 257 | 258 | public static implicit operator float(DataViewInfo it) => it.height; 259 | 260 | public DataViewInfo(int index, float height, float startPos, int spread) 261 | { 262 | this.dataIndex = index; 263 | this.height = height; 264 | this.startPosition = startPos; 265 | this.normalizedSpread = spread; 266 | } 267 | 268 | // instance 269 | public int dataIndex, normalizedSpread; 270 | public float height, startPosition; 271 | 272 | public override bool Equals(object obj) 273 | { 274 | DataViewInfo other = (DataViewInfo)obj; 275 | 276 | return this.dataIndex == other.dataIndex 277 | && this.height == other.height 278 | && this.startPosition == other.startPosition 279 | && this.normalizedSpread == other.normalizedSpread; 280 | } 281 | 282 | public override int GetHashCode() => base.GetHashCode(); 283 | } 284 | } 285 | --------------------------------------------------------------------------------