using System;
using System.IO;
using System.Reflection;
using HybridCLR;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Networking;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
///
/// AOT 启动器(必须位于 AOT 程序集,且不能编译期引用任何热更类型)。
///
/// 启动流程:
/// BootScene(纯 AOT 空场景)
/// → Launcher 在 AfterAssembliesLoaded 阶段加载热更 dll(Game.Hotfix / Game.LegacyScripts)
/// → 等 BootScene 加载完(AfterSceneLoad)后,通过 Addressables 加载真正的业务场景 UIScene
///
/// 为什么不能直接把 UIScene 放进 Build Settings 当启动场景:
/// HybridCLR 官方明确限制——热更脚本不能挂在"随主包的资源"上(含 Build Settings 里的场景)。
/// 因为构建时 IFilterBuildAssemblies 会把热更 dll 从序列化列表里剔除,导致场景里的热更脚本
/// 字段写不进包体,运行时反序列化就报 "The referenced script on this Behaviour is missing!"。
/// 解决办法:热更脚本所在场景必须走 AssetBundle/Addressables 加载。
///
/// 为什么加载 UIScene 要放在 AfterSceneLoad 而不是更早:
/// Addressables.LoadSceneAsync(..., Single) 会替换当前场景,如果在 Unity 自身加载第一个场景
/// (BeforeSceneLoad)期间发起,会和引擎的首次场景加载相互冲突。等第一个场景加载完再替换才安全。
///
public static class Launcher
{
/// 热更 dll 目录(相对 StreamingAssets)
private const string HotfixDir = "HybridCLR/Hotfix";
/// AOT 补充元数据 dll 目录(相对 StreamingAssets)
private const string AOTMetaDir = "HybridCLR/AOT";
/// 需要热更加载的程序集(与 ProjectSettings/HybridCLRSettings.asset 的 hotUpdateAssemblies 一致)
private static readonly string[] HotfixAssemblies = { "Game.Hotfix", "Game.LegacyScripts" };
/// 通过 Addressables 加载的主场景 key(需在 Addressables 里把 UIScene 标记为可寻址)
private const string MainSceneKey = "UIScene";
/// 是否已加载完成(多个阶段只加载一次)
private static bool _loaded;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
private static void BootAfterAssembliesLoaded() { EnsureLoaded(RuntimeInitializeLoadType.AfterAssembliesLoaded); }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
private static void BootBeforeSplashScreen() { EnsureLoaded(RuntimeInitializeLoadType.BeforeSplashScreen); }
/// 第一个场景(BootScene)加载完成后,再切换到业务主场景 UIScene
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void BootAfterSceneLoad()
{
// 兜底:若前面的阶段都没能加载(例如被平台时序打断),这里再试一次
if (!_loaded)
EnsureLoaded(RuntimeInitializeLoadType.AfterSceneLoad);
if (!_loaded)
return;
LoadMainSceneViaAddressables();
}
private static void EnsureLoaded(RuntimeInitializeLoadType phase)
{
if (_loaded)
{
Debug.Log($"[Launcher] 已在更早阶段加载完成,{phase} 阶段跳过");
return;
}
int frame = Time.frameCount;
Debug.Log($"[Launcher] 开始加载:阶段={phase},frame={frame}");
bool aotOk = LoadAOTMetadata();
bool hotOk = LoadHotfixAssemblies();
_loaded = aotOk && hotOk;
Debug.Log($"[Launcher] 加载完成:阶段={phase},AOT元数据={(aotOk ? "全部OK" : "有缺失")}," +
$"热更dll={(hotOk ? "全部OK" : "有失败")},Game.Hotfix 类型总数={HotfixTypeCount()}," +
$"GameManager 可解析={HotfixTypeResolvable("GameManager")}");
if (!_loaded)
{
Debug.LogError("[Launcher] 热更程序集未全部加载成功,游戏将无法正常运行。请检查 StreamingAssets/HybridCLR 下的 dll 是否已拷贝。");
}
}
///
/// 初始化 Addressables 并加载主场景 UIScene。
/// UIScene 必须事先在 Addressables Groups 窗口里标记为可寻址(key 为 "UIScene")。
///
private static void LoadMainSceneViaAddressables()
{
Debug.Log("[Launcher] 开始初始化 Addressables ...");
var initHandle = Addressables.InitializeAsync();
initHandle.Completed += handle =>
{
if (handle.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"[Launcher] Addressables 初始化失败: {handle.OperationException}");
return;
}
Debug.Log("[Launcher] Addressables 初始化完成,开始加载 UIScene ...");
var sceneHandle = Addressables.LoadSceneAsync(MainSceneKey, LoadSceneMode.Single);
sceneHandle.Completed += sceneOp =>
{
if (sceneOp.Status == AsyncOperationStatus.Succeeded)
{
Debug.Log($"[Launcher] UIScene 加载成功: {sceneOp.Result.Scene.name}");
}
else
{
Debug.LogError($"[Launcher] UIScene 加载失败: {sceneOp.OperationException}。" +
$"请确认已在 Addressables Groups 里把 UIScene 标记为可寻址,且 address key 为 '{MainSceneKey}'。");
}
};
};
}
///
/// 加载 AOT 补充元数据。清单来自 HybridCLR 生成的 AOTGenericReferences.PatchedAOTAssemblyList
/// (它由 HybridCLR/Generate/AOTGenericReferences.cs 产出,与本脚本同属 AOT 程序集)。
///
private static bool LoadAOTMetadata()
{
bool allOk = true;
foreach (var dllName in AOTGenericReferences.PatchedAOTAssemblyList)
{
// dllName 形如 "DOTween.dll",磁盘文件为 "DOTween.dll.bytes"
byte[] bytes = ReadStreamingAsset(Path.Combine(AOTMetaDir, dllName + ".bytes"));
if (bytes == null)
{
Debug.LogError($"[Launcher] 缺少 AOT 补充元数据: {dllName}");
allOk = false;
continue;
}
LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(bytes, HomologousImageMode.SuperSet);
if (err != LoadImageErrorCode.OK)
{
Debug.LogError($"[Launcher] AOT 元数据加载失败: {dllName} → {err}");
allOk = false;
}
}
return allOk;
}
/// 加载热更程序集(Assembly.Load 之后,场景里的热更 MonoBehaviour 才能被实例化)
private static bool LoadHotfixAssemblies()
{
bool allOk = true;
foreach (var asmName in HotfixAssemblies)
{
byte[] bytes = ReadStreamingAsset(Path.Combine(HotfixDir, asmName + ".dll.bytes"));
if (bytes == null)
{
Debug.LogError($"[Launcher] 缺少热更 dll: {asmName}");
allOk = false;
continue;
}
try
{
Assembly asm = Assembly.Load(bytes);
Debug.Log($"[Launcher] 热更程序集已加载: {asm.FullName}");
}
catch (System.Exception e)
{
Debug.LogError($"[Launcher] 热更 dll 加载异常: {asmName} → {e}");
allOk = false;
}
}
return allOk;
}
/// 诊断用:Game.Hotfix 里已加载的类型总数(-1 表示该程序集没找到)
private static int HotfixTypeCount()
{
var asm = FindAssembly("Game.Hotfix");
if (asm == null) return -1;
try { return asm.GetTypes().Length; } catch { return -2; }
}
/// 诊断用:能否在 Game.Hotfix 里解析到指定类型
private static bool HotfixTypeResolvable(string typeName)
{
var asm = FindAssembly("Game.Hotfix");
return asm != null && asm.GetType(typeName) != null;
}
private static Assembly FindAssembly(string name)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
if (asm.GetName().Name == name) return asm;
return null;
}
///
/// 读取 StreamingAssets 下的文件(兼容 Android 的 apk 内路径)。
/// 找不到或读取失败返回 null。
///
private static byte[] ReadStreamingAsset(string relativePath)
{
string fullPath = Path.Combine(Application.streamingAssetsPath, relativePath);
// Android / WebGL:StreamingAssets 在 apk/包体内,必须走 UnityWebRequest
#if UNITY_ANDROID && !UNITY_EDITOR
using (var req = UnityWebRequest.Get(fullPath))
{
req.SendWebRequest();
while (!req.isDone) { } // 启动阶段短暂阻塞,可接受
if (req.result != UnityWebRequest.Result.Success)
return null;
return req.downloadHandler.data;
}
#else
if (!File.Exists(fullPath))
return null;
return File.ReadAllBytes(fullPath);
#endif
}
}