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.Data.Helper; 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.Generic; using System.Linq; public static class Matchmaking { private static Dictionary Slots; private static Thread UpdateThread; public static void Init() { Slots = new Dictionary(); foreach (EventData e in Events.GetEvents()) { int mode = GameModeUtil.GetGameModeVariation(e.Location.GameMode); Slots.Add(e.Slot, new MatchmakingSlot(e, GamePlayUtil.GetPlayerCountWithGameModeVariation(mode))); // players count } UpdateThread = new Thread(Update); UpdateThread.Start(); } private static void Update() { while (true) { foreach (MatchmakingSlot slot in Slots.Values) { slot.Update(); } Thread.Sleep(250); } } 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 (int found, int max, int seconds) GetQueueStatus(int slot) { if (!Slots.ContainsKey(slot)) return (0, 0, 0); var s = Slots[slot]; return (s.QueueCount, s.PlayersRequiredPublic, s.SecondsLeftPublic); } public static int GetTotalQueueCount() { return Slots?.Values.Sum(slot => slot.QueueCount) ?? 0; } public static int SlotCount { get { return Slots?.Count ?? 0; } } 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 static readonly int[] AllowedBotBrawlers = new[] { 0, 5, 1, 10, 15, 8, 24, 28, 30 }; private static readonly Dictionary> BotSkinPoolByCharacter = new Dictionary>(); private Queue RequestQueue; private Queue RemoveQueue; private List Queue; private int PlayersRequired; private EventData EventData; private int SecondsLeft; private int Turns; public int PlayersRequiredPublic => PlayersRequired; public int SecondsLeftPublic => SecondsLeft; public int QueueCount => Queue.Count; public const int SEARCH_TIMEOUT = 10; private static BattlePlayer CreateBotPlayerSafe(int[] botBrawlers, Random rand, int playerIndex, int teamIndex, int? forcedCharacter = null) { if (botBrawlers == null || botBrawlers.Length == 0) { Logger.Warning("Matchmaking bot list is empty."); return BattlePlayer.CreateBotInfo("BOT", playerIndex, teamIndex); } List candidateCharacters = new List(); if (forcedCharacter.HasValue && forcedCharacter.Value > 0) { candidateCharacters.Add(forcedCharacter.Value); } foreach (int instanceId in botBrawlers.OrderBy(_ => rand.Next())) { int botCharacter = GlobalId.CreateGlobalId((int)DataType.Character, instanceId); if (!candidateCharacters.Contains(botCharacter)) { candidateCharacters.Add(botCharacter); } } foreach (int botCharacter in candidateCharacters) { CharacterData data = DataTables.Get(16).GetDataByGlobalId(botCharacter); if (data == null) { Logger.Warning($"Matchmaking bot character missing in DataTables: {botCharacter}"); continue; } string botName = data.ItemName; if (string.IsNullOrWhiteSpace(botName)) { botName = data.Name; } if (string.IsNullOrWhiteSpace(botName)) { botName = $"BOT_{botCharacter}"; } int skinId = ResolveBotSkinId(data, rand); return BattlePlayer.CreateBotInfo(botName.ToUpperInvariant(), playerIndex, teamIndex, botCharacter, skinId); } CharacterData fallback = DataTables.Get(16).GetDataByGlobalId(16000000); if (fallback == null) { Logger.Warning("Matchmaking bot fallback character 16000000 is missing in DataTables."); return BattlePlayer.CreateBotInfo("BOT", playerIndex, teamIndex); } string fallbackName = fallback.ItemName; if (string.IsNullOrWhiteSpace(fallbackName)) { fallbackName = fallback.Name; } if (string.IsNullOrWhiteSpace(fallbackName)) { fallbackName = "BOT"; } return BattlePlayer.CreateBotInfo(fallbackName.ToUpperInvariant(), playerIndex, teamIndex, fallback.GetGlobalId(), ResolveBotSkinId(fallback, rand)); } private static int ResolveBotSkinId(CharacterData character, Random rand) { if (character == null) { return 0; } if (!BotSkinPoolByCharacter.TryGetValue(character.GetGlobalId(), out List skins)) { skins = DataTables.Get(DataType.Skin) .GetDatas() .OfType() .Where(skin => IsBotSkinCandidate(skin, character)) .Select(skin => skin.GetGlobalId()) .Distinct() .ToList(); if (skins.Count == 0 && !string.IsNullOrWhiteSpace(character.DefaultSkin)) { SkinData defaultSkin = DataTables.Get(DataType.Skin).GetData(character.DefaultSkin); if (defaultSkin != null) { skins.Add(defaultSkin.GetGlobalId()); } } BotSkinPoolByCharacter[character.GetGlobalId()] = skins; } return skins.Count > 0 ? skins[rand.Next(skins.Count)] : 0; } private static bool IsBotSkinCandidate(SkinData skin, CharacterData character) { if (skin == null || character == null || string.IsNullOrWhiteSpace(skin.Name) || string.IsNullOrWhiteSpace(skin.Conf)) { return false; } SkinConfData skinConf = DataTables.Get(DataType.SkinConf).GetData(skin.Conf); if (skinConf == null || string.IsNullOrWhiteSpace(skinConf.Character) || !string.Equals(skinConf.Character, character.Name, StringComparison.Ordinal)) { return false; } if (skin.Name.EndsWith("Silver", StringComparison.OrdinalIgnoreCase) || skin.Name.EndsWith("Gold", StringComparison.OrdinalIgnoreCase)) { return false; } return !skin.Name.EndsWith("Default", StringComparison.OrdinalIgnoreCase); } public MatchmakingSlot(EventData eventData, int playersRequired) { PlayersRequired = PlayersRequired = playersRequired > 0 ? playersRequired : 6; EventData = eventData; Queue = new List(); RequestQueue = new Queue(); RemoveQueue = new Queue(); SecondsLeft = SEARCH_TIMEOUT; } public void Update() { try { if (Sessions.Maintenance) return; 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); } while (Queue.Count >= PlayersRequired) { var batch = Queue.Take(PlayersRequired).ToList(); Queue.RemoveRange(0, PlayersRequired); try { StartGame(batch); } catch (Exception ex) { Logger.Exception("StartGame failed", ex); } } if (Queue.Count > 0 && Queue.Count < PlayersRequired && SecondsLeft <= 0) { SecondsLeft = SEARCH_TIMEOUT; var batch = Queue.Take(Queue.Count).ToList(); Queue.RemoveRange(0, batch.Count); if (batch.Count > 0) { try { StartGame(batch); } catch (Exception ex) { Logger.Exception("StartGame (timeout) failed", ex); Console.WriteLine($"[MATCHMAKING] StartGame exception: {ex}"); } } } 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) { Logger.Exception("GameMatchmakingManager exception", e); } } public void Remove(MatchmakingEntry entry) { RemoveQueue.Enqueue(entry); } public void Add(MatchmakingEntry entry) { if (RequestQueue.Count == 0 && Queue.Count == 0) SecondsLeft = SEARCH_TIMEOUT; RequestQueue.Enqueue(entry); } int[] botBrawlers = AllowedBotBrawlers; public void StartGame(List entries) { // Remove players who cancelled or disconnected since batch was taken entries = entries.Where(e => e.Connection.IsOpen && e.Connection.MatchmakeSlot >= 0).ToList(); if (entries.Count == 0) return; BattleMode battle = new BattleMode(EventData.LocationId); battle.Id = Battles.Add(battle); AuditLogger.BattleCreated(battle.Id, EventData.LocationId, entries.Count, EventData.Location?.GameMode ?? "?"); 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++) { int teamIndex = i % 2; if (battle.GetTeamPlayersCount(teamIndex) >= 3) teamIndex = teamIndex == 1 ? 0 : 1; List teamBots = teamIndex == 0 ? team1BotsCharacters : team2BotsCharacters; // pick a bot not already on this team, fall back to any if all used int botCharacter = -1; foreach (int b in botBrawlers.OrderBy(_ => rand.Next())) { int candidate = 16000000 + b; if (!teamBots.Contains(candidate)) { botCharacter = candidate; break; } } if (botCharacter == -1) botCharacter = 16000000 + botBrawlers[rand.Next(0, botBrawlers.Length)]; teamBots.Add(botCharacter); BattlePlayer bot = CreateBotPlayerSafe(botBrawlers, rand, i, teamIndex, botCharacter); if (bot != null) battle.AddPlayer(bot, -1); } } else { sortedEntries = entries; for (int i = 0; i < entries.Count; i++) { UDPSocket socket = UDPGateway.CreateSocket(); socket.TCPConnection = sortedEntries[i].Connection; socket.Battle = battle; sortedEntries[i].Connection.UdpSessionId = socket.SessionId; int teamIndex = 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 = entries.Count; i < battle.GetPlayersCountWithGameModeVariation(); i++) { int botCharacter = GlobalId.CreateGlobalId((int)DataType.Character, botBrawlers[rand.Next(0, botBrawlers.Length)]); int teamIndex = i; BattlePlayer bot = CreateBotPlayerSafe(botBrawlers, rand, i, teamIndex, botCharacter); if (bot != null) { 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 || battle.GetGameModeVariation() == 9) ? 6 : 1; sortedEntries[i].Connection.Avatar.UdpSessionId = sortedEntries[i].Connection.UdpSessionId; startLoading.Players.AddRange(battle.GetPlayers()); sortedEntries[i].Connection.Send(startLoading); } 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; } } }