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 SUBSCRIBE_EVENTS = [ "OnJsonApiEvent_lol-champ-select_v1_session", "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; public event EventHandler? 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 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(); 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 }