namespace Supercell.Laser.Server.Logic.Game { using Supercell.Laser.Logic.Battle; using Supercell.Laser.Logic.Battle.Structures; using Supercell.Laser.Logic.Data; using Supercell.Laser.Logic.Home.Items; using Supercell.Laser.Logic.Listener; using Supercell.Laser.Logic.Message.Battle; using Supercell.Laser.Logic.Message.Home; using Supercell.Laser.Logic.Team; using Supercell.Laser.Logic.Util; using Supercell.Laser.Server.Networking; using Supercell.Laser.Server.Networking.Session; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Supercell.Laser.Logic.Ranked; using Supercell.Laser.Logic.Avatar.Structures; public static class Matchmaking { // Fix 8: ConcurrentDictionary — безопасно читается из Parallel.ForEach // и пишется из UpdateSlot (вызывается при обновлении событий). private static ConcurrentDictionary Slots; private static Thread UpdateThread; public static void UpdateSlot(EventData e) { if (e == null || e.Location == null) return; int mode = GameModeUtil.GetGameModeVariation(e.Location.GameMode); Slots[e.Slot] = new MatchmakingSlot(e, GamePlayUtil.GetPlayerCountWithGameModeVariation(mode)); } public static void Init() { Slots = new ConcurrentDictionary(); foreach (EventData e in Events.GetEvents()) { int mode = GameModeUtil.GetGameModeVariation(e.Location.GameMode); // Fix 6: Console.WriteLine заменён на WriteServerLog — не пишет в консоль. Logger.WriteServerLog($"[Matchmaking] Init slot={e.Slot} mode={mode}"); Slots.TryAdd(e.Slot, new MatchmakingSlot(e, GamePlayUtil.GetPlayerCountWithGameModeVariation(mode))); } UpdateThread = new Thread(Update); UpdateThread.IsBackground = true; UpdateThread.Start(); } private static void Update() { while (true) { // Fix 8: параллельный проход по слотам. // Каждый MatchmakingSlot независим; StartGame обращается к: // Battles (ConcurrentDictionary ✓), Sessions (ConcurrentDictionary ✓), // Teams (ConcurrentDictionary ✓ после правки Teams.cs), // UDPGateway.CreateSocket (Interlocked ✓). Parallel.ForEach(Slots.Values, slot => slot.Update()); Thread.Sleep(250); } } public static bool IsBannedRankedMode(int gameModeVariation) { return gameModeVariation == 300; } public static void RequestMatchmake(Connection connection, int slot, long team = -1) { if (!Slots.ContainsKey(slot)) return; connection.MatchmakeSlot = slot; MatchmakingEntry entry = new MatchmakingEntry(connection); entry.PlayerTeamId = team; connection.MatchmakingEntry = entry; Slots[slot].Add(entry); } public static void CancelMatchmake(Connection connection) { int slot = connection.MatchmakeSlot; if (Slots.ContainsKey(slot)) { connection.MatchmakeSlot = -1; Slots[slot].Remove(connection.MatchmakingEntry); if (connection.MatchmakingEntry.PlayerTeamId > 0) { TeamEntry team = Teams.Get(connection.MatchmakingEntry.PlayerTeamId); foreach (TeamMember member in team.Members) { member.IsReady = false; Session session = Sessions.GetSession(member.AccountId); if (session != null) { session.Connection.MatchmakingEntry.PlayerTeamId = -1; CancelMatchmake(session.Connection); MatchMakingCancelledMessage cancelled = new MatchMakingCancelledMessage(); session.Connection.Send(cancelled); } } team.TeamUpdated(); } } } } public class MatchmakingSlot { private Queue RequestQueue; private Queue RemoveQueue; private List Queue; private int PlayersRequired; private EventData EventData; private int SecondsLeft; private int Turns; public const int SEARCH_TIMEOUT = 7; public bool IsRanked => EventData.Slot == 14 || EventData.Slot == 15; public MatchmakingSlot(EventData eventData, int playersRequired) { PlayersRequired = playersRequired; EventData = eventData; Queue = new List(); RequestQueue = new Queue(); RemoveQueue = new Queue(); SecondsLeft = SEARCH_TIMEOUT; } public void Update() { try { if (Sessions.Maintenance) return; if (IsRanked) { int gameModeVariation = GameModeUtil.GetGameModeVariation(EventData.Location.GameModeVariation); if (Matchmaking.IsBannedRankedMode(gameModeVariation)) return; PlayersRequired = GamePlayUtil.GetPlayerCountWithGameModeVariation(gameModeVariation); } else { //int gameModeVariation = GameModeUtil.GetGameModeVariation(EventData.Location.GameModeVariation); // PlayersRequired = gameModeVariation == 6 ? 10 : 6; } foreach (MatchmakingEntry entry in Queue) { if (!entry.Connection.IsOpen) Remove(entry); } while (RemoveQueue.Count > 0) { Queue.Remove(RemoveQueue.Dequeue()); } while (RequestQueue.Count > 0) { MatchmakingEntry entry = RequestQueue.Dequeue(); MatchmakingEntry check = Queue.Find(x => x.Connection == entry.Connection); if (check != null) continue; Queue.Add(entry); } if (Queue.Count == 0) { Turns = 0; SecondsLeft = SEARCH_TIMEOUT; return; } while (Queue.Count >= PlayersRequired) { StartGame(Queue.Take(PlayersRequired).ToList()); Queue.RemoveRange(0, PlayersRequired); } if (Queue.Count > 0 && Queue.Count < PlayersRequired && SecondsLeft <= 0) { SecondsLeft = SEARCH_TIMEOUT; StartGame(Queue.Take(Queue.Count).ToList()); Queue.RemoveRange(0, Queue.Count); } if (Queue.Count > 0) { Turns++; if (Turns >= 4) { Turns = 0; SecondsLeft--; } } else { Turns = 0; SecondsLeft = SEARCH_TIMEOUT; } if (Queue.Count > 0) { MatchMakingStatusMessage message = new MatchMakingStatusMessage(); message.Seconds = SecondsLeft; message.Found = Queue.Count; message.Max = PlayersRequired; message.ShowTips = true; foreach (MatchmakingEntry entry in Queue) { entry.Connection.Messaging.Send(message); } } } catch (Exception e) { // Fix 6: Logger.Error — асинхронная очередь, без Console-лока в hot path. Logger.Error($"GameMatchmakingManager exception! {e.GetType().Name}: {e.Message}\n{e.StackTrace}"); } } public void Remove(MatchmakingEntry entry) { RemoveQueue.Enqueue(entry); } public void Add(MatchmakingEntry entry) { RequestQueue.Enqueue(entry); } /* public async Task StartRankedMatch(List entries) { //.WriteLine($"[Matchmaking] StartRankedMatch: slot={Slot} entries={entries?.Count ?? 0}"); if (entries == null || entries.Count < 1) { //.WriteLine("[Matchmaking] Not enough players for ranked."); return; } // 1. Фильтрация валидных игроков List validEntries = new List(); foreach (var entry in entries) { if (entry?.Connection?.IsOpen == true && entry.Connection.Avatar != null) { validEntries.Add(entry); } } if (validEntries.Count < 1) return; // 2. Инициализация матча RankedMatch ranked = new RankedMatch(false); ranked.MatchId = RankedMatchRegulator.Add(ranked); ranked.IsSoloMatch = (this.EventData.Slot == 14); // Массив для строгого распределения по слотам (0-5) mp_thestealdev[] finalSlots = new mp_thestealdev[6]; Random random = new Random(); // 3. Распределяем реальных игроков (1-й в синюю, 2-й в красную) for (int i = 0; i < validEntries.Count; i++) { var entry = validEntries[i]; int assignedTeam; int assignedGlobalIndex; if (i == 0) // Первый игрок (Ты) { assignedTeam = 0; assignedGlobalIndex = 0; // Первый слот синих } else if (i == 1) // Второй игрок (Твой друг) { assignedTeam = 1; assignedGlobalIndex = 3; // Первый слот красных (напротив тебя) } else { // Если игроков больше двух, заполняем свободные слоты по очереди assignedGlobalIndex = (i < 3) ? i : (i + 1); if (assignedGlobalIndex > 5) break; assignedTeam = assignedGlobalIndex < 3 ? 0 : 1; } entry.Connection.Avatar.RankedId = Convert.ToInt32(ranked.MatchId); var player = mp_thestealdev.CreateRankedPlayer( entry.Connection.Avatar, entry.Connection.Home, assignedTeam, assignedGlobalIndex ); finalSlots[assignedGlobalIndex] = player; ranked.AddMatchPlayer(player); //.WriteLine($"[Matchmaking] Added REAL player {entry.Connection.Avatar.Name} to Slot {assignedGlobalIndex} (Team {assignedTeam})"); } // 4. Заполняем оставшиеся "дырки" ботами for (int i = 0; i < 6; i++) { if (finalSlots[i] == null) { // Создаем бота-пустышку var bot = new mp_thestealdev(); bot.AccountId = 0; // Метка бота bot.DisplayData = new PlayerDisplayData(false, 0, 0, $"BOT {i + 1}"); bot.GlobalIndex = i; bot.TeamIndex = (i < 3) ? 0 : 1; bot.SlotIndex = (bot.TeamIndex == 0) ? i : (i - 3); // Рандомный боец для визуализации в лобби // int randomBrawlerId = botBrawlers[random.Next(botBrawlers.Length)]; // bot.CharacterPreview = 16000000 + randomBrawlerId; // Важно для логики RankedMatch: боты "уже выбрали" // bot.HasPickedCharacter = true; // bot.HasBanned = true; // bot.BanState = 1; // bot.PickState = 1; finalSlots[i] = bot; ranked.AddMatchPlayer(bot); //.WriteLine($"[Matchmaking] Filled Slot {i} with BOT:"); //.WriteLine($" -> Name: {bot.DisplayData.Name}"); //.WriteLine($" -> Team: {bot.TeamIndex} ({(bot.TeamIndex == 0 ? "Blue" : "Red")})"); //.WriteLine($" -> GlobalIndex: {bot.GlobalIndex}"); //.WriteLine($" -> SlotInsideTeam: {bot.SlotIndex}"); } } // 5. Запуск логики матча (таймеры, пакеты старта) try { ranked.Start(); //.WriteLine($"[Matchmaking] Ranked Match {ranked.MatchId} successfully started with {ranked.Players.Count} participants."); } catch (Exception ex) { //.WriteLine($"[Matchmaking] Error starting ranked: {ex.Message}"); } } */ /* public async Task StartRankedMatch(List entries) { if (entries == null || entries.Count < 2) return; var validEntries = new List(); foreach (var entry in entries) { if (entry?.Connection?.IsOpen == true && entry.Connection.Avatar != null) validEntries.Add(entry); else if (entry != null) Remove(entry); } if (validEntries.Count < 2) return; RankedMatch ranked = new RankedMatch(false); ranked.MatchId = RankedMatchRegulator.Add(ranked); ranked.IsSoloMatch = this.EventData.Slot == 14; var e1 = validEntries[0]; var e2 = validEntries[1]; e1.Connection.Avatar.RankedId = (int)ranked.MatchId; e2.Connection.Avatar.RankedId = (int)ranked.MatchId; var p1 = mp_thestealdev.CreateRankedPlayer( e1.Connection.Avatar, e1.Connection.Home, 0, 0 ); var p2 = mp_thestealdev.CreateRankedPlayer( e2.Connection.Avatar, e2.Connection.Home, 1, 3 ); ranked.AddMatchPlayer(p1); ranked.AddMatchPlayer(p2); try { ranked.Start(); } catch { Remove(e1); Remove(e2); } }*/ /* public async Task StartRankedMatch(List entries) { if (entries == null || entries.Count < 1) return; List validEntries = new List(); foreach (var entry in entries) { if (entry?.Connection?.IsOpen == true && entry.Connection.Avatar != null) validEntries.Add(entry); else if (entry != null) Remove(entry); } RankedMatch ranked = new RankedMatch(false); ranked.MatchId = RankedMatchRegulator.Add(ranked); ranked.IsSoloMatch = (this.EventData.Slot == 14); var groups = validEntries .GroupBy(x => x.PlayerTeamId) .OrderByDescending(g => g.Count()) .ToList(); List team0 = new List(); List team1 = new List(); foreach (var group in groups) { foreach (var p in group) { if (team0.Count < 3) team0.Add(p); else if (team1.Count < 3) team1.Add(p); } } int globalIndex = 0; for (int i = 0; i < team0.Count; i++) { var entry = team0[i]; entry.Connection.Avatar.RankedId = Convert.ToInt32(ranked.MatchId); var player = mp_thestealdev.CreateRankedPlayer( entry.Connection.Avatar, entry.Connection.Home, 0, globalIndex++); ranked.AddMatchPlayer(player); } for (int i = 0; i < team1.Count; i++) { var entry = team1[i]; entry.Connection.Avatar.RankedId = Convert.ToInt32(ranked.MatchId); var player = mp_thestealdev.CreateRankedPlayer( entry.Connection.Avatar, entry.Connection.Home, 1, globalIndex++); ranked.AddMatchPlayer(player); } try { ranked.Start(); } catch { foreach (var entry in validEntries) { if (entry != null) Remove(entry); } } }*/ public async Task StartRankedMatch(List entries) { if (entries == null || entries.Count < 1) return; List validEntries = new List(); foreach (var entry in entries) { if (entry?.Connection?.IsOpen == true && entry.Connection.Avatar != null) validEntries.Add(entry); else if (entry != null) Remove(entry); } RankedMatch ranked = new RankedMatch(false); ranked.MatchId = RankedMatchRegulator.Add(ranked); ranked.IsSoloMatch = (this.EventData.Slot == 14); List team0 = new(); List team1 = new(); Dictionary partyTeam = new Dictionary(); int toggle = 0; foreach (var entry in validEntries) { int assignedTeam; if (entry.PlayerTeamId > 0 && partyTeam.ContainsKey(entry.PlayerTeamId)) { assignedTeam = partyTeam[entry.PlayerTeamId]; } else { assignedTeam = toggle % 2; toggle++; if (entry.PlayerTeamId > 0) partyTeam[entry.PlayerTeamId] = assignedTeam; } if (assignedTeam == 0) team0.Add(entry); else team1.Add(entry); } int globalIndex = 0; for (int i = 0; i < team0.Count; i++) { var entry = team0[i]; entry.Connection.Avatar.RankedId = Convert.ToInt32(ranked.MatchId); var player = mp_thestealdev.CreateRankedPlayer( entry.Connection.Avatar, entry.Connection.Home, 0, i); player.GlobalIndex = globalIndex++; ranked.AddMatchPlayer(player); } for (int i = 0; i < team1.Count; i++) { var entry = team1[i]; entry.Connection.Avatar.RankedId = Convert.ToInt32(ranked.MatchId); var player = mp_thestealdev.CreateRankedPlayer( entry.Connection.Avatar, entry.Connection.Home, 1, i); player.GlobalIndex = globalIndex++; ranked.AddMatchPlayer(player); } try { ranked.Start(); } catch { foreach (var entry in validEntries) { if (entry != null) Remove(entry); } } } // Вспомогательный метод для создания бота (добавьте в класс MatchmakingSlot) private mp_thestealdev CreateRankedBotPlayer(int botCharacterGlobalId, int globalIndex, int teamIndex, int rankedId) { // Создаём фейкового бота var botPlayer = new mp_thestealdev(); // Заполняем фейковые данные botPlayer.AccountId = -Math.Abs(globalIndex + 1); // Отрицательный ID для ботов botPlayer.GlobalIndex = globalIndex; botPlayer.TeamIndex = teamIndex; botPlayer.Position = globalIndex; botPlayer.CharacterPreview = botCharacterGlobalId; // botPlayer.HasPickedCharacter = true; // Бот уже "выбрал" персонажа // botPlayer.HasBanned = true; // Бот "забанил" случайного персонажа // botPlayer.BannedBrawler = 0; // botPlayer.BanState = 0; // botPlayer.PickState = 1; // Фейковый DisplayData для бота botPlayer.DisplayData = new PlayerDisplayData(false, 0, 0, $"Bot_{globalIndex}"); // Получаем данные персонажа для бота var charData = DataTables.Get(16).GetDataByGlobalId(botCharacterGlobalId); if (charData != null) { // Можно добавить логику для выбора скина/гаджета } return botPlayer; } public static readonly int[] botBrawlers = new int[] { 0, 1, 3, 9, 28 }; // Allowed public void StartGame(List entries) { // The battle inits here // TODO: Merge it to BattleManager or smth.. if(IsRanked) { StartRankedMatch(entries); return; } BattleMode battle = new BattleMode(EventData.LocationId); if (battle.Location == null) { Logger.Error($"[Matchmaking] StartGame: Location is null, LocationId={EventData.LocationId}, slot={EventData.Slot}"); return; } if (EventData.BattlePlayerMap != null) battle.SetPlayerMap(EventData.BattlePlayerMap); battle.Id = Battles.Add(battle); Random rand = new Random(); List sortedEntries = new List(); Dictionary> teamEntries = new Dictionary>(); if (GameModeUtil.HasTwoTeams(battle.GetGameModeVariation())) { MatchmakingEntry[] entriesWithTeam = entries.FindAll(entry => entry.PlayerTeamId > 0).ToArray(); for (int i = 0; i < entriesWithTeam.Length; i++) { MatchmakingEntry entry = entriesWithTeam[i]; long teamId = entry.PlayerTeamId; if (!teamEntries.ContainsKey(teamId)) teamEntries.Add(teamId, new List()); teamEntries[teamId].Add(entry); entries.Remove(entry); } long[] ids = teamEntries.Keys.ToArray(); for (int i = 0; i < ids.Length; i++) { List teamedEntries = teamEntries[ids[i]]; for (int j = 0; j < teamedEntries.Count; j++) { sortedEntries.Add(teamedEntries[j]); teamedEntries[j].PrefferedTeam = i; } } for (int i = 0; i < entries.Count; i++) { sortedEntries.Add(entries[i]); } for (int i = 0; i < sortedEntries.Count; i++) { UDPSocket socket = UDPGateway.CreateSocket(); socket.TCPConnection = sortedEntries[i].Connection; socket.Battle = battle; sortedEntries[i].Connection.UdpSessionId = socket.SessionId; int teamIndex = i % 2; if (sortedEntries[i].PrefferedTeam != -1) { teamIndex = sortedEntries[i].PrefferedTeam; } if (battle.GetTeamPlayersCount(teamIndex) >= 3) { if (teamIndex == 1) teamIndex = 0; else teamIndex = 1; } BattlePlayer player = BattlePlayer.Create(sortedEntries[i].Connection.Home, sortedEntries[i].Connection.Avatar, i, teamIndex); player.TeamId = sortedEntries[i].PlayerTeamId; sortedEntries[i].Player = player; battle.AddPlayer(player, sortedEntries[i].Connection.UdpSessionId); } List team1BotsCharacters = new List(); List team2BotsCharacters = new List(); for (int i = sortedEntries.Count; i < battle.GetPlayersCountWithGameModeVariation(); i++) { bool isBotValid = false; int botCharacter = -1; while (!isBotValid) { botCharacter = 16000000 + botBrawlers[rand.Next(0, botBrawlers.Length)]; if (i % 2 == 0) { isBotValid = !team1BotsCharacters.Contains(botCharacter); } else { isBotValid = !team2BotsCharacters.Contains(botCharacter); } if (!isBotValid) isBotValid = !GameModeUtil.HasTwoTeams(battle.GetGameModeVariation()); } int teamIndex = i % 2; if (battle.GetTeamPlayersCount(teamIndex) >= 3) { if (teamIndex == 1) teamIndex = 0; else teamIndex = 1; } if (teamIndex == 0) { team1BotsCharacters.Add(botCharacter); } else { team2BotsCharacters.Add(botCharacter); } CharacterData data = DataTables.Get(16).GetDataByGlobalId(botCharacter); BattlePlayer bot = BattlePlayer.CreateBotInfo(data.ItemName.ToUpper(), i, teamIndex, botCharacter); battle.AddPlayer(bot, -1); } } else { if (battle.GetGameModeVariation() == 9) { List withTeam = entries.Where(e => e.PlayerTeamId > 0).ToList(); List solo = entries.Where(e => e.PlayerTeamId <= 0).ToList(); Dictionary> grouped = new Dictionary>(); foreach (var e in withTeam) { if (!grouped.ContainsKey(e.PlayerTeamId)) grouped[e.PlayerTeamId] = new List(); grouped[e.PlayerTeamId].Add(e); } sortedEntries = new List(); foreach (var group in grouped.Values) sortedEntries.AddRange(group); sortedEntries.AddRange(solo); } else { sortedEntries = entries; } for (int i = 0; i < sortedEntries.Count; i++) { UDPSocket socket = UDPGateway.CreateSocket(); socket.TCPConnection = sortedEntries[i].Connection; socket.Battle = battle; sortedEntries[i].Connection.UdpSessionId = socket.SessionId; int teamIndex = battle.GetGameModeVariation() == 9 ? i / 2 : i; BattlePlayer player = BattlePlayer.Create(sortedEntries[i].Connection.Home, sortedEntries[i].Connection.Avatar, i, teamIndex); player.TeamId = sortedEntries[i].PlayerTeamId; sortedEntries[i].Player = player; battle.AddPlayer(player, sortedEntries[i].Connection.UdpSessionId); } for (int i = sortedEntries.Count; i < battle.GetPlayersCountWithGameModeVariation(); i++) { int botCharacter = 16000000 + botBrawlers[rand.Next(0, botBrawlers.Length)]; int teamIndex = battle.GetGameModeVariation() == 9 ? i / 2 : i; CharacterData data = DataTables.Get(16).GetDataByGlobalId(botCharacter); BattlePlayer bot = BattlePlayer.CreateBotInfo(data.ItemName.ToUpper(), i, teamIndex, botCharacter); battle.AddPlayer(bot, -1); } } battle.AddGameObjects(); for (int i = 0; i < sortedEntries.Count; i++) { StartLoadingMessage startLoading = new StartLoadingMessage(); startLoading.LocationId = battle.Location.GetGlobalId(); startLoading.TeamIndex = sortedEntries[i].Player.TeamIndex; startLoading.OwnIndex = sortedEntries[i].Player.PlayerIndex; startLoading.GameMode = battle.GetGameModeVariation() == 6 ? 6 : battle.GetGameModeVariation() == 9 ? 9 : 1; startLoading.SetPlayerMap(battle.BattlePlayerMap); sortedEntries[i].Connection.Avatar.UdpSessionId = sortedEntries[i].Connection.UdpSessionId; startLoading.Players.AddRange(battle.GetPlayers()); sortedEntries[i].Connection.Send(startLoading); battle.BattleWithTrophies = true; } battle.Start(); } } public class MatchmakingEntry { public readonly Connection Connection; public BattlePlayer Player; public long PlayerTeamId; public int PrefferedTeam; public MatchmakingEntry(Connection connection) { PrefferedTeam = -1; this.Connection = connection; } } }