Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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/";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user