com.wallstop-studios.unity-helpers 2.0.0-rc69 → 2.0.0-rc71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/Editor/AnimationCopier.cs +875 -93
  2. package/Editor/AnimationCreator.cs +840 -137
  3. package/Editor/AnimationEventEditor.cs +4 -4
  4. package/Editor/AnimatorControllerCopier.cs +3 -3
  5. package/Editor/Extensions/UnityExtensions.cs +26 -0
  6. package/Editor/Extensions/UnityExtensions.cs.meta +3 -0
  7. package/Editor/Extensions.meta +3 -0
  8. package/Editor/FitTextureSizeWindow.cs +371 -0
  9. package/Editor/PrefabChecker.cs +722 -0
  10. package/Editor/SpriteAtlasGenerator.cs +598 -0
  11. package/Editor/SpriteAtlasGenerator.cs.meta +3 -0
  12. package/Editor/SpriteCropper.cs +407 -0
  13. package/Editor/SpriteCropper.cs.meta +3 -0
  14. package/Editor/SpriteSettingsApplier.cs +756 -92
  15. package/Editor/TextureResizerWizard.cs +3 -3
  16. package/Editor/TextureSettingsApplier.cs +9 -9
  17. package/Editor/WShowIfPropertyDrawer.cs +2 -2
  18. package/Runtime/Core/Attributes/EnumDisplayNameAttribute.cs +15 -0
  19. package/Runtime/Core/Attributes/EnumDisplayNameAttribute.cs.meta +3 -0
  20. package/Runtime/Core/Extension/EnumExtensions.cs +176 -1
  21. package/Runtime/Core/Extension/UnityExtensions.cs +1 -1
  22. package/Runtime/Tags/AttributeUtilities.cs +8 -7
  23. package/Runtime/Tags/EffectHandler.cs +2 -1
  24. package/Tests/Runtime/DataStructures/BalancedKDTreeTests.cs +1 -1
  25. package/Tests/Runtime/DataStructures/CyclicBufferTests.cs +1 -1
  26. package/Tests/Runtime/DataStructures/QuadTreeTests.cs +1 -1
  27. package/Tests/Runtime/DataStructures/UnbalancedKDTreeTests.cs +1 -1
  28. package/Tests/Runtime/Extensions/DictionaryExtensionTests.cs +1 -1
  29. package/Tests/Runtime/Extensions/EnumExtensionTests.cs +1 -1
  30. package/Tests/Runtime/Extensions/IListExtensionTests.cs +1 -1
  31. package/Tests/Runtime/Extensions/LoggingExtensionTests.cs +1 -1
  32. package/Tests/Runtime/Extensions/RandomExtensionTests.cs +1 -1
  33. package/Tests/Runtime/Extensions/StringExtensionTests.cs +1 -1
  34. package/Tests/Runtime/Helper/WallMathTests.cs +1 -1
  35. package/Tests/Runtime/Performance/KDTreePerformanceTests.cs +1 -1
  36. package/Tests/Runtime/Performance/QuadTreePerformanceTests.cs +1 -1
  37. package/Tests/Runtime/Performance/SpatialTreePerformanceTest.cs +1 -1
  38. package/Tests/Runtime/Performance/UnbalancedKDTreeTests.cs +1 -1
  39. package/Tests/Runtime/Random/RandomTestBase.cs +2 -2
  40. package/Tests/Runtime/Serialization/JsonSerializationTest.cs +1 -1
  41. package/package.json +1 -1
  42. package/Editor/FitTextureSizeWizard.cs +0 -147
  43. package/Editor/PrefabCheckWizard.cs +0 -167
  44. /package/Editor/{FitTextureSizeWizard.cs.meta → FitTextureSizeWindow.cs.meta} +0 -0
  45. /package/Editor/{PrefabCheckWizard.cs.meta → PrefabChecker.cs.meta} +0 -0
