Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
625efe6
fix: remove dangling asmdef GUID reference
CoderGamester Jul 28, 2026
944602c
fix: converge repo URLs, sample description, and asmdef naming/consis…
CoderGamester Jul 28, 2026
8a25068
docs: update the changelog to reflect the changes from the user persp…
CoderGamester Jul 29, 2026
e2f4761
feat: give the ServicesPlayground sample its own assembly
CoderGamester Jul 29, 2026
fadc731
fix: assign default actions when swapping in InputSystemUIInputModule
CoderGamester Jul 29, 2026
beb1608
fix: pool disposal + fake-null spawn + addressable-id collisions; RCR…
CoderGamester Aug 2, 2026
a0edbb3
docs: add UNFALSIFIABLE exemption and per-class verdicts to RCR (shar…
CoderGamester Aug 2, 2026
4afc7d6
docs: add the A3-reject verdict class to the RCR exemption rules
CoderGamester Aug 2, 2026
1dc48be
docs: record the first trustworthy coverage baseline (runtime 84.4%)
CoderGamester Aug 2, 2026
b0d1b61
style: use classic asserts in VersionServicesTest per Tests/AGENTS.md §9
CoderGamester Aug 2, 2026
89078f4
docs: add the A6 ENVIRONMENT admission criterion
CoderGamester Aug 2, 2026
dcb8c25
test: RCR-backfill 122 services EditMode tests
CoderGamester Aug 2, 2026
8eef6ff
test: RCR-backfill 58 services PlayMode tests
CoderGamester Aug 3, 2026
aa577c3
test: verdict pass - delete 3 A3 tests, keep 2 unproven A5
CoderGamester Aug 3, 2026
4222692
docs: record the verified coverage baseline and the OPEN rows the aud…
CoderGamester Aug 3, 2026
07abdf1
test: replace one-sided TimeService conversion bounds with two-sided …
CoderGamester Aug 3, 2026
08bfe69
test: strengthen nine services tests that asserted nothing observable
CoderGamester Aug 4, 2026
90f3ea4
docs: close the TimeService shrink-direction OPEN row
CoderGamester Aug 4, 2026
a5b1bb7
docs: add CLOSED as a §13 state and require re-derivation to close a row
CoderGamester Aug 4, 2026
dede053
test: write back 3 RCR annotations whose observation was recorded but…
CoderGamester Aug 4, 2026
a9097eb
docs+test: Smoke RCR exemption, the three unannotated states, and the…
CoderGamester Aug 4, 2026
809b5b5
docs: align XML doc comments with AGENTS.md §6.6
CoderGamester Aug 4, 2026
f440d49
test: run the TickService singleton mutation the exemption said did n…
CoderGamester Aug 4, 2026
a5a0a75
fix(tick): delete the unreachable singleton guard and re-point its te…
CoderGamester Aug 4, 2026
3fe9127
docs: hold private-member comments to the higher bar in AGENTS.md §6.6
CoderGamester Aug 4, 2026
b0f6e2b
docs: document the remaining public surface, per AGENTS.md §6.6
CoderGamester Aug 4, 2026
1b06b41
docs: refine Services 2.1.2 release changelog
CoderGamester Aug 4, 2026
2a38ec4
ci: add release-preflight check for develop->master PRs
CoderGamester Aug 4, 2026
73dffec
ci: add release-preflight check for develop->master PRs
CoderGamester Aug 4, 2026
a16f0b6
fix: restore CRLF line endings in CHANGELOG.md
CoderGamester Aug 4, 2026
bdf9bb3
build: exclude .github/ from the published tarball
CoderGamester Aug 4, 2026
528b939
ci: add release-preflight check for develop->master PRs
CoderGamester Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/release-preflight.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Release preflight — required status check for develop -> master PRs.
#
# Installed into each package repo by:
# release.py install-preflight <package>
#
# WHY THIS EXISTS
# These are solo repos: GitHub refuses to let a PR author review their own PR
# (HTTP 422 "Review cannot be requested from pull request author"), so a human
# review gate is unavailable. This gives a real blocking gate instead — the same
# package-local gates the release driver enforces, run before the merge rather
# than after it.
#
# THIS FILE MUST STAY OUT OF THE PUBLISHED TARBALL
# `.github/` IS packed by default — the published googlesheetimporter 0.7.2 asset
# still contains `package/.github/workflows/openai.yml`. Each package therefore
# lists `.github/` in its `.gitignore`, which Unity's packer uses as its
# pack-ignore list; git keeps tracking the file regardless, since .gitignore does
# not untrack existing paths. Measured on a real clone: 434 -> 433 entries,
# `.github` 1 -> 0, `Runtime` unchanged.
#
# Do NOT verify this on a copy with `.git` removed: the packer behaves differently
# without a repo and reports `.github` as excluded when it is not. `G24` also
# surfaces it as an unexpected added file if the ignore line is ever dropped.
#
# WHAT IT DOES NOT CHECK
# Only gates decidable from the package directory plus the base ref, so the check
# needs no token, no submodules and no network: G7 (bare SemVer), G8/G9 (CHANGELOG
# heading matches package.json and is newest+highest), G10 (date sane), G11
# (version advances past master), G15 (the PR touches both files). Remote-state
# gates (G0-G6, G12-G14) and the whole tarball chain (G20-G27) run locally in
# `release.py preflight` / `pack` before the PR is opened.

name: release-preflight

on:
pull_request:
branches: [master]

permissions:
contents: read

concurrency:
group: release-preflight-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
preflight:
runs-on: ubuntu-latest
steps:
# `lfs: true` is load-bearing, not a nicety. The default (false) leaves every
# LFS-tracked file as a ~130-byte pointer stub, which would make G28 fail on
# every PR. With it on, G28 becomes a genuine check that the LFS objects are
# FETCHABLE from the remote — the exact failure that hit uiservice, where the
# working tree held stubs while the published 1.2.1 had real content.
- name: Check out the package
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
persist-credentials: false

# The gate logic is shared rather than vendored into six repos, so there is
# one source of truth. `ref` is explicit: actions/checkout defaults to the
# target repo's DEFAULT branch (master), where the tooling does not exist
# yet — omitting it fails with "No such file or directory". Retarget this to
# master once the skill is merged there.
- name: Check out the release tooling
uses: actions/checkout@v4
with:
repository: CoderGamester/Frameworks
ref: develop
path: .release-tooling
sparse-checkout: .claude/skills/unity-package-release/scripts
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Preflight
run: |
python3 .release-tooling/.claude/skills/unity-package-release/scripts/release.py \
preflight-pr --path . --base "origin/${{ github.base_ref }}"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,6 @@ crashlytics-build.properties

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

# CI config: tracked in git, excluded from the published UPM tarball
.github/
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [2.1.2] - 2026-08-04

**Changed**:
- Added the `com.unity.test-framework.performance` (3.5.0) dependency so the package's test assemblies compile when tests are enabled.

**Fixed**:
- Fixed `GameObjectPool.Dispose(false)` and its generic equivalent so they preserve the sample entity when requested.
- Fixed pooling of Unity objects so destroyed pooled `GameObject` and `Behaviour` instances are skipped instead of being returned to callers.
- Fixed Addressable ID enum generation so addresses that sanitize to the same C# identifier receive distinct members.
- Fixed the Services Playground Input System setup by assigning default actions when its UI module is created, so sample controls respond as expected.

## [2.1.1] - 2026-07-04

**Changed**:
Expand Down
4 changes: 4 additions & 0 deletions Editor/AddressableIds/AddressableIdsEditorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,13 @@ public string AddressableLabel
? default
: new DateTime(_lastGenerationUtcTicks, DateTimeKind.Utc);

/// <summary>How many ids the last generation emitted.</summary>
public int LastGenerationIdCount => _lastGenerationIdCount;
/// <summary>How many labels the last generation emitted.</summary>
public int LastGenerationLabelCount => _lastGenerationLabelCount;
/// <summary>Script filename the last generation used; a change from the current setting makes the snapshot stale.</summary>
public string LastGenerationFilenameUsed => _lastGenerationFilenameUsed ?? string.Empty;
/// <summary>Label filter the last generation used; a change from the current setting makes the snapshot stale.</summary>
public string LastGenerationLabelFilterUsed => _lastGenerationLabelFilterUsed ?? string.Empty;

/// <summary>Sorted list of addressable addresses that were emitted in the last generation. Empty array when no snapshot.</summary>
Expand Down
44 changes: 26 additions & 18 deletions Editor/AddressableIds/AddressableIdsGeneratorUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,24 @@ public static string ResolveScriptPath(AddressableIdsEditorSettings settings)
return scriptPath;
}

