com.wallstop-studios.unity-helpers 2.1.1 → 2.1.2

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 (64) hide show
  1. package/AGENTS.md +1 -0
  2. package/Docs/ILIST_SORTING_PERFORMANCE.md +16 -16
  3. package/Docs/INDEX.md +1 -0
  4. package/Docs/RANDOM_PERFORMANCE.md +15 -15
  5. package/Docs/REFLECTION_HELPERS.md +84 -1
  6. package/Docs/REFLECTION_PERFORMANCE.md +169 -0
  7. package/{package-lock.json.meta → Docs/REFLECTION_PERFORMANCE.md.meta} +1 -1
  8. package/Docs/RELATIONAL_COMPONENTS.md +6 -0
  9. package/Docs/RELATIONAL_COMPONENT_PERFORMANCE.md +63 -0
  10. package/Docs/RELATIONAL_COMPONENT_PERFORMANCE.md.meta +7 -0
  11. package/Docs/SPATIAL_TREE_2D_PERFORMANCE.md +64 -64
  12. package/Docs/SPATIAL_TREE_3D_PERFORMANCE.md +64 -64
  13. package/Editor/Sprites/AnimationCopier.cs +1 -1
  14. package/Editor/Sprites/AnimationViewerWindow.cs +4 -4
  15. package/Editor/Sprites/SpriteSettingsApplierAPI.cs +2 -1
  16. package/Editor/Sprites/TextureResizerWizard.cs +4 -3
  17. package/Editor/Utils/ScriptableObjectSingletonCreator.cs +3 -3
  18. package/README.md +8 -3
  19. package/Runtime/Core/Attributes/BaseRelationalComponentAttribute.cs +147 -20
  20. package/Runtime/Core/Attributes/ChildComponentAttribute.cs +630 -117
  21. package/Runtime/Core/Attributes/NotNullAttribute.cs +5 -2
  22. package/Runtime/Core/Attributes/ParentComponentAttribute.cs +477 -103
  23. package/Runtime/Core/Attributes/RelationalComponentAssigner.cs +26 -3
  24. package/Runtime/Core/Attributes/RelationalComponentExtensions.cs +19 -3
  25. package/Runtime/Core/Attributes/SiblingComponentAttribute.cs +265 -92
  26. package/Runtime/Core/CodeGen.meta +8 -0
  27. package/Runtime/Core/DataStructure/ImmutableBitSet.cs +5 -20
  28. package/Runtime/Core/Helper/Logging/UnityLogTagFormatter.cs +11 -7
  29. package/Runtime/Core/Helper/Objects.cs +1 -1
  30. package/Runtime/Core/Helper/ReflectionHelpers.Factory.cs +5142 -0
  31. package/Runtime/Core/Helper/ReflectionHelpers.Factory.cs.meta +11 -0
  32. package/Runtime/Core/Helper/ReflectionHelpers.cs +1812 -1518
  33. package/Runtime/Core/Math/Line2D.cs +2 -4
  34. package/Runtime/Core/Math/Line3D.cs +2 -4
  35. package/Runtime/Core/Random/FlurryBurstRandom.cs +0 -6
  36. package/Runtime/Tags/AttributeMetadataCache.cs +4 -6
  37. package/Runtime/Tags/CosmeticEffectData.cs +1 -1
  38. package/Runtime/Visuals/UIToolkit/MultiFileSelectorElement.cs +3 -3
  39. package/Tests/Editor/Helper/HelpersTests.cs +2 -2
  40. package/Tests/Editor/Helper/ReflectionHelpersTypedEditorTests.cs +87 -0
  41. package/Tests/Editor/Helper/ReflectionHelpersTypedEditorTests.cs.meta +11 -0
  42. package/Tests/Editor/Helper/SpriteHelpersTests.cs +1 -1
  43. package/Tests/Editor/PrefabCheckerReportTests.cs +3 -3
  44. package/Tests/Editor/Sprites/AnimationCopierFilterTests.cs +18 -12
  45. package/Tests/Editor/Sprites/AnimationCopierWindowTests.cs +8 -7
  46. package/Tests/Editor/Sprites/AnimationViewerWindowTests.cs +2 -1
  47. package/Tests/Editor/Sprites/ScriptableSpriteAtlasEditorTests.cs +6 -5
  48. package/Tests/Editor/Sprites/SpriteCropperAdditionalTests.cs +2 -1
  49. package/Tests/Editor/Sprites/SpriteCropperTests.cs +7 -6
  50. package/Tests/Editor/Sprites/SpritePivotAdjusterAdditionalTests.cs +2 -1
  51. package/Tests/Editor/Sprites/SpritePivotAdjusterTests.cs +4 -3
  52. package/Tests/Editor/Sprites/TextureResizerWizardTests.cs +10 -9
  53. package/Tests/Editor/Sprites/TextureSettingsApplierAPITests.cs +2 -1
  54. package/Tests/Runtime/Helper/ObjectsTests.cs +1 -1
  55. package/Tests/Runtime/Helper/ReflectionHelperCapabilityMatrixTests.cs +2923 -0
  56. package/Tests/Runtime/Helper/ReflectionHelperCapabilityMatrixTests.cs.meta +11 -0
  57. package/Tests/Runtime/Helper/ReflectionHelperTests.cs +660 -0
  58. package/Tests/Runtime/Performance/ReflectionPerformanceTests.cs +1238 -0
  59. package/Tests/Runtime/Performance/ReflectionPerformanceTests.cs.meta +11 -0
  60. package/Tests/Runtime/Performance/RelationalComponentBenchmarkTests.cs +832 -0
  61. package/Tests/Runtime/Performance/RelationalComponentBenchmarkTests.cs.meta +11 -0
  62. package/package.json +1 -1
  63. package/Tests/Runtime/Performance/RelationComponentPerformanceTests.cs +0 -60
  64. package/Tests/Runtime/Performance/RelationComponentPerformanceTests.cs.meta +0 -3
