using System; using System.Collections.Generic; using System.IO; using UnityEngine; using XWorld.Framework; using XWorld.Framework.Protocol; namespace XGame.MiniGame { public sealed class PcLobbySmokeLauncher : MonoBehaviour { public string GatewayUrl = "ws://127.0.0.1:5005/ws?pid=1"; public string CdnBase = ""; public bool AutoStart; private readonly List _games = new List(); private string _status = "idle"; private XWebSocket _socket; private MiniGameNetChannel _lobbyNet; private MiniGameHost _gameHost; private bool _lobbyActive; private bool _matching; private float _matchElapsed; private void Start() { #if XW_DEVTEST if (GetComponent() == null) gameObject.AddComponent(); #endif if (AutoStart) ConnectLobby(); } private void OnDestroy() { _socket?.close(); _socket = null; } private void Update() { if (_lobbyActive) _lobbyNet?.Pump(); if (_matching) _matchElapsed += Time.deltaTime; } private void OnGUI() { if (!_lobbyActive && _matching) return; GUILayout.BeginArea(new Rect(16, 16, 640, 260), GUI.skin.box); GUILayout.Label("PC C# Lobby"); GUILayout.Label("Status: " + _status); GUILayout.Label("Gateway URL"); GatewayUrl = GUILayout.TextField(GatewayUrl); GUILayout.Label("CDN Base"); CdnBase = GUILayout.TextField(string.IsNullOrEmpty(CdnBase) ? DefaultCdnBase() : CdnBase); GUILayout.BeginHorizontal(); if (GUILayout.Button(_socket == null ? "Enter Lobby" : "Refresh Games", GUILayout.Width(140))) { if (_socket == null) ConnectLobby(); else RequestGameList(); } GUI.enabled = _socket != null && !_matching && _games.Count > 0; if (GUILayout.Button("Random Match", GUILayout.Width(140))) RandomMatch(); GUI.enabled = true; GUILayout.EndHorizontal(); if (_matching) GUILayout.Label("Matching: " + _matchElapsed.ToString("0.0") + "s / AI fill after server timeout"); GUILayout.Label("Discovered games:"); if (_games.Count == 0) GUILayout.Label("(none)"); for (int i = 0; i < _games.Count; i++) { var g = _games[i]; GUILayout.Label("- " + g.GameId + " v" + g.Version + " players " + g.MinPlayers + "-" + g.MaxPlayers); } GUILayout.EndArea(); } public void ConnectLobby() { string url = ResolvedGatewayUrl(); _status = "connecting " + url; _socket?.close(); _socket = new XWebSocket(); _socket.Connect(url, (sock, err) => { if (!string.IsNullOrEmpty(err) || sock == null) { _status = "connect failed: " + err; Debug.LogError("[PcLobby] " + _status); return; } _status = "lobby connected"; _lobbyNet = new MiniGameNetChannel(sock); _lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame; _lobbyActive = true; _matching = false; RequestGameList(); }); } private void RequestGameList() { _status = "requesting game list"; _lobbyNet?.SendFramework(FrameworkOpcode.GameListRequest, Array.Empty()); } private void RandomMatch() { _status = "matching random game"; _matching = true; _matchElapsed = 0f; _lobbyNet?.SendFramework(FrameworkOpcode.RandomMatchRequest, Array.Empty()); } private void OnLobbyFrameworkFrame(Frame frame) { switch ((FrameworkOpcode)frame.Opcode) { case FrameworkOpcode.GameListResponse: var list = GameListResponseMsg.Decode(frame.Payload); _games.Clear(); _games.AddRange(list.Games); _status = "games discovered: " + _games.Count; break; case FrameworkOpcode.MatchAssigned: var assigned = MatchAssignedMsg.Decode(frame.Payload); _status = "assigned " + assigned.GameId + " v" + assigned.Version + ", loading hot DLL"; EnterAssignedGame(assigned.GameId, assigned.Version); break; case FrameworkOpcode.Error: var err = ErrorMsg.Decode(frame.Payload); _status = "server error " + err.Code + ": " + err.Message; _matching = false; break; } } private void EnterAssignedGame(string gameId, int version) { _lobbyActive = false; _lobbyNet = null; _gameHost = gameObject.GetComponent(); if (_gameHost == null) _gameHost = gameObject.AddComponent(); _gameHost.OnGameExited = () => { _status = "returned to lobby"; _matching = false; _lobbyNet = new MiniGameNetChannel(_socket); _lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame; _lobbyActive = true; RequestGameList(); }; _gameHost.EnterGame(ResolvedCdnBase(), gameId, version, _socket, false); } private string ResolvedCdnBase() { #if XW_DEVTEST if (MiniGameTestOverride.IsActive && !string.IsNullOrEmpty(MiniGameTestOverride.CdnBase)) return MiniGameTestOverride.CdnBase; #endif return string.IsNullOrEmpty(CdnBase) ? DefaultCdnBase() : CdnBase; } private string ResolvedGatewayUrl() { #if XW_DEVTEST if (MiniGameTestOverride.IsActive && !string.IsNullOrEmpty(MiniGameTestOverride.GatewayUrl)) { string g = MiniGameTestOverride.GatewayUrl; return WithLocalPid(g); } #endif return WithLocalPid(GatewayUrl); } private static string WithLocalPid(string gatewayUrl) { if (string.IsNullOrWhiteSpace(gatewayUrl)) { return gatewayUrl; } int pid = GetLocalPlayerId(); 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 int GetLocalPlayerId() { const string key = "LocalClientId"; string id = XData.GetString(key, string.Empty); if (string.IsNullOrEmpty(id)) { id = SystemInfo.deviceUniqueIdentifier; if (string.IsNullOrEmpty(id) || id == "unsupported") { id = Guid.NewGuid().ToString("N"); } XData.SetString(key, id); XData.Save(); } unchecked { uint hash = 2166136261u; for (int i = 0; i < id.Length; i++) { hash ^= id[i]; hash *= 16777619u; } return (int)(hash % 2000000000u) + 1; } } private static string DefaultCdnBase() { // Editor: /Client/Assets -> /CDN/minigame/(由 XWorld/生成小游戏更新 菜单产出) var assets = new DirectoryInfo(Application.dataPath); var client = assets.Parent; var repo = client?.Parent; string root = repo != null ? repo.FullName : Directory.GetCurrentDirectory(); return "file:///" + root.Replace('\\', '/') + "/CDN/minigame/"; } } }