├── .gitignore ├── EditorForks ├── Properties │ └── AssemblyInfo.cs ├── UnityEditorForks.csproj ├── WorkaroundAssetDatabaseSaveAssetIfDirty.cs ├── WorkaroundUIToolkitMissingDefaultInspector.cs ├── WorkaroundUnityEditorSelectCreatedObject.cs ├── WorkaroundUnityMenuCommandContext.cs ├── WorkaroundUnityMissingAssetDatabaseFindAPI.cs ├── WorkaroundUnityPrefabEditingSafe.cs └── WorkaroundUnityUIToolkitBrokenObjectSelector.cs ├── EngineForks ├── FixUnityUndoBlock.cs ├── MissingClasses │ └── 2019-or-earlier │ │ └── HelpBox.cs ├── Properties │ └── AssemblyInfo.cs ├── UnityEngineForks.csproj ├── WorkaroundStarterAssetsDeletedInputFirstPersonController.cs ├── WorkaroundStarterAssetsDeletedInputStarterAssetsInputs.cs ├── WorkaroundUnityIMGUISetColor.cs ├── WorkaroundUnityInternal.cs ├── WorkaroundUnityMissingEditorVersion.cs ├── WorkaroundUnityMissingGetComponentsOnAncestors.cs ├── WorkaroundUnityMissingPlayerPrefsAPI.cs ├── WorkaroundUnityMissingTryGetComponent.cs ├── WorkaroundUnityTextGeneratorMissingErrorReporting.cs ├── WorkaroundUnityTexture2DPixelsPerPoint.cs ├── WorkaroundUnityUIToolkitButtonsWithImages.cs ├── WorkaroundUnityUIToolkitFindOrCreate.cs ├── WorkaroundUnityUIToolkitFoldoutToggleIsPrivate.cs ├── WorkaroundUnityUIToolkitMarginsAll.cs └── WorkaroundUnityUIToolkitToggleBrokenLabelBug.cs ├── PublishersFork.sln └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Critical! Do not commit your Unity folder! 2 | UnityEditorFolderSymlink/ 3 | 4 | # IDEs 5 | .idea 6 | 7 | # Microsoft C# build artifacts 8 | EditorForks/bin/ 9 | EditorForks/obj/ 10 | EngineForks/bin/ 11 | EngineForks/obj/ 12 | UnityEditorFolderSymlink 13 | -------------------------------------------------------------------------------- /EditorForks/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("UnityEditorForks")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("UnityEditorForks")] 12 | [assembly: AssemblyCopyright("Copyright © 2022")] 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("E43B98FF-ACF1-4561-B21E-355F0D4B5113")] 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("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /EditorForks/UnityEditorForks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {E43B98FF-ACF1-4561-B21E-355F0D4B5113} 8 | Library 9 | Properties 10 | UnityEditorForks 11 | UnityEditorForks 12 | v4.8 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ..\UnityEditorFolderSymlink\Data\Managed\UnityEditor.dll 41 | 42 | 43 | ..\UnityEditorFolderSymlink\Data\Managed\UnityEngine.dll 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 64 | -------------------------------------------------------------------------------- /EditorForks/WorkaroundAssetDatabaseSaveAssetIfDirty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEditor; 4 | 5 | namespace UnityEditorForks 6 | { 7 | /// 8 | /// c.f. this post: https://forum.unity.com/threads/resource-assetdatabase-saveassetifdirty-fix-backport-polyfill.1410765/ 9 | /// 10 | /// The Unity core API call "SaveAssetIfDirty" isn't included in a lot of current Unity versions, and the set for 11 | /// which it is/isn't present is non-trivial. This class works around that by providing one method call that works 12 | /// on all versions of Unity. 13 | /// 14 | public class WorkaroundAssetDatabaseSaveAssetIfDirty 15 | { 16 | static bool _saveIfDirtyMethodRetrieved; 17 | static MethodInfo _saveIfDirtyObjMethod; 18 | static MethodInfo _saveIfDirtyGuidMethod; 19 | 20 | static bool cacheReflectionsAndReturnResult(bool useGUID) 21 | { 22 | if (!_saveIfDirtyMethodRetrieved) 23 | { 24 | _saveIfDirtyMethodRetrieved = true; 25 | // fail silently 26 | try 27 | { 28 | var assembiles = AppDomain.CurrentDomain.GetAssemblies(); 29 | foreach (var assembly in assembiles) 30 | { 31 | if (assembly.GetName().Name == "UnityEditor") 32 | { 33 | var assetDatabaseType = assembly.GetType("UnityEditor.AssetDatabase", throwOnError: true); 34 | _saveIfDirtyObjMethod = assetDatabaseType.GetMethod("SaveAssetIfDirty", new Type[] { typeof(UnityEngine.Object) }); 35 | _saveIfDirtyGuidMethod = assetDatabaseType.GetMethod("SaveAssetIfDirty", new Type[] { typeof(GUID) }); 36 | break; 37 | } 38 | } 39 | if (useGUID) 40 | return _saveIfDirtyGuidMethod != null; 41 | else 42 | return _saveIfDirtyObjMethod != null; 43 | 44 | } 45 | catch (Exception) 46 | { 47 | return false; 48 | } 49 | } 50 | 51 | if (useGUID) 52 | return _saveIfDirtyGuidMethod != null; 53 | else 54 | return _saveIfDirtyObjMethod != null; 55 | } 56 | 57 | static object[] _parameters = new object[1]; 58 | 59 | public static void SaveAssetIfDirty(UnityEngine.Object obj) 60 | { 61 | #if UNITY_2021_2_OR_NEWER 62 | // Available in all versions. 63 | AssetDatabase.SaveAssetIfDirty(this); 64 | #elif UNITY_2020_3_OR_NEWER 65 | try 66 | { 67 | // Available in some: 68 | // Unity 2020.3.16+ has it 69 | // Unity 2021.1.17+ has it 70 | // Unity 2021.2.0+ has it 71 | if (cacheReflectionsAndReturnResult(useGUID: false)) 72 | { 73 | _parameters[0] = obj; 74 | _saveIfDirtyObjMethod.Invoke(null, _parameters); 75 | } 76 | else 77 | { 78 | AssetDatabase.SaveAssets(); 79 | } 80 | } 81 | catch(Exception) 82 | { 83 | AssetDatabase.SaveAssets(); 84 | } 85 | #else 86 | // SaveAssetIfDirty is never available. 87 | AssetDatabase.SaveAssets(); 88 | #endif 89 | } 90 | 91 | public static void SaveAssetIfDirty(GUID guid) 92 | { 93 | #if UNITY_2021_2_OR_NEWER 94 | // Available in all versions. 95 | AssetDatabase.SaveAssetIfDirty(this); 96 | #elif UNITY_2020_3_OR_NEWER 97 | // Available in some: 98 | // Unity 2020.3.16+ has it 99 | // Unity 2021.1.17+ has it 100 | // Unity 2021.2.0+ has it 101 | if (cacheReflectionsAndReturnResult(useGUID: true)) 102 | { 103 | _parameters[0] = guid; 104 | _saveIfDirtyObjMethod.Invoke(null, _parameters); 105 | } 106 | else 107 | { 108 | AssetDatabase.SaveAssets(); 109 | } 110 | #else 111 | // SaveAssetIfDirty is never available. 112 | AssetDatabase.SaveAssets(); 113 | #endif 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /EditorForks/WorkaroundUIToolkitMissingDefaultInspector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEditor.UIElements; 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | 8 | /// 9 | /// DrawDefaultInspector was a core feature of the Editor APIs for the 15+ years. The UIToolkit team decided to ignore 10 | /// it and break it and provide no alternative in their new system until 3 years after releasing the 'production ready' 11 | /// version. Unity 2022 and later have this method but it was never backported despite having zero dependencies and 12 | /// despite being a required part of Unity's own APIs. 13 | /// 14 | public class WorkaroundUIToolkitMissingDefaultInspector 15 | { 16 | /// 17 | /// This implementation provided by UIToolkit team via the forums, 18 | /// see the pre-release version here: https://forum.unity.com/threads/property-drawers.595369/#post-5426619 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static void FillDefaultInspector( VisualElement container, SerializedObject serializedObject, bool hideScript) 24 | { 25 | SerializedProperty property = serializedObject.GetIterator(); 26 | if (property.NextVisible(true)) // Expand first child. 27 | { 28 | do 29 | { 30 | if (property.propertyPath == "m_Script" && hideScript) 31 | { 32 | continue; 33 | } 34 | var field = new PropertyField(property); 35 | field.name = "PropertyField:" + property.propertyPath; 36 | 37 | 38 | if (property.propertyPath == "m_Script" && serializedObject.targetObject != null) 39 | { 40 | field.SetEnabled(false); 41 | } 42 | 43 | container.Add(field); 44 | } 45 | while (property.NextVisible(false)); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /EditorForks/WorkaroundUnityEditorSelectCreatedObject.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace UnityEditorForks 5 | { 6 | /// 7 | /// After you create an object in Editor, in 99.9% of cases you immediately want to let the user rename it - Unity's 8 | /// staff themselves make this the default behaviour for all Unity code, but so far have refused to let anyone else do it directly, 9 | /// so we have to ping the (public!) methods, and implement some (required!) artificial delays. 10 | /// 11 | /// Usage: 12 | /// 1. Create a new object in script - e.g. "var go = new GameObject()" 13 | /// 2. Call "go.SelectAndRename();" 14 | /// 15 | /// ...this will cause the new object to be automatically selected in the Editor *and* the UI built-in 'rename new object' 16 | /// interface to be triggered, allowing the user to interactively name it without wasting time and keystrokes. 17 | /// 18 | public static class WorkaroundUnityEditorSelectCreatedObject 19 | { 20 | public static void SelectAndRename( this GameObject go ) 21 | { 22 | Selection.activeGameObject = go; 23 | 24 | #if UNITY_2021_1_OR_NEWER 25 | EditorApplication.update += Delay1Frame_Rename; 26 | #else // Unity broke this in Unity 2021-ish; since approx Unity-2021 you need to use the other approach 27 | // Workaround for 10+ years bug that Unity still doesn't let you select things in Hierarchy: 28 | ((EditorWindow)Resources.FindObjectsOfTypeAll( typeof(UnityEditor.Editor).Assembly.GetType( "UnityEditor.SceneHierarchyWindow" ) )[0]).Focus(); 29 | #endif 30 | } 31 | 32 | /// Unity engineers couldn't make 'create object' execute correctly within a single frame, requiring MULTIPLE FRAMES before you can call the 'real' edit-name method 33 | private static void Delay1Frame_Rename() 34 | { 35 | EditorApplication.update -= Delay1Frame_Rename; 36 | EditorApplication.update += Rename; 37 | } 38 | 39 | static void Rename() 40 | { 41 | EditorApplication.update -= Rename; 42 | ((EditorWindow)Resources.FindObjectsOfTypeAll( typeof(UnityEditor.Editor).Assembly.GetType( "UnityEditor.SceneHierarchyWindow" ) )[0]).Focus(); 43 | EditorApplication.ExecuteMenuItem( "Window/General/Hierarchy" ); 44 | EditorApplication.ExecuteMenuItem( "Edit/Rename" ); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /EditorForks/WorkaroundUnityMenuCommandContext.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace UnityEditorForks 5 | { 6 | /// 7 | /// Unity's MenuCommand is missing a key feature: 'which GameObject did the user right-click to execute this menu?', 8 | /// which you typically want when using Unity's [MenuItem] attribute. 9 | /// 10 | /// The code in this class is based on Unity's own API docs where they read Selection.* when command.context is null. 11 | /// 12 | public static class WorkaroundUnityMenuCommandContext 13 | { 14 | /// 15 | /// 16 | /// 17 | /// The Unity-API 'command.context' if not-null, otherwise the selected GameObject from Hierarchy panel 18 | public static GameObject ContextGameObject( this MenuCommand command ) 19 | { 20 | GameObject _candidate; 21 | if( command != null && command.context != null ) 22 | _candidate = (command.context as GameObject); 23 | else if( Selection.activeGameObject != null ) 24 | _candidate = Selection.activeGameObject; 25 | else 26 | _candidate = null; 27 | 28 | return _candidate; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /EditorForks/WorkaroundUnityMissingAssetDatabaseFindAPI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using Object = UnityEngine.Object; 8 | 9 | namespace UnityEditorForks 10 | { 11 | /// 12 | /// Unity staff know that 'AssetDatabase.FindAssets' is broken in multiple core cases, but refuse to 13 | /// fix it (and keep refusing to update their docs to admit that they know it's broken - so the docs are also wrong). 14 | /// This class aims to fix the common cases that Unity won't. Most of these could be fixed in fewer lines of code by 15 | /// Unity fixing their internal classes e.g. SearchFilter - Unity staff don't seem to understand what 'internal' keyword 16 | /// in C# actuall means. 17 | /// 18 | /// Without heavy use of complex Reflection calls, only Unity staff can access the true Find API in Unity 2019-2023 19 | /// and the replacement ("use magic strings with the AssetDatabase.Find methods") is extremely fragile and incorrectly documented 20 | /// (I logged bugs against this more than 21 | /// two years ago, that were accepted, but Unity still hasn't fixed them - still hasn't corrected their own docs). 22 | /// 23 | /// This class wraps the bad API call into a sensible one, and adds some obvious missing API calls they should/would 24 | /// have included if they'd accepted input from real users before publishing it. 25 | /// 26 | /// 2023 UPDATE: this also mostly fixes the multiple-years-old bug in Unity that if the Type you're searching for doesn't exist 27 | /// in a file with the same literal name (due to sheer laziness by Unity staff) then Unity returns incorrect results. 28 | /// Unity's bug breaks all searches for T where T is itself Generic (e.g. T = MyClass). Unity can't be 29 | /// bothered to throw an exception, or to document this known failing - they just return wrong results instead. 30 | /// 31 | public static class WorkaroundUnityMissingAssetDatabaseFindAPI 32 | { 33 | /// 34 | /// Unity's new FindAssets API fails to support type-search, it supports only 'subtypes of ScriptableObject search', 35 | /// because they didn't think at all about the names or use-cases of their new API when creating it. So the obvious 36 | /// search-by-type will ALWAYS fail (by design - Unity's bad design) if you need to find 'assets that contain this 37 | /// type'. This method fixes that mistake in Unity's codebase by letting you search for "assets that contain 38 | /// components of type T". 39 | /// 40 | /// 41 | /// 42 | /// 43 | public static List FindAssetPrefabsByComponentType( bool debugSettingsDiscovery = false ) where T : UnityEngine.Object 44 | { 45 | var prefabAssets = AssetDatabase.FindAssets( "t:prefab" ); 46 | 47 | var foundComponentInstances = new List(); 48 | for( int i = 0; i < prefabAssets.Length; i++ ) 49 | { 50 | if( AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath( prefabAssets[i] ) ).TryGetComponent( out var component ) ) 51 | foundComponentInstances.Add( component ); 52 | } 53 | 54 | if( debugSettingsDiscovery ) Debug.Log( "Found objects at: " + string.Join( ",", foundComponentInstances.Select( settings => AssetDatabase.GetAssetPath( settings ) ) ) ); 55 | 56 | return foundComponentInstances; 57 | } 58 | 59 | /// 60 | /// Unity has an equivalent API call but for the past 4+ years they've refused to let non-Unity staff access it; you 61 | /// can access it via reflection and override their incorrect use of 'internal', or (their official recommendation) 62 | /// you can fake it using various combinations of their undocumented features (e.g. "a:" etc) of FindAssets(). 63 | /// 64 | /// It is particularly frustrating that we're converting a Type to a string so that Unity's code can convert it back 65 | /// to a Type, and re-create the generic we had in the first place but which a Unity team refuses to let anyone 66 | /// else call directly. 67 | /// 68 | /// 69 | /// 70 | /// 71 | public static List FindAssetsByType( bool debugSettingsDiscovery = false ) where T : UnityEngine.Object 72 | { 73 | /** Unity's API is wrongly documented - it does NOT support 'types' it only supports 'classnames'; we have 74 | * to do the conversion manually when those things differ 75 | */ 76 | if( typeof(T).IsGenericType ) 77 | { 78 | var settingsFileAssets = AssetDatabase.FindAssets( "t:ScriptableObject" ); 79 | var foundObjects = new List(); 80 | for( int i = 0; i < settingsFileAssets.Length; i++ ) 81 | { 82 | var so = AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath( settingsFileAssets[i] ) ); 83 | if( so is T ) 84 | foundObjects.Add( so as T ); 85 | } 86 | 87 | if( debugSettingsDiscovery ) Debug.Log( "Found " + settingsFileAssets.Length + " assets of type (" + typeof(T) + "): " + string.Join( ",", foundObjects.Select( settings => AssetDatabase.GetAssetPath( settings ) ) ) ); 88 | 89 | return foundObjects; 90 | } 91 | else 92 | { 93 | var settingsFileAssets = AssetDatabase.FindAssets( "t:" + typeof(T).Name ); 94 | var foundObjects = new List(); 95 | for( int i = 0; i < settingsFileAssets.Length; i++ ) 96 | { 97 | foundObjects.Add( AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath( settingsFileAssets[i] ) ) ); 98 | } 99 | 100 | if( debugSettingsDiscovery ) Debug.Log( "Found objects at: " + string.Join( ",", foundObjects.Select( settings => AssetDatabase.GetAssetPath( settings ) ) ) ); 101 | 102 | return foundObjects; 103 | } 104 | } 105 | 106 | /// 107 | /// Identical to except that it's non-generic (for when you're working with 108 | /// runtime-resolved Type objects, instead of compile-time resolved classnames) 109 | /// 110 | /// 111 | /// 112 | /// 113 | public static List FindAssetsByType( Type type, bool debugSettingsDiscovery = false ) 114 | { 115 | /** Unity's API is wrongly documented - it does NOT support 'types' it only supports 'classnames'; we have 116 | * to do the conversion manually when those things differ 117 | */ 118 | if( type.IsGenericType ) 119 | { 120 | var settingsFileAssets = AssetDatabase.FindAssets( "t:ScriptableObject" ); 121 | var foundObjects = new List(); 122 | for( int i = 0; i < settingsFileAssets.Length; i++ ) 123 | { 124 | var so = AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath( settingsFileAssets[i] ) ); 125 | if( type.IsAssignableFrom( so.GetType() ) ) 126 | foundObjects.Add( so ); 127 | } 128 | 129 | if( debugSettingsDiscovery ) Debug.Log( "Found " + settingsFileAssets.Length + " assets of type (" + type + "): " + string.Join( ",", foundObjects.Select( settings => AssetDatabase.GetAssetPath( settings ) ) ) ); 130 | 131 | return foundObjects; 132 | } 133 | else 134 | { 135 | var settingsFileAssets = AssetDatabase.FindAssets( "t:" + type.Name ); 136 | var foundObjects = new List(); 137 | for( int i = 0; i < settingsFileAssets.Length; i++ ) 138 | { 139 | foundObjects.Add( AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath( settingsFileAssets[i] ), type ) ); 140 | } 141 | 142 | if( debugSettingsDiscovery ) Debug.Log( "Found objects at: " + string.Join( ",", foundObjects.Select( settings => AssetDatabase.GetAssetPath( settings ) ) ) ); 143 | 144 | return foundObjects; 145 | } 146 | } 147 | 148 | 149 | /// 150 | /// Convenience version of so you don't have to remember the name of this class. 151 | /// 152 | /// Usage: 153 | /// 154 | /// foreach( var subclass in typeof(MyClass).FindAssetsByType() ) 155 | /// ... 156 | /// 157 | /// 158 | /// 159 | /// 160 | /// 161 | public static List FindAssetsByType( this Type t, bool debugSettingsDiscovery = false ) where T : UnityEngine.Object 162 | { 163 | return FindAssetsByType( debugSettingsDiscovery ); 164 | } 165 | 166 | /// 167 | /// Shortcut to checking and seeing if it has any results, without running the code 168 | /// for interpreting and loading them all 169 | /// 170 | /// 171 | /// 172 | public static bool ExistsAnyAssetsOfType() where T : UnityEngine.Object 173 | { 174 | /** Unity's API is wrongly documented - it does NOT support 'types' it only supports 'classnames'; we have 175 | * to do the conversion manually when those things differ 176 | */ 177 | if( typeof(T).IsGenericType ) 178 | { 179 | var settingsFileAssets = AssetDatabase.FindAssets( "t:ScriptableObject" ); 180 | var foundObjects = new List(); 181 | for( int i = 0; i < settingsFileAssets.Length; i++ ) 182 | { 183 | var so = AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath( settingsFileAssets[i] ) ); 184 | if( so is T ) 185 | return true; 186 | } 187 | 188 | return false; 189 | } 190 | else 191 | { 192 | var settingsFileAssets = AssetDatabase.FindAssets( "t:" + typeof(T).Name ); 193 | return settingsFileAssets.Length > 0; 194 | } 195 | } 196 | 197 | /// 198 | /// Convenience version of so you don't have to remember the name of this class. 199 | /// 200 | /// Usage: 201 | /// 202 | /// if( typeof(MyClass).ExistsAnyAssetsOftype() ) 203 | /// ... 204 | /// 205 | /// 206 | /// 207 | public static bool ExistsAnyAssetsOfType( this Type t ) 208 | { 209 | /** Unity's API is wrongly documented - it does NOT support 'types' it only supports 'classnames'; we have 210 | * to do the conversion manually when those things differ 211 | */ 212 | if( t.IsGenericType ) 213 | { 214 | var settingsFileAssets = AssetDatabase.FindAssets( "t:ScriptableObject" ); 215 | var foundObjects = new List(); 216 | for( int i = 0; i < settingsFileAssets.Length; i++ ) 217 | { 218 | var so = AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath( settingsFileAssets[i] ) ); 219 | if( t.IsAssignableFrom( so.GetType() ) ) 220 | return true; 221 | } 222 | 223 | return false; 224 | } 225 | else 226 | { 227 | var settingsFileAssets = AssetDatabase.FindAssets( "t:" + t.Name ); 228 | return settingsFileAssets.Length > 0; 229 | } 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /EditorForks/WorkaroundUnityPrefabEditingSafe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace UnityEditorForks 6 | { 7 | /// 8 | /// All versions of Unity prior to 2020.1 require this class since Unity made some mistakes when adding 9 | /// NestedPrefabs APIs that lead to scene/project corruption if you edit prefabs from scripts. 10 | /// 11 | /// Based on ideas in this thread: 12 | /// 13 | /// https://forum.unity.com/threads/how-do-i-edit-prefabs-from-scripts.685711/ 14 | /// 15 | /// Note: our implementation here is bit more useful than Unity's (it tracks Exceptions too!), so it's still useful 16 | /// even after Unity 2020.1.1 17 | /// 18 | /// Usage: 19 | /// 20 | /// public void MyMethod() 21 | /// { 22 | /// using( var safeEditing = new WorkaroundUnityPrefabEditingSafe( assetPath ) ) 23 | /// { 24 | /// .. // Do your prefab editing here 25 | /// } 26 | /// } 27 | /// 28 | public class WorkaroundUnityPrefabEditingSafe 29 | { 30 | public readonly string assetPath; 31 | public readonly GameObject prefabRoot; 32 | public readonly Exception thrownException; 33 | 34 | public bool isValid { get { return thrownException == null; } } 35 | 36 | public WorkaroundUnityPrefabEditingSafe(string assetPath) 37 | { 38 | this.assetPath = assetPath; 39 | try 40 | { 41 | prefabRoot = PrefabUtility.LoadPrefabContents(assetPath); 42 | } 43 | catch( Exception e ) 44 | { 45 | thrownException = e; 46 | } 47 | } 48 | 49 | public void Dispose() 50 | { 51 | if( prefabRoot != null ) 52 | { 53 | PrefabUtility.SaveAsPrefabAsset(prefabRoot, assetPath); 54 | PrefabUtility.UnloadPrefabContents(prefabRoot); 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /EditorForks/WorkaroundUnityUIToolkitBrokenObjectSelector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using Object = UnityEngine.Object; 8 | 9 | namespace UnityEditorForks 10 | { 11 | /// 12 | /// 2023: added support for Unity-2022.* 13 | /// 14 | /// The UIToolkit team broke the core API "ShowObjectPicker", implemented their own version, and then made it 'internal' 15 | /// to prevent anyone else from using the code. 16 | /// 17 | /// ***NOTE: Unity has TWO DIFFERENT INCOMPATIBLE CLASSES that both have the name "UnityEditor.ObjectSelector", and you 18 | /// can find yourself accessing different classes at Editor time. *** 19 | /// 20 | /// Usage: 21 | /// e.g. to pick a Material: 22 | /// ((Material)null).ShowObjectPicker( o => { Debug.Log( "selector closed"+o.name ); }, o => { Debug.Log( "selector updated"+o.name ); } ); 23 | /// 24 | /// (or you can directly access the static method using the full class name here) 25 | /// 26 | public static class WorkaroundUnityUIToolkitBrokenObjectSelector 27 | { 28 | public enum ObjectPickerSources 29 | { 30 | ASSETS, 31 | ASSETS_AND_SCENE, 32 | MONOBEHAVIOURS, 33 | } 34 | 35 | public static void ShowObjectPicker( this T initialValue, Action OnSelectorClosed, Action OnSelectionChanged, ObjectPickerSources sources = ObjectPickerSources.ASSETS ) where T : UnityEngine.Object 36 | { 37 | ShowObjectPicker( OnSelectorClosed, OnSelectionChanged, initialValue, sources ); 38 | } 39 | 40 | private static MethodInfo _InternalFetchMethod__ObjectSelector_Show( Type typeToShowInSelector ) 41 | { 42 | MethodInfo miShow = null; 43 | 44 | var hiddenType = typeof(UnityEditor.Editor).Assembly.GetType( "UnityEditor.ObjectSelector" ); 45 | var ps = hiddenType.GetProperties( BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public | BindingFlags.GetProperty ); 46 | //Debug.Log( "all props on "+(typeof(ObjectSelector))+": "+string.Join( ",\n", ps.Select( info => info.ToString() ) ) ); 47 | 48 | var unityVersion = UnityEngine.Application.unityVersion.Split( '.' ); 49 | /** 50 | * If definitely Unity-2019 or earlier, use the old version; for all others use the new version (so that this 51 | * is forwards-compatible with when Unity Marketing inevitably breaks all Unity version comparisons AGAIN while 52 | * the Unity Engineering team AGAIN fails to provide a working 'UnityVersion' API (come on, guys! It's not hard! 53 | * and only YOU can maintain it correctly, since you're the ones putting incomparable values into it!) 54 | */ 55 | if( int.TryParse( unityVersion[0], out int unityMajorVersion ) && unityMajorVersion < 2020 ) 56 | { 57 | /** Type 1: Unity - up to 2019 */ 58 | miShow = hiddenType.GetMethod( "Show", BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] 59 | { 60 | typeToShowInSelector, 61 | typeof(System.Type), 62 | typeof(SerializedProperty), 63 | typeof(bool), 64 | typeof(List), 65 | typeof(Action), 66 | typeof(Action) 67 | }, new ParameterModifier[0] ); 68 | } 69 | else if( unityMajorVersion < 2022 ) 70 | { 71 | /*** Type 2: Unity - 2020 until 2022 */ 72 | miShow = hiddenType.GetMethod( "Show", BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] 73 | { 74 | typeToShowInSelector, 75 | typeof(System.Type), 76 | typeof(UnityEngine.Object), 77 | typeof(bool), 78 | typeof(List), 79 | typeof(Action), 80 | typeof(Action) 81 | }, new ParameterModifier[0] ); 82 | } 83 | else 84 | { 85 | /*** Type 3: Unity - 2022 onwards */ 86 | miShow = hiddenType.GetMethod( "Show", BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] 87 | { 88 | typeToShowInSelector, 89 | typeof(System.Type), 90 | typeof(UnityEngine.Object), 91 | typeof(bool), 92 | typeof(List), 93 | typeof(Action), 94 | typeof(Action), 95 | typeof(bool) // new optional param added in Unity 2022 96 | }, new ParameterModifier[0] ); 97 | } 98 | 99 | /** 100 | * Something went wrong ... did Unity staff add a third class? Argh! 101 | */ 102 | if( miShow == null ) 103 | { 104 | string methodName = "Show"; 105 | Debug.LogError( "UNITY CHANGED THE API. NEW API: Found \"" + methodName + "\"methods: \n" + string.Join( ",\n", hiddenType.GetMethods( BindingFlags.NonPublic | BindingFlags.Instance ) 106 | .Where( info => info.Name == methodName ).Select( info => " " + info.Name + " {" + string.Join( ",\n ", info.GetParameters().Select( parameterInfo => parameterInfo.Name + ":" + parameterInfo.ParameterType ) ) + "\n}\n" ) ) ); 107 | } 108 | 109 | return miShow; 110 | } 111 | 112 | public static void ShowObjectPicker( Action OnSelectorClosed, Action OnSelectionChanged, T initialValueOrNull = null, ObjectPickerSources sources = ObjectPickerSources.ASSETS ) where T : UnityEngine.Object 113 | { 114 | MethodInfo miShow = _InternalFetchMethod__ObjectSelector_Show( typeof(T) ); 115 | 116 | Action onSelectorClosed; 117 | Action onSelectedUpdated; 118 | switch( sources ) 119 | { 120 | case ObjectPickerSources.ASSETS: 121 | case ObjectPickerSources.ASSETS_AND_SCENE: 122 | onSelectedUpdated = o => { OnSelectionChanged( o as T ); }; 123 | onSelectorClosed = o => OnSelectorClosed.Invoke( o as T ); 124 | break; 125 | case ObjectPickerSources.MONOBEHAVIOURS: 126 | onSelectedUpdated = o => OnSelectionChanged( (o as GameObject).GetComponent() ); 127 | onSelectorClosed = o => OnSelectorClosed.Invoke( (o as GameObject).GetComponent() ); 128 | break; 129 | default: 130 | throw new Exception( "Impossible value of sources parameter" ); 131 | } 132 | 133 | var hiddenType = typeof(UnityEditor.Editor).Assembly.GetType( "UnityEditor.ObjectSelector" ); 134 | PropertyInfo piGet = hiddenType.GetProperty( "get", BindingFlags.Public | BindingFlags.Static ); 135 | var os = piGet.GetValue( null ); 136 | miShow.Invoke( os, new object[] 137 | { 138 | initialValueOrNull, 139 | typeof(T), 140 | null, 141 | (sources == ObjectPickerSources.ASSETS_AND_SCENE) || (sources == ObjectPickerSources.MONOBEHAVIOURS), 142 | null, 143 | onSelectorClosed, 144 | onSelectedUpdated 145 | #if UNITY_2022_1_OR_NEWER 146 | , true 147 | #endif 148 | } 149 | ); 150 | } 151 | 152 | public static void ShowObjectPicker( Type type, Action OnSelectorClosed, Action OnSelectionChanged, Object initialValueOrNull = null, ObjectPickerSources sources = ObjectPickerSources.ASSETS ) 153 | { 154 | MethodInfo miShow = _InternalFetchMethod__ObjectSelector_Show( type ); 155 | 156 | Action onSelectorClosed; 157 | Action onSelectedUpdated; 158 | switch( sources ) 159 | { 160 | case ObjectPickerSources.ASSETS: 161 | case ObjectPickerSources.ASSETS_AND_SCENE: 162 | onSelectedUpdated = o => { OnSelectionChanged( o ); }; 163 | onSelectorClosed = o => OnSelectorClosed.Invoke( o ); 164 | break; 165 | case ObjectPickerSources.MONOBEHAVIOURS: 166 | onSelectedUpdated = o => OnSelectionChanged( (o as GameObject).GetComponent( type ) ); 167 | onSelectorClosed = o => OnSelectorClosed.Invoke( (o as GameObject).GetComponent( type ) ); 168 | break; 169 | default: 170 | throw new Exception( "Impossible value of sources parameter" ); 171 | } 172 | 173 | var hiddenType = typeof(UnityEditor.Editor).Assembly.GetType( "UnityEditor.ObjectSelector" ); 174 | PropertyInfo piGet = hiddenType.GetProperty( "get", BindingFlags.Public | BindingFlags.Static ); 175 | var os = piGet.GetValue( null ); 176 | miShow.Invoke( os, new object[] 177 | { 178 | initialValueOrNull, 179 | type, 180 | null, 181 | (sources == ObjectPickerSources.ASSETS_AND_SCENE) || (sources == ObjectPickerSources.MONOBEHAVIOURS), 182 | null, 183 | onSelectorClosed, 184 | onSelectedUpdated 185 | #if UNITY_2022_1_OR_NEWER 186 | , true 187 | #endif 188 | } 189 | ); 190 | } 191 | 192 | } 193 | } -------------------------------------------------------------------------------- /EngineForks/FixUnityUndoBlock.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is based on an original from Unity Technologies ApS - see below for the license - modified 3 | * slightly for our needs. 4 | * 5 | * If/when Unity releases an official version of the original class, this class will be deleted and we 6 | * will adopt the official version instead. 7 | * 8 | * Version: 2023.9.24 9 | * 10 | * Changes from Version 2019: 11 | * 12 | * Changes from Unity's original: 13 | * 1. Unity's original used compile-time flags, but when building DLLs or embedding DLLs these wont get rechecked later - this may not be a problem (this class is already an Editor-only class by definition?), but just in case: added the Application.isEditor logic 14 | * 2. Unity's original used the legacy "transform.parent = " which Unity staff have told me to avoid in the past (has bugs - especiall affects UI/uGUI - that Unity will probably never fix due to the compatibility issues it would cause) 15 | * 2b. ... which also means it had no support for the bool flag on Transform.SetParent(), which we use almost all the time 16 | * 3. Unity's original was missing an entry for "destroyObject" 17 | * 4. There appeared to be a single missing call to m_dirty (when reparenting) - all other methods were setting this 18 | * 5: added a 'DestroyGameObject' which Unity requires but the original class was missing 19 | * 6: added an auto-detect for Unity's embarrassingly badly-written Undo API 20 | */ 21 | /* 22 | // Labs Utilities copyright © 2020 Unity Technologies ApS 23 | 24 | // Licensed under the Unity Companion License for Unity-dependent projects--see Unity Companion License. 25 | 26 | // Unless expressly provided otherwise, the Software under this license is made available strictly on an 27 | // “AS IS” BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the license for details 28 | // on these and other terms and conditions. 29 | */ 30 | using System; 31 | using UnityEngine; 32 | using UnityEngine.SceneManagement; 33 | using UnityObject = UnityEngine.Object; 34 | 35 | #if UNITY_EDITOR 36 | using UnityEditor; 37 | using UnityEditor.SceneManagement; 38 | #endif 39 | 40 | namespace PublishersFork 41 | { 42 | /// 43 | /// Class that automatically groups a series of object actions together as a single undo-operation 44 | /// And works in both the editor and player (with player support simply turning off undo-operations) 45 | /// Mirrors the normal functions you find in the Undo class and collapses them into one operation 46 | /// when the block is complete 47 | /// Proper usage of this class is: 48 | /// using (var undoBlock = new FixUnityUndoBlock("Desired Undo Message")) 49 | /// { 50 | /// undoBlock.yourCodeToUndo() 51 | /// } 52 | /// 53 | /// 54 | /// Based on UndoBlock2023_06_25 55 | /// 56 | public class FixUnityUndoBlock : IDisposable 57 | { 58 | string m_UndoLabel; 59 | int m_UndoGroup; 60 | bool m_DisposedValue; // To detect redundant calls of Dispose 61 | bool m_TestMode; 62 | 63 | bool m_Dirty; 64 | 65 | /// 66 | /// Initialize a new UndoBlock 67 | /// 68 | /// BUG in Unity's original: you have to MANUALLY specify that TestRunner is running - because Undo.SetParent is broken, gives incorrect results when TestRunner is running EditMode tests 69 | /// 70 | /// The label to apply to the undo group created within this undo block 71 | /// Whether this is part of a test run - NOTE this is auto-detected and FORCED to 'true' where possible 72 | public FixUnityUndoBlock(string undoLabel, bool testMode = false) 73 | { 74 | #if UNITY_EDITOR 75 | if( Application.isEditor ) 76 | { 77 | m_Dirty = false; 78 | m_TestMode = testMode; 79 | 80 | /** Workaround for Unity's bad programming: detect that Unity's TestRunner is running, and force m_TestMode to true */ 81 | if( Application.isEditor && 82 | !Application.isPlaying && !m_TestMode 83 | && Environment.StackTrace.Contains( "UnityEngine.TestRunner" ) ) 84 | m_TestMode = true; 85 | 86 | if( !Application.isPlaying && !m_TestMode ) 87 | { 88 | Undo.IncrementCurrentGroup(); 89 | m_UndoGroup = Undo.GetCurrentGroup(); 90 | Undo.SetCurrentGroupName(undoLabel); 91 | m_UndoLabel = undoLabel; 92 | } 93 | else 94 | m_UndoGroup = -1; 95 | } 96 | else 97 | m_UndoGroup = -1; 98 | #else 99 | m_UndoGroup = -1; 100 | #endif 101 | } 102 | 103 | /// 104 | /// Register undo operations for a newly created object. 105 | /// 106 | /// The object that was created. 107 | public void RegisterCreatedObject(UnityObject objectToUndo) 108 | { 109 | #if UNITY_EDITOR 110 | if( Application.isEditor ) 111 | { 112 | if( !Application.isPlaying && !m_TestMode ) 113 | { 114 | Undo.RegisterCreatedObjectUndo(objectToUndo, m_UndoLabel); 115 | m_Dirty = true; 116 | } 117 | } 118 | #endif 119 | } 120 | 121 | public GameObject CreateGameObjectAsChildOf( string name, GameObject parent ) 122 | { 123 | return CreateGameObjectAsChildOf( name, parent!=null ? parent.transform : null ); 124 | } 125 | public GameObject CreateGameObjectAsChildOf( string name, Transform parent ) 126 | { 127 | return CreateGameObject( name, parent is RectTransform ? new []{typeof(RectTransform)} : null, parent ); 128 | } 129 | 130 | public GameObject CreateGameObject( string name, Type requiredComponent, Transform parent = null ) 131 | { 132 | return CreateGameObject( name, new[] { requiredComponent }, parent ); 133 | } 134 | 135 | /// 136 | /// This is the only correct way of creating GameObject instances in Unity - all others from the Unity libraries 137 | /// are known (by Unity) to be incorrect and/or broken, but Unity refuses to update their docs (they will not fix 138 | /// them because it would break existing projects). 139 | /// 140 | /// Changed from Unity's in-house version, to make it use the correct argument order that mirrors the core Unity API 141 | /// 142 | /// 2nd (optional) arg to "new GameObject()", almost only ever used with RectTransform 143 | /// Optional parent transform for the new GO 144 | /// Optional override of the default name for the new GO 145 | /// 146 | public GameObject CreateGameObject( string name, Type[] requiredComponents = null, Transform parent = null ) 147 | { 148 | if( name == null ) name = "New GameObject"; 149 | 150 | GameObject @return; 151 | if( requiredComponents != null ) 152 | @return = new GameObject( name, requiredComponents ); 153 | else 154 | @return = new GameObject(name); 155 | 156 | RegisterCreatedObject(@return); 157 | 158 | SetTransformParent(@return.transform, parent, false); // NB: false is required, otherwise Unity will mess up your SCALE 159 | 160 | return @return; 161 | } 162 | 163 | /// 164 | /// Records any changes done on the object after the RecordObject function. 165 | /// 166 | /// The reference to the object that you will be modifying. 167 | public void RecordObject(UnityObject objectToUndo) 168 | { 169 | #if UNITY_EDITOR 170 | if( Application.isEditor ) 171 | { 172 | if( !Application.isPlaying && !m_TestMode ) 173 | Undo.RecordObject(objectToUndo, m_UndoLabel); 174 | } 175 | #endif 176 | } 177 | 178 | /// 179 | /// Sets the parent of transform to the new parent and records an undo operation. 180 | /// 181 | /// The Transform component whose parent is to be changed. 182 | /// The parent Transform to be assigned. 183 | /// As per the Unity Transform.SetParent() method 184 | public void SetTransformParent(Transform transform, Transform newParent, bool worldPositionStays = true ) 185 | { 186 | #if UNITY_EDITOR 187 | if( Application.isEditor && 188 | !Application.isPlaying && !m_TestMode ) 189 | { 190 | /** 191 | * Undo.SetTransformParent is using the legacy version of transform.SetParent, which ideally we'd avoid, but we have no alternative API call 192 | */ 193 | if( worldPositionStays ) 194 | Undo.SetTransformParent(transform, newParent, m_UndoLabel); 195 | else 196 | { 197 | /** 198 | * ... but at least we can implement logic for the missing worldPositionStays parameter 199 | */ 200 | Vector3 scale = transform.localScale; 201 | Vector3 position = transform.position; 202 | Quaternion rotation = transform.rotation; 203 | 204 | /** 205 | * RectTransform has a lot of extra data we need to save and restore after reparenting 206 | */ 207 | RectTransform rt = (transform as RectTransform); 208 | Vector2 anchorMax, anchorMin, anchoredPosition, pivot, offsetMin, offsetMax, sizeDelta; 209 | anchorMax = anchorMin = anchoredPosition = pivot = offsetMin = offsetMax = sizeDelta = Vector2.zero; // Required by the C# compiler (doesn't do static analysis) 210 | if( transform is RectTransform ) 211 | { 212 | anchorMax = rt.anchorMax; 213 | anchorMin = rt.anchorMin; 214 | anchoredPosition = rt.anchoredPosition; 215 | pivot = rt.pivot; 216 | offsetMin = rt.offsetMin; 217 | offsetMax = rt.offsetMax; 218 | sizeDelta = rt.sizeDelta; 219 | } 220 | 221 | Undo.RecordObject(transform, "Reparenting "+transform.name); 222 | Undo.SetTransformParent(transform, newParent, m_UndoLabel); 223 | 224 | /** 225 | * Restore Transform: 226 | */ 227 | transform.localScale = scale; 228 | transform.position = position; 229 | transform.rotation = rotation; 230 | 231 | /** 232 | * Restore RectTransform: 233 | */ 234 | if( transform is RectTransform ) 235 | { 236 | rt.anchorMax = anchorMax; 237 | rt.anchorMin = anchorMin; 238 | rt.anchoredPosition = anchoredPosition; 239 | rt.pivot = pivot; 240 | rt.offsetMin = offsetMin; 241 | rt.offsetMax = offsetMax; 242 | rt.sizeDelta = sizeDelta; 243 | } 244 | } 245 | 246 | m_Dirty = true; 247 | } 248 | else 249 | { 250 | transform.SetParent( newParent, worldPositionStays ); 251 | } 252 | #else 253 | transform.SetParent(newParent, worldPositionStays); 254 | #endif 255 | } 256 | 257 | 258 | /// 259 | /// Adds a component to the game object and registers an undo operation for this action. 260 | /// 261 | /// The game object you want to add the component to. 262 | /// The type of component you want to add. 263 | /// The new component 264 | public T AddComponent(GameObject gameObject) where T : Component 265 | { 266 | #if UNITY_EDITOR 267 | if( Application.isEditor ) 268 | { 269 | if( !Application.isPlaying && !m_TestMode ) 270 | { 271 | m_Dirty = true; 272 | return Undo.AddComponent(gameObject); 273 | } 274 | } 275 | #endif 276 | 277 | return gameObject.AddComponent(); 278 | } 279 | 280 | public void DestroyComponent(T component) where T : Component 281 | { 282 | #if UNITY_EDITOR 283 | if( Application.isEditor ) 284 | { 285 | if( !Application.isPlaying && !m_TestMode ) 286 | { 287 | m_Dirty = true; 288 | Undo.DestroyObjectImmediate( component ); 289 | } 290 | else 291 | UnityObject.Destroy( component ); 292 | } 293 | else 294 | UnityObject.Destroy( component ); 295 | #else 296 | UnityObject.Destroy( component ); 297 | #endif 298 | } 299 | 300 | public void DestroyGameObject( GameObject go ) 301 | { 302 | #if UNITY_EDITOR 303 | if( Application.isEditor ) 304 | { 305 | if( !Application.isPlaying && !m_TestMode ) 306 | { 307 | m_Dirty = true; 308 | Undo.DestroyObjectImmediate( go ); 309 | } 310 | else 311 | UnityObject.Destroy( go ); 312 | } 313 | else 314 | UnityObject.Destroy( go ); 315 | #else 316 | UnityObject.Destroy( go ); 317 | #endif 318 | } 319 | 320 | /// 321 | /// Dispose of this object 322 | /// 323 | /// Whether to dispose this object 324 | protected virtual void Dispose(bool disposing) 325 | { 326 | if (!m_DisposedValue) 327 | { 328 | if (disposing && m_UndoGroup > -1) 329 | { 330 | #if UNITY_EDITOR 331 | if( Application.isEditor ) 332 | { 333 | if( !Application.isPlaying && !m_TestMode ) 334 | { 335 | Undo.CollapseUndoOperations(m_UndoGroup); 336 | if( m_Dirty ) 337 | { 338 | EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene()); 339 | } 340 | } 341 | 342 | m_Dirty = false; 343 | } 344 | #endif 345 | } 346 | 347 | m_DisposedValue = true; 348 | } 349 | } 350 | 351 | /// 352 | /// This code added to correctly implement the disposable pattern. 353 | /// 354 | public void Dispose() 355 | { 356 | // Do not change this code. Put cleanup code in Dispose(bool disposing) above. 357 | Dispose(true); 358 | } 359 | } 360 | } -------------------------------------------------------------------------------- /EngineForks/MissingClasses/2019-or-earlier/HelpBox.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UIElements; 3 | 4 | namespace PublishersFork 5 | { 6 | /// 7 | /// UIToolkit was launched in version 1.0.0 without the core "HelpBox" API call (which has been part of Unity's 8 | /// GUI library for about 15 years). They eventually added it post-launch, but that means most current versions 9 | /// of Unity are missing this class. 10 | /// 11 | /// This is a very simple temporary replacement that is API-compatible and can be deleted once you stop supporting 12 | /// Unity versions older than 2020.1.1 13 | /// 14 | /// The methods here automatically provide a HelpBox API that's compatible with Unity's eventually-added one, 15 | /// but only in Unity versions where the official one is missing. 16 | /// 17 | #if UNITY_2020_1_OR_NEWER 18 | #else 19 | public enum HelpBoxMessageType 20 | { 21 | Error, 22 | Info, 23 | None, 24 | Warning 25 | } 26 | public class HelpBox : VisualElement 27 | { 28 | public HelpBox( string text, HelpBoxMessageType msgType ) 29 | { 30 | style.flexDirection = UnityEngine.UIElements.FlexDirection.Row; 31 | style.alignItems = Align.Center; 32 | 33 | var icon = new Label() 34 | { 35 | text = "!", style = 36 | { 37 | width = 40, height = 40, color = Color.black, 38 | unityTextAlign = TextAnchor.MiddleCenter, 39 | fontSize = 32, 40 | marginTop = 10, 41 | marginBottom = 10, 42 | } 43 | }; 44 | switch (msgType) 45 | { 46 | case HelpBoxMessageType.Warning: 47 | icon.style.backgroundColor = new Color(1f, 0.93f, 0.44f); 48 | icon.text = "!"; 49 | break; 50 | 51 | case HelpBoxMessageType.Error: 52 | icon.style.backgroundColor = new Color(1f, 0.32f, 0.32f); 53 | icon.text = "!!!"; 54 | break; 55 | 56 | case HelpBoxMessageType.Info: 57 | icon.style.backgroundColor = new Color(0.81f, 0.76f, 0.77f); 58 | icon.text = "?"; 59 | break; 60 | } 61 | 62 | Add(icon); 63 | 64 | Add(new Label() 65 | { 66 | text = text, 67 | style = 68 | { 69 | whiteSpace = WhiteSpace.Normal, 70 | 71 | paddingBottom = 10, 72 | paddingLeft = 10, 73 | paddingRight = 10, 74 | paddingTop = 10, 75 | } 76 | }); 77 | } 78 | 79 | } 80 | #endif 81 | } -------------------------------------------------------------------------------- /EngineForks/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("UnityEngineForks")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("UnityEngineForks")] 12 | [assembly: AssemblyCopyright("Copyright © 2022")] 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("D50940CA-CAB4-4C80-8265-6A23E28E1F84")] 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("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /EngineForks/UnityEngineForks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {D50940CA-CAB4-4C80-8265-6A23E28E1F84} 8 | Library 9 | Properties 10 | ClassLibrary1 11 | UnityEngineForks 12 | v4.8 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ..\UnityEditorFolderSymlink\Data\Managed\UnityEngine.dll 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 71 | -------------------------------------------------------------------------------- /EngineForks/WorkaroundStarterAssetsDeletedInputFirstPersonController.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace PublishersFork 4 | { 5 | /// 6 | /// In 2022, despite the fact that "New Input" still isn't fully implemented, and is FULL of serious bugs, 7 | /// Unity staff deleted all Input code from their official assets provided to Publishers for use in demos. 8 | /// 9 | /// This class is a minimally edited version of the (now broken) FirstPersonController.cs class - as of 2022, 10 | /// Unity's staff inserted a runtime deliberate "stop the app" line of code rather than fix/maintain their 11 | /// existing Input code. 12 | /// 13 | /// NOTE: this class also removes dependency on CineMachine, which is unfortunate, but was the fastest/easiest way 14 | /// to re-instate the production-quality existing Input system. If you need CineMachine you'll need to tweak it 15 | /// yourself and decide how you're going to translate the traditional Inputs into CineMachine effects. 16 | /// 17 | /// Install instructions: 18 | /// 1. on the "PlayerCapsule" GameObject, replace the Monobehaviour "FirstPersonController" with 19 | /// this MonoBehaviour 20 | /// 2. on the "PlayerCapsule" GameObject, replace the Monobehaviour "StarterAssetsInputs" with 21 | /// 22 | /// 3. Add a "Camera" MonoBehaviour to your "PlayerCameraRoot" GameObject (child of the "PlayerCapsule") 23 | /// 4. Change the new Camera's tag to "MainCamera" 24 | /// 5. Delete the other CineMachine objects from your scene. 25 | /// 26 | /// NOTE: This REQUIRES you to also use the class 27 | /// since Unity's design splits the implementation into two classes. 28 | /// 29 | /// NOTE: This REQUIRES you to download/install the official (free) Package: https://assetstore.unity.com/packages/essentials/starter-assets-first-person-character-controller-196525 30 | /// 31 | /// NOTE: If you want to support both old and new input then it is easy to take the modifications in this 32 | /// pair of classes and add them to Unity's official classes - use "diff" to see exactly which lines of code 33 | /// needed modification. Unity staff could have (should have!) done this themselves but chose not to. 34 | /// 35 | public class WorkaroundStarterAssetsDeletedInputFirstPersonController : MonoBehaviour 36 | { 37 | [Header("Player")] 38 | [Tooltip("Move speed of the character in m/s")] 39 | public float MoveSpeed = 4.0f; 40 | [Tooltip("Sprint speed of the character in m/s")] 41 | public float SprintSpeed = 6.0f; 42 | [Tooltip("Rotation speed of the character")] 43 | public float RotationSpeed = 1.0f; 44 | [Tooltip("Acceleration and deceleration")] 45 | public float SpeedChangeRate = 10.0f; 46 | 47 | [Space(10)] 48 | [Tooltip("The height the player can jump")] 49 | public float JumpHeight = 1.2f; 50 | [Tooltip("The character uses its own gravity value. The engine default is -9.81f")] 51 | public float Gravity = -15.0f; 52 | 53 | [Space(10)] 54 | [Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")] 55 | public float JumpTimeout = 0.1f; 56 | [Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")] 57 | public float FallTimeout = 0.15f; 58 | 59 | [Header("Player Grounded")] 60 | [Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")] 61 | public bool Grounded = true; 62 | [Tooltip("Useful for rough ground")] 63 | public float GroundedOffset = -0.14f; 64 | [Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")] 65 | public float GroundedRadius = 0.5f; 66 | [Tooltip("What layers the character uses as ground")] 67 | public LayerMask GroundLayers; 68 | 69 | [Tooltip("How far in degrees can you move the camera up")] 70 | public float TopClamp = 90.0f; 71 | [Tooltip("How far in degrees can you move the camera down")] 72 | public float BottomClamp = -90.0f; 73 | 74 | // cinemachine 75 | private float _cinemachineTargetPitch; 76 | 77 | // player 78 | private float _speed; 79 | private float _rotationVelocity; 80 | private float _verticalVelocity; 81 | private float _terminalVelocity = 53.0f; 82 | 83 | // timeout deltatime 84 | private float _jumpTimeoutDelta; 85 | private float _fallTimeoutDelta; 86 | 87 | 88 | private CharacterController _controller; 89 | private WorkaroundStarterAssetsDeletedInputStarterAssetsInputs _input; 90 | private GameObject _mainCamera; 91 | 92 | private const float _threshold = 0.01f; 93 | 94 | private void Awake() 95 | { 96 | // get a reference to our main camera 97 | if (_mainCamera == null) 98 | { 99 | _mainCamera = GameObject.FindGameObjectWithTag("MainCamera"); 100 | } 101 | } 102 | 103 | private void Start() 104 | { 105 | _controller = GetComponent(); 106 | _input = GetComponent(); 107 | 108 | // reset our timeouts on start 109 | _jumpTimeoutDelta = JumpTimeout; 110 | _fallTimeoutDelta = FallTimeout; 111 | } 112 | 113 | private void Update() 114 | { 115 | _input.SampleInput(); 116 | JumpAndGravity(); 117 | GroundedCheck(); 118 | Move(); 119 | } 120 | 121 | private void LateUpdate() 122 | { 123 | CameraRotation(); 124 | } 125 | 126 | private void GroundedCheck() 127 | { 128 | // set sphere position, with offset 129 | Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z); 130 | Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore); 131 | } 132 | 133 | private void CameraRotation() 134 | { 135 | // if there is an input 136 | if (_input.look.sqrMagnitude >= _threshold) 137 | { 138 | //Don't multiply ANY input by Time.deltaTime, Unity. Also note the Unity employees who don't know what the word 'velocity' literally means! 139 | _rotationVelocity = _input.look.x * RotationSpeed; 140 | 141 | // clamp our pitch rotation 142 | _cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp); 143 | 144 | // Update Cinemachine camera target pitch 145 | _mainCamera.transform.Rotate( Vector3.right * _input.look.y * -1f * RotationSpeed ); 146 | 147 | // rotate the player left and right 148 | transform.Rotate(Vector3.up * _rotationVelocity); 149 | } 150 | } 151 | 152 | private void Move() 153 | { 154 | // set target speed based on move speed, sprint speed and if sprint is pressed 155 | float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed; 156 | 157 | // a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon 158 | 159 | // note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude 160 | // if there is no input, set the target speed to 0 161 | if (_input.move == Vector2.zero) targetSpeed = 0.0f; 162 | 163 | // a reference to the players current horizontal velocity 164 | float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude; 165 | 166 | float speedOffset = 0.1f; 167 | float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f; 168 | 169 | // accelerate or decelerate to target speed 170 | if (currentHorizontalSpeed < targetSpeed - speedOffset || currentHorizontalSpeed > targetSpeed + speedOffset) 171 | { 172 | // creates curved result rather than a linear one giving a more organic speed change 173 | // note T in Lerp is clamped, so we don't need to clamp our speed 174 | _speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude, Time.deltaTime * SpeedChangeRate); 175 | 176 | // round speed to 3 decimal places 177 | _speed = Mathf.Round(_speed * 1000f) / 1000f; 178 | } 179 | else 180 | { 181 | _speed = targetSpeed; 182 | } 183 | 184 | // normalise input direction 185 | Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized; 186 | 187 | // note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude 188 | // if there is a move input rotate player when the player is moving 189 | if (_input.move != Vector2.zero) 190 | { 191 | // move 192 | inputDirection = transform.right * _input.move.x + transform.forward * _input.move.y; 193 | } 194 | 195 | // move the player 196 | _controller.Move(inputDirection.normalized * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime); 197 | } 198 | 199 | private void JumpAndGravity() 200 | { 201 | if (Grounded) 202 | { 203 | // reset the fall timeout timer 204 | _fallTimeoutDelta = FallTimeout; 205 | 206 | // stop our velocity dropping infinitely when grounded 207 | if (_verticalVelocity < 0.0f) 208 | { 209 | _verticalVelocity = -2f; 210 | } 211 | 212 | // Jump 213 | if (_input.jump && _jumpTimeoutDelta <= 0.0f) 214 | { 215 | // the square root of H * -2 * G = how much velocity needed to reach desired height 216 | _verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity); 217 | } 218 | 219 | // jump timeout 220 | if (_jumpTimeoutDelta >= 0.0f) 221 | { 222 | _jumpTimeoutDelta -= Time.deltaTime; 223 | } 224 | } 225 | else 226 | { 227 | // reset the jump timeout timer 228 | _jumpTimeoutDelta = JumpTimeout; 229 | 230 | // fall timeout 231 | if (_fallTimeoutDelta >= 0.0f) 232 | { 233 | _fallTimeoutDelta -= Time.deltaTime; 234 | } 235 | 236 | // if we are not grounded, do not jump 237 | _input.jump = false; 238 | } 239 | 240 | // apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time) 241 | if (_verticalVelocity < _terminalVelocity) 242 | { 243 | _verticalVelocity += Gravity * Time.deltaTime; 244 | } 245 | } 246 | 247 | private static float ClampAngle(float lfAngle, float lfMin, float lfMax) 248 | { 249 | if (lfAngle < -360f) lfAngle += 360f; 250 | if (lfAngle > 360f) lfAngle -= 360f; 251 | return Mathf.Clamp(lfAngle, lfMin, lfMax); 252 | } 253 | 254 | private void OnDrawGizmosSelected() 255 | { 256 | Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f); 257 | Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f); 258 | 259 | if (Grounded) Gizmos.color = transparentGreen; 260 | else Gizmos.color = transparentRed; 261 | 262 | // when selected, draw a gizmo in the position of, and matching radius of, the grounded collider 263 | Gizmos.DrawSphere(new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z), GroundedRadius); 264 | } 265 | } 266 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundStarterAssetsDeletedInputStarterAssetsInputs.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace PublishersFork 4 | { 5 | /// 6 | /// NOTE: This REQUIRES you to also use the class 7 | /// since Unity's design splits the implementation into two classes. 8 | /// 9 | /// NOTE: If you want to support both old and new input then it is easy to take the modifications in this 10 | /// pair of classes and add them to Unity's official classes - use "diff" to see exactly which lines of code 11 | /// needed modification. Unity staff could have (should have!) done this themselves but chose not to. 12 | /// 13 | 14 | public class WorkaroundStarterAssetsDeletedInputStarterAssetsInputs : MonoBehaviour 15 | { 16 | [Header("Character Input Values")] 17 | public Vector2 move; 18 | public Vector2 look; 19 | public bool jump; 20 | public bool sprint; 21 | 22 | [Header("Movement Settings")] 23 | public bool analogMovement; 24 | 25 | [Header("Mouse Cursor Settings")] 26 | public bool cursorLocked = true; 27 | public bool cursorInputForLook = true; 28 | 29 | public void SampleInput() 30 | { 31 | move = Vector2.zero; 32 | if( Input.GetKey( KeyCode.LeftArrow ) || Input.GetKey( KeyCode.A ) ) 33 | move.x += -1f; 34 | if( Input.GetKey( KeyCode.RightArrow ) || Input.GetKey( KeyCode.D )) 35 | move.x += 1f; 36 | 37 | if( Input.GetKey( KeyCode.UpArrow ) || Input.GetKey( KeyCode.W ) ) 38 | move.y += 1f; 39 | if( Input.GetKey( KeyCode.DownArrow ) || Input.GetKey( KeyCode.S )) 40 | move.y += -1f; 41 | 42 | look.x = Input.GetAxis( "Mouse X" ); 43 | look.y = Input.GetAxis( "Mouse Y" ); 44 | 45 | sprint = Input.GetKey( KeyCode.LeftShift ); 46 | jump = Input.GetKeyDown( KeyCode.Space ); 47 | } 48 | 49 | #if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED 50 | public void OnMove(InputValue value) 51 | { 52 | MoveInput(value.Get()); 53 | } 54 | 55 | public void OnLook(InputValue value) 56 | { 57 | if(cursorInputForLook) 58 | { 59 | LookInput(value.Get()); 60 | } 61 | } 62 | 63 | public void OnJump(InputValue value) 64 | { 65 | JumpInput(value.isPressed); 66 | } 67 | 68 | public void OnSprint(InputValue value) 69 | { 70 | SprintInput(value.isPressed); 71 | } 72 | #endif 73 | 74 | 75 | public void MoveInput(Vector2 newMoveDirection) 76 | { 77 | move = newMoveDirection; 78 | } 79 | 80 | public void LookInput(Vector2 newLookDirection) 81 | { 82 | look = newLookDirection; 83 | } 84 | 85 | public void JumpInput(bool newJumpState) 86 | { 87 | jump = newJumpState; 88 | } 89 | 90 | public void SprintInput(bool newSprintState) 91 | { 92 | sprint = newSprintState; 93 | } 94 | 95 | private void OnApplicationFocus(bool hasFocus) 96 | { 97 | SetCursorState(cursorLocked); 98 | } 99 | 100 | private void SetCursorState(bool newState) 101 | { 102 | Cursor.lockState = newState ? CursorLockMode.Locked : CursorLockMode.None; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityIMGUISetColor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace PublishersFork 5 | { 6 | /// 7 | /// With IMGUI you often need to re-color an item, but IMGUI has no direct way of doing that. The standard approach 8 | /// is to save the GUI.color value, change it, draw your component, then reset the GUI.color - but this is error 9 | /// prone and a lot of boilerplate code to achieve something that should be 0 lines of code. 10 | /// 11 | /// This class is setup to allow you to write: 12 | /// 13 | /// public void MyGUIMethod() 14 | /// { 15 | /// using( new WorkaroundUnityIMGUISetColor( Color.red ) ) 16 | /// { 17 | /// GUI.Label( "Red-colored label" ); 18 | /// } 19 | /// } 20 | /// 21 | /// 'using' allows us to auto-reset the GUI.color. 22 | /// 23 | /// TODO: convert it to a struct (to prevent GC.alloc) and test it still works correctly 24 | /// 25 | public class WorkaroundUnityIMGUISetColor : IDisposable 26 | { 27 | private bool m_Disposed; 28 | 29 | internal virtual void Dispose(bool disposing) 30 | { 31 | if (this.m_Disposed) 32 | return; 33 | if (disposing) 34 | this.CloseScope(); 35 | this.m_Disposed = true; 36 | } 37 | 38 | ~WorkaroundUnityIMGUISetColor() 39 | { 40 | if (!this.m_Disposed) 41 | Debug.LogError(this.GetType().Name + " was not disposed! You should use the 'using' keyword or manually call Dispose."); 42 | this.Dispose(false); 43 | } 44 | 45 | public void Dispose() 46 | { 47 | this.Dispose(true); 48 | GC.SuppressFinalize((object) this); 49 | } 50 | 51 | private Color _originalColor; 52 | 53 | public WorkaroundUnityIMGUISetColor(Color newColor) 54 | { 55 | _originalColor = GUI.color; 56 | GUI.color = newColor; 57 | } 58 | 59 | protected void CloseScope() 60 | { 61 | GUI.color = _originalColor; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityInternal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using UnityEngine; 6 | 7 | namespace PublishersFork 8 | { 9 | /// 10 | /// C# engineers almost never need to invoke foreign internal/private code. In the last 5 years Unity's engineers have 11 | /// developed an obsession with over-using the 'internal' and 'private' keywords. 12 | /// 13 | /// Unity3D was originally built WITHOUT a lot of features and API's specifically because everything was public, so that end-developers could DIY 14 | /// solutions and fill the gaps. Until/unless the new Unity teams vastly increase the volume and quality of public 15 | /// APIs they write (and fill in the old gaps) their abuse of internal/private is a problem. 16 | /// 17 | /// In the meantime: Unity3D engineers need to invoke private/internals every month or so on any normal mid-sized project. 18 | /// ...this class makes it vastly easier to work with internal/private. 19 | /// 20 | /// NB: a design feature of C# means that when a class extends another class, C#'s own Reflection methods stop working 21 | /// for private/internal members - there are clear logical reasons for this, but pragmatically it's poor, because 22 | /// anyone having to invoke them needs to invoke all of them, not just a shallow subset. For normal C# development 23 | /// this code is written so rarely you wouldn't notice; in Unity3D it's written so often that the 'pragmatic' route 24 | /// becomes more important than the 'logical' one. The methods below take account of this. 25 | /// 26 | public static class WorkaroundUnityInternal 27 | { 28 | /// 29 | /// Invoked on an object, it finds the private (or internal) field with the given name - even if that field is 30 | /// hidden in a superclass/baseclass. It starts with the main class, and iterates up the class hierarchy until 31 | /// it finds it, or fails. 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | public static FieldInfo FindPrivateField( this object onObject, string fieldName ) 38 | { 39 | return FindPrivateField( onObject, fieldName, onObject.GetType() ); 40 | } 41 | 42 | /// 43 | /// Use instead - this is the more precise internal version that 44 | /// is able to selectively target different super-classes while iterating up the hierarchy 45 | /// 46 | /// 47 | /// 48 | /// 49 | /// 50 | public static FieldInfo FindPrivateField( this object onObject, string fieldName, Type objectType ) 51 | { 52 | FieldInfo result = objectType.GetField( fieldName, BindingFlags.Instance | BindingFlags.NonPublic ); 53 | if( result != null ) 54 | return result; 55 | 56 | if( objectType.BaseType != null ) 57 | return onObject.FindPrivateField( fieldName, objectType.BaseType ); 58 | 59 | throw new Exception( "Couldn't find field \"" + fieldName + "\" anywhere on type or supertypes of: " + onObject.GetType() ); 60 | } 61 | 62 | /// 63 | /// Invoked on an object, it finds the private (or internal) property with the given name - even if that field is 64 | /// hidden in a superclass/baseclass. It starts with the main class, and iterates up the class hierarchy until 65 | /// it finds it, or fails. 66 | /// 67 | /// 68 | /// 69 | /// 70 | /// 71 | public static PropertyInfo FindPrivateProperty( this object onObject, string propertyName ) 72 | { 73 | return FindPrivateProperty( onObject, propertyName, onObject.GetType() ); 74 | } 75 | 76 | /// 77 | /// Use instead - this is the more precise internal version that 78 | /// is able to selectively target different super-classes while iterating up the hierarchy 79 | /// 80 | /// 81 | /// 82 | /// 83 | /// 84 | public static PropertyInfo FindPrivateProperty( this object onObject, string propertyName, Type objectType ) 85 | { 86 | PropertyInfo result = objectType.GetProperty( propertyName, BindingFlags.Instance | BindingFlags.NonPublic ); 87 | if( result != null ) 88 | return result; 89 | 90 | if( objectType.BaseType != null ) 91 | return onObject.FindPrivateProperty( propertyName, objectType.BaseType ); 92 | 93 | throw new Exception( "Couldn't find property \"" + propertyName + "\" anywhere on type or supertypes of: " + onObject.GetType() ); 94 | } 95 | 96 | /// 97 | /// Invoked on an object, it finds the private (or internal) method with the given name - even if that field is 98 | /// hidden in a superclass/baseclass. It starts with the main class, and iterates up the class hierarchy until 99 | /// it finds it, or fails. 100 | /// 101 | /// 102 | /// 103 | /// 104 | /// 105 | public static MethodInfo FindPrivateMethod( this object onObject, string methodName, Type[] argTypes = null ) 106 | { 107 | return FindPrivateMethod( onObject, methodName, argTypes, onObject.GetType() ); 108 | } 109 | 110 | /// 111 | /// Use instead - this is the more precise internal version that 112 | /// is able to selectively target different super-classes while iterating up the hierarchy 113 | /// 114 | /// 115 | /// 116 | /// 117 | /// 118 | public static MethodInfo FindPrivateMethod( this object onObject, string methodName, Type[] argTypes, Type objectType ) 119 | { 120 | MethodInfo result = argTypes == null 121 | ? objectType.GetMethod( methodName, BindingFlags.Instance | BindingFlags.NonPublic ) 122 | : objectType.GetMethod( methodName, BindingFlags.Instance | BindingFlags.NonPublic, null, argTypes, null ); 123 | if( result != null ) 124 | return result; 125 | 126 | if( objectType.BaseType != null ) 127 | return FindPrivateMethod( onObject, methodName, argTypes, objectType.BaseType ); 128 | 129 | throw new Exception( "Couldn't find method \"" + methodName + "\"(" + (argTypes == null ? "" : "[" + argTypes.Length + " params]") + ") anywhere on type or supertypes of: " + onObject.GetType() ); 130 | } 131 | 132 | /// 133 | /// You should use where possible - but frequently Unity's methods 134 | /// return classes that are themselves incorrectly marked as 'internal', preventing you from compiling them. This 135 | /// method lets you disable typesafety to workaround Unity's mistakes. 136 | /// 137 | /// Returns the value of a private field on the supplied object 138 | /// 139 | /// 140 | /// 141 | /// 142 | public static object PrivateFieldValue( this object onObject, string fieldName ) 143 | { 144 | FieldInfo result = onObject.FindPrivateField( fieldName ); 145 | if( result != null ) 146 | return result.GetValue( onObject ); 147 | 148 | throw new Exception( "Couldn't find field \"" + fieldName + "\" anywhere on type or supertypes of: " + onObject.GetType() ); 149 | } 150 | 151 | /// 152 | /// Returns the value of a private field on the supplied object 153 | /// 154 | /// 155 | /// 156 | /// 157 | public static T PrivateFieldValue( this object onObject, string fieldName ) 158 | { 159 | FieldInfo result = onObject.FindPrivateField( fieldName ); 160 | if( result != null ) 161 | return (T) result.GetValue( onObject ); 162 | 163 | throw new Exception( "Couldn't find field \"" + fieldName + "\" anywhere on type or supertypes of: " + onObject.GetType() ); 164 | } 165 | 166 | /// 167 | /// You should use where possible - but frequently Unity's methods 168 | /// return classes that are themselves incorrectly marked as 'internal', preventing you from compiling them. This 169 | /// method lets you disable typesafety to workaround Unity's mistakes. 170 | /// 171 | /// Returns the value of a private property on the supplied object 172 | /// 173 | /// 174 | /// 175 | /// 176 | public static object PrivatePropertyValue( this object onObject, string fieldName ) 177 | { 178 | PropertyInfo result = onObject.FindPrivateProperty( fieldName ); 179 | return result.GetValue( onObject ); 180 | } 181 | 182 | /// 183 | /// Returns the value of a private property on the supplied object 184 | /// 185 | /// 186 | /// 187 | /// 188 | public static T PrivatePropertyValue( this object onObject, string fieldName ) 189 | { 190 | PropertyInfo result = onObject.FindPrivateProperty( fieldName ); 191 | return (T) result.GetValue( onObject ); 192 | } 193 | 194 | /// 195 | /// Invokes a private method on the supplied object 196 | /// 197 | /// 198 | /// 199 | /// 200 | public static object PrivateMethod( this object onObject, string methodName ) 201 | { 202 | MethodInfo result = onObject.FindPrivateMethod( methodName ); 203 | return result.Invoke( onObject, null ); 204 | } 205 | 206 | /// 207 | /// Invokes a private method on the supplied object 208 | /// 209 | /// 210 | /// 211 | /// 212 | public static object PrivateMethod( this object onObject, string methodName, Type[] argTypes, object[] args ) 213 | { 214 | MethodInfo result = onObject.FindPrivateMethod( methodName, argTypes ); 215 | return result.Invoke( onObject, args ); 216 | } 217 | } 218 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityMissingEditorVersion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEngine; 4 | 5 | namespace PublishersFork 6 | { 7 | /// 8 | /// Unity failed to provide a method "what's the version of the currently-executing UnityEditor" that uses C#'s 9 | /// Version class, or an equivalent - Unity only gives a string, which is useless in most cases. You cannot use 10 | /// comparison methods, greater/less-than etc. 11 | /// 12 | public class WorkaroundUnityMissingEditorVersion 13 | { 14 | /// 15 | /// Returns a valid C# Version object, stripping the letter out of Unity's proprietary versioning system 16 | /// i.e. 2021.2.3f would return: (2021, 2, 3). 17 | /// 18 | /// This allows you to compare Versions directly, e.g. "if( unityVersion > new Version(2020,1,2) )" 19 | /// 20 | public static Version unityVersion 21 | { 22 | get 23 | { 24 | var v = Application.unityVersion; 25 | var elements = v.Split('.'); 26 | var unityBuildNumber = int.Parse(new string(elements[2].Where(c => char.IsDigit(c)).ToArray())); 27 | 28 | return new Version(int.Parse(elements[0]), int.Parse(elements[1]), unityBuildNumber); 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityMissingGetComponentsOnAncestors.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace PublishersFork 4 | { 5 | /// 6 | /// Unity provides GetComponent(s)InChildren, and GetComponent(s)InParent, but the latter 7 | /// only returns items on the direct parent, and doesn't give you fine control over which 8 | /// parents are accessed. 9 | /// 10 | /// The methods here are commonly used for finding the outermost / containing Component 11 | /// (e.g. in UnityUI where you need to find the outermost UI component and check that it's 12 | /// on a Canvas) 13 | /// 14 | public static class WorkaroundUnityMissingGetComponentsOnAncestors 15 | { 16 | /// 17 | /// Finds the first instance of a specific MonoBehaviour, by going up the tree of Parents 18 | /// 19 | /// 20 | /// 21 | /// 22 | public static T GetFirstAncestorComponent(this Transform t) where T : MonoBehaviour 23 | { 24 | T parentT = null; 25 | Transform parentTransform = t.parent; 26 | while (parentTransform != null && parentT == null) 27 | { 28 | parentT = parentTransform.GetComponent(); 29 | parentTransform = parentTransform.parent; 30 | } 31 | 32 | return parentT; 33 | } 34 | 35 | /// 36 | /// Finds the root-most / highest-in-tree instance of a specific MonoBehaviour 37 | /// 38 | /// 39 | /// 40 | /// 41 | public static T GetHighestAncestorComponent(this Transform t) where T : MonoBehaviour 42 | { 43 | T parentT = null; 44 | Transform parentTransform = t.parent; 45 | while (parentTransform != null) 46 | { 47 | if (parentTransform.TryGetComponent(out parentT)) 48 | #pragma warning disable 642 49 | ; 50 | #pragma warning restore 642 51 | // keep looking in case there's an outer one left to find... 52 | parentTransform = parentTransform.parent; 53 | } 54 | 55 | return parentT; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityMissingPlayerPrefsAPI.cs: -------------------------------------------------------------------------------- 1 | #define SUPPORT_WINDOWS // If you make builds of your game for Windows, you'll want this on 2 | #define SUPPORT_MAC_OSX // If you make builds of your game for Mac/OSX, you'll want this on 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using UnityEngine; 9 | using System.Text; 10 | using System.Xml; 11 | #if UNITY_EDITOR 12 | using UnityEditor; 13 | #endif 14 | 15 | namespace PublishersFork 16 | { 17 | /// 18 | /// Unity staff still failed to fix the missing "PlayerPrefs.GetAllKeys()" API call in all versions of Unity. This class 19 | /// adds the critically-important missing API call which you need in order to check/remove/replace settings in any 20 | /// game. 21 | /// 22 | /// Heavily based on the code by Sabresaurus Ltd.: - web: http://www.sabresaurus.com - contact: http://www.sabresaurus.com/contact 23 | /// NB: you can get a free GUI for this on the AssetStore, written by Sabresaurus, at: https://assetstore.unity.com/packages/tools/playersprefs-editor-and-utilities-26656 24 | /// - however that was last updated in 2017 (!) and the documentation pages etc are now 404, so it appears unmaintained 25 | /// 26 | public class WorkaroundUnityMissingPlayerPrefsAPI 27 | { 28 | public static List PlayerPrefs_GetAllKeys() 29 | { 30 | var @return = new List(); 31 | 32 | #if UNITY_EDITOR 33 | string companyName = PlayerSettings.companyName; 34 | string productName = PlayerSettings.productName; 35 | #else 36 | string companyName = Application.companyName; // Editor-only: PlayerSettings.companyName; 37 | string productName = Application.productName; // Editor-only: PlayerSettings.productName; 38 | #endif 39 | 40 | #if SUPPORT_WINDOWS 41 | if( Application.platform == RuntimePlatform.WindowsEditor ) 42 | { 43 | // From Unity docs: On Windows, PlayerPrefs are stored in the registry under HKCU\Software\[company name]\[product name] key, where company and product names are the names set up in Project Settings. 44 | // From Unity 5.5 editor player prefs moved to a specific location 45 | 46 | /** 47 | * If the following line fails to compile, it's because your version of Unity is using Unity's weird 48 | * historical defaults for the .NET settings (by default: this WILL compile in an Editor folder and 49 | * WILL NOT compile in a normal folder - thanks, Unity!) 50 | * 51 | * Set your .Net in project settings to ".NET Framework 4.x" or greater 52 | */ 53 | Microsoft.Win32.RegistryKey registryKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey( "Software\\Unity\\UnityEditor\\" + companyName + "\\" + productName ); 54 | 55 | // Parse the registry if the specified registryKey exists 56 | if( registryKey != null ) 57 | { 58 | // Get an array of what keys (registry value names) are stored 59 | string[] valueNames = registryKey.GetValueNames(); 60 | 61 | // Parse and convert the registry saved player prefs into our array 62 | int i = 0; 63 | foreach( string valueName in valueNames ) 64 | { 65 | string key = valueName; 66 | 67 | // Remove the _h193410979 style suffix used on player pref keys in Windows registry 68 | int index = key.LastIndexOf( "_" ); 69 | key = key.Remove( index, key.Length - index ); 70 | 71 | @return.Add( key ); 72 | } 73 | } 74 | } 75 | #endif 76 | #if SUPPORT_MAC_OSX 77 | #if SUPPORT_WINDOWS 78 | else 79 | #endif 80 | if( Application.platform == RuntimePlatform.OSXEditor ) 81 | { 82 | // From Unity docs: On Mac OS X PlayerPrefs are stored in ~/Library/Preferences folder, in a file named unity.[company name].[product name].plist, where company and product names are the names set up in Project Settings. The same .plist file is used for both Projects run in the Editor and standalone players. 83 | 84 | // Construct the plist filename from the project's settings 85 | string plistFilename = string.Format( "unity.{0}.{1}.plist", companyName, productName ); 86 | // Now construct the fully qualified path 87 | string playerPrefsPath = Path.Combine( Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.Personal ), "Library/Preferences" ), plistFilename ); 88 | 89 | // Parse the player prefs file if it exists 90 | if( File.Exists( playerPrefsPath ) ) 91 | { 92 | // Parse the plist then cast it to a Dictionary 93 | object plist = Plist.readPlist( playerPrefsPath ); 94 | 95 | Dictionary parsed = plist as Dictionary; 96 | 97 | foreach( KeyValuePair pair in parsed ) 98 | { 99 | @return.Add( pair.Key ); 100 | } 101 | } 102 | } 103 | #endif 104 | else 105 | throw new Exception( "Unsupported platform for the 'workaround Unity bugs in PlayerPrefs' class: " + Application.platform ); 106 | 107 | return @return; 108 | } 109 | } 110 | 111 | #if SUPPORT_MAC_OSX 112 | // 113 | // PlistCS Property List (plist) serialization and parsing library. 114 | // 115 | // https://github.com/animetrics/PlistCS 116 | // 117 | // Copyright (c) 2011 Animetrics Inc. (marc@animetrics.com) 118 | // 119 | // Permission is hereby granted, free of charge, to any person obtaining a copy 120 | // of this software and associated documentation files (the "Software"), to deal 121 | // in the Software without restriction, including without limitation the rights 122 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 123 | // copies of the Software, and to permit persons to whom the Software is 124 | // furnished to do so, subject to the following conditions: 125 | // 126 | // The above copyright notice and this permission notice shall be included in 127 | // all copies or substantial portions of the Software. 128 | // 129 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 130 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 131 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 132 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 133 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 134 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 135 | // THE SOFTWARE. 136 | public static class Plist 137 | { 138 | private static List offsetTable = new List(); 139 | private static List objectTable = new List(); 140 | private static int refCount; 141 | private static int objRefSize; 142 | private static int offsetByteSize; 143 | private static long offsetTableOffset; 144 | 145 | #region Public Functions 146 | 147 | public static object readPlist(string path) 148 | { 149 | using (FileStream f = new FileStream(path, FileMode.Open, FileAccess.Read)) 150 | { 151 | return readPlist(f, plistType.Auto); 152 | } 153 | } 154 | 155 | public static object readPlistSource(string source) 156 | { 157 | return readPlist(System.Text.Encoding.UTF8.GetBytes(source)); 158 | } 159 | 160 | public static object readPlist(byte[] data) 161 | { 162 | return readPlist(new MemoryStream(data), plistType.Auto); 163 | } 164 | 165 | public static plistType getPlistType(Stream stream) 166 | { 167 | byte[] magicHeader = new byte[8]; 168 | stream.Read(magicHeader, 0, 8); 169 | 170 | if (BitConverter.ToInt64(magicHeader, 0) == 3472403351741427810) 171 | { 172 | return plistType.Binary; 173 | } 174 | else 175 | { 176 | return plistType.Xml; 177 | } 178 | } 179 | 180 | public static object readPlist(Stream stream, plistType type) 181 | { 182 | if (type == plistType.Auto) 183 | { 184 | type = getPlistType(stream); 185 | stream.Seek(0, SeekOrigin.Begin); 186 | } 187 | 188 | if (type == plistType.Binary) 189 | { 190 | using (BinaryReader reader = new BinaryReader(stream)) 191 | { 192 | byte[] data = reader.ReadBytes((int) reader.BaseStream.Length); 193 | return readBinary(data); 194 | } 195 | } 196 | else 197 | { 198 | XmlDocument xml = new XmlDocument(); 199 | xml.XmlResolver = null; 200 | xml.Load(stream); 201 | return readXml(xml); 202 | } 203 | } 204 | 205 | public static void writeXml(object value, string path) 206 | { 207 | using (StreamWriter writer = new StreamWriter(path)) 208 | { 209 | writer.Write(writeXml(value)); 210 | } 211 | } 212 | 213 | public static void writeXml(object value, Stream stream) 214 | { 215 | using (StreamWriter writer = new StreamWriter(stream)) 216 | { 217 | writer.Write(writeXml(value)); 218 | } 219 | } 220 | 221 | public static string writeXml(object value) 222 | { 223 | using (MemoryStream ms = new MemoryStream()) 224 | { 225 | XmlWriterSettings xmlWriterSettings = new XmlWriterSettings(); 226 | xmlWriterSettings.Encoding = new System.Text.UTF8Encoding(false); 227 | xmlWriterSettings.ConformanceLevel = ConformanceLevel.Document; 228 | xmlWriterSettings.Indent = true; 229 | 230 | using (XmlWriter xmlWriter = XmlWriter.Create(ms, xmlWriterSettings)) 231 | { 232 | xmlWriter.WriteStartDocument(); 233 | //xmlWriter.WriteComment("DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " + "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\""); 234 | xmlWriter.WriteDocType("plist", "-//Apple Computer//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd", null); 235 | xmlWriter.WriteStartElement("plist"); 236 | xmlWriter.WriteAttributeString("version", "1.0"); 237 | compose(value, xmlWriter); 238 | xmlWriter.WriteEndElement(); 239 | xmlWriter.WriteEndDocument(); 240 | xmlWriter.Flush(); 241 | xmlWriter.Close(); 242 | return System.Text.Encoding.UTF8.GetString(ms.ToArray()); 243 | } 244 | } 245 | } 246 | 247 | public static void writeBinary(object value, string path) 248 | { 249 | using (BinaryWriter writer = new BinaryWriter(new FileStream(path, FileMode.Create))) 250 | { 251 | writer.Write(writeBinary(value)); 252 | } 253 | } 254 | 255 | public static void writeBinary(object value, Stream stream) 256 | { 257 | using (BinaryWriter writer = new BinaryWriter(stream)) 258 | { 259 | writer.Write(writeBinary(value)); 260 | } 261 | } 262 | 263 | public static byte[] writeBinary(object value) 264 | { 265 | offsetTable.Clear(); 266 | objectTable.Clear(); 267 | refCount = 0; 268 | objRefSize = 0; 269 | offsetByteSize = 0; 270 | offsetTableOffset = 0; 271 | 272 | //Do not count the root node, subtract by 1 273 | int totalRefs = countObject(value) - 1; 274 | 275 | refCount = totalRefs; 276 | 277 | objRefSize = RegulateNullBytes(BitConverter.GetBytes(refCount)).Length; 278 | 279 | composeBinary(value); 280 | 281 | writeBinaryString("bplist00", false); 282 | 283 | offsetTableOffset = (long)objectTable.Count; 284 | 285 | offsetTable.Add(objectTable.Count - 8); 286 | 287 | offsetByteSize = RegulateNullBytes(BitConverter.GetBytes(offsetTable[offsetTable.Count-1])).Length; 288 | 289 | List offsetBytes = new List(); 290 | 291 | offsetTable.Reverse(); 292 | 293 | for (int i = 0; i < offsetTable.Count; i++) 294 | { 295 | offsetTable[i] = objectTable.Count - offsetTable[i]; 296 | byte[] buffer = RegulateNullBytes(BitConverter.GetBytes(offsetTable[i]), offsetByteSize); 297 | Array.Reverse(buffer); 298 | offsetBytes.AddRange(buffer); 299 | } 300 | 301 | objectTable.AddRange(offsetBytes); 302 | 303 | objectTable.AddRange(new byte[6]); 304 | objectTable.Add(Convert.ToByte(offsetByteSize)); 305 | objectTable.Add(Convert.ToByte(objRefSize)); 306 | 307 | var a = BitConverter.GetBytes((long) totalRefs + 1); 308 | Array.Reverse(a); 309 | objectTable.AddRange(a); 310 | 311 | objectTable.AddRange(BitConverter.GetBytes((long)0)); 312 | a = BitConverter.GetBytes(offsetTableOffset); 313 | Array.Reverse(a); 314 | objectTable.AddRange(a); 315 | 316 | return objectTable.ToArray(); 317 | } 318 | 319 | #endregion 320 | 321 | #region Private Functions 322 | 323 | private static object readXml(XmlDocument xml) 324 | { 325 | XmlNode rootNode = xml.DocumentElement.ChildNodes[0]; 326 | return parse(rootNode); 327 | } 328 | 329 | private static object readBinary(byte[] data) 330 | { 331 | offsetTable.Clear(); 332 | List offsetTableBytes = new List(); 333 | objectTable.Clear(); 334 | refCount = 0; 335 | objRefSize = 0; 336 | offsetByteSize = 0; 337 | offsetTableOffset = 0; 338 | 339 | List bList = new List(data); 340 | 341 | List trailer = bList.GetRange(bList.Count - 32, 32); 342 | 343 | parseTrailer(trailer); 344 | 345 | objectTable = bList.GetRange(0, (int)offsetTableOffset); 346 | 347 | offsetTableBytes = bList.GetRange((int)offsetTableOffset, bList.Count - (int)offsetTableOffset - 32); 348 | 349 | parseOffsetTable(offsetTableBytes); 350 | 351 | return parseBinary(0); 352 | } 353 | 354 | private static Dictionary parseDictionary(XmlNode node) 355 | { 356 | XmlNodeList children = node.ChildNodes; 357 | if (children.Count % 2 != 0) 358 | { 359 | throw new DataMisalignedException("Dictionary elements must have an even number of child nodes"); 360 | } 361 | 362 | Dictionary dict = new Dictionary(); 363 | 364 | for (int i = 0; i < children.Count; i += 2) 365 | { 366 | XmlNode keynode = children[i]; 367 | XmlNode valnode = children[i + 1]; 368 | 369 | if (keynode.Name != "key") 370 | { 371 | throw new ApplicationException("expected a key node"); 372 | } 373 | 374 | object result = parse(valnode); 375 | 376 | if (result != null) 377 | { 378 | dict.Add(keynode.InnerText, result); 379 | } 380 | } 381 | 382 | return dict; 383 | } 384 | 385 | private static List parseArray(XmlNode node) 386 | { 387 | List array = new List(); 388 | 389 | foreach (XmlNode child in node.ChildNodes) 390 | { 391 | object result = parse(child); 392 | if (result != null) 393 | { 394 | array.Add(result); 395 | } 396 | } 397 | 398 | return array; 399 | } 400 | 401 | private static void composeArray(List value, XmlWriter writer) 402 | { 403 | writer.WriteStartElement("array"); 404 | foreach (object obj in value) 405 | { 406 | compose(obj, writer); 407 | } 408 | writer.WriteEndElement(); 409 | } 410 | 411 | private static object parse(XmlNode node) 412 | { 413 | switch (node.Name) 414 | { 415 | case "dict": 416 | return parseDictionary(node); 417 | case "array": 418 | return parseArray(node); 419 | case "string": 420 | return node.InnerText; 421 | case "integer": 422 | // int result; 423 | //int.TryParse(node.InnerText, System.Globalization.NumberFormatInfo.InvariantInfo, out result); 424 | return Convert.ToInt32(node.InnerText, System.Globalization.NumberFormatInfo.InvariantInfo); 425 | case "real": 426 | return Convert.ToDouble(node.InnerText,System.Globalization.NumberFormatInfo.InvariantInfo); 427 | case "false": 428 | return false; 429 | case "true": 430 | return true; 431 | case "null": 432 | return null; 433 | case "date": 434 | return XmlConvert.ToDateTime(node.InnerText, XmlDateTimeSerializationMode.Utc); 435 | case "data": 436 | return Convert.FromBase64String(node.InnerText); 437 | } 438 | 439 | throw new ApplicationException(String.Format("Plist Node `{0}' is not supported", node.Name)); 440 | } 441 | 442 | private static void compose(object value, XmlWriter writer) 443 | { 444 | 445 | if (value == null || value is string) 446 | { 447 | writer.WriteElementString("string", value as string); 448 | } 449 | else if (value is int || value is long) 450 | { 451 | writer.WriteElementString("integer", ((int)value).ToString(System.Globalization.NumberFormatInfo.InvariantInfo)); 452 | } 453 | else if (value is System.Collections.Generic.Dictionary || 454 | value.GetType().ToString().StartsWith("System.Collections.Generic.Dictionary`2[System.String")) 455 | { 456 | //Convert to Dictionary 457 | Dictionary dic = value as Dictionary; 458 | if (dic == null) 459 | { 460 | dic = new Dictionary(); 461 | IDictionary idic = (IDictionary)value; 462 | foreach (var key in idic.Keys) 463 | { 464 | dic.Add(key.ToString(), idic[key]); 465 | } 466 | } 467 | writeDictionaryValues(dic, writer); 468 | } 469 | else if (value is List) 470 | { 471 | composeArray((List)value, writer); 472 | } 473 | else if (value is byte[]) 474 | { 475 | writer.WriteElementString("data", Convert.ToBase64String((Byte[])value)); 476 | } 477 | else if (value is float || value is double) 478 | { 479 | writer.WriteElementString("real", ((double)value).ToString(System.Globalization.NumberFormatInfo.InvariantInfo)); 480 | } 481 | else if (value is DateTime) 482 | { 483 | DateTime time = (DateTime)value; 484 | string theString = XmlConvert.ToString(time, XmlDateTimeSerializationMode.Utc); 485 | writer.WriteElementString("date", theString);//, "yyyy-MM-ddTHH:mm:ssZ")); 486 | } 487 | else if (value is bool) 488 | { 489 | writer.WriteElementString(value.ToString().ToLower(), ""); 490 | } 491 | else 492 | { 493 | throw new Exception(String.Format("Value type '{0}' is unhandled", value.GetType().ToString())); 494 | } 495 | } 496 | 497 | private static void writeDictionaryValues(Dictionary dictionary, XmlWriter writer) 498 | { 499 | writer.WriteStartElement("dict"); 500 | foreach (string key in dictionary.Keys) 501 | { 502 | object value = dictionary[key]; 503 | writer.WriteElementString("key", key); 504 | compose(value, writer); 505 | } 506 | writer.WriteEndElement(); 507 | } 508 | 509 | private static int countObject(object value) 510 | { 511 | int count = 0; 512 | switch (value.GetType().ToString()) 513 | { 514 | case "System.Collections.Generic.Dictionary`2[System.String,System.Object]": 515 | Dictionary dict = (Dictionary)value; 516 | foreach (string key in dict.Keys) 517 | { 518 | count += countObject(dict[key]); 519 | } 520 | count += dict.Keys.Count; 521 | count++; 522 | break; 523 | case "System.Collections.Generic.List`1[System.Object]": 524 | List list = (List)value; 525 | foreach (object obj in list) 526 | { 527 | count += countObject(obj); 528 | } 529 | count++; 530 | break; 531 | default: 532 | count++; 533 | break; 534 | } 535 | return count; 536 | } 537 | 538 | private static byte[] writeBinaryDictionary(Dictionary dictionary) 539 | { 540 | List buffer = new List(); 541 | List header = new List(); 542 | List refs = new List(); 543 | for (int i = dictionary.Count - 1; i >= 0; i--) 544 | { 545 | var o = new object[dictionary.Count]; 546 | dictionary.Values.CopyTo(o, 0); 547 | composeBinary(o[i]); 548 | offsetTable.Add(objectTable.Count); 549 | refs.Add(refCount); 550 | refCount--; 551 | } 552 | for (int i = dictionary.Count - 1; i >= 0; i--) 553 | { 554 | var o = new string[dictionary.Count]; 555 | dictionary.Keys.CopyTo(o, 0); 556 | composeBinary(o[i]);//); 557 | offsetTable.Add(objectTable.Count); 558 | refs.Add(refCount); 559 | refCount--; 560 | } 561 | 562 | if (dictionary.Count < 15) 563 | { 564 | header.Add(Convert.ToByte(0xD0 | Convert.ToByte(dictionary.Count))); 565 | } 566 | else 567 | { 568 | header.Add(0xD0 | 0xf); 569 | header.AddRange(writeBinaryInteger(dictionary.Count, false)); 570 | } 571 | 572 | 573 | foreach (int val in refs) 574 | { 575 | byte[] refBuffer = RegulateNullBytes(BitConverter.GetBytes(val), objRefSize); 576 | Array.Reverse(refBuffer); 577 | buffer.InsertRange(0, refBuffer); 578 | } 579 | 580 | buffer.InsertRange(0, header); 581 | 582 | 583 | objectTable.InsertRange(0, buffer); 584 | 585 | return buffer.ToArray(); 586 | } 587 | 588 | private static byte[] composeBinaryArray(List objects) 589 | { 590 | List buffer = new List(); 591 | List header = new List(); 592 | List refs = new List(); 593 | 594 | for (int i = objects.Count - 1; i >= 0; i--) 595 | { 596 | composeBinary(objects[i]); 597 | offsetTable.Add(objectTable.Count); 598 | refs.Add(refCount); 599 | refCount--; 600 | } 601 | 602 | if (objects.Count < 15) 603 | { 604 | header.Add(Convert.ToByte(0xA0 | Convert.ToByte(objects.Count))); 605 | } 606 | else 607 | { 608 | header.Add(0xA0 | 0xf); 609 | header.AddRange(writeBinaryInteger(objects.Count, false)); 610 | } 611 | 612 | foreach (int val in refs) 613 | { 614 | byte[] refBuffer = RegulateNullBytes(BitConverter.GetBytes(val), objRefSize); 615 | Array.Reverse(refBuffer); 616 | buffer.InsertRange(0, refBuffer); 617 | } 618 | 619 | buffer.InsertRange(0, header); 620 | 621 | objectTable.InsertRange(0, buffer); 622 | 623 | return buffer.ToArray(); 624 | } 625 | 626 | private static byte[] composeBinary(object obj) 627 | { 628 | byte[] value; 629 | switch (obj.GetType().ToString()) 630 | { 631 | case "System.Collections.Generic.Dictionary`2[System.String,System.Object]": 632 | value = writeBinaryDictionary((Dictionary)obj); 633 | return value; 634 | 635 | case "System.Collections.Generic.List`1[System.Object]": 636 | value = composeBinaryArray((List)obj); 637 | return value; 638 | 639 | case "System.Byte[]": 640 | value = writeBinaryByteArray((byte[])obj); 641 | return value; 642 | 643 | case "System.Double": 644 | value = writeBinaryDouble((double)obj); 645 | return value; 646 | 647 | case "System.Int32": 648 | value = writeBinaryInteger((int)obj, true); 649 | return value; 650 | 651 | case "System.String": 652 | value = writeBinaryString((string)obj, true); 653 | return value; 654 | 655 | case "System.DateTime": 656 | value = writeBinaryDate((DateTime)obj); 657 | return value; 658 | 659 | case "System.Boolean": 660 | value = writeBinaryBool((bool)obj); 661 | return value; 662 | 663 | default: 664 | return new byte[0]; 665 | } 666 | } 667 | 668 | public static byte[] writeBinaryDate(DateTime obj) 669 | { 670 | List buffer =new List(RegulateNullBytes(BitConverter.GetBytes(PlistDateConverter.ConvertToAppleTimeStamp(obj)), 8)); 671 | buffer.Reverse(); 672 | buffer.Insert(0, 0x33); 673 | objectTable.InsertRange(0, buffer); 674 | return buffer.ToArray(); 675 | } 676 | 677 | public static byte[] writeBinaryBool(bool obj) 678 | { 679 | List buffer = new List(new byte[1] { (bool)obj ? (byte)9 : (byte)8 }); 680 | objectTable.InsertRange(0, buffer); 681 | return buffer.ToArray(); 682 | } 683 | 684 | private static byte[] writeBinaryInteger(int value, bool write) 685 | { 686 | List buffer = new List(BitConverter.GetBytes((long) value)); 687 | buffer =new List(RegulateNullBytes(buffer.ToArray())); 688 | while (buffer.Count != Math.Pow(2, Math.Log(buffer.Count) / Math.Log(2))) 689 | buffer.Add(0); 690 | int header = 0x10 | (int)(Math.Log(buffer.Count) / Math.Log(2)); 691 | 692 | buffer.Reverse(); 693 | 694 | buffer.Insert(0, Convert.ToByte(header)); 695 | 696 | if (write) 697 | objectTable.InsertRange(0, buffer); 698 | 699 | return buffer.ToArray(); 700 | } 701 | 702 | private static byte[] writeBinaryDouble(double value) 703 | { 704 | List buffer =new List(RegulateNullBytes(BitConverter.GetBytes(value), 4)); 705 | while (buffer.Count != Math.Pow(2, Math.Log(buffer.Count) / Math.Log(2))) 706 | buffer.Add(0); 707 | int header = 0x20 | (int)(Math.Log(buffer.Count) / Math.Log(2)); 708 | 709 | buffer.Reverse(); 710 | 711 | buffer.Insert(0, Convert.ToByte(header)); 712 | 713 | objectTable.InsertRange(0, buffer); 714 | 715 | return buffer.ToArray(); 716 | } 717 | 718 | private static byte[] writeBinaryByteArray(byte[] value) 719 | { 720 | List buffer = new List(value); 721 | List header = new List(); 722 | if (value.Length < 15) 723 | { 724 | header.Add(Convert.ToByte(0x40 | Convert.ToByte(value.Length))); 725 | } 726 | else 727 | { 728 | header.Add(0x40 | 0xf); 729 | header.AddRange(writeBinaryInteger(buffer.Count, false)); 730 | } 731 | 732 | buffer.InsertRange(0, header); 733 | 734 | objectTable.InsertRange(0, buffer); 735 | 736 | return buffer.ToArray(); 737 | } 738 | 739 | private static byte[] writeBinaryString(string value, bool head) 740 | { 741 | List buffer = new List(); 742 | List header = new List(); 743 | foreach (char chr in value.ToCharArray()) 744 | buffer.Add(Convert.ToByte(chr)); 745 | 746 | if (head) 747 | { 748 | if (value.Length < 15) 749 | { 750 | header.Add(Convert.ToByte(0x50 | Convert.ToByte(value.Length))); 751 | } 752 | else 753 | { 754 | header.Add(0x50 | 0xf); 755 | header.AddRange(writeBinaryInteger(buffer.Count, false)); 756 | } 757 | } 758 | 759 | buffer.InsertRange(0, header); 760 | 761 | objectTable.InsertRange(0, buffer); 762 | 763 | return buffer.ToArray(); 764 | } 765 | 766 | private static byte[] RegulateNullBytes(byte[] value) 767 | { 768 | return RegulateNullBytes(value, 1); 769 | } 770 | 771 | private static byte[] RegulateNullBytes(byte[] value, int minBytes) 772 | { 773 | Array.Reverse(value); 774 | List bytes = new List(value); 775 | for (int i = 0; i < bytes.Count; i++) 776 | { 777 | if (bytes[i] == 0 && bytes.Count > minBytes) 778 | { 779 | bytes.Remove(bytes[i]); 780 | i--; 781 | } 782 | else 783 | break; 784 | } 785 | 786 | if (bytes.Count < minBytes) 787 | { 788 | int dist = minBytes - bytes.Count; 789 | for (int i = 0; i < dist; i++) 790 | bytes.Insert(0, 0); 791 | } 792 | 793 | value = bytes.ToArray(); 794 | Array.Reverse(value); 795 | return value; 796 | } 797 | 798 | private static void parseTrailer(List trailer) 799 | { 800 | offsetByteSize = BitConverter.ToInt32(RegulateNullBytes(trailer.GetRange(6, 1).ToArray(), 4), 0); 801 | objRefSize = BitConverter.ToInt32(RegulateNullBytes(trailer.GetRange(7, 1).ToArray(), 4), 0); 802 | byte[] refCountBytes = trailer.GetRange(12, 4).ToArray(); 803 | Array.Reverse(refCountBytes); 804 | refCount = BitConverter.ToInt32(refCountBytes, 0); 805 | byte[] offsetTableOffsetBytes = trailer.GetRange(24, 8).ToArray(); 806 | Array.Reverse(offsetTableOffsetBytes); 807 | offsetTableOffset = BitConverter.ToInt64(offsetTableOffsetBytes, 0); 808 | } 809 | 810 | private static void parseOffsetTable(List offsetTableBytes) 811 | { 812 | for (int i = 0; i < offsetTableBytes.Count; i += offsetByteSize) 813 | { 814 | byte[] buffer = offsetTableBytes.GetRange(i, offsetByteSize).ToArray(); 815 | Array.Reverse(buffer); 816 | offsetTable.Add(BitConverter.ToInt32(RegulateNullBytes(buffer, 4), 0)); 817 | } 818 | } 819 | 820 | private static object parseBinaryDictionary(int objRef) 821 | { 822 | Dictionary buffer = new Dictionary(); 823 | List refs = new List(); 824 | int refCount = 0; 825 | 826 | int refStartPosition; 827 | refCount = getCount(offsetTable[objRef], out refStartPosition); 828 | 829 | 830 | if (refCount < 15) 831 | refStartPosition = offsetTable[objRef] + 1; 832 | else 833 | refStartPosition = offsetTable[objRef] + 2 + RegulateNullBytes(BitConverter.GetBytes(refCount), 1).Length; 834 | 835 | for (int i = refStartPosition; i < refStartPosition + refCount * 2 * objRefSize; i += objRefSize) 836 | { 837 | byte[] refBuffer = objectTable.GetRange(i, objRefSize).ToArray(); 838 | Array.Reverse(refBuffer); 839 | refs.Add(BitConverter.ToInt32(RegulateNullBytes(refBuffer, 4), 0)); 840 | } 841 | 842 | for (int i = 0; i < refCount; i++) 843 | { 844 | buffer.Add((string)parseBinary(refs[i]), parseBinary(refs[i + refCount])); 845 | } 846 | 847 | return buffer; 848 | } 849 | 850 | private static object parseBinaryArray(int objRef) 851 | { 852 | List buffer = new List(); 853 | List refs = new List(); 854 | int refCount = 0; 855 | 856 | int refStartPosition; 857 | refCount = getCount(offsetTable[objRef], out refStartPosition); 858 | 859 | 860 | if (refCount < 15) 861 | refStartPosition = offsetTable[objRef] + 1; 862 | else 863 | //The following integer has a header aswell so we increase the refStartPosition by two to account for that. 864 | refStartPosition = offsetTable[objRef] + 2 + RegulateNullBytes(BitConverter.GetBytes(refCount), 1).Length; 865 | 866 | for (int i = refStartPosition; i < refStartPosition + refCount * objRefSize; i += objRefSize) 867 | { 868 | byte[] refBuffer = objectTable.GetRange(i, objRefSize).ToArray(); 869 | Array.Reverse(refBuffer); 870 | refs.Add(BitConverter.ToInt32(RegulateNullBytes(refBuffer, 4), 0)); 871 | } 872 | 873 | for (int i = 0; i < refCount; i++) 874 | { 875 | buffer.Add(parseBinary(refs[i])); 876 | } 877 | 878 | return buffer; 879 | } 880 | 881 | private static int getCount(int bytePosition, out int newBytePosition) 882 | { 883 | byte headerByte = objectTable[bytePosition]; 884 | byte headerByteTrail = Convert.ToByte(headerByte & 0xf); 885 | int count; 886 | if (headerByteTrail < 15) 887 | { 888 | count = headerByteTrail; 889 | newBytePosition = bytePosition + 1; 890 | } 891 | else 892 | count = (int)parseBinaryInt(bytePosition + 1, out newBytePosition); 893 | return count; 894 | } 895 | 896 | private static object parseBinary(int objRef) 897 | { 898 | byte header = objectTable[offsetTable[objRef]]; 899 | switch (header & 0xF0) 900 | { 901 | case 0: 902 | { 903 | //If the byte is 904 | //0 return null 905 | //9 return true 906 | //8 return false 907 | return (objectTable[offsetTable[objRef]] == 0) ? (object)null : ((objectTable[offsetTable[objRef]] == 9) ? true : false); 908 | } 909 | case 0x10: 910 | { 911 | return parseBinaryInt(offsetTable[objRef]); 912 | } 913 | case 0x20: 914 | { 915 | return parseBinaryReal(offsetTable[objRef]); 916 | } 917 | case 0x30: 918 | { 919 | return parseBinaryDate(offsetTable[objRef]); 920 | } 921 | case 0x40: 922 | { 923 | return parseBinaryByteArray(offsetTable[objRef]); 924 | } 925 | case 0x50://String ASCII 926 | { 927 | return parseBinaryAsciiString(offsetTable[objRef]); 928 | } 929 | case 0x60://String Unicode 930 | { 931 | return parseBinaryUnicodeString(offsetTable[objRef]); 932 | } 933 | case 0xD0: 934 | { 935 | return parseBinaryDictionary(objRef); 936 | } 937 | case 0xA0: 938 | { 939 | return parseBinaryArray(objRef); 940 | } 941 | } 942 | throw new Exception("This type is not supported"); 943 | } 944 | 945 | public static object parseBinaryDate(int headerPosition) 946 | { 947 | byte[] buffer = objectTable.GetRange(headerPosition + 1, 8).ToArray(); 948 | Array.Reverse(buffer); 949 | double appleTime = BitConverter.ToDouble(buffer, 0); 950 | DateTime result = PlistDateConverter.ConvertFromAppleTimeStamp(appleTime); 951 | return result; 952 | } 953 | 954 | private static object parseBinaryInt(int headerPosition) 955 | { 956 | int output; 957 | return parseBinaryInt(headerPosition, out output); 958 | } 959 | 960 | private static object parseBinaryInt(int headerPosition, out int newHeaderPosition) 961 | { 962 | byte header = objectTable[headerPosition]; 963 | int byteCount = (int)Math.Pow(2, header & 0xf); 964 | byte[] buffer = objectTable.GetRange(headerPosition + 1, byteCount).ToArray(); 965 | Array.Reverse(buffer); 966 | //Add one to account for the header byte 967 | newHeaderPosition = headerPosition + byteCount + 1; 968 | return BitConverter.ToInt32(RegulateNullBytes(buffer, 4), 0); 969 | } 970 | 971 | private static object parseBinaryReal(int headerPosition) 972 | { 973 | byte header = objectTable[headerPosition]; 974 | int byteCount = (int)Math.Pow(2, header & 0xf); 975 | byte[] buffer = objectTable.GetRange(headerPosition + 1, byteCount).ToArray(); 976 | Array.Reverse(buffer); 977 | 978 | // Sabresaurus Note: This wasn't producing the right results with doubles, needed singles anyway, so I 979 | // added this line. (original line is commented out) 980 | return BitConverter.ToSingle(buffer, 0); 981 | // return BitConverter.ToDouble(RegulateNullBytes(buffer, 8), 0); 982 | } 983 | 984 | private static object parseBinaryAsciiString(int headerPosition) 985 | { 986 | int charStartPosition; 987 | int charCount = getCount(headerPosition, out charStartPosition); 988 | 989 | var buffer = objectTable.GetRange(charStartPosition, charCount); 990 | return buffer.Count > 0 ? Encoding.ASCII.GetString(buffer.ToArray()) : string.Empty; 991 | } 992 | 993 | private static object parseBinaryUnicodeString(int headerPosition) 994 | { 995 | int charStartPosition; 996 | int charCount = getCount(headerPosition, out charStartPosition); 997 | charCount = charCount * 2; 998 | 999 | byte[] buffer = new byte[charCount]; 1000 | byte one, two; 1001 | 1002 | for (int i = 0; i < charCount; i+=2) 1003 | { 1004 | one = objectTable.GetRange(charStartPosition+i,1)[0]; 1005 | two = objectTable.GetRange(charStartPosition + i+1, 1)[0]; 1006 | 1007 | if (BitConverter.IsLittleEndian) 1008 | { 1009 | buffer[i] = two; 1010 | buffer[i + 1] = one; 1011 | } 1012 | else 1013 | { 1014 | buffer[i] = one; 1015 | buffer[i + 1] = two; 1016 | } 1017 | } 1018 | 1019 | return Encoding.Unicode.GetString(buffer); 1020 | } 1021 | 1022 | private static object parseBinaryByteArray(int headerPosition) 1023 | { 1024 | int byteStartPosition; 1025 | int byteCount = getCount(headerPosition, out byteStartPosition); 1026 | return objectTable.GetRange(byteStartPosition, byteCount).ToArray(); 1027 | } 1028 | 1029 | #endregion 1030 | } 1031 | 1032 | public enum plistType 1033 | { 1034 | Auto, Binary, Xml 1035 | } 1036 | 1037 | public static class PlistDateConverter 1038 | { 1039 | public static long timeDifference = 978307200; 1040 | 1041 | public static long GetAppleTime(long unixTime) 1042 | { 1043 | return unixTime - timeDifference; 1044 | } 1045 | 1046 | public static long GetUnixTime(long appleTime) 1047 | { 1048 | return appleTime + timeDifference; 1049 | } 1050 | 1051 | public static DateTime ConvertFromAppleTimeStamp(double timestamp) 1052 | { 1053 | DateTime origin = new DateTime(2001, 1, 1, 0, 0, 0, 0); 1054 | return origin.AddSeconds(timestamp); 1055 | } 1056 | 1057 | public static double ConvertToAppleTimeStamp(DateTime date) 1058 | { 1059 | DateTime begin = new DateTime(2001, 1, 1, 0, 0, 0, 0); 1060 | TimeSpan diff = date - begin; 1061 | return Math.Floor(diff.TotalSeconds); 1062 | } 1063 | } 1064 | #endif 1065 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityMissingTryGetComponent.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace PublishersFork 4 | { 5 | /// 6 | /// Because Unity added "TryGetComponent" but failed to add the other versions of this method, even 7 | /// though they are obviously required (logged as a bug with Unity many years ago, but they replied that they don't 8 | /// want to fix it). 9 | /// 10 | /// This is functionally identical to merged with 11 | /// except it lacks the performance optimization for TryGetComponent that Unity implemented in EDITOR (at runtime 12 | /// TryGetComponent is supposedly the same implementation as GetComponent). This is acceptable since the method is primarily used 13 | /// for code-readability and maintenance, not for the (very minor) performance improvements. 14 | /// 15 | public static class WorkaroundUnityMissingTryGetComponent 16 | { 17 | public static bool TryGetComponentInChildren( this GameObject go, out T result ) where T : Component 18 | { 19 | var firstChild = go.GetComponentInChildren(); 20 | if( firstChild != null ) 21 | { 22 | result = firstChild; 23 | return true; 24 | } 25 | else 26 | { 27 | result = null; 28 | return false; 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityTextGeneratorMissingErrorReporting.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using UnityEngine; 3 | 4 | namespace PublishersFork 5 | { 6 | /// 7 | /// Unity's TextGenerator class - the only way to find the size of a piece of text, so that you can adjust 8 | /// sizes of the rest of your UI, images, etc - silently deletes any error messages, and does not allow you 9 | /// to see them, while incorrectly returning "0" for the width or height of strings. This method finds the 10 | /// error, if you call it immediately after a TextGenerator public API method has failed/returned 0. 11 | /// 12 | public static class WorkaroundUnityTextGeneratorMissingErrorReporting 13 | { 14 | public static string FetchLastErrorString(this TextGenerator tg) 15 | { 16 | var infoProp = typeof(TextGenerator).GetField("m_LastValid", BindingFlags.Instance | BindingFlags.NonPublic); 17 | var tge = infoProp.GetValue(tg); 18 | return tge.ToString(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityTexture2DPixelsPerPoint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace PublishersFork 5 | { 6 | /// 7 | /// There is a core method in Texture2D without which it's impossible to display textures correctly 8 | /// on retina screens (which as of 2022 is most screens) - but Unity made it private to Unity 9 | /// and added a specific hack to make it available to the UIToolkit team. There is a public equivalent that is 10 | /// Editor-only, but if you're using UIToolkit in player/runtime/games then you need this without the Editor APIs. 11 | /// 12 | /// FYI Unity's internal hack is: "[VisibleToOtherModules(new string[] {"UnityEngine.UIElementsModule"})]" 13 | /// 14 | public static class WorkaroundUnityTexture2DPixelsPerPoint 15 | { 16 | /// 17 | /// Note: this would be a property, except C# doesn't support extension properties yet. 18 | /// 19 | /// 20 | /// 21 | public static float PixelsPerPoint(this Texture2D texture2D) 22 | { 23 | var property_pixelsPerPoint = typeof(Texture2D).GetProperty("pixelsPerPoint"); 24 | var value = property_pixelsPerPoint.GetValue(texture2D); 25 | if (value is float) 26 | return (float) value; 27 | else 28 | throw new Exception("Unity changed their internal API; was expecting a float from .pixelsPerPoint, received something else: "+value); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityUIToolkitButtonsWithImages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UIElements; 4 | 5 | namespace PublishersFork 6 | { 7 | /// 8 | /// REQUIRES: "WorkaroundUnityTexture2DPixelsPerPoint.cs" -- because Unity made the core API here only available to 9 | /// other Unity employees. 10 | /// 11 | /// UIToolkit's Button class handles images badly: it won't let you put an image on a button without breaking the 12 | /// image's size/aspect-ratio. This class has methods for doing what you'd expect/demand by default: buttons-with-images 13 | /// use the image as the size for the button! 14 | /// 15 | public static class WorkaroundUnityUIToolkitButtonsWithImages 16 | { 17 | public static Button AddButtonUsingImage( this VisualElement parent, Texture2D icon, Action clickAction ) 18 | { 19 | var b = new Button( clickAction ) 20 | { 21 | style = 22 | { 23 | backgroundImage = icon, 24 | width = icon.width/icon.PixelsPerPoint(), 25 | height = icon.height/icon.PixelsPerPoint() 26 | } 27 | }; 28 | parent.Add( b ); 29 | 30 | return b; 31 | } 32 | 33 | /// 34 | /// Optional variant: lets you override the size of the icon, specifying it in device-independent values (i.e. same 35 | /// as the image's own pixels) 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | public static Button AddButtonUsingImage( this VisualElement parent, Texture2D icon, Action clickAction, Vector2? overrideSize ) 43 | { 44 | var b = new Button( clickAction ) 45 | { 46 | style = 47 | { 48 | backgroundImage = icon, 49 | width = (overrideSize?.x ?? icon.width)/icon.PixelsPerPoint(), 50 | height = (overrideSize?.y ?? icon.height)/icon.PixelsPerPoint() 51 | } 52 | }; 53 | parent.Add( b ); 54 | 55 | return b; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityUIToolkitFindOrCreate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.UIElements; 6 | 7 | namespace PublishersFork 8 | { 9 | /// 10 | /// This is one of the most heavily used fixes/improvements to UIToolkit. 11 | /// 12 | /// UIToolkit - by design - needs you to NEVER 'Add' a component if it already has been added, but ... 13 | /// Unity's provided methods don't directly allow that. So instead of calling Add( VisualElement ), 14 | /// we have a method ReuseOrAddNew( VisualElement, ... ). 15 | /// 16 | /// This method lazily creates a new VisualElement instance if needed AND OTHERWISE it discovers and 17 | /// returns the EXISTING instance that matched the required parameters. 18 | /// 19 | public static class WorkaroundUnityUIToolkitFindOrCreate 20 | { 21 | /// 22 | /// Uses the provided function as a constructor to create the new VisualElement, so that the T parameter doesn't need 23 | /// to be specified (it should be inferred from the constructor). Automatically unhides the element if it existed 24 | /// already and was invisible. 25 | /// 26 | /// Use it like this: 27 | /// 28 | /// VisualElement myRoot = ... 29 | /// Label newLabel = myRoot.ReuseOrAppendNew( "kLabel1", () => new Label() { text = "Hello World" } ); 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | public static T ReuseOrAppendNew( this VisualElement localRoot, string kName, Func initialSetup, bool debug = false ) where T : VisualElement 37 | { 38 | var block = localRoot.Q( kName ); 39 | if( block == null ) 40 | { 41 | if( debug ) Debug.Log( localRoot.name+" has no child named: "+kName ); 42 | 43 | block = initialSetup(); 44 | block.name = kName; 45 | localRoot.Add( block ); 46 | } 47 | else if( block.style.display == DisplayStyle.None ) 48 | block.style.display = DisplayStyle.Flex; 49 | 50 | return block; 51 | } 52 | 53 | /// 54 | /// Uses the default constructor for the VisualElement subclass, recommend you use the alternate 55 | /// 56 | /// 57 | /// 58 | /// 59 | /// 60 | public static T ReuseOrAppendNew( this VisualElement root, string childName ) where T : VisualElement, new() 61 | { 62 | return ReuseOrAppendNew( root, childName, () => new T() {name = childName} ); 63 | } 64 | 65 | /// 66 | /// As for except that 67 | /// it allows you to run code once - AND ONCE ONLY - after the object Constructor happens (this is necessary 68 | /// for a lot of UIToolkit's VisualElement classes that - due to UIToolkit's design - are incapable of being 69 | /// created in a single line of code). 70 | /// 71 | /// 72 | /// 73 | /// 74 | /// 75 | /// 76 | /// 77 | public static T ReuseOrAppendNew( this VisualElement localRoot, string kName, Func initialSetup, Action postConstructionInitializer ) where T : VisualElement 78 | { 79 | return ReuseOrAppendNew( localRoot, kName, initialSetup, postConstructionInitializer, out var discard ); 80 | } 81 | 82 | public static T ReuseOrAppendNew( this VisualElement localRoot, string kName, Func initialSetup, Action postConstructionInitializer, out bool didCreate ) where T : VisualElement 83 | { 84 | var block = localRoot.Q( kName ); 85 | if( block == null ) 86 | { 87 | block = initialSetup(); 88 | block.name = kName; 89 | postConstructionInitializer( block ); 90 | localRoot.Add( block ); 91 | didCreate = true; 92 | } 93 | else 94 | { 95 | if( block.style.display == DisplayStyle.None ) 96 | block.style.display = DisplayStyle.Flex; 97 | 98 | didCreate = false; 99 | } 100 | 101 | return block; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityUIToolkitFoldoutToggleIsPrivate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEngine; 5 | using UnityEngine.UIElements; 6 | 7 | namespace PublishersFork 8 | { 9 | /// 10 | /// One fix, multiple bugs, in Unity's Foldout. 11 | /// 12 | /// 1. Foldout is hard coded to change its own position if it detects its parent is anything EXCEPT another Foldout. 13 | /// This seemingly exists because they wrote Foldout as a hack, instead of writing it properly - they made it 14 | /// use negative margins (poorly), but that causes unpleasant visual design if a foldout has no spare space to the left. 15 | /// This was a reported bug in early releases of Unity 2018 or so. Instead of fixing the original bug, with several 16 | /// options that would have been CSS standard approaches - or even: just write a better code solution! - they added a bug to hide it. 17 | /// 18 | /// 2. Foldout can't be styled, because someone at Unity decided to make the core variable - REQUIRED BY DESIGN to be public 19 | /// - into a private variable. People have complained about this to Unity publicly for years, and UIToolkit team is 20 | /// aware but chose to do nothing about it. There is no obvious reason for making this private - it only makes everyone's 21 | /// code worse. There is no benefit. Everyone is forced to write hacks to 'guess' where the style elements are and 22 | /// write very fragile code that will fail if Unity makes any minor change in the future. Or ... Unity could just make 23 | /// the variable public! 24 | /// 25 | /// To fix problem 1, use the .Toggle() method here and re-instate the original bug, removing the secondary bug. Then 26 | /// make sure you always embedd Foldout instances inside something that has at least 15 pixels of left spare space. e.g.: 27 | /// 28 | /// VisualElement parent = ... 29 | /// parent.marginLeft = 15f; 30 | /// 31 | /// Foldout myFoldout = ... 32 | /// parent.Add( myFoldout ); 33 | /// myFoldout.Toggle().style.marginLeft = -15f; // what Unity originally set it to 34 | /// 35 | /// To fix problem 2, use .Toggle() method and then style it - however as of 2022 there are ADDITIONAL BUGS in Toggle 36 | /// class, such that it returns the wrong object for .labelElement, and your styling will be lost; another workaround 37 | /// class fixes that, providing a Toggle.Label() method that returns the 'real' Label. 38 | /// 39 | public static class WorkaroundUnityUIToolkitFoldoutToggleIsPrivate 40 | { 41 | /// 42 | /// Without this you can still try to style Foldouts, but it's error prone and painful, and very easy to get wrong. 43 | /// 44 | /// 45 | /// The internal Toggle that Foldout uses to implement most of its functionality 46 | public static Toggle Toggle( this Foldout foldout ) 47 | { 48 | return typeof(Foldout).GetField( "m_Toggle", BindingFlags.Instance | BindingFlags.NonPublic ).GetValue( foldout ) as Toggle; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityUIToolkitMarginsAll.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine.UIElements; 2 | 3 | namespace PublishersFork 4 | { 5 | /// 6 | /// The CSS Specification has for 30 years allowed you to set the margins with shorthands for 7 | /// "all margins", "top and bottom only", and "left and right only". Unity refuses to follow 8 | /// the specification here. These methods bring Unity closer to the specification. 9 | /// 10 | public static class WorkaroundUnityUIToolkitMarginsAll 11 | { 12 | public static void MarginsLeftRight( this IStyle s, StyleLength len ) 13 | { 14 | s.marginLeft = s.marginRight = len; 15 | } 16 | 17 | public static void MarginsTopBottom( this IStyle s, StyleLength len ) 18 | { 19 | s.marginTop = s.marginBottom = len; 20 | } 21 | 22 | public static void Margins( this IStyle s, StyleLength len ) 23 | { 24 | s.MarginsLeftRight( len ); 25 | s.MarginsTopBottom( len ); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /EngineForks/WorkaroundUnityUIToolkitToggleBrokenLabelBug.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEngine; 5 | using UnityEngine.UIElements; 6 | 7 | namespace PublishersFork 8 | { 9 | /// 10 | /// When using a Foldout, it uses Toggles for rendering. 11 | /// 12 | /// But those Toggles return the wrong object for 'labelElement', seems that Unity forgot to read the class they were 13 | /// extending? (BaseField provided labelElement - but Toggle returns the wrong object, and maintains a private 14 | /// object instead) 15 | /// 16 | public static class WorkaroundUnityUIToolkitToggleBrokenLabelBug 17 | { 18 | /// 19 | /// This returns the 'real' Label, instead of the 'fake, won't work' Label returned by the public API. 20 | /// 21 | /// 22 | /// Label that contains the text of the Toggle, and is styleable 23 | public static Label Label( this Toggle toggle ) 24 | { 25 | return typeof(Toggle).GetField( "m_Label", BindingFlags.Instance | BindingFlags.NonPublic ).GetValue( toggle ) as Label; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /PublishersFork.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityEngineForks", "EngineForks\UnityEngineForks.csproj", "{D50940CA-CAB4-4C80-8265-6A23E28E1F84}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityEditorForks", "EditorForks\UnityEditorForks.csproj", "{E43B98FF-ACF1-4561-B21E-355F0D4B5113}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {D50940CA-CAB4-4C80-8265-6A23E28E1F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {D50940CA-CAB4-4C80-8265-6A23E28E1F84}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {D50940CA-CAB4-4C80-8265-6A23E28E1F84}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {D50940CA-CAB4-4C80-8265-6A23E28E1F84}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {E43B98FF-ACF1-4561-B21E-355F0D4B5113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {E43B98FF-ACF1-4561-B21E-355F0D4B5113}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {E43B98FF-ACF1-4561-B21E-355F0D4B5113}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {E43B98FF-ACF1-4561-B21E-355F0D4B5113}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PublishersFork 2 | Unity3D core API fixes - NOT released by Unity, discovered/invented by AssetStore publishers 3 | 4 | ## Wat? 5 | A large bundle of standalone files, each of which fixes or works around one or more bugs in Unity3D. Download whichever ones you need. 6 | 7 | # Install instructions 8 | 9 | ## Option1: grab the files you want 10 | 11 | ...and drop them into your project. (almost) All files are standalone, and don't require any extra files. 12 | 13 | ## Option2: edit, customize, and submit pull-requests 14 | 15 | Two optional .csproj files are included which let you open the files in your IDE and edit them there. This is a great way to double-check if there are any dependencies before adding a file to your project. 16 | 17 | ### Enabling the .csproj / .sln 18 | 19 | **Critical**: For the .sln to work you must create a symlink after cloning, and it must be named "UnityEditorFolderSymlink". Without this, the IDE won't be able to find the Unity DLL's it needs to compile code. 20 | 21 | #### Windows: 22 | 1. Open a command prompt 23 | 2. Navigate to the folder that you cloned this project into 24 | 3. Create a 'hard junction' using this command line: 25 | 26 | > mklink /D UnityEditorFolderSymlink "C:\path\to\your\Unity\install\Editor" 27 | 28 | #### Mac / OSX: 29 | 1. Open a shell window 30 | 2. Navigate to the folder that you cloned this project into 31 | 3. Create a 'symbolic link' using this command line: 32 | 33 | > ln -s /path/to/your/Unity/install/Editor UnityEditorFolderSymlink 34 | 35 | 36 | # Background 37 | As of Spring 2022, Unity is currently running 5-6 months behind on responding to bug reports and starting bugfixes; after they've started on a bug, it takes between 1 and 4 months for them to fix it (average: approximately 2 months). Once it's fixed, it takes them between 2 weeks and 4 months for them to publish the fix in the live versions of Unity. 38 | 39 | After that ... it still takes anywhere from 3-24 months for most users of Unity to upgrade to a version that contains the fix. 40 | 41 | Net result: When an AssetStore publisher finds a bug in Unity, isolates it, creates a reproducible test-case, and senda it off to Unity ... they still have to wait approximately 1 to 3 *years* before the fix will be live for all their end-users. Until then ... they need to maintain a workaround in their own codebase. 42 | 43 | Over the last 15 years I've built up a large collection of automated fixes for Unity3D bugs that Unity has either been "slow", "very slow" to deal with ... or "won't" fix. I'm cleaning them up and sticking them here for others to borrow (and/or adapt) as they see fit. 44 | 45 | # Usage instructions 46 | From trial and error I've found the best way to handle this is "1 file per bugfix or workaround, with a verbose name that describes the bug or feature". This has made it extremely easy to add a fix to an existing/new project -- and easy to delete it from the project once we stop supporting the old versions of Unity that needed the fix. 47 | 48 | So ... find a bug-fix/feature-improvement you like, and download that one file. Nearly always it will be self contained. Occasionally some will depend on others - I try to make this as rare as possible, but there are some low-level features missing from Unity that are so important they end up being used in a lot of places. 49 | --------------------------------------------------------------------------------