/// <summary>
/// Pure string-builder: turns <paramref name="addresses"/> into the generated C# enum member-block
/// source text (opening brace, one member per address, closing brace) that <see cref="GenerateScript"/>
/// inserts after the <c>public enum &lt;Name&gt;</c> header line. Takes no <c>AssetDatabase</c>
/// dependency — operates on plain address strings only, so it is directly unit-testable.
/// </summary>
internal static string BuildEnumSource(IReadOnlyCollection<string> addresses)
{
var addressList = addresses as IReadOnlyList<string> ?? new List<string>(addresses);
var stringBuilder = new StringBuilder();

stringBuilder.AppendLine("\t{");
AppendAddressEnumMembers(stringBuilder, addressList);
stringBuilder.AppendLine("\t}");

return stringBuilder.ToString();
}

private static List<AddressableAssetEntry> GetAssetList()
{
var assetList = new List<AddressableAssetEntry>();
Expand Down Expand Up @@ -278,9 +296,7 @@ private static void GenerateScript(List<AddressableAssetEntry> assetList, Addres
stringBuilder.AppendLine("{");

stringBuilder.AppendLine($"\tpublic enum {settings.ScriptFilename}");
stringBuilder.AppendLine("\t{");
GenerateAddressEnums(stringBuilder, assetList);
stringBuilder.AppendLine("\t}");
stringBuilder.Append(BuildEnumSource(ExtractAddresses(assetList)));

stringBuilder.AppendLine("");
stringBuilder.AppendLine("\tpublic enum AddressableLabel");
Expand Down Expand Up @@ -485,27 +501,22 @@ private static void ProcessData(IList<AddressableAssetEntry> assetList, Addressa
}
}

private static void GenerateAddressEnums(StringBuilder stringBuilder, IReadOnlyList<AddressableAssetEntry> assetList)
private static void AppendAddressEnumMembers(StringBuilder stringBuilder, IReadOnlyList<string> addresses)
{
var addedNames = new List<string>();

for (var i = 0; i < assetList.Count; i++)
for (var i = 0; i < addresses.Count; i++)
{
var name = ResolveSanitizedEnumName(assetList[i].address, addedNames, out _);
var name = ResolveSanitizedEnumName(addresses[i], addedNames, out _);
addedNames.Add(name);

stringBuilder.Append("\t\t");
stringBuilder.Append(GetCleanName(assetList[i].address, true));
stringBuilder.Append(i + 1 == assetList.Count ? "\n" : ",\n");
stringBuilder.Append(name);
stringBuilder.Append(i + 1 == addresses.Count ? "\n" : ",\n");
}
}

/// <summary>
/// Resolves the enum-member name for a given Addressable <paramref name="address"/>, applying the
/// same <c>name_filetype</c> fallback that <see cref="GenerateAddressEnums"/> uses when the cleaned
/// name collides with one already in <paramref name="seenNames"/>. Sets <paramref name="collided"/>
/// to <c>true</c> when the fallback path was taken.
/// </summary>
// Disambiguates a collision by re-cleaning the address with its file extension appended (name_filetype).
private static string ResolveSanitizedEnumName(string address, List<string> seenNames, out bool collided)
{
var name = GetCleanName(address, true);
Expand Down Expand Up @@ -566,10 +577,7 @@ private static List<string> DetectNullAssetTypes(IReadOnlyList<AddressableAssetE
return nulls;
}

/// <summary>
/// Returns the elements of <paramref name="left"/> that are not in <paramref name="right"/>.
/// Both inputs MUST be pre-sorted ordinally; output is also sorted ordinally.
/// </summary>
// Both inputs must already be sorted ordinally; nothing here enforces it.
private static List<string> SortedSetDiff(IReadOnlyList<string> left, IReadOnlyList<string> right)
{
var result = new List<string>();
Expand Down
10 changes: 10 additions & 0 deletions Editor/AssetsImporter/AssetConfigsImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,16 @@ public void Import(string assetsFolderPath = null)
$"To: '{typeof(TScriptableObject).Name}' - From '{scriptableObject.AssetsFolderPath}' ");
}

/// <summary>The filename fragment an id is matched against; override when assets are not named after the id.</summary>
protected virtual string IdPattern(TId id)
{
return id.ToString();
}

/// <summary>
/// Pairs each id with the asset whose path contains it, skipping ids with no match.
/// Override to change how ids are mapped onto the discovered assets.
/// </summary>
protected virtual List<Pair<TId, AssetReference>> OnImportIds(TScriptableObject scriptableObject,
List<string> assetGuids,
List<string> assetsPaths)
Expand All @@ -142,6 +147,7 @@ protected virtual List<Pair<TId, AssetReference>> OnImportIds(TScriptableObject
return list;
}

/// <summary>Index of the first path containing <c>/{id}.</c>, or -1; the dot is what stops a prefix matching.</summary>
protected int IndexOfId(string id, IList<string> assetsPath)
{
for (var i = 0; i < assetsPath.Count; i++)
Expand All @@ -155,16 +161,20 @@ protected int IndexOfId(string id, IList<string> assetsPath)
return -1;
}

/// <summary>Runs after the import has written the asset; the base implementation does nothing.</summary>
protected virtual void OnImportComplete(TScriptableObject scriptableObject) { }
}

