Skip to content

Commit 8dfa1d3

Browse files
Merge pull request #31 from CoderGamester/develop
Release 2.0.1
2 parents 18cfd93 + 46b5d6f commit 8dfa1d3

23 files changed

Lines changed: 705 additions & 34 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,6 @@ crashlytics-build.properties
7979

8080

8181

82+
83+
# Tests audit history (unity-tests-audit skill -- local developer state, never committed)
84+
.audit-history.md

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ flowchart TD
128128

129129
### Build/Version Info
130130
`Runtime/VersionServices.cs`
131-
- Static class for `version-data` Resources metadata. `VersionExternal` is always safe; `VersionInternal`, `Branch`, `Commit`, and `BuildNumber` require successful `LoadVersionDataAsync()` first.
131+
- Static class for `version-data` Resources metadata. `VersionExternal` is always safe; `VersionInternal`, `Branch`, `Commit`, and `BuildNumber` require either `LoadVersionData()` (sync) or `LoadVersionDataAsync()` (async) to have completed successfully first.
132132

133133
## 3. Key Directories / Files
134134

@@ -243,7 +243,9 @@ The concrete `PoolService` stays in `Runtime/` root under `GameLovers.Services`
243243
- `VersionEditorUtils` writes `version-data.txt` on every domain reload (`[InitializeOnLoadMethod]`) and can be invoked by build pipelines. It uses git CLI; failures are handled gracefully.
244244
- The **write folder** is configurable per-project via `VersioningEditorSettings.instance.ResourcesFolderPath` (default `Assets/Configs/Resources`). Change it from the Versioning tab in the Services Explorer (browse + reset). The chosen folder must contain a `Resources` path segment so `Resources.Load<TextAsset>("version-data")` can locate the file at runtime.
245245
- `VersioningEditorSettings` persists to `ProjectSettings/VersioningEditorSettings.asset` (editor-only, not committed to version control by default).
246-
- `VersionExternal` is always safe (reads `Application.version` directly). `VersionInternal`, `Branch`, `Commit`, and `BuildNumber` throw `Exception("Version Data not loaded.")` if data has not been loaded — call `LoadVersionDataAsync()` early in boot.
246+
- `VersionExternal` is always safe (reads `Application.version` directly). `VersionInternal`, `Branch`, `Commit`, and `BuildNumber` throw `Exception("Version Data not loaded.")` if data has not been loaded — call `LoadVersionData()` (sync) or `LoadVersionDataAsync()` (async) early in boot.
247+
- **Sync vs async load**: `LoadVersionData()` uses `Resources.Load<TextAsset>` (synchronous, main-thread); `LoadVersionDataAsync()` uses `Resources.LoadAsync<TextAsset>` wrapped in a `TaskCompletionSource`. Both funnel into the private `ApplyTextAsset(TextAsset, bool asyncContext)` helper that parses JSON, flips `_loaded`, and calls `Resources.UnloadAsset`. Behaviour is identical apart from the wording in the failure log line (`"Could not async load …"` vs `"Could not load …"`). Sync is the recommended default for the shipping `version-data.txt` (a few hundred bytes); async is only worth the ceremony if `VersionData` is extended with large embedded blobs (e.g. a baked manifest) that would noticeably stall the main thread.
248+
- `VersionServices.IsOutdatedVersion(string)` requires a 3-part `Major.Minor.Patch` semver and throws `IndexOutOfRangeException` on any 1- or 2-part input — the parser unconditionally accesses `Split('.')[0..2]`. Consumers that compare against `Application.version` must ensure `ProjectSettings.bundleVersion` is 3-part (defaults to `0.1` / `1.0` for fresh projects, which will throw). Either bump `bundleVersion` to a 3-part value, guard with `parts.Length < 3` at the call site, or harden the method itself with a length guard. Tests calling this against the host editor's `Application.version` should `Assert.Inconclusive` on shorter strings rather than `Assert.Throws` — the throw is a real production bug surface, not a documented contract.
247249

248250
### Editor Introspection (InternalsVisibleTo)
249251

