From f134e0c5174c160167f61755dcabd2b27e16d7a8 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sun, 21 Jan 2024 17:03:12 +0900 Subject: [PATCH] fix: work around VRCF's reflective hacks to ensure correct processing order (#126) This works around some of VRCFury's horrible hacks to improve compatibility between NDMF and VRCF. In particular, we detect when VRCFury is invoking us reflectively, and either skip optimizations (if running in play mode), or ignore VRCFury's invocation and allow VRChat's build hooks to run us (in a build). --- CHANGELOG.md | 1 + Editor/ApplyOnPlay.cs | 4 +- Editor/AvatarProcessor.cs | 67 +++++++++++++++++-- Editor/VRChat/BuildFrameworkPreprocessHook.cs | 4 +- Runtime/AlreadyProcessedTag.cs | 4 ++ 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e13a1b55..e257e84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Adjusted hook processing order to improve compatibility with VRCFury (#122) +- Worked around a hack in VRCFury that broke optimization plugins (#126) ### Removed diff --git a/Editor/ApplyOnPlay.cs b/Editor/ApplyOnPlay.cs index c850800b..a8c2a171 100644 --- a/Editor/ApplyOnPlay.cs +++ b/Editor/ApplyOnPlay.cs @@ -62,7 +62,9 @@ private static void MaybeProcessAvatar(ApplyOnPlayGlobalActivator.OnDemandSource { var avatar = RuntimeUtil.FindAvatarInParents(component.transform); if (avatar == null) return; - AvatarProcessor.ProcessAvatar(avatar.gameObject); + + // Skip optimizing the avatar as we might have VRCFury or similar running after us. + AvatarProcessor.ProcessAvatar(avatar.gameObject, BuildPhase.Transforming); } } diff --git a/Editor/AvatarProcessor.cs b/Editor/AvatarProcessor.cs index b95958f4..8b181dc9 100644 --- a/Editor/AvatarProcessor.cs +++ b/Editor/AvatarProcessor.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; +using System.Linq; using nadena.dev.ndmf.runtime; using UnityEditor; using UnityEngine; @@ -98,6 +99,37 @@ public static GameObject ProcessAvatarUI(GameObject obj) } } + #if NDMF_VRCSDK3_AVATARS + private static bool IsVRCFuryHack(System.Diagnostics.StackTrace trace) + { + foreach (var frame in trace.GetFrames()) + { + Debug.Log("Frame: " + frame.GetMethod().DeclaringType.FullName + " " + frame.GetMethod()); + } + + return trace.GetFrames().Any(frame => + frame.GetMethod().DeclaringType.FullName == "VF.Menu.NdmfFirstMenuItem" + ); + } + + private static bool InHookExecution(System.Diagnostics.StackTrace trace) + { + return trace.GetFrames().Any(frame => + typeof(VRC.SDKBase.Editor.BuildPipeline.IVRCSDKPreprocessAvatarCallback) + .IsAssignableFrom(frame.GetMethod().DeclaringType)); + } + #else + private static bool IsVRCFuryHack(System.Diagnostics.StackTrace trace) + { + return false; + } + + private static bool InHookExecution(System.Diagnostics.StackTrace trace) + { + return false; + } + #endif + /// /// Processes an avatar as part of an automated process. The resulting assets will be saved in a temporary /// location. @@ -105,16 +137,43 @@ public static GameObject ProcessAvatarUI(GameObject obj) /// public static void ProcessAvatar(GameObject root) { - if (root.GetComponent()) return; - + ProcessAvatar(root, BuildPhase.Optimizing); + } + + internal static void ProcessAvatar(GameObject root, BuildPhase lastPhase) { + if (root.GetComponent()?.processingCompleted == true) return; + + // HACK: VRCFury tries to invoke ProcessAvatar during its own processing, but this risks having Optimization + // phase passes run too early (before VRCF runs). Detect when we're being invoked like this and skip + // optimization. + System.Diagnostics.StackTrace stackTrace = new System.Diagnostics.StackTrace(); + if (IsVRCFuryHack(stackTrace)) + { + if (InHookExecution(stackTrace)) + { + Debug.Log("NDMF: Detected VRCFury hack from within VRChat build hooks - " + + "ignoring VRCFury invocation"); + // We're running from within VRChat build hooks, so just ignore VRCFury's request; + // we'll be run in the correct order anyway. + return; + } + else + { + Debug.Log("NDMF: Detected VRCFury hack from play mode - skipping optimization"); + // Skip optimizations, because they might break VRCFury processing. + lastPhase = BuildPhase.Transforming; + } + } + var buildContext = new BuildContext(root, TemporaryAssetRoot); - ProcessAvatar(buildContext, BuildPhase.Resolving, BuildPhase.Optimizing); + ProcessAvatar(buildContext, BuildPhase.Resolving, lastPhase); buildContext.Finish(); if (RuntimeUtil.IsPlaying) { - root.AddComponent(); + var tag = root.GetComponent() ?? root.AddComponent(); + tag.processingCompleted = true; } } diff --git a/Editor/VRChat/BuildFrameworkPreprocessHook.cs b/Editor/VRChat/BuildFrameworkPreprocessHook.cs index 9cf2a68b..71053b43 100644 --- a/Editor/VRChat/BuildFrameworkPreprocessHook.cs +++ b/Editor/VRChat/BuildFrameworkPreprocessHook.cs @@ -27,7 +27,7 @@ internal class BuildFrameworkPreprocessHook : IVRCSDKPreprocessAvatarCallback public bool OnPreprocessAvatar(GameObject avatarGameObject) { - if (avatarGameObject.GetComponent()) return true; + if (avatarGameObject.GetComponent()?.processingCompleted == true) return true; try { @@ -52,7 +52,7 @@ internal class BuildFrameworkOptimizeHook : IVRCSDKPreprocessAvatarCallback public bool OnPreprocessAvatar(GameObject avatarGameObject) { - if (avatarGameObject.GetComponent()) return true; + if (avatarGameObject.GetComponent()?.processingCompleted == true) return true; var holder = avatarGameObject.GetComponent(); if (holder == null) return true; diff --git a/Runtime/AlreadyProcessedTag.cs b/Runtime/AlreadyProcessedTag.cs index 7a4cf0c5..0ba4a6d3 100644 --- a/Runtime/AlreadyProcessedTag.cs +++ b/Runtime/AlreadyProcessedTag.cs @@ -6,6 +6,10 @@ namespace nadena.dev.ndmf.runtime [AddComponentMenu("")] internal class AlreadyProcessedTag : MonoBehaviour { + // VRCF creates this tag via reflection, but we're not actually done processing yet. + // We add this boolean so we can ignore any tags created surrepitiously by VRCF... + internal bool processingCompleted; + private void OnValidate() { hideFlags = HideFlags.HideAndDontSave;