com.wallstop-studios.unity-helpers 2.0.0-rc68 → 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.
- package/Editor/AnimationCopier.cs +875 -93
- package/Editor/AnimationCreator.cs +840 -137
- package/Editor/AnimationEventEditor.cs +4 -4
- package/Editor/AnimatorControllerCopier.cs +3 -3
- package/Editor/Extensions/UnityExtensions.cs +26 -0
- package/Editor/Extensions/UnityExtensions.cs.meta +3 -0
- package/Editor/Extensions.meta +3 -0
- package/Editor/FitTextureSizeWindow.cs +371 -0
- package/Editor/PrefabChecker.cs +716 -0
- package/Editor/SpriteAtlasGenerator.cs +598 -0
- package/Editor/SpriteAtlasGenerator.cs.meta +3 -0
- package/Editor/SpriteCropper.cs +407 -0
- package/Editor/SpriteCropper.cs.meta +3 -0
- package/Editor/SpriteSettingsApplier.cs +756 -92
- package/Editor/TextureResizerWizard.cs +3 -3
- package/Editor/TextureSettingsApplier.cs +9 -9
- package/Editor/WShowIfPropertyDrawer.cs +2 -2
- package/Runtime/Core/Attributes/EnumDisplayNameAttribute.cs +15 -0
- package/Runtime/Core/Attributes/EnumDisplayNameAttribute.cs.meta +3 -0
- package/Runtime/Core/Attributes/ValidateAssignmentAttribute.cs +1 -1
- package/Runtime/Core/Extension/EnumExtensions.cs +176 -1
- package/Runtime/Core/Extension/UnityExtensions.cs +1 -1
- package/Runtime/Core/Helper/Partials/LogHelpers.cs +1 -1
- package/Runtime/Core/Helper/Partials/MathHelpers.cs +1 -1
- package/Runtime/Core/Helper/Partials/ObjectHelpers.cs +3 -4
- package/Runtime/Tags/Attribute.cs +205 -0
- package/Runtime/Tags/Attribute.cs.meta +3 -0
- package/Runtime/Tags/AttributeEffect.cs +276 -0
- package/Runtime/Tags/AttributeEffect.cs.meta +3 -0
- package/Runtime/Tags/AttributeModification.cs +51 -0
- package/Runtime/Tags/AttributeModification.cs.meta +3 -0
- package/Runtime/Tags/AttributeUtilities.cs +209 -0
- package/Runtime/Tags/AttributeUtilities.cs.meta +3 -0
- package/Runtime/Tags/AttributesComponent.cs +163 -0
- package/Runtime/Tags/AttributesComponent.cs.meta +3 -0
- package/Runtime/Tags/CosmeticEffectComponent.cs +50 -0
- package/Runtime/Tags/CosmeticEffectComponent.cs.meta +3 -0
- package/Runtime/Tags/CosmeticEffectData.cs +63 -0
- package/Runtime/Tags/CosmeticEffectData.cs.meta +3 -0
- package/Runtime/Tags/EffectHandle.cs +63 -0
- package/Runtime/Tags/EffectHandle.cs.meta +3 -0
- package/Runtime/Tags/EffectHandler.cs +380 -0
- package/Runtime/Tags/EffectHandler.cs.meta +3 -0
- package/Runtime/Tags/ModificationAction.cs +9 -0
- package/Runtime/Tags/ModificationAction.cs.meta +3 -0
- package/Runtime/Tags/ModifierDurationType.cs +13 -0
- package/Runtime/Tags/ModifierDurationType.cs.meta +3 -0
- package/Runtime/{Utils → Tags}/TagHandler.cs +42 -5
- package/Runtime/Tags.meta +3 -0
- package/Tests/Runtime/DataStructures/BalancedKDTreeTests.cs +1 -1
- package/Tests/Runtime/DataStructures/CyclicBufferTests.cs +1 -1
- package/Tests/Runtime/DataStructures/QuadTreeTests.cs +1 -1
- package/Tests/Runtime/DataStructures/SpatialTreeTests.cs +1 -1
- package/Tests/Runtime/DataStructures/UnbalancedKDTreeTests.cs +1 -1
- package/Tests/Runtime/Extensions/DictionaryExtensionTests.cs +1 -1
- package/Tests/Runtime/Extensions/EnumExtensionTests.cs +1 -1
- package/Tests/Runtime/Extensions/IListExtensionTests.cs +1 -1
- package/Tests/Runtime/Extensions/LoggingExtensionTests.cs +1 -1
- package/Tests/Runtime/Extensions/RandomExtensionTests.cs +1 -1
- package/Tests/Runtime/Extensions/StringExtensionTests.cs +1 -1
- package/Tests/Runtime/Helper/ObjectHelperTests.cs +1 -0
- package/Tests/Runtime/Helper/WallMathTests.cs +1 -1
- package/Tests/Runtime/Performance/KDTreePerformanceTests.cs +1 -1
- package/Tests/Runtime/Performance/QuadTreePerformanceTests.cs +1 -1
- package/Tests/Runtime/Performance/SpatialTreePerformanceTest.cs +1 -1
- package/Tests/Runtime/Performance/UnbalancedKDTreeTests.cs +1 -1
- package/Tests/Runtime/Random/RandomTestBase.cs +2 -2
- package/Tests/Runtime/Serialization/JsonSerializationTest.cs +1 -1
- package/package.json +1 -1
- package/Editor/FitTextureSizeWizard.cs +0 -147
- package/Editor/PrefabCheckWizard.cs +0 -167
- /package/Editor/{FitTextureSizeWizard.cs.meta → FitTextureSizeWindow.cs.meta} +0 -0
- /package/Editor/{PrefabCheckWizard.cs.meta → PrefabChecker.cs.meta} +0 -0
- /package/Runtime/{Utils → Tags}/TagHandler.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
|
+
}
|