CHANGELOG.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to this package will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## [2.0.1] - 2026-05-20
8+
9+
**New**
10+
- Added new test suite for more rebust code coverage
11+
- Added `VersionServices.LoadVersionData()` — synchronous sibling of `LoadVersionDataAsync()` for consumers who want to populate version metadata at boot without an `await`. Both methods now funnel into a shared private `ApplyTextAsset` helper, so behaviour is identical. Sync is the recommended default for the shipping `version-data.txt` (a few hundred bytes); async remains available for cases where `VersionData` is extended with large embedded blobs. Covered by `Tests/EditMode/Unit/VersionServicesSyncLoadTest.cs`.
12+
13+
**Fixed**:
14+
- `Tests/AGENTS.md` §8 extended with a new **"Authorized reflection sites (storage-assertion exception)"** subsection.
15+
716
## [2.0.0] - 2026-04-26
817

918
**New**:
@@ -19,8 +28,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1928
- Added `AssetResolverService` (implements `IAssetResolverService` / `IAssetAdderService`) to `Runtime/` root (ns `GameLovers.Services`)
2029
- Added Editor/AssetsImporter/: `AssetsImporter`, `AssetsToolImporter`, `AssetConfigsImporter`, `AddressableIdsGenerator`, `AddressablesIdGeneratorSettings` (ns `GameLovers.Services.AssetsImporter.Editor`)
2130
- Added importable **Samples** under `Samples~/` (importable via Unity Package Manager > GameLovers Services > Samples).
22-
- **Services Playground** — single-scene, zero-setup walk-through that wires every foundation service via `MainInstaller` and exercises 10 of 13 Services Explorer tabs end-to-end.
23-
- **Asset Resolver** — focused demo of `AssetResolverService` end-to-end (`AddConfigs` / `RequestAsset` / `UnloadAssets`) with `SpriteConfigs : AssetConfigsScriptableObject<SpriteId, Sprite>`. Drives the three Services Explorer tabs the Playground does not cover (Asset Resolver, Assets Importer, Addressable Ids).
31+
- **Services Playground** — single-scene, zero-setup walk-through that wires every foundation service via `MainInstaller` and exercises 10 of 13 Services Explorer tabs end-to-end.
32+
- **Asset Resolver** — focused demo of `AssetResolverService` end-to-end (`AddConfigs` / `RequestAsset` / `UnloadAssets`) with `SpriteConfigs : AssetConfigsScriptableObject<SpriteId, Sprite>`. Drives the three Services Explorer tabs the Playground does not cover (Asset Resolver, Assets Importer, Addressable Ids).
2433

2534
**Changed**:
2635
- Addressable Ids generator and Assets Importer settings moved from `Assets/*.asset` ScriptableObjects to `ProjectSettings/` ScriptableSingletons (mirrors `VersioningEditorSettings`).
@@ -41,7 +50,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
4150
- `AddressablesAssetLoader.UnloadAsset` no longer calls `GC.Collect()`, `GC.WaitForPendingFinalizers()`, or `Resources.UnloadUnusedAssets()`. The method now only decrements the Addressables reference count. The old implementation caused PlayMode Test Runner crashes on macOS and O(total-assets-in-memory) main-thread stalls per per-asset release. Callers that need memory reclamation should invoke `Resources.UnloadUnusedAssets()` themselves at appropriate moments (scene transitions, boot, memory-pressure events); Unity also runs an unused-assets sweep automatically on `LoadSceneMode.Single` scene loads.
4251
- Corrected `IAssetLoader.UnloadAsset` XML documentation: removed the incorrect "will also destroy GameObject instances" claim — `Addressables.Release(gameObject)` does not destroy the instance; callers must `Object.Destroy` it separately.
4352
- `IAsyncCoroutine.StopCoroutine(bool triggerOnComplete)` now honors its `triggerOnComplete` parameter and flips `IsCompleted` to `true` and `IsRunning` to `false` after stopping. The previous implementation always invoked `OnComplete` callbacks regardless of the flag and left state flags unchanged, so consumers could not distinguish a stopped coroutine from a running one and `triggerOnComplete: false` was silently ignored.
44-
- `GameObjectPool.Dispose()` and `GameObjectPool<T>.Dispose()` now skip pooled entries whose underlying `GameObject` has already been destroyed by an external owner (e.g. a parent GameObject was destroyed while pooled instances were still reparented under it via `DespawnToSampleParent`).
53+
- `GameObjectPool.Dispose()` and `GameObjectPool<T>.Dispose()` now skip pooled entries whose underlying `GameObject` has already been destroyed by an external owner (e.g. a parent GameObject was destroyed while pooled instances were still reparented under it via `DespawnToSampleParent`).
4554

