fix: improve minigame reconnect flow
This commit is contained in:
@@ -23,6 +23,7 @@ namespace XWorld.Framework.Protocol
|
|||||||
LobbyLeave = 15,
|
LobbyLeave = 15,
|
||||||
WorldChatSend = 16,
|
WorldChatSend = 16,
|
||||||
WorldChatMessage = 17,
|
WorldChatMessage = 17,
|
||||||
|
MatchCancel = 18,
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class HeartbeatMsg
|
public sealed class HeartbeatMsg
|
||||||
@@ -47,19 +48,23 @@ namespace XWorld.Framework.Protocol
|
|||||||
{
|
{
|
||||||
public string GameId; // null 编码为空字符串
|
public string GameId; // null 编码为空字符串
|
||||||
public int Version;
|
public int Version;
|
||||||
|
public int RequestId;
|
||||||
|
|
||||||
public byte[] Encode()
|
public byte[] Encode()
|
||||||
{
|
{
|
||||||
var w = new PacketWriter();
|
var w = new PacketWriter();
|
||||||
w.WriteString(GameId);
|
w.WriteString(GameId);
|
||||||
w.WriteVarInt(Version);
|
w.WriteVarInt(Version);
|
||||||
|
w.WriteVarInt(RequestId);
|
||||||
return w.ToArray();
|
return w.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MatchRequestMsg Decode(byte[] data)
|
public static MatchRequestMsg Decode(byte[] data)
|
||||||
{
|
{
|
||||||
var r = new PacketReader(data);
|
var r = new PacketReader(data);
|
||||||
return new MatchRequestMsg { GameId = r.ReadString(), Version = r.ReadVarInt() };
|
var m = new MatchRequestMsg { GameId = r.ReadString(), Version = r.ReadVarInt() };
|
||||||
|
if (r.HasMore) m.RequestId = r.ReadVarInt();
|
||||||
|
return m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +72,9 @@ namespace XWorld.Framework.Protocol
|
|||||||
{
|
{
|
||||||
public string RoomId; // null 编码为空字符串
|
public string RoomId; // null 编码为空字符串
|
||||||
public int Version;
|
public int Version;
|
||||||
|
public string GameId;
|
||||||
public int SelfPlayerId;
|
public int SelfPlayerId;
|
||||||
|
public int RequestId;
|
||||||
public List<PlayerInfo> Players = new List<PlayerInfo>();
|
public List<PlayerInfo> Players = new List<PlayerInfo>();
|
||||||
|
|
||||||
public byte[] Encode()
|
public byte[] Encode()
|
||||||
@@ -91,6 +98,8 @@ namespace XWorld.Framework.Protocol
|
|||||||
w.WriteVarInt(p.AppearanceType);
|
w.WriteVarInt(p.AppearanceType);
|
||||||
w.WriteVarInt(p.AvatarId);
|
w.WriteVarInt(p.AvatarId);
|
||||||
}
|
}
|
||||||
|
w.WriteVarInt(RequestId);
|
||||||
|
w.WriteString(GameId);
|
||||||
return w.ToArray();
|
return w.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +140,14 @@ namespace XWorld.Framework.Protocol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (r.HasMore)
|
||||||
|
{
|
||||||
|
m.RequestId = r.ReadVarInt();
|
||||||
|
}
|
||||||
|
if (r.HasMore)
|
||||||
|
{
|
||||||
|
m.GameId = r.ReadString();
|
||||||
|
}
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,6 +161,24 @@ namespace XWorld.Framework.Protocol
|
|||||||
public int MaxPlayers;
|
public int MaxPlayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class MatchControlMsg
|
||||||
|
{
|
||||||
|
public int RequestId;
|
||||||
|
|
||||||
|
public byte[] Encode()
|
||||||
|
{
|
||||||
|
var w = new PacketWriter();
|
||||||
|
w.WriteVarInt(RequestId);
|
||||||
|
return w.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MatchControlMsg Decode(byte[] data)
|
||||||
|
{
|
||||||
|
var r = new PacketReader(data);
|
||||||
|
return r.HasMore ? new MatchControlMsg { RequestId = r.ReadVarInt() } : new MatchControlMsg();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class GameListResponseMsg
|
public sealed class GameListResponseMsg
|
||||||
{
|
{
|
||||||
public List<GameInfoMsg> Games = new List<GameInfoMsg>();
|
public List<GameInfoMsg> Games = new List<GameInfoMsg>();
|
||||||
@@ -187,19 +222,23 @@ namespace XWorld.Framework.Protocol
|
|||||||
{
|
{
|
||||||
public string GameId;
|
public string GameId;
|
||||||
public int Version;
|
public int Version;
|
||||||
|
public int RequestId;
|
||||||
|
|
||||||
public byte[] Encode()
|
public byte[] Encode()
|
||||||
{
|
{
|
||||||
var w = new PacketWriter();
|
var w = new PacketWriter();
|
||||||
w.WriteString(GameId);
|
w.WriteString(GameId);
|
||||||
w.WriteVarInt(Version);
|
w.WriteVarInt(Version);
|
||||||
|
w.WriteVarInt(RequestId);
|
||||||
return w.ToArray();
|
return w.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MatchAssignedMsg Decode(byte[] data)
|
public static MatchAssignedMsg Decode(byte[] data)
|
||||||
{
|
{
|
||||||
var r = new PacketReader(data);
|
var r = new PacketReader(data);
|
||||||
return new MatchAssignedMsg { GameId = r.ReadString(), Version = r.ReadVarInt() };
|
var m = new MatchAssignedMsg { GameId = r.ReadString(), Version = r.ReadVarInt() };
|
||||||
|
if (r.HasMore) m.RequestId = r.ReadVarInt();
|
||||||
|
return m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,19 +307,23 @@ namespace XWorld.Framework.Protocol
|
|||||||
{
|
{
|
||||||
public int Code;
|
public int Code;
|
||||||
public string Message; // null 编码为空字符串
|
public string Message; // null 编码为空字符串
|
||||||
|
public int RequestId;
|
||||||
|
|
||||||
public byte[] Encode()
|
public byte[] Encode()
|
||||||
{
|
{
|
||||||
var w = new PacketWriter();
|
var w = new PacketWriter();
|
||||||
w.WriteVarInt(Code);
|
w.WriteVarInt(Code);
|
||||||
w.WriteString(Message);
|
w.WriteString(Message);
|
||||||
|
w.WriteVarInt(RequestId);
|
||||||
return w.ToArray();
|
return w.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ErrorMsg Decode(byte[] data)
|
public static ErrorMsg Decode(byte[] data)
|
||||||
{
|
{
|
||||||
var r = new PacketReader(data);
|
var r = new PacketReader(data);
|
||||||
return new ErrorMsg { Code = r.ReadVarInt(), Message = r.ReadString() };
|
var m = new ErrorMsg { Code = r.ReadVarInt(), Message = r.ReadString() };
|
||||||
|
if (r.HasMore) m.RequestId = r.ReadVarInt();
|
||||||
|
return m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1066,7 +1066,7 @@ RectTransform:
|
|||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||||
m_AnchoredPosition: {x: 414, y: -333}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
m_SizeDelta: {x: 720, y: 500}
|
m_SizeDelta: {x: 720, y: 500}
|
||||||
m_Pivot: {x: 0.5, y: 0.5}
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
--- !u!222 &2156710259985382035
|
--- !u!222 &2156710259985382035
|
||||||
@@ -1766,8 +1766,8 @@ RectTransform:
|
|||||||
- {fileID: 6853786249801907381}
|
- {fileID: 6853786249801907381}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 1}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 0, y: 1}
|
m_AnchorMax: {x: 1, y: 1}
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
m_SizeDelta: {x: 0, y: 0}
|
m_SizeDelta: {x: 0, y: 0}
|
||||||
m_Pivot: {x: 0.5, y: 0.5}
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ public class XWorldUtil
|
|||||||
private const string LOCAL_SERVER_EXE = "../../Server/build-gateway-local3/Debug/AIProjectServer.exe";
|
private const string LOCAL_SERVER_EXE = "../../Server/build-gateway-local3/Debug/AIProjectServer.exe";
|
||||||
private const string LOCAL_SERVER_DLL_DIR = "../../Server/build-local-sqlite/vcpkg_installed/x64-windows/debug/bin";
|
private const string LOCAL_SERVER_DLL_DIR = "../../Server/build-local-sqlite/vcpkg_installed/x64-windows/debug/bin";
|
||||||
private const string LOCAL_SERVER_LOG_DIR = "../../Server/logs";
|
private const string LOCAL_SERVER_LOG_DIR = "../../Server/logs";
|
||||||
|
private const string MINI_GAME_GATEWAY_RUNNER_PROJECT = "../../Server/Gateway.Runner/Gateway.Runner.csproj";
|
||||||
private const string MINI_GAME_GATEWAY_RUNNER_DLL = "../../Server/Gateway.Runner/bin/Debug/net10.0/XWorld.Server.Gateway.Runner.dll";
|
private const string MINI_GAME_GATEWAY_RUNNER_DLL = "../../Server/Gateway.Runner/bin/Debug/net10.0/XWorld.Server.Gateway.Runner.dll";
|
||||||
private const string MINI_GAME_SERVER_GAMES_ROOT = "../../deploy/minigame";
|
private const string MINI_GAME_SERVER_GAMES_ROOT = "../../deploy/minigame";
|
||||||
private const string MINI_GAME_GATEWAY_PID_FILE = "pc-minigame-gateway.pid";
|
private const string MINI_GAME_GATEWAY_PID_FILE = "pc-minigame-gateway.pid";
|
||||||
|
private const string MINI_GAME_CDN_RUNNER_PROJECT = "../../Server/Cdn.Runner/Cdn.Runner.csproj";
|
||||||
private const string MINI_GAME_CDN_RUNNER_DLL = "../../Server/Cdn.Runner/bin/Debug/net10.0/XWorld.Server.Cdn.Runner.dll";
|
private const string MINI_GAME_CDN_RUNNER_DLL = "../../Server/Cdn.Runner/bin/Debug/net10.0/XWorld.Server.Cdn.Runner.dll";
|
||||||
private const string MINI_GAME_CDN_ROOT = "../../CDN";
|
private const string MINI_GAME_CDN_ROOT = "../../CDN";
|
||||||
private const string MINI_GAME_CDN_PID_FILE = "pc-cdn.pid";
|
private const string MINI_GAME_CDN_PID_FILE = "pc-cdn.pid";
|
||||||
@@ -37,6 +39,7 @@ public class XWorldUtil
|
|||||||
private const int LOCAL_INTERNAL_GAME_PORT = 7003;
|
private const int LOCAL_INTERNAL_GAME_PORT = 7003;
|
||||||
private const int MINI_GAME_GATEWAY_PORT = 5005;
|
private const int MINI_GAME_GATEWAY_PORT = 5005;
|
||||||
private const int MINI_GAME_TICK_MS = 100;
|
private const int MINI_GAME_TICK_MS = 100;
|
||||||
|
private const int MINI_GAME_RECONNECT_WINDOW_TICKS = 1800;
|
||||||
private const int MINI_GAME_MATCH_TIMEOUT_TICKS = 150;
|
private const int MINI_GAME_MATCH_TIMEOUT_TICKS = 150;
|
||||||
|
|
||||||
private static readonly List<Process> LocalServerProcesses = new List<Process>();
|
private static readonly List<Process> LocalServerProcesses = new List<Process>();
|
||||||
@@ -323,6 +326,22 @@ public class XWorldUtil
|
|||||||
{
|
{
|
||||||
StopExitedLocalServerProcesses();
|
StopExitedLocalServerProcesses();
|
||||||
SetLocalServerPrefs();
|
SetLocalServerPrefs();
|
||||||
|
|
||||||
|
if (IsVisibleMiniGameGatewayConsoleRunning())
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.Log("PC MiniGame Gateway console is already running; skipping build.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ShouldBuildMiniGameLocalServer(IsMiniGameGatewayRunnerRunning()))
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.Log("PC MiniGame Gateway is already running; skipping build.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BuildMiniGameLocalServer())
|
||||||
|
return;
|
||||||
|
|
||||||
// 内网 CDN(HTTP 静态文件)独立于网关,先确保起来(端口已开则跳过)
|
// 内网 CDN(HTTP 静态文件)独立于网关,先确保起来(端口已开则跳过)
|
||||||
StartMiniGameCdnRunnerProcess("XWorld PC CDN");
|
StartMiniGameCdnRunnerProcess("XWorld PC CDN");
|
||||||
if (IsVisibleMiniGameGatewayConsoleRunning())
|
if (IsVisibleMiniGameGatewayConsoleRunning())
|
||||||
@@ -390,12 +409,26 @@ public class XWorldUtil
|
|||||||
"OK");
|
"OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MenuItem("XWorld/Server/Build Local Server", false, 605)]
|
||||||
|
static public void BuildLocalServer()
|
||||||
|
{
|
||||||
|
if (BuildMiniGameLocalServer())
|
||||||
|
EditorUtility.DisplayDialog("XWorld Server", "Local server build finished.", "OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("XWorld/Server/Build CDN Runner", false, 606)]
|
||||||
|
static public void BuildCdnRunner()
|
||||||
|
{
|
||||||
|
if (BuildMiniGameRunnerProject("XWorld PC CDN", GetMiniGameCdnRunnerProjectPath(), GetMiniGameCdnRunnerPath()))
|
||||||
|
EditorUtility.DisplayDialog("XWorld Server", "CDN runner build finished.", "OK");
|
||||||
|
}
|
||||||
|
|
||||||
static public bool IsPcMiniGameGatewayReady()
|
static public bool IsPcMiniGameGatewayReady()
|
||||||
{
|
{
|
||||||
return IsMiniGameGatewayRunnerRunning();
|
return IsMiniGameGatewayRunnerRunning();
|
||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("XWorld/Server/Open Latest Log", false, 605)]
|
[MenuItem("XWorld/Server/Open Latest Log", false, 607)]
|
||||||
static public void OpenLatestLocalServerLog()
|
static public void OpenLatestLocalServerLog()
|
||||||
{
|
{
|
||||||
string logDir = Path.GetFullPath(Path.Combine(Application.dataPath, LOCAL_SERVER_LOG_DIR));
|
string logDir = Path.GetFullPath(Path.Combine(Application.dataPath, LOCAL_SERVER_LOG_DIR));
|
||||||
@@ -422,7 +455,7 @@ public class XWorldUtil
|
|||||||
[MenuItem("XWorld/Server/Open Local Server (Separate)", true)]
|
[MenuItem("XWorld/Server/Open Local Server (Separate)", true)]
|
||||||
static public bool CanOpenLocalServer()
|
static public bool CanOpenLocalServer()
|
||||||
{
|
{
|
||||||
return File.Exists(GetMiniGameGatewayRunnerPath());
|
return File.Exists(GetMiniGameGatewayRunnerProjectPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
[MenuItem("XWorld/Server/Stop Local Server", true)]
|
[MenuItem("XWorld/Server/Stop Local Server", true)]
|
||||||
@@ -484,6 +517,11 @@ public class XWorldUtil
|
|||||||
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_GATEWAY_RUNNER_DLL));
|
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_GATEWAY_RUNNER_DLL));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static private string GetMiniGameGatewayRunnerProjectPath()
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_GATEWAY_RUNNER_PROJECT));
|
||||||
|
}
|
||||||
|
|
||||||
static private string GetMiniGameServerGamesRoot()
|
static private string GetMiniGameServerGamesRoot()
|
||||||
{
|
{
|
||||||
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_SERVER_GAMES_ROOT));
|
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_SERVER_GAMES_ROOT));
|
||||||
@@ -501,6 +539,11 @@ public class XWorldUtil
|
|||||||
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_CDN_RUNNER_DLL));
|
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_CDN_RUNNER_DLL));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static private string GetMiniGameCdnRunnerProjectPath()
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_CDN_RUNNER_PROJECT));
|
||||||
|
}
|
||||||
|
|
||||||
static private string GetMiniGameCdnRoot()
|
static private string GetMiniGameCdnRoot()
|
||||||
{
|
{
|
||||||
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_CDN_ROOT));
|
return Path.GetFullPath(Path.Combine(Application.dataPath, MINI_GAME_CDN_ROOT));
|
||||||
@@ -523,6 +566,101 @@ public class XWorldUtil
|
|||||||
return Path.Combine(logDir, safeTitle + "-" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".log");
|
return Path.Combine(logDir, safeTitle + "-" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".log");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static private bool BuildMiniGameLocalServer()
|
||||||
|
{
|
||||||
|
if (!BuildMiniGameRunnerProject("XWorld PC minigame gateway", GetMiniGameGatewayRunnerProjectPath(), GetMiniGameGatewayRunnerPath()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static private bool ShouldBuildMiniGameLocalServer(bool isGatewayRunning)
|
||||||
|
{
|
||||||
|
return !isGatewayRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
static private bool BuildMiniGameRunnerProject(string title, string projectPath, string runnerPath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(projectPath))
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("XWorld Server", "Server project not found:\n" + projectPath, "OK");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string logPath = GetLocalServerLogPath(title + " build");
|
||||||
|
UnityEngine.Debug.Log("Building " + title + " from " + projectPath + "\nLog: " + logPath);
|
||||||
|
EditorUtility.DisplayProgressBar("XWorld Server", "Building " + title, 0.5f);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
ProcessStartInfo startInfo = new ProcessStartInfo();
|
||||||
|
startInfo.FileName = "dotnet";
|
||||||
|
startInfo.Arguments = "build " + QuoteProcessArg(projectPath) + " -c Debug --no-incremental -m:1";
|
||||||
|
startInfo.WorkingDirectory = GetProjectRoot();
|
||||||
|
startInfo.UseShellExecute = false;
|
||||||
|
startInfo.RedirectStandardOutput = true;
|
||||||
|
startInfo.RedirectStandardError = true;
|
||||||
|
startInfo.CreateNoWindow = true;
|
||||||
|
|
||||||
|
using (Process process = new Process())
|
||||||
|
{
|
||||||
|
process.StartInfo = startInfo;
|
||||||
|
process.OutputDataReceived += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.Data != null)
|
||||||
|
output.AppendLine(args.Data);
|
||||||
|
};
|
||||||
|
process.ErrorDataReceived += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.Data != null)
|
||||||
|
output.AppendLine(args.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
process.WaitForExit();
|
||||||
|
process.WaitForExit(1000);
|
||||||
|
|
||||||
|
File.WriteAllText(logPath, output.ToString(), new UTF8Encoding(false));
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("XWorld Server",
|
||||||
|
title + " build failed. Old DLL will not be started.\n\nLog:\n" + logPath,
|
||||||
|
"OK");
|
||||||
|
UnityEngine.Debug.LogError(title + " build failed with exit code " + process.ExitCode + ". Log: " + logPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
File.WriteAllText(logPath, ex.ToString(), new UTF8Encoding(false));
|
||||||
|
EditorUtility.DisplayDialog("XWorld Server",
|
||||||
|
title + " build failed before launch. Old DLL will not be started.\n\n" + ex.Message + "\n\nLog:\n" + logPath,
|
||||||
|
"OK");
|
||||||
|
UnityEngine.Debug.LogError(title + " build failed: " + ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
EditorUtility.ClearProgressBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsDotnetRunnerLaunchable(runnerPath))
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("XWorld Server",
|
||||||
|
title + " build finished, but runner output is incomplete:\n" + runnerPath +
|
||||||
|
"\n" + GetDotnetRunnerRuntimeConfigPath(runnerPath),
|
||||||
|
"OK");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnityEngine.Debug.Log(title + " build finished. Runner: " + runnerPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static private void StartServerProcess(string arguments, string title)
|
static private void StartServerProcess(string arguments, string title)
|
||||||
{
|
{
|
||||||
string exePath = GetLocalServerExePath();
|
string exePath = GetLocalServerExePath();
|
||||||
@@ -573,10 +711,12 @@ public class XWorldUtil
|
|||||||
static private void StartMiniGameGatewayRunnerProcess(string title)
|
static private void StartMiniGameGatewayRunnerProcess(string title)
|
||||||
{
|
{
|
||||||
string runnerPath = GetMiniGameGatewayRunnerPath();
|
string runnerPath = GetMiniGameGatewayRunnerPath();
|
||||||
if (!File.Exists(runnerPath))
|
if (!IsDotnetRunnerLaunchable(runnerPath))
|
||||||
{
|
{
|
||||||
EditorUtility.DisplayDialog("XWorld Server",
|
EditorUtility.DisplayDialog("XWorld Server",
|
||||||
"Gateway runner not found:\n" + runnerPath + "\n\nBuild Server/Gateway.Runner first.", "OK");
|
"Gateway runner output is incomplete:\n" + runnerPath +
|
||||||
|
"\n" + GetDotnetRunnerRuntimeConfigPath(runnerPath) +
|
||||||
|
"\n\nBuild Server/Gateway.Runner first.", "OK");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,6 +752,7 @@ public class XWorldUtil
|
|||||||
"Write-Host ''\r\n" +
|
"Write-Host ''\r\n" +
|
||||||
"& dotnet $runner --gamesRoot $gamesRoot --port " + MINI_GAME_GATEWAY_PORT +
|
"& dotnet $runner --gamesRoot $gamesRoot --port " + MINI_GAME_GATEWAY_PORT +
|
||||||
" --tickMs " + MINI_GAME_TICK_MS +
|
" --tickMs " + MINI_GAME_TICK_MS +
|
||||||
|
" --reconnectWindowTicks " + MINI_GAME_RECONNECT_WINDOW_TICKS +
|
||||||
" --matchTimeoutTicks " + MINI_GAME_MATCH_TIMEOUT_TICKS +
|
" --matchTimeoutTicks " + MINI_GAME_MATCH_TIMEOUT_TICKS +
|
||||||
" --lan --devToken " + DEV_DISCOVERY_TOKEN + // 开 DevDiscovery(UDP48923) 响应 + 广播 LAN IP;CDN 地址由 --lan 兜底为 http://<lanip>:15081/
|
" --lan --devToken " + DEV_DISCOVERY_TOKEN + // 开 DevDiscovery(UDP48923) 响应 + 广播 LAN IP;CDN 地址由 --lan 兜底为 http://<lanip>:15081/
|
||||||
" *>&1 | Tee-Object -FilePath $logPath\r\n" +
|
" *>&1 | Tee-Object -FilePath $logPath\r\n" +
|
||||||
@@ -645,9 +786,10 @@ public class XWorldUtil
|
|||||||
static private void StartMiniGameCdnRunnerProcess(string title)
|
static private void StartMiniGameCdnRunnerProcess(string title)
|
||||||
{
|
{
|
||||||
string runnerPath = GetMiniGameCdnRunnerPath();
|
string runnerPath = GetMiniGameCdnRunnerPath();
|
||||||
if (!File.Exists(runnerPath))
|
if (!IsDotnetRunnerLaunchable(runnerPath))
|
||||||
{
|
{
|
||||||
UnityEngine.Debug.LogWarning("[CDN] 未找到 Cdn.Runner:" + runnerPath + "\n请先构建 Server/Cdn.Runner(dotnet build Server/Server.sln)。");
|
UnityEngine.Debug.LogWarning("[CDN] Cdn.Runner output is incomplete; skipping CDN startup.\nDLL: " +
|
||||||
|
runnerPath + "\nRuntimeConfig: " + GetDotnetRunnerRuntimeConfigPath(runnerPath));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,6 +864,18 @@ public class XWorldUtil
|
|||||||
return (text ?? string.Empty).Replace("'", "''");
|
return (text ?? string.Empty).Replace("'", "''");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static private string GetDotnetRunnerRuntimeConfigPath(string runnerPath)
|
||||||
|
{
|
||||||
|
string directory = Path.GetDirectoryName(runnerPath);
|
||||||
|
string fileName = Path.GetFileNameWithoutExtension(runnerPath) + ".runtimeconfig.json";
|
||||||
|
return Path.Combine(directory ?? string.Empty, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static private bool IsDotnetRunnerLaunchable(string runnerPath)
|
||||||
|
{
|
||||||
|
return File.Exists(runnerPath) && File.Exists(GetDotnetRunnerRuntimeConfigPath(runnerPath));
|
||||||
|
}
|
||||||
|
|
||||||
static private void StopExitedLocalServerProcesses()
|
static private void StopExitedLocalServerProcesses()
|
||||||
{
|
{
|
||||||
LocalServerProcesses.RemoveAll(process =>
|
LocalServerProcesses.RemoveAll(process =>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public sealed class MatchWaitingPrefabLayoutTests
|
||||||
|
{
|
||||||
|
private const string PrefabPath = "Assets/Game/Art/UI/Prefab/UI_MatchWaiting.prefab";
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WaitingPanel_IsCenteredInSafeArea()
|
||||||
|
{
|
||||||
|
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
|
||||||
|
Assert.That(prefab, Is.Not.Null);
|
||||||
|
|
||||||
|
RectTransform root = prefab.GetComponent<RectTransform>();
|
||||||
|
Assert.That(root.anchorMin, Is.EqualTo(Vector2.zero));
|
||||||
|
Assert.That(root.anchorMax, Is.EqualTo(Vector2.one));
|
||||||
|
Assert.That(root.anchoredPosition, Is.EqualTo(Vector2.zero));
|
||||||
|
Assert.That(root.sizeDelta, Is.EqualTo(Vector2.zero));
|
||||||
|
|
||||||
|
Transform panel = prefab.transform.Find("SafeArea/Panel_Waiting");
|
||||||
|
Assert.That(panel, Is.Not.Null);
|
||||||
|
|
||||||
|
RectTransform rect = panel.GetComponent<RectTransform>();
|
||||||
|
Assert.That(rect.anchorMin, Is.EqualTo(new Vector2(0.5f, 0.5f)));
|
||||||
|
Assert.That(rect.anchorMax, Is.EqualTo(new Vector2(0.5f, 0.5f)));
|
||||||
|
Assert.That(rect.pivot, Is.EqualTo(new Vector2(0.5f, 0.5f)));
|
||||||
|
Assert.That(rect.anchoredPosition, Is.EqualTo(Vector2.zero));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7b43c5afbe3e421488be033e25c0c28a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Reflection;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using XGame;
|
||||||
using XGame.MiniGame;
|
using XGame.MiniGame;
|
||||||
|
|
||||||
public sealed class MiniGameStageIsolationTests
|
public sealed class MiniGameStageIsolationTests
|
||||||
@@ -96,6 +98,51 @@ public sealed class MiniGameStageIsolationTests
|
|||||||
Assert.IsTrue(behaviour.enabled);
|
Assert.IsTrue(behaviour.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SuspendAndResume_AutoCapturesCharacterSwitchUiRoot()
|
||||||
|
{
|
||||||
|
_root = new GameObject("root");
|
||||||
|
GameObject characterObject = new GameObject("character-switch");
|
||||||
|
characterObject.transform.SetParent(_root.transform);
|
||||||
|
GameObject characterUi = new GameObject("UI_CharacterSwitch");
|
||||||
|
characterUi.transform.SetParent(_root.transform);
|
||||||
|
var controller = characterObject.AddComponent<CharacterSwitchController>();
|
||||||
|
typeof(CharacterSwitchController)
|
||||||
|
.GetField("uiPanel", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||||
|
.SetValue(controller, characterUi);
|
||||||
|
Assert.AreSame(characterUi, controller.StageIsolationUiRoot);
|
||||||
|
var isolation = _root.AddComponent<MiniGameStageIsolation>();
|
||||||
|
|
||||||
|
isolation.SuspendLobbyForMiniGame();
|
||||||
|
|
||||||
|
Assert.IsFalse(characterUi.activeSelf);
|
||||||
|
|
||||||
|
isolation.ResumeLobbyAfterMiniGame();
|
||||||
|
|
||||||
|
Assert.IsTrue(characterUi.activeSelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CharacterSwitchUiLoadedWhileSuspended_IsHidden()
|
||||||
|
{
|
||||||
|
_root = new GameObject("root");
|
||||||
|
var isolation = _root.AddComponent<MiniGameStageIsolation>();
|
||||||
|
isolation.SuspendLobbyForMiniGame();
|
||||||
|
|
||||||
|
GameObject characterObject = new GameObject("character-switch");
|
||||||
|
characterObject.transform.SetParent(_root.transform);
|
||||||
|
GameObject characterUi = new GameObject("UI_CharacterSwitch");
|
||||||
|
characterUi.transform.SetParent(_root.transform);
|
||||||
|
var controller = characterObject.AddComponent<CharacterSwitchController>();
|
||||||
|
typeof(CharacterSwitchController)
|
||||||
|
.GetField("uiPanel", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||||
|
.SetValue(controller, characterUi);
|
||||||
|
|
||||||
|
controller.ApplyStageIsolationState();
|
||||||
|
|
||||||
|
Assert.IsFalse(characterUi.activeSelf);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void SuspendAndResume_IgnoreNullEntries()
|
public void SuspendAndResume_IgnoreNullEntries()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
@@ -33,3 +34,84 @@ public sealed class XWorldJsonPrefabAutoConvertTests
|
|||||||
return (bool)method.Invoke(null, null);
|
return (bool)method.Invoke(null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class XWorldLocalServerBuildTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void LocalServerMenuUsesGatewayProjectInsteadOfPrebuiltDll()
|
||||||
|
{
|
||||||
|
string gatewayProjectPath = InvokeStringPath("GetMiniGameGatewayRunnerProjectPath");
|
||||||
|
|
||||||
|
Assert.That(gatewayProjectPath.Replace('\\', '/'), Does.EndWith("/Server/Gateway.Runner/Gateway.Runner.csproj"));
|
||||||
|
Assert.That(File.Exists(gatewayProjectPath), Is.True);
|
||||||
|
Assert.That(XWorldUtil.CanOpenLocalServer(), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CdnRunnerBuildMenuUsesCdnProject()
|
||||||
|
{
|
||||||
|
string cdnProjectPath = InvokeStringPath("GetMiniGameCdnRunnerProjectPath");
|
||||||
|
|
||||||
|
Assert.That(cdnProjectPath.Replace('\\', '/'), Does.EndWith("/Server/Cdn.Runner/Cdn.Runner.csproj"));
|
||||||
|
Assert.That(File.Exists(cdnProjectPath), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LocalServerBuildIsSkippedWhenGatewayIsAlreadyRunning()
|
||||||
|
{
|
||||||
|
Assert.That(InvokeShouldBuildLocalServer(false), Is.True);
|
||||||
|
Assert.That(InvokeShouldBuildLocalServer(true), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DotnetRunnerLaunchableRequiresRuntimeConfigNextToDll()
|
||||||
|
{
|
||||||
|
string tempDir = Path.Combine(Path.GetTempPath(), "XWorldRunnerTest-" + System.Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string runnerPath = Path.Combine(tempDir, "Example.Runner.dll");
|
||||||
|
File.WriteAllText(runnerPath, string.Empty);
|
||||||
|
|
||||||
|
Assert.That(InvokeLaunchable(runnerPath), Is.False);
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(tempDir, "Example.Runner.runtimeconfig.json"), "{}");
|
||||||
|
|
||||||
|
Assert.That(InvokeLaunchable(runnerPath), Is.True);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string InvokeStringPath(string methodName)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(XWorldUtil).GetMethod(
|
||||||
|
methodName,
|
||||||
|
BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.That(method, Is.Not.Null);
|
||||||
|
return (string)method.Invoke(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InvokeLaunchable(string runnerPath)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(XWorldUtil).GetMethod(
|
||||||
|
"IsDotnetRunnerLaunchable",
|
||||||
|
BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.That(method, Is.Not.Null);
|
||||||
|
return (bool)method.Invoke(null, new object[] { runnerPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InvokeShouldBuildLocalServer(bool isGatewayRunning)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(XWorldUtil).GetMethod(
|
||||||
|
"ShouldBuildMiniGameLocalServer",
|
||||||
|
BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.That(method, Is.Not.Null);
|
||||||
|
return (bool)method.Invoke(null, new object[] { isGatewayRunning });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using TMPro;
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
using XGame.MiniGame;
|
||||||
using UObject = UnityEngine.Object;
|
using UObject = UnityEngine.Object;
|
||||||
|
|
||||||
namespace XGame
|
namespace XGame
|
||||||
@@ -35,6 +36,11 @@ namespace XGame
|
|||||||
get { return currentConfig == null ? string.Empty : currentConfig.id; }
|
get { return currentConfig == null ? string.Empty : currentConfig.id; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GameObject StageIsolationUiRoot
|
||||||
|
{
|
||||||
|
get { return uiPanel; }
|
||||||
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
LoadConfig();
|
LoadConfig();
|
||||||
@@ -178,6 +184,15 @@ namespace XGame
|
|||||||
|
|
||||||
CacheUI();
|
CacheUI();
|
||||||
BuildCharacterButtons();
|
BuildCharacterButtons();
|
||||||
|
ApplyStageIsolationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyStageIsolationState()
|
||||||
|
{
|
||||||
|
if (uiPanel != null && MiniGameStageIsolation.HasSuspendedLobby())
|
||||||
|
{
|
||||||
|
uiPanel.SetActive(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CacheUI()
|
private void CacheUI()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using XGame.MiniGame;
|
using XGame.MiniGame;
|
||||||
|
using XWorld.Framework;
|
||||||
using XWorld.Framework.Protocol;
|
using XWorld.Framework.Protocol;
|
||||||
using UObject = UnityEngine.Object;
|
using UObject = UnityEngine.Object;
|
||||||
|
|
||||||
@@ -37,13 +38,19 @@ namespace XGame
|
|||||||
private XWebSocket socket;
|
private XWebSocket socket;
|
||||||
private MiniGameNetChannel lobbyNet;
|
private MiniGameNetChannel lobbyNet;
|
||||||
private MiniGameHost gameHost;
|
private MiniGameHost gameHost;
|
||||||
|
private MiniGameStageIsolation stageIsolation;
|
||||||
private LobbyWorldController lobbyWorld;
|
private LobbyWorldController lobbyWorld;
|
||||||
private WorldChatModule worldChat;
|
private WorldChatModule worldChat;
|
||||||
private readonly List<GameInfoMsg> games = new List<GameInfoMsg>();
|
private readonly List<GameInfoMsg> games = new List<GameInfoMsg>();
|
||||||
|
private readonly List<NetMessage> pendingGameMessages = new List<NetMessage>();
|
||||||
|
private readonly HashSet<int> canceledMatchRequestIds = new HashSet<int>();
|
||||||
private bool lobbyActive;
|
private bool lobbyActive;
|
||||||
private bool openingLogin;
|
private bool openingLogin;
|
||||||
private bool openingLobby;
|
private bool openingLobby;
|
||||||
private bool openingWaiting;
|
private bool openingWaiting;
|
||||||
|
private bool matchRequestActive;
|
||||||
|
private int nextMatchRequestId;
|
||||||
|
private int currentMatchRequestId;
|
||||||
private string matchMessage = string.Empty;
|
private string matchMessage = string.Empty;
|
||||||
private string pendingGameId = string.Empty;
|
private string pendingGameId = string.Empty;
|
||||||
private int pendingGameVersion;
|
private int pendingGameVersion;
|
||||||
@@ -409,6 +416,7 @@ namespace XGame
|
|||||||
|
|
||||||
socket = sock;
|
socket = sock;
|
||||||
lobbyNet = new MiniGameNetChannel(sock);
|
lobbyNet = new MiniGameNetChannel(sock);
|
||||||
|
lobbyNet.OnGameMessage = OnLobbyGameMessage;
|
||||||
lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame;
|
lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame;
|
||||||
lobbyActive = true;
|
lobbyActive = true;
|
||||||
SetLobbyStatus("Lobby connected.");
|
SetLobbyStatus("Lobby connected.");
|
||||||
@@ -455,6 +463,9 @@ namespace XGame
|
|||||||
|
|
||||||
private void OnMatchClicked(string gameId)
|
private void OnMatchClicked(string gameId)
|
||||||
{
|
{
|
||||||
|
matchRequestActive = true;
|
||||||
|
currentMatchRequestId = ++nextMatchRequestId;
|
||||||
|
canceledMatchRequestIds.Remove(currentMatchRequestId);
|
||||||
matchMessage = "Matching...";
|
matchMessage = "Matching...";
|
||||||
pendingGameId = string.Empty;
|
pendingGameId = string.Empty;
|
||||||
pendingGameVersion = 0;
|
pendingGameVersion = 0;
|
||||||
@@ -468,7 +479,7 @@ namespace XGame
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(gameId))
|
if (string.IsNullOrEmpty(gameId))
|
||||||
{
|
{
|
||||||
lobbyNet.SendFramework(FrameworkOpcode.RandomMatchRequest, Array.Empty<byte>());
|
lobbyNet.SendFramework(FrameworkOpcode.RandomMatchRequest, new MatchControlMsg { RequestId = currentMatchRequestId }.Encode());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -483,7 +494,7 @@ namespace XGame
|
|||||||
}
|
}
|
||||||
pendingGameId = gameId;
|
pendingGameId = gameId;
|
||||||
pendingGameVersion = version;
|
pendingGameVersion = version;
|
||||||
lobbyNet.SendFramework(FrameworkOpcode.MatchRequest, new MatchRequestMsg { GameId = gameId, Version = version }.Encode());
|
lobbyNet.SendFramework(FrameworkOpcode.MatchRequest, new MatchRequestMsg { GameId = gameId, Version = version, RequestId = currentMatchRequestId }.Encode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,6 +541,8 @@ namespace XGame
|
|||||||
|
|
||||||
private void OnCloseWaiting()
|
private void OnCloseWaiting()
|
||||||
{
|
{
|
||||||
|
CancelCurrentMatchRequest();
|
||||||
|
matchRequestActive = false;
|
||||||
matchMessage = string.Empty;
|
matchMessage = string.Empty;
|
||||||
pendingGameId = string.Empty;
|
pendingGameId = string.Empty;
|
||||||
pendingGameVersion = 0;
|
pendingGameVersion = 0;
|
||||||
@@ -537,6 +550,34 @@ namespace XGame
|
|||||||
OpenLobby();
|
OpenLobby();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CancelPendingMatch()
|
||||||
|
=> CancelPendingMatch(currentMatchRequestId);
|
||||||
|
|
||||||
|
private void CancelCurrentMatchRequest()
|
||||||
|
{
|
||||||
|
if (currentMatchRequestId != 0)
|
||||||
|
{
|
||||||
|
canceledMatchRequestIds.Add(currentMatchRequestId);
|
||||||
|
}
|
||||||
|
CancelPendingMatch(currentMatchRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelPendingMatch(int requestId)
|
||||||
|
{
|
||||||
|
if (lobbyNet == null || !lobbyNet.IsConnected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lobbyNet.SendFramework(FrameworkOpcode.MatchCancel, new MatchControlMsg { RequestId = requestId }.Encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCurrentMatchResponse(int requestId)
|
||||||
|
=> matchRequestActive && (requestId == 0 || requestId == currentMatchRequestId);
|
||||||
|
|
||||||
|
private bool IsCanceledMatchResponse(int requestId)
|
||||||
|
=> requestId != 0 && canceledMatchRequestIds.Contains(requestId);
|
||||||
|
|
||||||
private void OnLobbyFrameworkFrame(Frame frame)
|
private void OnLobbyFrameworkFrame(Frame frame)
|
||||||
{
|
{
|
||||||
switch ((FrameworkOpcode)frame.Opcode)
|
switch ((FrameworkOpcode)frame.Opcode)
|
||||||
@@ -549,6 +590,16 @@ namespace XGame
|
|||||||
break;
|
break;
|
||||||
case FrameworkOpcode.MatchAssigned:
|
case FrameworkOpcode.MatchAssigned:
|
||||||
MatchAssignedMsg assigned = MatchAssignedMsg.Decode(frame.Payload);
|
MatchAssignedMsg assigned = MatchAssignedMsg.Decode(frame.Payload);
|
||||||
|
if (IsCanceledMatchResponse(assigned.RequestId))
|
||||||
|
{
|
||||||
|
Debug.Log("[CSharpClientApp] ignore canceled match assigned req=" + assigned.RequestId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!IsCurrentMatchResponse(assigned.RequestId))
|
||||||
|
{
|
||||||
|
Debug.Log("[CSharpClientApp] ignore stale match assigned req=" + assigned.RequestId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
pendingGameId = assigned.GameId;
|
pendingGameId = assigned.GameId;
|
||||||
pendingGameVersion = assigned.Version;
|
pendingGameVersion = assigned.Version;
|
||||||
Debug.Log("[CSharpClientApp] match assigned " + pendingGameId + "@" + pendingGameVersion);
|
Debug.Log("[CSharpClientApp] match assigned " + pendingGameId + "@" + pendingGameVersion);
|
||||||
@@ -556,10 +607,31 @@ namespace XGame
|
|||||||
break;
|
break;
|
||||||
case FrameworkOpcode.MatchFound:
|
case FrameworkOpcode.MatchFound:
|
||||||
MatchFoundMsg found = MatchFoundMsg.Decode(frame.Payload);
|
MatchFoundMsg found = MatchFoundMsg.Decode(frame.Payload);
|
||||||
|
if (IsCanceledMatchResponse(found.RequestId))
|
||||||
|
{
|
||||||
|
Debug.Log("[CSharpClientApp] ignore canceled match found req=" + found.RequestId);
|
||||||
|
CancelPendingMatch(found.RequestId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!matchRequestActive && !string.IsNullOrEmpty(found.GameId))
|
||||||
|
{
|
||||||
|
Debug.Log("[CSharpClientApp] resume game room=" + found.RoomId + " game=" + found.GameId + "@" + found.Version);
|
||||||
|
EnterAssignedGame(found.GameId, found.Version, found);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!IsCurrentMatchResponse(found.RequestId))
|
||||||
|
{
|
||||||
|
Debug.Log("[CSharpClientApp] ignore stale match found req=" + found.RequestId);
|
||||||
|
if (found.RequestId != 0)
|
||||||
|
{
|
||||||
|
CancelPendingMatch(found.RequestId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
Debug.Log("[CSharpClientApp] match found room=" + found.RoomId + " self=" + found.SelfPlayerId);
|
Debug.Log("[CSharpClientApp] match found room=" + found.RoomId + " self=" + found.SelfPlayerId);
|
||||||
if (string.IsNullOrEmpty(pendingGameId))
|
if (string.IsNullOrEmpty(pendingGameId))
|
||||||
{
|
{
|
||||||
SetWaitingStatus("Room ready, but no game was assigned.", true);
|
AbortMatchAndReturnLobby("Match expired, please retry.", found.RequestId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
SetWaitingStatus("Room ready.", false);
|
SetWaitingStatus("Room ready.", false);
|
||||||
@@ -567,6 +639,16 @@ namespace XGame
|
|||||||
break;
|
break;
|
||||||
case FrameworkOpcode.Error:
|
case FrameworkOpcode.Error:
|
||||||
ErrorMsg err = ErrorMsg.Decode(frame.Payload);
|
ErrorMsg err = ErrorMsg.Decode(frame.Payload);
|
||||||
|
if (IsCanceledMatchResponse(err.RequestId))
|
||||||
|
{
|
||||||
|
Debug.Log("[CSharpClientApp] ignore canceled match error req=" + err.RequestId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!IsCurrentMatchResponse(err.RequestId))
|
||||||
|
{
|
||||||
|
Debug.Log("[CSharpClientApp] ignore stale match error req=" + err.RequestId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
pendingGameId = string.Empty;
|
pendingGameId = string.Empty;
|
||||||
pendingGameVersion = 0;
|
pendingGameVersion = 0;
|
||||||
SetWaitingStatus("Server error " + err.Code + ": " + err.Message, true);
|
SetWaitingStatus("Server error " + err.Code + ": " + err.Message, true);
|
||||||
@@ -590,11 +672,28 @@ namespace XGame
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnLobbyGameMessage(NetMessage message)
|
||||||
|
{
|
||||||
|
if (gameHost != null)
|
||||||
|
{
|
||||||
|
gameHost.QueueInitialMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingGameMessages.Add(message);
|
||||||
|
if (pendingGameMessages.Count > 16)
|
||||||
|
{
|
||||||
|
pendingGameMessages.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void EnterAssignedGame(string gameId, int version, MatchFoundMsg found)
|
private void EnterAssignedGame(string gameId, int version, MatchFoundMsg found)
|
||||||
{
|
{
|
||||||
|
matchRequestActive = false;
|
||||||
pendingGameId = string.Empty;
|
pendingGameId = string.Empty;
|
||||||
pendingGameVersion = 0;
|
pendingGameVersion = 0;
|
||||||
lobbyActive = false;
|
lobbyActive = false;
|
||||||
|
SuspendLobbyStage();
|
||||||
lobbyWorld?.SendLeave();
|
lobbyWorld?.SendLeave();
|
||||||
CloseWorldChat();
|
CloseWorldChat();
|
||||||
lobbyNet = null;
|
lobbyNet = null;
|
||||||
@@ -607,17 +706,73 @@ namespace XGame
|
|||||||
}
|
}
|
||||||
gameHost.OnGameExited = () =>
|
gameHost.OnGameExited = () =>
|
||||||
{
|
{
|
||||||
|
ResumeLobbyStage();
|
||||||
matchMessage = "Returned to lobby.";
|
matchMessage = "Returned to lobby.";
|
||||||
lobbyNet = new MiniGameNetChannel(socket);
|
lobbyNet = new MiniGameNetChannel(socket);
|
||||||
|
lobbyNet.OnGameMessage = OnLobbyGameMessage;
|
||||||
lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame;
|
lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame;
|
||||||
lobbyActive = true;
|
lobbyActive = true;
|
||||||
OpenLobby();
|
OpenLobby();
|
||||||
JoinLobbyWorld();
|
JoinLobbyWorld();
|
||||||
RequestGameList();
|
RequestGameList();
|
||||||
};
|
};
|
||||||
gameHost.EnterGame(ResolvedCdnBase(), gameId, version, socket, false);
|
|
||||||
// 大厅链路 MatchFound 由大厅通道消费,宿主收不到,须显式注入房间信息(结算弹框判胜负用)
|
// 大厅链路 MatchFound 由大厅通道消费,宿主收不到,须显式注入房间信息(结算弹框判胜负用)
|
||||||
gameHost.SetMatchInfo(found.RoomId, found.SelfPlayerId);
|
gameHost.SetMatchInfo(found.RoomId, found.SelfPlayerId, found.Players);
|
||||||
|
gameHost.EnterGame(ResolvedCdnBase(), gameId, version, socket, false);
|
||||||
|
for (int i = 0; i < pendingGameMessages.Count; i++)
|
||||||
|
{
|
||||||
|
gameHost.QueueInitialMessage(pendingGameMessages[i]);
|
||||||
|
}
|
||||||
|
pendingGameMessages.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AbortMatchAndReturnLobby(string message, int requestId)
|
||||||
|
{
|
||||||
|
int cancelRequestId = requestId != 0 ? requestId : currentMatchRequestId;
|
||||||
|
if (cancelRequestId != 0)
|
||||||
|
{
|
||||||
|
canceledMatchRequestIds.Add(cancelRequestId);
|
||||||
|
}
|
||||||
|
CancelPendingMatch(cancelRequestId);
|
||||||
|
matchRequestActive = false;
|
||||||
|
pendingGameId = string.Empty;
|
||||||
|
pendingGameVersion = 0;
|
||||||
|
CloseWaiting();
|
||||||
|
OpenLobby();
|
||||||
|
SetLobbyStatus(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MiniGameStageIsolation ResolveStageIsolation()
|
||||||
|
{
|
||||||
|
if (stageIsolation != null)
|
||||||
|
{
|
||||||
|
return stageIsolation;
|
||||||
|
}
|
||||||
|
|
||||||
|
stageIsolation = GetComponent<MiniGameStageIsolation>();
|
||||||
|
if (stageIsolation == null)
|
||||||
|
{
|
||||||
|
stageIsolation = gameObject.AddComponent<MiniGameStageIsolation>();
|
||||||
|
}
|
||||||
|
return stageIsolation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SuspendLobbyStage()
|
||||||
|
{
|
||||||
|
MiniGameStageIsolation isolation = ResolveStageIsolation();
|
||||||
|
if (isolation != null)
|
||||||
|
{
|
||||||
|
isolation.SuspendLobbyForMiniGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResumeLobbyStage()
|
||||||
|
{
|
||||||
|
MiniGameStageIsolation isolation = ResolveStageIsolation();
|
||||||
|
if (isolation != null)
|
||||||
|
{
|
||||||
|
isolation.ResumeLobbyAfterMiniGame();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolvedCdnBase()
|
private string ResolvedCdnBase()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace XGame.MiniGame
|
|||||||
private GameClientCtx _ctx;
|
private GameClientCtx _ctx;
|
||||||
private MiniGameAssetLoader _assets;
|
private MiniGameAssetLoader _assets;
|
||||||
private RoomPlayerService _players;
|
private RoomPlayerService _players;
|
||||||
|
private readonly List<NetMessage> _queuedInitialMessages = new List<NetMessage>();
|
||||||
private bool _running;
|
private bool _running;
|
||||||
private bool _waitingForResultConfirm;
|
private bool _waitingForResultConfirm;
|
||||||
private string _gameId;
|
private string _gameId;
|
||||||
@@ -38,10 +39,22 @@ namespace XGame.MiniGame
|
|||||||
public void EnterGame(string cdnBase, string gameId, int version, XWebSocket sock, bool sendMatchRequest)
|
public void EnterGame(string cdnBase, string gameId, int version, XWebSocket sock, bool sendMatchRequest)
|
||||||
{
|
{
|
||||||
_gameId = gameId; _version = version;
|
_gameId = gameId; _version = version;
|
||||||
|
_queuedInitialMessages.Clear();
|
||||||
// FIX: XWCoroutine.StartCo is a static method (not Instance.StartCo)
|
// FIX: XWCoroutine.StartCo is a static method (not Instance.StartCo)
|
||||||
XWCoroutine.StartCo(CoEnter(cdnBase, gameId, version, sock, sendMatchRequest));
|
XWCoroutine.StartCo(CoEnter(cdnBase, gameId, version, sock, sendMatchRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void QueueInitialMessage(NetMessage message)
|
||||||
|
{
|
||||||
|
if (_client != null)
|
||||||
|
{
|
||||||
|
SafeCall(() => _client.OnNetMessage(message), "OnNetMessage");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_queuedInitialMessages.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
// 大厅链路:MatchFound 由大厅通道消费(sendMatchRequest=false 时宿主收不到),
|
// 大厅链路:MatchFound 由大厅通道消费(sendMatchRequest=false 时宿主收不到),
|
||||||
// 由外部把房间信息注入,否则结算弹框无法判断胜负(_selfPlayerId 恒 0)。
|
// 由外部把房间信息注入,否则结算弹框无法判断胜负(_selfPlayerId 恒 0)。
|
||||||
public void SetMatchInfo(string roomId, int selfPlayerId)
|
public void SetMatchInfo(string roomId, int selfPlayerId)
|
||||||
@@ -122,6 +135,7 @@ namespace XGame.MiniGame
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_client.OnEnter(_ctx);
|
_client.OnEnter(_ctx);
|
||||||
|
FlushQueuedInitialMessages();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -165,6 +179,7 @@ namespace XGame.MiniGame
|
|||||||
public void ExitGame()
|
public void ExitGame()
|
||||||
{
|
{
|
||||||
if (!_running && _client == null) return;
|
if (!_running && _client == null) return;
|
||||||
|
_queuedInitialMessages.Clear();
|
||||||
DestroyResultDialog();
|
DestroyResultDialog();
|
||||||
_waitingForResultConfirm = false;
|
_waitingForResultConfirm = false;
|
||||||
_running = false;
|
_running = false;
|
||||||
@@ -183,6 +198,7 @@ namespace XGame.MiniGame
|
|||||||
|
|
||||||
private void FailEnterAndExit(string reason)
|
private void FailEnterAndExit(string reason)
|
||||||
{
|
{
|
||||||
|
_queuedInitialMessages.Clear();
|
||||||
DestroyResultDialog();
|
DestroyResultDialog();
|
||||||
_waitingForResultConfirm = false;
|
_waitingForResultConfirm = false;
|
||||||
_running = false;
|
_running = false;
|
||||||
@@ -443,6 +459,18 @@ namespace XGame.MiniGame
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void FlushQueuedInitialMessages()
|
||||||
|
{
|
||||||
|
if (_client == null || _queuedInitialMessages.Count == 0) return;
|
||||||
|
NetMessage[] messages = _queuedInitialMessages.ToArray();
|
||||||
|
_queuedInitialMessages.Clear();
|
||||||
|
for (int i = 0; i < messages.Length; i++)
|
||||||
|
{
|
||||||
|
NetMessage message = messages[i];
|
||||||
|
SafeCall(() => _client.OnNetMessage(message), "OnNetMessage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void SafeCall(Action a, string where)
|
private static void SafeCall(Action a, string where)
|
||||||
{
|
{
|
||||||
try { a(); }
|
try { a(); }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using XGame;
|
||||||
|
|
||||||
namespace XGame.MiniGame
|
namespace XGame.MiniGame
|
||||||
{
|
{
|
||||||
@@ -17,6 +18,19 @@ namespace XGame.MiniGame
|
|||||||
|
|
||||||
public bool IsSuspended { get; private set; }
|
public bool IsSuspended { get; private set; }
|
||||||
|
|
||||||
|
public static bool HasSuspendedLobby()
|
||||||
|
{
|
||||||
|
MiniGameStageIsolation[] isolations = FindObjectsOfType<MiniGameStageIsolation>(true);
|
||||||
|
for (int i = 0; i < isolations.Length; i++)
|
||||||
|
{
|
||||||
|
if (isolations[i] != null && isolations[i].IsSuspended)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public void SuspendLobbyForMiniGame()
|
public void SuspendLobbyForMiniGame()
|
||||||
{
|
{
|
||||||
if (IsSuspended)
|
if (IsSuspended)
|
||||||
@@ -77,6 +91,7 @@ namespace XGame.MiniGame
|
|||||||
_behaviourStates.Clear();
|
_behaviourStates.Clear();
|
||||||
_canvasGroupStates.Clear();
|
_canvasGroupStates.Clear();
|
||||||
CaptureGameObjects(LobbyUiRoots);
|
CaptureGameObjects(LobbyUiRoots);
|
||||||
|
CaptureCharacterSwitchUiRoots();
|
||||||
CaptureGameObjects(LobbySceneRoots);
|
CaptureGameObjects(LobbySceneRoots);
|
||||||
CaptureBehaviours(LobbyInputBehaviours);
|
CaptureBehaviours(LobbyInputBehaviours);
|
||||||
CaptureBehaviours(LobbyTickBehaviours);
|
CaptureBehaviours(LobbyTickBehaviours);
|
||||||
@@ -86,6 +101,7 @@ namespace XGame.MiniGame
|
|||||||
private void ApplySuspendedState()
|
private void ApplySuspendedState()
|
||||||
{
|
{
|
||||||
SetGameObjectsActive(LobbyUiRoots, false);
|
SetGameObjectsActive(LobbyUiRoots, false);
|
||||||
|
SetCharacterSwitchUiRootsActive(false);
|
||||||
SetGameObjectsActive(LobbySceneRoots, false);
|
SetGameObjectsActive(LobbySceneRoots, false);
|
||||||
SetBehavioursEnabled(LobbyInputBehaviours, false);
|
SetBehavioursEnabled(LobbyInputBehaviours, false);
|
||||||
SetBehavioursEnabled(LobbyTickBehaviours, false);
|
SetBehavioursEnabled(LobbyTickBehaviours, false);
|
||||||
@@ -105,6 +121,19 @@ namespace XGame.MiniGame
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CaptureCharacterSwitchUiRoots()
|
||||||
|
{
|
||||||
|
CharacterSwitchController[] controllers = FindObjectsOfType<CharacterSwitchController>(true);
|
||||||
|
for (int i = 0; i < controllers.Length; i++)
|
||||||
|
{
|
||||||
|
GameObject target = controllers[i] != null ? controllers[i].StageIsolationUiRoot : null;
|
||||||
|
if (target != null)
|
||||||
|
{
|
||||||
|
_gameObjectStates.Add(new GameObjectState(target, target.activeSelf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CaptureBehaviours(Behaviour[] targets)
|
private void CaptureBehaviours(Behaviour[] targets)
|
||||||
{
|
{
|
||||||
if (targets == null) return;
|
if (targets == null) return;
|
||||||
@@ -143,6 +172,19 @@ namespace XGame.MiniGame
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SetCharacterSwitchUiRootsActive(bool active)
|
||||||
|
{
|
||||||
|
CharacterSwitchController[] controllers = FindObjectsOfType<CharacterSwitchController>(true);
|
||||||
|
for (int i = 0; i < controllers.Length; i++)
|
||||||
|
{
|
||||||
|
GameObject target = controllers[i] != null ? controllers[i].StageIsolationUiRoot : null;
|
||||||
|
if (target != null)
|
||||||
|
{
|
||||||
|
target.SetActive(active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void SetBehavioursEnabled(Behaviour[] targets, bool enabled)
|
private static void SetBehavioursEnabled(Behaviour[] targets, bool enabled)
|
||||||
{
|
{
|
||||||
if (targets == null) return;
|
if (targets == null) return;
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ namespace XWorld.Framework.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void MatchRequest_RoundTrips()
|
public void MatchRequest_RoundTrips()
|
||||||
{
|
{
|
||||||
var m = new MatchRequestMsg { GameId = "rock_paper_scissors", Version = 3 };
|
var m = new MatchRequestMsg { GameId = "rock_paper_scissors", Version = 3, RequestId = 42 };
|
||||||
var d = MatchRequestMsg.Decode(m.Encode());
|
var d = MatchRequestMsg.Decode(m.Encode());
|
||||||
Assert.Equal(m.GameId, d.GameId);
|
Assert.Equal(m.GameId, d.GameId);
|
||||||
Assert.Equal(m.Version, d.Version);
|
Assert.Equal(m.Version, d.Version);
|
||||||
|
Assert.Equal(m.RequestId, d.RequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -29,9 +30,11 @@ namespace XWorld.Framework.Tests
|
|||||||
{
|
{
|
||||||
var m = new MatchFoundMsg
|
var m = new MatchFoundMsg
|
||||||
{
|
{
|
||||||
|
GameId = "rps",
|
||||||
RoomId = "room-42",
|
RoomId = "room-42",
|
||||||
Version = 3,
|
Version = 3,
|
||||||
SelfPlayerId = 1001,
|
SelfPlayerId = 1001,
|
||||||
|
RequestId = 99,
|
||||||
Players =
|
Players =
|
||||||
{
|
{
|
||||||
new PlayerInfo
|
new PlayerInfo
|
||||||
@@ -55,9 +58,11 @@ namespace XWorld.Framework.Tests
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
var d = MatchFoundMsg.Decode(m.Encode());
|
var d = MatchFoundMsg.Decode(m.Encode());
|
||||||
|
Assert.Equal("rps", d.GameId);
|
||||||
Assert.Equal("room-42", d.RoomId);
|
Assert.Equal("room-42", d.RoomId);
|
||||||
Assert.Equal(3, d.Version);
|
Assert.Equal(3, d.Version);
|
||||||
Assert.Equal(1001, d.SelfPlayerId);
|
Assert.Equal(1001, d.SelfPlayerId);
|
||||||
|
Assert.Equal(99, d.RequestId);
|
||||||
Assert.Equal(2, d.Players.Count);
|
Assert.Equal(2, d.Players.Count);
|
||||||
Assert.Equal(1001, d.Players[0].PlayerId);
|
Assert.Equal(1001, d.Players[0].PlayerId);
|
||||||
Assert.Equal("Alice", d.Players[0].Name);
|
Assert.Equal("Alice", d.Players[0].Name);
|
||||||
@@ -101,10 +106,30 @@ namespace XWorld.Framework.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Error_RoundTrips()
|
public void Error_RoundTrips()
|
||||||
{
|
{
|
||||||
var m = new ErrorMsg { Code = 404, Message = "version mismatch" };
|
var m = new ErrorMsg { Code = 404, Message = "version mismatch", RequestId = 7 };
|
||||||
var d = ErrorMsg.Decode(m.Encode());
|
var d = ErrorMsg.Decode(m.Encode());
|
||||||
Assert.Equal(404, d.Code);
|
Assert.Equal(404, d.Code);
|
||||||
Assert.Equal("version mismatch", d.Message);
|
Assert.Equal("version mismatch", d.Message);
|
||||||
|
Assert.Equal(7, d.RequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MatchControl_RoundTrips()
|
||||||
|
{
|
||||||
|
var m = new MatchControlMsg { RequestId = 123 };
|
||||||
|
var d = MatchControlMsg.Decode(m.Encode());
|
||||||
|
Assert.Equal(123, d.RequestId);
|
||||||
|
Assert.Equal(0, MatchControlMsg.Decode(System.Array.Empty<byte>()).RequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MatchAssigned_RoundTrips()
|
||||||
|
{
|
||||||
|
var m = new MatchAssignedMsg { GameId = "rps", Version = 2, RequestId = 88 };
|
||||||
|
var d = MatchAssignedMsg.Decode(m.Encode());
|
||||||
|
Assert.Equal("rps", d.GameId);
|
||||||
|
Assert.Equal(2, d.Version);
|
||||||
|
Assert.Equal(88, d.RequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -141,6 +166,7 @@ namespace XWorld.Framework.Tests
|
|||||||
Assert.Equal((ushort)7, (ushort)FrameworkOpcode.Error);
|
Assert.Equal((ushort)7, (ushort)FrameworkOpcode.Error);
|
||||||
Assert.Equal((ushort)16, (ushort)FrameworkOpcode.WorldChatSend);
|
Assert.Equal((ushort)16, (ushort)FrameworkOpcode.WorldChatSend);
|
||||||
Assert.Equal((ushort)17, (ushort)FrameworkOpcode.WorldChatMessage);
|
Assert.Equal((ushort)17, (ushort)FrameworkOpcode.WorldChatMessage);
|
||||||
|
Assert.Equal((ushort)18, (ushort)FrameworkOpcode.MatchCancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -209,6 +235,7 @@ namespace XWorld.Framework.Tests
|
|||||||
MatchFoundMsg d = MatchFoundMsg.Decode(w.ToArray());
|
MatchFoundMsg d = MatchFoundMsg.Decode(w.ToArray());
|
||||||
|
|
||||||
Assert.Equal("legacy-room", d.RoomId);
|
Assert.Equal("legacy-room", d.RoomId);
|
||||||
|
Assert.Equal(0, d.RequestId);
|
||||||
Assert.Equal(2, d.Players.Count);
|
Assert.Equal(2, d.Players.Count);
|
||||||
Assert.Equal(1001, d.Players[0].PlayerId);
|
Assert.Equal(1001, d.Players[0].PlayerId);
|
||||||
Assert.Equal("LegacyAlice", d.Players[0].Name);
|
Assert.Equal("LegacyAlice", d.Players[0].Name);
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ namespace XWorld.Server.Gateway.Runner
|
|||||||
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "Server.Host.Tests", "TestGames"));
|
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "Server.Host.Tests", "TestGames"));
|
||||||
int port = int.TryParse(GetArg(args, "--port"), out int p) ? p : 5005;
|
int port = int.TryParse(GetArg(args, "--port"), out int p) ? p : 5005;
|
||||||
int tickMs = int.TryParse(GetArg(args, "--tickMs"), out int t) ? t : 100;
|
int tickMs = int.TryParse(GetArg(args, "--tickMs"), out int t) ? t : 100;
|
||||||
|
long reconnectWindowTicks = long.TryParse(GetArg(args, "--reconnectWindowTicks"), out long rw)
|
||||||
|
? rw
|
||||||
|
: ServerLoop.DefaultReconnectWindowTicks;
|
||||||
long matchTimeoutTicks = long.TryParse(GetArg(args, "--matchTimeoutTicks"), out long mt) ? mt : 150;
|
long matchTimeoutTicks = long.TryParse(GetArg(args, "--matchTimeoutTicks"), out long mt) ? mt : 150;
|
||||||
string devToken = GetArg(args, "--devToken");
|
string devToken = GetArg(args, "--devToken");
|
||||||
string advertiseWs = GetArg(args, "--advertiseWs");
|
string advertiseWs = GetArg(args, "--advertiseWs");
|
||||||
@@ -49,13 +52,15 @@ namespace XWorld.Server.Gateway.Runner
|
|||||||
}
|
}
|
||||||
|
|
||||||
var host = new GameServerHost();
|
var host = new GameServerHost();
|
||||||
await host.StartAsync(gamesRoot, tickMs, matchTimeoutTicks: matchTimeoutTicks, listenPort: port,
|
await host.StartAsync(gamesRoot, tickMs, reconnectWindowTicks: reconnectWindowTicks,
|
||||||
|
matchTimeoutTicks: matchTimeoutTicks, listenPort: port,
|
||||||
bindAllInterfaces: lan,
|
bindAllInterfaces: lan,
|
||||||
devDiscoveryToken: devToken, advertiseGatewayUrl: advertiseWs, advertiseResourceBaseUrl: advertiseCdn,
|
devDiscoveryToken: devToken, advertiseGatewayUrl: advertiseWs, advertiseResourceBaseUrl: advertiseCdn,
|
||||||
authSecret: authSecret, authDbPath: authDbPath);
|
authSecret: authSecret, authDbPath: authDbPath);
|
||||||
Console.WriteLine("XWorld Gateway smoke server started.");
|
Console.WriteLine("XWorld Gateway smoke server started.");
|
||||||
Console.WriteLine(" gamesRoot: " + gamesRoot);
|
Console.WriteLine(" gamesRoot: " + gamesRoot);
|
||||||
Console.WriteLine(" tickMs: " + tickMs + ", matchTimeoutTicks: " + matchTimeoutTicks);
|
Console.WriteLine(" tickMs: " + tickMs + ", reconnectWindowTicks: " + reconnectWindowTicks +
|
||||||
|
", matchTimeoutTicks: " + matchTimeoutTicks);
|
||||||
Console.WriteLine(" ws: " + host.WsBaseUrl + "/ws?pid=1");
|
Console.WriteLine(" ws: " + host.WsBaseUrl + "/ws?pid=1");
|
||||||
if (!string.IsNullOrEmpty(devToken))
|
if (!string.IsNullOrEmpty(devToken))
|
||||||
Console.WriteLine(" dev-discovery: ON (UDP " + host.DevDiscoveryPort + "), advertise ws=" +
|
Console.WriteLine(" dev-discovery: ON (UDP " + host.DevDiscoveryPort + "), advertise ws=" +
|
||||||
|
|||||||
@@ -192,6 +192,67 @@ namespace XWorld.Server.Gateway.Tests
|
|||||||
Assert.Contains(Frames(c), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.Error);
|
Assert.Contains(Frames(c), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MatchCancel_AfterRoomCreated_ClearsRoomAndAllowsPlayerToMatchAgain()
|
||||||
|
{
|
||||||
|
var loop = NewLoop(matchTimeoutTicks: 0);
|
||||||
|
var c = new FakeConnection(5);
|
||||||
|
loop.Submit(new ConnectCommand { PlayerId = 5, Connection = c });
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 5, Frame = FrameworkFrame(FrameworkOpcode.RandomMatchRequest) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
Assert.True(HasMatchFound(c), "precondition: player should be in a room before cancel");
|
||||||
|
|
||||||
|
c.Sent.Clear();
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 5, Frame = FrameworkFrame(FrameworkOpcode.MatchCancel) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 5, Frame = FrameworkFrame(FrameworkOpcode.RandomMatchRequest) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
Assert.Contains(Frames(c), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchAssigned);
|
||||||
|
Assert.DoesNotContain(Frames(c), f =>
|
||||||
|
f.Channel == Channel.Framework &&
|
||||||
|
f.Opcode == (ushort)FrameworkOpcode.Error &&
|
||||||
|
ErrorMsg.Decode(f.Payload).Message == "already in room or queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MatchCancel_InTwoPlayerRoom_ClearsRoomForAllPlayers()
|
||||||
|
{
|
||||||
|
var loop = NewLoop(matchTimeoutTicks: 100);
|
||||||
|
var c1 = new FakeConnection(1);
|
||||||
|
var c2 = new FakeConnection(2);
|
||||||
|
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c1 });
|
||||||
|
loop.Submit(new ConnectCommand { PlayerId = 2, Connection = c2 });
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameCodec.Decode(RpsJoinFrame()) });
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 2, Frame = FrameCodec.Decode(RpsJoinFrame()) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
Assert.True(HasMatchFound(c1), "precondition: player 1 should be in a room before cancel");
|
||||||
|
Assert.True(HasMatchFound(c2), "precondition: player 2 should be in a room before cancel");
|
||||||
|
|
||||||
|
c1.Sent.Clear();
|
||||||
|
c2.Sent.Clear();
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameworkFrame(FrameworkOpcode.MatchCancel) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameCodec.Decode(RpsJoinFrame()) });
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 2, Frame = FrameCodec.Decode(RpsJoinFrame()) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
Assert.True(HasMatchFound(c1), "player 1 should be able to match again after cancel");
|
||||||
|
Assert.True(HasMatchFound(c2), "player 2 should be able to match again after the room was cancelled");
|
||||||
|
Assert.DoesNotContain(Frames(c1), f =>
|
||||||
|
f.Channel == Channel.Framework &&
|
||||||
|
f.Opcode == (ushort)FrameworkOpcode.Error &&
|
||||||
|
ErrorMsg.Decode(f.Payload).Message == "already in room or queue");
|
||||||
|
Assert.DoesNotContain(Frames(c2), f =>
|
||||||
|
f.Channel == Channel.Framework &&
|
||||||
|
f.Opcode == (ushort)FrameworkOpcode.Error &&
|
||||||
|
ErrorMsg.Decode(f.Payload).Message == "already in room or queue");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Player 1 disconnects while still queued (before match forms).
|
/// Player 1 disconnects while still queued (before match forms).
|
||||||
/// DrainAndTick processes the DisconnectCommand (which dequeues pid=1) BEFORE Poll,
|
/// DrainAndTick processes the DisconnectCommand (which dequeues pid=1) BEFORE Poll,
|
||||||
|
|||||||
@@ -140,6 +140,42 @@ namespace XWorld.Server.Gateway.Tests
|
|||||||
Assert.Contains(Frames(c1b), f => f.Channel == Channel.Game);
|
Assert.Contains(Frames(c1b), f => f.Channel == Channel.Game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reconnect_ReplaysMatchFoundAndLatestGameSnapshot()
|
||||||
|
{
|
||||||
|
var loop = NewLoop(reconnectWindow: 100);
|
||||||
|
var c1 = new FakeConnection(1);
|
||||||
|
var c2 = new FakeConnection(2);
|
||||||
|
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c1 });
|
||||||
|
loop.Submit(new ConnectCommand { PlayerId = 2, Connection = c2 });
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameCodec.Decode(JoinFrame("rps", 1)) });
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 2, Frame = FrameCodec.Decode(JoinFrame("rps", 1)) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
loop.Submit(new DisconnectCommand { PlayerId = 1, Connection = c1 });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
var c1b = new FakeConnection(1);
|
||||||
|
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c1b });
|
||||||
|
loop.DrainAndTick(0f);
|
||||||
|
|
||||||
|
Frame resumeFrame = Frames(c1b).Find(f =>
|
||||||
|
f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchFound);
|
||||||
|
Assert.NotEqual(default, resumeFrame);
|
||||||
|
MatchFoundMsg resume = MatchFoundMsg.Decode(resumeFrame.Payload);
|
||||||
|
Assert.Equal("rps", resume.GameId);
|
||||||
|
Assert.Equal(1, resume.Version);
|
||||||
|
Assert.Equal(1, resume.SelfPlayerId);
|
||||||
|
Assert.Equal(2, resume.Players.Count);
|
||||||
|
Assert.Contains(Frames(c1b), f => f.Channel == Channel.Game);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultReconnectWindow_IsThreeMinutesAtDefaultTickRate()
|
||||||
|
{
|
||||||
|
Assert.Equal(1800, ServerLoop.DefaultReconnectWindowTicks);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExpiredSession_IsSwept()
|
public void ExpiredSession_IsSwept()
|
||||||
{
|
{
|
||||||
@@ -173,6 +209,27 @@ namespace XWorld.Server.Gateway.Tests
|
|||||||
Assert.Contains(Frames(c1), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.Error);
|
Assert.Contains(Frames(c1), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MatchCancel_WhileQueued_CancelsQueuedMatchAndAllowsPlayerToMatchAgain()
|
||||||
|
{
|
||||||
|
var loop = NewLoop();
|
||||||
|
var c = new FakeConnection(1);
|
||||||
|
loop.Submit(new ConnectCommand { PlayerId = 1, Connection = c });
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 1, Frame = FrameCodec.Decode(JoinFrame("rps", 1)) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
c.Sent.Clear();
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 1, Frame = new Frame(Channel.Framework, (ushort)FrameworkOpcode.MatchCancel, System.Array.Empty<byte>()) });
|
||||||
|
loop.Submit(new FrameCommand { PlayerId = 1, Frame = new Frame(Channel.Framework, (ushort)FrameworkOpcode.RandomMatchRequest, System.Array.Empty<byte>()) });
|
||||||
|
loop.DrainAndTick(1f);
|
||||||
|
|
||||||
|
Assert.Contains(Frames(c), f => f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchAssigned);
|
||||||
|
Assert.DoesNotContain(Frames(c), f =>
|
||||||
|
f.Channel == Channel.Framework &&
|
||||||
|
f.Opcode == (ushort)FrameworkOpcode.Error &&
|
||||||
|
ErrorMsg.Decode(f.Payload).Message == "already in room or queue");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GameFrame_WithNoRoom_IsIgnoredSafely()
|
public void GameFrame_WithNoRoom_IsIgnoredSafely()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace XWorld.Server.Gateway
|
|||||||
public string WsBaseUrl { get; private set; } // 例如 ws://127.0.0.1:54321
|
public string WsBaseUrl { get; private set; } // 例如 ws://127.0.0.1:54321
|
||||||
public int DevDiscoveryPort => _discovery?.Port ?? 0;
|
public int DevDiscoveryPort => _discovery?.Port ?? 0;
|
||||||
|
|
||||||
public async Task StartAsync(string gamesRoot, int tickIntervalMs, long reconnectWindowTicks = 100,
|
public async Task StartAsync(string gamesRoot, int tickIntervalMs, long reconnectWindowTicks = ServerLoop.DefaultReconnectWindowTicks,
|
||||||
long matchTimeoutTicks = 150, float? logicalDt = null, string publicKeyPem = null, int listenPort = 0,
|
long matchTimeoutTicks = 150, float? logicalDt = null, string publicKeyPem = null, int listenPort = 0,
|
||||||
bool bindAllInterfaces = false,
|
bool bindAllInterfaces = false,
|
||||||
string devDiscoveryToken = null, string advertiseGatewayUrl = null, string advertiseResourceBaseUrl = null,
|
string devDiscoveryToken = null, string advertiseGatewayUrl = null, string advertiseResourceBaseUrl = null,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ namespace XWorld.Server.Gateway
|
|||||||
{
|
{
|
||||||
public sealed class Matchmaker
|
public sealed class Matchmaker
|
||||||
{
|
{
|
||||||
public sealed class Seat { public int PlayerId; public bool IsAi; }
|
public sealed class Seat { public int PlayerId; public bool IsAi; public int RequestId; }
|
||||||
public sealed class Match { public string GameId; public int Version; public List<Seat> Seats = new List<Seat>(); }
|
public sealed class Match { public string GameId; public int Version; public List<Seat> Seats = new List<Seat>(); }
|
||||||
|
|
||||||
private sealed class Waiter { public int Pid; public long EnqueuedTick; }
|
private sealed class Waiter { public int Pid; public int RequestId; public long EnqueuedTick; }
|
||||||
private sealed class Bucket { public int PlayerCount; public List<Waiter> Waiters = new List<Waiter>(); }
|
private sealed class Bucket { public int PlayerCount; public List<Waiter> Waiters = new List<Waiter>(); }
|
||||||
|
|
||||||
private readonly long _timeoutTicks;
|
private readonly long _timeoutTicks;
|
||||||
@@ -17,11 +17,11 @@ namespace XWorld.Server.Gateway
|
|||||||
public Matchmaker(long timeoutTicks) { _timeoutTicks = timeoutTicks; }
|
public Matchmaker(long timeoutTicks) { _timeoutTicks = timeoutTicks; }
|
||||||
private static string Key(string g, int v) => $"{g}@{v}";
|
private static string Key(string g, int v) => $"{g}@{v}";
|
||||||
|
|
||||||
public void Enqueue(int pid, string gameId, int version, int playerCount, long tick)
|
public void Enqueue(int pid, string gameId, int version, int playerCount, long tick, int requestId = 0)
|
||||||
{
|
{
|
||||||
string k = Key(gameId, version);
|
string k = Key(gameId, version);
|
||||||
if (!_buckets.TryGetValue(k, out var b)) { b = new Bucket { PlayerCount = playerCount }; _buckets[k] = b; }
|
if (!_buckets.TryGetValue(k, out var b)) { b = new Bucket { PlayerCount = playerCount }; _buckets[k] = b; }
|
||||||
b.Waiters.Add(new Waiter { Pid = pid, EnqueuedTick = tick });
|
b.Waiters.Add(new Waiter { Pid = pid, RequestId = requestId, EnqueuedTick = tick });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从等待队列移除某玩家(断线时调用);返回是否移除
|
// 从等待队列移除某玩家(断线时调用);返回是否移除
|
||||||
@@ -51,7 +51,7 @@ namespace XWorld.Server.Gateway
|
|||||||
var m = new Match { GameId = gv[0], Version = int.Parse(gv[1]) };
|
var m = new Match { GameId = gv[0], Version = int.Parse(gv[1]) };
|
||||||
int take = b.Waiters.Count >= b.PlayerCount ? b.PlayerCount : b.Waiters.Count;
|
int take = b.Waiters.Count >= b.PlayerCount ? b.PlayerCount : b.Waiters.Count;
|
||||||
for (int i = 0; i < take; i++)
|
for (int i = 0; i < take; i++)
|
||||||
m.Seats.Add(new Seat { PlayerId = b.Waiters[i].Pid, IsAi = false });
|
m.Seats.Add(new Seat { PlayerId = b.Waiters[i].Pid, RequestId = b.Waiters[i].RequestId, IsAi = false });
|
||||||
b.Waiters.RemoveRange(0, take);
|
b.Waiters.RemoveRange(0, take);
|
||||||
while (m.Seats.Count < b.PlayerCount)
|
while (m.Seats.Count < b.PlayerCount)
|
||||||
m.Seats.Add(new Seat { PlayerId = -(++_aiSeq), IsAi = true }); // AI 用负 pid
|
m.Seats.Add(new Seat { PlayerId = -(++_aiSeq), IsAi = true }); // AI 用负 pid
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using XWorld.Framework;
|
using XWorld.Framework;
|
||||||
using XWorld.Framework.Protocol;
|
using XWorld.Framework.Protocol;
|
||||||
@@ -11,14 +12,16 @@ namespace XWorld.Server.Gateway
|
|||||||
private readonly string _roomId; // 保留作诊断/日志标识
|
private readonly string _roomId; // 保留作诊断/日志标识
|
||||||
private readonly IReadOnlyList<int> _players;
|
private readonly IReadOnlyList<int> _players;
|
||||||
private readonly SessionManager _sessions;
|
private readonly SessionManager _sessions;
|
||||||
|
private readonly Action<NetMessage> _onBroadcast;
|
||||||
|
|
||||||
public RoomOutputSink(string roomId, IReadOnlyList<int> players, SessionManager sessions)
|
public RoomOutputSink(string roomId, IReadOnlyList<int> players, SessionManager sessions, Action<NetMessage> onBroadcast = null)
|
||||||
{
|
{
|
||||||
_roomId = roomId; _players = players; _sessions = sessions;
|
_roomId = roomId; _players = players; _sessions = sessions; _onBroadcast = onBroadcast;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Broadcast(NetMessage message)
|
public void Broadcast(NetMessage message)
|
||||||
{
|
{
|
||||||
|
_onBroadcast?.Invoke(message);
|
||||||
byte[] frame = FrameCodec.Encode(new Frame(Channel.Game, message.Opcode, message.Payload));
|
byte[] frame = FrameCodec.Encode(new Frame(Channel.Game, message.Opcode, message.Payload));
|
||||||
foreach (var pid in _players)
|
foreach (var pid in _players)
|
||||||
_sessions.GetConnection(pid)?.Send(frame);
|
_sessions.GetConnection(pid)?.Send(frame);
|
||||||
|
|||||||
+163
-21
@@ -21,16 +21,31 @@ namespace XWorld.Server.Gateway
|
|||||||
private readonly Matchmaker _matchmaker;
|
private readonly Matchmaker _matchmaker;
|
||||||
private readonly ChannelsNS.Channel<ServerCommand> _cmds =
|
private readonly ChannelsNS.Channel<ServerCommand> _cmds =
|
||||||
ChannelsNS.Channel.CreateUnbounded<ServerCommand>();
|
ChannelsNS.Channel.CreateUnbounded<ServerCommand>();
|
||||||
|
public const long DefaultReconnectWindowTicks = 1800;
|
||||||
private const int MaxWorldChatTextLength = 200;
|
private const int MaxWorldChatTextLength = 200;
|
||||||
private readonly Dictionary<string, List<int>> _roomPlayers = new Dictionary<string, List<int>>();
|
private readonly Dictionary<string, List<int>> _roomPlayers = new Dictionary<string, List<int>>();
|
||||||
|
private readonly Dictionary<string, ActiveRoomInfo> _activeRooms = new Dictionary<string, ActiveRoomInfo>();
|
||||||
private readonly Dictionary<int, LobbyPlayerMsg> _lobbyPlayers = new Dictionary<int, LobbyPlayerMsg>();
|
private readonly Dictionary<int, LobbyPlayerMsg> _lobbyPlayers = new Dictionary<int, LobbyPlayerMsg>();
|
||||||
private readonly Dictionary<string, int> _playerCountCache = new Dictionary<string, int>(); // "gameId@version" -> playerCount
|
private readonly Dictionary<string, int> _playerCountCache = new Dictionary<string, int>(); // "gameId@version" -> playerCount
|
||||||
private readonly HashSet<int> _queuedPids = new HashSet<int>(); // pids currently in matchmaker queue
|
private readonly HashSet<int> _queuedPids = new HashSet<int>(); // pids currently in matchmaker queue
|
||||||
|
private readonly Dictionary<int, int> _queuedRequestIds = new Dictionary<int, int>();
|
||||||
|
private readonly Dictionary<int, int> _activeRoomRequestIds = new Dictionary<int, int>();
|
||||||
private readonly Random _random = new Random(1);
|
private readonly Random _random = new Random(1);
|
||||||
private long _tick;
|
private long _tick;
|
||||||
private int _roomSeq;
|
private int _roomSeq;
|
||||||
|
|
||||||
public ServerLoop(string gamesRoot, ILogger logger, long reconnectWindowTicks = 100,
|
private sealed class ActiveRoomInfo
|
||||||
|
{
|
||||||
|
public string RoomId;
|
||||||
|
public string GameId;
|
||||||
|
public int Version;
|
||||||
|
public List<PlayerInfo> Players;
|
||||||
|
public Dictionary<int, int> RequestIds = new Dictionary<int, int>();
|
||||||
|
public bool HasLastGameMessage;
|
||||||
|
public NetMessage LastGameMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerLoop(string gamesRoot, ILogger logger, long reconnectWindowTicks = DefaultReconnectWindowTicks,
|
||||||
long matchTimeoutTicks = 150, string publicKeyPem = null)
|
long matchTimeoutTicks = 150, string publicKeyPem = null)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -104,11 +119,12 @@ namespace XWorld.Server.Gateway
|
|||||||
case ConnectCommand cc:
|
case ConnectCommand cc:
|
||||||
_sessions.OnConnect(cc.PlayerId, cc.Connection);
|
_sessions.OnConnect(cc.PlayerId, cc.Connection);
|
||||||
_logger.Info($"session connect pid={cc.PlayerId}");
|
_logger.Info($"session connect pid={cc.PlayerId}");
|
||||||
|
SendRoomResumeIfNeeded(cc.PlayerId);
|
||||||
break;
|
break;
|
||||||
case DisconnectCommand dc:
|
case DisconnectCommand dc:
|
||||||
_sessions.OnDisconnect(dc.PlayerId, dc.Connection, _tick);
|
_sessions.OnDisconnect(dc.PlayerId, dc.Connection, _tick);
|
||||||
_logger.Info($"session disconnect pid={dc.PlayerId}");
|
_logger.Info($"session disconnect pid={dc.PlayerId}");
|
||||||
if (_queuedPids.Remove(dc.PlayerId)) _matchmaker.Remove(dc.PlayerId);
|
CancelQueuedMatch(dc.PlayerId);
|
||||||
if (_lobbyPlayers.Remove(dc.PlayerId)) BroadcastLobbyState();
|
if (_lobbyPlayers.Remove(dc.PlayerId)) BroadcastLobbyState();
|
||||||
break;
|
break;
|
||||||
case FrameCommand fc: Route(fc.PlayerId, fc.Frame); break;
|
case FrameCommand fc: Route(fc.PlayerId, fc.Frame); break;
|
||||||
@@ -140,13 +156,16 @@ namespace XWorld.Server.Gateway
|
|||||||
break;
|
break;
|
||||||
case FrameworkOpcode.MatchRequest:
|
case FrameworkOpcode.MatchRequest:
|
||||||
var req = MatchRequestMsg.Decode(frame.Payload);
|
var req = MatchRequestMsg.Decode(frame.Payload);
|
||||||
EnqueueMatch(pid, req.GameId, req.Version);
|
EnqueueMatch(pid, req.GameId, req.Version, req.RequestId);
|
||||||
break;
|
break;
|
||||||
case FrameworkOpcode.GameListRequest:
|
case FrameworkOpcode.GameListRequest:
|
||||||
SendFramework(pid, FrameworkOpcode.GameListResponse, BuildGameList().Encode());
|
SendFramework(pid, FrameworkOpcode.GameListResponse, BuildGameList().Encode());
|
||||||
break;
|
break;
|
||||||
case FrameworkOpcode.RandomMatchRequest:
|
case FrameworkOpcode.RandomMatchRequest:
|
||||||
EnqueueRandomMatch(pid);
|
EnqueueRandomMatch(pid, MatchControlMsg.Decode(frame.Payload).RequestId);
|
||||||
|
break;
|
||||||
|
case FrameworkOpcode.MatchCancel:
|
||||||
|
CancelMatchState(pid, MatchControlMsg.Decode(frame.Payload).RequestId);
|
||||||
break;
|
break;
|
||||||
case FrameworkOpcode.LobbyJoin:
|
case FrameworkOpcode.LobbyJoin:
|
||||||
HandleLobbyJoin(pid, frame.Payload);
|
HandleLobbyJoin(pid, frame.Payload);
|
||||||
@@ -155,6 +174,7 @@ namespace XWorld.Server.Gateway
|
|||||||
HandleLobbyMove(pid, frame.Payload);
|
HandleLobbyMove(pid, frame.Payload);
|
||||||
break;
|
break;
|
||||||
case FrameworkOpcode.LobbyLeave:
|
case FrameworkOpcode.LobbyLeave:
|
||||||
|
CancelQueuedMatch(pid);
|
||||||
if (_lobbyPlayers.Remove(pid)) BroadcastLobbyState();
|
if (_lobbyPlayers.Remove(pid)) BroadcastLobbyState();
|
||||||
break;
|
break;
|
||||||
case FrameworkOpcode.WorldChatSend:
|
case FrameworkOpcode.WorldChatSend:
|
||||||
@@ -272,21 +292,23 @@ namespace XWorld.Server.Gateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnqueueRandomMatch(int pid)
|
private void EnqueueRandomMatch(int pid, int requestId)
|
||||||
{
|
{
|
||||||
var games = BuildGameList().Games;
|
var games = BuildGameList().Games;
|
||||||
if (games.Count == 0)
|
if (games.Count == 0)
|
||||||
{
|
{
|
||||||
SendFramework(pid, FrameworkOpcode.Error,
|
SendFramework(pid, FrameworkOpcode.Error,
|
||||||
new ErrorMsg { Code = 1, Message = "no minigame available" }.Encode());
|
new ErrorMsg { Code = 1, Message = "no minigame available", RequestId = requestId }.Encode());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var selected = games[_random.Next(games.Count)];
|
var selected = games[_random.Next(games.Count)];
|
||||||
|
if (EnqueueMatch(pid, selected.GameId, selected.Version, requestId))
|
||||||
|
{
|
||||||
_logger.Info($"random match pid={pid} assigned {selected.GameId}@{selected.Version}");
|
_logger.Info($"random match pid={pid} assigned {selected.GameId}@{selected.Version}");
|
||||||
SendFramework(pid, FrameworkOpcode.MatchAssigned,
|
SendFramework(pid, FrameworkOpcode.MatchAssigned,
|
||||||
new MatchAssignedMsg { GameId = selected.GameId, Version = selected.Version }.Encode());
|
new MatchAssignedMsg { GameId = selected.GameId, Version = selected.Version, RequestId = requestId }.Encode());
|
||||||
EnqueueMatch(pid, selected.GameId, selected.Version);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private GameListResponseMsg BuildGameList()
|
private GameListResponseMsg BuildGameList()
|
||||||
@@ -332,17 +354,17 @@ namespace XWorld.Server.Gateway
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnqueueMatch(int pid, string gameId, int version)
|
private bool EnqueueMatch(int pid, string gameId, int version, int requestId = 0)
|
||||||
{
|
{
|
||||||
var s = _sessions.Get(pid);
|
var s = _sessions.Get(pid);
|
||||||
if (s == null) return; // 未建立会话,忽略
|
if (s == null) return false; // 未建立会话,忽略
|
||||||
|
|
||||||
if (s.RoomId != null || _queuedPids.Contains(pid))
|
if (s.RoomId != null || _queuedPids.Contains(pid))
|
||||||
{
|
{
|
||||||
// 已在房间内或已在队列中:拒绝
|
// 已在房间内或已在队列中:拒绝
|
||||||
SendFramework(pid, FrameworkOpcode.Error,
|
SendFramework(pid, FrameworkOpcode.Error,
|
||||||
new ErrorMsg { Code = 2, Message = "already in room or queue" }.Encode());
|
new ErrorMsg { Code = 2, Message = "already in room or queue", RequestId = requestId }.Encode());
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取游戏的 playerCount(缓存)
|
// 读取游戏的 playerCount(缓存)
|
||||||
@@ -360,14 +382,76 @@ namespace XWorld.Server.Gateway
|
|||||||
{
|
{
|
||||||
_logger.Error($"读取 game.json 失败 {gameId}@{version}: {ex.Message}");
|
_logger.Error($"读取 game.json 失败 {gameId}@{version}: {ex.Message}");
|
||||||
SendFramework(pid, FrameworkOpcode.Error,
|
SendFramework(pid, FrameworkOpcode.Error,
|
||||||
new ErrorMsg { Code = 1, Message = "game not found" }.Encode());
|
new ErrorMsg { Code = 1, Message = "game not found", RequestId = requestId }.Encode());
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_queuedPids.Add(pid);
|
_queuedPids.Add(pid);
|
||||||
_matchmaker.Enqueue(pid, gameId, version, playerCount, _tick);
|
_queuedRequestIds[pid] = requestId;
|
||||||
_logger.Info($"enqueue match pid={pid} game={gameId}@{version} playerCount={playerCount} tick={_tick}");
|
_matchmaker.Enqueue(pid, gameId, version, playerCount, _tick, requestId);
|
||||||
|
_logger.Info($"enqueue match pid={pid} req={requestId} game={gameId}@{version} playerCount={playerCount} tick={_tick}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelQueuedMatch(int pid, int requestId = 0)
|
||||||
|
{
|
||||||
|
if (requestId != 0 &&
|
||||||
|
_queuedRequestIds.TryGetValue(pid, out int queuedRequestId) &&
|
||||||
|
queuedRequestId != requestId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_queuedPids.Remove(pid))
|
||||||
|
{
|
||||||
|
_queuedRequestIds.Remove(pid);
|
||||||
|
_matchmaker.Remove(pid);
|
||||||
|
_logger.Info($"cancel queued match pid={pid} req={requestId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelMatchState(int pid, int requestId)
|
||||||
|
{
|
||||||
|
CancelQueuedMatch(pid, requestId);
|
||||||
|
CancelActiveRoom(pid, requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelActiveRoom(int pid, int requestId)
|
||||||
|
{
|
||||||
|
var s = _sessions.Get(pid);
|
||||||
|
if (s?.RoomId == null) return;
|
||||||
|
if (requestId != 0 &&
|
||||||
|
_activeRoomRequestIds.TryGetValue(pid, out int activeRequestId) &&
|
||||||
|
activeRequestId != requestId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string roomId = s.RoomId;
|
||||||
|
|
||||||
|
if (_roomPlayers.TryGetValue(roomId, out var players))
|
||||||
|
{
|
||||||
|
var roomPids = new List<int>(players);
|
||||||
|
_roomPlayers.Remove(roomId);
|
||||||
|
_activeRooms.Remove(roomId);
|
||||||
|
for (int i = 0; i < roomPids.Count; i++)
|
||||||
|
{
|
||||||
|
int roomPid = roomPids[i];
|
||||||
|
var roomSession = _sessions.Get(roomPid);
|
||||||
|
if (roomSession?.RoomId == roomId) roomSession.RoomId = null;
|
||||||
|
_activeRoomRequestIds.Remove(roomPid);
|
||||||
|
}
|
||||||
|
_host.EndRoom(roomId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_activeRooms.Remove(roomId);
|
||||||
|
s.RoomId = null;
|
||||||
|
_activeRoomRequestIds.Remove(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Info($"cancel active match pid={pid} req={requestId} room={roomId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateRoomForSeats(Matchmaker.Match match)
|
private void CreateRoomForSeats(Matchmaker.Match match)
|
||||||
@@ -397,10 +481,23 @@ namespace XWorld.Server.Gateway
|
|||||||
{
|
{
|
||||||
realPids.Add(seat.PlayerId);
|
realPids.Add(seat.PlayerId);
|
||||||
_queuedPids.Remove(seat.PlayerId);
|
_queuedPids.Remove(seat.PlayerId);
|
||||||
|
_queuedRequestIds.Remove(seat.PlayerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sink = new RoomOutputSink(roomId, realPids, _sessions);
|
var allPlayers = new List<PlayerInfo>(playerInfos);
|
||||||
|
var roomInfo = new ActiveRoomInfo
|
||||||
|
{
|
||||||
|
RoomId = roomId,
|
||||||
|
GameId = match.GameId,
|
||||||
|
Version = match.Version,
|
||||||
|
Players = allPlayers
|
||||||
|
};
|
||||||
|
var sink = new RoomOutputSink(roomId, realPids, _sessions, message =>
|
||||||
|
{
|
||||||
|
roomInfo.HasLastGameMessage = true;
|
||||||
|
roomInfo.LastGameMessage = new NetMessage(message.Opcode, message.Payload ?? Array.Empty<byte>());
|
||||||
|
});
|
||||||
var cfg = new RoomConfig
|
var cfg = new RoomConfig
|
||||||
{
|
{
|
||||||
GameId = match.GameId,
|
GameId = match.GameId,
|
||||||
@@ -409,6 +506,7 @@ namespace XWorld.Server.Gateway
|
|||||||
PlayerCount = seats.Count
|
PlayerCount = seats.Count
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_activeRooms[roomId] = roomInfo;
|
||||||
Room room;
|
Room room;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -417,6 +515,7 @@ namespace XWorld.Server.Gateway
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error($"建房失败 {match.GameId}@{match.Version}: {ex.Message}");
|
_logger.Error($"建房失败 {match.GameId}@{match.Version}: {ex.Message}");
|
||||||
|
_activeRooms.Remove(roomId);
|
||||||
// Notify real players of error
|
// Notify real players of error
|
||||||
var errPayload = new ErrorMsg { Code = 1, Message = "create room failed" }.Encode();
|
var errPayload = new ErrorMsg { Code = 1, Message = "create room failed" }.Encode();
|
||||||
foreach (int rpid in realPids)
|
foreach (int rpid in realPids)
|
||||||
@@ -424,20 +523,30 @@ namespace XWorld.Server.Gateway
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build MatchFound message with all players info
|
|
||||||
var allPlayers = new List<PlayerInfo>(playerInfos);
|
|
||||||
|
|
||||||
// For each real player: set session.RoomId + send MatchFound
|
// For each real player: set session.RoomId + send MatchFound
|
||||||
foreach (int rpid in realPids)
|
foreach (int rpid in realPids)
|
||||||
{
|
{
|
||||||
var s = _sessions.Get(rpid);
|
var s = _sessions.Get(rpid);
|
||||||
if (s != null) s.RoomId = roomId;
|
if (s != null) s.RoomId = roomId;
|
||||||
|
int requestId = 0;
|
||||||
|
for (int i = 0; i < seats.Count; i++)
|
||||||
|
{
|
||||||
|
if (!seats[i].IsAi && seats[i].PlayerId == rpid)
|
||||||
|
{
|
||||||
|
requestId = seats[i].RequestId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_activeRoomRequestIds[rpid] = requestId;
|
||||||
|
roomInfo.RequestIds[rpid] = requestId;
|
||||||
|
|
||||||
var found = new MatchFoundMsg
|
var found = new MatchFoundMsg
|
||||||
{
|
{
|
||||||
|
GameId = match.GameId,
|
||||||
RoomId = roomId,
|
RoomId = roomId,
|
||||||
Version = match.Version,
|
Version = match.Version,
|
||||||
SelfPlayerId = rpid
|
SelfPlayerId = rpid,
|
||||||
|
RequestId = requestId
|
||||||
};
|
};
|
||||||
foreach (var pi in allPlayers) found.Players.Add(pi);
|
foreach (var pi in allPlayers) found.Players.Add(pi);
|
||||||
SendFramework(rpid, FrameworkOpcode.MatchFound, found.Encode());
|
SendFramework(rpid, FrameworkOpcode.MatchFound, found.Encode());
|
||||||
@@ -452,6 +561,7 @@ namespace XWorld.Server.Gateway
|
|||||||
{
|
{
|
||||||
if (!_roomPlayers.TryGetValue(roomId, out var players)) return;
|
if (!_roomPlayers.TryGetValue(roomId, out var players)) return;
|
||||||
_roomPlayers.Remove(roomId);
|
_roomPlayers.Remove(roomId);
|
||||||
|
_activeRooms.Remove(roomId);
|
||||||
var msg = new RoomEndMsg
|
var msg = new RoomEndMsg
|
||||||
{
|
{
|
||||||
WinnerPlayerId = result?.WinnerPlayerId ?? 0,
|
WinnerPlayerId = result?.WinnerPlayerId ?? 0,
|
||||||
@@ -464,6 +574,7 @@ namespace XWorld.Server.Gateway
|
|||||||
SendFramework(pid, FrameworkOpcode.RoomEnd, payload);
|
SendFramework(pid, FrameworkOpcode.RoomEnd, payload);
|
||||||
var s = _sessions.Get(pid);
|
var s = _sessions.Get(pid);
|
||||||
if (s != null) s.RoomId = null;
|
if (s != null) s.RoomId = null;
|
||||||
|
_activeRoomRequestIds.Remove(pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,5 +583,36 @@ namespace XWorld.Server.Gateway
|
|||||||
_sessions.GetConnection(pid)?.Send(
|
_sessions.GetConnection(pid)?.Send(
|
||||||
FrameCodec.Encode(new Frame(Channel.Framework, (ushort)op, payload)));
|
FrameCodec.Encode(new Frame(Channel.Framework, (ushort)op, payload)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SendGame(int pid, NetMessage message)
|
||||||
|
{
|
||||||
|
_sessions.GetConnection(pid)?.Send(
|
||||||
|
FrameCodec.Encode(new Frame(Channel.Game, message.Opcode, message.Payload)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendRoomResumeIfNeeded(int pid)
|
||||||
|
{
|
||||||
|
var session = _sessions.Get(pid);
|
||||||
|
if (session?.RoomId == null) return;
|
||||||
|
if (!_activeRooms.TryGetValue(session.RoomId, out var room))
|
||||||
|
{
|
||||||
|
session.RoomId = null;
|
||||||
|
_activeRoomRequestIds.Remove(pid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var found = new MatchFoundMsg
|
||||||
|
{
|
||||||
|
GameId = room.GameId,
|
||||||
|
RoomId = room.RoomId,
|
||||||
|
Version = room.Version,
|
||||||
|
SelfPlayerId = pid,
|
||||||
|
RequestId = room.RequestIds.TryGetValue(pid, out int requestId) ? requestId : 0
|
||||||
|
};
|
||||||
|
for (int i = 0; i < room.Players.Count; i++) found.Players.Add(room.Players[i]);
|
||||||
|
SendFramework(pid, FrameworkOpcode.MatchFound, found.Encode());
|
||||||
|
if (room.HasLastGameMessage) SendGame(pid, room.LastGameMessage);
|
||||||
|
_logger.Info($"resume room pid={pid} room={room.RoomId} game={room.GameId}@{room.Version}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,23 @@ namespace XWorld.Server.Host.Tests
|
|||||||
() => host.CreateRoom("dup", "rps", 1, Cfg(), Players, new Capture()));
|
() => host.CreateRoom("dup", "rps", 1, Cfg(), Players, new Capture()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EndRoom_ReleasesRoomImmediately()
|
||||||
|
{
|
||||||
|
var host = NewHost();
|
||||||
|
var cap = new Capture();
|
||||||
|
host.CreateRoom("manual", "rps", 1, Cfg(), Players, cap);
|
||||||
|
|
||||||
|
Assert.True(host.EndRoom("manual"));
|
||||||
|
Assert.Equal(0, host.ActiveRoomCount);
|
||||||
|
|
||||||
|
int broadcasts = cap.Broadcasts.Count;
|
||||||
|
host.DeliverTo("manual", 1, new NetMessage(1, new byte[] { (byte)Choice.Rock }));
|
||||||
|
host.TickAll(0.1f);
|
||||||
|
|
||||||
|
Assert.Equal(broadcasts, cap.Broadcasts.Count);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TickAll_ReturnsEndedRoomIds()
|
public void TickAll_ReturnsEndedRoomIds()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -84,6 +84,14 @@ namespace XWorld.Server.Host
|
|||||||
entry.Room.Deliver(playerId, message);
|
entry.Room.Deliver(playerId, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool EndRoom(string roomId)
|
||||||
|
{
|
||||||
|
if (!_rooms.TryGetValue(roomId, out var entry)) return false;
|
||||||
|
entry.Room.End();
|
||||||
|
ReleaseRoom(roomId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void ReleaseRoom(string roomId)
|
private void ReleaseRoom(string roomId)
|
||||||
{
|
{
|
||||||
if (!_rooms.TryGetValue(roomId, out var entry)) return;
|
if (!_rooms.TryGetValue(roomId, out var entry)) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user