com.wallstop-studios.unity-helpers 2.1.0 → 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 (106) hide show
  1. package/AGENTS.md +1 -0
  2. package/Docs/ILIST_SORTING_PERFORMANCE.md +92 -0
  3. package/{package-lock.json.meta → Docs/ILIST_SORTING_PERFORMANCE.md.meta} +1 -1
  4. package/Docs/INDEX.md +11 -1
  5. package/Docs/Images/random_generators.svg +7 -7
  6. package/Docs/RANDOM_PERFORMANCE.md +17 -14
  7. package/Docs/REFLECTION_HELPERS.md +84 -1
  8. package/Docs/REFLECTION_PERFORMANCE.md +169 -0
  9. package/Docs/REFLECTION_PERFORMANCE.md.meta +7 -0
  10. package/Docs/RELATIONAL_COMPONENTS.md +6 -0
  11. package/Docs/RELATIONAL_COMPONENT_PERFORMANCE.md +63 -0
  12. package/Docs/RELATIONAL_COMPONENT_PERFORMANCE.md.meta +7 -0
  13. package/Docs/SPATIAL_TREE_2D_PERFORMANCE.md +64 -64
  14. package/Docs/SPATIAL_TREE_3D_PERFORMANCE.md +64 -64
  15. package/Editor/Core/Helper/AnimationEventHelpers.cs +1 -1
  16. package/Editor/Sprites/AnimationCopier.cs +1 -1
  17. package/Editor/Sprites/AnimationViewerWindow.cs +4 -4
  18. package/Editor/Sprites/SpriteSettingsApplierAPI.cs +2 -1
  19. package/Editor/Sprites/TextureResizerWizard.cs +4 -3
  20. package/Editor/Utils/ScriptableObjectSingletonCreator.cs +3 -3
  21. package/README.md +33 -18
  22. package/Runtime/Core/Attributes/BaseRelationalComponentAttribute.cs +147 -20
  23. package/Runtime/Core/Attributes/ChildComponentAttribute.cs +630 -117
  24. package/Runtime/Core/Attributes/NotNullAttribute.cs +5 -2
  25. package/Runtime/Core/Attributes/ParentComponentAttribute.cs +477 -103
  26. package/Runtime/Core/Attributes/RelationalComponentAssigner.cs +26 -3
  27. package/Runtime/Core/Attributes/RelationalComponentExtensions.cs +19 -3
  28. package/Runtime/Core/Attributes/SiblingComponentAttribute.cs +265 -92
  29. package/Runtime/Core/CodeGen.meta +8 -0
  30. package/Runtime/Core/DataStructure/ImmutableBitSet.cs +5 -20
  31. package/Runtime/Core/Extension/IListExtensions.cs +720 -12
  32. package/Runtime/Core/Helper/Logging/UnityLogTagFormatter.cs +11 -7
  33. package/Runtime/Core/Helper/Objects.cs +1 -1
  34. package/Runtime/Core/Helper/ReflectionHelpers.Factory.cs +5142 -0
  35. package/Runtime/Core/Helper/ReflectionHelpers.Factory.cs.meta +11 -0
  36. package/Runtime/Core/Helper/ReflectionHelpers.cs +1812 -1518
  37. package/Runtime/Core/Helper/UnityMainThreadDispatcher.cs +2 -3
  38. package/Runtime/Core/Math/Line2D.cs +2 -4
  39. package/Runtime/Core/Math/Line3D.cs +2 -4
  40. package/Runtime/Core/Random/AbstractRandom.cs +52 -5
  41. package/Runtime/Core/Random/DotNetRandom.cs +3 -3
  42. package/Runtime/Core/Random/FlurryBurstRandom.cs +279 -0
  43. package/Runtime/Core/Random/FlurryBurstRandom.cs.meta +3 -0
  44. package/Runtime/Core/Random/IllusionFlow.cs +3 -3
  45. package/Runtime/Core/Random/LinearCongruentialGenerator.cs +3 -3
  46. package/Runtime/Core/Random/PcgRandom.cs +6 -6
  47. package/Runtime/Core/Random/PhotonSpinRandom.cs +387 -0
  48. package/Runtime/Core/Random/PhotonSpinRandom.cs.meta +3 -0
  49. package/Runtime/Core/Random/RomuDuo.cs +3 -3
  50. package/Runtime/Core/Random/SplitMix64.cs +3 -3
  51. package/Runtime/Core/Random/SquirrelRandom.cs +6 -4
  52. package/Runtime/Core/Random/StormDropRandom.cs +271 -0
  53. package/Runtime/Core/Random/StormDropRandom.cs.meta +3 -0
  54. package/Runtime/Core/Random/UnityRandom.cs +3 -3
  55. package/Runtime/Core/Random/WyRandom.cs +6 -4
  56. package/Runtime/Core/Random/XorShiftRandom.cs +3 -3
  57. package/Runtime/Core/Random/XoroShiroRandom.cs +3 -3
  58. package/Runtime/Tags/AttributeMetadataCache.cs +316 -9
  59. package/Runtime/Tags/CosmeticEffectData.cs +1 -1
  60. package/Runtime/Visuals/UIToolkit/MultiFileSelectorElement.cs +3 -3
  61. package/Tests/Editor/Helper/HelpersTests.cs +2 -2
  62. package/Tests/Editor/Helper/ReflectionHelpersTypedEditorTests.cs +87 -0
  63. package/Tests/Editor/Helper/ReflectionHelpersTypedEditorTests.cs.meta +11 -0
  64. package/Tests/Editor/Helper/SpriteHelpersTests.cs +1 -1
  65. package/Tests/Editor/PrefabCheckerReportTests.cs +3 -3
  66. package/Tests/Editor/Sprites/AnimationCopierFilterTests.cs +18 -12
  67. package/Tests/Editor/Sprites/AnimationCopierWindowTests.cs +8 -7
  68. package/Tests/Editor/Sprites/AnimationViewerWindowTests.cs +2 -1
  69. package/Tests/Editor/Sprites/ScriptableSpriteAtlasEditorTests.cs +6 -5
  70. package/Tests/Editor/Sprites/SpriteCropperAdditionalTests.cs +2 -1
  71. package/Tests/Editor/Sprites/SpriteCropperTests.cs +7 -6
  72. package/Tests/Editor/Sprites/SpritePivotAdjusterAdditionalTests.cs +2 -1
  73. package/Tests/Editor/Sprites/SpritePivotAdjusterTests.cs +4 -3
  74. package/Tests/Editor/Sprites/TextureResizerWizardTests.cs +10 -9
  75. package/Tests/Editor/Sprites/TextureSettingsApplierAPITests.cs +2 -1
  76. package/Tests/Editor/Tags/AttributeMetadataCacheTests.cs +192 -0
  77. package/Tests/Editor/Tags/AttributeMetadataCacheTests.cs.meta +11 -0
  78. package/Tests/Editor/Tags.meta +8 -0
  79. package/Tests/Runtime/Extensions/IListExtensionTests.cs +187 -1
  80. package/Tests/Runtime/Helper/ObjectsTests.cs +4 -4
  81. package/Tests/Runtime/Helper/ReflectionHelperCapabilityMatrixTests.cs +2923 -0
  82. package/Tests/Runtime/Helper/ReflectionHelperCapabilityMatrixTests.cs.meta +11 -0
  83. package/Tests/Runtime/Helper/ReflectionHelperTests.cs +660 -0
  84. package/Tests/Runtime/Integrations/Reflex/RelationalComponentsReflexTests.cs +2 -2
  85. package/Tests/Runtime/Performance/IListSortingPerformanceTests.cs +346 -0
  86. package/Tests/Runtime/Performance/IListSortingPerformanceTests.cs.meta +11 -0
  87. package/Tests/Runtime/Performance/RandomPerformanceTests.cs +3 -0
  88. package/Tests/Runtime/Performance/ReflectionPerformanceTests.cs +1238 -0
  89. package/Tests/Runtime/Performance/ReflectionPerformanceTests.cs.meta +11 -0
  90. package/Tests/Runtime/Performance/RelationalComponentBenchmarkTests.cs +832 -0
  91. package/Tests/Runtime/Performance/RelationalComponentBenchmarkTests.cs.meta +11 -0
  92. package/Tests/Runtime/Random/FlurryBurstRandomTests.cs +12 -0
  93. package/Tests/Runtime/Random/FlurryBurstRandomTests.cs.meta +3 -0
  94. package/Tests/Runtime/Random/PhotonSpinRandomTests.cs +12 -0
  95. package/Tests/Runtime/Random/PhotonSpinRandomTests.cs.meta +3 -0
  96. package/Tests/Runtime/Random/RandomProtoSerializationTests.cs +14 -0
  97. package/Tests/Runtime/Random/RandomTestBase.cs +39 -4
  98. package/Tests/Runtime/Random/StormDropRandomTests.cs +12 -0
  99. package/Tests/Runtime/Random/StormDropRandomTests.cs.meta +3 -0
  100. package/Tests/Runtime/Serialization/ProtoInterfaceResolutionEdgeTests.cs +2 -2
  101. package/Tests/Runtime/Serialization/ProtoRootRegistrationTests.cs +1 -1
  102. package/Tests/Runtime/Serialization/ProtoSerializeBehaviorTests.cs +1 -1
  103. package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs +2 -2
  104. package/package.json +1 -1
  105. package/Tests/Runtime/Performance/RelationComponentPerformanceTests.cs +0 -60
  106. package/Tests/Runtime/Performance/RelationComponentPerformanceTests.cs.meta +0 -3