4655
**Breaking Changes** — see `MIGRATION.md` for details:
4756
- Pool types moved from `GameLovers.Services` to `GameLovers.Services.Pooling` (`IPoolService`, `IObjectPool`, `IObjectPool<T>`, `ObjectPool<T>`, `ObjectPoolBase<T>`, `GameObjectPool`, `GameObjectPool<T>`, `IPoolEntitySpawn`, `IPoolEntitySpawn<T>`, `IPoolEntityDespawn`, `IPoolEntityObject<T>`). `PoolService` concrete class remains in `GameLovers.Services`.
@@ -71,7 +80,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7180
**Fixed**:
7281
- Fixed the *README.md* file to now follow best practices in OSS standards for Unity's package library projects
7382
- Fixed linter warnings in *VersionServices.cs* (redundant field initialization, unused lambda parameter, member shadowing)
74-
- Fixed *GameObjectPool* not invoking *IPoolEntitySpawn.OnSpawn()* and *IPoolEntityDespawn.OnDespawn()* on components attached to spawned GameObjects.
83+
- Fixed *GameObjectPool* not invoking *IPoolEntitySpawn.OnSpawn()* and *IPoolEntityDespawn.OnDespawn()* on components attached to spawned GameObjects.
7584

7685
## [0.15.1] - 2025-09-24
7786

@@ -84,7 +93,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
8493
- Added *StartDelayCall* method to *ICoroutineService* to allow deferred methods to be safely executed within the bounds of a Unity Coroutine
8594
- Added the possibility to know the current state of an *IAsyncCoroutine*
8695
- Added the access to the Sample Entity used to generete new entites within an *IObjectPool<T>* and destroy it when disposing the object pool
87-
- Added the possibility to reset an *IObjectPool<T>* to a new state
96+
- Added the possibility to reset an *IObjectPool<T>* to a new state
8897

8998
## [0.14.1] - 2024-11-30
9099

@@ -219,7 +228,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
219228

220229
**Changed**:
221230
- Renamed *IDataWriter* and it's *FlushData* methods to *IDataSaver* & *SaveData* respectively to match with it's execution logic scope
222-
- Moved the *AddData* to the *IDataService* to allow the *IDataSaver* have the single responsibility of saving data into disk
231+
- Moved the *AddData* to the *IDataService* to allow the *IDataSaver* have the single responsibility of saving data into disk
223232

224233
## [0.4.1] - 2020-07-09
225234

@@ -243,7 +252,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
243252
- Now the *MainInstaller* checks the object binding relationship in compile time
244253
- Improved the *ObjectPools* helper classes with a now static global instatiator for game objects.
245254
- Now the *PoolService* is only a service container for objects pools and no longer creates/initializes new pools.
246-
- Removed *Pool.Clear* functionality. Use *DespawnAll* or delete the pool instead
255+
- Removed *Pool.Clear* functionality. Use *DespawnAll* or delete the pool instead
247256

248257
**Fixed**:
249258
- The *CoroutineService* no longer fails on null coroutines

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,13 @@ cmd.ExecuteCommand(new LevelUpCommand());
232232
### Version Services
233233

234234
```csharp
235-
await VersionServices.LoadVersionDataAsync();
235+
// Pick one of:
236+
VersionServices.LoadVersionData(); // sync — recommended for tiny version-data.txt payloads
237+
await VersionServices.LoadVersionDataAsync(); // async — use if VersionData embeds large blobs
238+
236239
string branch = VersionServices.Branch;
237240
string commit = VersionServices.Commit;
238-
string ext = VersionServices.VersionExternal; // always safe, no await needed
241+
string ext = VersionServices.VersionExternal; // always safe, no load needed
239242
```
240243

241244
### Asset Loading

Runtime/VersionServices.cs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,27 @@ public struct VersionData
5555
private static VersionData _versionData;
5656
private static bool _loaded;
5757

