Skip to content

Commit 62b2655

Browse files
committed
feat(services): Explorer hardening + AddressableIds freshness + AssetResolver full coverage
- AddressableIds tab: persisted last-generation snapshot in AddressableIdsEditorSettings (sorted address/label arrays + timestamp + filename/label-filter used). New cheap-tier ComputeFreshness (file-stat only) drives a color-coded freshness banner on every Refresh; new expensive-tier Diff (GetAssetList + ProcessData) runs only on the user's "Check for stale Ids" button click and renders added/removed/warning lists in a Foldout. - Services Explorer hardening: ServiceTab gains MakeStickyFoldout (per- tab persisted collapsed state, with evt.target == foldout filter to block ChangeEvent<bool> bubbling through nested foldouts) and TryShortCircuitRefresh (digest-based early-out to avoid destroying mouse-captured VisualElements mid-click). AssetResolverTab and MessageBrokerTab adopt both; AssetResolverTab's destructive toggle invalidates the digest on change. - AssetResolver sample now exercises all three Explorer tabs the ServicesPlayground does not cover: AssetResolverExample binds IAssetResolverService through MainInstaller (populates Asset Resolver tab live); new SpriteConfigsImporter surfaces a sample row in the Assets Importer tab; post-processor applies 'services-sample-asset-resolver' label so Addressable Ids tab can demo a sample-scoped Generate. Adds a sample-runtime asmdef + an in-scene "Open Services Explorer" button that jumps to the AssetResolver tab via a sample-scoped editor menu. - Confirmed the AssetResolver "removing the dedicated group is the user's undo" design after a self-cleanup attempt was found structurally impossible (Unity recompiles before firing OnPostprocessAllAssets for deletions, dropping the post-processor class itself). Documented in AGENTS.md so the dead pattern is not re-attempted. Folds all of the above into the unreleased ## [2.0.0] CHANGELOG entry per the pre-publication versioning policy. Made-with: Cursor
1 parent c7351ed commit 62b2655

15 files changed

Lines changed: 1333 additions & 104 deletions

AGENTS.md

Lines changed: 14 additions & 3 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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.0] - 2026-04-27
7+
## [2.0.0] - 2026-04-26
88

99
**New**:
1010
- Added **Services Explorer** window (`Tools > GameLovers > Services Explorer`) with 13 live-refresh tabs: Overview, Installer, MessageBroker, Tick, Coroutine, Pool, Data, Time, RNG, AssetResolver, Versioning, Assets Importer, Addressable Ids — works in both Edit and Play mode
@@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2020
- Added Editor/AssetsImporter/: `AssetsImporter`, `AssetsToolImporter`, `AssetConfigsImporter`, `AddressableIdsGenerator`, `AddressablesIdGeneratorSettings` (ns `GameLovers.Services.AssetsImporter.Editor`)
2121
- Added importable **Samples** under `Samples~/` (importable via Unity Package Manager > GameLovers Services > Samples).
2222
- **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). Ships with editor automation (`AssetResolverSampleSetup` + `AssetPostprocessor`) that auto-marks `Sprites/` PNGs as Addressable in a dedicated group `GameLoversServicesSamples_AssetResolver`, renames non-canonical filenames to `Hero/Coin/Enemy` (substring match, alphabetical fallback), and wires `SpriteConfigs.asset` rows on import. Manual escape hatches: `Tools > GameLovers > Samples > Asset Resolver > Refresh Addressables` menu and a sample-scoped **Refresh AssetResolver Sample Addressables** button on the `SpriteConfigs.asset` inspector. Existing user mappings to other sprites are respected.
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).
2424

2525
**Changed**:
2626
- Addressable Ids generator and Assets Importer settings moved from `Assets/*.asset` ScriptableObjects to `ProjectSettings/` ScriptableSingletons (mirrors `VersioningEditorSettings`).
@@ -42,7 +42,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
4242
- 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.
4343
- `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.
4444
- `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`).
45-
- `PerformanceTestSetup.InitializePerformanceTestMetadata()` now also primes the `PT_Settings` PlayerPref (with `MeasurementCount = -1`, the package's "no override" sentinel). The previous setup primed only `PT_Run`, which left `RunSettings.Instance` lazy-loading from an empty JSON in EditMode — the loader silently returned `null` and `MethodMeasurement.SettingsOverride()` then NRE'd at the first `Measure.Method(...).Run()` call before any warmup ran. All EditMode `[Test, Performance]` tests in the suite (`ObjectPoolPerformanceTest`, `MessageBrokerPerformanceTest`) were affected. Added `PerformanceTestSetupTest.MeasureMethod_AfterInitialize_DoesNotThrow` as a regression sentinel so a future change to `PerformanceTestSetup` that drops either PlayerPref will fail loudly with a clear class name.
4645

4746
**Breaking Changes** — see `MIGRATION.md` for details:
4847
- 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`.

