diff --git a/RuntimeUnityEditor.Bepin5/LogViewer/LogViewerWindow.cs b/RuntimeUnityEditor.Bepin5/LogViewer/LogViewerWindow.cs index 32c71b2..a8f39e7 100644 --- a/RuntimeUnityEditor.Bepin5/LogViewer/LogViewerWindow.cs +++ b/RuntimeUnityEditor.Bepin5/LogViewer/LogViewerWindow.cs @@ -228,8 +228,15 @@ protected override void DrawContents() } else { - GUIUtility.systemCopyBuffer = entry.GetClipboardString(); - RuntimeUnityEditorCore.Logger.Log(Core.Utils.Abstractions.LogLevel.Message, $"[{nameof(LogViewerWindow)}] Copied to clipboard"); + try + { + UnityFeatureHelper.systemCopyBuffer = entry.GetClipboardString(); + RuntimeUnityEditorCore.Logger.Log(Core.Utils.Abstractions.LogLevel.Message, $"[{nameof(LogViewerWindow)}] Copied to clipboard"); + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(Core.Utils.Abstractions.LogLevel.Message | Core.Utils.Abstractions.LogLevel.Error, $"[{nameof(LogViewerWindow)}] Failed to copy to clipboard: " + e.Message); + } } } diff --git a/RuntimeUnityEditor/Features/ContextMenu.cs b/RuntimeUnityEditor/Features/ContextMenu.cs index a3c00d7..f1f0975 100644 --- a/RuntimeUnityEditor/Features/ContextMenu.cs +++ b/RuntimeUnityEditor/Features/ContextMenu.cs @@ -118,6 +118,7 @@ o is Sprite || if (o is Texture2D t) { + //todo GetRawTextureData is not available in Unity 4.x t.LoadRawTextureData(newTex.GetRawTextureData()); t.Apply(true); UnityEngine.Object.Destroy(newTex); @@ -185,7 +186,7 @@ public void Show(object obj, MemberInfo objMemberInfo) /// Screen position to show the menu at. public void Show(object obj, MemberInfo objMemberInfo, Vector2 clickPoint) { - _windowRect = new Rect(clickPoint, new Vector2(100, 100)); + _windowRect = new Rect(clickPoint.x, clickPoint.y, 100, 100); if (obj != null) { diff --git a/RuntimeUnityEditor/Features/Gizmos/GizmoDrawer.cs b/RuntimeUnityEditor/Features/Gizmos/GizmoDrawer.cs index 4ef6ca2..1355407 100644 --- a/RuntimeUnityEditor/Features/Gizmos/GizmoDrawer.cs +++ b/RuntimeUnityEditor/Features/Gizmos/GizmoDrawer.cs @@ -22,6 +22,8 @@ public sealed class GizmoDrawer : FeatureBase protected override void Initialize(InitSettings initSettings) { + UnityFeatureHelper.EnsureCameraRenderEventsAreAvailable(); + Enabled = false; DisplayName = "Gizmos"; ObjectTreeViewer.Instance.TreeSelectionChanged += UpdateState; diff --git a/RuntimeUnityEditor/Features/WireframeFeature.cs b/RuntimeUnityEditor/Features/WireframeFeature.cs index 76c8fc4..334f9e2 100644 --- a/RuntimeUnityEditor/Features/WireframeFeature.cs +++ b/RuntimeUnityEditor/Features/WireframeFeature.cs @@ -23,6 +23,8 @@ public sealed class WireframeFeature : FeatureBase protected override void Initialize(InitSettings initSettings) { + UnityFeatureHelper.EnsureCameraRenderEventsAreAvailable(); + DisplayName = "Wireframe"; Enabled = false; diff --git a/RuntimeUnityEditor/RuntimeUnityEditorCore.cs b/RuntimeUnityEditor/RuntimeUnityEditorCore.cs index ab42ab7..42560bb 100644 --- a/RuntimeUnityEditor/RuntimeUnityEditorCore.cs +++ b/RuntimeUnityEditor/RuntimeUnityEditorCore.cs @@ -207,7 +207,7 @@ internal RuntimeUnityEditorCore(InitSettings initSettings) if (feature is Taskbar) throw new InvalidOperationException("WindowManager somehow failed to initialize! I am die, thank you forever.", e); - Logger.Log(LogLevel.Warning, $"Failed to initialize {feature.GetType().Name} - " + e); + Logger.Log(LogLevel.Warning, $"Failed to initialize {feature.GetType().Name} - {(e is NotSupportedException ? e.Message : e.ToString())}"); } } diff --git a/RuntimeUnityEditor/Utils/Abstractions/UnityFeatureHelper.cs b/RuntimeUnityEditor/Utils/Abstractions/UnityFeatureHelper.cs index b535bed..ccf8aea 100644 --- a/RuntimeUnityEditor/Utils/Abstractions/UnityFeatureHelper.cs +++ b/RuntimeUnityEditor/Utils/Abstractions/UnityFeatureHelper.cs @@ -4,7 +4,9 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using UnityEngine; +using UnityEngine.SceneManagement; namespace RuntimeUnityEditor.Core.Utils.Abstractions { @@ -19,6 +21,19 @@ public static class UnityFeatureHelper static UnityFeatureHelper() { SupportsScenes = _scene != null && _sceneManager != null; + + if (SupportsScenes) + { + try + { + _ = SceneProxyMethods.GetSceneCount(); + } + catch + { + SupportsScenes = false; + } + } + if (!SupportsScenes) RuntimeUnityEditorCore.Logger.Log(LogLevel.Warning, "UnityEngine.SceneManager and/or UnityEngine.SceneManagement.Scene are not available, some features will be disabled"); @@ -48,51 +63,133 @@ static UnityFeatureHelper() } } + #region Scenes + /// - /// UnityEngine.SceneManagement.SceneManager is available, used by . + /// UnityEngine.SceneManagement.SceneManager is available, used by . /// public static bool SupportsScenes { get; private set; } /// - /// TextEditor.cursorIndex is available. + /// Get root game objects in active scene, or nothing if game doesn't support this. /// - public static bool SupportsCursorIndex { get; } + public static GameObject[] GetActiveSceneGameObjects() + { + return SupportsScenes ? SceneProxyMethods.GetActiveSceneGameObjects() : new GameObject[0]; + } - /// - /// C# REPL SHOULD be able to run in this environment (mcs might still be unhappy). - /// - public static bool SupportsRepl { get; } + public static int sceneCount => SupportsScenes ? SceneProxyMethods.GetSceneCount() : 0; - /// - /// Get root game objects in active scene, or nothing if game doesn't support this. - /// - public static GameObject[] GetSceneGameObjects() + public static bool GetSceneName(this GameObject go, out string sceneName) { - try + if (SupportsScenes) { - return GetSceneGameObjectsInternal(); + sceneName = SceneProxyMethods.GetSceneName(go); + return true; } - catch (Exception) + + sceneName = null; + return false; + } + + public static GameObject[] GetSceneRootObjects(int sceneLoadIndex) + { + if (!SupportsScenes) return new GameObject[0]; + return SceneProxyMethods.GetRootGameObjects(sceneLoadIndex); + } + + public static SceneWrapper GetSceneAt(int sceneLoadIndex) + { + if (!SupportsScenes) return default; + return SceneProxyMethods.GetSceneAt(sceneLoadIndex); + } + + public static bool UnloadScene(string sceneName) + { + if (!SupportsScenes) return false; + return SceneProxyMethods.UnloadScene(sceneName); + } + + public readonly struct SceneWrapper + { + public SceneWrapper(string name, int buildIndex, int rootCount, bool isLoaded, bool isDirty, string path) + { + this.name = name; + this.buildIndex = buildIndex; + this.rootCount = rootCount; + this.isLoaded = isLoaded; + this.isDirty = isDirty; + this.path = path; + } + + public readonly string name; + public readonly int buildIndex; + public readonly int rootCount; + public readonly bool isLoaded; + public readonly bool isDirty; + public readonly string path; + + public override string ToString() { - SupportsScenes = false; - return new GameObject[0]; + return $"Name: {name}\nBuildIndex: {buildIndex}\nRootCount: {rootCount}\nIsLoaded: {isLoaded}\nIsDirty: {isDirty}\nPath: {path}"; } } - private static GameObject[] GetSceneGameObjectsInternal() + /// + /// Proxy methods for SceneManager and Scene to avoid exceptions in old Unity versions (mostly 4.x) that do not support them. + /// By fencing all references here and never calling them directly it's possible to have no reflection overhead when they are available. + /// The NoInlining attribute is necessary for this to work reliably, because the JIT might inline the calls all the way back to the original caller, which creates an impossible to catch TypeLoadException. + /// + private static class SceneProxyMethods { - // Reflection for compatibility with Unity 4.x - var activeScene = _sceneManager.GetMethod("GetActiveScene", BindingFlags.Static | BindingFlags.Public); - if (activeScene == null) throw new ArgumentNullException(nameof(activeScene)); - var scene = activeScene.Invoke(null, null); - - var rootGameObjects = scene.GetType().GetMethod("GetRootGameObjects", BindingFlags.Instance | BindingFlags.Public, null, new Type[]{}, null); - if (rootGameObjects == null) throw new ArgumentNullException(nameof(rootGameObjects)); - var objects = rootGameObjects.Invoke(scene, null); - - return (GameObject[])objects; + [MethodImpl(MethodImplOptions.NoInlining)] + public static SceneWrapper GetSceneAt(int sceneLoadIndex) + { + var scene = SceneManager.GetSceneAt(sceneLoadIndex); + return new SceneWrapper(scene.name, scene.buildIndex, scene.rootCount, scene.isLoaded, scene.isDirty, scene.path); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static GameObject[] GetRootGameObjects(int sceneLoadIndex) + { + var scene = SceneManager.GetSceneAt(sceneLoadIndex); + return scene.GetRootGameObjects(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool UnloadScene(string sceneName) + { + return SceneManager.UnloadScene(sceneName); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int GetSceneCount() => SceneManager.sceneCount; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static string GetSceneName(GameObject go) + { + return go.scene.name; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static GameObject[] GetActiveSceneGameObjects() + { + return SceneManager.GetActiveScene().GetRootGameObjects(); + } } - + + #endregion + + /// + /// TextEditor.cursorIndex is available. + /// + public static bool SupportsCursorIndex { get; } + + /// + /// C# REPL SHOULD be able to run in this environment (mcs might still be unhappy). + /// + public static bool SupportsRepl { get; } + /// /// Figure out where the log file is written to and open it. /// @@ -141,8 +238,8 @@ bool TryOpen(string path) candidates.Clear(); // Fall back to more aggresive brute search // BepInEx 5.x log file, can be "LogOutput.log.1" or higher if multiple game instances run - candidates.AddRange(Directory.GetFiles(rootDir,"LogOutput.log*", SearchOption.AllDirectories)); - candidates.AddRange(Directory.GetFiles(rootDir,"output_log.txt", SearchOption.AllDirectories)); + candidates.AddRange(Directory.GetFiles(rootDir, "LogOutput.log*", SearchOption.AllDirectories)); + candidates.AddRange(Directory.GetFiles(rootDir, "output_log.txt", SearchOption.AllDirectories)); latestLog = candidates.Where(File.Exists).OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault(); if (TryOpen(latestLog)) return; @@ -173,5 +270,35 @@ public static Texture2D LoadTexture(byte[] texData) return tex; } + + /// + /// Throws if not available + /// + public static string systemCopyBuffer + { + get => SystemCopyBufferProxy.systemCopyBuffer; + set => SystemCopyBufferProxy.systemCopyBuffer = value; + } + + /// + private static class SystemCopyBufferProxy + { + public static string systemCopyBuffer + { + [MethodImpl(MethodImplOptions.NoInlining)] get => GUIUtility.systemCopyBuffer; + [MethodImpl(MethodImplOptions.NoInlining)] set => GUIUtility.systemCopyBuffer = value; + } + } + + /// + /// Throws if Camera.onPreRender and Camera.onPostRender are not available. + /// They are not available in Unity 4.x + /// + public static void EnsureCameraRenderEventsAreAvailable() + { + var cameraType = typeof(Camera); + if (cameraType.GetField(nameof(Camera.onPreRender), BindingFlags.Static | BindingFlags.Public) == null || cameraType.GetField(nameof(Camera.onPostRender), BindingFlags.Static | BindingFlags.Public) == null) + throw new NotSupportedException("Camera.onPreRender and/or Camera.onPostRender are not available"); + } } } diff --git a/RuntimeUnityEditor/Utils/Extensions.cs b/RuntimeUnityEditor/Utils/Extensions.cs index 5b39252..a800983 100644 --- a/RuntimeUnityEditor/Utils/Extensions.cs +++ b/RuntimeUnityEditor/Utils/Extensions.cs @@ -59,6 +59,16 @@ public static object CallPrivate(this object self, string name, params object[] return self.GetType().GetMethod(name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy).Invoke(self, p); } + public static object TryGetFieldValue(this object self, string name) + { + return AccessTools.Field(self.GetType(), name)?.GetValue(self); + } + + public static object TryGetPropertyValue(this object self, string name, object[] index = null) + { + return AccessTools.Property(self.GetType(), name)?.GetValue(self, index); + } + public static void ExecuteDelayed(this MonoBehaviour self, Action action, int waitCount = 1) { self.StartCoroutine(ExecuteDelayed_Routine(action, waitCount)); diff --git a/RuntimeUnityEditor/Windows/ChangeHistory/ChangeHistoryWindow.cs b/RuntimeUnityEditor/Windows/ChangeHistory/ChangeHistoryWindow.cs index 05bae96..33d20bb 100644 --- a/RuntimeUnityEditor/Windows/ChangeHistory/ChangeHistoryWindow.cs +++ b/RuntimeUnityEditor/Windows/ChangeHistory/ChangeHistoryWindow.cs @@ -42,14 +42,28 @@ protected override void DrawContents() if (GUILayout.Button("Copy all to clipboard")) { - GUIUtility.systemCopyBuffer = string.Join("\n", Change.Changes.Select(c => c.GetDisplayString()).ToArray()); - RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, $"Copied {Change.Changes.Count} changes to clipboard"); + try + { + UnityFeatureHelper.systemCopyBuffer = string.Join("\n", Change.Changes.Select(c => c.GetDisplayString()).ToArray()); + RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, $"Copied {Change.Changes.Count} changes to clipboard"); + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Message | LogLevel.Error, "Failed to copy to clipboard: " + e.Message); + } } if (GUILayout.Button("...as pseudo-code")) { - GUIUtility.systemCopyBuffer = string.Join("\n", Change.Changes.Select(ConvertChangeToPseudoCodeString).ToArray()); - RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, $"Copied {Change.Changes.Count} changes to clipboard (converted to pseudo-code)"); + try + { + UnityFeatureHelper.systemCopyBuffer = string.Join("\n", Change.Changes.Select(ConvertChangeToPseudoCodeString).ToArray()); + RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, $"Copied {Change.Changes.Count} changes to clipboard (converted to pseudo-code)"); + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Message | LogLevel.Error, "Failed to copy to clipboard: " + e.Message); + } } GUILayout.FlexibleSpace(); diff --git a/RuntimeUnityEditor/Windows/Inspector/VariableFieldDrawer.cs b/RuntimeUnityEditor/Windows/Inspector/VariableFieldDrawer.cs index 8865019..b35e163 100644 --- a/RuntimeUnityEditor/Windows/Inspector/VariableFieldDrawer.cs +++ b/RuntimeUnityEditor/Windows/Inspector/VariableFieldDrawer.cs @@ -369,7 +369,8 @@ private static void DrawSprite(Sprite spr, string objectName) extraData += $"TextureRect={spr.textureRect}"; } - GUILayout.Label($"Name={spr.name} Rect={spr.rect} Pivot={spr.pivot} Packed={spr.packed} {extraData}"); + // pivot is not supported in Unity 4.x + GUILayout.Label($"Name={spr.name} Rect={spr.rect} Pivot={spr.TryGetPropertyValue(nameof(Sprite.pivot)) ?? "UNSUPPORTED"} Packed={spr.packed} {extraData}"); GUILayout.FlexibleSpace(); diff --git a/RuntimeUnityEditor/Windows/ObjectTree/ObjectTreeViewer.cs b/RuntimeUnityEditor/Windows/ObjectTree/ObjectTreeViewer.cs index cb8a964..faeb910 100644 --- a/RuntimeUnityEditor/Windows/ObjectTree/ObjectTreeViewer.cs +++ b/RuntimeUnityEditor/Windows/ObjectTree/ObjectTreeViewer.cs @@ -145,7 +145,7 @@ private void DisplayObjectTreeHelper(GameObject go, int indent, ref int currentC { GUI.color = Color.cyan; } - else if (go.scene.name == null && !go.activeInHierarchy) + else if (go.GetSceneName(out var sceneName) && sceneName == null && !go.activeInHierarchy) { GUI.color = new Color(0.6f, 0.6f, 0.4f, 1); } diff --git a/RuntimeUnityEditor/Windows/ObjectTree/RootGameObjectSearcher.cs b/RuntimeUnityEditor/Windows/ObjectTree/RootGameObjectSearcher.cs index 2167f79..52c6650 100644 --- a/RuntimeUnityEditor/Windows/ObjectTree/RootGameObjectSearcher.cs +++ b/RuntimeUnityEditor/Windows/ObjectTree/RootGameObjectSearcher.cs @@ -86,7 +86,7 @@ public void Refresh(bool full, Predicate objectFilter) { if (UnityFeatureHelper.SupportsScenes) { - var newItems = UnityFeatureHelper.GetSceneGameObjects(); + var newItems = UnityFeatureHelper.GetActiveSceneGameObjects(); for (var index = 0; index < newItems.Length; index++) _cachedRootGameObjects.InsertSorted(newItems[index], GameObjectNameComparer.Instance); } @@ -295,6 +295,9 @@ public static bool SearchReferencesInComponent(object objInstance, Component c) private static bool IsGameObjectNull(GameObject o) { + // Looks like Unity 4.x doesn't set InstanceID to 0 after object is destroyed. Checking SupportsScenes seems close enough. + if (!UnityFeatureHelper.SupportsScenes) return o == null; + // This is around 25% faster than o == null // Object.IsNativeObjectAlive would be even better at above 35% but isn't public and reflection would eat the gains var isGameObjectNull = (object)o == null || o.GetInstanceID() == 0;