com.wallstop-studios.unity-helpers 2.0.0-rc81.9 → 2.0.0

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 (168) hide show
  1. package/.editorconfig +1 -1
  2. package/.gitattributes +1 -1
  3. package/.githooks/pre-commit +31 -5
  4. package/.githooks/pre-push +50 -0
  5. package/.github/dependabot.yml +24 -2
  6. package/.github/scripts/check-markdown-links.ps1 +77 -0
  7. package/.github/scripts/check_markdown_links.py +89 -0
  8. package/.github/scripts/check_markdown_url_encoding.py +74 -0
  9. package/.github/scripts/validate_markdown_links.py +194 -0
  10. package/.github/workflows/csharpier-autofix.yml +152 -0
  11. package/.github/workflows/format-on-demand.yml +305 -0
  12. package/.github/workflows/lint-doc-links.yml +8 -5
  13. package/.github/workflows/markdown-json.yml +6 -2
  14. package/.github/workflows/prettier-autofix.yml +195 -0
  15. package/.github/workflows/update-dotnet-tools.yml +80 -0
  16. package/.github/workflows/yaml-format-lint.yml +41 -0
  17. package/.lychee.toml +4 -4
  18. package/.markdownlint.jsonc +21 -0
  19. package/.pre-commit-config.yaml +11 -3
  20. package/.yamllint.yaml +31 -0
  21. package/AGENTS.md +5 -1
  22. package/CHANGELOG.md +11 -0
  23. package/CONTRIBUTING.md +49 -0
  24. package/CONTRIBUTING.md.meta +7 -0
  25. package/EDITOR_TOOLS_GUIDE.md +4 -0
  26. package/Editor/AnimationEventEditor.cs +337 -160
  27. package/Editor/Core/Helper/AnimationEventHelpers.cs +178 -152
  28. package/Editor/CustomEditors/PersistentDirectoryGUI.cs +20 -11
  29. package/Editor/CustomEditors/TexturePlatformOverrideEntryDrawer.cs +11 -2
  30. package/Editor/FitTextureSizeWindow.cs +43 -19
  31. package/Editor/PersistentDirectorySettings.cs +64 -12
  32. package/Editor/PrefabChecker.cs +72 -5
  33. package/Editor/Sprites/AnimationCopier.cs +132 -56
  34. package/Editor/Sprites/AnimationCreator.cs +63 -22
  35. package/Editor/Sprites/AnimationViewerWindow.cs +42 -6
  36. package/Editor/Sprites/TexturePlatformNameHelper.cs +50 -39
  37. package/Editor/Sprites/TextureResizerWizard.cs +23 -1
  38. package/Editor/Sprites/TextureSettingsApplierWindow.cs +148 -85
  39. package/Editor/Tools/ImageBlurTool.cs +81 -10
  40. package/Editor/Utils/EditorUi.cs +1 -1
  41. package/Editor/Utils/ScriptableObjectSingletonCreator.cs +1 -1
  42. package/GETTING_STARTED.md +40 -56
  43. package/RANDOM_PERFORMANCE.md +12 -12
  44. package/README.md +395 -2407
  45. package/RELATIONAL_COMPONENTS.md +92 -83
  46. package/Runtime/AssemblyInfo.cs +2 -0
  47. package/Runtime/Core/Attributes/NotNullAttribute.cs +1 -3
  48. package/Runtime/Core/Attributes/RelationalComponentAssigner.cs +50 -5
  49. package/Runtime/Core/DataStructure/CyclicBuffer.cs +0 -1
  50. package/Runtime/Core/Extension/RandomExtensions.cs +68 -0
  51. package/Runtime/Core/Extension/WallstopStudiosLogger.cs +16 -0
  52. package/Runtime/Core/Helper/Partials/ObjectHelpers.cs +4 -1
  53. package/Runtime/Core/Helper/ReflectionHelpers.cs +21 -10
  54. package/Runtime/Core/Helper/SpriteHelpers.cs +3 -1
  55. package/Runtime/Core/Helper/UnityMainThreadDispatcher.cs +45 -1
  56. package/Runtime/Core/Serialization/JsonConverters/GameObjectConverter.cs +13 -5
  57. package/Runtime/Core/Serialization/JsonConverters/ResolutionConverter.cs +1 -1
  58. package/Runtime/Core/Serialization/JsonConverters/TypeConverter.cs +1 -1
  59. package/Runtime/Core/Serialization/Serializer.cs +101 -0
  60. package/Runtime/Integrations/VContainer/AssemblyInfo.cs +9 -0
  61. package/Runtime/Integrations/VContainer/AssemblyInfo.cs.meta +3 -0
  62. package/Runtime/Integrations/VContainer/ObjectResolverRelationalExtensions.cs +96 -0
  63. package/Runtime/Integrations/VContainer/RelationalComponentEntryPoint.cs +90 -10
  64. package/Runtime/Integrations/VContainer/RelationalComponentsBuilderExtensions.cs +13 -1
  65. package/Runtime/Integrations/VContainer/RelationalObjectPools.cs +114 -0
  66. package/Runtime/Integrations/VContainer/RelationalObjectPools.cs.meta +11 -0
  67. package/Runtime/Integrations/VContainer/RelationalSceneAssignmentOptions.cs +16 -4
  68. package/Runtime/Integrations/VContainer/RelationalSceneLoadListener.cs +241 -0
  69. package/Runtime/Integrations/VContainer/RelationalSceneLoadListener.cs.meta +11 -0
  70. package/Runtime/Integrations/Zenject/AssemblyInfo.cs +9 -0
  71. package/Runtime/Integrations/Zenject/AssemblyInfo.cs.meta +3 -0
  72. package/Runtime/Integrations/Zenject/DiContainerRelationalExtensions.cs +69 -2
  73. package/Runtime/Integrations/Zenject/RelationalComponentSceneInitializer.cs +89 -12
  74. package/Runtime/Integrations/Zenject/RelationalComponentsInstaller.cs +23 -1
  75. package/Runtime/Integrations/Zenject/RelationalMemoryPools.cs +44 -0
  76. package/Runtime/Integrations/Zenject/RelationalMemoryPools.cs.meta +11 -0
  77. package/Runtime/Integrations/Zenject/RelationalSceneAssignmentOptions.cs +16 -10
  78. package/Runtime/Integrations/Zenject/RelationalSceneLoadListener.cs +243 -0
  79. package/Runtime/Integrations/Zenject/RelationalSceneLoadListener.cs.meta +11 -0
  80. package/Runtime/Tags/AttributeMetadataCache.cs +1 -4
  81. package/Runtime/Utils/Buffers.cs +4 -4
  82. package/Runtime/Utils/ScriptableObjectSingleton.cs +0 -1
  83. package/Runtime/Utils/SetTextureImportData.cs +3 -1
  84. package/Runtime/Utils/TextureScale.cs +10 -2
  85. package/Runtime/Visuals/UGUI/EnhancedImage.cs +6 -0
  86. package/Runtime/Visuals/UIToolkit/LayeredImage.cs +4 -1
  87. package/SERIALIZATION.md +15 -0
  88. package/SPATIAL_TREE_2D_PERFORMANCE.md +85 -82
  89. package/SPATIAL_TREE_3D_PERFORMANCE.md +94 -91
  90. package/Samples~/DI - VContainer/README.md +232 -51
  91. package/Samples~/DI - VContainer/Scripts/GameLifetimeScope.cs +22 -4
  92. package/Samples~/DI - VContainer/Scripts/RelationalConsumer.cs +5 -2
  93. package/Samples~/DI - VContainer/Scripts/Spawner.cs +113 -4
  94. package/Samples~/DI - Zenject/README.md +217 -53
  95. package/Samples~/DI - Zenject/Scripts/RelationalConsumer.cs +3 -0
  96. package/Samples~/DI - Zenject/Scripts/RelationalConsumerPool.cs +37 -0
  97. package/Samples~/DI - Zenject/Scripts/RelationalConsumerPool.cs.meta +12 -0
  98. package/Samples~/DI - Zenject/Scripts/SpawnerZenject.cs +74 -3
  99. package/Samples~/Random - PRNG/README.md +2 -1
  100. package/Samples~/Relational Components - Basic/README.md +3 -1
  101. package/Samples~/Serialization - JSON/README.md +2 -1
  102. package/Samples~/Spatial Structures - 2D and 3D/README.md +2 -1
  103. package/Samples~/UGUI - EnhancedImage/README.md +2 -1
  104. package/Samples~/UI Toolkit - MultiFile Selector (Editor)/README.md +2 -1
  105. package/THIRD_PARTY_NOTICES.md +1 -1
  106. package/Tests/Editor/Attributes/AnimationEventHelpersTests.cs +16 -0
  107. package/Tests/Editor/Core/Attributes/RelationalComponentAssignerTests.cs +3 -3
  108. package/Tests/Editor/Integrations/VContainer/VContainerRelationalEntryPointTests.cs +6 -2
  109. package/Tests/Editor/Integrations/VContainer/VContainerRelationalHelpersTests.cs +170 -0
  110. package/Tests/Editor/Integrations/VContainer/VContainerRelationalHelpersTests.cs.meta +11 -0
  111. package/Tests/Editor/Integrations/VContainer/WallstopStudios.UnityHelpers.Tests.Editor.VContainer.asmdef +2 -1
  112. package/Tests/Editor/Integrations/Zenject/WallstopStudios.UnityHelpers.Tests.Editor.Zenject.asmdef +3 -2
  113. package/Tests/Editor/Integrations/Zenject/ZenjectRelationalHelpersTests.cs +131 -0
  114. package/Tests/Editor/Integrations/Zenject/ZenjectRelationalHelpersTests.cs.meta +11 -0
  115. package/Tests/Editor/Integrations/Zenject/ZenjectRelationalInitializerTests.cs +6 -2
  116. package/Tests/Editor/PersistentDirectorySettingsTests.cs +59 -0
  117. package/Tests/Editor/PersistentDirectorySettingsTests.cs.meta +11 -0
  118. package/Tests/Editor/PrefabCheckerReportTests.cs +32 -0
  119. package/Tests/Editor/PrefabCheckerReportTests.cs.meta +11 -0
  120. package/Tests/Editor/Sprites/AnimationCopierFilterTests.cs +64 -0
  121. package/Tests/Editor/Sprites/AnimationCopierFilterTests.cs.meta +11 -0
  122. package/Tests/Editor/Sprites/AnimationCopierWindowTests.cs +1 -1
  123. package/Tests/Editor/Sprites/AnimationViewerWindowTests.cs +38 -0
  124. package/Tests/Editor/Sprites/AnimationViewerWindowTests.cs.meta +11 -0
  125. package/Tests/Editor/Sprites/ScriptableSpriteAtlasEditorTests.cs +1 -1
  126. package/Tests/Editor/Sprites/SpriteCropperAdditionalTests.cs +12 -12
  127. package/Tests/Editor/Sprites/SpriteCropperTests.cs +9 -9
  128. package/Tests/Editor/Sprites/SpritePivotAdjusterTests.cs +3 -3
  129. package/Tests/Editor/Sprites/TexturePlatformNameHelperTests.cs +18 -0
  130. package/Tests/Editor/Sprites/TextureResizerWizardTests.cs +5 -5
  131. package/Tests/Editor/Sprites/TextureSettingsApplierAPITests.cs +3 -3
  132. package/Tests/Editor/Sprites/TextureSettingsApplierWizardAdditionalTests.cs +4 -4
  133. package/Tests/Editor/Sprites/TextureSettingsApplierWizardTests.cs +4 -4
  134. package/Tests/Editor/Tools/ImageBlurToolTests.cs +22 -110
  135. package/Tests/Editor/Utils/CommonTestBase.cs +43 -1
  136. package/Tests/Editor/Utils/ScriptableObjectSingletonCreatorTests.cs +5 -5
  137. package/Tests/Editor/Windows/FitTextureSizeWindowTests.cs +66 -74
  138. package/Tests/Runtime/Attributes/RelationalComponentInitializerTests.cs +4 -15
  139. package/Tests/Runtime/DataStructures/SpatialTree3DBoundsConsistencyTests.cs +29 -29
  140. package/Tests/Runtime/Integrations/VContainer/RelationalComponentsVContainerTests.cs +259 -218
  141. package/Tests/Runtime/Integrations/VContainer/RelationalObjectPoolsVContainerTests.cs +86 -0
  142. package/Tests/Runtime/Integrations/VContainer/RelationalObjectPoolsVContainerTests.cs.meta +11 -0
  143. package/Tests/Runtime/Integrations/Zenject/RelationalComponentsZenjectTests.cs +255 -227
  144. package/Tests/Runtime/Performance/SpatialTree2DPerformanceTests.cs +5 -0
  145. package/Tests/Runtime/Performance/SpatialTree3DPerformanceTests.cs +3 -0
  146. package/Tests/Runtime/Serialization/JsonConverterAdditionalTests.cs +30 -0
  147. package/Tests/Runtime/Serialization/JsonConverterAdditionalTests.cs.meta +11 -0
  148. package/Tests/Runtime/Serialization/JsonConverterTests.cs +8 -12
  149. package/Tests/Runtime/Serialization/JsonSerializationTest.cs +16 -5
  150. package/Tests/Runtime/Serialization/SerializerAdditionalTests.cs +12 -0
  151. package/Tests/Runtime/Serialization/SerializerFileIoTests.cs +105 -0
  152. package/Tests/Runtime/Serialization/SerializerFileIoTests.cs.meta +11 -0
  153. package/Tests/Runtime/Serialization/UnityEngineObjectJsonTests.cs +247 -0
  154. package/Tests/Runtime/Serialization/UnityEngineObjectJsonTests.cs.meta +11 -0
  155. package/Tests/Runtime/TestUtils/CommonTestBase.cs +88 -0
  156. package/Tests/Runtime/Utils/CoroutineHandlerTests.cs +1 -1
  157. package/Tests/Runtime/Utils/LZMAComprehensiveTests.cs +1 -1
  158. package/Tests/Runtime/Utils/LZMATests.cs +1 -1
  159. package/Tests/Runtime/Utils/MatchColliderToSpriteTests.cs +1 -1
  160. package/Tests/Runtime/Visuals/EnhancedImageTests.cs +25 -56
  161. package/Tests/Runtime/Visuals/VisualsTestHelpers.cs +1 -8
  162. package/package-lock.json.meta +7 -0
  163. package/package.json +8 -4
  164. package/scripts/check-eol.ps1 +4 -5
  165. package/scripts/lint-tests.ps1 +156 -0
  166. package/scripts/lint-tests.ps1.meta +7 -0
  167. package/scripts/normalize-eol.ps1 +6 -9
  168. package/.github/workflows/csharpier.yml +0 -135
