From 32e015554646164e1f6e55d93226680460f7a007 Mon Sep 17 00:00:00 2001
From: bd_ <bd_@nadena.dev>
Date: Mon, 29 Jan 2024 19:10:01 +0900
Subject: [PATCH] feat: add non-component-based hook execution deduplication
 (#142)

This is in preparation for coordination with VRCF:
https://discord.com/channels/1001423276553285653/1001424777401077830/1201398914574712842
---
 CHANGELOG.md                                  |  1 +
 Editor/VRChat/BuildFrameworkPreprocessHook.cs | 11 ++++-
 Editor/VRChat/HookDedup.cs                    | 43 +++++++++++++++++++
 Editor/VRChat/HookDedup.cs.meta               |  3 ++
 4 files changed, 57 insertions(+), 1 deletion(-)
 create mode 100644 Editor/VRChat/HookDedup.cs
 create mode 100644 Editor/VRChat/HookDedup.cs.meta

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0305ac8..07e5c8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [unreleased]
 
 ### Added
+- Added a non-component-based check for double execution of hooks (#142)
 
 ### Fixed
 
diff --git a/Editor/VRChat/BuildFrameworkPreprocessHook.cs b/Editor/VRChat/BuildFrameworkPreprocessHook.cs
index 71053b4..d3fc84c 100644
--- a/Editor/VRChat/BuildFrameworkPreprocessHook.cs
+++ b/Editor/VRChat/BuildFrameworkPreprocessHook.cs
@@ -27,8 +27,13 @@ internal class BuildFrameworkPreprocessHook : IVRCSDKPreprocessAvatarCallback
 
         public bool OnPreprocessAvatar(GameObject avatarGameObject)
         {
+            // Legacy: For VRCF
             if (avatarGameObject.GetComponent<AlreadyProcessedTag>()?.processingCompleted == true) return true;
 
+            var state = HookDedup.RecordAvatar(avatarGameObject);
+            if (state.ranEarlyHook) return true;
+            state.ranEarlyHook = true;
+
             try
             {
                 var holder = avatarGameObject.AddComponent<ContextHolder>();
@@ -53,7 +58,11 @@ internal class BuildFrameworkOptimizeHook : IVRCSDKPreprocessAvatarCallback
         public bool OnPreprocessAvatar(GameObject avatarGameObject)
         {
             if (avatarGameObject.GetComponent<AlreadyProcessedTag>()?.processingCompleted == true) return true;
-
+            
+            var state = HookDedup.RecordAvatar(avatarGameObject);
+            if (state.ranOptimization) return true;
+            state.ranOptimization = true;
+            
             var holder = avatarGameObject.GetComponent<ContextHolder>();
             if (holder == null) return true;
 
diff --git a/Editor/VRChat/HookDedup.cs b/Editor/VRChat/HookDedup.cs
new file mode 100644
index 0000000..f4d59df
--- /dev/null
+++ b/Editor/VRChat/HookDedup.cs
@@ -0,0 +1,43 @@
+#if NDMF_VRCSDK3_AVATARS
+
+using System.Runtime.CompilerServices;
+using UnityEngine;
+
+namespace nadena.dev.ndmf.VRChat
+{
+    /// <summary>
+    /// Ensures that we don't run hooks more than once. This is in preparation for coordinating with VRCFury to have all
+    /// nondestructive utilities independently execute hooks as part of Apply on Play, while deduplicating to ensure that
+    /// we don't rerun hooks on the same avatar.
+    /// </summary>
+    internal static class HookDedup
+    {
+        internal class State
+        {
+            internal bool ranEarlyHook;
+            internal bool ranOptimization;
+        }
+
+        private static ConditionalWeakTable<GameObject, State> _avatars = new ConditionalWeakTable<GameObject, State>();
+        
+        public static State RecordAvatar(GameObject root)
+        {
+            if (_avatars.TryGetValue(root, out var state))
+            {
+                return state;
+            }
+
+            state = new State();
+            _avatars.Add(root, state);
+
+            return state;
+        }
+
+        public static bool HasAvatar(GameObject root)
+        {
+            return _avatars.TryGetValue(root, out _);
+        }
+    }
+}
+
+#endif
\ No newline at end of file
diff --git a/Editor/VRChat/HookDedup.cs.meta b/Editor/VRChat/HookDedup.cs.meta
new file mode 100644
index 0000000..c764a08
--- /dev/null
+++ b/Editor/VRChat/HookDedup.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a4f36e298dab4f8ba30bcd68940cf3a6
+timeCreated: 1706520401
\ No newline at end of file