From ee385a41072a9bc4bd04abf89a360b087fdad956 Mon Sep 17 00:00:00 2001 From: cneicy Date: Wed, 26 Aug 2026 02:48:49 +0800 Subject: [PATCH] chore: initialize standalone UPM package --- .gitea/workflows/publish.yml | 49 +++++ .gitea/workflows/unity-verify.yml | 39 ++++ .gitignore | 10 + .npmignore | 8 + CHANGELOG.md | 25 +++ CHANGELOG.md.meta | 7 + Development~/UnityProject/.gitignore | 6 + Development~/UnityProject/Assets/.gitkeep | 0 .../UnityProject/Packages/manifest.json | 15 ++ .../ProjectSettings/ProjectVersion.txt | 2 + Editor.meta | 8 + Editor/ShrinkApp.Core.Editor.asmdef | 17 ++ Editor/ShrinkApp.Core.Editor.asmdef.meta | 7 + Editor/ShrinkAppEditorMenu.cs | 39 ++++ Editor/ShrinkAppEditorMenu.cs.meta | 11 ++ README.md | 31 +++ README.md.meta | 7 + Runtime.meta | 8 + Runtime/ShrinkApp.Core.Runtime.asmdef | 16 ++ Runtime/ShrinkApp.Core.Runtime.asmdef.meta | 7 + Runtime/ShrinkApp.cs | 81 ++++++++ Runtime/ShrinkApp.cs.meta | 11 ++ Runtime/ShrinkAppGeneratedRegistry.cs | 53 +++++ Runtime/ShrinkAppGeneratedRegistry.cs.meta | 11 ++ Runtime/ShrinkAppHost.cs | 167 ++++++++++++++++ Runtime/ShrinkAppHost.cs.meta | 11 ++ Runtime/ShrinkAppInstallers.cs | 16 ++ Runtime/ShrinkAppInstallers.cs.meta | 11 ++ Runtime/ShrinkAppTypes.cs | 181 ++++++++++++++++++ Runtime/ShrinkAppTypes.cs.meta | 11 ++ package.json | 25 +++ package.json.meta | 7 + 32 files changed, 897 insertions(+) create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitea/workflows/unity-verify.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.md.meta create mode 100644 Development~/UnityProject/.gitignore create mode 100644 Development~/UnityProject/Assets/.gitkeep create mode 100644 Development~/UnityProject/Packages/manifest.json create mode 100644 Development~/UnityProject/ProjectSettings/ProjectVersion.txt create mode 100644 Editor.meta create mode 100644 Editor/ShrinkApp.Core.Editor.asmdef create mode 100644 Editor/ShrinkApp.Core.Editor.asmdef.meta create mode 100644 Editor/ShrinkAppEditorMenu.cs create mode 100644 Editor/ShrinkAppEditorMenu.cs.meta create mode 100644 README.md create mode 100644 README.md.meta create mode 100644 Runtime.meta create mode 100644 Runtime/ShrinkApp.Core.Runtime.asmdef create mode 100644 Runtime/ShrinkApp.Core.Runtime.asmdef.meta create mode 100644 Runtime/ShrinkApp.cs create mode 100644 Runtime/ShrinkApp.cs.meta create mode 100644 Runtime/ShrinkAppGeneratedRegistry.cs create mode 100644 Runtime/ShrinkAppGeneratedRegistry.cs.meta create mode 100644 Runtime/ShrinkAppHost.cs create mode 100644 Runtime/ShrinkAppHost.cs.meta create mode 100644 Runtime/ShrinkAppInstallers.cs create mode 100644 Runtime/ShrinkAppInstallers.cs.meta create mode 100644 Runtime/ShrinkAppTypes.cs create mode 100644 Runtime/ShrinkAppTypes.cs.meta create mode 100644 package.json create mode 100644 package.json.meta diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..55ec911 --- /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/ShrinkApp.Core.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..c3c76d3 --- /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/ShrinkApp.Core.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..6723b34 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +本文件记录 `ShrinkApp.Core` 的包内变更。 + +## [0.1.1] - 2026-08-16 + +### Added + +- `ShrinkAppSettings.hostingMode`(`ShrinkAppHostingMode`):`ClassicHost` 保持原有自动启动;`ContextLoader` 让位给 ShrinkContext 加载器宿主(见 `ShrinkContext.AppAdapter`),支持运行中按模块 disable/enable。 +- `ShrinkApp.InitializeForExternalHost(services, settings)`:外部宿主把服务容器接回 `ShrinkApp.Services` 静态入口。 +- `ShrinkAppInstallers.GetDiscoveredInstallerTypes()`:编译期安装器发现的公共入口,供外部宿主枚举。 +- `ShrinkAppContext.CreateStandalone(services, settings)`:非 MonoBehaviour 宿主构建上下文。 + +### Changed + +- `Bootstrap` 在 `ContextLoader` 模式下直接返回,经典路径零行为变化。 + +## [0.1.0] - 2026-05-18 + +### Added + +- 新增统一宿主层 `ShrinkApp.Core`。 +- 新增 `ShrinkApp`、`ShrinkAppHost`、`ShrinkAppSettings`、`ShrinkAppServices`、`ShrinkAppContext`。 +- 新增模块安装器契约:`IShrinkAppModuleInstaller` 与 `[ShrinkAppModuleInstaller]`。 +- 新增运行时生命周期事件:`ShrinkAppStartedEvent`、`ShrinkAppStartFailedEvent`。 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..5c6f252 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3557769562f2f7042a462bcf44b9fedf +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..2205f69 --- /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-app-core": "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/Editor.meta b/Editor.meta new file mode 100644 index 0000000..218b5d8 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 346774b237ebc2b49bc74a69b150780e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ShrinkApp.Core.Editor.asmdef b/Editor/ShrinkApp.Core.Editor.asmdef new file mode 100644 index 0000000..c8b5011 --- /dev/null +++ b/Editor/ShrinkApp.Core.Editor.asmdef @@ -0,0 +1,17 @@ +{ + "name": "ShrinkApp.Core.Editor", + "rootNamespace": "ShrinkApp.Editor", + "references": [ + "ShrinkApp.Core.Runtime" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Editor/ShrinkApp.Core.Editor.asmdef.meta b/Editor/ShrinkApp.Core.Editor.asmdef.meta new file mode 100644 index 0000000..d500713 --- /dev/null +++ b/Editor/ShrinkApp.Core.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d16f21ff20129e14283877a81cca3054 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ShrinkAppEditorMenu.cs b/Editor/ShrinkAppEditorMenu.cs new file mode 100644 index 0000000..e95c817 --- /dev/null +++ b/Editor/ShrinkAppEditorMenu.cs @@ -0,0 +1,39 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +namespace ShrinkApp.Editor +{ + public static class ShrinkAppEditorMenu + { + [MenuItem("ShrinkApp/创建 Settings")] + public static void CreateSettingsAsset() + { + const string resourcesDir = "Assets/Resources"; + if (!AssetDatabase.IsValidFolder(resourcesDir)) + AssetDatabase.CreateFolder("Assets", "Resources"); + + const string assetPath = "Assets/Resources/ShrinkAppSettings.asset"; + var existing = AssetDatabase.LoadAssetAtPath(assetPath); + if (existing != null) + { + Selection.activeObject = existing; + EditorGUIUtility.PingObject(existing); + return; + } + + // 旧版本曾生成过 m_Script=fileID:0 的无效占位资产。删除该占位后 + // 才能在同一路径创建真正绑定 ShrinkAppSettings 类型的资产。 + if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(assetPath))) + AssetDatabase.DeleteAsset(assetPath); + + var asset = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(asset, assetPath); + AssetDatabase.SaveAssets(); + Selection.activeObject = asset; + EditorGUIUtility.PingObject(asset); + Debug.Log("[ShrinkApp] 已创建 ShrinkAppSettings.asset"); + } + } +} +#endif diff --git a/Editor/ShrinkAppEditorMenu.cs.meta b/Editor/ShrinkAppEditorMenu.cs.meta new file mode 100644 index 0000000..b182ab1 --- /dev/null +++ b/Editor/ShrinkAppEditorMenu.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 30f867e31a8e02e41b25f1ef5b4dddec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..3213240 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# ShrinkApp.Core + +`ShrinkApp.Core` 是 `Shrink` 系列模块的统一宿主层。它不直接替代各个基础包的独立使用方式,而是在项目需要“导入一组包后直接起盘”时,提供统一的启动链、模块安装器、服务容器和基础生命周期事件。 + +## 当前定位 + +- 统一宿主:在 `BeforeSceneLoad` 阶段创建 `ShrinkAppHost` +- 模块安装器:通过编译期注入的安装器注册表获取 `IShrinkAppModuleInstaller` +- 服务容器:各模块通过 `RegisterServices(...)` 暴露门面服务 +- 生命周期事件:通过 `ShrinkEventBus` 发布 `ShrinkAppStartedEvent` / `ShrinkAppStartFailedEvent` + +## 核心类型 + +```csharp +ShrinkApp +ShrinkAppHost +ShrinkAppSettings +ShrinkAppServices +ShrinkAppContext +IShrinkAppModuleInstaller +[ShrinkAppModuleInstaller] +``` + +## 启动流程 + +1. `ShrinkApp` 在 `RuntimeInitializeOnLoadMethod(BeforeSceneLoad)` 创建宿主。 +2. 宿主从编译期注册表 `ShrinkAppGeneratedRegistry` 读取全部安装器类型。 +3. 安装器按 `DependsOn + Order` 排序。 +4. 先执行 `RegisterServices(...)`,再顺序执行 `InitializeAsync(...)`。 +5. 启动完成后在首场景 `Start()` 阶段发布 `ShrinkAppStartedEvent`。 +6. 任一安装器重复、依赖缺失、循环依赖、初始化异常都会 fail-fast。 diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..8efa3a2 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 271d3627f55ae2b498a8b430db8d2b41 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..fce7d1c --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c30f0c5343e70914d970bffb896bd45a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkApp.Core.Runtime.asmdef b/Runtime/ShrinkApp.Core.Runtime.asmdef new file mode 100644 index 0000000..18b3716 --- /dev/null +++ b/Runtime/ShrinkApp.Core.Runtime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "ShrinkApp.Core.Runtime", + "rootNamespace": "ShrinkApp", + "references": [ + "ShrinkEventBus.Runtime", + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Runtime/ShrinkApp.Core.Runtime.asmdef.meta b/Runtime/ShrinkApp.Core.Runtime.asmdef.meta new file mode 100644 index 0000000..3d3abf1 --- /dev/null +++ b/Runtime/ShrinkApp.Core.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b5b7047dbc0ca2e438a92daa2baa200c +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkApp.cs b/Runtime/ShrinkApp.cs new file mode 100644 index 0000000..9e96ba4 --- /dev/null +++ b/Runtime/ShrinkApp.cs @@ -0,0 +1,81 @@ +#nullable enable + +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace ShrinkApp +{ + public static class ShrinkApp + { + public static ShrinkAppHost? Host { get; private set; } + public static ShrinkAppContext? Context { get; private set; } + public static ShrinkAppSettings Settings => ShrinkAppSettings.Instance; + public static ShrinkAppServices Services => Context?.Services ?? throw new System.InvalidOperationException("ShrinkApp is not initialized."); + public static bool IsRunning => (Host != null && Host.IsRunning) || _externalHostRunning; + + private static bool _externalHostRunning; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticState() + { + Host = null; + Context = null; + _externalHostRunning = false; + ShrinkAppSettings.ResetCachedInstance(); + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Bootstrap() + { + if (Host != null) + return; + + var settings = ShrinkAppSettings.Instance; + if (settings.hostingMode == ShrinkAppHostingMode.ContextLoader) + { + // 阶段 2 过渡:经典宿主让位,由 ShrinkContext 加载器宿主(ShrinkContext.AppAdapter)接管启动 + return; + } + + var hostGameObject = new GameObject("ShrinkAppHost"); + var host = hostGameObject.AddComponent(); + if (settings.dontDestroyOnLoad) + UnityEngine.Object.DontDestroyOnLoad(hostGameObject); + + var services = new ShrinkAppServices(); + var context = new ShrinkAppContext(host, settings, services); + host.Configure(context); + + Host = host; + Context = context; + host.StartAppAsync().Forget(ex => + { + Debug.LogException(ex); + Debug.LogError("[ShrinkApp] 启动失败: " + ex.Message); + }); + } + + /// + /// 阶段 2 过渡:由外部宿主(如 ShrinkContext 加载器宿主)提供已初始化的服务容器。 + /// Host 保持 null——外部宿主不经过 ShrinkAppHost 生命周期,IsRunning 以外部宿主为准。 + /// + public static void InitializeForExternalHost(ShrinkAppServices services, ShrinkAppSettings? settings = null) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + + Context = new ShrinkAppContext(null!, settings ?? Settings, services); + _externalHostRunning = false; + } + + /// + /// Lets a non- orchestrator expose its lifecycle through + /// the legacy static facade while the migration is in progress. + /// + public static void SetExternalHostRunning(bool running) + { + _externalHostRunning = running; + } + } +} diff --git a/Runtime/ShrinkApp.cs.meta b/Runtime/ShrinkApp.cs.meta new file mode 100644 index 0000000..d44777f --- /dev/null +++ b/Runtime/ShrinkApp.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 10523dad09cece4438f29620b3253fdb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkAppGeneratedRegistry.cs b/Runtime/ShrinkAppGeneratedRegistry.cs new file mode 100644 index 0000000..8d79f3c --- /dev/null +++ b/Runtime/ShrinkAppGeneratedRegistry.cs @@ -0,0 +1,53 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ShrinkApp +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class ShrinkAppInstallerRegistryAttribute : Attribute + { + public ShrinkAppInstallerRegistryAttribute(params Type[] installerTypes) + { + InstallerTypes = installerTypes ?? Array.Empty(); + } + + public Type[] InstallerTypes { get; } + } + + internal static class ShrinkAppGeneratedRegistry + { + public static IReadOnlyList GetInstallerTypes() + { + var types = new List(); + var seen = new HashSet(); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + object[] attributes; + try + { + attributes = assembly.GetCustomAttributes(typeof(ShrinkAppInstallerRegistryAttribute), false); + } + catch + { + continue; + } + + foreach (var attribute in attributes.OfType()) + { + foreach (var installerType in attribute.InstallerTypes ?? Array.Empty()) + { + if (installerType == null || !seen.Add(installerType)) + continue; + + types.Add(installerType); + } + } + } + + return types; + } + } +} diff --git a/Runtime/ShrinkAppGeneratedRegistry.cs.meta b/Runtime/ShrinkAppGeneratedRegistry.cs.meta new file mode 100644 index 0000000..0ee7d05 --- /dev/null +++ b/Runtime/ShrinkAppGeneratedRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1818116c8e85f5e489c0c4955f506ff2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkAppHost.cs b/Runtime/ShrinkAppHost.cs new file mode 100644 index 0000000..33adaae --- /dev/null +++ b/Runtime/ShrinkAppHost.cs @@ -0,0 +1,167 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +using ShrinkEventBus; +using UnityEngine; + +namespace ShrinkApp +{ + public sealed class ShrinkAppHost : MonoBehaviour + { + private readonly List _startedModules = new(); + private ShrinkAppContext? _context; + private bool _unityStarted; + private bool _publishedStartedEvent; + + public bool IsRunning { get; private set; } + public bool HasFailed { get; private set; } + public string LastError { get; private set; } = string.Empty; + public IReadOnlyList StartedModules => _startedModules; + + internal void Configure(ShrinkAppContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + internal async UniTask StartAppAsync() + { + if (IsRunning || HasFailed) + return; + if (_context == null) + throw new InvalidOperationException("ShrinkAppHost is not configured."); + + var installers = CreateInstallerInstances(); + var orderedInstallers = SortInstallers(installers, _context.Settings); + + try + { + foreach (var installer in orderedInstallers) + installer.RegisterServices(_context); + + foreach (var installer in orderedInstallers) + { + await installer.InitializeAsync(_context); + _startedModules.Add(installer.ModuleId); + } + + IsRunning = true; + PublishStartedEventIfReady(); + } + catch (Exception ex) + { + HasFailed = true; + LastError = ex.Message; + EventBus.Post(new ShrinkAppStartFailedEvent + { + ErrorMessage = ex.Message, + FailedModuleId = _startedModules.LastOrDefault() ?? string.Empty + }); + throw; + } + } + + private void Start() + { + _unityStarted = true; + PublishStartedEventIfReady(); + } + + private void PublishStartedEventIfReady() + { + if (_publishedStartedEvent || !_unityStarted || !IsRunning) + return; + + _publishedStartedEvent = true; + EventBus.Post(new ShrinkAppStartedEvent + { + ModuleIds = _startedModules.ToArray() + }); + } + + private static IReadOnlyList CreateInstallerInstances() + { + var installers = new List(); + foreach (var installerType in ShrinkAppGeneratedRegistry.GetInstallerTypes()) + { + if (installerType == null) + continue; + if (!typeof(IShrinkAppModuleInstaller).IsAssignableFrom(installerType)) + throw new InvalidOperationException($"Installer type does not implement IShrinkAppModuleInstaller: {installerType.FullName}"); + if (installerType.IsAbstract) + throw new InvalidOperationException($"Installer type cannot be abstract: {installerType.FullName}"); + + if (Activator.CreateInstance(installerType) is not IShrinkAppModuleInstaller installer) + throw new InvalidOperationException($"Failed to create installer: {installerType.FullName}"); + + installers.Add(installer); + } + + return installers; + } + + private static IReadOnlyList SortInstallers( + IReadOnlyList installers, + ShrinkAppSettings settings) + { + var disabled = new HashSet( + settings.disabledModuleIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()) ?? + Enumerable.Empty(), + StringComparer.OrdinalIgnoreCase); + + var enabledInstallers = installers + .Where(installer => !disabled.Contains(installer.ModuleId)) + .ToArray(); + + var byId = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var installer in enabledInstallers) + { + if (string.IsNullOrWhiteSpace(installer.ModuleId)) + throw new InvalidOperationException($"Installer {installer.GetType().FullName} has an empty ModuleId."); + if (!byId.TryAdd(installer.ModuleId.Trim(), installer)) + throw new InvalidOperationException($"Duplicate installer ModuleId: {installer.ModuleId}"); + } + + var visitState = new Dictionary(StringComparer.OrdinalIgnoreCase); + var ordered = new List(); + foreach (var installer in enabledInstallers.OrderBy(item => item.Order).ThenBy(item => item.ModuleId, StringComparer.OrdinalIgnoreCase)) + { + Visit(installer, byId, visitState, ordered); + } + + return ordered; + } + + private static void Visit( + IShrinkAppModuleInstaller installer, + IReadOnlyDictionary byId, + IDictionary visitState, + IList ordered) + { + var state = visitState.TryGetValue(installer.ModuleId, out var existingState) ? existingState : 0; + if (state == 2) + return; + if (state == 1) + throw new InvalidOperationException($"Circular installer dependency detected at module: {installer.ModuleId}"); + + visitState[installer.ModuleId] = 1; + foreach (var dependencyId in installer.DependsOn ?? Array.Empty()) + { + if (string.IsNullOrWhiteSpace(dependencyId)) + continue; + + if (!byId.TryGetValue(dependencyId.Trim(), out var dependency)) + throw new InvalidOperationException( + $"Installer dependency missing. Module={installer.ModuleId}, DependsOn={dependencyId}"); + + Visit(dependency, byId, visitState, ordered); + } + + visitState[installer.ModuleId] = 2; + if (!ordered.Contains(installer)) + ordered.Add(installer); + } + } +} diff --git a/Runtime/ShrinkAppHost.cs.meta b/Runtime/ShrinkAppHost.cs.meta new file mode 100644 index 0000000..3d7ab55 --- /dev/null +++ b/Runtime/ShrinkAppHost.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df43c81d962edd64cbd214b2697b4179 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkAppInstallers.cs b/Runtime/ShrinkAppInstallers.cs new file mode 100644 index 0000000..fbf277f --- /dev/null +++ b/Runtime/ShrinkAppInstallers.cs @@ -0,0 +1,16 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace ShrinkApp +{ + /// + /// 编译期发现的应用模块安装器类型(公共入口)。 + /// 经典 ShrinkAppHost 与外部宿主(ShrinkContext 加载器宿主)共用同一份发现结果。 + /// + public static class ShrinkAppInstallers + { + public static IReadOnlyList GetDiscoveredInstallerTypes() => + ShrinkAppGeneratedRegistry.GetInstallerTypes(); + } +} diff --git a/Runtime/ShrinkAppInstallers.cs.meta b/Runtime/ShrinkAppInstallers.cs.meta new file mode 100644 index 0000000..2ea0f8d --- /dev/null +++ b/Runtime/ShrinkAppInstallers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c22f2e857d66bae458ff3dd6d9519d0d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ShrinkAppTypes.cs b/Runtime/ShrinkAppTypes.cs new file mode 100644 index 0000000..28159b1 --- /dev/null +++ b/Runtime/ShrinkAppTypes.cs @@ -0,0 +1,181 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +using ShrinkEventBus; +using UnityEngine; + +namespace ShrinkApp +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class ShrinkAppModuleInstallerAttribute : Attribute + { + } + + public interface IShrinkAppModuleInstaller + { + string ModuleId { get; } + int Order { get; } + IReadOnlyList DependsOn { get; } + void RegisterServices(ShrinkAppContext context); + UniTask InitializeAsync(ShrinkAppContext context); + } + + public sealed class ShrinkAppContext + { + internal ShrinkAppContext(ShrinkAppHost host, ShrinkAppSettings settings, ShrinkAppServices services) + { + Host = host; + Settings = settings; + Services = services; + } + + /// + /// 为非 MonoBehaviour 宿主(如 ShrinkContext 纤程适配器)构建上下文。 + /// Host 为 null:适用于不经过 ShrinkAppHost 启动流的独立编排场景。 + /// + public static ShrinkAppContext CreateStandalone(ShrinkAppServices services, ShrinkAppSettings? settings = null) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + return new ShrinkAppContext(null!, settings ?? ShrinkAppSettings.Instance, services); + } + + public ShrinkAppHost Host { get; } + public ShrinkAppSettings Settings { get; } + public ShrinkAppServices Services { get; } + } + + public sealed class ShrinkAppServices + { + private readonly Dictionary _services = new(); + + public void Register(TService service) where TService : class + { + if (service == null) + throw new ArgumentNullException(nameof(service)); + + _services[typeof(TService)] = service; + } + + public bool TryGet(out TService? service) where TService : class + { + if (_services.TryGetValue(typeof(TService), out var boxed) && boxed is TService typed) + { + service = typed; + return true; + } + + service = null; + return false; + } + + /// + /// Removes a service only when the current registration is the supplied instance. + /// This lets context-driven components expose the legacy service facade as a + /// reversible effect without removing a newer replacement during teardown. + /// + public bool TryUnregister(TService service) where TService : class + { + if (service == null) + throw new ArgumentNullException(nameof(service)); + + if (!_services.TryGetValue(typeof(TService), out var current) || + !ReferenceEquals(current, service)) + return false; + + return _services.Remove(typeof(TService)); + } + + public TService GetRequired() where TService : class + { + if (!TryGet(out var service) || service == null) + throw new InvalidOperationException($"Required service is not registered: {typeof(TService).FullName}"); + + return service; + } + + public IReadOnlyList GetRegisteredServiceTypeNames() + { + return _services.Keys + .Select(type => type.FullName ?? type.Name) + .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + } + + /// ShrinkApp 的宿主模式(阶段 2 过渡:两种模式并存,按项目选择)。 + public enum ShrinkAppHostingMode + { + /// 经典模式:ShrinkAppHost 在 BeforeSceneLoad 自动创建并按拓扑序启动安装器。 + ClassicHost = 0, + + /// 加载器模式:经典宿主让位,由 ShrinkContext 加载器宿主(ShrinkContext.AppAdapter)接管启动, + /// 支持运行中按模块 disable/enable 与依赖响应式等待。 + ContextLoader = 1, + } + + [CreateAssetMenu(fileName = "ShrinkAppSettings", menuName = "ShrinkApp/Settings")] + public class ShrinkAppSettings : ScriptableObject + { + private static ShrinkAppSettings? _instance; + + public static ShrinkAppSettings Instance + { + get + { + if (_instance != null) + return _instance; + + _instance = Resources.Load("ShrinkAppSettings"); + +#if UNITY_EDITOR + if (!_instance) + { + var guids = UnityEditor.AssetDatabase.FindAssets("t:ShrinkAppSettings"); + if (guids.Length > 0) + { + var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[0]); + _instance = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + } + } +#endif + + if (!_instance) + { + _instance = CreateInstance(); + Debug.LogWarning( + "[ShrinkApp] 未找到 ShrinkAppSettings,当前使用默认配置。建议通过 Assets -> Create -> ShrinkApp -> Settings 创建配置。"); + } + + return _instance!; + } + internal set => _instance = value; + } + + internal static void ResetCachedInstance() + { + _instance = null; + } + + public bool dontDestroyOnLoad = true; + public bool verboseLogging; + public string[] disabledModuleIds = Array.Empty(); + + [Tooltip("宿主模式:ClassicHost 为经典自动启动;ContextLoader 由 ShrinkContext 加载器宿主接管(阶段 2 过渡)")] + public ShrinkAppHostingMode hostingMode = ShrinkAppHostingMode.ClassicHost; + } + + public sealed class ShrinkAppStartedEvent : IShrinkEvent + { + public IReadOnlyList ModuleIds { get; set; } = Array.Empty(); + } + + public sealed class ShrinkAppStartFailedEvent : IShrinkEvent + { + public string ErrorMessage { get; set; } = string.Empty; + public string FailedModuleId { get; set; } = string.Empty; + } +} diff --git a/Runtime/ShrinkAppTypes.cs.meta b/Runtime/ShrinkAppTypes.cs.meta new file mode 100644 index 0000000..c988efd --- /dev/null +++ b/Runtime/ShrinkAppTypes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 384dd878beec6b848b64aa843b464e95 +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..fd2edd8 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "com.cneicy.shrink-app-core", + "version": "0.1.1", + "displayName": "ShrinkApp Core", + "description": "Shrink 系列统一宿主层,提供模块安装器、服务容器与统一启动流程。", + "unity": "2022.3", + "dependencies": { + "com.cneicy.shrink-eventbus": "2.0.0", + "com.cneicy.shrink-shared-codegen": "0.1.0", + "com.cysharp.unitask": "2.5.10" + }, + "keywords": [ + "app", + "bootstrap", + "framework", + "host", + "starter" + ], + "author": { + "name": "cneicy", + "url": "https://git.crash.work/ShrinkSDK" + }, + "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkApp.Core", + "changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkApp.Core/src/branch/main/CHANGELOG.md" +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..55828d4 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 30fa106725f9afc46af98aff2ee92198 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: