using System.Collections.ObjectModel; using System.IO; using System.Text.Json; using System.Windows; using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LeagueAPI; using LeagueAPI.ARAM; using LeagueAPI.Models.ChampSelect; using LeagueAPI.Models.DDragon.Champions; using LeagueAPI.Models.GameClient; using LeagueAPI.Models.ReadyCheck; using LeagueAPI.Utils; namespace ARAMUtility.ViewModel; public partial class MainViewModel : ObservableObject, IDisposable { private const int TEAM_CHAMPIONS_MAX = 5; private const int BENCH_CHAMPIONS_MAX = 10; public enum ConnectionStatus { Disconnected, Connecting, Connected, } private bool _isDisposed; private readonly Lock _syncRoot = new(); private readonly SemaphoreSlim _championUpdateSemaphore = new(1, 1); private Dispatcher Dispatcher { get; } = Dispatcher.CurrentDispatcher; [ObservableProperty] public partial string Title { get; set; } = "ARAM Utility"; [ObservableProperty] public partial ObservableCollection TeamChampions { get; private set; } = []; [ObservableProperty] public partial ObservableCollection BenchChampions { get; private set; } = []; [ObservableProperty] public partial bool IsDisconnected { get; private set; } = false; [ObservableProperty, NotifyPropertyChangedFor(nameof(ReconnectButtonVisibility))] public partial ConnectionStatus Status { get; private set; } = ConnectionStatus.Disconnected; public Visibility ReconnectButtonVisibility => Status == ConnectionStatus.Disconnected ? Visibility.Visible : Visibility.Collapsed; [ObservableProperty] public partial bool AutoAccept { get; set; } = true; private Dictionary _allChampions = []; private List _needChampionIds = []; private readonly APIClient _client = new(); private readonly LcuWebsocket _lcuWebsocket = new(); private Task _lcuWebsocketTask; private readonly ARAMBalanceService _aramBalanceService = new(); private readonly ProcessWatcher _leagueWatcher = new(); private readonly CachingHttpClient _localCachingClient = new(insecure: true); public MainViewModel() { _lcuWebsocket.Connecting += (_, _) => UpdateConnectionStatus(false, ConnectionStatus.Connecting); _lcuWebsocket.Connected += (_, _) => UpdateConnectionStatus(false, ConnectionStatus.Connected); _lcuWebsocket.Disconnected += (_, _) => UpdateConnectionStatus(true, ConnectionStatus.Disconnected); _lcuWebsocket.LcuApiEvent += OnLcuApiEvent; #if DEBUG _lcuWebsocket.LcuApiException += (sender, e) => { File.AppendAllText("socketerror.log", $"[{DateTime.Now:s}] {e.Message}\n{e.StackTrace}\n\n"); }; #endif _lcuWebsocketTask = _lcuWebsocket.Connect(); _leagueWatcher.LeagueOfLegendsExeFound += UpdateChampionListsFromGame; } internal async void OnInit(object? sender, RoutedEventArgs e) { ChampionData[] champions = await _client.GetAllChampionsAsync(); Dictionary championDictionary = champions .Where(c => c.Id != -1) .ToDictionary(key => key.Id); lock (_syncRoot) { _allChampions = championDictionary; } try { await UpdateNeedChampionIdsAsync(); } catch (InvalidOperationException) { UpdateConnectionStatus(false, ConnectionStatus.Disconnected); return; } finally { await FillChampionLists(); } } private void UpdateConnectionStatus(bool isConnected, ConnectionStatus status) { Dispatcher.Invoke(() => { IsDisconnected = isConnected; Status = status; }); } private async void OnLcuApiEvent(object? sender, LcuApiEvent apiEvent) { switch (apiEvent.Uri) { case "/lol-matchmaking/v1/ready-check": LolMatchmakingMatchmakingReadyCheckResource? readyCheck = apiEvent.Data.Deserialize(); if (readyCheck is not null) { if (readyCheck.PlayerResponse != LolMatchmakingMatchmakingReadyCheckResponse.Accepted) { if (AutoAccept) { await Task.Delay(TimeSpan.FromMilliseconds(Random.Shared.Next(100, 1500))); await _client.MatchmakingAcceptAsync(); } } else { await UpdateNeedChampionIdsAsync(); } } break; case "/lol-champ-select/v1/session": ChampSelectSession? session = apiEvent.Data.Deserialize(); await ShowChampionsAsync(session); break; default: File.AppendAllText("socket.log", $"{apiEvent.Uri}: {apiEvent.EventType} - {apiEvent.Data?.ToJsonString()}\n"); break; } } private async Task UpdateNeedChampionIdsAsync() { IEnumerable completedChampionIds = await _client.GetAllRandomAllChampionsCompletedChampionsAsync(); IEnumerable needChampionIds = _allChampions.Keys.Except(completedChampionIds); lock (_syncRoot) { _needChampionIds = [.. needChampionIds]; } } private async Task FillChampionLists() { string defaultImagePath = await ResourceService.GetChampionIconPathAsync(-1); Dispatcher.Invoke(() => { while (TeamChampions.Count < TEAM_CHAMPIONS_MAX) { TeamChampions.Add(GetEmptyChampionViewModel()); } while (BenchChampions.Count < BENCH_CHAMPIONS_MAX) { BenchChampions.Add(GetEmptyChampionViewModel()); } }); ChampionViewModel GetEmptyChampionViewModel() { return new ChampionViewModel(new ChampionData()) { ImagePath = defaultImagePath, IsNeededForChallenge = false }; } } private async Task ShowChampionsAsync(ChampSelectSession? session) { await _championUpdateSemaphore.WaitAsync(); (IEnumerable teamChampions, IEnumerable benchChampions) = APIClient.GetSelectableChampionIds(session); if (!teamChampions.Any() && !benchChampions.Any()) { TeamChampions.Clear(); BenchChampions.Clear(); } else { await UpdateChampions(TeamChampions, teamChampions); await UpdateChampions(BenchChampions, benchChampions); } await FillChampionLists(); _championUpdateSemaphore.Release(); } private async Task UpdateChampions(ObservableCollection viewModel, IEnumerable championIds) { try { Dispatcher.Invoke(() => viewModel.Clear()); } catch (NotSupportedException) { Dispatcher.Invoke(() => { while (viewModel.Count > 0) { viewModel.RemoveAt(0); } }); } await _aramBalanceService.EnsureIsLoadedAsync(); foreach (int championId in championIds) { ChampionData? championData = await _client.GetChampionByIdAsync(championId); if (championData is null || championData.Name is null) { continue; } string imagePath = await ResourceService.GetChampionIconPathAsync(championId); ChampionViewModel vm = new(championData with { AramBalance = _aramBalanceService.GetAramChampion(championData.Name) }) { IsNeededForChallenge = _needChampionIds.Contains(championData.Id), ImagePath = imagePath, }; Dispatcher.Invoke(() => { viewModel.Add(vm); }); } } private async void UpdateChampionListsFromGame(object? sender, EventArgs e) { await Task.Delay(TimeSpan.FromSeconds(5)); string playerlistJson = await _localCachingClient.GetStringAsync("https://127.0.0.1:2999/liveclientdata/playerlist", TimeSpan.FromSeconds(30)); PlayerResponse[]? allPlayerResponse = JsonSerializer.Deserialize(playerlistJson); if (allPlayerResponse is not { Length: > 0 }) { return; } List blueTeam = []; List redTeam = []; foreach (PlayerResponse player in allPlayerResponse) { if (player.Team != TeamId.ORDER && player.Team != TeamId.CHAOS) { continue; } int championId = await _client.GetChampionIdByNameAsync(player.ChampionName); if (player.Team == TeamId.ORDER) { blueTeam.Add(championId); } if (player.Team == TeamId.CHAOS) { redTeam.Add(championId); } } await UpdateChampions(TeamChampions, blueTeam); await UpdateChampions(BenchChampions, redTeam); await FillChampionLists(); } [RelayCommand(AllowConcurrentExecutions = false)] private async Task Connect() { if (_lcuWebsocketTask is null || _lcuWebsocketTask.IsCompleted) { _lcuWebsocketTask?.Dispose(); _lcuWebsocketTask = _lcuWebsocket.Connect(); } } [RelayCommand(AllowConcurrentExecutions = false)] private async Task OpenLobby() { await _client.CreateMayhemLobbyAsync(); } [RelayCommand(AllowConcurrentExecutions = false)] private async Task StartQueueing() { await _client.StartMatchmakingQueueAsync(); } [RelayCommand(AllowConcurrentExecutions = false)] private async Task ReloadARAMBalance() { await _aramBalanceService.ReloadAsync(force: true); MessageBox.Show("Reloaded ARAM balance data.", "Info", MessageBoxButton.OK, MessageBoxImage.Exclamation); } [RelayCommand] private async Task Quit() { Application.Current.Shutdown(); } #region IDisposable protected virtual void Dispose(bool disposing) { if (!_isDisposed) { if (disposing) { _allChampions = []; _client?.Dispose(); _lcuWebsocket?.Dispose(); _localCachingClient?.Dispose(); } _isDisposed = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } #endregion }