Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 54 additions & 8 deletions .github/workflows/release-preflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ name: release-preflight
on:
pull_request:
branches: [master]
types: [opened, synchronize, reopened, closed]

permissions:
contents: read
Expand All @@ -45,6 +46,7 @@ concurrency:

jobs:
preflight:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
# `lfs: true` is load-bearing, not a nicety. The default (false) leaves every
Expand All @@ -53,32 +55,76 @@ jobs:
# 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
uses: actions/checkout@v7
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.
# one source of truth. Pinning its full commit keeps an unchanged package PR
# reproducible when Frameworks/develop advances; the installer performs
# intentional snapshot updates across all package repositories.
- name: Check out the release tooling
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
repository: CoderGamester/Frameworks
ref: develop
# Immutable tooling snapshot. Update intentionally through
# `release.py install-preflight`, never by following a moving branch.
ref: be8c147f3d12d94a4055d556ffdc0a26e077e5ec
path: .release-tooling
sparse-checkout: .claude/skills/unity-package-release/scripts
persist-credentials: false

- name: Assert the release tooling checkout
run: |
test -f .release-tooling/.claude/skills/unity-package-release/scripts/release.py

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
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 }}"

# GITHUB_TOKEN cannot dispatch into another repository. Add a fine-grained
# FRAMEWORKS_DISPATCH_TOKEN secret (Contents: read/write on Frameworks) to each
# package repository to activate this hand-off. A missing token is reported as
# a warning rather than turning an already-merged release PR red.
notify-release:
if: github.event.action == 'closed' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Notify the host release workflow
env:
DISPATCH_TOKEN: ${{ secrets.FRAMEWORKS_DISPATCH_TOKEN }}
PACKAGE_REPOSITORY: ${{ github.event.repository.name }}
run: |
if [ -z "$DISPATCH_TOKEN" ]; then
echo "::warning::FRAMEWORKS_DISPATCH_TOKEN is not configured; skipping host release notification."
exit 0
fi

case "$PACKAGE_REPOSITORY" in
Unity-GameData) package=gamedata ;;
Unity-GoogleSheet-Importer) package=googlesheetimporter ;;
Unity-MobileServices) package=mobileservices ;;
Unity-Services) package=services ;;
Statechart-HFSM) package=statechart ;;
Unity-UiService) package=uiservice ;;
*)
echo "::error::Unknown package repository: $PACKAGE_REPOSITORY"
exit 1
;;
esac

payload=$(printf '{"event_type":"upm-release","client_payload":{"package":"%s","phase":"status"}}' "$package")
curl --fail-with-body --location --request POST \
--url https://api.github.com/repos/CoderGamester/Frameworks/dispatches \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer $DISPATCH_TOKEN" \
--header "X-GitHub-Api-Version: 2022-11-28" \
--data "$payload"
369 changes: 59 additions & 310 deletions AGENTS.md

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@
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).
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [2.2.0] - 2026-10-17

**New**:
- **Data storage backends**: `DataService` now persists through a pluggable `IDataStorage` (`HasKey`, `Read`, `Write`, `Delete` by string key). `new DataService()` keeps using `PlayerPrefs` through the new `PlayerPrefsDataStorage`, so existing saves load unchanged.
- **File saves**: `FileDataStorage` stores each type as a `{key}.json` file in `Application.persistentDataPath` or a directory you choose. Every write goes to a temporary file that then replaces the previous one, so an interrupted save keeps the last complete value.
- **WebGL saves**: `BrowserLocalDataStorage` stores data in the browser's `localStorage`, which is keyed by page origin and survives new uploads on hosts that change the build path every upload, unlike `PlayerPrefs` and `persistentDataPath` on WebGL. Writes are synchronous; it throws in the Editor and on other platforms instead of silently storing nothing.
- **Key prefix**: `new DataService(storage, keyPrefix)` stores each type under `{keyPrefix}.{typeof(T).Name}`, so separate profiles or modes keep separate saves. Without a prefix, keys stay `typeof(T).Name`.

**Changed**:
- The Services Explorer Data tab checks and deletes saved values through the bound service's storage instead of always using `PlayerPrefs`.
- Lowered the declared Unity minimum from 6000.3 to 6000.0 and documented 6000.0.x, 6000.3.x, and 6000.5.x as compatibility reference streams.

**Fixed**:
- The AssetResolver sample now assigns default actions to the Input System UI module it adds at runtime, so its buttons respond to input instead of looking interactive and receiving nothing.

**Docs**:
- The install instructions now list UniTask as an explicit Git dependency, because Unity does not resolve Git dependencies transitively.

## [2.1.2] - 2026-08-04

Expand Down
11 changes: 0 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,12 +1 @@
# Claude Code Guide — GameLovers Services

This package's contributor/agent guide lives in `AGENTS.md`.
Claude Code will automatically import it below.

@AGENTS.md

## Claude-Specific Notes