@@ -598,8 +598,8 @@ namespace WallstopStudios.UnityHelpers.Editor.Sprites
598
598
  // Prefer Assets-relative paths for UI components that expect them
599
599
  if (!candidate.StartsWith("Assets", StringComparison.OrdinalIgnoreCase))
600
600
  {
601
- string assetsRoot = Application.dataPath.Replace('\\', '/');
602
- string full = candidate.Replace('\\', '/');
601
+ string assetsRoot = Application.dataPath.SanitizePath();
602
+ string full = candidate.SanitizePath();
603
603
  if (full.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase))
604
604
  {
605
605
  candidate = "Assets" + full.Substring(assetsRoot.Length);
@@ -622,10 +622,10 @@ namespace WallstopStudios.UnityHelpers.Editor.Sprites
622
622
  }
623
623
 
624
624
  // Ensure Assets-relative if possible
625
- string path = assetsRelativeDir.Replace('\\', '/');
625
+ string path = assetsRelativeDir.SanitizePath();
626
626
  if (!path.StartsWith("Assets", StringComparison.OrdinalIgnoreCase))
627
627
  {
628
- string assetsRoot = Application.dataPath.Replace('\\', '/');
628
+ string assetsRoot = Application.dataPath.SanitizePath();
629
629
  string full = path;
630
630
  if (full.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase))