@@ -0,0 +1,722 @@
1
+ namespace WallstopStudios.UnityHelpers.Editor
2
+ {
3
+ #if UNITY_EDITOR
4
+ using System;
5
+ using System.Collections;
6
+ using System.Collections.Generic;
7
+ using System.IO;
8
+ using System.Linq;
9
+ using System.Reflection;
10
+ using UnityEditor;
11
+ using UnityEngine;
12
+ using Core.Attributes;
13
+ using Core.Extension;
14
+ using Core.Helper;
15
+ using WallstopStudios.UnityHelpers.Utils;
16
+ using Object = UnityEngine.Object;
17
+
18
+ public sealed class PrefabChecker : EditorWindow
19
+ {
20
+ private const float ToggleWidth = 18f;
21
+ private const float ToggleSpacing = 4f;
22
+
23
+ private static readonly Dictionary<Type, List<FieldInfo>> FieldsByType = new();
24
+ private static readonly Dictionary<Type, List<FieldInfo>> ListFieldsByType = new();
25
+ private static readonly Dictionary<Type, List<FieldInfo>> StringFieldsByType = new();
26
+ private static readonly Dictionary<Type, List<FieldInfo>> ObjectFieldsByType = new();
27
+ private static readonly Dictionary<Type, List<RequireComponent>> RequiredComponentsByType =
28
+ new();
29
+
30
+ private readonly List<string> _assetPaths = new();
31
+ private Vector2 _scrollPosition;
32
+
33
+ private bool _checkMissingScripts = true;
34
+ private bool _checkNullElementsInLists = true;
35
+ private bool _checkMissingRequiredComponents = true;
36
+ private bool _checkEmptyStringFields;
37
+ private bool _checkNullObjectReferences = true;
38
+ private bool _onlyCheckNullObjectsWithAttribute = true;
39
+ private bool _checkDisabledRootGameObjects = true;
40
+ private bool _checkDisabledComponents;
41
+
42
+ private const string DefaultPrefabsFolder = "Assets/Prefabs";
43
+
44
+ [MenuItem("Tools/Wallstop Studios/Unity Helpers/Prefab Checker", priority = -1)]
45
+ public static void ShowWindow()
46
+ {
47
+ GetWindow<PrefabChecker>("Prefab Check");
48
+ }
49
+
50
+ private void OnEnable()
51
+ {
52
+ PopulateDefaultPaths();
53
+ }
54
+
55
+ private void PopulateDefaultPaths()
56
+ {
57
+ if (_assetPaths.Count == 0 && AssetDatabase.IsValidFolder(DefaultPrefabsFolder))
58
+ {
59
+ _assetPaths.Add(DefaultPrefabsFolder);
60
+ }
61
+ }
62
+
63
+ private void OnGUI()
64
+ {
65
+ _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
66
+ try
67
+ {
68
+ DrawConfigurationOptions();
69
+
70
+ EditorGUILayout.Space();
71
+ EditorGUILayout.LabelField("Target Folders", EditorStyles.boldLabel);
72
+ DrawAssetPaths();
73
+ if (GUILayout.Button("Add Folder"))
74
+ {
75
+ AddFolder();
76
+ }
77
+
78
+ EditorGUILayout.Space();
79
+ if (GUILayout.Button("Run Checks", GUILayout.Height(30)))
80
+ {
81
+ RunChecks();
82
+ }
83
+ }
84
+ finally
85
+ {
86
+ EditorGUILayout.EndScrollView();
87
+ }
88
+ }
89
+
90
+ private void DrawConfigurationOptions()
91
+ {
92
+ EditorGUILayout.LabelField("Validation Checks", EditorStyles.boldLabel);
93
+
94
+ var drawRightAlignedToggle = SetupDrawRightAlignedToggle();
95
+
96
+ float targetAlignmentX = 0f;
97
+ bool alignmentCalculated = false;
98
+
99
+ DrawAndAlign(
100
+ new GUIContent(
101
+ "Missing Scripts",
102
+ "Check for GameObjects with missing script references."
103
+ ),
104
+ () => _checkMissingScripts,
105
+ v => _checkMissingScripts = v,
106
+ false
107
+ );
108
+ DrawAndAlign(
109
+ new GUIContent(
110
+ "Nulls in Lists/Arrays",
111
+ "Check for null elements within serialized lists or arrays."
112
+ ),
113
+ () => _checkNullElementsInLists,
114
+ v => _checkNullElementsInLists = v,
115
+ false
116
+ );
117
+ DrawAndAlign(
118
+ new GUIContent(
119
+ "Missing Required Components",
120
+ "Check if components are missing dependencies defined by [RequireComponent]."
121
+ ),
122
+ () => _checkMissingRequiredComponents,
123
+ v => _checkMissingRequiredComponents = v,
124
+ false
125
+ );
126
+ DrawAndAlign(
127
+ new GUIContent(
128
+ "Empty String Fields",
129
+ "Check for serialized string fields that are empty."
130
+ ),
131
+ () => _checkEmptyStringFields,
132
+ v => _checkEmptyStringFields = v,
133
+ false
134
+ );
135
+
136
+ DrawAndAlign(
137
+ new GUIContent(
138
+ "Null Object References",
139
+ "Check for serialized UnityEngine.Object fields that are null."
140
+ ),
141
+ () => _checkNullObjectReferences,
142
+ v => _checkNullObjectReferences = v,
143
+ false
144
+ );
145
+
146
+ bool wasEnabled = GUI.enabled;
147
+ GUI.enabled = wasEnabled && _checkNullObjectReferences;
148
+ try
149
+ {
150
+ EditorGUI.indentLevel++;
151
+ try
152
+ {
153
+ DrawAndAlign(
154
+ new GUIContent(
155
+ "Only if [ValidateAssignment]",
156
+ "Only report null object references if the field has the [ValidateAssignment] attribute."
157
+ ),
158
+ () => _onlyCheckNullObjectsWithAttribute,
159
+ v => _onlyCheckNullObjectsWithAttribute = v,
160
+ true
161
+ );
162
+ }
163
+ finally
164
+ {
165
+ EditorGUI.indentLevel--;
166
+ }
167
+ }
168
+ finally
169
+ {
170
+ GUI.enabled = wasEnabled;
171
+ }
172
+
173
+ DrawAndAlign(
174
+ new GUIContent(
175
+ "Disabled Root GameObject",
176
+ "Check if the prefab's root GameObject is inactive."
177
+ ),
178
+ () => _checkDisabledRootGameObjects,
179
+ v => _checkDisabledRootGameObjects = v,
180
+ false
181
+ );
182
+ DrawAndAlign(
183
+ new GUIContent(
184
+ "Disabled Components",
185
+ "Check for any components on the prefab that are disabled."
186
+ ),
187
+ () => _checkDisabledComponents,
188
+ v => _checkDisabledComponents = v,
189
+ false
190
+ );
191
+ return;
192
+
193
+ void DrawAndAlign(
194
+ GUIContent content,
195
+ Func<bool> getter,
196
+ Action<bool> setter,
197
+ bool isNested
198
+ )
199
+ {
200
+ switch (alignmentCalculated)
201
+ {
202
+ case false when !isNested:
203
+ {
204
+ float viewWidth = EditorGUIUtility.currentViewWidth;
205
+
206
+ float availableWidth = viewWidth - 18f;
207
+ targetAlignmentX = availableWidth - ToggleWidth;
208
+ alignmentCalculated = true;
209
+ break;
210
+ }
211
+ case false when isNested:
212
+ {
213
+ float viewWidth = EditorGUIUtility.currentViewWidth;
214
+ float availableWidth = viewWidth - 20f - 15f;
215
+ targetAlignmentX = availableWidth - ToggleWidth;
216
+ alignmentCalculated = true;
217
+ this.LogWarn(
218
+ $"Calculated alignment X based on first item being nested. Alignment might be approximate."
219
+ );
220
+ break;
221
+ }
222
+ }
223
+
224
+ float? overrideX = isNested ? targetAlignmentX : null;
225
+
226
+ bool newValue = drawRightAlignedToggle(content, getter(), overrideX, isNested);
227
+ if (newValue != getter())
228
+ {
229
+ setter(newValue);
230
+ }
231
+ }
232
+ }
233
+
234
+ private static Func<GUIContent, bool, float?, bool, bool> SetupDrawRightAlignedToggle()
235
+ {
236
+ return (label, value, overrideToggleX, isNested) =>
237
+ {
238
+ Rect lineRect;
239
+ if (isNested)
240
+ {
241
+ EditorGUI.indentLevel++;
242
+ }
243
+
244
+ try
245
+ {
246
+ lineRect = EditorGUILayout.GetControlRect(
247
+ true,
248
+ EditorGUIUtility.singleLineHeight
249
+ );
250
+ }
251
+ finally
252
+ {
253
+ if (isNested)
254
+ {
255
+ EditorGUI.indentLevel--;
256
+ }
257
+ }
258
+
259
+ float defaultToggleX = lineRect.x + lineRect.width - ToggleWidth;
260
+ float finalToggleX = overrideToggleX ?? defaultToggleX;
261
+
262
+ finalToggleX = Mathf.Max(finalToggleX, lineRect.x + ToggleSpacing);
263
+
264
+ Rect toggleRect = new(finalToggleX, lineRect.y, ToggleWidth, lineRect.height);
265
+
266
+ float labelWidth = Mathf.Max(0, finalToggleX - lineRect.x - ToggleSpacing);
267
+ Rect labelRect = new(lineRect.x, lineRect.y, labelWidth, lineRect.height);
268
+
269
+ EditorGUI.LabelField(labelRect, label);
270
+ return EditorGUI.Toggle(toggleRect, value);
271
+ };
272
+ }
273
+
274
+ private void DrawAssetPaths()
275
+ {
276
+ if (_assetPaths.Count == 0)
277
+ {
278
+ EditorGUILayout.HelpBox(
279
+ "No target folders specified. Add folders containing prefabs to check.",
280
+ MessageType.Info
281
+ );
282
+ }
283
+
284
+ List<string> pathsToRemove = null;
285
+
286
+ foreach (string assetPath in _assetPaths)
287
+ {
288
+ using (new EditorGUILayout.HorizontalScope())
289
+ {
290
+ string currentPath = assetPath;
291
+ EditorGUILayout.LabelField(currentPath);
292
+
293
+ DefaultAsset folderAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(
294
+ currentPath
295
+ );
296
+ if (folderAsset != null)
297
+ {
298
+ if (GUILayout.Button("Ping", GUILayout.Width(50)))
299
+ {
300
+ EditorGUIUtility.PingObject(folderAsset);
301
+ }
302
+ }
303
+ else
304
+ {
305
+ GUILayout.Space(56);
306
+ }
307
+
308
+ if (GUILayout.Button("X", GUILayout.Width(25)))
309
+ {
310
+ pathsToRemove ??= new List<string>();
311
+ pathsToRemove.Add(currentPath);
312
+ }
313
+ }
314
+ }
315
+
316
+ if (pathsToRemove != null)
317
+ {
318
+ foreach (string path in pathsToRemove)
319
+ {
320
+ _assetPaths.Remove(path);
321
+ }
322
+ Repaint();
323
+ }
324
+ }
325
+
326
+ private void AddFolder()
327
+ {
328
+ string projectPath = Directory.GetParent(Application.dataPath)?.FullName;
329
+ if (string.IsNullOrWhiteSpace(projectPath))
330
+ {
331
+ this.LogError($"Failed to find project path!");
332
+ return;
333
+ }
334
+ string absolutePath = EditorUtility.OpenFolderPanel(
335
+ "Select Prefab Folder",
336
+ "Assets",
337
+ ""
338
+ );
339
+
340
+ if (!string.IsNullOrEmpty(absolutePath))
341
+ {
342
+ if (absolutePath.StartsWith(projectPath, StringComparison.Ordinal))
343
+ {
344
+ string relativePath =
345
+ "Assets" + absolutePath.Substring(projectPath.Length).Replace('\\', '/');
346
+
347
+ if (AssetDatabase.IsValidFolder(relativePath))
348
+ {
349
+ if (!_assetPaths.Contains(relativePath))
350
+ {
351
+ _assetPaths.Add(relativePath);
352
+ }
353
+ else
354
+ {
355
+ this.LogWarn($"Folder '{relativePath}' is already in the list.");
356
+ }
357
+ }
358
+ else
359
+ {
360
+ this.LogWarn(
361
+ $"Selected path '{relativePath}' is not a valid Unity folder."
362
+ );
363
+ }
364
+ }
365
+ else
366
+ {
367
+ this.LogError(
368
+ $"Selected folder must be inside the Unity project's Assets folder. Project path: {projectPath}, Selected path: {absolutePath}"
369
+ );
370
+ }
371
+ }
372
+ }
373
+
374
+ private void RunChecks()
375
+ {
376
+ if (_assetPaths == null || _assetPaths.Count == 0)
377
+ {
378
+ this.LogError($"No asset paths specified. Add folders containing prefabs.");
379
+ return;
380
+ }
381
+
382
+ List<string> validPaths = _assetPaths
383
+ .Where(p => !string.IsNullOrEmpty(p) && AssetDatabase.IsValidFolder(p))
384
+ .ToList();
385
+
386
+ if (validPaths.Count == 0)
387
+ {
388
+ this.LogError(
389
+ $"None of the specified paths are valid folders: {string.Join(", ", _assetPaths)}"
390
+ );
391
+ return;
392
+ }
393
+
394
+ this.Log($"Starting prefab check for folders: {string.Join(", ", validPaths)}");
395
+ int totalPrefabsChecked = 0;
396
+ int totalIssuesFound = 0;
397
+
398
+ foreach (GameObject prefab in Helpers.EnumeratePrefabs(validPaths))
399
+ {
400
+ totalPrefabsChecked++;
401
+ int issuesForThisPrefab = 0;
402
+ string prefabPath = AssetDatabase.GetAssetPath(prefab);
403
+
404
+ if (_checkDisabledRootGameObjects && !prefab.activeSelf)
405
+ {
406
+ prefab.LogWarn($"Prefab root GameObject is disabled.");
407
+ issuesForThisPrefab++;
408
+ }
409
+
410
+ List<MonoBehaviour> componentBuffer = Buffers<MonoBehaviour>.List;
411
+ prefab.GetComponentsInChildren(true, componentBuffer);
412
+
413
+ foreach (MonoBehaviour script in componentBuffer)
414
+ {
415
+ if (_checkMissingScripts && !script)
416
+ {
417
+ GameObject owner = FindOwnerOfMissingScript(prefab, componentBuffer);
418
+ string ownerName = owner ? owner.name : "[[Unknown GameObject]]";
419
+ Object context = owner ? (Object)owner : prefab;
420
+ context.LogError($"Detected missing script on GameObject '{ownerName}'.");
421
+ issuesForThisPrefab++;
422
+ continue;
423
+ }
424
+
425
+ if (!script)
426
+ {
427
+ continue;
428
+ }
429
+
430
+ Type scriptType = script.GetType();
431
+ GameObject ownerGameObject = script.gameObject;
432
+
433
+ if (_checkNullElementsInLists)
434
+ {
435
+ issuesForThisPrefab += ValidateNoNullsInLists(script, ownerGameObject);
436
+ }
437
+
438
+ if (_checkMissingRequiredComponents)
439
+ {
440
+ issuesForThisPrefab += ValidateRequiredComponents(script, ownerGameObject);
441
+ }
442
+
443
+ if (_checkEmptyStringFields)
444
+ {
445
+ issuesForThisPrefab += ValidateEmptyStrings(script, ownerGameObject);
446
+ }
447
+
448
+ if (_checkNullObjectReferences)
449
+ {
450
+ issuesForThisPrefab += ValidateNullObjectReferences(
451
+ script,
452
+ ownerGameObject
453
+ );
454
+ }
455
+
456
+ if (_checkDisabledComponents && script is Behaviour { enabled: false })
457
+ {
458
+ ownerGameObject.LogWarn(
459
+ $"Component '{scriptType.Name}' on GameObject '{ownerGameObject.name}' is disabled."
460
+ );
461
+ issuesForThisPrefab++;
462
+ }
463
+ }
464
+
465
+ if (issuesForThisPrefab > 0)
466
+ {
467
+ prefab.LogWarn(
468
+ $"Prefab '{prefab.name}' at path '{prefabPath}' has {issuesForThisPrefab} potential issues."
469
+ );
470
+ totalIssuesFound += issuesForThisPrefab;
471
+ }
472
+ }
473
+
474
+ if (totalIssuesFound > 0)
475
+ {
476
+ this.LogError(
477
+ $"Prefab check complete. Found {totalIssuesFound} potential issues across {totalPrefabsChecked} prefabs."
478
+ );
479
+ }
480
+ else
481
+ {
482
+ this.Log(
483
+ $"Prefab check complete. No issues found in {totalPrefabsChecked} prefabs."
484
+ );
485
+ }
486
+ }
487
+
488
+ private static GameObject FindOwnerOfMissingScript(
489
+ GameObject prefabRoot,
490
+ List<MonoBehaviour> buffer
491
+ )
492
+ {
493
+ Transform[] allTransforms = prefabRoot.GetComponentsInChildren<Transform>(true);
494
+ foreach (Transform transform in allTransforms)
495
+ {
496
+ MonoBehaviour[] components = transform.GetComponents<MonoBehaviour>();
497
+ if (
498
+ components.Length
499
+ == buffer.Count(c => c != null && c.gameObject == transform.gameObject)
500
+ )
501
+ {
502
+ continue;
503
+ }
504
+
505
+ bool foundInNonNullBuffer = components.Any(buffer.Contains);
506
+ if (foundInNonNullBuffer)
507
+ {
508
+ return transform.gameObject;
509
+ }
510
+
511
+ if (components.Length != 0 || !buffer.Exists(c => c == null))
512
+ {
513
+ continue;
514
+ }
515
+
516
+ HashSet<GameObject> gameObjectsWithComponentsInBuffer = buffer
517
+ .Where(c => c != null)
518
+ .Select(c => c.gameObject)
519
+ .ToHashSet();
520
+ if (!gameObjectsWithComponentsInBuffer.Contains(transform.gameObject))
521
+ {
522
+ return transform.gameObject;
523
+ }
524
+ }
525
+
526
+ return prefabRoot;
527
+ }
528
+
529
+ private static IEnumerable<FieldInfo> GetFieldsToCheck(
530
+ Type componentType,
531
+ Dictionary<Type, List<FieldInfo>> cache
532
+ )
533
+ {
534
+ return cache.GetOrAdd(
535
+ componentType,
536
+ type =>
537
+ type.GetFields(
538
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
539
+ )
540
+ .Where(field =>
541
+ field.IsPublic
542
+ || field.GetCustomAttributes(typeof(SerializeField), true).Any()
543
+ )
544
+ .ToList()
545
+ );
546
+ }
547
+
548
+ private static int ValidateNoNullsInLists(Object component, GameObject context)
549
+ {
550
+ int issueCount = 0;
551
+ Type componentType = component.GetType();
552
+
553
+ foreach (
554
+ FieldInfo field in ListFieldsByType.GetOrAdd(
555
+ componentType,
556
+ type =>
557
+ GetFieldsToCheck(type, FieldsByType)
558
+ .Where(f =>
559
+ typeof(IEnumerable).IsAssignableFrom(f.FieldType)
560
+ && f.FieldType != typeof(string)
561
+ )
562
+ .ToList()
563
+ )
564
+ )
565
+ {
566
+ object fieldValue = field.GetValue(component);
567
+
568
+ if (fieldValue is not IEnumerable list)
569
+ {
570
+ continue;
571
+ }
572
+
573
+ int index = 0;
574
+ if (list is Object unityObject)
575
+ {
576
+ if (unityObject == null)
577
+ {
578
+ unityObject.LogError(
579
+ $"Field '{field.Name}' ({field.FieldType.Name}) on component '{componentType.Name}' has a null enumerable."
580
+ );
581
+ }
582
+ // Ignore all enumerable unity objects, they're spooky
583
+ continue;
584
+ }
585
+ foreach (object element in list)
586
+ {
587
+ if (element == null || (element is Object unityObj && !unityObj))
588
+ {
589
+ context.LogError(
590
+ $"Field '{field.Name}' ({field.FieldType.Name}) on component '{componentType.Name}' has a null or missing element at index {index}."
591
+ );
592
+ issueCount++;
593
+ }
594
+ index++;
595
+ }
596
+ }
597
+ return issueCount;
598
+ }
599
+
600
+ private static int ValidateRequiredComponents(Component component, GameObject context)
601
+ {
602
+ int issueCount = 0;
603
+ Type componentType = component.GetType();
604
+
605
+ List<RequireComponent> required = RequiredComponentsByType.GetOrAdd(
606
+ componentType,
607
+ type =>
608
+ type.GetCustomAttributes(typeof(RequireComponent), true)
609
+ .Cast<RequireComponent>()
610
+ .ToList()
611
+ );
612
+
613
+ if (required.Count <= 0)
614
+ {
615
+ return issueCount;
616
+ }
617
+
618
+ foreach (RequireComponent requirement in required)
619
+ {
620
+ if (
621
+ requirement.m_Type0 != null
622
+ && component.GetComponent(requirement.m_Type0) == null
623
+ )
624
+ {
625
+ context.LogError(
626
+ $"Component '{componentType.Name}' requires component '{requirement.m_Type0.Name}', but it is missing."
627
+ );
628
+ issueCount++;
629
+ }
630
+ if (
631
+ requirement.m_Type1 != null
632
+ && component.GetComponent(requirement.m_Type1) == null
633
+ )
634
+ {
635
+ context.LogError(
636
+ $"Component '{componentType.Name}' requires component '{requirement.m_Type1.Name}', but it is missing."
637
+ );
638
+ issueCount++;
639
+ }
640
+ if (
641
+ requirement.m_Type2 != null
642
+ && component.GetComponent(requirement.m_Type2) == null
643
+ )
644
+ {
645
+ context.LogError(
646
+ $"Component '{componentType.Name}' requires component '{requirement.m_Type2.Name}', but it is missing."
647
+ );
648
+ issueCount++;
649
+ }
650
+ }
651
+ return issueCount;
652
+ }
653
+
654
+ private static int ValidateEmptyStrings(Object component, GameObject context)
655
+ {
656
+ int issueCount = 0;
657
+ Type componentType = component.GetType();
658
+
659
+ foreach (
660
+ FieldInfo field in StringFieldsByType.GetOrAdd(
661
+ componentType,
662
+ type =>
663
+ GetFieldsToCheck(type, FieldsByType)
664
+ .Where(f => f.FieldType == typeof(string))
665
+ .ToList()
666
+ )
667
+ )
668
+ {
669
+ object fieldValue = field.GetValue(component);
670
+ if (fieldValue is string stringValue && string.IsNullOrEmpty(stringValue))
671
+ {
672
+ context.LogWarn(
673
+ $"String field '{field.Name}' on component '{componentType.Name}' is null or empty."
674
+ );
675
+ issueCount++;
676
+ }
677
+ }
678
+ return issueCount;
679
+ }
680
+
681
+ private int ValidateNullObjectReferences(Object component, GameObject context)
682
+ {
683
+ int issueCount = 0;
684
+ Type componentType = component.GetType();
685
+
686
+ foreach (
687
+ FieldInfo field in ObjectFieldsByType.GetOrAdd(
688
+ componentType,
689
+ type =>
690
+ GetFieldsToCheck(type, FieldsByType)
691
+ .Where(f => typeof(Object).IsAssignableFrom(f.FieldType))
692
+ .ToList()
693
+ )
694
+ )
695
+ {
696
+ bool hasValidateAttribute = field
697
+ .GetCustomAttributes(typeof(ValidateAssignmentAttribute), true)
698
+ .Any();
699
+
700
+ if (_onlyCheckNullObjectsWithAttribute && !hasValidateAttribute)
701
+ {
702
+ continue;
703
+ }
704
+
705
+ object fieldValue = field.GetValue(component);
706
+
707
+ if (fieldValue != null && (fieldValue is not Object unityObj || unityObj))
708
+ {
709
+ continue;
710
+ }
711
+
712
+ string attributeMarker = hasValidateAttribute ? " (has [ValidateAssignment])" : "";
713
+ context.LogError(
714
+ $"Object reference field '{field.Name}'{attributeMarker} on component '{componentType.Name}' is null or missing."
715
+ );
716
+ issueCount++;
717
+ }
718
+ return issueCount;
719
+ }
720
+ }
721
+ #endif
722
+ }