Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions RuntimeUnityEditor.Bepin5/LogViewer/LogViewerWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion RuntimeUnityEditor/Features/ContextMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -185,7 +186,7 @@ public void Show(object obj, MemberInfo objMemberInfo)
/// <param name="clickPoint">Screen position to show the menu at.</param>
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)
{
Expand Down
2 changes: 2 additions & 0 deletions RuntimeUnityEditor/Features/Gizmos/GizmoDrawer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public sealed class GizmoDrawer : FeatureBase<GizmoDrawer>

protected override void Initialize(InitSettings initSettings)
{
UnityFeatureHelper.EnsureCameraRenderEventsAreAvailable();

Enabled = false;
DisplayName = "Gizmos";
ObjectTreeViewer.Instance.TreeSelectionChanged += UpdateState;
Expand Down
2 changes: 2 additions & 0 deletions RuntimeUnityEditor/Features/WireframeFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public sealed class WireframeFeature : FeatureBase<WireframeFeature>

protected override void Initialize(InitSettings initSettings)
{
UnityFeatureHelper.EnsureCameraRenderEventsAreAvailable();

DisplayName = "Wireframe";
Enabled = false;

Expand Down
2 changes: 1 addition & 1 deletion RuntimeUnityEditor/RuntimeUnityEditorCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())}");
}
}

Expand Down
187 changes: 157 additions & 30 deletions RuntimeUnityEditor/Utils/Abstractions/UnityFeatureHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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");

Expand Down Expand Up @@ -48,51 +63,133 @@ static UnityFeatureHelper()
}
}

#region Scenes

/// <summary>
/// UnityEngine.SceneManagement.SceneManager is available, used by <see cref="GetSceneGameObjects"/>.
/// UnityEngine.SceneManagement.SceneManager is available, used by <see cref="GetActiveSceneGameObjects"/>.
/// </summary>
public static bool SupportsScenes { get; private set; }

/// <summary>
/// TextEditor.cursorIndex is available.
/// Get root game objects in active scene, or nothing if game doesn't support this.
/// </summary>
public static bool SupportsCursorIndex { get; }
public static GameObject[] GetActiveSceneGameObjects()
{
return SupportsScenes ? SceneProxyMethods.GetActiveSceneGameObjects() : new GameObject[0];
}

/// <summary>
/// C# REPL SHOULD be able to run in this environment (mcs might still be unhappy).
/// </summary>
public static bool SupportsRepl { get; }
public static int sceneCount => SupportsScenes ? SceneProxyMethods.GetSceneCount() : 0;

/// <summary>
/// Get root game objects in active scene, or nothing if game doesn't support this.
/// </summary>
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()
/// <summary>
/// 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.
/// </summary>
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

/// <summary>
/// TextEditor.cursorIndex is available.
/// </summary>
public static bool SupportsCursorIndex { get; }

/// <summary>
/// C# REPL SHOULD be able to run in this environment (mcs might still be unhappy).
/// </summary>
public static bool SupportsRepl { get; }

/// <summary>
/// Figure out where the log file is written to and open it.
/// </summary>
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -173,5 +270,35 @@ public static Texture2D LoadTexture(byte[] texData)

return tex;
}

/// <summary>
/// Throws if not available
/// </summary>
public static string systemCopyBuffer
{
get => SystemCopyBufferProxy.systemCopyBuffer;
set => SystemCopyBufferProxy.systemCopyBuffer = value;
}

/// <see cref="SceneProxyMethods"/>
private static class SystemCopyBufferProxy
{
public static string systemCopyBuffer
{
[MethodImpl(MethodImplOptions.NoInlining)] get => GUIUtility.systemCopyBuffer;
[MethodImpl(MethodImplOptions.NoInlining)] set => GUIUtility.systemCopyBuffer = value;
}
}

/// <summary>
/// Throws if Camera.onPreRender and Camera.onPostRender are not available.
/// They are not available in Unity 4.x
/// </summary>
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");
}
}
}
10 changes: 10 additions & 0 deletions RuntimeUnityEditor/Utils/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
22 changes: 18 additions & 4 deletions RuntimeUnityEditor/Windows/ChangeHistory/ChangeHistoryWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion RuntimeUnityEditor/Windows/Inspector/VariableFieldDrawer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion RuntimeUnityEditor/Windows/ObjectTree/ObjectTreeViewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void Refresh(bool full, Predicate<GameObject> 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);
}
Expand Down Expand Up @@ -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;
Expand Down