Files
LeagueARAMTracker/LeagueAPI/LcuWebsocket.cs
2026-03-13 01:22:28 +01:00

158 lines
4.9 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using LeagueAPI.Utils;
namespace LeagueAPI;
public class LcuWebsocket : IDisposable
{
private const int OPCODE_SUBSCRIBE = 5;
private const int OPCODE_UNSUBSCRIBE = 6;
private const int OPCODE_SEND = 8;
private static readonly IReadOnlyList<string> SUBSCRIBE_EVENTS =
[
"OnJsonApiEvent_lol-champ-select_v1_session",
"OnJsonApiEvent_lol-matchmaking_v1_search",
"OnJsonApiEvent_lol-matchmaking_v1_ready-check",
];
private bool _isDisposed;
private readonly ClientWebSocket _socket = new();
public event EventHandler? Connecting;
public event EventHandler? Connected;
public event EventHandler? Disconnected;
public event EventHandler<LcuApiEvent>? LcuApiEvent;
public event EventHandler<Exception>? LcuApiException;
[MemberNotNull(nameof(RiotAuthentication))]
public ProcessInfo? ProcessInfo { get; internal set; }
public RiotAuthentication? RiotAuthentication
{
get
{
if (ProcessInfo is not null)
{
return new RiotAuthentication(ProcessInfo.RemotingAuthToken);
}
return null;
}
}
public async Task Connect()
{
Connecting?.Invoke(this, EventArgs.Empty);
try
{
ProcessInfo = ProcessFinder.GetProcessInfo();
}
catch (Exception ex)
{
return;
}
if (!ProcessFinder.IsPortOpen(ProcessInfo))
{
throw new InvalidOperationException("Failed to connect to LCUx process port.");
}
// ignore SSL certificate errors
_socket.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true;
_socket.Options.Credentials = RiotAuthentication.ToNetworkCredential();
Uri uri = new($"wss://127.0.0.1:{ProcessInfo.AppPort}/");
try
{
await _socket.ConnectAsync(uri, CancellationToken.None);
Connected?.Invoke(this, EventArgs.Empty);
foreach (string eventName in SUBSCRIBE_EVENTS)
{
string message = $"[{OPCODE_SUBSCRIBE}, \"{eventName}\"]";
byte[] buffer = Encoding.UTF8.GetBytes(message);
Memory<byte> memory = new(buffer);
await _socket.SendAsync(memory, WebSocketMessageType.Text, true, CancellationToken.None);
}
while (_socket.State is WebSocketState.Open)
{
try
{
WebsocketMessageResult result = await _socket.ReadFullMessage(CancellationToken.None);
if (result.MessageType is WebSocketMessageType.Close)
{
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
}
else
{
if (result.Message.Length == 0)
{
continue;
}
JsonElement response = JsonElement.Parse(result.Message);
if (response.ValueKind != JsonValueKind.Array
|| response.GetArrayLength() != 3
|| !response[0].TryGetInt32(out int value)
|| value != OPCODE_SEND)
{
continue;
}
LcuApiEvent? apiEvent = response[2].Deserialize<LcuApiEvent>();
if (apiEvent is null)
{
continue;
}
LcuApiEvent?.Invoke(this, apiEvent);
}
}
catch (Exception ex)
{
LcuApiException?.Invoke(this, ex);
}
}
}
catch (Exception ex) when (ex is not WebSocketException)
{
LcuApiException?.Invoke(this, ex);
}
finally
{
Disconnected?.Invoke(this, EventArgs.Empty);
}
}
#region IDisposable
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
if (_socket.State == WebSocketState.Open)
{
_socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None)
.Wait();
}
_socket?.Dispose();
ProcessInfo = null;
}
_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
}