- Treat `AGENTS.md` as the source of truth.
- If anything in this file appears to conflict with `AGENTS.md`, prefer `AGENTS.md`.
- For user-facing usage, see `README.md`.
16 changes: 12 additions & 4 deletions Editor/AddressableIds/AddressableIdsEditorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,23 @@ public string AddressableLabel
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>
/// <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>
/// <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>
/// <summary>
/// Sorted list of addressable addresses that were emitted in the last generation. Empty array when no snapshot.
/// </summary>
public IReadOnlyList<string> LastGenerationAddresses => _lastGenerationAddresses ?? Array.Empty<string>();

/// <summary>Sorted list of addressable labels that were emitted in the last generation. Empty array when no snapshot.</summary>
/// <summary>
/// Sorted list of addressable labels that were emitted in the last generation. Empty array when no snapshot.
/// </summary>
public IReadOnlyList<string> LastGenerationLabels => _lastGenerationLabels ?? Array.Empty<string>();

/// <summary>
Expand Down
14 changes: 10 additions & 4 deletions Editor/AssetsImporter/AssetConfigsImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ 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>
/// <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();
Expand Down Expand Up @@ -147,7 +149,9 @@ 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>
/// <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 @@ -171,7 +175,9 @@ public abstract class AssetsConfigsGeneratorImporter<TAsset> : IAssetConfigsGene
/// <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>
/// <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>
Expand Down Expand Up @@ -298,4 +304,4 @@ private string GenerateImporterScript(Type importerType, Type assetType)
return stringBuilder.ToString();
}
}
}
}
36 changes: 18 additions & 18 deletions Editor/Explorer/Tabs/AddressableIdsTab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ public class AddressableIdsTab : ServiceTab
{
private const string PendingPlaceholderText = "Click to compute";
private const string PendingFoldoutText = "Details (click 'Check for stale Ids' first)";
private static readonly Color WarnColor = new Color(0.95f, 0.75f, 0.30f);
private static readonly Color ErrorColor = new Color(0.9f, 0.5f, 0.4f);
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);
private static readonly Color _warnColor = new Color(0.95f, 0.75f, 0.30f);
private static readonly Color _errorColor = new Color(0.9f, 0.5f, 0.4f);
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";
Expand Down Expand Up @@ -131,7 +131,7 @@ protected override void BuildUi()

_lastGenerationLabel = new Label();
_lastGenerationLabel.style.fontSize = 10;
_lastGenerationLabel.style.color = new StyleColor(MutedColor);
_lastGenerationLabel.style.color = new StyleColor(_mutedColor);
_lastGenerationLabel.style.marginTop = 2;
scroll.Add(_lastGenerationLabel);

Expand All @@ -152,7 +152,7 @@ protected override void BuildUi()

_pendingSummaryLabel = new Label(PendingPlaceholderText);
_pendingSummaryLabel.AddToClassList("row-label");
_pendingSummaryLabel.style.color = new StyleColor(MutedColor);
_pendingSummaryLabel.style.color = new StyleColor(_mutedColor);
pendingRow.Add(_pendingSummaryLabel);
scroll.Add(pendingRow);

Expand Down Expand Up @@ -197,20 +197,20 @@ private void RefreshOutput()
if (!freshness.ScriptExists)
{
_freshnessBanner.text = "Not generated yet — click 'Generate Addressable Ids'.";
_freshnessBanner.style.color = new StyleColor(ErrorColor);
_freshnessBanner.style.color = new StyleColor(_errorColor);
_freshnessBanner.style.display = DisplayStyle.Flex;
}
else if (freshness.IsStale)
{
_freshnessBanner.text = $"Out of date — Addressables modified after the file was generated " +
$"({freshness.LatestSourceWriteTime:HH:mm:ss} > {freshness.ScriptWriteTime:HH:mm:ss}).";
_freshnessBanner.style.color = new StyleColor(WarnColor);
_freshnessBanner.style.color = new StyleColor(_warnColor);
_freshnessBanner.style.display = DisplayStyle.Flex;
}
else
{
_freshnessBanner.text = $"Up to date — script newer than Addressables ({freshness.ScriptWriteTime:HH:mm:ss}).";
_freshnessBanner.style.color = new StyleColor(OkColor);
_freshnessBanner.style.color = new StyleColor(_okColor);
_freshnessBanner.style.display = DisplayStyle.Flex;
}