/// <inheritdoc />
public abstract class AssetsConfigsGeneratorImporter<TAsset> : IAssetConfigsGeneratorImporter
{
/// <summary>Assembly-qualified name of the id enum the generated script should use.</summary>
public abstract string TIdName { get; }

/// <summary>Assembly-qualified name of the configs ScriptableObject the generated script should target.</summary>
public abstract string TScriptableObjectName { get; }

/// <summary>When true the previous generated script is kept as a backup before being overwritten.</summary>
public virtual bool CacheScriptAsOld => true;

/// <inheritdoc />
Expand Down
4 changes: 4 additions & 0 deletions Editor/Explorer/Tabs/AddressableIdsTab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public class AddressableIdsTab : ServiceTab
private static readonly Color OkColor = new Color(0.6f, 0.9f, 0.6f);
private static readonly Color MutedColor = new Color(0.7f, 0.7f, 0.7f);

/// <inheritdoc />
public override string DisplayName => "Addressable Ids";
/// <inheritdoc />
protected override int RefreshIntervalMs => 2000;

private TextField _filenameField;
Expand All @@ -43,6 +45,7 @@ public class AddressableIdsTab : ServiceTab
private VisualElement _removedList;
private VisualElement _warningsList;

/// <inheritdoc />
protected override void BuildUi()
{
var scroll = new ScrollView(ScrollViewMode.Vertical);
Expand Down Expand Up @@ -174,6 +177,7 @@ protected override void BuildUi()
RefreshOutput();
}

/// <inheritdoc />
protected override void Refresh()
{
var settings = AddressableIdsEditorSettings.instance;
Expand Down
11 changes: 4 additions & 7 deletions Editor/Explorer/Tabs/AssetResolverTab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ namespace GameLovers.Services.Editor.Explorer.Tabs
/// </summary>
public class AssetResolverTab : ServiceTab
{
/// <inheritdoc />
public override string DisplayName => "Asset Resolver";

private ScrollView _scroll;
private VisualElement _tree;
private Toggle _destructiveToggle;
private Label _countLabel;

/// <inheritdoc />
protected override void BuildUi()
{
var header = new VisualElement();
Expand Down Expand Up @@ -51,6 +53,7 @@ protected override void BuildUi()
Add(bar);
}

/// <inheritdoc />
protected override void Refresh()
{
var resolver = TryResolve<IAssetResolverService>() as AssetResolverService;
Expand Down Expand Up @@ -136,13 +139,7 @@ protected override void Refresh()
}
}

/// <summary>
/// Builds a deterministic digest of every piece of state the rebuild path renders:
/// the not-bound branch, the destructive-toggle flag (gates per-row Unload buttons),
/// and per-row <c>(assetType, idType, id, loaded)</c> tuples. When two consecutive
/// refreshes produce the same digest the rebuild can be skipped — keeping rapid
/// foldout clicks from getting destroyed mid-click by the 250 ms timer.
/// </summary>
// Must cover every input the rebuild conditions on, the destructive toggle included; see AGENTS.md §4.
private string ComputeDigest(AssetResolverService resolver)
{
if (resolver == null)
Expand Down
4 changes: 4 additions & 0 deletions Editor/Explorer/Tabs/AssetsImporterTab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ namespace GameLovers.Services.Editor.Explorer.Tabs
/// </summary>
public class AssetsImporterTab : ServiceTab
{
/// <inheritdoc />
public override string DisplayName => "Assets Importer";
/// <inheritdoc />
protected override int RefreshIntervalMs => 2000;

private Toggle _autoImportToggle;
private VisualElement _importerList;
private List<ImportData> _cachedImporters;

/// <inheritdoc />
protected override void BuildUi()
{
var scroll = new ScrollView(ScrollViewMode.Vertical);
Expand Down Expand Up @@ -54,6 +57,7 @@ protected override void BuildUi()
Add(bar);
}

/// <inheritdoc />
protected override void Refresh()
{
_autoImportToggle.SetValueWithoutNotify(AssetsImporterEditorSettings.instance.AutoUpdateOnRefresh);
Expand Down
4 changes: 4 additions & 0 deletions Editor/Explorer/Tabs/CoroutineTab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ namespace GameLovers.Services.Editor.Explorer.Tabs
/// </summary>
public class CoroutineTab : ServiceTab
{
/// <inheritdoc />
public override string DisplayName => "Coroutine";

private ScrollView _scroll;
private VisualElement _list;
private Label _totalLabel;

/// <inheritdoc />
protected override void BuildUi()
{
var header = new VisualElement();
Expand All @@ -34,6 +36,7 @@ protected override void BuildUi()
Add(bar);
}

/// <inheritdoc />
protected override void Refresh()
{
_list.Clear();
Expand Down Expand Up @@ -87,6 +90,7 @@ protected override void Refresh()
// Forcibly clear the active-coroutine list synchronously when the user stops
// play mode. Belt-and-braces against bootstraps that fail to dispose the
// coroutine service / call MainInstaller.Clean() in OnDestroy.
/// <inheritdoc />
protected override void OnExitingPlayMode()
{
_totalLabel.text = "Active: 0";
Expand Down
Loading
Loading