commit ad256f109b94da3b5b27b8c09d97cdb9dc8fd4f2 Author: cneicy Date: Sun Aug 16 23:20:40 2026 +0800 feat(cordis): 接入上下文组合与模组事务热替换 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b12dc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# This .gitignore file should be placed at the root of your Unity project directory +# +# +# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore +# +.utmp/ +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Uu]ser[Ss]ettings/ +*.log + +# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control. +*.blend1 +*.blend1.meta + +# MemoryCaptures can get excessive in size. +# They also could contain extremely sensitive data +/[Mm]emoryCaptures/ + +# Recordings can get excessive in size +/[Rr]ecordings/ + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +/[Aa]ssets/Plugins/Editor/JetBrains* +# Jetbrains Rider personal-layer settings +*.DotSettings.user + +# Visual Studio cache directory +.vs/ + +# Gradle cache directory +.gradle/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ +*.unityproj +*.sln +*.csproj +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +# Unity3D generated meta files +*.pidb.meta +*.pdb.meta +*.mdb.meta + +# Unity3D generated file on crash reports +sysinfo.txt + +# Mono auto generated files +mono_crash.* + +# Builds +*.apk +*.aab +*.unitypackage +*.unitypackage.meta +*.app + +# Crashlytics generated file +crashlytics-build.properties + +# TestRunner generated files +InitTestScene*.unity* + +# Addressables default ignores, before user customizations +/ServerData +/[Aa]ssets/StreamingAssets/aa* +/[Aa]ssets/AddressableAssetsData/link.xml* +/[Aa]ssets/Addressables_Temp* +# By default, Addressables content builds will generate addressables_content_state.bin +# files in platform-specific subfolders, for example: +# /Assets/AddressableAssetsData/OSX/addressables_content_state.bin +/[Aa]ssets/AddressableAssetsData/*/*.bin* + +# Visual Scripting auto-generated files +/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db +/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta +/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers +/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta + +# Auto-generated scenes by play mode tests +/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity* +Assets/StreamingAssets/Whisper/ggml-base.bin +.planning/ +AGENTS.md +Assets/Screenshots/ +output/ +tmp/ +Packages/ +Assets/unity-mcp/ +Assets/unity-mcp.meta +.vscode/ +.claude/ +.codex/ +.funplay/ +CLAUDE.md +KNOWN_PITFALLS.md +Assets/~UnityDirMonSyncFile~84ae11425aac2204fbff8739574b1de6~ +.idea/ +GeneratedServers/ +GeneratedModSdk/ diff --git a/Assets/Modules.meta b/Assets/Modules.meta new file mode 100644 index 0000000..dfeb169 --- /dev/null +++ b/Assets/Modules.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2a618cfe20080eb41a405afb915c6598 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core.meta b/Assets/Modules/ShrinkApp.Core.meta new file mode 100644 index 0000000..5c719ba --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aacbfcd500ef7304b9d5c78a9467b191 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core/CHANGELOG.md b/Assets/Modules/ShrinkApp.Core/CHANGELOG.md new file mode 100644 index 0000000..6723b34 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/CHANGELOG.md.meta b/Assets/Modules/ShrinkApp.Core/CHANGELOG.md.meta new file mode 100644 index 0000000..5c6f252 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3557769562f2f7042a462bcf44b9fedf +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core/Editor.meta b/Assets/Modules/ShrinkApp.Core/Editor.meta new file mode 100644 index 0000000..218b5d8 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 346774b237ebc2b49bc74a69b150780e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core/Editor/ShrinkApp.Core.Editor.asmdef b/Assets/Modules/ShrinkApp.Core/Editor/ShrinkApp.Core.Editor.asmdef new file mode 100644 index 0000000..c8b5011 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Editor/ShrinkApp.Core.Editor.asmdef.meta b/Assets/Modules/ShrinkApp.Core/Editor/ShrinkApp.Core.Editor.asmdef.meta new file mode 100644 index 0000000..d500713 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/Editor/ShrinkApp.Core.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d16f21ff20129e14283877a81cca3054 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core/Editor/ShrinkAppEditorMenu.cs b/Assets/Modules/ShrinkApp.Core/Editor/ShrinkAppEditorMenu.cs new file mode 100644 index 0000000..5240aa3 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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 (AssetDatabase.LoadMainAssetAtPath(assetPath) != null) + 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/Assets/Modules/ShrinkApp.Core/Editor/ShrinkAppEditorMenu.cs.meta b/Assets/Modules/ShrinkApp.Core/Editor/ShrinkAppEditorMenu.cs.meta new file mode 100644 index 0000000..b182ab1 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/README.md b/Assets/Modules/ShrinkApp.Core/README.md new file mode 100644 index 0000000..3213240 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/README.md.meta b/Assets/Modules/ShrinkApp.Core/README.md.meta new file mode 100644 index 0000000..8efa3a2 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 271d3627f55ae2b498a8b430db8d2b41 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core/Runtime.meta b/Assets/Modules/ShrinkApp.Core/Runtime.meta new file mode 100644 index 0000000..fce7d1c --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c30f0c5343e70914d970bffb896bd45a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.Core.Runtime.asmdef b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.Core.Runtime.asmdef new file mode 100644 index 0000000..18b3716 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.Core.Runtime.asmdef.meta b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.Core.Runtime.asmdef.meta new file mode 100644 index 0000000..3d3abf1 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.Core.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b5b7047dbc0ca2e438a92daa2baa200c +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.cs b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.cs new file mode 100644 index 0000000..9e96ba4 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.cs.meta b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkApp.cs.meta new file mode 100644 index 0000000..d44777f --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppGeneratedRegistry.cs b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppGeneratedRegistry.cs new file mode 100644 index 0000000..8d79f3c --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppGeneratedRegistry.cs.meta b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppGeneratedRegistry.cs.meta new file mode 100644 index 0000000..0ee7d05 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppHost.cs b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppHost.cs new file mode 100644 index 0000000..5c01775 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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.TriggerEvent(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.TriggerEvent(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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppHost.cs.meta b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppHost.cs.meta new file mode 100644 index 0000000..3d7ab55 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppInstallers.cs b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppInstallers.cs new file mode 100644 index 0000000..fbf277f --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppInstallers.cs.meta b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppInstallers.cs.meta new file mode 100644 index 0000000..2ea0f8d --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppTypes.cs b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppTypes.cs new file mode 100644 index 0000000..f2bf101 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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 : EventBase + { + public IReadOnlyList ModuleIds { get; set; } = Array.Empty(); + } + + public sealed class ShrinkAppStartFailedEvent : EventBase + { + public string ErrorMessage { get; set; } = string.Empty; + public string FailedModuleId { get; set; } = string.Empty; + } +} diff --git a/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppTypes.cs.meta b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppTypes.cs.meta new file mode 100644 index 0000000..c988efd --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/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/Assets/Modules/ShrinkApp.Core/package.json b/Assets/Modules/ShrinkApp.Core/package.json new file mode 100644 index 0000000..1b8bb53 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/package.json @@ -0,0 +1,22 @@ +{ + "name": "com.cneicy.shrink-app-core", + "version": "0.1.1", + "displayName": "ShrinkApp Core", + "description": "Shrink 系列统一宿主层,提供模块安装器、服务容器与统一启动流程。", + "unity": "2022.3", + "dependencies": { + "com.cneicy.shrink-eventbus": "1.1.5", + "com.cysharp.unitask": "2.5.10" + }, + "keywords": [ + "app", + "bootstrap", + "framework", + "host", + "starter" + ], + "author": { + "name": "cneicy", + "url": "https://github.com/cneicy" + } +} diff --git a/Assets/Modules/ShrinkApp.Core/package.json.meta b/Assets/Modules/ShrinkApp.Core/package.json.meta new file mode 100644 index 0000000..55828d4 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Core/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 30fa106725f9afc46af98aff2ee92198 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Starter.Basic.meta b/Assets/Modules/ShrinkApp.Starter.Basic.meta new file mode 100644 index 0000000..5662808 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Starter.Basic.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f69712f8fd5471e46b41e82204b5e845 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Starter.Basic/CHANGELOG.md b/Assets/Modules/ShrinkApp.Starter.Basic/CHANGELOG.md new file mode 100644 index 0000000..0b1bc05 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Starter.Basic/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +本文件记录 `ShrinkApp.Starter.Basic` 的包内变更。 + +## [0.1.0] - 2026-05-18 + +### Added + +- 新增最小起盘 Starter 包。 +- 新增场景生成器 `ShrinkAppBasicStarterGenerator`。 +- 新增示例控制器 `ShrinkAppBasicSampleController`。 +- 新增 `ShrinkAppBasicDemoRuntime`,补齐默认命令源、状态命令与最小 loopback 网络示例。 +- 新增 `ShrinkAppBasicStarterSettings`,提供最小 Starter 级别的默认配置资产。 +- 示例场景继续扩展为小型调试台,补齐命令输入、输出日志、模块/服务/存档/命令/网络状态面板。 +- 状态面板进一步细化为已启动模块、已注册服务、存档槽位、命令列表、网络诊断五块只读视图。 diff --git a/Assets/Modules/ShrinkApp.Starter.Basic/CHANGELOG.md.meta b/Assets/Modules/ShrinkApp.Starter.Basic/CHANGELOG.md.meta new file mode 100644 index 0000000..eba6b24 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Starter.Basic/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e3e78d4720b16b84fbf0d766b0a0122b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Starter.Basic/Editor.meta b/Assets/Modules/ShrinkApp.Starter.Basic/Editor.meta new file mode 100644 index 0000000..ffcd571 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Starter.Basic/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f6be676789111fd4a9afd67b011a12c9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkApp.Starter.Basic.Editor.asmdef b/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkApp.Starter.Basic.Editor.asmdef new file mode 100644 index 0000000..66e68bd --- /dev/null +++ b/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkApp.Starter.Basic.Editor.asmdef @@ -0,0 +1,22 @@ +{ + "name": "ShrinkApp.Starter.Basic.Editor", + "rootNamespace": "ShrinkApp.Starter.Basic.Editor", + "references": [ + "ShrinkApp.Starter.Basic.Runtime", + "ShrinkApp.Core.Runtime", + "ShrinkApp.Core.Editor", + "ShrinkDataSaver.Runtime", + "ShrinkDataSaver.Integration.App", + "UnityEngine.UI" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkApp.Starter.Basic.Editor.asmdef.meta b/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkApp.Starter.Basic.Editor.asmdef.meta new file mode 100644 index 0000000..0b63870 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkApp.Starter.Basic.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: aa988409913004043b6cbe3d19de32bd +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkAppBasicStarterGenerator.cs b/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkAppBasicStarterGenerator.cs new file mode 100644 index 0000000..b80a754 --- /dev/null +++ b/Assets/Modules/ShrinkApp.Starter.Basic/Editor/ShrinkAppBasicStarterGenerator.cs @@ -0,0 +1,215 @@ +#if UNITY_EDITOR +using ShrinkApp.Editor; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.UI; + +namespace ShrinkApp.Starter.Basic.Editor +{ + public static class ShrinkAppBasicStarterGenerator + { + private const string MenuPath = "ShrinkApp/Starter/生成 Basic Entry 场景"; + + [MenuItem(MenuPath)] + public static void Generate() + { + EnsureAssets(); + BuildScene(); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + Debug.Log("[ShrinkApp.Starter.Basic] 已生成 Basic Entry 场景与配置。"); + } + + private static void EnsureAssets() + { + ShrinkAppEditorMenu.CreateSettingsAsset(); + + const string resourcesDir = "Assets/Resources"; + if (!AssetDatabase.IsValidFolder(resourcesDir)) + AssetDatabase.CreateFolder("Assets", "Resources"); + + const string dataSaverSettingsPath = "Assets/Resources/ShrinkDataSaverSettings.asset"; + if (AssetDatabase.LoadAssetAtPath(dataSaverSettingsPath) == null) + { + var asset = ScriptableObject.CreateInstance("ShrinkDataSaverSettings"); + if (asset == null) + throw new System.InvalidOperationException( + "Cannot create ShrinkDataSaverSettings. Ensure ShrinkDataSaver.Runtime is imported and compiled."); + + AssetDatabase.CreateAsset(asset, dataSaverSettingsPath); + } + + const string starterSettingsPath = "Assets/Resources/ShrinkAppBasicStarterSettings.asset"; + if (AssetDatabase.LoadAssetAtPath(starterSettingsPath) == null) + { + var starterAsset = ScriptableObject.CreateInstance(); + starterAsset.autoBindLoopbackTransport = true; + starterAsset.defaultCommandText = "starter status"; + starterAsset.defaultLoopbackSessionId = 1; + starterAsset.logCommandOutputToConsole = true; + starterAsset.maxCommandOutputLines = 12; + AssetDatabase.CreateAsset(starterAsset, starterSettingsPath); + } + } + + private static void BuildScene() + { + const string scenesDir = "Assets/Scenes"; + if (!AssetDatabase.IsValidFolder(scenesDir)) + AssetDatabase.CreateFolder("Assets", "Scenes"); + + var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + + new GameObject("Main Camera", typeof(Camera)); + new GameObject("EventSystem", typeof(UnityEngine.EventSystems.EventSystem), + typeof(UnityEngine.EventSystems.StandaloneInputModule)); + + var canvasGo = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); + var canvas = canvasGo.GetComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + var scaler = canvasGo.GetComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920, 1080); + + var panelGo = new GameObject("Panel", typeof(RectTransform), typeof(Image)); + panelGo.transform.SetParent(canvasGo.transform, false); + var panelRect = panelGo.GetComponent(); + panelRect.sizeDelta = new Vector2(980f, 720f); + panelRect.anchoredPosition = Vector2.zero; + panelGo.GetComponent().color = new Color(0.12f, 0.15f, 0.2f, 0.92f); + + var statusText = CreateText("StatusText", panelGo.transform, new Vector2(0f, 240f), new Vector2(840f, 120f), + "ShrinkApp Basic starter ready."); + statusText.alignment = TextAnchor.MiddleCenter; + var detailText = CreateText("DetailText", panelGo.transform, new Vector2(0f, 135f), new Vector2(840f, 140f), + "app=False, save=False, cmd=False, net=False"); + detailText.alignment = TextAnchor.UpperLeft; + detailText.fontSize = 20; + var commandInputField = CreateInputField("CommandInput", panelGo.transform, new Vector2(0f, 20f), + new Vector2(520f, 54f), "starter status"); + var outputText = CreateText("OutputText", panelGo.transform, new Vector2(0f, -250f), new Vector2(840f, 180f), + string.Empty); + outputText.alignment = TextAnchor.UpperLeft; + outputText.fontSize = 20; + outputText.horizontalOverflow = HorizontalWrapMode.Wrap; + outputText.verticalOverflow = VerticalWrapMode.Overflow; + var saveMetaText = CreateText("SaveMetaText", panelGo.transform, new Vector2(-280f, -430f), new Vector2(240f, 150f), + "No save slots found."); + saveMetaText.alignment = TextAnchor.UpperLeft; + saveMetaText.fontSize = 18; + var moduleText = CreateText("ModuleText", panelGo.transform, new Vector2(0f, -430f), new Vector2(240f, 150f), + "No started modules."); + moduleText.alignment = TextAnchor.UpperLeft; + moduleText.fontSize = 18; + var serviceText = CreateText("ServiceText", panelGo.transform, new Vector2(280f, -430f), new Vector2(240f, 150f), + "No registered services."); + serviceText.alignment = TextAnchor.UpperLeft; + serviceText.fontSize = 18; + var commandListText = CreateText("CommandListText", panelGo.transform, new Vector2(-280f, -600f), new Vector2(240f, 150f), + "Command service not ready."); + commandListText.alignment = TextAnchor.UpperLeft; + commandListText.fontSize = 18; + var networkText = CreateText("NetworkText", panelGo.transform, new Vector2(280f, -600f), new Vector2(240f, 150f), + "Network service not ready."); + networkText.alignment = TextAnchor.UpperLeft; + networkText.fontSize = 18; + + var saveButton = CreateButton("SaveButton", panelGo.transform, new Vector2(-250f, -70f), "Save Slot 0"); + var loadButton = CreateButton("LoadButton", panelGo.transform, new Vector2(0f, -70f), "Load Slot 0"); + var commandButton = CreateButton("CommandButton", panelGo.transform, new Vector2(250f, -70f), "Run Command"); + var loopbackButton = CreateButton("LoopbackButton", panelGo.transform, new Vector2(-250f, -165f), "Bind Loopback"); + var disconnectButton = CreateButton("DisconnectLoopbackButton", panelGo.transform, new Vector2(0f, -165f), "Disconnect Loopback"); + var refreshButton = CreateButton("RefreshButton", panelGo.transform, new Vector2(250f, -165f), "Refresh Status"); + var clearOutputButton = CreateButton("ClearOutputButton", panelGo.transform, new Vector2(0f, -345f), "Clear Output"); + + var controller = panelGo.AddComponent(); + var serializedObject = new SerializedObject(controller); + serializedObject.FindProperty("statusText").objectReferenceValue = statusText; + serializedObject.FindProperty("detailText").objectReferenceValue = detailText; + serializedObject.FindProperty("outputText").objectReferenceValue = outputText; + serializedObject.FindProperty("moduleText").objectReferenceValue = moduleText; + serializedObject.FindProperty("serviceText").objectReferenceValue = serviceText; + serializedObject.FindProperty("saveMetaText").objectReferenceValue = saveMetaText; + serializedObject.FindProperty("commandListText").objectReferenceValue = commandListText; + serializedObject.FindProperty("networkText").objectReferenceValue = networkText; + serializedObject.FindProperty("saveButton").objectReferenceValue = saveButton; + serializedObject.FindProperty("loadButton").objectReferenceValue = loadButton; + serializedObject.FindProperty("commandButton").objectReferenceValue = commandButton; + serializedObject.FindProperty("loopbackButton").objectReferenceValue = loopbackButton; + serializedObject.FindProperty("refreshButton").objectReferenceValue = refreshButton; + serializedObject.FindProperty("clearOutputButton").objectReferenceValue = clearOutputButton; + serializedObject.FindProperty("disconnectLoopbackButton").objectReferenceValue = disconnectButton; + serializedObject.FindProperty("commandInputField").objectReferenceValue = commandInputField; + serializedObject.ApplyModifiedPropertiesWithoutUndo(); + + EditorSceneManager.SaveScene(scene, "Assets/Scenes/ShrinkAppEntry.unity"); + } + + private static Text CreateText(string name, Transform parent, Vector2 anchoredPosition, Vector2 size, string text) + { + var go = new GameObject(name, typeof(RectTransform), typeof(Text)); + go.transform.SetParent(parent, false); + var rect = go.GetComponent(); + rect.sizeDelta = size; + rect.anchoredPosition = anchoredPosition; + var label = go.GetComponent(); + label.text = text; + label.font = Resources.GetBuiltinResource("LegacyRuntime.ttf"); + label.fontSize = 28; + label.color = Color.white; + return label; + } + + private static Button CreateButton(string name, Transform parent, Vector2 anchoredPosition, string labelText) + { + var go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(Button)); + go.transform.SetParent(parent, false); + var rect = go.GetComponent(); + rect.sizeDelta = new Vector2(220f, 72f); + rect.anchoredPosition = anchoredPosition; + go.GetComponent().color = new Color(0.24f, 0.45f, 0.75f, 1f); + + var label = CreateText("Label", go.transform, Vector2.zero, new Vector2(220f, 72f), labelText); + label.alignment = TextAnchor.MiddleCenter; + label.resizeTextForBestFit = true; + label.resizeTextMinSize = 18; + label.resizeTextMaxSize = 28; + + return go.GetComponent