using System; using System.Collections; using System.Text; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using XWorld.Framework; using XWorld.Framework.Protocol; namespace XGame.MiniGame { // 一次小游戏会话的宿主。EnterGame 启动整条链路;每帧驱动;ExitGame 收尾。 public sealed class MiniGameHost : MonoBehaviour { // 标准 UGUI 结算弹窗预设(由 Doc/UIPrefabCreater/UI_MiniGameResult.json 经管线生成,进 game_art_ui_prefab AB) private const string ResultDialogPrefabPath = "Assets/Game/Art/UI/Prefab/UI_MiniGameResult.prefab"; private IGameClient _client; private MiniGameNetChannel _net; private GameClientCtx _ctx; private MiniGameAssetLoader _assets; private bool _running; private bool _waitingForResultConfirm; private string _gameId; private int _version; private string _roomId; private int _selfPlayerId; private GameObject _resultDialog; public Action OnGameExited; // 入口:cdnBase 小游戏 CDN 根;sock 已连接的 XWebSocket(来自大厅/或本宿主新建) public void EnterGame(string cdnBase, string gameId, int version, XWebSocket sock) => EnterGame(cdnBase, gameId, version, sock, true); public void EnterGame(string cdnBase, string gameId, int version, XWebSocket sock, bool sendMatchRequest) { _gameId = gameId; _version = version; // FIX: XWCoroutine.StartCo is a static method (not Instance.StartCo) XWCoroutine.StartCo(CoEnter(cdnBase, gameId, version, sock, sendMatchRequest)); } // 大厅链路:MatchFound 由大厅通道消费(sendMatchRequest=false 时宿主收不到), // 由外部把房间信息注入,否则结算弹框无法判断胜负(_selfPlayerId 恒 0)。 public void SetMatchInfo(string roomId, int selfPlayerId) { _roomId = roomId; _selfPlayerId = selfPlayerId; } private IEnumerator CoEnter(string cdnBase, string gameId, int version, XWebSocket sock, bool sendMatchRequest) { // 1) 下载 + 校验 var dl = new MiniGameDownloader(cdnBase, gameId, version); bool ok = false; yield return dl.Run(r => ok = r); if (!ok) { Debug.LogError("[MiniGameHost] 下载失败: " + dl.Error); yield break; } // 2) 加载 Core/Client DLL,反射入口 var asmLoader = new MiniGameAssemblyLoader(); if (!asmLoader.Load(dl.LocalDir, dl.Manifest)) { Debug.LogError("[MiniGameHost] 加载程序集失败: " + asmLoader.Error); yield break; } // 3) 网络通道 _net = new MiniGameNetChannel(sock); _net.OnGameMessage = msg => { if (_client != null) SafeCall(() => _client.OnNetMessage(msg), "OnNetMessage"); }; _net.OnFrameworkFrame = OnFrameworkFrame; // 4) 反射实例化 IGameClient + 注入 ctx _client = asmLoader.CreateClient(); _assets = new MiniGameAssetLoader(dl.LocalDir, dl.Manifest); _ctx = new GameClientCtx( _assets, new UnityLogger($"[{gameId}]"), msg => { if (_net != null) _net.SendGame(msg); }, () => ExitGame()); if (sendMatchRequest) { _net.SendFramework(FrameworkOpcode.MatchRequest, new MatchRequestMsg { GameId = gameId, Version = version }.Encode()); } // 6) OnEnter 接管 // 注意:OnEnter 在 MatchFound 到达之前调用,房间可能尚未就绪;小游戏应等待后续网络消息再依赖房间状态 SafeCall(() => _client.OnEnter(_ctx), "OnEnter"); _running = true; Debug.Log("[MiniGameHost] 小游戏已进入: " + gameId); } private void OnFrameworkFrame(Frame f) { switch ((FrameworkOpcode)f.Opcode) { case FrameworkOpcode.MatchFound: MatchFoundMsg found = MatchFoundMsg.Decode(f.Payload); _roomId = found.RoomId; _selfPlayerId = found.SelfPlayerId; Debug.Log("[MiniGameHost] 房间就绪: " + _roomId); break; case FrameworkOpcode.RoomEnd: Debug.Log("[MiniGameHost] 房间结束,弹结算框"); ShowResultDialog(RoomEndMsg.Decode(f.Payload)); break; // Snapshot/Heartbeat 等:本步骤交给小游戏自行通过 Game 帧处理;如需框架级快照可在此扩展 } } private void Update() { if (!_running) return; _net?.Pump(); if (_client != null) SafeCall(() => _client.OnUpdate(Time.deltaTime), "OnUpdate"); } public void ExitGame() { if (!_running && _client == null) return; DestroyResultDialog(); _waitingForResultConfirm = false; _running = false; if (_client != null) { SafeCall(() => _client.OnExit(), "OnExit"); _client = null; } _net = null; _ctx = null; if (_assets != null) { _assets.UnloadAll(); _assets = null; } // 卸载本小游戏资源 AB Debug.Log("[MiniGameHost] 已退出小游戏: " + _gameId); OnGameExited?.Invoke(); // 返回大厅:交回 Lua game_module_runner 或大厅 UI(按现有大厅返回流程) } private void ShowResultDialog(RoomEndMsg roomEnd) { if (_waitingForResultConfirm) { return; } _waitingForResultConfirm = true; _running = false; DestroyResultDialog(); EnsureEventSystem(); string resultText = DecodeResultBlob(roomEnd.ResultBlob); string title = ResultTitle(roomEnd, resultText); string message = ResultMessage(resultText); // 项目规则:资源一律异步加载(同步只读本地不下载);预设不可用时回退代码构建——结算窗口宁丑不缺 XWCoroutine.StartCo(CoShowResultDialog(title, message)); } private IEnumerator CoShowResultDialog(string title, string message) { GameObject prefab = null; yield return XResLoader.coLoadRes(ResultDialogPrefabPath, typeof(GameObject), obj => prefab = obj as GameObject); if (!_waitingForResultConfirm) { yield break; // 加载期间用户已退出(ExitGame),不再弹结算 } if (prefab == null || !TryShowResultDialogFromPrefab(prefab, title, message)) { Debug.LogWarning("[MiniGameHost] 结算预设不可用,回退代码构建: " + ResultDialogPrefabPath); ShowResultDialogFallback(title, message); } } // 标准 UGUI 结算预设(Doc/UIPrefabCreater/UI_MiniGameResult.json 生成),按名绑定节点 private bool TryShowResultDialogFromPrefab(GameObject prefab, string title, string message) { GameObject instance = Instantiate(prefab); instance.name = "MiniGameResultDialog"; TextMeshProUGUI titleText = FindChildComponent(instance.transform, "Txt_Title"); TextMeshProUGUI messageText = FindChildComponent(instance.transform, "Txt_Message"); Button okButton = FindChildComponent