58+
/// <summary>
59+
/// Load the internal version string from resources synchronously. Should be called once
60+
/// when the app is started. Intended for tiny payloads (the default <c>version-data.txt</c>
61+
/// is a few hundred bytes); use <see cref="LoadVersionDataAsync"/> if <see cref="VersionData"/>
62+
/// is extended with large blobs that would noticeably stall the main thread.
63+
/// </summary>
64+
public static void LoadVersionData()
65+
{
66+
try
67+
{
68+
var textAsset = Resources.Load<TextAsset>(VersionDataFilename);
69+
70+
ApplyTextAsset(textAsset, asyncContext: false);
71+
}
72+
catch (Exception e)
73+
{
74+
Debug.LogError($"Error loading version data: {e.Message}");
75+
_loaded = false;
76+
}
77+
}
78+
5879
/// <summary>
5980
/// Load the internal version string from resources async. Should be called once when the
6081
/// app is started.
@@ -70,17 +91,7 @@ public static async Task LoadVersionDataAsync()
7091

7192
var textAsset = await source.Task;
7293

73-
if (!textAsset)
74-
{
75-
Debug.LogError("Could not async load version data from Resources.");
76-
_loaded = false;
77-
return;
78-
}
79-
80-
_versionData = JsonUtility.FromJson<VersionData>(textAsset.text);
81-
_loaded = true;
82-
83-
Resources.UnloadAsset(textAsset);
94+
ApplyTextAsset(textAsset, asyncContext: true);
8495
}
8596
catch (Exception e)
8697
{
@@ -89,6 +100,21 @@ public static async Task LoadVersionDataAsync()
89100
}
90101
}
91102

103+
private static void ApplyTextAsset(TextAsset textAsset, bool asyncContext)
104+
{
105+
if (!textAsset)
106+
{
107+
Debug.LogError($"Could not {(asyncContext ? "async " : string.Empty)}load version data from Resources.");
108+
_loaded = false;
109+
return;
110+
}
111+
112+
_versionData = JsonUtility.FromJson<VersionData>(textAsset.text);
113+
_loaded = true;
114+
115+
Resources.UnloadAsset(textAsset);
116+
}
117+
92118
/// <summary>
93119
/// Requests to check if the provided version is newer compared to the local app version
94120
/// </summary>