Editor/AddressableIds/AddressableIdsEditorSettings.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using UnityEditor;
34
using UnityEngine;
45

@@ -15,6 +16,15 @@ internal sealed class AddressableIdsEditorSettings : ScriptableSingleton<Address
1516
[SerializeField] private string _namespace = "Game.Ids";
1617
[SerializeField] private string _addressableLabel = "GenerateIds";
1718

19+
// ---- Last-generation snapshot (persisted) ----
20+
[SerializeField] private long _lastGenerationUtcTicks;
21+
[SerializeField] private int _lastGenerationIdCount;
22+
[SerializeField] private int _lastGenerationLabelCount;
23+
[SerializeField] private string _lastGenerationFilenameUsed;
24+
[SerializeField] private string _lastGenerationLabelFilterUsed;
25+
[SerializeField] private string[] _lastGenerationAddresses = Array.Empty<string>();
26+
[SerializeField] private string[] _lastGenerationLabels = Array.Empty<string>();
27+
1828
/// <summary>Name of the generated C# file (without extension) and the enum/class it contains.</summary>
1929
public string ScriptFilename
2030
{
@@ -66,6 +76,65 @@ public string AddressableLabel
6676
}
6777
}
6878

79+
// ---- Last-generation snapshot accessors ----
80+
81+
/// <summary>True when a generation snapshot has been recorded by <see cref="RecordGeneration"/>.</summary>
82+
public bool HasSnapshot => _lastGenerationUtcTicks != 0L;
83+
84+
/// <summary>UTC timestamp of the last successful generation, or <c>default(DateTime)</c> when none.</summary>
85+
public DateTime LastGenerationUtc => _lastGenerationUtcTicks == 0L
86+
? default
87+
: new DateTime(_lastGenerationUtcTicks, DateTimeKind.Utc);
88+
89+
public int LastGenerationIdCount => _lastGenerationIdCount;
90+
public int LastGenerationLabelCount => _lastGenerationLabelCount;
91+
public string LastGenerationFilenameUsed => _lastGenerationFilenameUsed ?? string.Empty;
92+
public string LastGenerationLabelFilterUsed => _lastGenerationLabelFilterUsed ?? string.Empty;
93+
94+
/// <summary>Sorted list of addressable addresses that were emitted in the last generation. Empty array when no snapshot.</summary>
95+
public IReadOnlyList<string> LastGenerationAddresses => _lastGenerationAddresses ?? Array.Empty<string>();
96+
97+
/// <summary>Sorted list of addressable labels that were emitted in the last generation. Empty array when no snapshot.</summary>
98+
public IReadOnlyList<string> LastGenerationLabels => _lastGenerationLabels ?? Array.Empty<string>();
99+
100+
/// <summary>
101+
/// Records the snapshot of the last successful generation: addresses, labels, and the
102+
/// generator settings (filename, label filter) that were used at that moment. Both lists are
103+
/// stored sorted so subsequent set-diffs can be done in O(n+m) without re-sorting at read time.
104+
/// Persists immediately via <c>Save(true)</c>.
105+
/// </summary>
106+
internal void RecordGeneration(IReadOnlyList<string> addresses, IReadOnlyList<string> labels)
107+
{
108+
_lastGenerationUtcTicks = DateTime.UtcNow.Ticks;
109+
_lastGenerationIdCount = addresses?.Count ?? 0;
110+
_lastGenerationLabelCount = labels?.Count ?? 0;
111+
_lastGenerationFilenameUsed = ScriptFilename;
112+
_lastGenerationLabelFilterUsed = AddressableLabel;
113+
114+
_lastGenerationAddresses = SortedCopy(addresses);
115+
_lastGenerationLabels = SortedCopy(labels);
116+
117+
Save(true);
118+
}
119+
120+
private static string[] SortedCopy(IReadOnlyList<string> source)
121+
{
122+
if (source == null || source.Count == 0)
123+
{
124+
return Array.Empty<string>();
125+
}
126+
127+
var copy = new string[source.Count];
128+
129+
for (var i = 0; i < source.Count; i++)
130+
{
131+
copy[i] = source[i];
132+
}
133+
134+
Array.Sort(copy, StringComparer.Ordinal);
135+
return copy;
136+
}
137+
69138
/// <summary>
70139
/// Validates <paramref name="identifier"/> for use as a C# script filename / enum name.
71140
/// Returns <c>true</c> when valid; populates <paramref name="error"/> on failure.

0 commit comments

Comments
 (0)