using Newtonsoft.Json; using Supercell.Laser.Logic.Avatar; using Supercell.Laser.Logic.Avatar.Structures; using Supercell.Laser.Logic.Battle; using Supercell.Laser.Logic.Data; using Supercell.Laser.Logic.Data.Helper; using Supercell.Laser.Logic.Helper; using Supercell.Laser.Logic.Home; using Supercell.Laser.Logic.Home.Items; using Supercell.Laser.Logic.Home.Structures; using Supercell.Laser.Logic.Listener; using Supercell.Laser.Logic.Message; using Supercell.Laser.Logic.Message.Home; using Supercell.Laser.Logic.Message.Ranked; using Supercell.Laser.Logic.Time; using Supercell.Laser.Titan.DataStream; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Supercell.Laser.Logic.Ranked { public class RankedMatch { private enum MatchState { WaitingToStart, BanPhase, PickPhase, BattleStarting, BattleInProgress, Finished } private long _matchId; private List _players; private Timer _timer; private bool _isTerminated; private int _startTick; private GameTime _gameTime; private int _preparationStartTick; private int _restartTicks = -1; private bool _isBattleStarting; private readonly object _syncLock = new(); private static readonly Random Random = new(); private MatchState _state; private List _banOrder; private int _banStartTick; private List _pickOrder; private int _pickIndex; private int _pickStartTick; public int TimePerPick = 15 * 20; public int TimePerBan = 15 * 20; private int _currentMapId; private int _teamServeOrder; private int _finalPrepStartTick; private bool _roundFinished; private bool _isRestarting; private int _lastTerminateTick; public bool IsStopped { get; private set; } public long MatchId { get => _matchId; set => _matchId = value; } public long CurrentBattleId { get; set; } public bool IsSoloMatch { get; set; } public List Players => _players; public bool IsPickActive => _state == MatchState.PickPhase; public int RedWins { get; set; } public int BlueWins { get; set; } public int CurrentRound { get; set; } = 1; public bool MatchOver => RedWins >= 2 || BlueWins >= 2; public RankedMatch(bool isFastPickup) { _gameTime = new(); _players = new(); _teamServeOrder = Random.Next(2); _state = MatchState.WaitingToStart; } public void Start() { _startTick = GetTicks(); _timer = new Timer(OnTimerTick, null, 0, 50); Console.WriteLine($"[RankedMatch] Started with _startTick={_startTick}"); } public void Stop() { if (IsStopped) return; IsStopped = true; _timer?.Dispose(); } public void AddMatchPlayer(mp_thestealdev player) => _players.Add(player); public mp_thestealdev GetPlayer(long id) => _players.Find(p => p.AccountId == id); private void OnTimerTick(object state) => Tick(); public void OnRoundFinished(int winnerTeam) { lock (_syncLock) { if (_roundFinished) return; _roundFinished = true; _isRestarting = false; if (winnerTeam == 0) BlueWins++; else if (winnerTeam == 1) RedWins++; if (!MatchOver) { CurrentRound++; _restartTicks = GetTicks() + 260; } } } public mp_thestealdev GetExpectedPicker() { lock (_syncLock) { if (_state == MatchState.PickPhase && _pickIndex < _pickOrder.Count) { int global = _pickOrder[_pickIndex]; var p = _players.FirstOrDefault(p => p.GlobalIndex == global); return p; } return null; } } public bool BanHero(mp_thestealdev player, int brawlerId, bool confirm) { lock (_syncLock) { if (_state != MatchState.BanPhase) return false; if (player.SlotIndex != 0 || player.HasBanned) return false; int globalHero = GlobalId.CreateGlobalId(16, brawlerId); if (confirm) { if (_players.Any(p => p.HasBanned && p.BannedBrawler == globalHero)) return false; player.BannedBrawler = globalHero; player.HasBanned = true; player.BanState = 0; Send(new RankedMatchBanHeroResponseMessage { PlayerIndex = player.GlobalIndex, BannedCharacter = globalHero, BannerAccountId = player.AccountId, Success = true }); if (_players.Where(p => p.SlotIndex == 0).All(p => p.HasBanned)) { EndBanPhase(); } } else { player.CharacterPreview = globalHero; Send(new RankedMatchHeroDataUpdatedMessage { Player = player }); } return true; } } public bool PickHero(mp_thestealdev player, int brawlerId, bool confirm) { lock (_syncLock) { Console.WriteLine($"[PickHero] confirm={confirm}, player={player.AccountId}, state={_state}, pickIndex={_pickIndex}, pickOrder={string.Join(",", _pickOrder)}"); // return false; if (_state != MatchState.PickPhase) { Console.WriteLine("[PickHero] not in PickPhase"); return false; } var expected = GetExpectedPicker(); if (expected == null) { Console.WriteLine("[PickHero] expected is null"); return false; } if (expected.AccountId != player.AccountId) { Console.WriteLine($"[PickHero] wrong player: expected={expected.AccountId}, got={player.AccountId}"); return false; } if (player.HasPickedCharacter) { Console.WriteLine("[PickHero] already picked"); return false; } int globalHero = GlobalId.GetClassId(brawlerId) == 16 ? brawlerId : GlobalId.CreateGlobalId(16, brawlerId); if (confirm) { bool hasHero = player.Avatar.HasHero(globalHero) || player.Avatar.HasHero(brawlerId); if (!hasHero) { Console.WriteLine("[PickHero] hero not owned"); return false; } if (_players.Any(p => p.HasBanned && p.BannedBrawler == globalHero)) { Console.WriteLine("[PickHero] hero is banned"); return false; } if (_players.Any(p => p.HasPickedCharacter && p.CharacterPreview == globalHero)) { Console.WriteLine("[PickHero] hero already picked"); return false; } player.CharacterPreview = globalHero; player.HasPickedCharacter = true; player.IsPickingLocked = true; Send(new RankedMatchHeroPickedMessage { Player = player }); SendPickUpdateToAll(); _pickIndex++; if (_pickIndex >= _pickOrder.Count) { EndPickPhase(); } else { _pickStartTick = GetTicks(); UpdatePickStates(); SendPickStarted(); } Console.WriteLine($"[PickHero] pick successful, new pickIndex={_pickIndex}"); } else { player.CharacterPreview = globalHero; Send(new RankedMatchHeroDataUpdatedMessage { Player = player }); } return true; } } private void SendPickUpdateToAll() { foreach (var p in _players) { SendToSingle(p, new RankedMatchHeroDataUpdatedMessage { Player = p }); } } public void Tick() { if (IsStopped) return; _gameTime.IncreaseTick(); lock (_syncLock) { int currentTicks = GetTicks(); if (_state == MatchState.WaitingToStart) { if (currentTicks == 10) { InitializeMatch(); } else if (currentTicks == 180) { StartBanPhase(); } return; } if (_restartTicks != -1 && currentTicks >= _restartTicks) { if (!_isRestarting) { _isRestarting = true; LogicServerListener.Instance.StartMatchBattle(_players, null, _currentMapId, (int)_matchId, 0, false); _restartTicks = -1; _roundFinished = false; } return; } if (currentTicks - _lastTerminateTick >= 30) { _lastTerminateTick = currentTicks; TerminateTick(); } if (_state == MatchState.BanPhase) { int elapsed = currentTicks - _banStartTick; if (elapsed >= TimePerBan) { // AutoBan(); } } else if (_state == MatchState.PickPhase) { int elapsed = currentTicks - _pickStartTick; if (elapsed >= TimePerPick) { // AutoPick(); } } else if (_state == MatchState.BattleStarting) { if (currentTicks >= _finalPrepStartTick + 200) { _state = MatchState.BattleInProgress; LogicServerListener.Instance.StartMatchBattle(_players, null, _currentMapId, (int)_matchId, 0, false); } } } } private void TerminateTick() { if (_state == MatchState.Finished || _state == MatchState.BattleInProgress) return; int currentTicks = GetTicks(); foreach (var p in _players) { bool disconnected = p.Home?.HomeMode?.GameListener == null; if (disconnected) { TerminateError(1, p.DisplayData?.Name ?? "Unknown"); return; } } if (_state == MatchState.BanPhase) { int elapsed = currentTicks - _banStartTick; if (elapsed < TimePerBan) return; var captains = _players.Where(p => p.SlotIndex == 0 && !p.HasBanned).ToList(); foreach (var captain in captains) { if (captain.CharacterPreview != 0) { int hero = GlobalId.GetInstanceId(captain.CharacterPreview); BanHero(captain, hero, true); } else { TerminateError(2, captain.DisplayData?.Name ?? "Unknown"); return; } } } else if (_state == MatchState.PickPhase) { int elapsed = currentTicks - _pickStartTick; if (elapsed < TimePerPick) return; var current = GetExpectedPicker(); if (current != null && !current.HasPickedCharacter) { if (current.CharacterPreview != 0) { int hero = GlobalId.GetInstanceId(current.CharacterPreview); PickHero(current, hero, true); } else { TerminateError(2, current.DisplayData?.Name ?? "Unknown"); return; } } } } private void InitializeMatch() { _currentMapId = CalculateRandomMap(); BuildBanOrder(); BuildPickOrder(); var ordered = _players.OrderBy(p => p.GlobalIndex).ToList(); foreach (var player in _players) { var startMessage = new RankedMatchStartedMessage { Players = ordered, SelectedMap = GlobalId.GetInstanceId(_currentMapId), Team = _teamServeOrder, MatchId = _matchId, OwnGlobalIndex = player.GlobalIndex }; SendToSingle(player, startMessage); } Console.WriteLine($"[RankedMatch] InitializeMatch completed for MatchId={_matchId}, Players={_players.Count}"); } private void BuildBanOrder() { var captains = _players.Where(p => p.SlotIndex == 0).Select(p => p.GlobalIndex).ToList(); _banOrder = new List(); _banOrder.AddRange(captains); Console.WriteLine($"[RankedMatch] BanOrder = [{string.Join(",", _banOrder)}]"); } private void BuildPickOrder() { var team0 = _players.Where(p => p.TeamIndex == 0).OrderBy(p => p.SlotIndex).Select(p => p.GlobalIndex).ToList(); var team1 = _players.Where(p => p.TeamIndex == 1).OrderBy(p => p.SlotIndex).Select(p => p.GlobalIndex).ToList(); _pickOrder = new List(); if (team0.Count > 0 && team1.Count > 0) { _pickOrder.Add(team0[0]); for (int i = 0; i < team1.Count; i++) _pickOrder.Add(team1[i]); for (int i = 1; i < team0.Count; i++) _pickOrder.Add(team0[i]); } else { _pickOrder.AddRange(team0); _pickOrder.AddRange(team1); } Console.WriteLine($"[RankedMatch] PickOrder = [{string.Join(",", _pickOrder)}]"); } private void StartBanPhase() { if (_banOrder.Count == 0) { Console.WriteLine("[RankedMatch] No banners, skip BanPhase"); EndBanPhase(); return; } _state = MatchState.BanPhase; _banStartTick = GetTicks(); foreach (var p in _players) { p.HasBanned = false; p.BannedBrawler = 0; p.BanState = _banOrder.Contains(p.GlobalIndex) ? 1 : 0; } Send(new RankedMatchBanStartedMessage { TimeLeft = TimePerBan, QueueIndexer = -1 }); Console.WriteLine($"[RankedMatch] BanPhase started for MatchId={_matchId}"); } private void AutoBan() { var captains = _players.Where(p => p.SlotIndex == 0 && !p.HasBanned).ToList(); foreach (var captain in captains) { int randomHero = GetRandomUnbannedHero(); BanHero(captain, GlobalId.GetInstanceId(randomHero), true); } } private int GetRandomUnbannedHero() { var allHeroes = Enumerable.Range(0, 71).Select(i => GlobalId.CreateGlobalId(16, i)).ToList(); var banned = _players.Where(p => p.HasBanned).Select(p => p.BannedBrawler).ToList(); var available = allHeroes.Except(banned).ToList(); return available.Count > 0 ? available[Random.Next(available.Count)] : 0; } private void EndBanPhase() { _state = MatchState.PickPhase; var banned = _players.ToDictionary(p => p.GlobalIndex, p => p.BannedBrawler); Send(new RankedMatchBanEndedMessage { BannedBrawlers = banned }); StartPickPhase(); } private void StartPickPhase() { if (_pickOrder.Count == 0) { Console.WriteLine("[RankedMatch] No players, skip PickPhase"); EndPickPhase(); return; } _pickIndex = 0; _pickStartTick = GetTicks(); foreach (var p in _players) { p.HasPickedCharacter = false; p.IsPickingLocked = false; p.CharacterPreview = 0; } UpdatePickStates(); SendPickStarted(); Console.WriteLine($"[RankedMatch] PickPhase started, pickIndex={_pickIndex}, pickOrder={string.Join(",", _pickOrder)}"); } private void UpdatePickStates() { for (int i = 0; i < _players.Count; i++) { var p = _players[i]; if (_pickIndex < _pickOrder.Count && p.GlobalIndex == _pickOrder[_pickIndex]) p.PickState = 0; else p.PickState = 1; } } private void SendPickStarted() { if (_pickIndex >= _pickOrder.Count) return; var current = GetExpectedPicker(); int next = (_pickIndex + 1 < _pickOrder.Count) ? _pickOrder[_pickIndex + 1] : -1; Send(new RankedMatchPickStartedMessage { Player = current, TimeLeft = TimePerPick, NextPlayerIndex = next }); } private void AutoPick() { var current = GetExpectedPicker(); if (current == null || current.HasPickedCharacter) return; int randomHero = GetRandomAvailableHero(); PickHero(current, GlobalId.GetInstanceId(randomHero), true); } private int GetRandomAvailableHero() { var allHeroes = Enumerable.Range(0, 71).Select(i => GlobalId.CreateGlobalId(16, i)).ToList(); var banned = _players.Where(p => p.HasBanned).Select(p => p.BannedBrawler).ToList(); var picked = _players.Where(p => p.HasPickedCharacter).Select(p => p.CharacterPreview).ToList(); var available = allHeroes.Except(banned).Except(picked).ToList(); if (available.Count == 0) return 0; return available[Random.Next(available.Count)]; } private void EndPickPhase() { _state = MatchState.BattleStarting; _finalPrepStartTick = GetTicks(); Send(new RankedMatchFinalPreparationStartedMessage()); SendPickUpdateToAll(); Console.WriteLine($"[RankedMatch] PickPhase ended, battle will start in 200 ticks"); } private int CalculateRandomMap() { var data = DataTables.Get(DataType.RankedE).GetDatas().Cast().ToList(); return DataTables.Get(DataType.Location).GetData(data[Random.Next(data.Count)].Name).GetGlobalId(); } private void Send(GameMessage message) { foreach (var p in _players) SendToSingle(p, message); } public void SendToAll(RankedMatch match, GameMessage message) => Send(message); public async Task SendAsync(GameMessage message) => Send(message); private void SendToSingle(mp_thestealdev player, GameMessage message) { try { player?.Home?.HomeMode?.GameListener?.SendMessage(message); } catch { } } public int GetTicks() => _gameTime.GetTick(); public void ForceAdvancePick() { lock (_syncLock) { if (_state != MatchState.PickPhase) return; if (_pickIndex >= _pickOrder.Count) return; var current = GetExpectedPicker(); if (current != null && !current.HasPickedCharacter && current.CharacterPreview != 0) { int hero = GlobalId.GetInstanceId(current.CharacterPreview); PickHero(current, hero, true); } else if (current != null && !current.HasPickedCharacter) { AutoPick(); } } } public int RestartRound() { _isRestarting = true; LogicServerListener.Instance.StartMatchBattle(_players, null, _currentMapId, (int)_matchId, 0, false); return 0; } public void TerminateError(int reason, string name) { lock (_syncLock) { if (_state == MatchState.Finished) return; _state = MatchState.Finished; Stop(); Send(new RankedMatchTerminatedMessage { Name = name, Reason = reason }); Console.WriteLine($"[RankedMatch] Terminated: reason={reason}, name={name}"); } } } public class mp_thestealdev { public long AccountId; public PlayerDisplayData DisplayData; public int CharacterPreview; public int BanState; public int PickState; public bool HasPickedCharacter; public bool HasBanned; public int GlobalIndex; public int TeamIndex; public int SlotIndex; public int BattleSlotIndex; public int Position; public int BannedBrawler; public bool IsPickingLocked; public bool PickConfirmed; [JsonIgnore] public readonly ClientHome Home; [JsonIgnore] public readonly ClientAvatar Avatar; public mp_thestealdev() { } public mp_thestealdev(ClientAvatar avatar, ClientHome home) { Home = home; Avatar = avatar; } public static mp_thestealdev CreateRankedPlayer(ClientAvatar avatar, ClientHome home, int teamIndex, int slotIndex) { var player = new mp_thestealdev(avatar, home) { TeamIndex = teamIndex, SlotIndex = slotIndex, Position = slotIndex, GlobalIndex = teamIndex * 3 + slotIndex, DisplayData = new PlayerDisplayData(home.HasPremiumPass, home.ThumbnailId, home.NameColorId, avatar.Name), AccountId = avatar.AccountId }; return player; } public void Encode(ByteStream stream) { ByteStreamHelper.EncodeLogicLong(stream, AccountId); stream.WriteBoolean(DisplayData != null); DisplayData?.Encode(stream); stream.WriteVInt(BanState); stream.WriteVInt(TeamIndex); stream.WriteVInt(SlotIndex); ByteStreamHelper.WriteDataReference(stream, CharacterPreview); var hero = Avatar?.GetHero(CharacterPreview); int level = hero?.PowerLevel ?? 1; stream.WriteVInt(level + 1); int skinGlobalId = 0; if (hero?.SelectedSkinId > 0) skinGlobalId = GlobalId.CreateGlobalId(29, hero.SelectedSkinId); ByteStreamHelper.WriteDataReference(stream, skinGlobalId); CardData starPowerCard = null; CardData gadgetCard = null; if (hero != null && Avatar != null) { if (hero.SelectedGadgetId > 0) { var gadget = DataTables.Get(23).GetDataByGlobalId(hero.SelectedGadgetId); if (gadget != null && Avatar.UnlockedMeta.Contains(gadget.GetGlobalId())) gadgetCard = gadget; } if (hero.PowerLevel >= 9 && hero.SelectedStarPowerId > 0) { var starPower = DataTables.Get(23).GetDataByGlobalId(hero.SelectedStarPowerId); if (starPower != null && Avatar.SelectedMeta.Contains(starPower.GetGlobalId())) starPowerCard = starPower; } } ByteStreamHelper.WriteDataReference(stream, starPowerCard); ByteStreamHelper.WriteDataReference(stream, gadgetCard); stream.WriteBoolean(HasPickedCharacter); } } }