Tests/AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ Why both keys: `RunSettings.Instance` is a lazy-loaded singleton (`ResourcesLoad
6767
- Use `[Order(n)]` when tests must run in sequence (e.g., `VersionServicesIntegrationTest` resets static state, then loads, then reads).
6868
- Reset shared static state in `[SetUp]` (reflection into private fields is acceptable for static classes like `VersionServices`).
6969

70+
### Authorized reflection sites (storage-assertion exception)
71+
Reflection on private state is also authorized when a setter has no observable readback path through the public API and exercising the side-effect would require a runtime environment the EditMode harness cannot provide (e.g., a partially-loaded `AssetReference`). In those cases, asserting the storage field directly via `BindingFlags.NonPublic | BindingFlags.Instance` is acceptable and preferable to a Red-testability skip. The test method MUST be a single setter-storage assertion (not a multi-step behavioural assertion); if behaviour is what you need to verify, refactor to expose an `internal` accessor under `InternalsVisibleTo` instead.
72+
73+
Currently authorized:
74+
- `AssetResolverServiceTest.AddDebugConfigs_StoresAllProvided` — reads the private `AssetResolverService._errorMaterial` field to confirm `AddDebugConfigs` stored its argument. The fallback-material lookup path (`AssetResolverService.Convert<T>` at `Runtime/AssetResolverService.cs:474`) only fires when `!assetReference.IsDone`, which the EditMode harness cannot fabricate without a real Addressables catalog. Documented here per the Type B audit run on 2026-05-04 (Referee §4 missed-anti-pattern finding, parent picked option A).
75+
7076
## 9. Test Directory Layout
7177

7278
| Directory | Contents |

Tests/EditMode/Unit/AddressableIdsEditorSettingsTest.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@ namespace GameLoversEditor.Services.Tests
88
[TestFixture]
99
public class AddressableIdsEditorSettingsTest
1010
{
11+
// AddressableIdsEditorSettings is a ScriptableSingleton persisted to ProjectSettings/;
12+
// snapshot + restore so test mutations don't leak into the user's project.
13+
private string _originalScriptFilename;
14+
private string _originalNamespace;
15+
private string _originalAddressableLabel;
16+
17+
[SetUp]
18+
public void SaveOriginalSettings()
19+
{
20+
_originalScriptFilename = AddressableIdsEditorSettings.instance.ScriptFilename;
21+
_originalNamespace = AddressableIdsEditorSettings.instance.Namespace;
22+
_originalAddressableLabel = AddressableIdsEditorSettings.instance.AddressableLabel;
23+
}
24+
25+
[TearDown]
26+
public void RestoreOriginalSettings()
27+
{
28+
AddressableIdsEditorSettings.instance.ScriptFilename = _originalScriptFilename;
29+
AddressableIdsEditorSettings.instance.Namespace = _originalNamespace;
30+
AddressableIdsEditorSettings.instance.AddressableLabel = _originalAddressableLabel;
31+
}
32+
1133
// ---- IsValidIdentifier ----
1234

1335
[Test]
@@ -116,5 +138,43 @@ public void IsValidNamespace_DeepHierarchy_ReturnsTrue()
116138
Assert.IsTrue(AddressableIdsEditorSettings.IsValidNamespace("Com.GameLovers.Game.Ids", out var error));
117139
Assert.IsNull(error);
118140
}
141+
142+
// ---- Setter normalization ----
143+
144+
[Test]
145+
public void ScriptFilename_SetterNormalizesAndPersists()
146+
{
147+
AddressableIdsEditorSettings.instance.ScriptFilename = " CustomFilename ";
148+
149+
Assert.AreEqual("CustomFilename", AddressableIdsEditorSettings.instance.ScriptFilename);
150+
151+
AddressableIdsEditorSettings.instance.ScriptFilename = null;
152+
153+
Assert.AreEqual("AddressableId", AddressableIdsEditorSettings.instance.ScriptFilename);
154+
}
155+
156+
[Test]
157+
public void Namespace_SetterNormalizesAndPersists()
158+
{
159+
AddressableIdsEditorSettings.instance.Namespace = " Custom.Namespace ";
160+
161+
Assert.AreEqual("Custom.Namespace", AddressableIdsEditorSettings.instance.Namespace);
162+
163+
AddressableIdsEditorSettings.instance.Namespace = null;
164+
165+
Assert.AreEqual("Game.Ids", AddressableIdsEditorSettings.instance.Namespace);
166+
}
167+
168+
[Test]
169+
public void AddressableLabel_SetterNormalizesAndPersists()
170+
{
171+
AddressableIdsEditorSettings.instance.AddressableLabel = " custom-label ";
172+
173+
Assert.AreEqual("custom-label", AddressableIdsEditorSettings.instance.AddressableLabel);
174+
175+
AddressableIdsEditorSettings.instance.AddressableLabel = null;
176+
177+
Assert.AreEqual(string.Empty, AddressableIdsEditorSettings.instance.AddressableLabel);
178+
}
119179
}
120180
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Collections.Generic;
2+
using GameLovers.GameData;
3+
using GameLovers.Services.AssetsImporter;
4+
using NUnit.Framework;
5+
using UnityEngine;
6+
using UnityEngine.AddressableAssets;
7+
8+
// ReSharper disable once CheckNamespace
9+
10+
namespace GameLoversEditor.Services.Tests
11+
{
12+
[TestFixture]
13+
public class AssetConfigsScriptableObjectTest
14+
{
15+
private class TestAssetConfigs : AssetConfigsScriptableObject<int, Sprite> { }
16+
17+
[Test]
18+
public void OnAfterDeserialize_RebuildsDictionaryFromConfigs()
19+
{
20+
var so = ScriptableObject.CreateInstance<TestAssetConfigs>();
21+
22+
var refA = new AssetReference();
23+
var refB = new AssetReference();
24+
so.Configs = new List<Pair<int, AssetReference>>
25+
{
26+
new Pair<int, AssetReference>(1, refA),
27+
new Pair<int, AssetReference>(2, refB)
28+
};
29+
30+
so.OnAfterDeserialize();
31+
32+
Assert.IsNotNull(so.ConfigsDictionary);
33+
Assert.AreEqual(2, so.ConfigsDictionary.Count);
34+
Assert.AreSame(refA, so.ConfigsDictionary[1]);
35+
Assert.AreSame(refB, so.ConfigsDictionary[2]);
36+
37+
Object.DestroyImmediate(so);
38+
}
39+
}
40+
}

Tests/EditMode/Unit/AssetConfigsScriptableObjectTest.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)