Show champs ingame

This commit is contained in:
2026-04-29 18:15:21 +02:00
parent 06d5113711
commit 9f6105b970
5 changed files with 260 additions and 40 deletions
+79 -24
View File
@@ -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;
@@ -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<ChampionViewModel> viewModel, IEnumerable<int> championIds)
private async Task UpdateChampions(ObservableCollection<ChampionViewModel> viewModel, IEnumerable<int> 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<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)]
@@ -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;
+17 -3
View File
@@ -24,6 +24,8 @@ public class APIClient : IDisposable
private readonly Dictionary<string, ChampionResponse> _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<string> DDragonGetAsync(string path)
private async Task<string> DDragonGetAsync(string path)
{
return await CachingHttpClient.GetStringAsync($"{DDRAGON_BASE_URL}{path}");
return await _ddragonClient.GetStringAsync($"{DDRAGON_BASE_URL}{path}");
}
private static async Task<T?> DDragonGetAsync<T>(string path)
private async Task<T?> DDragonGetAsync<T>(string path)
{
string json = await DDragonGetAsync(path);
return JsonSerializer.Deserialize<T>(json);
@@ -135,6 +137,17 @@ public class APIClient : IDisposable
return championData.FirstOrDefault(c => c.Id == id);
}
public async Task<int> 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();
+71 -12
View File
@@ -1,18 +1,77 @@
namespace LeagueAPI;
public class HttpCache : Dictionary<string, string> { }
public static class CachingHttpClient
public class HttpCache : Dictionary<string, InvalidatableCacheObject<string>> { }
public record struct InvalidatableCacheObject<T>(
T Value,
DateTime? InvalidateTime = null
)
{
private static HttpCache _cache = ResourceService.GetHttpCache() ?? [];
private static HttpClient _client = new();
public static async Task<string> 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<string> GetStringAsync(string requestUri, TimeSpan? invalidateAfter = null)
{
if (_cache.TryGetValue(requestUri, out InvalidatableCacheObject<string> 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
}
@@ -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
);
+69
View File
@@ -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
}