commit 38dcec46bb327948d2863887e6376c84a3adff5d Author: cneicy Date: Wed Aug 26 02:50:08 2026 +0800 chore: initialize standalone UPM package diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..08c36cb --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish UPM package + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }} + steps: + - name: Fetch tagged revision + shell: bash + run: | + set -eu + ref="${{ gitea.sha }}" + test -n "$ref" + git init . + git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.App.git" + git fetch --depth=1 origin "$ref" + git checkout --detach FETCH_HEAD + + - name: Validate immutable release version + shell: bash + run: | + set -eu + tag="$(git describe --exact-match --tags HEAD)" + version="$(node -p "require('./package.json').version")" + test "$tag" = "v$version" + npm pack --dry-run + + - name: Publish to ShrinkSDK registry + shell: bash + run: | + set -eu + : "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}" + npmrc="$HOME/.npmrc" + cleanup() { rm -f "$npmrc"; } + trap cleanup EXIT + printf '%s\n' \ + 'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \ + '//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc" + npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/ \ No newline at end of file diff --git a/.gitea/workflows/unity-verify.yml b/.gitea/workflows/unity-verify.yml new file mode 100644 index 0000000..28adf2d --- /dev/null +++ b/.gitea/workflows/unity-verify.yml @@ -0,0 +1,39 @@ +name: Verify standalone Unity package + +on: + workflow_dispatch: + +jobs: + editmode: + runs-on: unity-2022.3.62f3 + container: + image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3 + volumes: + - /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity + - /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses + steps: + - name: Fetch selected revision + shell: bash + run: | + set -eu + ref="${{ gitea.sha }}" + git init . + git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.App.git" + git fetch --depth=1 origin "$ref" + git checkout --detach FETCH_HEAD + + - name: Run package EditMode tests + shell: bash + run: | + set -eu + unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)" + test -n "$unity_bin" + "$unity_bin" \ + -batchmode \ + -nographics \ + -quit \ + -projectPath "$PWD/Development~/UnityProject" \ + -runTests \ + -testPlatform EditMode \ + -testResults "$PWD/TestResults/editmode.xml" \ + -logFile "$PWD/TestResults/unity.log" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..246935d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/Development~/UnityProject/[Ll]ibrary/ +/Development~/UnityProject/[Tt]emp/ +/Development~/UnityProject/[Oo]bj/ +/Development~/UnityProject/[Ll]ogs/ +/Development~/UnityProject/[Uu]ser[Ss]ettings/ +/Development~/UnityProject/TestResults/ +/Tools~/**/[Bb]in/ +/Tools~/**/[Oo]bj/ +*.user +*.DotSettings.user diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..700f03d --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +.git/ +.gitea/ +Development~/ +Tools~/ +*.csproj +*.sln +*.user +*.DotSettings.user diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..453ee5f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +本文件记录 `ShrinkDataSaver.Integration.App` 的包内变更。 + +## [Unreleased] + +### Added + +- 拆分 `IShrinkDataSaverReader` / `IShrinkDataSaverWriter` 能力契约。 +- 支持通过 ShrinkContext intercept 向派生上下文提供只读 DataSaver 视图。 + +## [0.1.0] - 2026-05-18 + +### Added + +- 新增 `ShrinkDataSaver` 与 `ShrinkApp.Core` 的桥接包。 +- 新增 `ShrinkDataSaverAppInstaller`,把 DataSaver 纳入统一宿主启动链。 +- 新增 `ShrinkDataSaverService` 作为宿主服务门面。 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..73974a0 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3c6cf6fb5b92c5848b40e963a80a58c1 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Development~/UnityProject/.gitignore b/Development~/UnityProject/.gitignore new file mode 100644 index 0000000..aa2eb00 --- /dev/null +++ b/Development~/UnityProject/.gitignore @@ -0,0 +1,6 @@ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +TestResults/ \ No newline at end of file diff --git a/Development~/UnityProject/Assets/.gitkeep b/Development~/UnityProject/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Development~/UnityProject/Packages/manifest.json b/Development~/UnityProject/Packages/manifest.json new file mode 100644 index 0000000..c2a127f --- /dev/null +++ b/Development~/UnityProject/Packages/manifest.json @@ -0,0 +1,15 @@ +{ + "scopedRegistries": [ + { + "name": "ShrinkSDK", + "url": "https://git.crash.work/api/packages/ShrinkSDK/npm/", + "scopes": [ + "com.cneicy" + ] + } + ], + "dependencies": { + "com.unity.test-framework": "1.1.33", + "com.cneicy.shrink-datasaver-integration-app": "file:../../.." + } +} diff --git a/Development~/UnityProject/ProjectSettings/ProjectVersion.txt b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..587f809 --- /dev/null +++ b/Development~/UnityProject/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2022.3.62f3 +m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e11b9e --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# ShrinkDataSaver.Integration.App + +`ShrinkDataSaver.Integration.App` 是 `ShrinkDataSaver` 与 `ShrinkApp.Core` 的桥接层。它的职责不是重新实现存档系统,而是把 `ShrinkDataSaver` 接到统一宿主链里,让项目在接入 `ShrinkApp` 时由宿主接管初始化顺序。 + +## 当前能力 + +- 提供原生 `ShrinkDataSaverAppComponent`,发布 `shrink.service.datasaver` +- 在组件激活阶段调用 `ShrinkDataSaverRuntime.Initialize()` +- ContextLoader 下向 `ShrinkApp.Services` 注册可逆的 `ShrinkDataSaverService` 门面 +- 区分 `IShrinkDataSaverReader` 与 `IShrinkDataSaverWriter`;默认上下文取得完整写能力 +- 支持以 `ShrinkDataSaverAccess.MetadataKey = ShrinkDataSaverAccess.ReadOnly` 的 intercept 向派生上下文只暴露读取能力,写接口和具体服务请求会被明确拒绝 +- `ShrinkDataSaverAppInstaller` 仅保留给 ClassicHost 兼容路径,已标记过时 +- 保留 `ShrinkDataSaverBootstrap` 的旧用法兼容;若宿主已接管,旧 bootstrap 会自动退出 + +## 只读上下文 + +```csharp +var communityContext = ctx.Intercept( + ShrinkDataSaverAppComponent.ServiceKey, + new Dictionary + { + [ShrinkDataSaverAccess.MetadataKey] = ShrinkDataSaverAccess.ReadOnly + }); + +var reader = communityContext.Get( + ShrinkDataSaverAppComponent.ServiceKey); +``` + +这是服务能力的最小化暴露:只约束通过 `ShrinkCtx.Get` 解析该键的调用方。它不会限制代码绕过上下文后直接访问静态 API,也不构成不可信模组沙箱。 diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..0b4d654 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 85bcc5ac108273c41acdd89c5e2d0291 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..13ca782 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 79b02908f32342443bd1a986d535a7d2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkDataSaver.Integration.App.asmdef b/Runtime/ShrinkDataSaver.Integration.App.asmdef new file mode 100644 index 0000000..1789bab --- /dev/null +++ b/Runtime/ShrinkDataSaver.Integration.App.asmdef @@ -0,0 +1,18 @@ +{ + "name": "ShrinkDataSaver.Integration.App", + "rootNamespace": "ShrinkDataSaver.Integration.App", + "references": [ + "ShrinkDataSaver.Runtime", + "ShrinkApp.Core.Runtime", + "ShrinkContext.Core.Runtime", + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Runtime/ShrinkDataSaver.Integration.App.asmdef.meta b/Runtime/ShrinkDataSaver.Integration.App.asmdef.meta new file mode 100644 index 0000000..b6e8a15 --- /dev/null +++ b/Runtime/ShrinkDataSaver.Integration.App.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b6cfc65e2b6dc8942866f0f374874564 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkDataSaverAppComponent.cs b/Runtime/ShrinkDataSaverAppComponent.cs new file mode 100644 index 0000000..eb4b937 --- /dev/null +++ b/Runtime/ShrinkDataSaverAppComponent.cs @@ -0,0 +1,65 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using ShrinkApp; +using ShrinkContext; +using ShrinkDataSaver; + +namespace ShrinkDataSaver.Integration.App +{ + /// + /// ShrinkDataSaver 的原生 Cordis 组件(阶段 3): + /// - 提供 shrink.service.datasaver 余效应键(门面服务); + /// - 初始化为前向;停用时的终止动作为最终落盘设置(补偿式收尾: + /// 存档写入本身跨会话持久,不可严格求逆,以 flush 保证一致性,见论文 6.1 扣留与补偿); + /// - 与经典 ShrinkDataSaverAppInstaller(ModuleId "shrink.datasaver")二选一使用。 + /// + public sealed class ShrinkDataSaverAppComponent : IShrinkComponent + { + public const string ServiceKey = "shrink.service.datasaver"; + public const string ModuleKey = "app.module.shrink.datasaver"; + + private static readonly string[] ProvideKeys = { ModuleKey, ServiceKey }; + + public string Name => "shrink.datasaver"; + + public IReadOnlyList Inject => Array.Empty(); + + public IReadOnlyList Provide => ProvideKeys; + + public async UniTask ApplyAsync(ShrinkCtx ctx, object? config) + { + // 编辑器/EditMode 环境下 DontDestroyOnLoad 会抛异常,驱动只在播放模式常驻 + ShrinkDataSaverRuntime.Initialize(new ShrinkDataSaverRuntimeConfig + { + DontDestroyOnLoadDriver = UnityEngine.Application.isPlaying + }); + var facade = new ShrinkDataSaverService(); + ctx.Set(ServiceKey, facade, new ShrinkDataSaverAccessPolicy(facade)); + ctx.Set(ModuleKey, Name); + + if (config is ShrinkAppServices appServices) + { + appServices.Register(facade); + ctx.EffectInverse(() => + { + appServices.TryUnregister(facade); + return UniTask.CompletedTask; + }); + } + + await ctx.EffectAsync(async () => + { + await UniTask.CompletedTask; + return () => + { + // 终止动作(触发式):停用时把防抖中的设置写盘。 + // 磁盘 IO 在线程池完成,不阻塞停用链;未初始化环境安全跳过(SaveAsync 守卫)。 + ShrinkSettings.SaveAsync().Forget(); + return UniTask.CompletedTask; + }; + }); + } + } +} diff --git a/Runtime/ShrinkDataSaverAppComponent.cs.meta b/Runtime/ShrinkDataSaverAppComponent.cs.meta new file mode 100644 index 0000000..d3849d9 --- /dev/null +++ b/Runtime/ShrinkDataSaverAppComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ce2260a6cfecb349abe6119fdf37c12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkDataSaverAppInstaller.cs b/Runtime/ShrinkDataSaverAppInstaller.cs new file mode 100644 index 0000000..a1bdf6b --- /dev/null +++ b/Runtime/ShrinkDataSaverAppInstaller.cs @@ -0,0 +1,108 @@ +#nullable enable + +using System; +using Cysharp.Threading.Tasks; +using ShrinkApp; +using ShrinkContext; + +namespace ShrinkDataSaver.Integration.App +{ + [ShrinkAppModuleInstaller] + [Obsolete("ClassicHost compatibility only. ContextLoader projects should use ShrinkDataSaverAppComponent from a composition root.")] + public sealed class ShrinkDataSaverAppInstaller : IShrinkAppModuleInstaller + { + public string ModuleId => "shrink.datasaver"; + public int Order => -2000; + public System.Collections.Generic.IReadOnlyList DependsOn => Array.Empty(); + + public void RegisterServices(ShrinkAppContext context) + { + context.Services.Register(new ShrinkDataSaverService()); + } + + public UniTask InitializeAsync(ShrinkAppContext context) + { + ShrinkDataSaverRuntime.Initialize(); + return UniTask.CompletedTask; + } + } + + /// 不产生存档写入的读取能力。 + public interface IShrinkDataSaverReader + { + int LoadedSlot { get; } + int GetRecentSlotIndex(); + UniTask GetRecommendedContinueSlotAsync(); + UniTask GetAllMetaAsync(); + } + + /// 完整存档能力;社区模组可通过 intercept 被降权为仅 + public interface IShrinkDataSaverWriter : IShrinkDataSaverReader + { + UniTask SaveSlotAsync(int slotIndex, SaveOptions? options = null); + UniTask LoadSlotAsync(int slotIndex, string? decryptionKey = null); + } + + public static class ShrinkDataSaverAccess + { + public const string MetadataKey = "access"; + public const string ReadOnly = "read-only"; + } + + public sealed class ShrinkDataSaverService : IShrinkDataSaverWriter + { + public int LoadedSlot => ShrinkSave.LoadedSlot; + public int GetRecentSlotIndex() => ShrinkSave.GetRecentSlotIndex(); + public UniTask GetRecommendedContinueSlotAsync() => ShrinkSave.GetRecommendedContinueSlotAsync(); + public UniTask GetAllMetaAsync() => ShrinkSave.GetAllMetaAsync(); + public UniTask SaveSlotAsync(int slotIndex, SaveOptions? options = null) => ShrinkSave.SaveSlotAsync(slotIndex, options); + public UniTask LoadSlotAsync(int slotIndex, string? decryptionKey = null) => ShrinkSave.LoadSlotAsync(slotIndex, decryptionKey); + } + + internal sealed class ShrinkDataSaverReadOnlyService : IShrinkDataSaverReader + { + private readonly IShrinkDataSaverReader _inner; + + public ShrinkDataSaverReadOnlyService(IShrinkDataSaverReader inner) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + } + + public int LoadedSlot => _inner.LoadedSlot; + public int GetRecentSlotIndex() => _inner.GetRecentSlotIndex(); + public UniTask GetRecommendedContinueSlotAsync() => _inner.GetRecommendedContinueSlotAsync(); + public UniTask GetAllMetaAsync() => _inner.GetAllMetaAsync(); + } + + /// DataSaver provider 侧的访问介导;仅约束经 ShrinkCtx.Get 取得的能力。 + internal sealed class ShrinkDataSaverAccessPolicy : IShrinkCoeffectAccessPolicy + { + private readonly ShrinkDataSaverReadOnlyService _readOnly; + + public ShrinkDataSaverAccessPolicy(ShrinkDataSaverService full) + { + _readOnly = new ShrinkDataSaverReadOnlyService( + full ?? throw new ArgumentNullException(nameof(full))); + } + + public object? Resolve(ShrinkCoeffectAccessContext context, object? value) + { + if (!context.TryGetMetadata(ShrinkDataSaverAccess.MetadataKey, out var access) || + !string.Equals(access, ShrinkDataSaverAccess.ReadOnly, StringComparison.Ordinal)) + return value; + + if (typeof(IShrinkDataSaverWriter).IsAssignableFrom(context.RequestedType) || + context.RequestedType == typeof(ShrinkDataSaverService)) + { + throw new ShrinkCoeffectAccessDeniedException(context.Key, + "the current context grants read-only DataSaver access"); + } + + if (context.RequestedType == typeof(object) || context.RequestedType.IsInstanceOfType(_readOnly)) + return _readOnly; + + throw new ShrinkCoeffectAccessDeniedException(context.Key, + $"read-only DataSaver view cannot satisfy requested type '{context.RequestedType.FullName}'"); + } + } +} diff --git a/Runtime/ShrinkDataSaverAppInstaller.cs.meta b/Runtime/ShrinkDataSaverAppInstaller.cs.meta new file mode 100644 index 0000000..3cf8795 --- /dev/null +++ b/Runtime/ShrinkDataSaverAppInstaller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9eea84dd572634c438c99aef69c51d9f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..04393b3 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "com.cneicy.shrink-datasaver-integration-app", + "version": "0.1.0", + "displayName": "ShrinkDataSaver - App Integration", + "description": "ShrinkDataSaver 与 ShrinkApp 的桥接层,提供宿主接管初始化与服务注册。", + "unity": "2022.3", + "dependencies": { + "com.cneicy.shrink-datasaver": "2.2.0", + "com.cneicy.shrink-app-core": "0.1.1", + "com.cysharp.unitask": "2.5.10", + "com.cneicy.shrink-context-core": "0.1.0" + }, + "keywords": [ + "save", + "integration", + "app", + "bootstrap" + ], + "author": { + "name": "cneicy", + "url": "https://git.crash.work/ShrinkSDK" + }, + "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.App", + "changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkDataSaver.Integration.App/src/branch/main/CHANGELOG.md" +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..a188326 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8358963c0e3299e478b163272aa6b09b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: