using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.WebSockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BingoAPI.Conditions; using BingoAPI.Events; using BingoAPI.Events.BuiltIn; using BingoAPI.Goals; using BingoAPI.Helpers; using BingoAPI.Models; using BingoAPI.Models.Settings; using BingoAPI.Networking.Clients; using BingoAPI.Networking.Converters; using BingoAPI.Networking.DTOs; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("WarperSan")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Library that allows to communicate with BingoSync's servers through code")] [assembly: AssemblyFileVersion("0.1.3.0")] [assembly: AssemblyInformationalVersion("0.1.3-alpha+05041a50d4823d4c32c4fcae9420886e16396084")] [assembly: AssemblyProduct("BingoAPI")] [assembly: AssemblyTitle("BingoAPI")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/WarperSan/BingoAPI")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace System { [ExcludeFromCodeCoverage] [Embedded] internal readonly struct Range : IEquatable { private static class HashHelpers { public static int Combine(int h1, int h2) { uint num = (uint)((h1 << 5) | (h1 >>> 27)); return ((int)num + h1) ^ h2; } } private static class ThrowHelper { [DoesNotReturn] public static void ThrowArgumentOutOfRangeException() { throw new ArgumentOutOfRangeException("length"); } } public Index Start { get; } public Index End { get; } public static Range All => Index.Start..Index.End; public Range(Index start, Index end) { Start = start; End = end; } public override bool Equals([NotNullWhen(true)] object? value) { if (value is Range { Start: var start } range && start.Equals(Start)) { return range.End.Equals(End); } return false; } public bool Equals(Range other) { if (other.Start.Equals(Start)) { return other.End.Equals(End); } return false; } public override int GetHashCode() { return HashHelpers.Combine(Start.GetHashCode(), End.GetHashCode()); } public override string ToString() { return Start.ToString() + ".." + End; } public static Range StartAt(Index start) { return start..Index.End; } public static Range EndAt(Index end) { return Index.Start..end; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public (int Offset, int Length) GetOffsetAndLength(int length) { Index start = Start; int num = ((!start.IsFromEnd) ? start.Value : (length - start.Value)); Index end = End; int num2 = ((!end.IsFromEnd) ? end.Value : (length - end.Value)); if ((uint)num2 > (uint)length || (uint)num > (uint)num2) { ThrowHelper.ThrowArgumentOutOfRangeException(); } return (Offset: num, Length: num2 - num); } } [ExcludeFromCodeCoverage] [Embedded] internal readonly struct Index : IEquatable { private static class ThrowHelper { [DoesNotReturn] public static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() { throw new ArgumentOutOfRangeException("value", "Non-negative number required."); } } private readonly int _value; public static Index Start => new Index(0); public static Index End => new Index(-1); public int Value { get { if (_value < 0) { return ~_value; } return _value; } } public bool IsFromEnd => _value < 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public Index(int value, bool fromEnd = false) { if (value < 0) { ThrowHelper.ThrowValueArgumentOutOfRange_NeedNonNegNumException(); } if (fromEnd) { _value = ~value; } else { _value = value; } } private Index(int value) { _value = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Index FromStart(int value) { if (value < 0) { ThrowHelper.ThrowValueArgumentOutOfRange_NeedNonNegNumException(); } return new Index(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Index FromEnd(int value) { if (value < 0) { ThrowHelper.ThrowValueArgumentOutOfRange_NeedNonNegNumException(); } return new Index(~value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetOffset(int length) { int num = _value; if (IsFromEnd) { num += length + 1; } return num; } public override bool Equals([NotNullWhen(true)] object? value) { if (value is Index) { return _value == ((Index)value)._value; } return false; } public bool Equals(Index other) { return _value == other._value; } public override int GetHashCode() { return _value; } public static implicit operator Index(int value) { return FromStart(value); } public override string ToString() { if (IsFromEnd) { return ToStringFromEnd(); } return ((uint)Value).ToString(); } private string ToStringFromEnd() { return "^" + Value; } } } namespace System.Runtime.Versioning { [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class RequiresPreviewFeaturesAttribute : Attribute { public string? Message { get; } public string? Url { get; set; } public RequiresPreviewFeaturesAttribute() { } public RequiresPreviewFeaturesAttribute(string? message) { Message = message; } } } namespace System.Runtime.CompilerServices { [EditorBrowsable(EditorBrowsableState.Never)] [ExcludeFromCodeCoverage] [Embedded] internal static class IsExternalInit { } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class AsyncMethodBuilderAttribute : Attribute { public Type BuilderType { get; } public AsyncMethodBuilderAttribute(Type builderType) { BuilderType = builderType; } } [EditorBrowsable(EditorBrowsableState.Never)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class ExtensionMarkerAttribute : Attribute { public string Name { get; } public ExtensionMarkerAttribute(string name) { Name = name; } } [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class OverloadResolutionPriorityAttribute : Attribute { public int Priority { get; } public OverloadResolutionPriorityAttribute(int priority) { Priority = priority; } } [AttributeUsage(AttributeTargets.Method, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class ModuleInitializerAttribute : Attribute { } [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] [EditorBrowsable(EditorBrowsableState.Never)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class RequiresLocationAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class CompilerLoweringPreserveAttribute : Attribute { } [AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class SkipLocalsInitAttribute : Attribute { } [AttributeUsage(AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class ParamCollectionAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class CollectionBuilderAttribute : Attribute { public Type BuilderType { get; } public string MethodName { get; } public CollectionBuilderAttribute(Type builderType, string methodName) { BuilderType = builderType; MethodName = methodName; } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class RequiredMemberAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class InterpolatedStringHandlerAttribute : Attribute { } [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class CompilerFeatureRequiredAttribute : Attribute { public const string RefStructs = "RefStructs"; public const string RequiredMembers = "RequiredMembers"; public string FeatureName { get; } public bool IsOptional { get; set; } public CompilerFeatureRequiredAttribute(string featureName) { FeatureName = featureName; } } [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class CallerArgumentExpressionAttribute : Attribute { public string ParameterName { get; } public CallerArgumentExpressionAttribute(string parameterName) { ParameterName = parameterName; } } [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute { public string[] Arguments { get; } public InterpolatedStringHandlerArgumentAttribute(string argument) { Arguments = new string[1] { argument }; } public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) { Arguments = arguments; } } } namespace System.Diagnostics.CodeAnalysis { [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class DoesNotReturnIfAttribute : Attribute { public bool ParameterValue { get; } public DoesNotReturnIfAttribute(bool parameterValue) { ParameterValue = parameterValue; } } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class NotNullIfNotNullAttribute : Attribute { public string ParameterName { get; } public NotNullIfNotNullAttribute(string parameterName) { ParameterName = parameterName; } } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class AllowNullAttribute : Attribute { } [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class MaybeNullWhenAttribute : Attribute { public bool ReturnValue { get; } public MaybeNullWhenAttribute(bool returnValue) { ReturnValue = returnValue; } } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class UnscopedRefAttribute : Attribute { } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class MaybeNullAttribute : Attribute { } [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class ConstantExpectedAttribute : Attribute { public object? Min { get; set; } public object? Max { get; set; } } [AttributeUsage(AttributeTargets.Method, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class DoesNotReturnAttribute : Attribute { } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class DisallowNullAttribute : Attribute { } [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class ExperimentalAttribute : Attribute { public string DiagnosticId { get; } public string? UrlFormat { get; set; } public ExperimentalAttribute(string diagnosticId) { DiagnosticId = diagnosticId; } } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class MemberNotNullAttribute : Attribute { public string[] Members { get; } public MemberNotNullAttribute(string member) { Members = new string[1] { member }; } public MemberNotNullAttribute(params string[] members) { Members = members; } } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class NotNullAttribute : Attribute { } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class MemberNotNullWhenAttribute : Attribute { public bool ReturnValue { get; } public string[] Members { get; } public MemberNotNullWhenAttribute(bool returnValue, string member) { ReturnValue = returnValue; Members = new string[1] { member }; } public MemberNotNullWhenAttribute(bool returnValue, params string[] members) { ReturnValue = returnValue; Members = members; } } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class StringSyntaxAttribute : Attribute { public const string CompositeFormat = "CompositeFormat"; public const string DateOnlyFormat = "DateOnlyFormat"; public const string DateTimeFormat = "DateTimeFormat"; public const string EnumFormat = "EnumFormat"; public const string GuidFormat = "GuidFormat"; public const string Json = "Json"; public const string NumericFormat = "NumericFormat"; public const string Regex = "Regex"; public const string TimeOnlyFormat = "TimeOnlyFormat"; public const string TimeSpanFormat = "TimeSpanFormat"; public const string Uri = "Uri"; public const string Xml = "Xml"; public string Syntax { get; } public object?[] Arguments { get; } public StringSyntaxAttribute(string syntax) { Syntax = syntax; Arguments = new object[0]; } public StringSyntaxAttribute(string syntax, params object?[] arguments) { Syntax = syntax; Arguments = arguments; } } [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class NotNullWhenAttribute : Attribute { public bool ReturnValue { get; } public NotNullWhenAttribute(bool returnValue) { ReturnValue = returnValue; } } [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] [ExcludeFromCodeCoverage] [Embedded] internal sealed class SetsRequiredMembersAttribute : Attribute { } } namespace Microsoft.CodeAnalysis { [Embedded] [AttributeUsage(AttributeTargets.All)] [ExcludeFromCodeCoverage] internal sealed class EmbeddedAttribute : Attribute { } } namespace BingoAPI.Networking { internal class LoggingHandler : DelegatingHandler { public LoggingHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { string arg = ""; if (request.Content != null) { arg = await request.Content.ReadAsStringAsync(); } Log.Debug($"Request:\n{request}\n{arg}"); HttpResponseMessage response = await base.SendAsync(request, cancellationToken); string arg2 = ""; if (response.Content != null) { arg2 = await response.Content.ReadAsStringAsync(); } Log.Debug($"Response:\n{response}\n{arg2}"); return response; } } internal sealed class RequestBuilder { private HttpMethod _method = HttpMethod.Get; private string? _endpoint; private HttpContent? _content; private RequestBuilder WithMethod(HttpMethod method) { _method = method; return this; } public RequestBuilder Get() { return WithMethod(HttpMethod.Get); } public RequestBuilder Post() { return WithMethod(HttpMethod.Post); } public RequestBuilder Put() { return WithMethod(HttpMethod.Put); } public RequestBuilder ToEndpoint(string endpoint) { _endpoint = endpoint; return this; } public RequestBuilder WithJson(object json) { string content = JsonConvert.SerializeObject(json); _content = new StringContent(content, Encoding.UTF8, "application/json"); return this; } public RequestBuilder WithForm(object form) { List> list = new List>(); var enumerable = from m in form.GetType().GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(delegate(MemberInfo m) { MemberTypes memberType = m.MemberType; return (memberType == MemberTypes.Field || memberType == MemberTypes.Property) ? true : false; }) select new { Member = m, Attribute = m.GetCustomAttribute() } into m where m.Attribute != null select m; foreach (var item in enumerable) { string key = item.Attribute?.Name ?? item.Member.Name; MemberInfo member = item.Member; string text = ((member is FieldInfo fieldInfo) ? fieldInfo.GetValue(form)?.ToString() : ((!(member is PropertyInfo propertyInfo)) ? null : propertyInfo.GetValue(form)?.ToString())); string text2 = text; if (text2 == null) { text2 = ""; } list.Add(new KeyValuePair(key, text2)); } _content = new FormUrlEncodedContent(list); return this; } public HttpRequestMessage Build() { HttpRequestMessage httpRequestMessage = new HttpRequestMessage { Method = _method, Content = _content }; if (_endpoint != null) { httpRequestMessage.RequestUri = new Uri(_endpoint, UriKind.Relative); } return httpRequestMessage; } } public sealed class Session : IDisposable { private readonly HttpClient _client; private readonly BingoApiClient _api; private readonly BingoSocketClient _socket = new BingoSocketClient(); private readonly EventDispatcher _dispatcher; private string? _roomCode; public Team Team { get; private set; } [MemberNotNullWhen(true, "_roomCode")] public bool IsInRoom { [MemberNotNullWhen(true, "_roomCode")] get { return _roomCode != null; } } public Session(EventDispatcher dispatcher) { _dispatcher = dispatcher; _client = new HttpClient(new LoggingHandler(new HttpClientHandler())); _client.Timeout = TimeSpan.FromSeconds(30.0); _client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("BingoAPI", "1.0.0")); _client.BaseAddress = new Uri("https://bingosync.com"); _api = new BingoApiClient(_client); } public async Task CreateRoom(CreateRoomSettings settings, CancellationToken ct = default(CancellationToken)) { throw new NotImplementedException(); } public async Task JoinRoom(JoinRoomSettings settings, CancellationToken ct = default(CancellationToken)) { if (IsInRoom) { Log.Error("Tried to join a room while being connected."); return false; } Log.Info("Joining room '" + settings.Code + "'..."); try { string socketKey = await _api.JoinRoom(settings, ct); await _socket.Connect(socketKey, OnMessageReceived, ct); GetSocketInformationResponse getSocketInformationResponse = await _api.GetSocketInformation(socketKey, ct); _roomCode = getSocketInformationResponse.Code; Team = Team.Red; Player player = new Player { Name = settings.Nickname, Team = Team, UUID = getSocketInformationResponse.PlayerUUID }; _dispatcher.DispatchConnect(player); Log.Info("Room '" + settings.Code + "' was joined."); return true; } catch (Exception arg) { Log.Error($"Failed to join the room '{settings.Code}': {arg}"); return false; } } public async Task LeaveRoom(CancellationToken ct = default(CancellationToken)) { if (!IsInRoom) { Log.Error("Tried to leave the room before being connected."); return false; } string room = _roomCode; Log.Info("Leaving the room '" + room + "'..."); try { await _socket.Disconnect(ct); _roomCode = null; _dispatcher.DispatchDisconnect(); Log.Info("Left the room '" + room + "'."); return true; } catch (Exception arg) { Log.Error($"Failed to leave the room '{room}: {arg}"); return false; } } public async Task SendMessage(string message, CancellationToken ct = default(CancellationToken)) { if (!IsInRoom) { Log.Error("Tried to send a message before being connected."); return false; } Log.Info("Sending the following chat message: '" + message + "'..."); try { await _api.SendMessage(_roomCode, message, ct); Log.Info("Sent the following chat message: '" + message + "'."); return true; } catch (Exception arg) { Log.Error($"Failed to sent the chat message: {arg}"); return false; } } public async Task ChangeTeam(Team team, CancellationToken ct = default(CancellationToken)) { if (!IsInRoom) { Log.Error("Tried to change team before being connected."); return false; } if (team == Team) { Log.Error("Tried to change to the same team."); return false; } Log.Info($"Changing team to '{team}'..."); try { await _api.ChangeTeam(_roomCode, team, ct); Team = team; Log.Info($"Changed team to '{team}'."); return true; } catch (Exception arg) { Log.Error($"Failed to change the team: {arg}"); return false; } } public async Task GetCard(GoalPool pool, CancellationToken ct = default(CancellationToken)) { if (!IsInRoom) { Log.Error("Tried to get the squares before being connected."); return null; } Log.Info("Getting the squares of the room '" + _roomCode + "'..."); try { Square[] array = await _api.GetSquares(_roomCode, ct); Log.Info($"Got {array.Length} squares for room '{_roomCode}'."); return new Card(array, pool); } catch (Exception arg) { Log.Error($"Failed to get squares for room '{_roomCode}': {arg}"); return null; } } public async Task GetSquares(CancellationToken ct = default(CancellationToken)) { if (!IsInRoom) { Log.Error("Tried to get the squares before being connected."); return null; } Log.Info("Getting the squares of the room '" + _roomCode + "'..."); try { Square[] array = await _api.GetSquares(_roomCode, ct); Log.Info($"Got {array.Length} squares for room '{_roomCode}'."); return array; } catch (Exception arg) { Log.Error($"Failed to get squares for room '{_roomCode}': {arg}"); return null; } } public async Task MarkSquare(int index, CancellationToken ct = default(CancellationToken)) { if (!IsInRoom) { Log.Error("Tried to mark a square before being connected."); return false; } if (Team == Team.None) { Log.Error("Tried to clear a square without being in a team."); return false; } Log.Info($"Marking the square #{index} for the team '{Team}'..."); try { await _api.MarkSquare(_roomCode, Team, index, ct); Log.Info($"Marked the square #{index} for the team '{Team}'."); return true; } catch (Exception arg) { Log.Error($"Failed to mark the square #{index} for the team '{Team}': {arg}"); return false; } } public async Task ClearSquare(int index, CancellationToken ct = default(CancellationToken)) { if (!IsInRoom) { Log.Error("Tried to clear a square before being connected."); return false; } if (Team == Team.None) { Log.Error("Tried to clear a square without being in a team."); return false; } Log.Info($"Clearing the square #{index} for the team '{Team}'..."); try { await _api.ClearSquare(_roomCode, Team, index, ct); Log.Info($"Cleared the square #{index} for the team '{Team}'."); return true; } catch (Exception arg) { Log.Error($"Failed to clear the square #{index} for the team '{Team}': {arg}"); return false; } } public async Task RevealCard(CancellationToken ct = default(CancellationToken)) { if (!IsInRoom) { Log.Error("Tried to reveal the card before being connected."); return false; } Log.Info("Revealing the card for the room '" + _roomCode + "'..."); try { await _api.RevealCard(_roomCode, ct); Log.Info("Revealed the card for the room '" + _roomCode + "'."); return true; } catch (Exception arg) { Log.Error($"Failed to reveal the card for the room '{_roomCode}': {arg}"); return false; } } private void OnMessageReceived(string message) { IEvent obj = JsonConvert.DeserializeObject(message); if (obj == null) { Log.Warning($"Failed to deserialize the message into a '{typeof(IEvent)}': {message}"); } else { _dispatcher.Dispatch(obj); } } public void Dispose() { _client.Dispose(); _socket.Dispose(); } } } namespace BingoAPI.Networking.DTOs { internal record ChangeTeamRequest { [JsonProperty("room")] public required string Code { get; init; } [JsonProperty("color")] public required Team Team { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected ChangeTeamRequest(ChangeTeamRequest original) { Code = original.Code; Team = original.Team; } public ChangeTeamRequest() { } } internal record ClearSquareRequest { [JsonProperty("room")] public required string Code { get; init; } [JsonProperty("color")] public required Team Team { get; init; } [JsonProperty("slot")] public required string Index { get; init; } [JsonProperty("remove_color")] public bool RemoveColor => true; [CompilerGenerated] [SetsRequiredMembers] protected ClearSquareRequest(ClearSquareRequest original) { Code = original.Code; Team = original.Team; Index = original.Index; } public ClearSquareRequest() { } } internal record CreateRoomRequest { [DataMember(Name = "game_type")] public int GameType => 18; [DataMember(Name = "lockout_mode")] private int LockoutMode { get { if (!IsLockout) { return 1; } return 2; } } [DataMember(Name = "is_spectator")] public bool IsSpectator => false; [DataMember(Name = "variant_type")] private int VariantType { get { if (!IsRandomized) { return 18; } return 172; } } [DataMember(Name = "hide_card")] public bool HideCard => false; private const int RANDOMIZED_VARIANT_TYPE = 172; private const int FIXED_BOARD_VARIANT_TYPE = 18; private const int LOCKOUT_MODE = 2; private const int NON_LOCKOUT_MODE = 1; [DataMember(Name = "room_name")] public required string RoomName; [DataMember(Name = "passphrase")] public required string Password; [DataMember(Name = "nickname")] public required string Nickname; public required bool IsLockout; [DataMember(Name = "seed")] public required string Seed; public required bool IsRandomized; [DataMember(Name = "custom_json")] public required string Board; [DataMember(Name = "csrfmiddlewaretoken")] public required string CreationToken; [CompilerGenerated] protected virtual bool PrintMembers(StringBuilder builder) { RuntimeHelpers.EnsureSufficientExecutionStack(); builder.Append("RoomName = "); builder.Append((object?)RoomName); builder.Append(", Password = "); builder.Append((object?)Password); builder.Append(", Nickname = "); builder.Append((object?)Nickname); builder.Append(", GameType = "); builder.Append(GameType.ToString()); builder.Append(", IsLockout = "); builder.Append(IsLockout.ToString()); builder.Append(", Seed = "); builder.Append((object?)Seed); builder.Append(", IsSpectator = "); builder.Append(IsSpectator.ToString()); builder.Append(", IsRandomized = "); builder.Append(IsRandomized.ToString()); builder.Append(", Board = "); builder.Append((object?)Board); builder.Append(", HideCard = "); builder.Append(HideCard.ToString()); builder.Append(", CreationToken = "); builder.Append((object?)CreationToken); return true; } [CompilerGenerated] [SetsRequiredMembers] protected CreateRoomRequest(CreateRoomRequest original) { RoomName = original.RoomName; Password = original.Password; Nickname = original.Nickname; IsLockout = original.IsLockout; Seed = original.Seed; IsRandomized = original.IsRandomized; Board = original.Board; CreationToken = original.CreationToken; } public CreateRoomRequest() { } } internal record GetSocketInformationResponse { [JsonProperty("room")] [JsonRequired] public required string Code { get; init; } [JsonProperty("player")] [JsonRequired] public required string PlayerUUID { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected GetSocketInformationResponse(GetSocketInformationResponse original) { Code = original.Code; PlayerUUID = original.PlayerUUID; } public GetSocketInformationResponse() { } } internal record JoinRoomRequest { [JsonProperty("room")] public required string Code { get; init; } [JsonProperty("password")] public required string Password { get; init; } [JsonProperty("nickname")] public required string Username { get; init; } [JsonProperty("is_spectator")] public bool IsSpectator => false; [CompilerGenerated] [SetsRequiredMembers] protected JoinRoomRequest(JoinRoomRequest original) { Code = original.Code; Password = original.Password; Username = original.Username; } public JoinRoomRequest() { } } internal record JoinRoomResponse { [JsonProperty("socket_key")] [JsonRequired] public required string SocketKey { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected JoinRoomResponse(JoinRoomResponse original) { SocketKey = original.SocketKey; } public JoinRoomResponse() { } } internal record MarkSquareRequest { [JsonProperty("room")] public required string Code { get; init; } [JsonProperty("color")] public required Team Team { get; init; } [JsonProperty("slot")] public required string Index { get; init; } [JsonProperty("remove_color")] public bool RemoveColor => false; [CompilerGenerated] [SetsRequiredMembers] protected MarkSquareRequest(MarkSquareRequest original) { Code = original.Code; Team = original.Team; Index = original.Index; } public MarkSquareRequest() { } } internal record RevealCardRequest { [JsonProperty("room")] public required string Code { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected RevealCardRequest(RevealCardRequest original) { Code = original.Code; } public RevealCardRequest() { } } internal record SendMessageRequest { [JsonProperty("room")] public required string Code { get; init; } [JsonProperty("text")] public required string Message { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected SendMessageRequest(SendMessageRequest original) { Code = original.Code; Message = original.Message; } public SendMessageRequest() { } } [JsonConverter(typeof(SlotIndexConverter))] public record SlotIndex { public readonly int Index; internal SlotIndex(int index) { Index = index; } } internal record Tokens { public required string PublicToken { get; init; } public required string CreationToken { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected Tokens(Tokens original) { PublicToken = original.PublicToken; CreationToken = original.CreationToken; } public Tokens() { } } } namespace BingoAPI.Networking.Converters { internal class ConditionConverter : JsonConverter { private const string ACTION_KEY = "action"; public override bool CanWrite => false; public override void WriteJson(JsonWriter writer, ICondition? value, JsonSerializer serializer) { throw new NotImplementedException(); } public override ICondition ReadJson(JsonReader reader, Type objectType, ICondition? existingValue, bool hasExistingValue, JsonSerializer serializer) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) JObject val = JObject.Load(reader); string text = ((JToken)val).Value((object)"action"); if (text == null) { throw new JsonException(string.Format("Expected '{0}' property: {1}", "action", val)); } ConditionData data = new ConditionData(val); return ConditionRegistry.Create(text, data); } } internal class EventConverter : JsonConverter { public override bool CanWrite => false; public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new InvalidOperationException(); } public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { JObject val = JObject.Load(reader); string text = ((JToken)val).Value((object)"type"); return text switch { "chat" => ((JToken)val).ToObject(), "goal" => ((JToken)val).ToObject(), "color" => ((JToken)val).ToObject(), "revealed" => ((JToken)val).ToObject(), "new-card" => ((JToken)val).ToObject(), "connection" => ((JToken)val).ToObject(), _ => throw new InvalidOperationException($"No event was found of type '{text}': {val}"), }; } public override bool CanConvert(Type objectType) { return objectType == typeof(IEvent); } } internal class SlotIndexConverter : JsonConverter { private const string PREFIX = "slot"; public override void WriteJson(JsonWriter writer, SlotIndex? value, JsonSerializer serializer) { int num = 0; if (value != null) { num = value.Index + 1; } writer.WriteValue(string.Format("{0}{1}", "slot", num)); } public override SlotIndex ReadJson(JsonReader reader, Type objectType, SlotIndex? existingValue, bool hasExistingValue, JsonSerializer serializer) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) if (!(reader.Value is string text)) { throw new JsonException(string.Format("Expected a '{0}', but got '{1}'.", "String", reader.ValueType)); } if (!text.StartsWith("slot")) { throw new JsonException("Expected value starting with 'slot'."); } string text2 = text.Substring("slot".Length); if (!int.TryParse(text2, out var result)) { throw new JsonException("Could not parse index from '" + text2 + "'."); } if (result <= 0) { throw new JsonException("Index must be greater than 0."); } return new SlotIndex(result - 1); } } internal class StringEqualConverter : JsonConverter { private readonly string _value; public override bool CanWrite => false; public StringEqualConverter(string value) { _value = value; } public override void WriteJson(JsonWriter writer, bool value, JsonSerializer serializer) { throw new InvalidOperationException(string.Format("Class '{0}' cannot write a '{1}' as '{2}'.", "StringEqualConverter", typeof(bool), typeof(string))); } public override bool ReadJson(JsonReader reader, Type objectType, bool existingValue, bool hasExistingValue, JsonSerializer serializer) { if (!(reader.Value is string a)) { return false; } return string.Equals(a, _value); } } internal class TeamConverter : JsonConverter { private static readonly Dictionary TeamMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["pink"] = Team.Pink, ["red"] = Team.Red, ["orange"] = Team.Orange, ["brown"] = Team.Brown, ["yellow"] = Team.Yellow, ["green"] = Team.Green, ["teal"] = Team.Teal, ["blue"] = Team.Blue, ["navy"] = Team.Navy, ["purple"] = Team.Purple, ["blank"] = Team.None }; public override void WriteJson(JsonWriter writer, Team value, JsonSerializer serializer) { List list = new List(); foreach (KeyValuePair teamMapping in TeamMappings) { if (teamMapping.Value != Team.None && value.HasFlag(teamMapping.Value)) { list.Add(teamMapping.Key); } } writer.WriteValue(string.Join(" ", list)); } public override Team ReadJson(JsonReader reader, Type objectType, Team existingValue, bool hasExistingValue, JsonSerializer serializer) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) if (!(reader.Value is string text)) { throw new JsonException($"Expected a '{typeof(string)}', but got '{reader.ValueType}'."); } Team team = Team.None; string[] array = text.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text2 in array) { if (!TeamMappings.TryGetValue(text2, out var value)) { throw new InvalidOperationException("Unknown team '" + text2 + "'"); } team |= value; } return team; } } } namespace BingoAPI.Networking.Clients { internal sealed class BingoApiClient { public const Team DEFAULT_TEAM = Team.Red; private readonly HttpClient _client; public BingoApiClient(HttpClient client) { _client = client; } private Task Send(HttpRequestMessage request, CancellationToken ct) { return _client.SendAsync(request, ct); } private async Task SendAsync(HttpRequestMessage request, CancellationToken ct) { using HttpResponseMessage httpResponseMessage = await Send(request, ct); httpResponseMessage.EnsureSuccessStatusCode(); } private async Task SendAndParse(HttpRequestMessage request, CancellationToken ct) { using HttpResponseMessage response = await _client.SendAsync(request, ct); response.EnsureSuccessStatusCode(); T val = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); if (val == null) { throw new InvalidOperationException("Failed to deserialize response to " + typeof(T).Name); } return val; } private async Task GetTokens(CancellationToken ct) { using HttpRequestMessage request = new RequestBuilder().Get().ToEndpoint("").Build(); using HttpResponseMessage response = await Send(request, ct); response.EnsureSuccessStatusCode(); CookieContainer cookieContainer = new CookieContainer(); IEnumerable values = response.Headers.GetValues("Set-Cookie"); foreach (string item in values) { cookieContainer.SetCookies(_client.BaseAddress, item); } CookieCollection cookies = cookieContainer.GetCookies(_client.BaseAddress); Cookie publicTokenCookie = cookies["csrftoken"]; if (publicTokenCookie == null) { throw new KeyNotFoundException("No cookie was set for 'csrftoken'."); } Match match = Regex.Match(await response.Content.ReadAsStringAsync(), "]*name=\"csrfmiddlewaretoken\"[^>]*value=\"(.*?)\"[^>]*>"); if (!match.Success) { throw new KeyNotFoundException("Could not find any input with 'csrfmiddlewaretoken'."); } return new Tokens { PublicToken = publicTokenCookie.Value, CreationToken = match.Groups[1].Value }; } public async Task CreateRoom(CreateRoomSettings settings, CancellationToken ct) { throw new NotImplementedException(); } public async Task JoinRoom(JoinRoomSettings settings, CancellationToken ct) { JoinRoomRequest json = new JoinRoomRequest { Code = settings.Code, Password = settings.Password, Username = settings.Nickname }; using HttpRequestMessage request = new RequestBuilder().Post().ToEndpoint("/api/join-room").WithJson(json) .Build(); return (await SendAndParse(request, ct)).SocketKey; } public async Task MarkSquare(string room, Team team, int index, CancellationToken ct) { MarkSquareRequest json = new MarkSquareRequest { Code = room, Team = team, Index = (index + 1).ToString() }; using HttpRequestMessage request = new RequestBuilder().Put().ToEndpoint("/api/select").WithJson(json) .Build(); await SendAsync(request, ct); } public async Task ClearSquare(string room, Team team, int index, CancellationToken ct) { ClearSquareRequest json = new ClearSquareRequest { Code = room, Team = team, Index = (index + 1).ToString() }; using HttpRequestMessage request = new RequestBuilder().Put().ToEndpoint("/api/select").WithJson(json) .Build(); await SendAsync(request, ct); } public async Task SendMessage(string room, string message, CancellationToken ct) { SendMessageRequest json = new SendMessageRequest { Code = room, Message = message }; using HttpRequestMessage request = new RequestBuilder().Put().ToEndpoint("/api/chat").WithJson(json) .Build(); await SendAsync(request, ct); } public async Task ChangeTeam(string room, Team team, CancellationToken ct) { ChangeTeamRequest json = new ChangeTeamRequest { Code = room, Team = team }; using HttpRequestMessage request = new RequestBuilder().Put().ToEndpoint("/api/color").WithJson(json) .Build(); await SendAsync(request, ct); } public async Task GetSquares(string room, CancellationToken ct) { using HttpRequestMessage request = new RequestBuilder().Get().ToEndpoint("/room/" + room + "/board").Build(); return await SendAndParse(request, ct); } public async Task RevealCard(string room, CancellationToken ct) { RevealCardRequest json = new RevealCardRequest { Code = room }; using HttpRequestMessage request = new RequestBuilder().Put().ToEndpoint("/api/revealed").WithJson(json) .Build(); await SendAsync(request, ct); } public async Task GetSocketInformation(string socketKey, CancellationToken ct) { using HttpRequestMessage request = new RequestBuilder().Get().ToEndpoint("/api/socket/" + socketKey).Build(); return await SendAndParse(request, ct); } } internal sealed class BingoSocketClient : IDisposable { private WebSocket? _socket; private CancellationTokenSource? _cts; private Task? _socketReceiveTask; public async Task Connect(string socketKey, Action onMessageReceived, CancellationToken ct) { if (_socket != null) { throw new InvalidOperationException("Socket is already connected."); } ClientWebSocket socket = new ClientWebSocket(); try { await socket.ConnectAsync(new Uri("wss://sockets.bingosync.com/broadcast"), ct); string s = JsonConvert.SerializeObject((object)new { socket_key = socketKey }); await socket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(s)), WebSocketMessageType.Text, endOfMessage: true, ct); _socket = socket; _cts = new CancellationTokenSource(); _socketReceiveTask = ReceiveLoop(_socket, onMessageReceived, _cts.Token); } catch { socket.Dispose(); throw; } } public async Task Disconnect(CancellationToken ct) { if (_socket == null) { return; } _cts?.Cancel(); try { if (_socket.State == WebSocketState.Open) { await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnecting", ct); } } catch (Exception ex) { Log.Error("Error closing WebSocket: " + ex.Message); } if (_socketReceiveTask != null) { try { await _socketReceiveTask; } catch (OperationCanceledException) { } catch (ObjectDisposedException) { } catch (Exception ex4) { Log.Error("Receive loop failed during disconnect: " + ex4.Message); } } CleanUp(); } private static async Task ReceiveLoop(WebSocket socket, Action onReceive, CancellationToken ct) { byte[] buffer = new byte[1024]; using MemoryStream ms = new MemoryStream(); while (!ct.IsCancellationRequested && socket.State == WebSocketState.Open) { WebSocketReceiveResult webSocketReceiveResult; do { webSocketReceiveResult = await socket.ReceiveAsync(new ArraySegment(buffer), ct); ms.Write(buffer, 0, webSocketReceiveResult.Count); } while (!webSocketReceiveResult.EndOfMessage); if (webSocketReceiveResult.MessageType == WebSocketMessageType.Close) { Log.Debug("Close message was received."); break; } if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) { string text = Encoding.UTF8.GetString(ms.ToArray()); Log.Debug("Message received:\n" + text); try { onReceive(text); } catch (Exception arg) { Log.Error($"Error handling socket message: {arg}"); } } ms.Seek(0L, SeekOrigin.Begin); ms.SetLength(0L); } } private void CleanUp() { _socket?.Dispose(); _cts?.Dispose(); _socket = null; _cts = null; _socketReceiveTask = null; } public void Dispose() { CleanUp(); } } } namespace BingoAPI.Models { public sealed class Card { private struct CardSquare { public readonly Goal Goal; public Team Teams { get; set; } public CardSquare(Square square, Goal goal) { Goal = goal; Teams = square.Teams; } } private readonly CardSquare[] _squares; internal Card(Square[] squares, GoalPool pool) { _squares = new CardSquare[squares.Length]; foreach (Square square in squares) { int index = square.Slot.Index; if (index < 0 || index >= _squares.Length) { throw new ArgumentOutOfRangeException("square"); } if (!pool.TryGet(square, out Goal goal)) { throw new KeyNotFoundException("Failed to find a goal under the name '" + square.Text + "'."); } _squares[index] = new CardSquare(square, goal); } } public Goal GetGoalAt(int index) { return _squares[index].Goal; } public Goal[] GetAllGoals() { HashSet hashSet = new HashSet(); CardSquare[] squares = _squares; for (int i = 0; i < squares.Length; i++) { CardSquare cardSquare = squares[i]; hashSet.Add(cardSquare.Goal); } return hashSet.ToArray(); } public int[] FindByGoal(Goal goal) { List list = new List(); for (int i = 0; i < _squares.Length; i++) { if (!(GetGoalAt(i) != goal)) { list.Add(i); } } return list.ToArray(); } public Team GetTeamsAt(int index) { return _squares[index].Teams; } public bool IsMarkedBy(int index, Team team) { return GetTeamsAt(index).HasFlag(team); } public void Mark(int index, Team team) { _squares[index].Teams |= team; } public void Unmark(int index, Team team) { _squares[index].Teams &= (Team)(ushort)(~(int)team); } } public record Player { [JsonProperty("uuid")] [JsonRequired] public required string UUID { get; init; } [JsonProperty("name")] [JsonRequired] public required string Name { get; init; } [JsonProperty("color")] [JsonRequired] public required Team Team { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected Player(Player original) { UUID = original.UUID; Name = original.Name; Team = original.Team; } public Player() { } } public record Square { [JsonProperty("name")] [JsonRequired] public required string Text { get; init; } [JsonProperty("slot")] [JsonRequired] public required SlotIndex Slot { get; init; } [JsonProperty("colors")] [JsonRequired] public required Team Teams { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected Square(Square original) { Text = original.Text; Slot = original.Slot; Teams = original.Teams; } public Square() { } } [Flags] [JsonConverter(typeof(TeamConverter))] public enum Team : ushort { None = 0, Pink = 1, Red = 2, Orange = 4, Brown = 8, Yellow = 0x10, Green = 0x20, Teal = 0x40, Blue = 0x80, Navy = 0x100, Purple = 0x200 } } namespace BingoAPI.Models.Settings { public record CreateRoomSettings { public string Name { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; public string Nickname { get; set; } = string.Empty; public bool IsRandomized { get; set; } public bool IsLockout { get; set; } public string Seed { get; set; } = string.Empty; } public record JoinRoomSettings { public string Code { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; public string Nickname { get; set; } = string.Empty; } } namespace BingoAPI.Helpers { public static class Log { public enum LogLevel { Debug, Info, Warning, Error } public static Action? Logger { private get; set; } private static void LogMessage(LogLevel level, string? message) { Logger?.Invoke(level, message ?? string.Empty); } internal static void Debug(string? message) { LogMessage(LogLevel.Debug, message); } internal static void Info(string? message) { LogMessage(LogLevel.Info, message); } internal static void Warning(string? message) { LogMessage(LogLevel.Warning, message); } internal static void Error(string? message) { LogMessage(LogLevel.Error, message); } } public static class Network { public static bool TryGetRoomCode(string url, [NotNullWhen(true)] out string? code) { Match match = Regex.Match(url, "(?<=/room/)[a-zA-Z\\d-_]+"); if (!match.Success) { code = null; return false; } code = match.Value; return true; } } } namespace BingoAPI.Goals { public sealed record Goal { [JsonProperty("name")] [JsonRequired] public required string Name { get; init; } [JsonProperty("condition")] [JsonRequired] public required ICondition Condition { get; init; } [CompilerGenerated] [SetsRequiredMembers] private Goal(Goal original) { Name = original.Name; Condition = original.Condition; } public Goal() { } } public sealed class GoalPool : IEnumerable, IEnumerable { private readonly Dictionary _goals = new Dictionary(StringComparer.OrdinalIgnoreCase); public int Count => _goals.Count; public void Add(Goal goal) { if (!TryAdd(goal)) { throw new ArgumentException("The goal has already been added.", "goal"); } } public bool TryAdd(Goal goal) { if (_goals.ContainsKey(goal.Name)) { return false; } _goals.Add(goal.Name, goal); return true; } public bool TryGet(Square square, [NotNullWhen(true)] out Goal? goal) { return _goals.TryGetValue(square.Text, out goal); } public IEnumerator GetEnumerator() { return _goals.Values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public sealed record GoalSet { [JsonProperty("name")] public string Name { get; init; } = string.Empty; [JsonProperty("description")] public string Description { get; init; } = string.Empty; [JsonProperty("goals")] [JsonRequired] public required Goal[] Goals { get; init; } [CompilerGenerated] [SetsRequiredMembers] private GoalSet(GoalSet original) { Name = original.Name; Description = original.Description; Goals = original.Goals; } public GoalSet() { } } public sealed class GoalTracker { public delegate void GoalChangedCallback(Goal goal); private readonly HashSet _trackedGoals = new HashSet(); private readonly HashSet _metGoals = new HashSet(); public event GoalChangedCallback? OnGoalMarked; public event GoalChangedCallback? OnGoalCleared; public bool TryAdd(Goal goal) { return _trackedGoals.Add(goal); } public void Clear() { _trackedGoals.Clear(); _metGoals.Clear(); } public void Evaluate() { foreach (Goal trackedGoal in _trackedGoals) { bool wasInitiallyMet = _metGoals.Contains(trackedGoal); bool isCurrentlyMet; try { isCurrentlyMet = trackedGoal.Condition.IsMet(); } catch (Exception arg) { Log.Error($"Error while evaluating '{trackedGoal.Name}': {arg}"); isCurrentlyMet = false; } DispatchChange(wasInitiallyMet, isCurrentlyMet, trackedGoal); } } private void DispatchChange(bool wasInitiallyMet, bool isCurrentlyMet, Goal goal) { if (wasInitiallyMet != isCurrentlyMet) { if (isCurrentlyMet) { _metGoals.Add(goal); this.OnGoalMarked?.Invoke(goal); } else { _metGoals.Remove(goal); this.OnGoalCleared?.Invoke(goal); } } } } } namespace BingoAPI.Events { public sealed class EventDispatcher { public delegate void ConnectionCallback(Player player); public delegate void DisconnectionCallback(Player player); public delegate void MarkCallback(Player player, Square square, Team team); public delegate void ClearCallback(Player player, Square square, Team team); public delegate void ChatCallback(Player player, string message, ulong timestamp); public delegate void TeamCallback(Player player, Team newTeam); public delegate void RevealCallback(Player player); public delegate void GenerateCallback(Player player, bool isHidden); private Player? _localPlayer; public event ConnectionCallback? OnSelfConnected; public event DisconnectionCallback? OnSelfDisconnected; public event MarkCallback? OnSelfSquareMarked; public event ClearCallback? OnSelfSquareCleared; public event ChatCallback? OnSelfMessageSent; public event TeamCallback? OnSelfTeamChanged; public event RevealCallback? OnSelfCardRevealed; public event GenerateCallback? OnSelfCardGenerated; public event ConnectionCallback? OnOtherConnected; public event DisconnectionCallback? OnOtherDisconnected; public event MarkCallback? OnOtherSquareMarked; public event ClearCallback? OnOtherSquareCleared; public event ChatCallback? OnOtherMessageSent; public event TeamCallback? OnOtherTeamChanged; public event RevealCallback? OnOtherCardRevealed; public event GenerateCallback? OnOtherCardGenerated; private bool IsLocal(Player player) { return player.UUID == _localPlayer?.UUID; } internal void DispatchConnect(Player player) { _localPlayer = player; this.OnSelfConnected?.Invoke(player); } internal void DispatchDisconnect() { if (!(_localPlayer == null)) { this.OnSelfDisconnected?.Invoke(_localPlayer); _localPlayer = null; } } internal void Dispatch(IEvent evt) { if (!(evt is ConnectionEvent connectionEvent)) { if (!(evt is ChatEvent evt2)) { if (!(evt is ColorEvent evt3)) { if (!(evt is GoalEvent goalEvent)) { if (!(evt is CardRevealedEvent evt4)) { if (evt is CardGeneratedEvent evt5) { DispatchCardGenerated(evt5); } } else { DispatchCardRevealed(evt4); } } else if (goalEvent.HasBeenCleared) { DispatchGoalCleared(goalEvent); } else { DispatchGoalMarked(goalEvent); } } else { DispatchColorEvent(evt3); } } else { DispatchChatEvent(evt2); } } else if (connectionEvent.IsConnected) { DispatchConnectedEvent(connectionEvent); } else { DispatchDisconnectedEvent(connectionEvent); } } private void DispatchConnectedEvent(ConnectionEvent evt) { if (!IsLocal(evt.Player)) { this.OnOtherConnected?.Invoke(evt.Player); } } private void DispatchDisconnectedEvent(ConnectionEvent evt) { if (!IsLocal(evt.Player)) { this.OnOtherDisconnected?.Invoke(evt.Player); } } private void DispatchChatEvent(ChatEvent evt) { if (IsLocal(evt.Player)) { this.OnSelfMessageSent?.Invoke(evt.Player, evt.Text, evt.Timestamp); } else { this.OnOtherMessageSent?.Invoke(evt.Player, evt.Text, evt.Timestamp); } } private void DispatchColorEvent(ColorEvent evt) { if (IsLocal(evt.Player)) { this.OnSelfTeamChanged?.Invoke(evt.Player, evt.NewColor); } else { this.OnOtherTeamChanged?.Invoke(evt.Player, evt.NewColor); } } private void DispatchGoalMarked(GoalEvent evt) { if (IsLocal(evt.Player)) { this.OnSelfSquareMarked?.Invoke(evt.Player, evt.Square, evt.Team); } else { this.OnOtherSquareMarked?.Invoke(evt.Player, evt.Square, evt.Team); } } private void DispatchGoalCleared(GoalEvent evt) { if (IsLocal(evt.Player)) { this.OnSelfSquareCleared?.Invoke(evt.Player, evt.Square, evt.Team); } else { this.OnOtherSquareCleared?.Invoke(evt.Player, evt.Square, evt.Team); } } private void DispatchCardRevealed(CardRevealedEvent evt) { if (IsLocal(evt.Player)) { this.OnSelfCardRevealed?.Invoke(evt.Player); } else { this.OnOtherCardRevealed?.Invoke(evt.Player); } } private void DispatchCardGenerated(CardGeneratedEvent evt) { if (IsLocal(evt.Player)) { this.OnSelfCardGenerated?.Invoke(evt.Player, evt.IsCardHidden); } else { this.OnOtherCardGenerated?.Invoke(evt.Player, evt.IsCardHidden); } } } internal interface IEvent { } } namespace BingoAPI.Events.BuiltIn { internal record CardGeneratedEvent : IEvent { [JsonProperty("player")] [JsonRequired] public required Player Player { get; init; } [JsonProperty("hide_card")] [JsonRequired] public required bool IsCardHidden { get; init; } [JsonProperty("timestamp")] [JsonRequired] public required ulong Timestamp { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected CardGeneratedEvent(CardGeneratedEvent original) { Player = original.Player; IsCardHidden = original.IsCardHidden; Timestamp = original.Timestamp; } public CardGeneratedEvent() { } } internal record CardRevealedEvent : IEvent { [JsonProperty("player")] [JsonRequired] public required Player Player { get; init; } [JsonProperty("timestamp")] [JsonRequired] public required ulong Timestamp { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected CardRevealedEvent(CardRevealedEvent original) { Player = original.Player; Timestamp = original.Timestamp; } public CardRevealedEvent() { } } internal record ChatEvent : IEvent { [JsonProperty("player")] [JsonRequired] public required Player Player { get; init; } [JsonProperty("timestamp")] [JsonRequired] public required ulong Timestamp { get; init; } [JsonProperty("text")] [JsonRequired] public required string Text { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected ChatEvent(ChatEvent original) { Player = original.Player; Timestamp = original.Timestamp; Text = original.Text; } public ChatEvent() { } } internal record ColorEvent : IEvent { [JsonProperty("player")] [JsonRequired] public required Player Player { get; init; } [JsonProperty("player_color")] [JsonRequired] public required Team PreviousColor { get; init; } [JsonProperty("color")] [JsonRequired] public required Team NewColor { get; init; } [JsonProperty("timestamp")] [JsonRequired] public required ulong Timestamp { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected ColorEvent(ColorEvent original) { Player = original.Player; PreviousColor = original.PreviousColor; NewColor = original.NewColor; Timestamp = original.Timestamp; } public ColorEvent() { } } internal record ConnectionEvent : IEvent { [JsonProperty("player")] [JsonRequired] public required Player Player { get; init; } [JsonProperty("room")] [JsonRequired] public required string RoomId { get; init; } [JsonProperty("timestamp")] [JsonRequired] public required ulong Timestamp { get; init; } [JsonProperty("event_type")] [JsonConverter(typeof(StringEqualConverter), new object[] { "connected" })] public required bool IsConnected { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected ConnectionEvent(ConnectionEvent original) { Player = original.Player; RoomId = original.RoomId; Timestamp = original.Timestamp; IsConnected = original.IsConnected; } public ConnectionEvent() { } } internal record GoalEvent : IEvent { [JsonProperty("player")] [JsonRequired] public required Player Player { get; init; } [JsonProperty("timestamp")] [JsonRequired] public required ulong Timestamp { get; init; } [JsonProperty("square")] [JsonRequired] public required Square Square { get; init; } [JsonProperty("color")] [JsonRequired] public required Team Team { get; init; } [JsonProperty("remove")] [JsonRequired] public required bool HasBeenCleared { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected GoalEvent(GoalEvent original) { Player = original.Player; Timestamp = original.Timestamp; Square = original.Square; Team = original.Team; HasBeenCleared = original.HasBeenCleared; } public GoalEvent() { } } } namespace BingoAPI.Conditions { [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method)] public class ConditionAttribute : Attribute { private record FactoryEntry { public required string Action { get; init; } public required Func Factory { get; init; } public required string SourceName { get; init; } [CompilerGenerated] [SetsRequiredMembers] protected FactoryEntry(FactoryEntry original) { Action = original.Action; Factory = original.Factory; SourceName = original.SourceName; } public FactoryEntry() { } } public readonly string Action; public ConditionAttribute(string action) { Action = action; } public static void AddAll() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { AddAll(assembly); } } public static void AddAll(Assembly assembly) { IEnumerable types; try { types = assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types; } foreach (Type item in types) { if (!(item == null)) { AddAll(item); } } } public static void AddAll(Type type) { IEnumerable enumerable = GetFactoryMethods(type).Concat(GetFactoryConstructors(type)); foreach (FactoryEntry item in enumerable) { Log.Debug("Trying to add '" + item.Action + "' from '" + item.SourceName + "'."); ConditionRegistry.TryAdd(item.Action, item.Factory); } } private static IEnumerable GetFactoryMethods(Type type) { MethodInfo[] methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); MethodInfo[] array = methods; foreach (MethodInfo method in array) { ConditionAttribute customAttribute = method.GetCustomAttribute(); if (customAttribute == null) { continue; } if (!typeof(ICondition).IsAssignableFrom(method.ReturnType)) { Log.Warning("Method '" + method.Name + "' must return 'ICondition'."); continue; } ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length != 1 || parameters[0].ParameterType != typeof(ConditionData)) { Log.Warning("Method '" + method.Name + "' must only take 'ConditionData'."); continue; } yield return new FactoryEntry { Action = customAttribute.Action, Factory = (ConditionData data) => (ICondition)method.Invoke(null, new object[1] { data }), SourceName = method.Name }; } } private static IEnumerable GetFactoryConstructors(Type type) { ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); ConstructorInfo[] array = constructors; foreach (ConstructorInfo ctor in array) { ConditionAttribute customAttribute = ctor.GetCustomAttribute(); if (customAttribute == null || !typeof(ICondition).IsAssignableFrom(type)) { continue; } ParameterInfo[] parameters = ctor.GetParameters(); if (parameters.Length != 1 || parameters[0].ParameterType != typeof(ConditionData)) { Log.Warning("Constructor '" + type.Name + "' must only take 'ConditionData'."); continue; } yield return new FactoryEntry { Action = customAttribute.Action, Factory = (ConditionData data) => (ICondition)ctor.Invoke(new object[1] { data }), SourceName = (type.FullName ?? type.Name) }; } } } public sealed class ConditionData { private readonly JObject _json; internal ConditionData(JObject json) { _json = json; } private bool TryGetParameter(string key, out T? value) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Invalid comparison between Unknown and I4 value = default(T); JToken obj = _json["params"]; JObject val = (JObject)(object)((obj is JObject) ? obj : null); if (val == null) { return false; } JToken val2 = default(JToken); if (!val.TryGetValue(key, ref val2)) { return false; } if ((int)val2.Type == 10) { return false; } value = val2.ToObject(); return value != null; } public T GetRequiredParameter(string key) where T : notnull { //IL_0028: Unknown result type (might be due to invalid IL or missing references) if (!TryGetParameter(key, out T value) || value == null) { throw new JsonException($"Failed to find '{key}' of type '{typeof(T)}'."); } return value; } public T GetOptionalParameter(string key, T defaultValue) where T : notnull { if (!TryGetParameter(key, out T value)) { return defaultValue; } T val = value; if (val == null) { return defaultValue; } return val; } } public static class ConditionRegistry { private static readonly Dictionary> Factories = new Dictionary>(StringComparer.OrdinalIgnoreCase); public static void TryAdd(string action, Func factory) { if (!Factories.ContainsKey(action)) { Factories.Add(action, factory); } } internal static ICondition Create(string action, ConditionData data) { if (!Factories.TryGetValue(action, out Func value)) { throw new InvalidOperationException("No condition registered under the action '" + action + "'."); } return value(data); } } [JsonConverter(typeof(ConditionConverter))] public interface ICondition { bool IsMet(); } } namespace BingoAPI.Conditions.BuiltIn { internal sealed class AndCondition : ICondition { private readonly ICondition[] _conditions; [Condition("AND")] public AndCondition(ConditionData data) { _conditions = data.GetRequiredParameter("conditions"); } public bool IsMet() { return _conditions.All((ICondition condition) => condition.IsMet()); } } internal sealed class NotCondition : ICondition { private readonly ICondition _condition; [Condition("NOT")] public NotCondition(ConditionData data) { _condition = data.GetRequiredParameter("condition"); } public bool IsMet() { return !_condition.IsMet(); } } internal sealed class OrCondition : ICondition { private readonly ICondition[] _conditions; [Condition("OR")] public OrCondition(ConditionData data) { _conditions = data.GetRequiredParameter("conditions"); } public bool IsMet() { return _conditions.Any((ICondition condition) => condition.IsMet()); } } internal sealed class SomeCondition : ICondition { private readonly ICondition[] _conditions; private readonly uint _amount; [Condition("SOME")] public SomeCondition(ConditionData data) { _conditions = data.GetRequiredParameter("conditions"); _amount = data.GetOptionalParameter("amount", 1u); } public bool IsMet() { if (_conditions.Length < _amount) { return false; } int num = 0; ICondition[] conditions = _conditions; foreach (ICondition condition in conditions) { if (condition.IsMet()) { num++; if (num >= _amount) { return true; } } } return false; } } }