diff --git a/ARAMUtility/ViewModel/MainViewModel.cs b/ARAMUtility/ViewModel/MainViewModel.cs index b376f05..d563ecc 100644 --- a/ARAMUtility/ViewModel/MainViewModel.cs +++ b/ARAMUtility/ViewModel/MainViewModel.cs @@ -9,7 +9,9 @@ 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; @@ -46,7 +48,7 @@ public partial class MainViewModel : ObservableObject, IDisposable [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; @@ -56,6 +58,8 @@ public partial class MainViewModel : ObservableObject, IDisposable 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() { @@ -71,6 +75,8 @@ public partial class MainViewModel : ObservableObject, IDisposable }; #endif _lcuWebsocketTask = _lcuWebsocket.Connect(); + + _leagueWatcher.LeagueOfLegendsExeFound += UpdateChampionListsFromGame; } internal async void OnInit(object? sender, RoutedEventArgs e) @@ -152,14 +158,17 @@ public partial class MainViewModel : ObservableObject, IDisposable private async Task FillChampionLists() { string defaultImagePath = await ResourceService.GetChampionIconPathAsync(-1); - while (TeamChampions.Count < TEAM_CHAMPIONS_MAX) + Dispatcher.Invoke(() => { - TeamChampions.Add(GetEmptyChampionViewModel()); - } - while (BenchChampions.Count < BENCH_CHAMPIONS_MAX) - { - BenchChampions.Add(GetEmptyChampionViewModel()); - } + while (TeamChampions.Count < TEAM_CHAMPIONS_MAX) + { + TeamChampions.Add(GetEmptyChampionViewModel()); + } + while (BenchChampions.Count < BENCH_CHAMPIONS_MAX) + { + BenchChampions.Add(GetEmptyChampionViewModel()); + } + }); ChampionViewModel GetEmptyChampionViewModel() { @@ -185,29 +194,74 @@ public partial class MainViewModel : ObservableObject, IDisposable await FillChampionLists(); _championUpdateSemaphore.Release(); + } - async Task UpdateChampions(ObservableCollection viewModel, IEnumerable championIds) + private async Task UpdateChampions(ObservableCollection viewModel, IEnumerable championIds) + { + try { - viewModel.Clear(); - await _aramBalanceService.EnsureIsLoadedAsync(); - - foreach (int championId in championIds) + Dispatcher.Invoke(() => viewModel.Clear()); + } + catch (NotSupportedException) + { + Dispatcher.Invoke(() => { - ChampionData? championData = await _client.GetChampionByIdAsync(championId); - if (championData is null || championData.Name is null) + while (viewModel.Count > 0) { - continue; + viewModel.RemoveAt(0); } + }); + } + await _aramBalanceService.EnsureIsLoadedAsync(); - string imagePath = await ResourceService.GetChampionIconPathAsync(championId); - ChampionViewModel vm = new(championData with { AramBalance = _aramBalanceService.GetAramChampion(championData.Name) }) - { - IsNeededForChallenge = _needChampionIds.Contains(championData.Id), - ImagePath = imagePath, - }; - viewModel.Add(vm); + 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)] @@ -253,8 +307,9 @@ public partial class MainViewModel : ObservableObject, IDisposable if (disposing) { _allChampions = []; - _client.Dispose(); - _lcuWebsocket.Dispose(); + _client?.Dispose(); + _lcuWebsocket?.Dispose(); + _localCachingClient?.Dispose(); } _isDisposed = true; diff --git a/LeagueAPI/APIClient.cs b/LeagueAPI/APIClient.cs index d8c245e..9879ab7 100644 --- a/LeagueAPI/APIClient.cs +++ b/LeagueAPI/APIClient.cs @@ -24,6 +24,8 @@ public class APIClient : IDisposable private readonly Dictionary _championResponseCache = []; + private readonly CachingHttpClient _ddragonClient = new(); + public APIClient() { _lcuHttpClient = new(new LcuHttpClientHandler()); @@ -81,12 +83,12 @@ public class APIClient : IDisposable } #region DDragon - private static async Task DDragonGetAsync(string path) + private async Task DDragonGetAsync(string path) { - return await CachingHttpClient.GetStringAsync($"{DDRAGON_BASE_URL}{path}"); + return await _ddragonClient.GetStringAsync($"{DDRAGON_BASE_URL}{path}"); } - private static async Task DDragonGetAsync(string path) + private async Task DDragonGetAsync(string path) { string json = await DDragonGetAsync(path); return JsonSerializer.Deserialize(json); @@ -135,6 +137,17 @@ public class APIClient : IDisposable return championData.FirstOrDefault(c => c.Id == id); } + + public async Task GetChampionIdByNameAsync(string name) + { + ChampionData[] championData = await GetAllChampionsAsync(); + if (championData is not { Length: > 0 }) + { + return -1; + } + + return championData.FirstOrDefault(c => c.Name == name)?.Id ?? -1; + } #endregion #region IDisposable @@ -145,6 +158,7 @@ public class APIClient : IDisposable if (disposing) { _lcuHttpClient?.Dispose(); + _ddragonClient?.Dispose(); } _championResponseCache?.Clear(); diff --git a/LeagueAPI/CachingHttpClient.cs b/LeagueAPI/CachingHttpClient.cs index 7afa787..7ec2ef9 100644 --- a/LeagueAPI/CachingHttpClient.cs +++ b/LeagueAPI/CachingHttpClient.cs @@ -1,18 +1,77 @@ namespace LeagueAPI; -public class HttpCache : Dictionary { } - -public static class CachingHttpClient +public class HttpCache : Dictionary> { } +public record struct InvalidatableCacheObject( + T Value, + DateTime? InvalidateTime = null + ) { - private static HttpCache _cache = ResourceService.GetHttpCache() ?? []; - private static HttpClient _client = new(); - - public static async Task GetStringAsync(string requestUri) + public readonly bool IsValid() { - if (_cache.TryGetValue(requestUri, out string? response)) - { - return response; - } - return await _client.GetStringAsync(requestUri); + return !InvalidateTime.HasValue || InvalidateTime < DateTime.UtcNow; } } + +public class CachingHttpClient : IDisposable +{ + private bool _isDisposed; + private readonly HttpCache _cache = ResourceService.GetHttpCache() ?? []; + private readonly HttpClient _client = new(); + + public CachingHttpClient(bool insecure = false) + { + if (insecure) + { + HttpClientHandler handler = new() + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }; + _client = new(handler); + } + else + { + _client = new(); + } + } + + public async Task GetStringAsync(string requestUri, TimeSpan? invalidateAfter = null) + { + if (_cache.TryGetValue(requestUri, out InvalidatableCacheObject response) && response.IsValid()) + { + return response.Value; + } + string result = await _client.GetStringAsync(requestUri); + if (invalidateAfter is not null && invalidateAfter > TimeSpan.Zero) + { + _cache[requestUri] = new(result, DateTime.UtcNow + invalidateAfter); + } + else + { + _cache[requestUri] = new(result); + } + return result; + } + + #region IDisposable + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _client?.Dispose(); + } + + _cache?.Clear(); + _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 +} diff --git a/LeagueAPI/Models/GameClient/PlayerResponse.cs b/LeagueAPI/Models/GameClient/PlayerResponse.cs new file mode 100644 index 0000000..8c7e4d2 --- /dev/null +++ b/LeagueAPI/Models/GameClient/PlayerResponse.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace LeagueAPI.Models.GameClient; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum TeamId +{ + ALL, + UNKNOWN, + ORDER, + CHAOS, + NEUTRAL, +} + +public record class PlayerResponse( + [property: JsonPropertyName("championName")] string ChampionName, + [property: JsonPropertyName("isBot")] bool IsBot, + [property: JsonPropertyName("rawChampionName")] string RawChampionName, + [property: JsonPropertyName("team")] TeamId Team, + [property: JsonPropertyName("riotId")] string RiotId, + [property: JsonPropertyName("riotIdGameName")] string RiotIdGameName, + [property: JsonPropertyName("riotIdTagLine")] string RiotIdTagLine + ); diff --git a/LeagueAPI/Utils/ProcessWatcher.cs b/LeagueAPI/Utils/ProcessWatcher.cs new file mode 100644 index 0000000..a1e9cd2 --- /dev/null +++ b/LeagueAPI/Utils/ProcessWatcher.cs @@ -0,0 +1,69 @@ +using System.Diagnostics; + +namespace LeagueAPI.Utils; + +public class ProcessWatcher : IDisposable +{ + private bool _isDisposed; + private Timer _timer; + + public bool IsGameRunning { get; private set; } = false; + + public event EventHandler? LeagueOfLegendsExeFound; + + public TimeSpan Interval + { + get => field; + set + { + if (field != value) + { + field = value; + _timer?.Change(TimeSpan.Zero, value); + } + } + } = TimeSpan.FromSeconds(10); + + public ProcessWatcher() + { + _timer = new(OnTimerTick, null, TimeSpan.Zero, Interval); + } + + private void OnTimerTick(object? state) + { + if (Process.GetProcessesByName("League of Legends").Length != 0) + { + if (!IsGameRunning) + { + LeagueOfLegendsExeFound?.Invoke(this, EventArgs.Empty); + } + IsGameRunning = true; + } + else + { + IsGameRunning = false; + } + } + + #region IDisposable + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _timer?.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 +}