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

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