1+ using System . Collections . Generic ;
2+ using System . Linq ;
3+ using SOBaseEvents ;
4+ using UnityEditor ;
5+ using UnityEditor . SceneManagement ;
6+ using UnityEngine ;
7+
8+ namespace Editor . AuditorTool
9+ {
10+ public class EventSystemAuditor : EditorWindow
11+ {
12+ private const string PrefabExtension = ".prefab" ;
13+ private const string SceneExtension = ".unity" ;
14+
15+ private Vector2 _scrollPos ;
16+ private List < ScriptableObject > _allProjectEvents = new List < ScriptableObject > ( ) ;
17+ private Dictionary < ScriptableObject , bool > _expandedStates = new Dictionary < ScriptableObject , bool > ( ) ;
18+ private Dictionary < ScriptableObject , Dictionary < string , List < UsageDetail > > > _masterResults =
19+ new Dictionary < ScriptableObject , Dictionary < string , List < UsageDetail > > > ( ) ;
20+ private GUIStyle _headerStyle ;
21+
22+ private struct UsageDetail
23+ {
24+ public string GameObjectName ;
25+ public string ComponentTypeName ;
26+ public Object Context ;
27+ }
28+
29+ [ MenuItem ( "EspidiGames/SO Events/SO Event System Auditor" ) ]
30+ public static void ShowWindow ( )
31+ {
32+ GetWindow < EventSystemAuditor > ( "Event Auditor" ) ;
33+ }
34+
35+ private void OnEnable ( )
36+ {
37+ GetWindow < EventSystemAuditor > ( "Event Auditor" ) ;
38+ RefreshAndScanAll ( ) ;
39+ }
40+
41+ private void OnGUI ( )
42+ {
43+ RenderTopToolBar ( ) ;
44+ RenderResultsSection ( ) ;
45+ }
46+
47+ private void RenderTopToolBar ( )
48+ {
49+ EditorGUILayout . BeginHorizontal ( EditorStyles . toolbar ) ;
50+
51+ if ( GUILayout . Button ( "Refresh & Scan Project" , EditorStyles . toolbarButton ) )
52+ {
53+ RefreshAndScanAll ( ) ;
54+ }
55+
56+ GUILayout . FlexibleSpace ( ) ;
57+
58+ if ( GUILayout . Button ( "Expand All" , EditorStyles . toolbarButton ) )
59+ {
60+ SetEventUsagesExpandedState ( true ) ;
61+ }
62+
63+ if ( GUILayout . Button ( "Collapse All" , EditorStyles . toolbarButton ) )
64+ {
65+ SetEventUsagesExpandedState ( false ) ;
66+ }
67+
68+ EditorGUILayout . EndHorizontal ( ) ;
69+ }
70+
71+ private void RenderResultsSection ( )
72+ {
73+ _scrollPos = EditorGUILayout . BeginScrollView ( _scrollPos ) ;
74+
75+ if ( _allProjectEvents . Count == 0 )
76+ {
77+ EditorGUILayout . HelpBox ( "No ScriptableObjects implementing ISOEventBase were found in the project." ,
78+ MessageType . Info ) ;
79+ }
80+
81+ foreach ( var soEvent in _allProjectEvents )
82+ {
83+ RenderSOEventsUsages ( soEvent ) ;
84+ }
85+
86+ EditorGUILayout . EndScrollView ( ) ;
87+ }
88+
89+ private void RenderSOEventsUsages ( ScriptableObject soEvent )
90+ {
91+ // Default to expanded if state not found
92+ if ( ! _expandedStates . ContainsKey ( soEvent ) )
93+ {
94+ _expandedStates [ soEvent ] = true ;
95+ }
96+
97+ var expanded = _expandedStates [ soEvent ] ;
98+ _headerStyle ??= CreateHeaderStyle ( ) ;
99+
100+ // Visual Feedback: Highlight background if expanded
101+ if ( expanded )
102+ {
103+ GUI . backgroundColor = new Color ( 0.8f , 0.9f , 1f ) ;
104+ }
105+
106+ EditorGUILayout . BeginVertical ( EditorStyles . helpBox ) ;
107+
108+ RenderScriptableObjectEventEntryHeaderToggle ( soEvent , expanded ) ;
109+ GUI . backgroundColor = Color . white ;
110+
111+ if ( expanded )
112+ {
113+ EditorGUILayout . Space ( 2 ) ;
114+ if ( _masterResults . ContainsKey ( soEvent ) && _masterResults [ soEvent ] . Count > 0 )
115+ {
116+ foreach ( var assetEntry in _masterResults [ soEvent ] )
117+ {
118+ // Group by Asset (Prefab/Scene)
119+ EditorGUILayout . BeginVertical ( EditorStyles . textArea ) ;
120+
121+ GUILayout . Label ( $ "📂 { System . IO . Path . GetFileName ( assetEntry . Key ) } ", EditorStyles . boldLabel ) ;
122+
123+ foreach ( var detail in assetEntry . Value )
124+ {
125+ RenderSOEventUsage ( detail ) ;
126+ }
127+
128+ EditorGUILayout . EndVertical ( ) ;
129+ EditorGUILayout . Space ( 1 ) ;
130+ }
131+ }
132+ else
133+ {
134+ EditorGUILayout . LabelField ( " No usages detected in prefabs or scenes." , EditorStyles . centeredGreyMiniLabel ) ;
135+ }
136+ }
137+
138+ EditorGUILayout . EndVertical ( ) ;
139+ EditorGUILayout . Space ( 2 ) ;
140+ }
141+
142+ private static GUIStyle CreateHeaderStyle ( )
143+ {
144+ var headerStyle = new GUIStyle ( EditorStyles . miniButtonMid ) ;
145+ headerStyle . alignment = TextAnchor . MiddleLeft ;
146+ headerStyle . fontStyle = FontStyle . Bold ;
147+ headerStyle . fontSize = 11 ;
148+ headerStyle . fixedHeight = 25 ;
149+
150+ return headerStyle ;
151+ }
152+
153+ private void RenderScriptableObjectEventEntryHeaderToggle ( ScriptableObject soEvent , bool expanded )
154+ {
155+ var arrow = expanded ? "▼" : "▶" ;
156+ if ( GUILayout . Button ( $ " { arrow } { soEvent . name . ToUpper ( ) } [{ soEvent . GetType ( ) . Name } ]", _headerStyle ) )
157+ {
158+ _expandedStates [ soEvent ] = ! expanded ;
159+ }
160+ }
161+
162+ private static void RenderSOEventUsage ( UsageDetail detail )
163+ {
164+ EditorGUILayout . BeginHorizontal ( ) ;
165+ GUILayout . Space ( 15 ) ;
166+ // Deep link to component
167+ if ( GUILayout . Button ( $ " # GO: { detail . GameObjectName } ({ detail . ComponentTypeName } )", EditorStyles . label ) )
168+ {
169+ EditorGUIUtility . PingObject ( detail . Context ) ;
170+ }
171+ EditorGUILayout . EndHorizontal ( ) ;
172+ }
173+
174+ private void SetEventUsagesExpandedState ( bool expand )
175+ {
176+ var keys = _expandedStates . Keys . ToList ( ) ;
177+
178+ foreach ( var key in keys )
179+ {
180+ _expandedStates [ key ] = expand ;
181+ }
182+ }
183+
184+ #region === Asset scanning logic ===
185+
186+ private void RefreshAndScanAll ( )
187+ {
188+ _allProjectEvents . Clear ( ) ;
189+ _masterResults . Clear ( ) ;
190+
191+ FindAllScriptableObjectEventAssets ( ) ;
192+ ScanDependencies ( ) ;
193+ }
194+
195+ private void FindAllScriptableObjectEventAssets ( )
196+ {
197+ var guids = AssetDatabase . FindAssets ( "t:ScriptableObject" ) ;
198+
199+ foreach ( var guid in guids )
200+ {
201+ var path = AssetDatabase . GUIDToAssetPath ( guid ) ;
202+ var scriptableObjectAsset = AssetDatabase . LoadAssetAtPath < ScriptableObject > ( path ) ;
203+
204+ if ( scriptableObjectAsset is ISOEventBase )
205+ {
206+ _allProjectEvents . Add ( scriptableObjectAsset ) ;
207+ _masterResults [ scriptableObjectAsset ] = new Dictionary < string , List < UsageDetail > > ( ) ;
208+ if ( ! _expandedStates . ContainsKey ( scriptableObjectAsset ) )
209+ {
210+ _expandedStates [ scriptableObjectAsset ] = true ;
211+ }
212+ }
213+ }
214+ }
215+
216+ private void ScanDependencies ( )
217+ {
218+ var potentialAssets = AssetDatabase . FindAssets ( "t:Prefab t:Scene" ) ;
219+ foreach ( var guid in potentialAssets )
220+ {
221+ var assetPath = AssetDatabase . GUIDToAssetPath ( guid ) ;
222+ var dependencies = AssetDatabase . GetDependencies ( assetPath ) ;
223+
224+ foreach ( var soEvent in _allProjectEvents )
225+ {
226+ if ( dependencies . Contains ( AssetDatabase . GetAssetPath ( soEvent ) ) )
227+ {
228+ if ( assetPath . EndsWith ( PrefabExtension ) )
229+ {
230+ ScanPrefab ( assetPath , soEvent ) ;
231+ }
232+ else if ( assetPath . EndsWith ( SceneExtension ) )
233+ {
234+ ScanScene ( assetPath , soEvent ) ;
235+ }
236+ }
237+ }
238+ }
239+ }
240+
241+ private void ScanPrefab ( string path , ScriptableObject target )
242+ {
243+ var root = AssetDatabase . LoadAssetAtPath < GameObject > ( path ) ;
244+ var allComponents = root . GetComponentsInChildren < Component > ( true ) ;
245+ CheckComponents ( path , allComponents , target ) ;
246+ }
247+
248+ private void ScanScene ( string path , ScriptableObject target )
249+ {
250+ // Load scene additively and silently to inspect contents
251+ var tempScene = EditorSceneManager . OpenScene ( path , OpenSceneMode . Additive ) ;
252+ var allComponents = Resources . FindObjectsOfTypeAll < Component > ( ) . Where ( c => c . gameObject . scene == tempScene ) ;
253+ CheckComponents ( path , allComponents , target ) ;
254+ EditorSceneManager . CloseScene ( tempScene , true ) ;
255+ }
256+
257+ private void CheckComponents ( string assetPath , IEnumerable < Component > components , ScriptableObject target )
258+ {
259+ foreach ( var component in components )
260+ {
261+ if ( component == null )
262+ {
263+ continue ;
264+ }
265+
266+ // Iterate through all serialized properties to find the SO reference
267+ var serializedObject = new SerializedObject ( component ) ;
268+ var property = serializedObject . GetIterator ( ) ;
269+
270+ while ( property . NextVisible ( true ) )
271+ {
272+ if ( property . propertyType == SerializedPropertyType . ObjectReference && property . objectReferenceValue == target )
273+ {
274+ if ( ! _masterResults [ target ] . ContainsKey ( assetPath ) )
275+ {
276+ _masterResults [ target ] [ assetPath ] = new List < UsageDetail > ( ) ;
277+ }
278+
279+ _masterResults [ target ] [ assetPath ] . Add ( new UsageDetail {
280+ GameObjectName = component . gameObject . name ,
281+ ComponentTypeName = component . GetType ( ) . Name ,
282+ Context = component
283+ } ) ;
284+ break ;
285+ }
286+ }
287+ }
288+ }
289+
290+ #endregion
291+ }
292+ }
0 commit comments