Files
AIC-Project/Client/Assets/Script/xmain/MiniGame/PcLobbySmokeLauncher.cs
T
2026-07-10 10:24:29 +08:00

246 lines
8.7 KiB
C#

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<GameInfoMsg> _games = new List<GameInfoMsg>();
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<DevServerSwitchUI>() == null) gameObject.AddComponent<DevServerSwitchUI>();
#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<byte>());
}
private void RandomMatch()
{
_status = "matching random game";
_matching = true;
_matchElapsed = 0f;
_lobbyNet?.SendFramework(FrameworkOpcode.RandomMatchRequest, Array.Empty<byte>());
}
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<MiniGameHost>();
if (_gameHost == null) _gameHost = gameObject.AddComponent<MiniGameHost>();
_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: <repo>/Client/Assets -> <repo>/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/";
}
}
}