1+ #if MA_VRCSDK3_AVATARS
2+
3+ using System . Collections . Generic ;
4+ using HarmonyLib ;
5+ using VRC . SDK3 . Avatars . ScriptableObjects ;
6+
7+ namespace nadena . dev . modular_avatar . core . editor . HarmonyPatches
8+ {
9+ // Workaround for https://feedback.vrchat.com/sdk-bug-reports/p/sdk-3101-pathologically-slow-behavior-in-vrcexpressionsmenu-issubmenurecursive-c
10+ //
11+ // VRCExpressionsMenuEditor uses IsSubmenuRecursive to check whether a menu is a submenu of the avatar root menu
12+ // in OnEnable, with the intent of setting the "parameters" field if it is. However, this traversal does not deal
13+ // with having multiple paths to the same menu, and has only very limited protection from loops (in that there is
14+ // an "arbitrary" depth limit of 16). This can result in editor hangs when there are many paths to the same node.
15+ //
16+ // Since there's no point visiting the same node multiple times for the same needle menu, we cache whether we've
17+ // visited the menu before here. It's theoretically possible for this to give a different result, since the second
18+ // visit might have a different depth, but I'll leave it up to VRC to do the "correct" fix, and just prevent the
19+ // editor hang for now.
20+ internal static class VRCExpressionMenuHangPatch
21+ {
22+ private static readonly HashSet < VRCExpressionsMenu > _visited = new ( ) ;
23+
24+ public static void Patch ( Harmony obj )
25+ {
26+ var ty_editor = AccessTools . TypeByName ( "VRCExpressionsMenuEditor" ) ;
27+ if ( ty_editor == null ) return ;
28+
29+ var onEnable = AccessTools . Method ( ty_editor , "OnEnable" ) ;
30+ var isSubmenuRecursive = AccessTools . Method ( ty_editor , "IsSubmenuRecursive" ) ;
31+
32+ if ( onEnable == null || isSubmenuRecursive == null ) return ;
33+
34+ obj . Patch ( onEnable ,
35+ // Clear first to ensure we start with a clean slate
36+ new HarmonyMethod ( typeof ( VRCExpressionMenuHangPatch ) , nameof ( ClearVisited ) ) ,
37+ // Clear after to avoid keeping references to objects longer than necessary (potential memory leak)
38+ new HarmonyMethod ( typeof ( VRCExpressionMenuHangPatch ) , nameof ( ClearVisited ) )
39+ ) ;
40+ obj . Patch ( isSubmenuRecursive ,
41+ new HarmonyMethod ( typeof ( VRCExpressionMenuHangPatch ) , nameof ( IsSubmenuRecursivePrefix ) ) ) ;
42+ }
43+
44+ private static void ClearVisited ( )
45+ {
46+ _visited . Clear ( ) ;
47+ }
48+
49+ private static bool IsSubmenuRecursivePrefix (
50+ VRCExpressionsMenu needle ,
51+ VRCExpressionsMenu haystack ,
52+ int depth ,
53+ ref bool __result
54+ )
55+ {
56+ if ( haystack != null && ! _visited . Add ( haystack ) )
57+ {
58+ // Already visited
59+ __result = false ;
60+ return false ;
61+ }
62+
63+ return true ;
64+ }
65+ }
66+ }
67+
68+ #endif
0 commit comments