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.ReadyCheck; namespace ARAMUtility.ViewModel; public partial class MainViewModel : ObservableObject, IDisposable { private const int TEAM_CHAMPIONS_MAX = 5; private const int BENCH_CHAMPIONS_MAX = 10; 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] public partial string ConnectionStatus { get; private set; } = "Not connected."; private Dictionary _allChampions = []; private List _needChampionIds = []; private readonly APIClient _client = new(); private readonly LcuWebsocket _lcuWebsocket = new(); private Task _lcuWebsocketTask; private readonly ARAMBalanceService _aramBalanceService = new(); public MainViewModel() { _lcuWebsocket.Connecting += (_, _) => UpdateConnectionStatus(false, "Connecting ..."); _lcuWebsocket.Connected += (_, _) => UpdateConnectionStatus(false, "Connected."); _lcuWebsocket.Disconnected += (_, _) => UpdateConnectionStatus(true, "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(); } 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; } await UpdateNeedChampionIdsAsync(); await FillChampionLists(); } private void UpdateConnectionStatus(bool isConnected, string statusMessage) { Dispatcher.Invoke(() => { IsDisconnected = isConnected; ConnectionStatus = statusMessage; }); } 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 && readyCheck.PlayerResponse == LolMatchmakingMatchmakingReadyCheckResponse.Accepted) { 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); 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(); async Task UpdateChampions(ObservableCollection viewModel, IEnumerable championIds) { viewModel.Clear(); await _aramBalanceService.EnsureIsLoadedAsync(); foreach (int championId in championIds) { ChampionData? championData = await _client.GetChampionByIdAsync(championId); if (championData is null) { continue; } string imagePath = await ResourceService.GetChampionIconPathAsync(championId); ChampionViewModel vm = new(championData with { AramBalance = _aramBalanceService.GetAramStats(championData.Id) }) { IsNeededForChallenge = _needChampionIds.Contains(championData.Id), ImagePath = imagePath, }; viewModel.Add(vm); } } } [RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(IsDisconnected))] 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(); } _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 }