@@ -214,7 +214,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
214
214
  string assetName = Path.GetFileName(targetAssetPath);
215
215
 
216
216
  // Ensure the target parent folder exists and use its exact-cased path
217
- string targetParent = Path.GetDirectoryName(normalizedTarget)?.Replace('\\', '/');
217
+ string targetParent = Path.GetDirectoryName(normalizedTarget)?.SanitizePath();
218
218
  if (!string.IsNullOrWhiteSpace(targetParent))
219
219
  {
220
220
  string resolvedParent = EnsureAndResolveFolderPath(targetParent);
@@ -303,7 +303,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
303
303
  }
304
304
 
305
305
  // Final guard: ensure parent exists just before moving
306
- string parent = Path.GetDirectoryName(normalizedTarget)?.Replace('\\', '/');
306
+ string parent = Path.GetDirectoryName(normalizedTarget)?.SanitizePath();
307
307
  if (!string.IsNullOrWhiteSpace(parent) && !AssetDatabase.IsValidFolder(parent))
308
308
  {
309
309
  string ensured = EnsureAndResolveFolderPath(parent);
@@ -324,7 +324,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
324
324
  }
325
325
 
326
326
  // Retry after ensuring parent and performing save/refresh if parent folder may not yet be registered
327
- string parentDir = Path.GetDirectoryName(normalizedTarget)?.Replace('\\', '/');
327
+ string parentDir = Path.GetDirectoryName(normalizedTarget)?.SanitizePath();
328
328
  bool retried = false;
329
329
  if (!string.IsNullOrWhiteSpace(parentDir))
