using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using ChannelsNS = System.Threading.Channels; using XWorld.Framework; using XWorld.Framework.Protocol; using XWorld.Server.Host; namespace XWorld.Server.Gateway { // 单线程 actor:独占 RoomHost 与 SessionManager;命令入队、周期性排空+推进。 public sealed class ServerLoop { private readonly RoomHost _host; private readonly SessionManager _sessions = new SessionManager(); private readonly ILogger _logger; private readonly long _reconnectWindowTicks; private readonly string _gamesRoot; private readonly Matchmaker _matchmaker; private readonly ChannelsNS.Channel _cmds = ChannelsNS.Channel.CreateUnbounded(); private readonly Dictionary> _roomPlayers = new Dictionary>(); private readonly Dictionary _lobbyPlayers = new Dictionary(); private readonly Dictionary _playerCountCache = new Dictionary(); // "gameId@version" -> playerCount private readonly HashSet _queuedPids = new HashSet(); // pids currently in matchmaker queue private readonly Random _random = new Random(1); private long _tick; private int _roomSeq; public ServerLoop(string gamesRoot, ILogger logger, long reconnectWindowTicks = 100, long matchTimeoutTicks = 150, string publicKeyPem = null) { _logger = logger; _reconnectWindowTicks = reconnectWindowTicks; _gamesRoot = gamesRoot; _host = new RoomHost(new GameModuleRegistry(new GameModuleLoader(publicKeyPem), gamesRoot), logger); _matchmaker = new Matchmaker(matchTimeoutTicks); } // 线程安全:网络线程调用,仅入队 public void Submit(ServerCommand cmd) => _cmds.Writer.TryWrite(cmd); // 仅 actor 线程调用(_sessions 无锁);测试在单线程下使用 public bool HasSession(int pid) => _sessions.Get(pid) != null; // 测试直调;真实循环周期性调它。 // 两步排空:先处理所有 Connect/Disconnect/Framework(含 MatchRequest 入队), // 再 Poll 匹配器建房,再处理 Game 帧(此时 RoomId 已就位)。 public void DrainAndTick(float dt) { // 第一遍:收集所有命令,立刻处理非游戏帧(connect/disconnect/framework) var deferred = new List(); // 本批游戏帧,延后到建房后处理 while (_cmds.Reader.TryRead(out var cmd)) { if (cmd is FrameCommand fc && fc.Frame.Channel == Channel.Game) deferred.Add(fc); else Process(cmd); } // Poll 匹配器 → 对本 tick 内已凑齐或超时的队列建房 var matches = _matchmaker.Poll(_tick); for (int i = 0; i < matches.Count; i++) { _logger.Info($"match formed {matches[i].GameId}@{matches[i].Version} seats={matches[i].Seats.Count} tick={_tick}"); CreateRoomForSeats(matches[i]); } // 第二遍:处理本批游戏帧(此时 RoomId 已就位) for (int i = 0; i < deferred.Count; i++) { var fc = deferred[i]; Route(fc.PlayerId, fc.Frame); } _tick++; var ended = _host.TickAll(dt); for (int i = 0; i < ended.Count; i++) OnRoomEnded(ended[i].RoomId, ended[i].Result); var expired = _sessions.SweepExpired(_tick, _reconnectWindowTicks); for (int i = 0; i < expired.Count; i++) _logger.Info($"session {expired[i].PlayerId} 重连窗口过期,清除"); } public async Task RunAsync(int tickIntervalMs, CancellationToken ct) => await RunAsync(tickIntervalMs, tickIntervalMs / 1000f, ct); public async Task RunAsync(int tickIntervalMs, float logicalDt, CancellationToken ct) { while (!ct.IsCancellationRequested) { DrainAndTick(logicalDt); try { await Task.Delay(tickIntervalMs, ct).ConfigureAwait(false); } catch (OperationCanceledException) { break; } } } private void Process(ServerCommand cmd) { switch (cmd) { case ConnectCommand cc: _sessions.OnConnect(cc.PlayerId, cc.Connection); _logger.Info($"session connect pid={cc.PlayerId}"); break; case DisconnectCommand dc: _sessions.OnDisconnect(dc.PlayerId, dc.Connection, _tick); _logger.Info($"session disconnect pid={dc.PlayerId}"); if (_queuedPids.Remove(dc.PlayerId)) _matchmaker.Remove(dc.PlayerId); if (_lobbyPlayers.Remove(dc.PlayerId)) BroadcastLobbyState(); break; case FrameCommand fc: Route(fc.PlayerId, fc.Frame); break; } } private void Route(int pid, Frame frame) { if (frame.Channel == Channel.Framework) HandleFramework(pid, frame); else { var s = _sessions.Get(pid); if (s?.RoomId != null) _host.DeliverTo(s.RoomId, pid, new NetMessage(frame.Opcode, frame.Payload)); } } private void HandleFramework(int pid, Frame frame) { FrameworkOpcode op = (FrameworkOpcode)frame.Opcode; if (op != FrameworkOpcode.LobbyMove) { _logger.Info($"framework pid={pid} op={op} bytes={frame.Payload?.Length ?? 0}"); } switch (op) { case FrameworkOpcode.Heartbeat: SendFramework(pid, FrameworkOpcode.Heartbeat, frame.Payload); // 原样回显 break; case FrameworkOpcode.MatchRequest: var req = MatchRequestMsg.Decode(frame.Payload); EnqueueMatch(pid, req.GameId, req.Version); break; case FrameworkOpcode.GameListRequest: SendFramework(pid, FrameworkOpcode.GameListResponse, BuildGameList().Encode()); break; case FrameworkOpcode.RandomMatchRequest: EnqueueRandomMatch(pid); break; case FrameworkOpcode.LobbyJoin: HandleLobbyJoin(pid, frame.Payload); break; case FrameworkOpcode.LobbyMove: HandleLobbyMove(pid, frame.Payload); break; case FrameworkOpcode.LobbyLeave: if (_lobbyPlayers.Remove(pid)) BroadcastLobbyState(); break; } } private void HandleLobbyJoin(int pid, byte[] payload) { var req = LobbyJoinMsg.Decode(payload); string name = string.IsNullOrWhiteSpace(req.Name) ? $"P{pid}" : req.Name; int figure = req.Figure <= 0 ? 1 : req.Figure; if (!_lobbyPlayers.TryGetValue(pid, out var player)) { float lane = (_lobbyPlayers.Count % 6) - 2.5f; player = new LobbyPlayerMsg { PlayerId = pid, Name = name, Figure = figure, X = lane * 1.5f, Y = 0f, Z = 0f, DirX = 0f, DirZ = 1f, Moving = false }; _lobbyPlayers[pid] = player; } else { player.Name = name; player.Figure = figure; } _logger.Info($"lobby join pid={pid} name={name} figure={figure} players={_lobbyPlayers.Count} pids={string.Join(",", _lobbyPlayers.Keys)}"); BroadcastLobbyState(); } private void HandleLobbyMove(int pid, byte[] payload) { if (!_lobbyPlayers.TryGetValue(pid, out var player)) { _logger.Warn($"lobby move ignored pid={pid} reason=not_joined players={_lobbyPlayers.Count} pids={string.Join(",", _lobbyPlayers.Keys)}"); return; } var move = LobbyMoveMsg.Decode(payload); player.X = move.X; player.Y = move.Y; player.Z = move.Z; player.DirX = move.DirX; player.DirZ = move.DirZ; player.Moving = move.Moving; move.PlayerId = pid; byte[] encoded = move.Encode(); foreach (int targetPid in _lobbyPlayers.Keys) { SendFramework(targetPid, FrameworkOpcode.LobbyMove, encoded); } } private void BroadcastLobbyState() { if (_lobbyPlayers.Count == 0) return; var state = new LobbyStateMsg(); foreach (var player in _lobbyPlayers.Values) { state.Players.Add(player); } byte[] payload = state.Encode(); _logger.Info($"lobby state players={_lobbyPlayers.Count} pids={string.Join(",", _lobbyPlayers.Keys)}"); foreach (int pid in _lobbyPlayers.Keys) { SendFramework(pid, FrameworkOpcode.LobbyState, payload); } } private void EnqueueRandomMatch(int pid) { var games = BuildGameList().Games; if (games.Count == 0) { SendFramework(pid, FrameworkOpcode.Error, new ErrorMsg { Code = 1, Message = "no minigame available" }.Encode()); return; } var selected = games[_random.Next(games.Count)]; _logger.Info($"random match pid={pid} assigned {selected.GameId}@{selected.Version}"); SendFramework(pid, FrameworkOpcode.MatchAssigned, new MatchAssignedMsg { GameId = selected.GameId, Version = selected.Version }.Encode()); EnqueueMatch(pid, selected.GameId, selected.Version); } private GameListResponseMsg BuildGameList() { var latest = new Dictionary(StringComparer.OrdinalIgnoreCase); if (!Directory.Exists(_gamesRoot)) return new GameListResponseMsg(); foreach (var gameDir in Directory.GetDirectories(_gamesRoot)) { foreach (var versionDir in Directory.GetDirectories(gameDir)) { string manifestPath = Path.Combine(versionDir, "game.json"); if (!File.Exists(manifestPath)) continue; try { var manifest = GameManifest.Load(manifestPath); if (string.Equals(manifest.GameId, "lobby", StringComparison.OrdinalIgnoreCase)) continue; if (manifest.PlayerCount < 2 || manifest.PlayerCount > 8) continue; if (!latest.TryGetValue(manifest.GameId, out var old) || manifest.Version > old.Version) { latest[manifest.GameId] = new GameInfoMsg { GameId = manifest.GameId, Version = manifest.Version, DisplayName = manifest.GameId, MinPlayers = 2, MaxPlayers = Math.Max(2, Math.Min(8, manifest.PlayerCount)), }; } } catch (Exception ex) { _logger.Warn($"skip invalid game manifest {manifestPath}: {ex.Message}"); } } } var msg = new GameListResponseMsg(); foreach (var game in latest.Values) msg.Games.Add(game); msg.Games.Sort((a, b) => string.Compare(a.GameId, b.GameId, StringComparison.OrdinalIgnoreCase)); return msg; } private void EnqueueMatch(int pid, string gameId, int version) { var s = _sessions.Get(pid); if (s == null) return; // 未建立会话,忽略 if (s.RoomId != null || _queuedPids.Contains(pid)) { // 已在房间内或已在队列中:拒绝 SendFramework(pid, FrameworkOpcode.Error, new ErrorMsg { Code = 2, Message = "already in room or queue" }.Encode()); return; } // 读取游戏的 playerCount(缓存) string cacheKey = $"{gameId}@{version}"; if (!_playerCountCache.TryGetValue(cacheKey, out int playerCount)) { string manifestPath = Path.Combine(_gamesRoot, gameId, version.ToString(), "game.json"); try { var manifest = GameManifest.Load(manifestPath); playerCount = manifest.PlayerCount; _playerCountCache[cacheKey] = playerCount; } catch (Exception ex) { _logger.Error($"读取 game.json 失败 {gameId}@{version}: {ex.Message}"); SendFramework(pid, FrameworkOpcode.Error, new ErrorMsg { Code = 1, Message = "game not found" }.Encode()); return; } } _queuedPids.Add(pid); _matchmaker.Enqueue(pid, gameId, version, playerCount, _tick); _logger.Info($"enqueue match pid={pid} game={gameId}@{version} playerCount={playerCount} tick={_tick}"); } private void CreateRoomForSeats(Matchmaker.Match match) { string roomId = $"r{++_roomSeq}"; var seats = match.Seats; int aiCount = 0; for (int i = 0; i < seats.Count; i++) { if (seats[i].IsAi) aiCount++; } _logger.Info($"create room {roomId} game={match.GameId}@{match.Version} real={seats.Count - aiCount} ai={aiCount}"); // Build PlayerInfo[] for all seats var playerInfos = new PlayerInfo[seats.Count]; var realPids = new List(); for (int i = 0; i < seats.Count; i++) { var seat = seats[i]; playerInfos[i] = new PlayerInfo { PlayerId = seat.PlayerId, Name = seat.IsAi ? "AI" : $"P{seat.PlayerId}", IsAI = seat.IsAi }; if (!seat.IsAi) { realPids.Add(seat.PlayerId); _queuedPids.Remove(seat.PlayerId); } } var sink = new RoomOutputSink(roomId, realPids, _sessions); var cfg = new RoomConfig { GameId = match.GameId, Version = match.Version, Seed = (ulong)_roomSeq, PlayerCount = seats.Count }; Room room; try { room = _host.CreateRoom(roomId, match.GameId, match.Version, cfg, playerInfos, sink); } catch (Exception ex) { _logger.Error($"建房失败 {match.GameId}@{match.Version}: {ex.Message}"); // Notify real players of error var errPayload = new ErrorMsg { Code = 1, Message = "create room failed" }.Encode(); foreach (int rpid in realPids) SendFramework(rpid, FrameworkOpcode.Error, errPayload); return; } // Build MatchFound message with all players info var allPlayers = new List(playerInfos); // For each real player: set session.RoomId + send MatchFound foreach (int rpid in realPids) { var s = _sessions.Get(rpid); if (s != null) s.RoomId = roomId; var found = new MatchFoundMsg { RoomId = roomId, Version = match.Version, SelfPlayerId = rpid }; foreach (var pi in allPlayers) found.Players.Add(pi); SendFramework(rpid, FrameworkOpcode.MatchFound, found.Encode()); } _roomPlayers[roomId] = realPids; if (!room.IsActive) OnRoomEnded(roomId, room.TrustedEndResult); // 极端:Start 即结束 } private void OnRoomEnded(string roomId, RoomEndResult result) { if (!_roomPlayers.TryGetValue(roomId, out var players)) return; _roomPlayers.Remove(roomId); var msg = new RoomEndMsg { WinnerPlayerId = result?.WinnerPlayerId ?? 0, ResultBlob = result?.ResultBlob ?? Array.Empty() }; byte[] payload = msg.Encode(); for (int i = 0; i < players.Count; i++) { int pid = players[i]; SendFramework(pid, FrameworkOpcode.RoomEnd, payload); var s = _sessions.Get(pid); if (s != null) s.RoomId = null; } } private void SendFramework(int pid, FrameworkOpcode op, byte[] payload) { _sessions.GetConnection(pid)?.Send( FrameCodec.Encode(new Frame(Channel.Framework, (ushort)op, payload))); } } }