Expand All @@ -235,7 +235,7 @@ private void RefreshOutput()
private void ResetPendingChanges()
{
_pendingSummaryLabel.text = PendingPlaceholderText;
_pendingSummaryLabel.style.color = new StyleColor(MutedColor);
_pendingSummaryLabel.style.color = new StyleColor(_mutedColor);
_pendingFoldout.text = PendingFoldoutText;
_pendingFoldout.value = false;
_addedList.Clear();
Expand Down Expand Up @@ -269,24 +269,24 @@ private void PopulateDiffFoldout(DiffResult diff)
_removedList.Clear();
_warningsList.Clear();

AppendDiffSubsection(_addedList, $"Added addresses ({diff.AddedAddresses.Count})", diff.AddedAddresses, OkColor);
AppendDiffSubsection(_addedList, $"Added labels ({diff.AddedLabels.Count})", diff.AddedLabels, OkColor);
AppendDiffSubsection(_addedList, $"Added addresses ({diff.AddedAddresses.Count})", diff.AddedAddresses, _okColor);
AppendDiffSubsection(_addedList, $"Added labels ({diff.AddedLabels.Count})", diff.AddedLabels, _okColor);

AppendDiffSubsection(_removedList, $"Removed addresses ({diff.RemovedAddresses.Count})", diff.RemovedAddresses, ErrorColor);
AppendDiffSubsection(_removedList, $"Removed labels ({diff.RemovedLabels.Count})", diff.RemovedLabels, ErrorColor);
AppendDiffSubsection(_removedList, $"Removed addresses ({diff.RemovedAddresses.Count})", diff.RemovedAddresses, _errorColor);
AppendDiffSubsection(_removedList, $"Removed labels ({diff.RemovedLabels.Count})", diff.RemovedLabels, _errorColor);

AppendDiffSubsection(_warningsList,
$"Sanitized-name collisions ({diff.SanitizedNameCollisions.Count})",
diff.SanitizedNameCollisions, WarnColor);
diff.SanitizedNameCollisions, _warnColor);
AppendDiffSubsection(_warningsList,
$"Entries with null AssetType ({diff.NullAssetTypeAddresses.Count})",
diff.NullAssetTypeAddresses, WarnColor);
diff.NullAssetTypeAddresses, _warnColor);

if (!diff.HasSnapshot)
{
var lbl = new Label("No snapshot recorded yet — generate once to enable add/remove diffs.");
lbl.style.fontSize = 10;
lbl.style.color = new StyleColor(MutedColor);
lbl.style.color = new StyleColor(_mutedColor);
lbl.style.marginTop = 4;
_addedList.Add(lbl);
}
Expand Down Expand Up @@ -338,7 +338,7 @@ private static string FormatSummary(DiffResult diff)

private static Color SummaryColor(DiffResult diff)
{
return TotalDiffCount(diff) == 0 ? OkColor : WarnColor;
return TotalDiffCount(diff) == 0 ? _okColor : _warnColor;
}

private static int TotalDiffCount(DiffResult diff)
Expand Down
19 changes: 9 additions & 10 deletions Editor/Explorer/Tabs/DataTab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace GameLovers.Services.Editor.Explorer.Tabs
{
/// <summary>
/// Displays all data entries held by <see cref="IDataService"/>.
/// Supports Save, Load, AddOrReplace, and Delete PlayerPrefs key.
/// Supports Save, Load, and deleting the saved value from the service's storage.
/// </summary>
public class DataTab : ServiceTab
{
Expand Down Expand Up @@ -81,21 +81,21 @@ protected override void Refresh()
var dataType = kvp.Key;
var data = kvp.Value;
var json = TrySerializeJson(data);
var prefsKey = dataType.Name;
var hasPrefs = PlayerPrefs.HasKey(prefsKey);
var storageKey = dataService.GetStorageKey(dataType);
var isSaved = dataService.Storage.HasKey(storageKey);

var container = new VisualElement();
container.style.marginBottom = 6;

var row = MakeRow(dataType.Name, hasPrefs ? "[saved]" : "[in-memory only]");
var row = MakeRow(dataType.Name, isSaved ? "[saved]" : "[in-memory only]");

var saveBtn = MakeRowButton("Save", () => OnSave(dataService, dataType));
row.Add(saveBtn);

var loadBtn = MakeRowButton("Load", () => { OnLoad(dataService, dataType); Refresh(); });
row.Add(loadBtn);

var deleteBtn = MakeRowButton("Del PlayerPrefs", () => OnDeletePrefs(prefsKey), danger: true);
var deleteBtn = MakeRowButton("Del Saved", () => OnDeleteSaved(dataService, storageKey), danger: true);
row.Add(deleteBtn);

container.Add(row);
Expand Down Expand Up @@ -173,16 +173,15 @@ private void OnLoad(DataService dataService, Type dataType)
}
}

private void OnDeletePrefs(string key)
private void OnDeleteSaved(DataService dataService, string key)
{
if (!EditorUtility.DisplayDialog("Delete PlayerPrefs Key",
$"Delete PlayerPrefs key \"{key}\"? This cannot be undone.", "Delete", "Cancel"))
if (!EditorUtility.DisplayDialog("Delete Saved Data",
$"Delete the saved value \"{key}\"? This cannot be undone.", "Delete", "Cancel"))
{
return;
}

PlayerPrefs.DeleteKey(key);
PlayerPrefs.Save();
dataService.Storage.Delete(key);
Refresh();
}
}
Expand Down
Loading
Loading