@@ -314,114 +314,102 @@ Notes:
314
314
 
315
315
  ## Dependency Injection Integrations
316
316
 
317
- Unity Helpers provides optional integration assemblies that only compile when Zenject or VContainer is present in your project. Install the corresponding DI package via the Unity Package Manager and the helpers become available automatically (no additional scripting defines required).
317
+ **Stop choosing between DI and clean hierarchy references** - Unity Helpers provides seamless integrations with Zenject and VContainer that automatically wire up your relational component fields right after dependency injection completes.
318
318
 
319
- Why use the DI integrations
319
+ ### The DI Pain Point
320
320
 
321
- - Eliminate boilerplate: hydrate relational fields after DI injection automatically.
322
- - Consistent behavior: integrates with constructor/property injection and works with runtime instantiation.
323
- - Safe fallback: if the DI binding is missing, falls back to the non-DI path so fields still populate.
321
+ Without these integrations, you're stuck writing `Awake()` methods full of `GetComponent` boilerplate **even when using a DI framework**:
324
322
 
325
- Supported package IDs (auto-detected)
326
-
327
- - Zenject/Extenject: `com.extenject.zenject`, `com.modesttree.zenject`, `com.svermeulen.extenject`
328
- - VContainer: `jp.cysharp.vcontainer`, `jp.hadashikick.vcontainer`
323
+ ```csharp
324
+ public class Enemy : MonoBehaviour
325
+ {
326
+ [Inject] private IHealthSystem _health; // ✅ DI handles this
329
327
 
330
- Manual or source imports (no UPM)
328
+ private Animator _animator; // Still manual boilerplate
329
+ private Rigidbody2D _rigidbody; // ❌ Still manual boilerplate
331
330
 
332
- - If you import Zenject/VContainer as source, a .unitypackage, or a plain DLL, Unity cannot infer package IDs and the `versionDefines` in the asmdefs won’t trigger.
333
- - Add scripting defines in Project Settings to enable the integrations:
334
- - `Project Settings > Player > Other Settings > Scripting Define Symbols`
335
- - Add `ZENJECT_PRESENT` when Zenject/Extenject is present and/or `VCONTAINER_PRESENT` when VContainer is present.
336
- - Set defines per target platform (Standalone, Android, iOS, etc.).
337
- - After adding, Unity recompiles and the optional assemblies under `Runtime/Integrations/*` compile automatically.
331
+ void Awake()
332
+ {
333
+ _animator = GetComponent<Animator>();
334
+ _rigidbody = GetComponent<Rigidbody2D>();
335
+ // ... 15 more lines of GetComponent hell
336
+ }
337
+ }
338
+ ```
338
339
 
339
- ### Quick Start — VContainer
340
+ ### The Integration Solution
340
341
 
341
- 1. Register integration in your `LifetimeScope`
342
+ With the DI integrations, **everything just works**:
342
343
 
343
344
  ```csharp
344
- using VContainer;
345
- using VContainer.Unity;
346
- using WallstopStudios.UnityHelpers.Integrations.VContainer;
347
-
348
- public sealed class GameLifetimeScope : LifetimeScope
345
+ public class Enemy : MonoBehaviour
349
346
  {
350
- protected override void Configure(IContainerBuilder builder)
351
- {
352
- // Registers IRelationalComponentAssigner and a scene entry point
353
- builder.RegisterRelationalComponents();
347
+ [Inject] private IHealthSystem _health; // ✅ DI injection
348
+ [SiblingComponent] private Animator _animator; // ✅ Relational auto-wiring
349
+ [SiblingComponent] private Rigidbody2D _rigidbody; // Relational auto-wiring
354
350
 
355
- // Or customize scanning (active objects only)
356
- // builder.RegisterRelationalComponents(new RelationalSceneAssignmentOptions(includeInactive: false));
357
- }
351
+ // No Awake() needed! Both DI and hierarchy references wired automatically
358
352
  }
359
353
  ```
360
354
 
361
- 1. Build up runtime instances (DI + relational fields)
355
+ ### Why Use the DI Integrations
362
356
 
363
- ```csharp
364
- using UnityEngine;
365
- using VContainer;
366
- using WallstopStudios.UnityHelpers.Integrations.VContainer;
357
+ - **Zero boilerplate** - No `Awake()` method needed, no manual `GetComponent` calls, no validation code
358
+ - **Consistent behavior** - Works seamlessly with constructor/property/field injection and runtime instantiation
359
+ - **Safe fallback** - Gracefully degrades to standard behavior if DI binding is missing
360
+ - **Risk-free adoption** - Use incrementally, mix DI and non-DI components freely
367
361
 
368
- public sealed class Spawner : MonoBehaviour
369
- {
370
- [Inject] private IObjectResolver _resolver;
371
- [SerializeField] private Enemy _enemyPrefab;
362
+ ### Supported Packages (Auto-detected)
372
363
 
373
- public Enemy Spawn(Transform parent)
374
- {
375
- Enemy enemy = Instantiate(_enemyPrefab, parent);
376
- return _resolver.BuildUpWithRelations(enemy);
377
- }
378
- }
379
- ```
364
+ Unity Helpers automatically detects these packages via UPM:
380
365
 
381
- 1. Apply to whole hierarchies when needed
366
+ - **Zenject/Extenject**: `com.extenject.zenject`, `com.modesttree.zenject`, `com.svermeulen.extenject`
367
+ - **VContainer**: `jp.cysharp.vcontainer`, `jp.hadashikick.vcontainer`
382
368
 
383
- ```csharp
384
- _resolver.AssignRelationalHierarchy(root, includeInactiveChildren: false);
385
- ```
369
+ > 💡 **UPM packages work out-of-the-box** - No scripting defines needed!
386
370
 
387
- ### Quick Start Zenject
371
+ ### Manual or Source Imports (Non-UPM)
388
372
 
389
- 1. Add the installer to your SceneContext
373
+ If you import Zenject/VContainer as source code, .unitypackage, or raw DLLs (not via UPM), you need to manually add scripting defines:
390
374
 
391
- - Add a `SceneContext` to your scene.
392
- - Add `RelationalComponentsInstaller` to the same GameObject.
393
- - Toggle "Assign Scene On Initialize" to run a one-time scene scan after the container builds.
375
+ 1. Open `Project Settings > Player > Other Settings > Scripting Define Symbols`
376
+ 2. Add the appropriate define(s) for your target platforms:
377
+ - `ZENJECT_PRESENT` - When using Zenject/Extenject
378
+ - `VCONTAINER_PRESENT` - When using VContainer
379
+ 3. Unity will recompile and the integration assemblies under `Runtime/Integrations/*` will activate automatically
394
380
 
395
- 1. Instantiate prefabs with DI + relational assignment
381
+ ### VContainer at a Glance
396
382
 
397
- ```csharp
398
- using UnityEngine;
399
- using Zenject;
400
- using WallstopStudios.UnityHelpers.Integrations.Zenject;
383
+ - **Enable once per scope**
401
384
 
402
- public sealed class Spawner
403
- {
404
- readonly DiContainer _container;
405
- readonly Enemy _enemyPrefab;
385
+ ```csharp
386
+ builder.RegisterRelationalComponents(
387
+ new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true),
388
+ enableAdditiveSceneListener: true
389
+ );
390
+ ```
406
391
 
407
- public Spawner(DiContainer container, Enemy enemyPrefab)
408
- {
409
- _container = container;
410
- _enemyPrefab = enemyPrefab;
411
- }
392
+ - **Runtime helpers**
393
+ - `_resolver.InstantiateComponentWithRelations(componentPrefab, parent)`
394
+ - `_resolver.InstantiateGameObjectWithRelations(rootPrefab, parent, includeInactiveChildren: true)`
395
+ - `_resolver.AssignRelationalHierarchy(existingRoot, includeInactiveChildren: true)`
396
+ - `RelationalObjectPools.CreatePoolWithRelations(...)` + `pool.GetWithRelations(resolver)`
412
397
 
413
- public Enemy Spawn(Transform parent)
414
- {
415
- return _container.InstantiateComponentWithRelations(_enemyPrefab, parent);
416
- }
417
- }
418
- ```
398
+ - **Full walkthrough**: [DI – VContainer sample](Samples~/DI%20-%20VContainer/README.md)
419
399
 
420
- 1. Apply to whole hierarchies
400
+ ### Zenject at a Glance
421
401
 
422
- ```csharp
423
- Container.AssignRelationalHierarchy(root, includeInactiveChildren: true);
424
- ```
402
+ - **Install once per scene**
403
+ - Add `RelationalComponentsInstaller` to your `SceneContext`.
404
+ - Toggles cover include-inactive scanning, single-pass strategy, and additive-scene listening.
405
+
406
+ - **Runtime helpers**
407
+ - `_container.InstantiateComponentWithRelations(componentPrefab, parent)`
408
+ - `_container.InstantiateGameObjectWithRelations(rootPrefab, parent, includeInactiveChildren: true)`
409
+ - `_container.AssignRelationalHierarchy(existingRoot, includeInactiveChildren: true)`
410
+ - Subclass `RelationalMemoryPool<T>` to hydrate pooled items on spawn.
411
+
412
+ - **Full walkthrough**: [DI – Zenject sample](Samples~/DI%20-%20Zenject/README.md)
425
413
 
426
414
  Notes
427
415
 
@@ -479,7 +467,21 @@ Beginner-friendly overview
479
467
 
480
468
  VContainer (1.16.x)
481
469
 
482
- - Runtime usage (LifetimeScope): Call `builder.RegisterRelationalComponents()` in `LifetimeScope.Configure`. The entry point runs automatically after the container builds.
470
+ - Runtime usage (LifetimeScope): Call `builder.RegisterRelationalComponents()` in `LifetimeScope.Configure`. The entry point runs automatically after the container builds. You can enable an additive-scene listener and customize scan options:
471
+
472
+ ```csharp
473
+ using VContainer;
474
+ using VContainer.Unity;
475
+ using WallstopStudios.UnityHelpers.Integrations.VContainer;
476
+
477
+ protected override void Configure(IContainerBuilder builder)
478
+ {
479
+ // Single-pass scan + additive scene listener
480
+ var options = new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true);
481
+ builder.RegisterRelationalComponents(options, enableAdditiveSceneListener: true);
482
+ }
483
+ ```
484
+
483
485
  - Tests without LifetimeScope: Construct the entry point and call `Initialize()` yourself, and register your `AttributeMetadataCache` instance so the assigner uses it:
484
486
 
485
487
  ```csharp
@@ -500,7 +502,8 @@ VContainer (1.16.x)
500
502
  entry.Initialize();
501
503
  ```
502
504
 
503
- - Inject vs BuildUp: Use `resolver.Inject(component)` before calling `resolver.AssignRelationalComponents(component)`.
505
+ - Inject vs BuildUp: Use `resolver.InjectWithRelations(component)` to inject + assign in one call, or `resolver.Inject(component)` then `resolver.AssignRelationalComponents(component)`.
506
+ - Prefabs & GameObjects: `resolver.InstantiateComponentWithRelations(prefab, parent)` or `resolver.InstantiateGameObjectWithRelations(prefab, parent)`; to inject existing hierarchies use `resolver.InjectGameObjectWithRelations(root)`.
504
507
 
505
508
  - EditMode reliability: In EditMode tests, prefer `[UnityTest]` and `yield return null` after creating objects and after initializing the entry point so Unity has a frame to register new objects before `FindObjectsOfType` runs and to allow assignments to complete.
506
509
  - Active scene filter: Entry points operate on the active scene only. In EditMode, create a new scene with `SceneManager.CreateScene`, set it active, and move your test hierarchy into it before calling `Initialize()`.
@@ -508,10 +511,16 @@ VContainer (1.16.x)
508
511
 
509
512
  Zenject/Extenject
510
513
 
511
- - Runtime usage: Add `RelationalComponentsInstaller` to your `SceneContext`. It binds `IRelationalComponentAssigner` and runs `RelationalComponentSceneInitializer` once the container is ready.
514
+ - Runtime usage: Add `RelationalComponentsInstaller` to your `SceneContext`. It binds `IRelationalComponentAssigner` and runs `RelationalComponentSceneInitializer` once the container is ready. The installer exposes toggles to assign on initialize and to listen for additive scenes.
512
515
  - Tests: Bind a concrete `AttributeMetadataCache` instance and construct the assigner with that cache. Then resolve `IInitializable` and call `Initialize()`.
513
516
  - EditMode reliability: As with VContainer, consider `[UnityTest]` with a `yield return null` after creating objects and after calling `Initialize()` to allow Unity to register objects and complete assignments.
514
- - Active scene filter: The initializer operates on the active scene only. Create and set an active scene and move your test hierarchy into it before calling `Initialize()`.
517
+ - Active scene filter: Initial one-time scan operates on the active scene only. The additive-scene listener processes only newly loaded scenes (not all loaded scenes).
518
+ - Prefabs & GameObjects: `container.InstantiateComponentWithRelations(...)`, `container.InstantiateGameObjectWithRelations(...)`, or `container.InjectGameObjectWithRelations(root)`; to inject + assign a single instance: `container.InjectWithRelations(component)`.
519
+
520
+ ### Object Pools (DI-aware)
521
+
522
+ - Zenject: use `RelationalMemoryPool<T>` (or `<TParam, T>`) to assign relational fields in `OnSpawned` automatically.
523
+ - VContainer: create pools with `RelationalObjectPools.CreatePoolWithRelations(...)` and rent via `pool.GetWithRelations(resolver)` to inject + assign.
515
524
 
516
525
  Common pitfalls and how to avoid them
517
526
 
@@ -5,3 +5,5 @@ using System.Runtime.CompilerServices;
5
5
  [assembly: InternalsVisibleTo("WallstopStudios.UnityHelpers.Editor")]
6
6
  [assembly: InternalsVisibleTo("WallstopStudios.UnityHelpers.Tests.Runtime.Zenject")]
7
7
  [assembly: InternalsVisibleTo("WallstopStudios.UnityHelpers.Tests.Runtime.VContainer")]
8
+ [assembly: InternalsVisibleTo("WallstopStudios.UnityHelpers.Tests.Editor.VContainer")]
9
+ [assembly: InternalsVisibleTo("WallstopStudios.UnityHelpers.Tests.Editor.Zenject")]
@@ -13,9 +13,7 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
13
13
  {
14
14
  #if UNITY_EDITOR
15
15
  IEnumerable<FieldInfo> properties =
16
- WallstopStudios.UnityHelpers.Core.Helper.ReflectionHelpers.GetFieldsWithAttribute<NotNullAttribute>(
17
- o.GetType()
18
- );
16
+ Helper.ReflectionHelpers.GetFieldsWithAttribute<NotNullAttribute>(o.GetType());
19
17
 
20
18
  foreach (FieldInfo field in properties)
21
19
  {
@@ -2,6 +2,7 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
2
2
  {
3
3
  using System;
4
4
  using System.Collections.Generic;
5
+ using System.Linq;
5
6
  using Tags;
6
7
  using UnityEngine;
7
8
  using WallstopStudios.UnityHelpers.Utils;
@@ -39,14 +40,58 @@ namespace WallstopStudios.UnityHelpers.Core.Attributes
39
40
  AttributeMetadataCache cache = _metadataCache ?? AttributeMetadataCache.Instance;
40
41
  if (cache == null)
41
42
  {
42
- return false;
43
+ // Fallback to reflection-based discovery when no cache is available
44
+ return HasRelationalAttributesViaReflection(componentType);
43
45
  }
44
46
 
45
- return cache.TryGetRelationalFields(
46
- componentType,
47
- out AttributeMetadataCache.RelationalFieldMetadata[] fields
47
+ Type current = componentType;
48
+ while (current != null && typeof(Component).IsAssignableFrom(current))
49
+ {
50
+ if (
51
+ cache.TryGetRelationalFields(
52
+ current,
53
+ out AttributeMetadataCache.RelationalFieldMetadata[] fields
54
+ )
55
+ && fields.Length > 0
48
56
  )
49
- && fields.Length > 0;
57
+ {
58
+ return true;
59
+ }
60
+ current = current.BaseType;
61
+ }
62
+
63
+ // Fallback: inspect fields via reflection to detect relational attributes
64
+ return HasRelationalAttributesViaReflection(componentType);
65
+ }
66
+
67
+ private static bool HasRelationalAttributesViaReflection(Type componentType)
68
+ {
69
+ Type current = componentType;
70
+ while (current != null && typeof(Component).IsAssignableFrom(current))
71
+ {
72
+ // Prefer ReflectionHelpers so Editor TypeCache can accelerate lookups
73
+ bool has =
74
+ Helper
75
+ .ReflectionHelpers.GetFieldsWithAttribute<ParentComponentAttribute>(current)
76
+ .Any()
77
+ || Helper
78
+ .ReflectionHelpers.GetFieldsWithAttribute<ChildComponentAttribute>(current)
79
+ .Any()
80
+ || Helper
81
+ .ReflectionHelpers.GetFieldsWithAttribute<SiblingComponentAttribute>(
82
+ current
83
+ )
84
+ .Any();
85
+
86
+ if (has)
87
+ {
88
+ return true;
89
+ }
90
+
91
+ current = current.BaseType;
92
+ }
93
+
94
+ return false;
50
95
  }
51
96
 
52
97
  /// <inheritdoc />
@@ -4,7 +4,6 @@ namespace WallstopStudios.UnityHelpers.Core.DataStructure
4
4
  using System.Collections;
5
5
  using System.Collections.Generic;
6
6
  using System.Runtime.CompilerServices;
7
- using Extension;
8
7
  using Helper;
9
8
  using ProtoBuf;
10
9
  using UnityEngine;
@@ -17,6 +17,74 @@ namespace WallstopStudios.UnityHelpers.Core.Extension
17
17
  /// </remarks>
18
18
  public static class RandomExtensions
19
19
  {
20
+ /// <summary>
21
+ /// Returns an index sampled from the provided weights (unnormalized). Negative weights are treated as zero.
22
+ /// </summary>
23
+ public static int NextWeightedIndex(this IRandom random, IReadOnlyList<float> weights)
24
+ {
25
+ if (weights == null)
26
+ {
27
+ throw new ArgumentNullException(nameof(weights));
28
+ }
29
+ if (weights.Count == 0)
30
+ {
31
+ throw new ArgumentException("Weights cannot be empty", nameof(weights));
32
+ }
33
+ double total = 0;
34
+ for (int i = 0; i < weights.Count; i++)
35
+ {
36
+ if (weights[i] > 0)
37
+ {
38
+ total += weights[i];
39
+ }
40
+ }
41
+ if (total <= 0)
42
+ {
43
+ throw new ArgumentException("Sum of weights must be > 0", nameof(weights));
44
+ }
45
+ double r = random.NextDouble() * total;
46
+ double acc = 0;
47
+ for (int i = 0; i < weights.Count; i++)
48
+ {
49
+ float w = weights[i];
50
+ if (w <= 0)
51
+ {
52
+ continue;
53
+ }
54
+ acc += w;
55
+ if (r <= acc)
56
+ {
57
+ return i;
58
+ }
59
+ }
60
+ return weights.Count - 1;
61
+ }
62
+
63
+ /// <summary>
64
+ /// Returns an element sampled according to the given weights list. Throws if lengths mismatch.
65
+ /// </summary>
66
+ public static T NextWeightedElement<T>(
67
+ this IRandom random,
68
+ IReadOnlyList<T> items,
69
+ IReadOnlyList<float> weights
70
+ )
71
+ {
72
+ if (items == null)
73
+ {
74
+ throw new ArgumentNullException(nameof(items));
75
+ }
76
+ if (weights == null)
77
+ {
78
+ throw new ArgumentNullException(nameof(weights));
79
+ }
80
+ if (items.Count != weights.Count)
81
+ {
82
+ throw new ArgumentException("Items and weights length must match.");
83
+ }
84
+ int idx = random.NextWeightedIndex(weights);
85
+ return items[idx];
86
+ }
87
+
20
88
  /// <summary>
21
89
  /// Generates a random 2D vector with components in the range [-amplitude, amplitude].
22
90
  /// </summary>
@@ -74,6 +74,22 @@ namespace WallstopStudios.UnityHelpers.Core.Extension
74
74
  LoggingEnabled = false;
75
75
  }
76
76
 
77
+ /// <summary>
78
+ /// Gets whether global logging is enabled.
79
+ /// </summary>
80
+ public static bool IsGlobalLoggingEnabled()
81
+ {
82
+ return LoggingEnabled;
83
+ }
84
+
85
+ /// <summary>
86
+ /// Sets global logging enabled/disabled without requiring an Object instance.
87
+ /// </summary>
88
+ public static void SetGlobalLoggingEnabled(bool enabled)
89
+ {
90
+ LoggingEnabled = enabled;
91
+ }
92
+
77
93
  public static void EnableLogging(this Object component)
78
94
  {
79
95
  Disabled.Remove(component);
@@ -3,11 +3,14 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
3
3
  using System;
4
4
  using System.Collections.Generic;
5
5
  using Extension;
6
- using UnityEditor;
7
6
  using UnityEngine;
8
7
  using UnityEngine.SceneManagement;
9
8
  using WallstopStudios.UnityHelpers.Utils;
10
9
  using Object = UnityEngine.Object;
10
+ #if UNITY_EDITOR
11
+ using UnityEditor;
12
+ #endif
13
+
11
14
  #if UNITY_EDITOR
12
15
  using UnityEditor.SceneManagement;
13
16
  #endif
@@ -12,7 +12,9 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
12
12
  using System.Linq.Expressions;
13
13
  using System.Reflection;
14
14
  using System.Runtime.CompilerServices;
15
+ #if UNITY_EDITOR
15
16
  using UnityEditor;
17
+ #endif
16
18
  #if EMIT_DYNAMIC_IL
17
19
  using System.Reflection.Emit;
18
20
  #endif
@@ -50,10 +52,9 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
50
52
  {
51
53
  // Cache for type resolution by name
52
54
  #if !SINGLE_THREADED
53
- private static readonly System.Collections.Concurrent.ConcurrentDictionary<
54
- string,
55
- Type
56
- > TypeResolutionCache = new(StringComparer.Ordinal);
55
+ private static readonly ConcurrentDictionary<string, Type> TypeResolutionCache = new(
56
+ StringComparer.Ordinal
57
+ );
57
58
  #else
58
59
  private static readonly Dictionary<string, Type> TypeResolutionCache = new(
59
60
  StringComparer.Ordinal
@@ -383,6 +384,17 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
383
384
  return ListCreators.GetOrAdd(elementType, type => GetListCreator(type)).Invoke();
384
385
  }
385
386
 
387
+ // Test helpers to avoid reflection in tests when asserting cache state
388
+ internal static bool IsFieldGetterCached(FieldInfo field)
389
+ {
390
+ return field != null && FieldGetterCache.ContainsKey(field);
391
+ }
392
+
393
+ internal static bool IsFieldSetterCached(FieldInfo field)
394
+ {
395
+ return field != null && FieldSetterCache.ContainsKey(field);
396
+ }
397
+
386
398
  /// <summary>
387
399
  /// Builds a cached delegate that returns the value of a field as <see cref="object"/>.
388
400
  /// Supports instance and static fields.
@@ -3713,7 +3725,7 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
3713
3725
  #if UNITY_EDITOR
3714
3726
  try
3715
3727
  {
3716
- TypeCache.TypeCollection list = UnityEditor.TypeCache.GetTypesDerivedFrom<T>();
3728
+ TypeCache.TypeCollection list = TypeCache.GetTypesDerivedFrom<T>();
3717
3729
  return list.Where(t =>
3718
3730
  t != null && (includeAbstract || (t.IsClass && !t.IsAbstract))
3719
3731
  );
@@ -3747,7 +3759,7 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
3747
3759
  #if UNITY_EDITOR
3748
3760
  try
3749
3761
  {
3750
- TypeCache.TypeCollection list = UnityEditor.TypeCache.GetTypesDerivedFrom(baseType);
3762
+ TypeCache.TypeCollection list = TypeCache.GetTypesDerivedFrom(baseType);
3751
3763
  return list.Where(t =>
3752
3764
  t != null && (includeAbstract || (t.IsClass && !t.IsAbstract))
3753
3765
  );
@@ -3799,8 +3811,7 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
3799
3811
  #if UNITY_EDITOR
3800
3812
  try
3801
3813
  {
3802
- TypeCache.TypeCollection types =
3803
- UnityEditor.TypeCache.GetTypesWithAttribute<TAttribute>();
3814
+ TypeCache.TypeCollection types = TypeCache.GetTypesWithAttribute<TAttribute>();
3804
3815
  return types.Where(t =>
3805
3816
  t != null && (includeAbstract || (t.IsClass && !t.IsAbstract))
3806
3817
  );
@@ -3855,7 +3866,7 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
3855
3866
  try
3856
3867
  {
3857
3868
  TypeCache.MethodCollection methods =
3858
- UnityEditor.TypeCache.GetMethodsWithAttribute<TAttribute>();
3869
+ TypeCache.GetMethodsWithAttribute<TAttribute>();
3859
3870
  IEnumerable<MethodInfo> filtered = methods;
3860
3871
  if (within != null)
3861
3872
  {
@@ -3892,7 +3903,7 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
3892
3903
  try
3893
3904
  {
3894
3905
  TypeCache.FieldInfoCollection fields =
3895
- UnityEditor.TypeCache.GetFieldsWithAttribute<TAttribute>();
3906
+ TypeCache.GetFieldsWithAttribute<TAttribute>();
3896
3907
  IEnumerable<FieldInfo> filtered = fields;
3897
3908
  if (within != null)
3898
3909
  {
@@ -1,8 +1,10 @@
1
1
  namespace WallstopStudios.UnityHelpers.Core.Helper
2
2
  {
3
3
  using Extension;
4
- using UnityEditor;
5
4
  using UnityEngine;
5
+ #if UNITY_EDITOR
6
+ using UnityEditor;
7
+ #endif
6
8
 
7
9
  /// <summary>
8
10
  /// Sprite and texture utilities for editor workflows.
@@ -2,9 +2,11 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
2
2
  {
3
3
  using System;
4
4
  using System.Collections.Concurrent;
5
- using UnityEditor;
6
5
  using UnityEngine;
7
6
  using Utils;
7
+ #if UNITY_EDITOR
8
+ using UnityEditor;
9
+ #endif
8
10
 
9
11
  /// <summary>
10
12
  /// Thread-safe dispatcher that enqueues work to run on Unity's main thread.
@@ -96,5 +98,47 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
96
98
  }
97
99
  }
98
100
  }
101
+
102
+ /// <summary>
103
+ /// Posts an action to run on the main thread and returns a Task that completes after execution.
104
+ /// </summary>
105
+ public System.Threading.Tasks.Task RunAsync(Action action)
106
+ {
107
+ var tcs = new System.Threading.Tasks.TaskCompletionSource<bool>();
108
+ RunOnMainThread(() =>
109
+ {
110
+ try
111
+ {
112
+ action();
113
+ tcs.SetResult(true);
114
+ }
115
+ catch (Exception ex)
116
+ {
117
+ tcs.SetException(ex);
118
+ }
119
+ });
120
+ return tcs.Task;
121
+ }
122
+
123
+ /// <summary>
124
+ /// Posts a function to run on the main thread and returns its result via Task.
125
+ /// </summary>
126
+ public System.Threading.Tasks.Task<T> Post<T>(Func<T> func)
127
+ {
128
+ var tcs = new System.Threading.Tasks.TaskCompletionSource<T>();
129
+ RunOnMainThread(() =>
130
+ {
131
+ try
132
+ {
133
+ T result = func();
134
+ tcs.SetResult(result);
135
+ }
136
+ catch (Exception ex)
137
+ {
138
+ tcs.SetException(ex);
139
+ }
140
+ });
141
+ return tcs.Task;
142
+ }
99
143
  }
100
144
  }
@@ -26,11 +26,19 @@ namespace WallstopStudios.UnityHelpers.Core.Serialization.JsonConverters
26
26
  JsonSerializerOptions options
27
27
  )
28
28
  {
29
- writer.WriteStringValue(
30
- value == null
31
- ? "{}"
32
- : new { name = value.name, type = value.GetType().FullName }.ToString()
33
- );
29
+ if (value == null)
30
+ {
31
+ writer.WriteNullValue();
32
+ return;
33
+ }
34
+
35
+ writer.WriteStartObject();
36
+ writer.WriteString("name", value.name);
37
+ // Use AssemblyQualifiedName to disambiguate type for diagnostics
38
+ writer.WriteString("type", value.GetType().AssemblyQualifiedName);
39
+ // Emit the actual Unity instance ID without transformation for correctness.
40
+ writer.WriteNumber("instanceId", value.GetInstanceID());
41
+ writer.WriteEndObject();
34
42
  }
35
43
  }
36
44
  }
@@ -141,7 +141,7 @@ namespace WallstopStudios.UnityHelpers.Core.Serialization.JsonConverters
141
141
  #if UNITY_2022_2_OR_NEWER
142
142
  uint num = value.refreshRateRatio.numerator;
143
143
  uint den = value.refreshRateRatio.denominator;
144
- int hz = den != 0 ? (int)System.Math.Round((double)num / den) : 0;
144
+ int hz = den != 0 ? (int)Math.Round((double)num / den) : 0;
145
145
  writer.WriteNumber(RefreshRateProp, hz);
146
146
  writer.WritePropertyName(RefreshRatioProp);
147
147
  writer.WriteStartObject();
@@ -19,7 +19,7 @@ namespace WallstopStudios.UnityHelpers.Core.Serialization.JsonConverters
19
19
  string typeName = reader.GetString();
20
20
  return string.IsNullOrWhiteSpace(typeName)
21
21
  ? null
22
- : Core.Helper.ReflectionHelpers.TryResolveType(typeName);
22
+ : Helper.ReflectionHelpers.TryResolveType(typeName);
23
23
  }
24
24
 
25
25
  public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options)