631
631
  {
@@ -7,6 +7,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Sprites
7
7
  using System.Text.RegularExpressions;
8
8
  using UnityEditor;
9
9
  using UnityEngine;
10
+ using WallstopStudios.UnityHelpers.Core.Helper;
10
11
 
11
12
  /// <summary>
12
13
  /// Public API to apply SpriteSettings profiles to assets. Mirrors the window logic
@@ -83,7 +84,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Sprites
83
84
 
84
85
  private static string SanitizePath(string p)
85
86
  {
86
- return string.IsNullOrEmpty(p) ? p : p.Replace('\\', '/');
87
+ return string.IsNullOrEmpty(p) ? p : p.SanitizePath();
87
88
  }
88
89
 
89
90
  public static SpriteSettings FindMatchingSettings(
@@ -10,6 +10,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Sprites
10
10
  using UnityEngine;
11
11
  using UnityEngine.Serialization;
12
12
  using WallstopStudios.UnityHelpers.Core.Extension;
13
+ using WallstopStudios.UnityHelpers.Core.Helper;
13
14
  using WallstopStudios.UnityHelpers.Utils;
14
15
  using Object = UnityEngine.Object;
15
16
 
@@ -299,7 +300,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Sprites
299
300
  {
300
301
  string fileName = Path.GetFileName(assetPath);
301
302
  finalAssetPath = Path.Combine(outputDirAssetPath, fileName)
302
- .Replace('\\', '/');
303
+ .SanitizePath();
303
304
  EnsureDirectory(finalAssetPath);
304
305
  }
305
306
 
@@ -420,12 +421,12 @@ namespace WallstopStudios.UnityHelpers.Editor.Sprites
420
421
  0,
421
422
  Application.dataPath.Length - "Assets".Length
422
423
  );
423
- return Path.Combine(projectRoot, assetPath).Replace('\\', '/');
424
+ return Path.Combine(projectRoot, assetPath).SanitizePath();
424
425
  }
425
426
 
426
427
  private static void EnsureDirectory(string assetPath)
