using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using UnityEngine; using XGame.MiniGame; using XWorld.Framework.Protocol; using UObject = UnityEngine.Object; namespace XGame { public sealed class CSharpClientApp : MonoBehaviour { private const string LoginTokenKey = "LoginToken"; private const string LoginTokenTimeKey = "LoginTokenTime"; private const string LoginAccountKey = "LoginAccount"; private const string LoginRememberKey = "LoginRememberAccount"; private const string LoginPasswordKey = "LoginPassword"; private const string LoginRememberPasswordKey = "LoginRememberPassword"; private const string LoginAutoKey = "LoginAutoLogin"; private const string LoginAgreementKey = "LoginAgreement"; private const string LoginUserIdKey = "LoginUserId"; private const string LoginGuestIdKey = "LoginGuestId"; private const string LoginPlayerIdKey = "LoginPlayerId"; private const string LocalClientIdKey = "LocalClientId"; private const double TokenValidSeconds = 7 * 24 * 60 * 60; public static CSharpClientApp Instance { get; private set; } private AuthRegisterLoginController loginController; private GameObject loginPanel; private GameLobbyController lobbyController; private GameObject lobbyPanel; private MatchWaitingController waitingController; private GameObject waitingPanel; private XWebSocket socket; private MiniGameNetChannel lobbyNet; private MiniGameHost gameHost; private LobbyWorldController lobbyWorld; private readonly List games = new List(); private bool lobbyActive; private bool openingLogin; private bool openingLobby; private bool openingWaiting; private string matchMessage = string.Empty; private string pendingGameId = string.Empty; private int pendingGameVersion; private float nextLobbyMoveFrameLogTime; private int m_pid;//当前已鉴权的玩家 id(来自登录/注册返回的 token) public static IEnumerator Init() { CSharpClientApp app = EnsureInstance(); yield return app.StartFlow(); } public static void RestartCurrent() { if (Instance == null) { return; } Instance.StopFlow(); XWCoroutine.StartCo(Instance.StartFlow()); } private static CSharpClientApp EnsureInstance() { GameObject main = GameObject.Find("Main"); if (main == null) { main = new GameObject("Main"); } CSharpClientApp app = main.GetComponent(); if (app == null) { app = main.AddComponent(); } return app; } private void Awake() { Instance = this; } private IEnumerator StartFlow() { Debug.Log("[CSharpClientApp] start"); if (HasValidToken()) { EnterLobby(); yield break; } ClearToken(); OpenLoginPanel(string.Empty); yield break; } private void Update() { if (lobbyActive) { lobbyNet?.Pump(); } } private void OnDestroy() { StopFlow(); if (Instance == this) { Instance = null; } } private void StopFlow() { lobbyActive = false; lobbyWorld?.SendLeave(); lobbyNet = null; socket?.close(); socket = null; CloseLoginPanel(); CloseLobby(); CloseWaiting(); } private void OpenLoginPanel(string reason) { if (loginPanel != null || openingLogin) { ShowLoginError(reason); return; } openingLogin = true; XWCoroutine.StartCo(CoOpenLoginPanel(reason)); } private IEnumerator CoOpenLoginPanel(string reason) { UObject loaded = null; yield return XResLoader.coLoadRes("Assets/Game/Art/UI/Prefab/AuthRegisterLoginView.prefab", typeof(GameObject), obj => loaded = obj); openingLogin = false; GameObject asset = loaded as GameObject; if (asset == null) { Debug.LogError("[CSharpClientApp] load login panel failed"); yield break; } loginPanel = Instantiate(asset); AttachToMain(loginPanel); loginController = loginPanel.GetComponent(); if (loginController == null) { loginController = loginPanel.AddComponent(); } loginController.OnLogin = OnLoginClicked; loginController.OnRegister = OnRegisterClicked; loginController.OnGuestLogin = OnGuestLoginClicked; bool rememberPassword = XData.GetInt(LoginRememberPasswordKey, 0) == 1; string account = (XData.GetInt(LoginRememberKey, 0) == 1 || rememberPassword) ? XData.GetString(LoginAccountKey, "") : ""; string password = rememberPassword ? XData.GetString(LoginPasswordKey, "") : ""; loginController.SetInitialState(account, password, rememberPassword, XData.GetInt(LoginAutoKey, 0) == 1, XData.GetInt(LoginAgreementKey, 0) == 1); loginController.SetServerInfo("Recommended Server", GlobalData.bLocal ? "Local" : "Online"); loginController.SetVersion("Version 0.1.0"); ShowLoginError(reason); // 真机上旧 lua(client/login) 仍会再实例化一份 AuthRegisterLoginView 并盖在上层抢点击 // (那份没挂控制器、按钮无监听 → "点登录没反应")。lua 资源(bs.unity3d)热更未必到位, // 这里在稳定热更的 C# 侧兜底:开面板后一段时间内持续销毁除本面板外的重复登录面板。 StartCoroutine(EnsureSingleLoginPanel(loginPanel)); } //保证场景中只有 C# 这份(已接好委托)的登录面板存活,销毁其它来源的重复实例。 //用时间窗口轮询是为兼容"重复面板比本面板更晚创建"(lua 与 C# 启动时序不定)的情况。 private IEnumerator EnsureSingleLoginPanel(GameObject keep) { float t = 0f; while (t < 3f && keep != null) { var grs = UnityEngine.Object.FindObjectsOfType(); foreach (var g in grs) { if (g == null || g.gameObject == keep) { continue; } if (!g.gameObject.name.Contains("AuthRegisterLoginView")) { continue; } Debug.Log($"[CSharpClientApp] destroy duplicate login panel {g.gameObject.name} #{g.gameObject.GetInstanceID()}"); Destroy(g.gameObject); } t += Time.unscaledDeltaTime; yield return null; } } private void OnLoginClicked(string account, string password, bool rememberPassword, bool autoLogin) { account = account ?? string.Empty; password = password ?? string.Empty; if (account.Length == 0) { ShowLoginError("Please enter account."); return; } if (password.Length == 0) { ShowLoginError("Please enter password."); return; } XData.SetInt(LoginRememberKey, rememberPassword ? 1 : 0); XData.SetInt(LoginRememberPasswordKey, rememberPassword ? 1 : 0); XData.SetInt(LoginAutoKey, autoLogin ? 1 : 0); XData.SetInt(LoginAgreementKey, 1); if (rememberPassword) { XData.SetString(LoginAccountKey, account); XData.SetString(LoginPasswordKey, password); } else { XData.DeleteKey(LoginAccountKey); XData.DeleteKey(LoginPasswordKey); } if (GlobalData.bLocal) { // 本地/内网 dev:网关未启用鉴权,沿用 ?pid= 路径 SaveSession(account, account); EnterLobby(); return; } ShowLoginError(""); XWCoroutine.StartCo(AuthClient.Login(account, password, (ok, token, pid, err) => { if (!ok) { ShowLoginError(err); return; } SaveSession(account, account, token, pid); EnterLobby(); })); } private void OnGuestLoginClicked(string account, string password, bool remember, bool autoLogin) { string guestId = GetGuestId(); XData.SetInt(LoginAgreementKey, 1); XData.SetInt(LoginRememberKey, 1); XData.SetInt(LoginRememberPasswordKey, 0); XData.SetString(LoginAccountKey, "guest:" + guestId); XData.DeleteKey(LoginPasswordKey); SaveSession("guest:" + guestId, guestId); EnterLobby(); } private void OnRegisterClicked(string account, string password, string confirmPassword) { account = account ?? string.Empty; password = password ?? string.Empty; confirmPassword = confirmPassword ?? string.Empty; if (account.Length == 0) { ShowLoginError("Please enter account."); return; } if (password.Length == 0) { ShowLoginError("Please enter password."); return; } if (password != confirmPassword) { ShowLoginError("Passwords do not match."); return; } XData.SetInt(LoginRememberKey, 1); XData.SetInt(LoginRememberPasswordKey, 0); XData.SetInt(LoginAutoKey, 0); XData.SetInt(LoginAgreementKey, 1); XData.SetString(LoginAccountKey, account); XData.DeleteKey(LoginPasswordKey); if (GlobalData.bLocal) { SaveSession(account, account); EnterLobby(); return; } ShowLoginError(""); XWCoroutine.StartCo(AuthClient.Register(account, password, (ok, token, pid, err) => { if (!ok) { ShowLoginError(err); return; } SaveSession(account, account, token, pid); EnterLobby(); })); } private void EnterLobby() { CloseLoginPanel(); OpenLobby(); ConnectLobby(); } private void OpenLobby() { if (lobbyPanel != null || openingLobby) { RefreshLobby(); return; } openingLobby = true; XWCoroutine.StartCo(CoOpenLobby()); } private IEnumerator CoOpenLobby() { UObject loaded = null; yield return XResLoader.coLoadRes("Assets/Game/Art/UI/Prefab/UI_GameLobby.prefab", typeof(GameObject), obj => loaded = obj); openingLobby = false; GameObject asset = loaded as GameObject; if (asset == null) { Debug.LogError("[CSharpClientApp] load lobby panel failed"); yield break; } lobbyPanel = Instantiate(asset); AttachToMain(lobbyPanel); lobbyController = lobbyPanel.GetComponent(); if (lobbyController == null) { lobbyController = lobbyPanel.AddComponent(); } lobbyController.OnMatch = OnMatchClicked; RefreshLobby(); } private void ConnectLobby() { if (socket != null && socket.IsConected()) { JoinLobbyWorld(); RequestGameList(); return; } string rawNode = GlobalData.CurNode ?? string.Empty; #if XW_DEVTEST if (MiniGameTestOverride.IsActive && !string.IsNullOrEmpty(MiniGameTestOverride.GatewayUrl)) { rawNode = AddDevPid(MiniGameTestOverride.GatewayUrl); } #endif if (rawNode.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) || rawNode.StartsWith("wss://", StringComparison.OrdinalIgnoreCase)) { int localPid = GetLobbyPlayerId(); if (GlobalData.bLocal) { // 本地/内网 dev 网关未启用鉴权,只认 ?pid=;每台设备必须使用不同 pid。 rawNode = WithPlayerId(rawNode); Debug.Log("[CSharpClientApp] local lobby pid=" + localPid + " url=" + rawNode); } else { string token = !string.IsNullOrEmpty(GlobalData.Token) ? GlobalData.Token : XData.GetString(LoginTokenKey, ""); if (!string.IsNullOrEmpty(token)) { // 生产:带签名 token 连接(网关验签取权威 pid) string sep = rawNode.Contains("?") ? "&" : "?"; rawNode = rawNode + sep + "token=" + System.Uri.EscapeDataString(token); } else { rawNode = WithPlayerId(rawNode); } } SetLobbyStatus("Connecting " + rawNode); lsocket.connect(rawNode, 0, OnLobbyConnected); return; } string node = NormalizeNode(rawNode, 7777); SplitHostPort(node, 7777, out string host, out int port); SetLobbyStatus("Connecting " + host + ":" + port); lsocket.connect(host, port, OnLobbyConnected); } private void OnLobbyConnected(XWebSocket sock, string err) { if (!string.IsNullOrEmpty(err) || sock == null) { SetLobbyStatus("Network unavailable."); Debug.LogError("[CSharpClientApp] lobby connect failed: " + err); return; } socket = sock; lobbyNet = new MiniGameNetChannel(sock); lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame; lobbyActive = true; SetLobbyStatus("Lobby connected."); JoinLobbyWorld(); RequestGameList(); } private void RequestGameList() { if (lobbyNet == null) { RefreshLobby(); return; } lobbyNet.SendFramework(FrameworkOpcode.GameListRequest, Array.Empty()); } private void RefreshLobby() { if (lobbyController == null) { return; } lobbyController.BeginGames(); if (games.Count == 0) { lobbyController.AddGame(string.Empty, "Match Game", "Server will pick an available game"); } else { foreach (GameInfoMsg game in games) { string name = string.IsNullOrEmpty(game.DisplayName) ? game.GameId : game.DisplayName; lobbyController.AddGame(game.GameId, name, "players " + game.MinPlayers + "-" + game.MaxPlayers); } } lobbyController.EndGames(); if (!string.IsNullOrEmpty(matchMessage)) { lobbyController.SetStatus(matchMessage); } } private void OnMatchClicked(string gameId) { matchMessage = "Matching..."; pendingGameId = string.Empty; pendingGameVersion = 0; CloseLobby(); OpenWaiting(gameId ?? string.Empty, matchMessage, false); if (lobbyNet == null) { SetWaitingStatus("Network unavailable.", true); return; } if (string.IsNullOrEmpty(gameId)) { lobbyNet.SendFramework(FrameworkOpcode.RandomMatchRequest, Array.Empty()); } else { int version = 0; for (int i = 0; i < games.Count; i++) { if (games[i].GameId == gameId) { version = games[i].Version; break; } } pendingGameId = gameId; pendingGameVersion = version; lobbyNet.SendFramework(FrameworkOpcode.MatchRequest, new MatchRequestMsg { GameId = gameId, Version = version }.Encode()); } } private void OpenWaiting(string gameId, string message, bool failed) { if (waitingPanel != null) { waitingController?.SetWaiting(gameId, message); waitingController?.SetStatus(message, failed); return; } if (openingWaiting) { return; } openingWaiting = true; XWCoroutine.StartCo(CoOpenWaiting(gameId, message, failed)); } private IEnumerator CoOpenWaiting(string gameId, string message, bool failed) { UObject loaded = null; yield return XResLoader.coLoadRes("Assets/Game/Art/UI/Prefab/UI_MatchWaiting.prefab", typeof(GameObject), obj => loaded = obj); openingWaiting = false; GameObject asset = loaded as GameObject; if (asset == null) { Debug.LogError("[CSharpClientApp] load match waiting panel failed"); yield break; } waitingPanel = Instantiate(asset); AttachToMain(waitingPanel); waitingController = waitingPanel.GetComponent(); if (waitingController == null) { waitingController = waitingPanel.AddComponent(); } waitingController.OnCloseRequested = OnCloseWaiting; waitingController.SetWaiting(gameId, message); waitingController.SetStatus(message, failed); } private void OnCloseWaiting() { matchMessage = string.Empty; pendingGameId = string.Empty; pendingGameVersion = 0; CloseWaiting(); OpenLobby(); } private void OnLobbyFrameworkFrame(Frame frame) { switch ((FrameworkOpcode)frame.Opcode) { case FrameworkOpcode.GameListResponse: games.Clear(); games.AddRange(GameListResponseMsg.Decode(frame.Payload).Games); matchMessage = "Games discovered: " + games.Count; RefreshLobby(); break; case FrameworkOpcode.MatchAssigned: MatchAssignedMsg assigned = MatchAssignedMsg.Decode(frame.Payload); pendingGameId = assigned.GameId; pendingGameVersion = assigned.Version; Debug.Log("[CSharpClientApp] match assigned " + pendingGameId + "@" + pendingGameVersion); SetWaitingStatus("Waiting for players...", false); break; case FrameworkOpcode.MatchFound: MatchFoundMsg found = MatchFoundMsg.Decode(frame.Payload); Debug.Log("[CSharpClientApp] match found room=" + found.RoomId + " self=" + found.SelfPlayerId); if (string.IsNullOrEmpty(pendingGameId)) { SetWaitingStatus("Room ready, but no game was assigned.", true); break; } SetWaitingStatus("Room ready.", false); EnterAssignedGame(pendingGameId, pendingGameVersion, found); break; case FrameworkOpcode.Error: ErrorMsg err = ErrorMsg.Decode(frame.Payload); pendingGameId = string.Empty; pendingGameVersion = 0; SetWaitingStatus("Server error " + err.Code + ": " + err.Message, true); break; case FrameworkOpcode.LobbyState: Debug.Log("[CSharpClientApp] recv LobbyState bytes=" + (frame.Payload?.Length ?? 0)); lobbyWorld?.HandleFrame(frame); break; case FrameworkOpcode.LobbyMove: if (Time.unscaledTime >= nextLobbyMoveFrameLogTime) { nextLobbyMoveFrameLogTime = Time.unscaledTime + 1f; Debug.Log("[CSharpClientApp] recv LobbyMove bytes=" + (frame.Payload?.Length ?? 0)); } lobbyWorld?.HandleFrame(frame); break; } } private void EnterAssignedGame(string gameId, int version, MatchFoundMsg found) { pendingGameId = string.Empty; pendingGameVersion = 0; lobbyActive = false; lobbyWorld?.SendLeave(); lobbyNet = null; CloseWaiting(); CloseLobby(); gameHost = GetComponent(); if (gameHost == null) { gameHost = gameObject.AddComponent(); } gameHost.OnGameExited = () => { matchMessage = "Returned to lobby."; lobbyNet = new MiniGameNetChannel(socket); lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame; lobbyActive = true; OpenLobby(); JoinLobbyWorld(); RequestGameList(); }; gameHost.EnterGame(ResolvedCdnBase(), gameId, version, socket, false); // 大厅链路 MatchFound 由大厅通道消费,宿主收不到,须显式注入房间信息(结算弹框判胜负用) gameHost.SetMatchInfo(found.RoomId, found.SelfPlayerId); } private string ResolvedCdnBase() { string cdn = GlobalData.CurCDN; #if XW_DEVTEST if (MiniGameTestOverride.IsActive && !string.IsNullOrEmpty(MiniGameTestOverride.CdnBase)) { cdn = MiniGameTestOverride.CdnBase; } #endif if (string.IsNullOrEmpty(cdn)) { cdn = "http://127.0.0.1:15081/"; } cdn = NormalizeCdnBase(cdn); if (!cdn.EndsWith("/")) { cdn += "/"; } if (cdn.EndsWith("/minigame/", StringComparison.OrdinalIgnoreCase)) { return cdn; } return cdn + "minigame/"; } #if XW_DEVTEST private static string AddDevPid(string gatewayUrl) { if (string.IsNullOrWhiteSpace(gatewayUrl)) { return gatewayUrl; } return WithPlayerId(gatewayUrl); } #endif private void JoinLobbyWorld() { if (lobbyNet == null) { return; } if (lobbyWorld == null) { lobbyWorld = gameObject.GetComponent(); if (lobbyWorld == null) { lobbyWorld = gameObject.AddComponent(); } } lobbyWorld.Initialize( (op, payload) => lobbyNet?.SendFramework(op, payload ?? Array.Empty()), GetLobbyPlayerId(), GetLobbyPlayerName(), 0); } private static string GetLobbyPlayerName() { string account = XData.GetString(LoginAccountKey, string.Empty); if (!string.IsNullOrEmpty(account)) { return account; } string userId = XData.GetString(LoginUserIdKey, string.Empty); return string.IsNullOrEmpty(userId) ? "Player" : userId; } private static int GetStablePlayerId() { string userId = GlobalData.bLocal ? GetLocalClientId() : XData.GetString(LoginUserIdKey, string.Empty); if (string.IsNullOrEmpty(userId)) { userId = XData.GetString(LoginAccountKey, string.Empty); } if (string.IsNullOrEmpty(userId)) { userId = SystemInfo.deviceUniqueIdentifier; } int parsed; if (int.TryParse(userId, out parsed) && parsed > 0) { return parsed; } unchecked { uint hash = 2166136261u; for (int i = 0; i < userId.Length; i++) { hash ^= userId[i]; hash *= 16777619u; } return (int)(hash % 2000000000u) + 1; } } private int GetLobbyPlayerId() { if (GlobalData.bLocal) { return GetStablePlayerId(); } if (m_pid > 0) { return m_pid; } int savedPid = XData.GetInt(LoginPlayerIdKey, 0); if (savedPid > 0) { m_pid = savedPid; return savedPid; } return GetStablePlayerId(); } private static string GetLocalClientId() { string localId = XData.GetString(LocalClientIdKey, string.Empty); if (!string.IsNullOrEmpty(localId)) { return localId; } localId = SystemInfo.deviceUniqueIdentifier; if (string.IsNullOrEmpty(localId) || localId == "unsupported") { localId = Guid.NewGuid().ToString("N"); } XData.SetString(LocalClientIdKey, localId); XData.Save(); return localId; } private static string WithPlayerId(string gatewayUrl) { if (string.IsNullOrWhiteSpace(gatewayUrl)) { return gatewayUrl; } int pid = GetStablePlayerId(); int queryStart = gatewayUrl.IndexOf('?'); if (queryStart < 0) { return gatewayUrl + "?pid=" + pid; } string head = gatewayUrl.Substring(0, queryStart); string query = gatewayUrl.Substring(queryStart + 1); string[] parts = query.Split('&'); bool replaced = false; for (int i = 0; i < parts.Length; i++) { if (parts[i].StartsWith("pid=", StringComparison.OrdinalIgnoreCase)) { parts[i] = "pid=" + pid; replaced = true; break; } } return head + "?" + (replaced ? string.Join("&", parts) : query + "&pid=" + pid); } private static string NormalizeCdnBase(string cdnBase) { string url = string.IsNullOrWhiteSpace(cdnBase) ? "http://127.0.0.1:15081/" : cdnBase.Trim(); if (url.StartsWith("http://file://", StringComparison.OrdinalIgnoreCase)) { url = url.Substring("http://".Length); } else if (url.StartsWith("https://file://", StringComparison.OrdinalIgnoreCase)) { url = url.Substring("https://".Length); } if (url.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { url = NormalizeFileUrl(url); } else if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { url = "http://" + url; } return url.EndsWith("/") ? url : url + "/"; } private static string NormalizeFileUrl(string url) { string rawPath = url.Substring("file://".Length).Replace('\\', '/'); while (rawPath.StartsWith("/") && rawPath.Length > 2 && rawPath[2] == ':') { rawPath = rawPath.Substring(1); } string localPath = Uri.UnescapeDataString(rawPath).Replace('/', Path.DirectorySeparatorChar); try { return new Uri(localPath).AbsoluteUri; } catch { return "file:///" + rawPath.Replace("#", "%23"); } } private void SaveSession(string account, string userId, string token = null, int pid = 0) { string sessionToken = string.IsNullOrEmpty(token) ? Guid.NewGuid().ToString("N") : token; XData.SetString(LoginTokenKey, sessionToken); XData.SetString(LoginTokenTimeKey, Now().ToString(CultureInfo.InvariantCulture)); XData.SetString(LoginUserIdKey, string.IsNullOrEmpty(userId) ? account : userId); XData.SetString(LoginAccountKey, account); if (pid > 0) { m_pid = pid; XData.SetInt(LoginPlayerIdKey, pid); } else if (!GlobalData.bLocal) { m_pid = 0; XData.DeleteKey(LoginPlayerIdKey); } XData.Save(); GlobalData.Token = sessionToken; } private bool HasValidToken() { string token = XData.GetString(LoginTokenKey, ""); double savedAt = ParseDouble(XData.GetString(LoginTokenTimeKey, "0")); if (string.IsNullOrEmpty(token) || savedAt <= 0) { return false; } if (Now() - savedAt > TokenValidSeconds) { return false; } GlobalData.Token = token; m_pid = XData.GetInt(LoginPlayerIdKey, 0); return true; } private static void ClearToken() { XData.DeleteKey(LoginTokenKey); XData.DeleteKey(LoginTokenTimeKey); XData.DeleteKey(LoginPlayerIdKey); XData.Save(); } private static string GetGuestId() { string guestId = XData.GetString(LoginGuestIdKey, ""); if (!string.IsNullOrEmpty(guestId)) { return guestId; } string savedAccount = XData.GetString(LoginAccountKey, ""); if (savedAccount.StartsWith("guest:", StringComparison.Ordinal)) { guestId = savedAccount.Substring("guest:".Length); } if (string.IsNullOrEmpty(guestId)) { guestId = SystemInfo.deviceUniqueIdentifier; } if (string.IsNullOrEmpty(guestId) || guestId == "unsupported") { guestId = ((long)(Now() * 1000)).ToString(CultureInfo.InvariantCulture); } XData.SetString(LoginGuestIdKey, guestId); XData.Save(); return guestId; } private static double Now() { return Utility.GetTimeTick() / 1000.0; } private static double ParseDouble(string value) { double result; return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result) ? result : 0; } private void ShowLoginError(string message) { if (loginController != null && !string.IsNullOrEmpty(message)) { loginController.ShowError(message); } } private void SetLobbyStatus(string message) { matchMessage = message ?? string.Empty; if (lobbyController != null) { lobbyController.SetStatus(matchMessage); } } private void SetWaitingStatus(string message, bool failed) { matchMessage = message ?? string.Empty; if (waitingController != null) { waitingController.SetStatus(matchMessage, failed); } if (lobbyController != null) { lobbyController.SetStatus(matchMessage); } } private void CloseLoginPanel() { if (loginPanel != null) { Destroy(loginPanel); } loginPanel = null; loginController = null; } private void CloseLobby() { if (lobbyPanel != null) { Destroy(lobbyPanel); } lobbyPanel = null; lobbyController = null; } private void CloseWaiting() { if (waitingPanel != null) { Destroy(waitingPanel); } waitingPanel = null; waitingController = null; } // 登录/大厅/匹配等待都是普通功能界面 → 挂到 UIRoot/Normal 层(见 UI 挂载规则) private static void AttachToMain(GameObject obj) { UIManager.AttachTo(UILayer.Normal, obj); } private static string NormalizeNode(string node, int defaultPort) { string address = string.IsNullOrEmpty(node) ? "127.0.0.1:" + defaultPort : node.Trim(); if (address.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) { address = address.Substring("http://".Length); } else if (address.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { address = address.Substring("https://".Length); } int slash = address.IndexOf('/'); if (slash >= 0) { address = address.Substring(0, slash); } if (address.IndexOf(':') < 0) { address += ":" + defaultPort; } return address; } private static void SplitHostPort(string node, int defaultPort, out string host, out int port) { host = node; port = defaultPort; int colon = node.LastIndexOf(':'); if (colon >= 0) { host = node.Substring(0, colon); int.TryParse(node.Substring(colon + 1), out port); } if (string.IsNullOrEmpty(host)) { host = "127.0.0.1"; } if (port <= 0) { port = defaultPort; } } } }