Files
LeagueARAMTracker/ARAMUtility/ViewModel/MainViewModel.cs
T
2026-04-29 18:15:21 +02:00

327 lines
11 KiB
C#

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<ChampionViewModel> TeamChampions { get; private set; } = [];
[ObservableProperty]
public partial ObservableCollection<ChampionViewModel> 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<int, ChampionData> _allChampions = [];
private List<int> _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<int, ChampionData> 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<LolMatchmakingMatchmakingReadyCheckResource>();
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<ChampSelectSession>();
await ShowChampionsAsync(session);
break;
default:
File.AppendAllText("socket.log", $"{apiEvent.Uri}: {apiEvent.EventType} - {apiEvent.Data?.ToJsonString()}\n");
break;
}
}
private async Task UpdateNeedChampionIdsAsync()
{
IEnumerable<int> completedChampionIds = await _client.GetAllRandomAllChampionsCompletedChampionsAsync();
IEnumerable<int> 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<int> teamChampions, IEnumerable<int> 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<ChampionViewModel> viewModel, IEnumerable<int> 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<PlayerResponse[]>(playerlistJson);
if (allPlayerResponse is not { Length: > 0 })
{
return;
}
List<int> blueTeam = [];
List<int> 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
}