427
428
  {
428
- string dirAsset = Path.GetDirectoryName(assetPath)?.Replace('\\', '/');
429
+ string dirAsset = Path.GetDirectoryName(assetPath)?.SanitizePath();
429
430
  if (string.IsNullOrEmpty(dirAsset))
430
431
  {
431
432
  return;
@@ -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
@@ -454,24 +454,27 @@ Already read the [Top 5 Time-Savers](#-top-5-time-savers)? Jump directly to the
454
454
 
455
455
  ### Random Number Generators
456
456
 
457
- Unity Helpers includes **12 high-quality random number generators**, all implementing a rich `IRandom` interface:
457
+ Unity Helpers includes **15 high-quality random number generators**, all implementing a rich `IRandom` interface:
458
458
 
459
459
  #### Available Generators
460
460
 
461
- | Generator | Speed | Quality | Use Case |
462
- | ------------------------------- | --------- | --------- | ---------------------------------------- |
463
- | **IllusionFlow** ⭐ | Fast | Good | Default choice (via PRNG.Instance) |
464
- | **PcgRandom** | Very Fast | Excellent | Deterministic gameplay; explicit seeding |
465
- | **RomuDuo** | Fastest | Good | Maximum performance needed |
466
- | **LinearCongruentialGenerator** | Fastest | Fair | Simple, fast generation |
467
- | **XorShiftRandom** | Very Fast | Good | General purpose |
468
- | **XoroShiroRandom** | Very Fast | Good | General purpose |
469
- | **SplitMix64** | Very Fast | Good | Initialization, hashing |
470
- | **SquirrelRandom** | Moderate | Good | Hash-based generation |
471
- | **WyRandom** | Moderate | Good | Hashing applications |
472
- | **DotNetRandom** | Moderate | Good | .NET compatibility |
473
- | **SystemRandom** | Slow | Good | Backward compatibility |
474
- | **UnityRandom** | Very Slow | Good | Unity compatibility |
461
+ | Generator | Speed | Quality | Use Case |
462
+ | ------------------------------- | --------- | --------- | ------------------------------------------ |
463
+ | **IllusionFlow** ⭐ | Very Fast | Excellent | Default choice (via PRNG.Instance) |
464
+ | **PcgRandom**| Very Fast | Excellent | Deterministic gameplay; explicit seeding |
465
+ | **FlurryBurstRandom** | Very Fast | Excellent | High-quality PCG/Xoshiro alternative |
466
+ | **RomuDuo** | Very Fast | Good | Maximum performance needed |
467
+ | **LinearCongruentialGenerator** | Fastest | Fair | Simple, fast generation |
468
+ | **XorShiftRandom** | Very Fast | Good | General purpose |
469
+ | **XoroShiroRandom** | Very Fast | Good | General purpose |
470
+ | **SplitMix64** | Very Fast | Good | Initialization, hashing |
471
+ | **StormDropRandom** | Fast | Excellent | Large-buffer streams for heavy simulations |
472
+ | **PhotonSpinRandom** | Moderate | Excellent | Bulk generation; long non-overlapping runs |
473
+ | **SquirrelRandom** | Moderate | Good | Hash-based generation |
474
+ | **WyRandom** | Moderate | Good | Hashing applications |
475
+ | **DotNetRandom** | Moderate | Good | .NET compatibility |
476
+ | **SystemRandom** | Slow | Good | Backward compatibility |
477
+ | **UnityRandom** | Very Slow | Good | Unity compatibility |
475
478
 
476
479
  ⭐ **Recommended**: Use `PRNG.Instance` (currently IllusionFlow)
477
480
 
@@ -657,6 +660,7 @@ public class Enemy : MonoBehaviour
657
660
  ```
658
661
 
659
662
  See the in-depth guide: [Relational Components](Docs/RELATIONAL_COMPONENTS.md).
663
+ Performance snapshots: [Relational Component Performance Benchmarks](Docs/RELATIONAL_COMPONENT_PERFORMANCE.md).
660
664
 
661
665
  ---
662
666
 
@@ -1120,9 +1124,16 @@ Unity Helpers is built with performance as a top priority:
1120
1124
 
1121
1125
  **Reflection:**
1122
1126
 
1123
- - IL-emitted delegates 10-100x faster than System.Reflection
1124
- - Safe for IL2CPP and AOT platforms
1125
- - [📊 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)
1131
+
1132
+ **List Sorting:**
1133
+
1134
+ - Multiple adaptive algorithms (`Ghost`, `Meteor`, `Power`, `Grail`, `Pattern-Defeating QuickSort`, `Insertion`) tuned for `IList<T>`
1135
+ - Deterministic datasets (sorted, nearly sorted, shuffled) across sizes from 100 to 1,000,000
1136
+ - [📊 IList Sorting Performance Benchmarks](Docs/ILIST_SORTING_PERFORMANCE.md)
1126
1137
 
1127
1138
  ---
1128
1139
 
@@ -1155,8 +1166,12 @@ Unity Helpers is built with performance as a top priority:
1155
1166
 
1156
1167
  **Performance & Reference:**
1157
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)
1158
1172
  - Random Performance — [Random Performance](Docs/RANDOM_PERFORMANCE.md)
1159
1173
  - Reflection Helpers — [Reflection Helpers](Docs/REFLECTION_HELPERS.md)
1174
+ - IList Sorting Performance — [IList Sorting Performance](Docs/ILIST_SORTING_PERFORMANCE.md)
1160
1175
 
1161
1176
  **Project Info:**
1162
1177
 
@@ -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