Files
ShrinkContext.AppAdapter/Runtime/ShrinkAppLoaderBootstrapper.cs
T
cneicy 57512d9a31
Publish UPM package / publish (push) Successful in 2s
feat(startup): 支持等待 ContextLoader 启动
2026-08-28 03:44:10 +08:00

128 lines
4.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using ShrinkApp;
using ShrinkEventBus;
using UnityEngine;
namespace ShrinkContext.AppAdapter
{
/// <summary>
/// ShrinkApp 加载器宿主的自动引导:
/// hostingMode == ContextLoader 时在 BeforeSceneLoad 创建常驻 GameObject 并启动 ShrinkAppLoaderHost
/// 同时把服务容器接回 ShrinkApp 静态入口(InitializeForExternalHost),保证场景脚本照常访问 ShrinkApp.Services。
/// ClassicHost 模式下完全不动——经典路径零行为变化。
/// </summary>
public sealed class ShrinkAppLoaderBootstrapper : MonoBehaviour
{
private static ShrinkAppLoaderBootstrapper? _instance;
private static UniTaskCompletionSource _startupCompletion = new();
/// <summary>
/// Starter/组合包在 SubsystemRegistration 阶段设置的默认装配表。
/// AppAdapter 保持不依赖具体业务模块,组合根负责注册原生组件;若兼容 installer 已被发现则覆盖同 id。
/// </summary>
public static System.Action<ShrinkAppLoaderHost>? DefaultComposition { get; set; }
public static ShrinkAppLoaderBootstrapper? Instance => _instance;
public static Exception? StartError { get; private set; }
public ShrinkAppLoaderHost Host { get; private set; } = null!;
/// <summary>
/// 等待 ContextLoader 宿主完成启动。支持多个并发等待者;宿主失败时以原始异常结束。
/// </summary>
public static async UniTask WaitUntilStartedAsync(CancellationToken cancellationToken = default)
{
if (_instance?.Host?.IsRunning == true)
return;
if (StartError != null)
throw StartError;
if (ShrinkAppSettings.Instance.hostingMode != ShrinkAppHostingMode.ContextLoader)
{
throw new InvalidOperationException(
"ShrinkAppLoaderBootstrapper requires ShrinkAppHostingMode.ContextLoader.");
}
var task = _startupCompletion.Task;
if (cancellationToken.CanBeCanceled)
await task.AttachExternalCancellation(cancellationToken);
else
await task;
if (StartError != null)
throw StartError;
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStaticState()
{
_instance = null;
StartError = null;
_startupCompletion = new UniTaskCompletionSource();
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void AutoBootstrap()
{
if (ShrinkAppSettings.Instance.hostingMode != ShrinkAppHostingMode.ContextLoader)
return;
var gameObject = new GameObject("ShrinkAppLoaderHost");
DontDestroyOnLoad(gameObject);
gameObject.AddComponent<ShrinkAppLoaderBootstrapper>();
}
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
StartHostAsync().Forget();
}
private async UniTask StartHostAsync()
{
try
{
Host = new ShrinkAppLoaderHost();
DefaultComposition?.Invoke(Host);
var profile = ShrinkAppCompositionProfile.LoadDefault();
if (profile != null)
Host.ApplyComposition(profile.ResolveDocument());
global::ShrinkApp.ShrinkApp.InitializeForExternalHost(Host.Services);
await Host.StartAsync();
CompleteStartup(null);
}
catch (Exception ex)
{
CompleteStartup(ex);
EventBus.Post(new ShrinkAppStartFailedEvent
{
ErrorMessage = ex.Message,
FailedModuleId = string.Empty
});
Debug.LogException(ex);
Debug.LogError("[ShrinkApp.LoaderHost] 启动失败: " + ex.Message);
}
}
private static void CompleteStartup(Exception? error)
{
StartError = error;
_startupCompletion.TrySetResult();
}
private void OnDestroy()
{
if (_instance == this)
_instance = null;
}
}
}