330
330
  {
package/README.md CHANGED
@@ -660,6 +660,7 @@ public class Enemy : MonoBehaviour
660
660
  ```
661
661
 
662
662
  See the in-depth guide: [Relational Components](Docs/RELATIONAL_COMPONENTS.md).
663
+ Performance snapshots: [Relational Component Performance Benchmarks](Docs/RELATIONAL_COMPONENT_PERFORMANCE.md).
663
664
 
664
665
  ---
665
666
 
@@ -1123,9 +1124,10 @@ Unity Helpers is built with performance as a top priority:
1123
1124
 
1124
1125
  **Reflection:**
1125
1126
 
1126
- - IL-emitted delegates 10-100x faster than System.Reflection
1127
- - Safe for IL2CPP and AOT platforms
1128
- - [📊 Reflection Performance](Docs/REFLECTION_HELPERS.md)
1127
+ - Cached delegates are 10-100x faster than raw `System.Reflection` (boxed scenarios improve the most)
1128
+ - Safe for IL2CPP and AOT platforms; capability overrides (`ReflectionHelpers.OverrideReflectionCapabilities`) let tests force expression/IL fallbacks
1129
+ - Run the benchmarks via **ReflectionPerformanceTests.Benchmark** (EditMode Test Runner) and commit the updated markdown section
1130
+ - [📘 Reflection Performance Guide](Docs/ReflectionPerformance.md) and [📊 Benchmarks](Docs/REFLECTION_PERFORMANCE.md)
1129
1131
 
1130
1132
  **List Sorting:**
1131
1133
 
@@ -1164,6 +1166,9 @@ Unity Helpers is built with performance as a top priority:
1164
1166
 
1165
1167
  **Performance & Reference:**
1166
1168
 
1169
+ - Reflection Performance Guide — [Reflection Performance](Docs/ReflectionPerformance.md)
1170
+ - Reflection AOT/Burst Validation — [IL2CPP & Burst Validation](Docs/ReflectionAotBurstValidation.md)
1171
+ - Reflection Benchmark CI Proposal — [Benchmark CI Proposal](Docs/ReflectionBenchmarkCIProposal.md)
1167
1172
  - Random Performance — [Random Performance](Docs/RANDOM_PERFORMANCE.md)
1168
1173
  - Reflection Helpers — [Reflection Helpers](Docs/REFLECTION_HELPERS.md)
1169
1174
  - IList Sorting Performance — [IList Sorting Performance](Docs/ILIST_SORTING_PERFORMANCE.md)
@@ -84,6 +84,12 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
84
84
  /// </summary>
85
85
  internal static class RelationalComponentProcessor
86
86
  {
87
+ private static readonly MethodInfo CreateFieldAccessorGenericMethod =
88
+ typeof(RelationalComponentProcessor).GetMethod(
89
+ nameof(CreateFieldAccessorGeneric),
90
+ BindingFlags.NonPublic | BindingFlags.Static
91
+ );
92
+
87
93
  internal enum FieldKind : byte
88
94
  {
89
95
  Single = 0,
@@ -168,42 +174,135 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
168
174
  {
169
175
  public readonly FieldInfo field;
170
176
  public readonly TAttribute attribute;
171
- public readonly Action<object, object> setter;
172
- public readonly Func<object, object> getter;
177
+ private readonly FieldAccessor accessor;
178
+ private readonly FilterParameters filters;
173
179
  public readonly FieldKind kind;
174
180
  public readonly Type elementType;
175
181
  public readonly Func<int, Array> arrayCreator;
176
182
  public readonly Func<int, IList> listCreator;
177
183
  public readonly Func<int, object> hashSetCreator;
178
184
  public readonly Action<object, object> hashSetAdder;
185
+ public readonly Action<object> hashSetClearer;
179
186
  public readonly bool isInterface;
180
187
 
181
188
  public FieldMetadata(
182
189
  FieldInfo field,
183
190
  TAttribute attribute,
184
- Action<object, object> setter,
185
- Func<object, object> getter,
191
+ FilterParameters filters,
192
+ FieldAccessor accessor,
186
193
  FieldKind kind,
187
194
  Type elementType,
188
195
  Func<int, Array> arrayCreator,
189
196
  Func<int, IList> listCreator,
190
197
  Func<int, object> hashSetCreator,
191
198
  Action<object, object> hashSetAdder,
199
+ Action<object> hashSetClearer,
192
200
  bool isInterface
193
201
  )
194
202
  {
195
203
  this.field = field;
196
204
  this.attribute = attribute;
197
- this.setter = setter;
198
- this.getter = getter;
205
+ this.accessor = accessor ?? FieldAccessor.Null;
206
+ this.filters = filters;
199
207
  this.kind = kind;
200
208
  this.elementType = elementType;
201
209
  this.arrayCreator = arrayCreator;
202
210
  this.listCreator = listCreator;
203
211
  this.hashSetCreator = hashSetCreator;
204
212
  this.hashSetAdder = hashSetAdder;
213
+ this.hashSetClearer = hashSetClearer;
205
214
  this.isInterface = isInterface;
206
215
  }
216
+
217
+ public bool HasFilters => this.filters.RequiresPostProcessing;
218
+
219
+ public FilterParameters Filters => this.filters;
220
+
221
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
222
+ public object GetValue(Component component)
223
+ {
224
+ return this.accessor.Get(component);
225
+ }
226
+
227
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
228
+ public void SetValue(Component component, object value)
229
+ {
230
+ this.accessor.Set(component, value);
231
+ }
232
+ }
233
+
234
+ internal abstract class FieldAccessor
235
+ {
236
+ public static readonly FieldAccessor Null = new NullFieldAccessor();
237
+
238
+ public abstract object Get(Component component);
239
+ public abstract void Set(Component component, object value);
240
+
241
+ private sealed class NullFieldAccessor : FieldAccessor
242
+ {
243
+ public override object Get(Component component)
244
+ {
245
+ return null;
246
+ }
247
+
248
+ public override void Set(Component component, object value) { }
249
+ }
250
+ }
251
+
252
+ private sealed class FieldAccessor<TComponent, TValue> : FieldAccessor
253
+ where TComponent : Component
254
+ {
255
+ private readonly FieldSetter<TComponent, TValue> setter;
256
+ private readonly Func<TComponent, TValue> getter;
257
+
258
+ public FieldAccessor(FieldInfo field)
259
+ {
260
+ this.setter = ReflectionHelpers.GetFieldSetter<TComponent, TValue>(field);
261
+ this.getter = ReflectionHelpers.GetFieldGetter<TComponent, TValue>(field);
262
+ }
263
+
264
+ public override object Get(Component component)
265
+ {
266
+ if (component == null)
267
+ {
268
+ return null;
269
+ }
270
+
271
+ TComponent typedComponent = (TComponent)component;
272
+ return this.getter(typedComponent);
273
+ }
274
+
275
+ public override void Set(Component component, object value)
276
+ {
277
+ if (component == null)
278
+ {
279
+ return;
280
+ }
281
+
282
+ TComponent typedComponent = (TComponent)component;
283
+ TValue typedValue = value != null ? (TValue)value : default;
284
+ this.setter(ref typedComponent, typedValue);
285
+ }
286
+ }
287
+
288
+ private static FieldAccessor CreateFieldAccessor(Type componentType, FieldInfo field)
289
+ {
290
+ if (componentType == null || !typeof(Component).IsAssignableFrom(componentType))
291
+ {
292
+ return FieldAccessor.Null;
293
+ }
294
+
295
+ MethodInfo generic = CreateFieldAccessorGenericMethod.MakeGenericMethod(
296
+ componentType,
297
+ field.FieldType
298
+ );
299
+ return (FieldAccessor)generic.Invoke(null, new object[] { field });
300
+ }
301
+
302
+ private static FieldAccessor CreateFieldAccessorGeneric<TComponent, TValue>(FieldInfo field)
303
+ where TComponent : Component
304
+ {
305
+ return new FieldAccessor<TComponent, TValue>(field);
207
306
  }
208
307
 
209
308
  internal static FieldMetadata<TAttribute>[] GetFieldMetadata<TAttribute>(Type componentType)
@@ -275,6 +374,7 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
275
374
  Func<int, IList> listCreator = null;
276
375
  Func<int, object> hashSetCreator = null;
277
376
  Action<object, object> hashSetAdder = null;
377
+ Action<object> hashSetClearer = null;
278
378
 
279
379
  switch (kind)
280
380
  {
@@ -291,6 +391,9 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
291
391
  resolvedElementType
292
392
  );
293
393
  hashSetAdder = ReflectionHelpers.GetHashSetAdder(resolvedElementType);
394
+ hashSetClearer = ReflectionHelpers.GetHashSetClearer(
395
+ resolvedElementType
396
+ );
294
397
  break;
295
398
  }
296
399
 
@@ -304,18 +407,21 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
304
407
  )
305
408
  );
306
409
 
410
+ FilterParameters filters = new(attribute);
411
+
307
412
  result.Add(
308
413
  new FieldMetadata<TAttribute>(
309
414
  field,
310
415
  attribute,
311
- ReflectionHelpers.GetFieldSetter(field),
312
- ReflectionHelpers.GetFieldGetter(field),
416
+ filters,
417
+ CreateFieldAccessor(componentType, field),
313
418
  kind,
314
419
  resolvedElementType,
315
420
  arrayCreator,
316
421
  listCreator,
317
422
  hashSetCreator,
318
423
  hashSetAdder,
424
+ hashSetClearer,
319
425
  isInterface
320
426
  )
321
427
  );
@@ -343,6 +449,7 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
343
449
  Func<int, IList> listCreator = null;
344
450
  Func<int, object> hashSetCreator = null;
345
451
  Action<object, object> hashSetAdder = null;
452
+ Action<object> hashSetClearer = null;
346
453
 
347
454
  switch (kind)
348
455
  {
@@ -357,6 +464,7 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
357
464
  elementType
358
465
  );
359
466
  hashSetAdder = ReflectionHelpers.GetHashSetAdder(elementType);
467
+ hashSetClearer = ReflectionHelpers.GetHashSetClearer(elementType);
360
468
  break;
361
469
  }
362
470
 
@@ -367,18 +475,21 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
367
475
  || (!elementType.IsSealed && elementType != typeof(Component))
368
476
  );
369
477
 
478
+ FilterParameters filters = new(attribute);
479
+
370
480
  return (FieldMetadata<TAttribute>?)
371
481
  new FieldMetadata<TAttribute>(
372
482
  field,
373
483
  attribute,
374
- ReflectionHelpers.GetFieldSetter(field),
375
- ReflectionHelpers.GetFieldGetter(field),
484
+ filters,
485
+ CreateFieldAccessor(componentType, field),
376
486
  kind,
377
487
  elementType,
378
488
  arrayCreator,
379
489
  listCreator,
380
490
  hashSetCreator,
381
491
  hashSetAdder,
492
+ hashSetClearer,
382
493
  isInterface
383
494
  );
384
495
  })
@@ -414,7 +525,7 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
414
525
 
415
526
  internal static bool ShouldSkipAssignment<TAttribute>(
416
527
  FieldMetadata<TAttribute> metadata,
417
- object component
528
+ Component component
418
529
  )
419
530
  where TAttribute : BaseRelationalComponentAttribute
420
531
  {
@@ -423,7 +534,7 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
423
534
  return false;
424
535
  }
425
536
 
426
- object currentValue = metadata.getter(component);
537
+ object currentValue = metadata.GetValue(component);
427
538
 
428
539
  return ValueHelpers.IsAssigned(currentValue);
429
540
  }
@@ -453,26 +564,42 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
453
564
  {
454
565
  case FieldKind.Array:
455
566
 
456
- metadata.setter(component, metadata.arrayCreator(0));
567
+ metadata.SetValue(component, metadata.arrayCreator(0));
457
568
 
458
569
  break;
459
570
 
460
571
  case FieldKind.List:
461
-
462
- metadata.setter(component, metadata.listCreator(0));
463
-
572
+ {
573
+ object existing = metadata.GetValue(component);
574
+ if (existing is IList list)
575
+ {
576
+ list.Clear();
577
+ }
578
+ else
579
+ {
580
+ metadata.SetValue(component, metadata.listCreator(0));
581
+ }
582
+ }
464
583
  break;
465
584
 
466
585
  case FieldKind.HashSet:
467
-
468
- metadata.setter(component, metadata.hashSetCreator(0));
469
-
586
+ {
587
+ object existing = metadata.GetValue(component);
588
+ if (existing != null && metadata.hashSetClearer != null)
589
+ {
590
+ metadata.hashSetClearer(existing);
591
+ }
592
+ else
593
+ {
594
+ metadata.SetValue(component, metadata.hashSetCreator(0));
595
+ }
596
+ }
470
597
  break;
471
598
  }
472
599
  }
473
600
 
474
601
  [MethodImpl(MethodImplOptions.AggressiveInlining)]
475
- private static bool PassesStateAndFilters(
602
+ internal static bool PassesStateAndFilters(
476
603
  Component candidate,
477
604
  FilterParameters filters,
478
605
  bool filterDisabledComponents = true