diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f7e30e..a0a8cd6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Changed +- TinyJson DataMember attributes now require a `Name` field to be passed in order for them to be parsed. +- TinyJson public fields and properties will no longer automatically get parsed without a DataMember attribute. + +### Added +- Added Protobuf support for WebSockets via the `Nakama.Protobuf` namespace. + ## [2.7.0] - 2020-10-19 ### Changed - Upgrade code generator to new Swagger format. diff --git a/Nakama.sln b/Nakama.sln index 338971c2..c3016704 100644 --- a/Nakama.sln +++ b/Nakama.sln @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4CBC1D9A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nakama.Tests", "tests\Nakama.Tests\Nakama.Tests.csproj", "{AFD0AF63-EA45-49A6-8889-3E65A48F0521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nakama.Protobuf", "src\Nakama.Protobuf\Nakama.Protobuf.csproj", "{C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,9 +50,22 @@ Global {AFD0AF63-EA45-49A6-8889-3E65A48F0521}.Release|x64.Build.0 = Release|Any CPU {AFD0AF63-EA45-49A6-8889-3E65A48F0521}.Release|x86.ActiveCfg = Release|Any CPU {AFD0AF63-EA45-49A6-8889-3E65A48F0521}.Release|x86.Build.0 = Release|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|x64.Build.0 = Debug|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Debug|x86.Build.0 = Debug|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|Any CPU.Build.0 = Release|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|x64.ActiveCfg = Release|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|x64.Build.0 = Release|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|x86.ActiveCfg = Release|Any CPU + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {10E81453-27EC-4D8E-82A7-DEDEFABBD648} = {651F309B-3615-44A7-A5B4-832B4ACE2258} {AFD0AF63-EA45-49A6-8889-3E65A48F0521} = {4CBC1D9A-0727-4D31-A3BC-484EF98E7617} + {C9CECF38-A4B4-4B91-9ADD-1F37ACA78E2D} = {651F309B-3615-44A7-A5B4-832B4ACE2258} EndGlobalSection EndGlobal diff --git a/src/Nakama.Protobuf/Nakama.Protobuf.csproj b/src/Nakama.Protobuf/Nakama.Protobuf.csproj new file mode 100644 index 00000000..a4193514 --- /dev/null +++ b/src/Nakama.Protobuf/Nakama.Protobuf.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + netstandard2.0;net461 + + + Nakama Authors & contributors + Heroic Labs + Nakama is an open-source server designed to power modern games and apps. This package adds Protobuf support to Websocket messages sent betweens the dotnet-client and the Nakama server. + NakamaClientProtobuf + Apache-2.0 + clientsdk;nakama;gameserver;backend;restapi + https://github.com/heroiclabs/nakama-dotnet + + diff --git a/src/Nakama.Protobuf/ProtobufAdapter.cs b/src/Nakama.Protobuf/ProtobufAdapter.cs new file mode 100644 index 00000000..659b996a --- /dev/null +++ b/src/Nakama.Protobuf/ProtobufAdapter.cs @@ -0,0 +1,263 @@ +/** + * Copyright 2020 The Nakama Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Nakama.SocketInternal; +using Nakama.Ninja.WebSockets; +using ProtoBuf; +using System.IO; + +[module: CompatibilityLevel(CompatibilityLevel.Level300)] + +namespace Nakama.Protobuf +{ + /// + /// A Protobuf adapter which uses the WebSocket protocol with Nakama server. + /// + public class ProtobufAdapter : ISocketAdapter + { + private const int KeepAliveIntervalSec = 15; + private const int MaxMessageSize = 1024 * 256; + private const int SendTimeoutSec = 10; + + /// + public event Action Connected; + + /// + public event Action Closed; + + /// + public event Action ReceivedError; + + /// + public event Action> Received; + + /// + public string Format + { + get + { + return "protobuf"; + } + } + + /// + /// If the WebSocket is connected. + /// + public bool IsConnected { get; private set; } + + /// + /// If the WebSocket is connecting. + /// + public bool IsConnecting { get; private set; } + + private readonly WebSocketClientOptions _options; + private readonly TimeSpan _sendTimeoutSec; + private CancellationTokenSource _cancellationSource; + private WebSocket _webSocket; + private Uri _uri; + + public ProtobufAdapter(int keepAliveIntervalSec = KeepAliveIntervalSec, int sendTimeoutSec = SendTimeoutSec) : + this(new WebSocketClientOptions + { + IncludeExceptionInCloseResponse = true, + KeepAliveInterval = TimeSpan.FromSeconds(keepAliveIntervalSec), + NoDelay = true + }, sendTimeoutSec) {} + + public ProtobufAdapter(WebSocketClientOptions options, int sendTimeoutSec) + { + _options = options; + _sendTimeoutSec = TimeSpan.FromSeconds(sendTimeoutSec); + } + + /// + public void Close() + { + _cancellationSource?.Cancel(); + + if (_webSocket == null) return; + _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + _webSocket = null; + IsConnecting = false; + IsConnected = false; + } + + /// + public async void Connect(Uri uri, int timeout) + { + if (_webSocket != null) + { + ReceivedError?.Invoke(new SocketException((int) SocketError.IsConnected)); + return; + } + + _cancellationSource = new CancellationTokenSource(); + _uri = uri; + IsConnecting = true; + + var clientFactory = new WebSocketClientFactory(); + try + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); + var lcts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationSource.Token, cts.Token); + using (_webSocket = await clientFactory.ConnectAsync(_uri, _options, lcts.Token)) + { + IsConnected = true; + IsConnecting = false; + Connected?.Invoke(); + + await ReceiveLoop(_webSocket, _cancellationSource.Token); + } + } + catch (TaskCanceledException) + { + // No error, the socket got closed via the cancellation signal. + } + catch (ObjectDisposedException) + { + // No error, the socket got closed. + } + catch (Exception e) + { + ReceivedError?.Invoke(e); + } + finally + { + Close(); + Closed?.Invoke(); + } + } + + public WebSocketMessageEnvelope DeserializeEnvelope(ArraySegment buffer) + { + WebSocketMessageEnvelope envelope = null; + + try + { + envelope = Serializer.Deserialize(buffer.AsMemory()); + } + catch (Exception e) + { + ReceivedError?.Invoke(new FormatException("Could not deserialize protocol buffer.", e)); + } + + return envelope; + } + + public void Dispose() + { + _webSocket?.Dispose(); + } + + /// + public async void Send(WebSocketMessageEnvelope envelope, CancellationToken cancellationToken, + bool reliable = true) + { + + if (_webSocket == null) + { + ReceivedError?.Invoke(new SocketException((int) SocketError.NotConnected)); + return; + } + + try + { + var stream = new MemoryStream(); + Serializer.Serialize(stream, envelope); + + var asByteArray = stream.ToArray(); + + var sendTask = _webSocket.SendAsync(new ArraySegment(asByteArray), WebSocketMessageType.Binary, true, cancellationToken); + + await Task.WhenAny(sendTask, Task.Delay(_sendTimeoutSec, cancellationToken)); + } + catch (Exception e) + { + Close(); + ReceivedError?.Invoke(e); + } + } + + public override string ToString() + { + return + $"WebSocketAdapter(IsConnected={IsConnected}, IsConnecting={IsConnecting}, MaxMessageSize={MaxMessageSize}, Uri='{_uri}')"; + } + + private async Task ReceiveLoop(WebSocket webSocket, CancellationToken cancellationToken) + { + var buffer = new byte[MaxMessageSize]; + while (true) + { + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken) + .ConfigureAwait(false); + if (result == null) + { + break; + } + + if (result.MessageType == WebSocketMessageType.Close) + { + break; + } + + var data = await ReadFrames(result, webSocket, buffer); + + if (data.Count == 0) + { + break; + } + + try + { + Received?.Invoke(data); + } + catch (Exception e) + { + ReceivedError?.Invoke(e); + } + } + } + + private async Task> ReadFrames(WebSocketReceiveResult result, WebSocket webSocket, + byte[] buffer) + { + var count = result.Count; + while (!result.EndOfMessage) + { + if (count >= MaxMessageSize) + { + var closeMessage = $"Maximum message size {MaxMessageSize} bytes reached."; + await webSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, + CancellationToken.None); + ReceivedError?.Invoke(new WebSocketException(WebSocketError.HeaderError)); + return new ArraySegment(); + } + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer, count, MaxMessageSize - count), + CancellationToken.None).ConfigureAwait(false); + count += result.Count; + } + + return new ArraySegment(buffer, 0, count); + } + } +} diff --git a/src/Nakama/ApiClient.gen.cs b/src/Nakama/ApiClient.gen.cs index af154375..9faa2089 100644 --- a/src/Nakama/ApiClient.gen.cs +++ b/src/Nakama/ApiClient.gen.cs @@ -545,7 +545,7 @@ public interface IApiAccountFacebookInstantGame { /// - /// + /// /// string SignedPlayerInfo { get; } @@ -3182,23 +3182,23 @@ public override string ToString() } /// - /// + /// /// public interface IRpcStatus { /// - /// + /// /// int Code { get; } /// - /// + /// /// IEnumerable Details { get; } /// - /// + /// /// string Message { get; } } diff --git a/src/Nakama/ChannelJoinMessage.cs b/src/Nakama/ChannelType.cs similarity index 58% rename from src/Nakama/ChannelJoinMessage.cs rename to src/Nakama/ChannelType.cs index 3ea2e67e..f6f33e23 100644 --- a/src/Nakama/ChannelJoinMessage.cs +++ b/src/Nakama/ChannelType.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,33 +14,8 @@ * limitations under the License. */ -using System.Runtime.Serialization; - namespace Nakama { - /// - /// Send a channel join message to the server. - /// - internal class ChannelJoinMessage - { - [DataMember(Name="hidden"), Preserve] - public bool Hidden { get; set; } - - [DataMember(Name="persistence"), Preserve] - public bool Persistence { get; set; } - - [DataMember(Name="target"), Preserve] - public string Target { get; set; } - - [DataMember(Name="type"), Preserve] - public int Type { get; set; } - - public override string ToString() - { - return $"ChannelJoinMessage(Hidden={Hidden}, Persistence={Persistence}, Target='{Target}', Type={Type})"; - } - } - /// /// The available channel types on the server. /// diff --git a/src/Nakama/IChannel.cs b/src/Nakama/IChannel.cs index 2f4a17cc..ca712a8d 100644 --- a/src/Nakama/IChannel.cs +++ b/src/Nakama/IChannel.cs @@ -15,7 +15,6 @@ */ using System.Collections.Generic; -using System.Runtime.Serialization; namespace Nakama { @@ -59,50 +58,4 @@ public interface IChannel /// string UserIdTwo { get; } } - - /// - internal class Channel : IChannel - { - [DataMember(Name="id"), Preserve] - public string Id { get; set; } - - public IEnumerable Presences => _presences ?? UserPresence.NoPresences; - [DataMember(Name="presences"), Preserve] - public List _presences { get; set; } - - public IUserPresence Self => _self; - [DataMember(Name="self"), Preserve] - public UserPresence _self { get; set; } - - [DataMember(Name="room_name"), Preserve] - public string RoomName { get; set; } - - [DataMember(Name="group_id"), Preserve] - public string GroupId { get; set; } - - [DataMember(Name="user_id_one"), Preserve] - public string UserIdOne { get; set; } - - [DataMember(Name="user_id_two"), Preserve] - public string UserIdTwo { get; set; } - - public override bool Equals(object obj) - { - if (!(obj is Channel item)) - { - return false; - } - return Equals(item); - } - - private bool Equals(IChannel other) => string.Equals(Id, other.Id); - - public override int GetHashCode() => Id != null ? Id.GetHashCode() : 0; - - public override string ToString() - { - var presences = string.Join(", ", Presences); - return $"Channel(Id='{Id}', Presences=[{presences}], Self={Self}, RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; - } - } } diff --git a/src/Nakama/IChannelMessageAck.cs b/src/Nakama/IChannelMessageAck.cs index 8d122317..40f61bc9 100644 --- a/src/Nakama/IChannelMessageAck.cs +++ b/src/Nakama/IChannelMessageAck.cs @@ -14,8 +14,6 @@ * limitations under the License. */ -using System.Runtime.Serialization; - namespace Nakama { /// @@ -78,36 +76,4 @@ public interface IChannelMessageAck /// string UserIdTwo { get; } } - - /// - internal class ChannelMessageAck : IChannelMessageAck - { - [DataMember(Name = "channel_id"), Preserve] public string ChannelId { get; set; } - - [DataMember(Name = "code"), Preserve] public int Code { get; set; } - - [DataMember(Name = "create_time"), Preserve] public string CreateTime { get; set; } - - [DataMember(Name = "message_id"), Preserve] public string MessageId { get; set; } - - [DataMember(Name = "persistent"), Preserve] public bool Persistent { get; set; } - - [DataMember(Name = "update_time"), Preserve] public string UpdateTime { get; set; } - - [DataMember(Name = "username"), Preserve] public string Username { get; set; } - - [DataMember(Name="room_name"), Preserve] public string RoomName { get; set; } - - [DataMember(Name="group_id"), Preserve] public string GroupId { get; set; } - - [DataMember(Name="user_id_one"), Preserve] public string UserIdOne { get; set; } - - [DataMember(Name="user_id_two"), Preserve] public string UserIdTwo { get; set; } - - public override string ToString() - { - return - $"ChannelMessageAck(ChannelId='{ChannelId}', Code={Code}, CreateTime={CreateTime}, MessageId='{MessageId}', Persistent={Persistent}, UpdateTime={UpdateTime}, Username='{Username}', RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; - } - } } diff --git a/src/Nakama/IChannelPresenceEvent.cs b/src/Nakama/IChannelPresenceEvent.cs index a6cb4605..33b9b28a 100644 --- a/src/Nakama/IChannelPresenceEvent.cs +++ b/src/Nakama/IChannelPresenceEvent.cs @@ -15,7 +15,6 @@ */ using System.Collections.Generic; -using System.Runtime.Serialization; namespace Nakama { @@ -59,38 +58,4 @@ public interface IChannelPresenceEvent /// string UserIdTwo { get; } } - - /// - internal class ChannelPresenceEvent : IChannelPresenceEvent - { - [DataMember(Name="channel_id"), Preserve] - public string ChannelId { get; set; } - - public IEnumerable Joins => _joins ?? new List(0); - [DataMember(Name="joins"), Preserve] - public List _joins { get; set; } - - public IEnumerable Leaves => _leaves ?? new List(0); - [DataMember(Name="leaves"), Preserve] - public List _leaves { get; set; } - - [DataMember(Name="room_name"), Preserve] - public string RoomName { get; set; } - - [DataMember(Name="group_id"), Preserve] - public string GroupId { get; set; } - - [DataMember(Name="user_id_one"), Preserve] - public string UserIdOne { get; set; } - - [DataMember(Name="user_id_two"), Preserve] - public string UserIdTwo { get; set; } - - public override string ToString() - { - var joins = string.Join(",", Joins); - var leaves = string.Join(",", Leaves); - return $"ChannelPresenceEvent(ChannelId='{ChannelId}', Joins=[{joins}], Leaves=[{leaves}], RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; - } - } } diff --git a/src/Nakama/IMatch.cs b/src/Nakama/IMatch.cs index c9ef0bc4..00af1c80 100644 --- a/src/Nakama/IMatch.cs +++ b/src/Nakama/IMatch.cs @@ -54,29 +54,4 @@ public interface IMatch /// IUserPresence Self { get; } } - - /// - internal class Match : IMatch - { - [DataMember(Name = "authoritative"), Preserve] public bool Authoritative { get; set; } - - [DataMember(Name = "match_id"), Preserve] public string Id { get; set; } - - [DataMember(Name = "label"), Preserve] public string Label { get; set; } - - public IEnumerable Presences => _presences ?? UserPresence.NoPresences; - [DataMember(Name = "presences"), Preserve] public List _presences { get; set; } - - [DataMember(Name = "size"), Preserve] public int Size { get; set; } - - public IUserPresence Self => _self; - [DataMember(Name = "self"), Preserve] public UserPresence _self { get; set; } - - public override string ToString() - { - var presences = string.Join(", ", Presences); - return - $"Match(Authoritative={Authoritative}, Id='{Id}', Label='{Label}', Presences=[{presences}], Size={Size}, Self={Self})"; - } - } } diff --git a/src/Nakama/IMatchPresenceEvent.cs b/src/Nakama/IMatchPresenceEvent.cs index 404fa37e..ea548540 100644 --- a/src/Nakama/IMatchPresenceEvent.cs +++ b/src/Nakama/IMatchPresenceEvent.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ using System.Collections.Generic; -using System.Runtime.Serialization; namespace Nakama { @@ -39,23 +38,4 @@ public interface IMatchPresenceEvent /// string MatchId { get; } } - - /// - internal class MatchPresenceEvent : IMatchPresenceEvent - { - public IEnumerable Joins => _joins ?? UserPresence.NoPresences; - [DataMember(Name = "joins"), Preserve] public List _joins { get; set; } - - public IEnumerable Leaves => _leaves ?? UserPresence.NoPresences; - [DataMember(Name = "leaves"), Preserve] public List _leaves { get; set; } - - [DataMember(Name = "match_id"), Preserve] public string MatchId { get; set; } - - public override string ToString() - { - var joins = string.Join(", ", Joins); - var leaves = string.Join(", ", Leaves); - return $"MatchPresenceEvent(Joins=[{joins}], Leaves=[{leaves}], MatchId='{MatchId}')"; - } - } } diff --git a/src/Nakama/IMatchState.cs b/src/Nakama/IMatchState.cs index b6f27522..6d2de3e6 100644 --- a/src/Nakama/IMatchState.cs +++ b/src/Nakama/IMatchState.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,6 @@ * limitations under the License. */ -using System; -using System.Runtime.Serialization; - namespace Nakama { /// @@ -47,26 +44,4 @@ public interface IMatchState /// IUserPresence UserPresence { get; } } - - /// - internal class MatchState : IMatchState - { - private static readonly byte[] NoBytes = new byte[0]; - - [DataMember(Name = "match_id"), Preserve] public string MatchId { get; set; } - - public long OpCode => Convert.ToInt64(_opCode); - [DataMember(Name = "op_code"), Preserve] public string _opCode { get; set; } - - public byte[] State => _state == null ? NoBytes : Convert.FromBase64String(_state); - [DataMember(Name = "data"), Preserve] public string _state { get; set; } - - public IUserPresence UserPresence => _userPresence; - [DataMember(Name = "presence"), Preserve] public UserPresence _userPresence { get; set; } - - public override string ToString() - { - return $"MatchState(MatchId='{MatchId}', OpCode={OpCode}, State='{_state}', UserPresence={UserPresence})"; - } - } } diff --git a/src/Nakama/IMatchmakerMatched.cs b/src/Nakama/IMatchmakerMatched.cs index 85e69c29..4a42d02b 100644 --- a/src/Nakama/IMatchmakerMatched.cs +++ b/src/Nakama/IMatchmakerMatched.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ using System.Collections.Generic; -using System.Runtime.Serialization; namespace Nakama { @@ -73,50 +72,4 @@ public interface IMatchmakerUser /// IDictionary StringProperties { get; } } - - /// - internal class MatchmakerMatched : IMatchmakerMatched - { - [DataMember(Name = "match_id"), Preserve] public string MatchId { get; set; } - - [DataMember(Name = "ticket"), Preserve] public string Ticket { get; set; } - - [DataMember(Name = "token"), Preserve] public string Token { get; set; } - - public IEnumerable Users => _users ?? new List(0); - [DataMember(Name = "users"), Preserve] public List _users { get; set; } - - public IMatchmakerUser Self => _self; - [DataMember(Name = "self"), Preserve] public MatchmakerUser _self { get; set; } - - public override string ToString() - { - var users = string.Join(", ", Users); - return - $"MatchmakerMatched(MatchId='{MatchId}', Ticket='{Ticket}', Token='{Token}', Users=[{users}], Self={Self})"; - } - } - - /// - internal class MatchmakerUser : IMatchmakerUser - { - public IDictionary NumericProperties => _numericProperties ?? new Dictionary(); - - [DataMember(Name = "numeric_properties"), Preserve] - public Dictionary _numericProperties { get; set; } - - public IUserPresence Presence => _presence; - [DataMember(Name = "presence"), Preserve] public UserPresence _presence { get; set; } - - public IDictionary StringProperties => _stringProperties ?? new Dictionary(); - - [DataMember(Name = "string_properties"), Preserve] - public Dictionary _stringProperties { get; set; } - - public override string ToString() - { - return - $"MatchmakerUser(NumericProperties={NumericProperties}, Presence={Presence}, StringProperties={StringProperties})"; - } - } } diff --git a/src/Nakama/IMatchmakerTicket.cs b/src/Nakama/IMatchmakerTicket.cs index 18003bb5..be79b65d 100644 --- a/src/Nakama/IMatchmakerTicket.cs +++ b/src/Nakama/IMatchmakerTicket.cs @@ -28,16 +28,4 @@ public interface IMatchmakerTicket /// string Ticket { get; } } - - /// - internal class MatchmakerTicket : IMatchmakerTicket - { - [DataMember(Name="ticket"), Preserve] - public string Ticket { get; set; } - - public override string ToString() - { - return $"MatchmakerTicket(Ticket='{Ticket}')"; - } - } } diff --git a/src/Nakama/ISocket.cs b/src/Nakama/ISocket.cs index 1d8d467a..e18c47e6 100644 --- a/src/Nakama/ISocket.cs +++ b/src/Nakama/ISocket.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Nakama.SocketInternal; namespace Nakama { @@ -268,7 +269,7 @@ Task SendMatchStateAsync(string matchId, long opCode, byte[] state, /// The users to unfollow. /// A task which represents the asynchronous operation. Task UnfollowUsersAsync(IEnumerable users); - + /// /// Unfollow one or more users from their status updates. /// diff --git a/src/Nakama/IStatus.cs b/src/Nakama/IStatus.cs index 79d69e70..260ceb6f 100644 --- a/src/Nakama/IStatus.cs +++ b/src/Nakama/IStatus.cs @@ -29,18 +29,4 @@ public interface IStatus /// IEnumerable Presences { get; } } - - /// - internal class Status : IStatus - { - public IEnumerable Presences => _presences ?? UserPresence.NoPresences; - [DataMember(Name="presences"), Preserve] - public List _presences { get; set; } - - public override string ToString() - { - var presences = string.Join(", ", Presences); - return $"Status(Presences=[{presences}])"; - } - } } diff --git a/src/Nakama/IStatusPresenceEvent.cs b/src/Nakama/IStatusPresenceEvent.cs index a4a9b6ed..b36347da 100644 --- a/src/Nakama/IStatusPresenceEvent.cs +++ b/src/Nakama/IStatusPresenceEvent.cs @@ -15,7 +15,6 @@ */ using System.Collections.Generic; -using System.Runtime.Serialization; namespace Nakama { @@ -40,21 +39,4 @@ public interface IStatusPresenceEvent /// IEnumerable Joins { get; } } - - /// - internal class StatusPresenceEvent : IStatusPresenceEvent - { - public IEnumerable Leaves => _leaves ?? UserPresence.NoPresences; - [DataMember(Name = "leaves"), Preserve] public List _leaves { get; set; } - - public IEnumerable Joins => _joins ?? UserPresence.NoPresences; - [DataMember(Name = "joins"), Preserve] public List _joins { get; set; } - - public override string ToString() - { - var joins = string.Join(", ", Joins); - var leaves = string.Join(", ", Leaves); - return $"StatusPresenceEvent(Leaves=[{leaves}], Joins=[{joins}])"; - } - } } diff --git a/src/Nakama/IStreamPresenceEvent.cs b/src/Nakama/IStreamPresenceEvent.cs index 56e25ece..d16edd97 100644 --- a/src/Nakama/IStreamPresenceEvent.cs +++ b/src/Nakama/IStreamPresenceEvent.cs @@ -90,59 +90,4 @@ public interface IStream /// string Subject { get; } } - - /// - internal class StreamPresenceEvent : IStreamPresenceEvent - { - public IEnumerable Leaves => _leaves ?? UserPresence.NoPresences; - [DataMember(Name = "leaves"), Preserve] public List _leaves { get; set; } - - public IEnumerable Joins => _joins ?? UserPresence.NoPresences; - [DataMember(Name = "joins"), Preserve] public List _joins { get; set; } - - public IStream Stream => _stream; - [DataMember(Name = "stream"), Preserve] public Stream _stream { get; set; } - - public override string ToString() - { - var leaves = string.Join(", ", Leaves); - var joins = string.Join(", ", Joins); - return $"StreamPresenceEvent(Leaves=[{leaves}], Joins=[{joins}], Stream={Stream})"; - } - } - - /// - internal class StreamState : IStreamState - { - public IUserPresence Sender => _sender; - [DataMember(Name = "sender"), Preserve] public UserPresence _sender { get; set; } - - public string State => _state; - [DataMember(Name = "data"), Preserve] public string _state { get; set; } - - public IStream Stream => _stream; - [DataMember(Name = "stream"), Preserve] public Stream _stream { get; set; } - - public override string ToString() - { - return $"StreamState(Sender={Sender}, State='{_state}', Stream={Stream})"; - } - } - - /// - internal class Stream : IStream - { - [DataMember(Name = "descriptor"), Preserve] public string Descriptor { get; set; } - - [DataMember(Name = "label"), Preserve] public string Label { get; set; } - - [DataMember(Name = "mode"), Preserve] public int Mode { get; set; } - - [DataMember(Name = "subject"), Preserve] public string Subject { get; set; } - - public override string ToString() - { - return $"Stream(Descriptor='{Descriptor}', Label='{Label}', Mode={Mode}, Subject='{Subject}')"; - } - } } diff --git a/src/Nakama/IUserPresence.cs b/src/Nakama/IUserPresence.cs index 87264b62..4cbcb281 100644 --- a/src/Nakama/IUserPresence.cs +++ b/src/Nakama/IUserPresence.cs @@ -53,46 +53,4 @@ public interface IUserPresence /// string UserId { get; } } - - /// - internal class UserPresence : IUserPresence - { - internal static readonly IReadOnlyList NoPresences = new List(0); - - [DataMember(Name = "persistence"), Preserve] public bool Persistence { get; set; } - - [DataMember(Name = "session_id"), Preserve] public string SessionId { get; set; } - - [DataMember(Name = "status"), Preserve] public string Status { get; set; } - - [DataMember(Name = "username"), Preserve] public string Username { get; set; } - - [DataMember(Name = "user_id"), Preserve] public string UserId { get; set; } - - public override bool Equals(object obj) - { - if (!(obj is UserPresence item)) - { - return false; - } - return Equals(item); - } - - private bool Equals(IUserPresence other) => string.Equals(SessionId, other.SessionId) && string.Equals(UserId, other.UserId); - - public override int GetHashCode() - { - unchecked - { - // ReSharper disable twice NonReadonlyMemberInGetHashCode - return ((SessionId?.GetHashCode() ?? 0) * 397) ^ (UserId?.GetHashCode() ?? 0); - } - } - - public override string ToString() - { - return - $"UserPresence(Persistence={Persistence}, SessionId='{SessionId}', Status='{Status}', Username='{Username}', UserId='{UserId}')"; - } - } } diff --git a/src/Nakama/Socket.cs b/src/Nakama/Socket.cs index 6c23d5aa..b059dbbc 100644 --- a/src/Nakama/Socket.cs +++ b/src/Nakama/Socket.cs @@ -21,6 +21,7 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using Nakama.SocketInternal; using Nakama.TinyJson; namespace Nakama @@ -162,6 +163,7 @@ public async Task AddMatchmakerAsync(string query = "*", int NumericProperties = numericProperties } }; + var response = await SendAsync(envelope); return response.MatchmakerTicket; } @@ -185,7 +187,7 @@ public Task ConnectAsync(ISession session, bool appearOnline = false, var uri = new UriBuilder(_baseUri) { Path = "/ws", - Query = $"lang=en&status={appearOnline}&token={session.AuthToken}" + Query = $"lang=en&status={appearOnline}&token={session.AuthToken}&format={_adapter.Format}" }.Uri; tcs.Task.ContinueWith(_ => { @@ -256,6 +258,7 @@ public async Task JoinChatAsync(string target, ChannelType type, bool Type = (int) type } }; + var response = await SendAsync(envelope); return response.Channel; } @@ -375,7 +378,7 @@ public async Task RpcAsync(string funcId, string payload = null) var envelope = new WebSocketMessageEnvelope { Cid = $"{_cid++}", - Rpc = new ApiRpc + Rpc = new Nakama.SocketInternal.ApiRpc { Id = funcId, Payload = payload @@ -509,8 +512,18 @@ public static ISocket From(IClient client, ISocketAdapter adapter) private void ReceivedMessage(ArraySegment buffer) { - var contents = System.Text.Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); - var envelope = contents.FromJson(); + WebSocketMessageEnvelope envelope; + + try + { + envelope = _adapter.DeserializeEnvelope(buffer); + } + catch (Exception e) + { + Logger?.ErrorFormat("Error deserializing socket envelope: '{0}'", e.Message); + return; + } + try { if (!string.IsNullOrEmpty(envelope.Cid)) @@ -580,7 +593,7 @@ private void ReceivedMessage(ArraySegment buffer) } else { - Logger?.ErrorFormat("Received unrecognised message: '{0}'", contents); + Logger?.ErrorFormat("Received unrecognised message: '{0}'", envelope); } } catch (Exception e) @@ -591,17 +604,15 @@ private void ReceivedMessage(ArraySegment buffer) private Task SendAsync(WebSocketMessageEnvelope envelope) { - var json = envelope.ToJson(); - var buffer = System.Text.Encoding.UTF8.GetBytes(json); if (string.IsNullOrEmpty(envelope.Cid)) { - _adapter.Send(new ArraySegment(buffer), CancellationToken.None); + _adapter.Send(envelope, CancellationToken.None); return null; // No response required. } var completer = new TaskCompletionSource(); _responses[envelope.Cid] = completer; - _adapter.Send(new ArraySegment(buffer), CancellationToken.None); + _adapter.Send(envelope, CancellationToken.None); return completer.Task; } diff --git a/src/Nakama/SocketInternal/ApiChannelMessage.cs b/src/Nakama/SocketInternal/ApiChannelMessage.cs new file mode 100644 index 00000000..411b1693 --- /dev/null +++ b/src/Nakama/SocketInternal/ApiChannelMessage.cs @@ -0,0 +1,118 @@ + + +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class ApiChannelMessage : IApiChannelMessage + { + /// + [DataMember(Name="channel_id", Order = 1), Preserve] + public string ChannelId { get; set; } + + /// + public int Code => _codeValue.HasValue ? _codeValue.Value : _code; + + /// + [DataMember(Name="content", Order = 6), Preserve] + public string Content { get; set; } + + /// + public string CreateTime => _createTimeValue.HasValue ? _createTimeValue.Value.ToString() : _createTime; + + /// + [DataMember(Name="group_id", Order = 11), Preserve] + public string GroupId { get; set; } + + /// + [DataMember(Name="message_id", Order = 2), Preserve] + public string MessageId { get; set; } + + /// + public bool Persistent => _persistentValue.HasValue ? _persistentValue.Value : _persistent; + + /// + [DataMember(Name="room_name", Order = 10), Preserve] + public string RoomName { get; set; } + + /// + [DataMember(Name="sender_id", Order = 4), Preserve] + public string SenderId { get; set; } + + /// + public string UpdateTime => _updateTimeValue.HasValue ? _updateTimeValue.Value.ToString() : _updateTime.ToString(); + + /// + [DataMember(Name="user_id_one", Order = 12), Preserve] + public string UserIdOne { get; set; } + + /// + [DataMember(Name="user_id_two", Order = 13), Preserve] + public string UserIdTwo { get; set; } + + /// + [DataMember(Name="username", Order = 5), Preserve] + public string Username { get; set; } + + [DataMember(Name="code"), Preserve] + private int _code { get; set; } + + [DataMember(Order = 3), Preserve] + private IntValue _codeValue; + + [DataMember(Name="create_time"), Preserve] + private string _createTime; + + [DataMember(Order = 7), Preserve] + private IntValue _createTimeValue; + + [DataMember(Name="persistent"), Preserve] + private bool _persistent; + + [DataMember(Order = 9), Preserve] + private BoolValue _persistentValue; + + [DataMember(Name="update_time"), Preserve] + private int _updateTime; + + [DataMember(Order = 8), Preserve] + private IntValue _updateTimeValue; + + public override string ToString() + { + var output = ""; + output = string.Concat(output, "ChannelId: ", ChannelId, ", "); + output = string.Concat(output, "Code: ", _code, ", "); + output = string.Concat(output, "Content: ", Content, ", "); + output = string.Concat(output, "CreateTime: ", CreateTime, ", "); + output = string.Concat(output, "GroupId: ", GroupId, ", "); + output = string.Concat(output, "MessageId: ", MessageId, ", "); + output = string.Concat(output, "Persistent: ", Persistent, ", "); + output = string.Concat(output, "RoomName: ", RoomName, ", "); + output = string.Concat(output, "SenderId: ", SenderId, ", "); + output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); + output = string.Concat(output, "UserIdOne: ", UserIdOne, ", "); + output = string.Concat(output, "UserIdTwo: ", UserIdTwo, ", "); + output = string.Concat(output, "Username: ", Username, ", "); + return output; + } + } +} diff --git a/src/Nakama/SocketInternal/ApiNotification.cs b/src/Nakama/SocketInternal/ApiNotification.cs new file mode 100644 index 00000000..e82ad038 --- /dev/null +++ b/src/Nakama/SocketInternal/ApiNotification.cs @@ -0,0 +1,75 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + [DataContract] + public class ApiNotification : IApiNotification + { + /// + public int Code => _codeValue.HasValue ? _codeValue.Value : _code; + + /// + [DataMember(Name="content", Order = 3), Preserve] + public string Content { get; set; } + + /// + public string CreateTime => _createTimeValue.HasValue ? _createTimeValue.Value.ToString() : _createTime; + + /// + [DataMember(Name="id", Order = 1), Preserve] + public string Id { get; set; } + + /// + [DataMember(Name="persistent", Order = 7), Preserve] + public bool Persistent { get; set; } + + /// + [DataMember(Name="sender_id", Order = 5), Preserve] + public string SenderId { get; set; } + + /// + [DataMember(Name="subject", Order = 2), Preserve] + public string Subject { get; set; } + + [DataMember(Name="code"), Preserve] + private int _code; + + [DataMember(Order = 4), Preserve] + private int? _codeValue { get; set; } + + [DataMember(Name="create_time"), Preserve] + private string _createTime; + + [DataMember(Order = 6), Preserve] + private IntValue _createTimeValue; + + public override string ToString() + { + var output = ""; + output = string.Concat(output, "Code: ", Code, ", "); + output = string.Concat(output, "Content: ", Content, ", "); + output = string.Concat(output, "CreateTime: ", CreateTime, ", "); + output = string.Concat(output, "Id: ", Id, ", "); + output = string.Concat(output, "Persistent: ", Persistent, ", "); + output = string.Concat(output, "SenderId: ", SenderId, ", "); + output = string.Concat(output, "Subject: ", Subject, ", "); + return output; + } + } +} diff --git a/src/Nakama/SocketInternal/ApiRpc.cs b/src/Nakama/SocketInternal/ApiRpc.cs new file mode 100644 index 00000000..4f870131 --- /dev/null +++ b/src/Nakama/SocketInternal/ApiRpc.cs @@ -0,0 +1,46 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ +/// + [DataContract] + public class ApiRpc : IApiRpc + { + /// + [DataMember(Name="http_key", Order = 1), Preserve] + public string HttpKey { get; set; } + + /// + [DataMember(Name="id", Order = 2), Preserve] + public string Id { get; set; } + + /// + [DataMember(Name="payload", Order = 3), Preserve] + public string Payload { get; set; } + + public override string ToString() + { + var output = ""; + output = string.Concat(output, "HttpKey: ", HttpKey, ", "); + output = string.Concat(output, "Id: ", Id, ", "); + output = string.Concat(output, "Payload: ", Payload, ", "); + return output; + } + } +} diff --git a/src/Nakama/SocketInternal/Channel.cs b/src/Nakama/SocketInternal/Channel.cs new file mode 100644 index 00000000..1dfaeac6 --- /dev/null +++ b/src/Nakama/SocketInternal/Channel.cs @@ -0,0 +1,70 @@ + + +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal + { + /// + [DataContract] + public class Channel : IChannel + { + [DataMember(Name="id", Order = 1), Preserve] + public string Id { get; set; } + + public IEnumerable Presences => _presences ?? UserPresence.NoPresences; + [DataMember(Name="presences", Order = 2), Preserve] + public List _presences { get; set; } + + public IUserPresence Self => _self; + [DataMember(Name="self", Order = 3), Preserve] + public UserPresence _self { get; set; } + + [DataMember(Name="room_name", Order = 4), Preserve] + public string RoomName { get; set; } + + [DataMember(Name="group_id", Order = 5), Preserve] + public string GroupId { get; set; } + + [DataMember(Name="user_id_one", Order = 6), Preserve] + public string UserIdOne { get; set; } + + [DataMember(Name="user_id_two", Order = 7), Preserve] + public string UserIdTwo { get; set; } + + public override bool Equals(object obj) + { + if (!(obj is Channel item)) + { + return false; + } + return Equals(item); + } + + private bool Equals(IChannel other) => string.Equals(Id, other.Id); + + public override int GetHashCode() => Id != null ? Id.GetHashCode() : 0; + + public override string ToString() + { + var presences = string.Join(", ", Presences); + return $"Channel(Id='{Id}', Presences=[{presences}], Self={Self}, RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; + } + } + } diff --git a/src/Nakama/SocketInternal/ChannelJoinMessage.cs b/src/Nakama/SocketInternal/ChannelJoinMessage.cs new file mode 100644 index 00000000..5a6ded40 --- /dev/null +++ b/src/Nakama/SocketInternal/ChannelJoinMessage.cs @@ -0,0 +1,70 @@ +/** + * Copyright 2020 The Nakama Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + /// Send a channel join message to the server. + /// + [DataContract] + public class ChannelJoinMessage + { + public bool Hidden + { + get => _hiddenValue.HasValue ? _hiddenValue.Value : _hidden; + set + { + _hidden = value; + _hiddenValue.Value = value; + } + } + + public bool Persistence + { + get => _persistenceValue.HasValue ? _persistenceValue.Value : _persistence; + set + { + _persistenceValue.Value = value; + _persistence = value; + } + } + + [DataMember(Name="target", Order = 1), Preserve] + public string Target { get; set; } + + [DataMember(Name="type", Order = 2), Preserve] + public int Type { get; set; } + + [DataMember(Order = 4), Preserve] + private BoolValue _hiddenValue; + + [DataMember(Name="hidden"), Preserve] + private bool _hidden; + + [DataMember(Name="persistence"), Preserve] + private bool _persistence; + + [DataMember(Order = 3), Preserve] + private BoolValue _persistenceValue; + + public override string ToString() + { + return $"ChannelJoinMessage(Hidden={Hidden}, Persistence={Persistence}, Target='{Target}', Type={Type})"; + } + } +} diff --git a/src/Nakama/ChannelLeaveMessage.cs b/src/Nakama/SocketInternal/ChannelLeaveMessage.cs similarity index 82% rename from src/Nakama/ChannelLeaveMessage.cs rename to src/Nakama/SocketInternal/ChannelLeaveMessage.cs index 37cf3268..8b25ffc8 100644 --- a/src/Nakama/ChannelLeaveMessage.cs +++ b/src/Nakama/SocketInternal/ChannelLeaveMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// A leave message to a chat channel. /// - internal class ChannelLeaveMessage + [DataContract] + public class ChannelLeaveMessage { - [DataMember(Name="channel_id"), Preserve] + [DataMember(Name="channel_id", Order = 1), Preserve] public string ChannelId { get; set; } public override string ToString() diff --git a/src/Nakama/SocketInternal/ChannelMessageAck.cs b/src/Nakama/SocketInternal/ChannelMessageAck.cs new file mode 100644 index 00000000..75b97d96 --- /dev/null +++ b/src/Nakama/SocketInternal/ChannelMessageAck.cs @@ -0,0 +1,84 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class ChannelMessageAck : IChannelMessageAck + { + [DataMember(Name = "channel_id", Order = 1), Preserve] + public string ChannelId { get; set; } + + public int Code => _codeValue.HasValue ? _codeValue.Value : _code; + + public string CreateTime => _createTimeValue.HasValue ? _createTimeValue.Value.ToString() : _createTime; + + [DataMember(Name = "message_id", Order = 2), Preserve] + public string MessageId { get; set; } + + public bool Persistent => _persistentValue.HasValue ? _persistentValue.Value : _persistent; + + public string UpdateTime => _updateTimeValue.HasValue ? _updateTimeValue.Value.ToString() : _updateTime; + + [DataMember(Name = "username", Order = 4), Preserve] + public string Username { get; set; } + + [DataMember(Name="room_name", Order = 8), Preserve] + public string RoomName { get; set; } + + [DataMember(Name="group_id", Order = 9), Preserve] + public string GroupId { get; set; } + + [DataMember(Name="user_id_one", Order = 10), Preserve] + public string UserIdOne { get; set; } + + [DataMember(Name="user_id_two", Order = 11), Preserve] + public string UserIdTwo { get; set; } + + [DataMember(Name = "code"), Preserve] + private int _code; + + [DataMember(Order = 3), Preserve] + private IntValue _codeValue; + + [DataMember(Name = "create_time"), Preserve] + private string _createTime; + + [DataMember(Order = 5), Preserve] + private IntValue _createTimeValue; + + [DataMember(Name = "persistent"), Preserve] + private bool _persistent; + + [DataMember(Order = 7), Preserve] + private BoolValue _persistentValue; + + [DataMember(Name = "update_time"), Preserve] + private string _updateTime; + + [DataMember(Order = 6), Preserve] + private IntValue _updateTimeValue; + + public override string ToString() + { + return + $"ChannelMessageAck(ChannelId='{ChannelId}', Code={Code}, CreateTime={CreateTime}, MessageId='{MessageId}', Persistent={Persistent}, UpdateTime={UpdateTime}, Username='{Username}', RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; + } + } +} diff --git a/src/Nakama/SocketInternal/ChannelPresenceEvent.cs b/src/Nakama/SocketInternal/ChannelPresenceEvent.cs new file mode 100644 index 00000000..7d2387b8 --- /dev/null +++ b/src/Nakama/SocketInternal/ChannelPresenceEvent.cs @@ -0,0 +1,57 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ +/// + [DataContract] + public class ChannelPresenceEvent : IChannelPresenceEvent + { + [DataMember(Name="channel_id", Order = 1), Preserve] + public string ChannelId { get; set; } + + public IEnumerable Joins => _joins ?? new List(0); + [DataMember(Name="joins", Order = 2), Preserve] + public List _joins { get; set; } + + public IEnumerable Leaves => _leaves ?? new List(0); + [DataMember(Name="leaves", Order = 3), Preserve] + public List _leaves { get; set; } + + [DataMember(Name="room_name", Order = 4), Preserve] + public string RoomName { get; set; } + + [DataMember(Name="group_id", Order= 5), Preserve] + public string GroupId { get; set; } + + [DataMember(Name="user_id_one", Order = 6), Preserve] + public string UserIdOne { get; set; } + + [DataMember(Name="user_id_two", Order = 7), Preserve] + public string UserIdTwo { get; set; } + + public override string ToString() + { + var joins = string.Join(",", Joins); + var leaves = string.Join(",", Leaves); + return $"ChannelPresenceEvent(ChannelId='{ChannelId}', Joins=[{joins}], Leaves=[{leaves}], RoomName='{RoomName}', GroupId='{GroupId}', UserIdOne='{UserIdOne}', UserIdTwo='{UserIdTwo}')"; + } + } +} + diff --git a/src/Nakama/ChannelRemoveMessage.cs b/src/Nakama/SocketInternal/ChannelRemoveMessage.cs similarity index 79% rename from src/Nakama/ChannelRemoveMessage.cs rename to src/Nakama/SocketInternal/ChannelRemoveMessage.cs index cadebf49..a110e8d8 100644 --- a/src/Nakama/ChannelRemoveMessage.cs +++ b/src/Nakama/SocketInternal/ChannelRemoveMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,18 @@ using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// Remove a message from a chat channel. /// - internal class ChannelRemoveMessage + [DataContract] + public class ChannelRemoveMessage { - [DataMember(Name="channel_id"), Preserve] + [DataMember(Name="channel_id", Order = 1), Preserve] public string ChannelId { get; set; } - [DataMember(Name="message_id"), Preserve] + [DataMember(Name="message_id", Order = 2), Preserve] public string MessageId { get; set; } public override string ToString() diff --git a/src/Nakama/ChannelSendMessage.cs b/src/Nakama/SocketInternal/ChannelSendMessage.cs similarity index 79% rename from src/Nakama/ChannelSendMessage.cs rename to src/Nakama/SocketInternal/ChannelSendMessage.cs index 7ca2135e..e5647f9e 100644 --- a/src/Nakama/ChannelSendMessage.cs +++ b/src/Nakama/SocketInternal/ChannelSendMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,18 @@ using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// Send a chat message to a channel on the server. /// - internal class ChannelSendMessage + [DataContract] + public class ChannelSendMessage { - [DataMember(Name="channel_id"), Preserve] + [DataMember(Name="channel_id", Order = 1), Preserve] public string ChannelId { get; set; } - [DataMember(Name="content"), Preserve] + [DataMember(Name="content", Order = 2), Preserve] public string Content { get; set; } public override string ToString() diff --git a/src/Nakama/ChannelUpdateMessage.cs b/src/Nakama/SocketInternal/ChannelUpdateMessage.cs similarity index 79% rename from src/Nakama/ChannelUpdateMessage.cs rename to src/Nakama/SocketInternal/ChannelUpdateMessage.cs index 085d3a87..aff73b61 100644 --- a/src/Nakama/ChannelUpdateMessage.cs +++ b/src/Nakama/SocketInternal/ChannelUpdateMessage.cs @@ -16,20 +16,21 @@ using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// Update a chat message which has been sent to a channel. /// - internal class ChannelUpdateMessage + [DataContract] + public class ChannelUpdateMessage { - [DataMember(Name="channel_id"), Preserve] + [DataMember(Name="channel_id", Order = 1), Preserve] public string ChannelId { get; set; } - [DataMember(Name="message_id"), Preserve] + [DataMember(Name="message_id", Order = 2), Preserve] public string MessageId { get; set; } - [DataMember(Name="content"), Preserve] + [DataMember(Name="content", Order = 3), Preserve] public string Content { get; set; } public override string ToString() diff --git a/src/Nakama/ISocketAdapter.cs b/src/Nakama/SocketInternal/ISocketAdapter.cs similarity index 78% rename from src/Nakama/ISocketAdapter.cs rename to src/Nakama/SocketInternal/ISocketAdapter.cs index 48c3a83b..f4aefccc 100644 --- a/src/Nakama/ISocketAdapter.cs +++ b/src/Nakama/SocketInternal/ISocketAdapter.cs @@ -17,7 +17,7 @@ using System; using System.Threading; -namespace Nakama +namespace Nakama.SocketInternal { /// /// An adapter which implements a socket with a protocol supported by Nakama. @@ -44,6 +44,11 @@ public interface ISocketAdapter : IDisposable /// event Action> Received; + /// + /// The format of the socket messages. + /// + string Format { get; } + /// /// If the socket is connected. /// @@ -66,12 +71,20 @@ public interface ISocketAdapter : IDisposable /// The timeout for the connect attempt on the socket. void Connect(Uri uri, int timeout); + + /// + /// Deserialize a WebSocketMessageEnvelope from an array of bytes. + /// + /// The array of bytes. + /// The deserialized envelope. + WebSocketMessageEnvelope DeserializeEnvelope(ArraySegment buffer); + /// /// Send data to the server with an asynchronous operation. /// - /// The buffer with the message to send. + /// The envelope with the message to send. /// A cancellation token used to propagate when the operation should be canceled. /// If the message should be sent reliably (will be ignored by some protocols). - void Send(ArraySegment buffer, CancellationToken cancellationToken, bool reliable = true); + void Send(WebSocketMessageEnvelope envelope, CancellationToken cancellationToken, bool reliable = true); } } diff --git a/src/Nakama/SocketInternal/Match.cs b/src/Nakama/SocketInternal/Match.cs new file mode 100644 index 00000000..4267db28 --- /dev/null +++ b/src/Nakama/SocketInternal/Match.cs @@ -0,0 +1,60 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class Match : IMatch + { + [DataMember(Name = "authoritative", Order = 2), Preserve] + public bool Authoritative { get; set; } + + [DataMember(Name = "match_id", Order = 1), Preserve] + public string Id { get; set; } + + public string Label => _labelValue.Value ?? _label; + + public IEnumerable Presences => _presences ?? UserPresence.NoPresences; + + [DataMember(Name = "presences", Order = 5), Preserve] + private List _presences { get; set; } + + [DataMember(Name = "size", Order = 4), Preserve] + public int Size { get; set; } + + public IUserPresence Self => _self; + + [DataMember(Name = "self", Order = 6), Preserve] + private UserPresence _self; + + [DataMember(Order = 3), Preserve] + private StringValue _labelValue; + + [DataMember(Name = "label"), Preserve] + private string _label; + + public override string ToString() + { + var presences = string.Join(", ", Presences); + return + $"Match(Authoritative={Authoritative}, Id='{Id}', Label='{Label}', Presences=[{presences}], Size={Size}, Self={Self})"; + } + } +} diff --git a/src/Nakama/SocketInternal/MatchCreateMessage.cs b/src/Nakama/SocketInternal/MatchCreateMessage.cs new file mode 100644 index 00000000..52a7ffb0 --- /dev/null +++ b/src/Nakama/SocketInternal/MatchCreateMessage.cs @@ -0,0 +1,32 @@ + + +using System.Runtime.Serialization; +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +namespace Nakama.SocketInternal +{ + /// + /// A create message for a match on the server. + /// + [DataContract] + public class MatchCreateMessage + { + public override string ToString() + { + return "MatchCreateMessage()"; + } + } +} diff --git a/src/Nakama/MatchJoinMessage.cs b/src/Nakama/SocketInternal/MatchJoinMessage.cs similarity index 78% rename from src/Nakama/MatchJoinMessage.cs rename to src/Nakama/SocketInternal/MatchJoinMessage.cs index 835074ea..90482e94 100644 --- a/src/Nakama/MatchJoinMessage.cs +++ b/src/Nakama/SocketInternal/MatchJoinMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,20 +17,21 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// A join message for a match on the server. /// - internal class MatchJoinMessage + [DataContract] + public class MatchJoinMessage { - [DataMember(Name="match_id"), Preserve] + [DataMember(Name="match_id", Order = 1), Preserve] public string MatchId { get; set; } - [DataMember(Name="token"), Preserve] + [DataMember(Name="token", Order = 2), Preserve] public string Token { get; set; } - [DataMember(Name="metadata"), Preserve] + [DataMember(Name="metadata", Order = 3), Preserve] public IDictionary Metadata { get; set; } public override string ToString() diff --git a/src/Nakama/MatchLeaveMessage.cs b/src/Nakama/SocketInternal/MatchLeaveMessage.cs similarity index 82% rename from src/Nakama/MatchLeaveMessage.cs rename to src/Nakama/SocketInternal/MatchLeaveMessage.cs index 5ea9cb63..d30c57ab 100644 --- a/src/Nakama/MatchLeaveMessage.cs +++ b/src/Nakama/SocketInternal/MatchLeaveMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// A leave message for a match on the server. /// - internal class MatchLeaveMessage + [DataContract] + public class MatchLeaveMessage { - [DataMember(Name="match_id"), Preserve] + [DataMember(Name="match_id", Order = 1), Preserve] public string MatchId { get; set; } public override string ToString() diff --git a/src/Nakama/SocketInternal/MatchPresenceEvent.cs b/src/Nakama/SocketInternal/MatchPresenceEvent.cs new file mode 100644 index 00000000..aa83d0a7 --- /dev/null +++ b/src/Nakama/SocketInternal/MatchPresenceEvent.cs @@ -0,0 +1,47 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class MatchPresenceEvent : IMatchPresenceEvent + { + public IEnumerable Joins => _joins ?? UserPresence.NoPresences; + + [DataMember(Name = "joins", Order = 2), Preserve] + private List _joins { get; set; } + + public IEnumerable Leaves => _leaves ?? UserPresence.NoPresences; + + [DataMember(Name = "leaves", Order = 3), Preserve] + private List _leaves; + + [DataMember(Name = "match_id", Order = 1), Preserve] + public string MatchId { get; set; } + + public override string ToString() + { + var joins = string.Join(", ", Joins); + var leaves = string.Join(", ", Leaves); + return $"MatchPresenceEvent(Joins=[{joins}], Leaves=[{leaves}], MatchId='{MatchId}')"; + } + } + +} diff --git a/src/Nakama/MatchSendMessage.cs b/src/Nakama/SocketInternal/MatchSendMessage.cs similarity index 71% rename from src/Nakama/MatchSendMessage.cs rename to src/Nakama/SocketInternal/MatchSendMessage.cs index f26e3130..3ab8b425 100644 --- a/src/Nakama/MatchSendMessage.cs +++ b/src/Nakama/SocketInternal/MatchSendMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,25 +17,29 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// Send new state to a match on the server. /// - internal class MatchSendMessage + [DataContract] + public class MatchSendMessage { - [DataMember(Name="match_id"), Preserve] + [DataMember(Name="match_id", Order = 1), Preserve] public string MatchId { get; set; } - [DataMember(Name="op_code"), Preserve] + [DataMember(Name="op_code", Order = 2), Preserve] public string OpCode { get; set; } - [DataMember(Name="presences"), Preserve] + [DataMember(Name="presences", Order = 4), Preserve] public List Presences { get; set; } - [DataMember(Name="data"), Preserve] + [DataMember(Name="data", Order = 3), Preserve] public string State { get; set; } + [DataMember(Name="reliable", Order = 5), Preserve] + public bool Reliable { get; set; } + public override string ToString() { var presences = string.Join(", ", Presences); diff --git a/src/Nakama/SocketInternal/MatchState.cs b/src/Nakama/SocketInternal/MatchState.cs new file mode 100644 index 00000000..3ad49c89 --- /dev/null +++ b/src/Nakama/SocketInternal/MatchState.cs @@ -0,0 +1,51 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class MatchState : IMatchState + { + private static readonly byte[] NoBytes = new byte[0]; + + [DataMember(Name = "match_id", Order = 1), Preserve] + public string MatchId { get; set; } + + public long OpCode => Convert.ToInt64(_opCode); + + [DataMember(Name = "op_code", Order = 3), Preserve] + private string _opCode { get; set; } + + public byte[] State => _state == null ? NoBytes : Convert.FromBase64String(_state); + + [DataMember(Name = "data", Order = 4), Preserve] + private string _state; + + public IUserPresence UserPresence => _userPresence; + + [DataMember(Name = "presence", Order = 2), Preserve] + private UserPresence _userPresence; + + public override string ToString() + { + return $"MatchState(MatchId='{MatchId}', OpCode={OpCode}, State='{_state}', UserPresence={UserPresence})"; + } + } +} diff --git a/src/Nakama/MatchmakerAddMessage.cs b/src/Nakama/SocketInternal/MatchmakerAddMessage.cs similarity index 66% rename from src/Nakama/MatchmakerAddMessage.cs rename to src/Nakama/SocketInternal/MatchmakerAddMessage.cs index e5c36147..c96fef9e 100644 --- a/src/Nakama/MatchmakerAddMessage.cs +++ b/src/Nakama/SocketInternal/MatchmakerAddMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,25 +17,30 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// Add the user to the matchmaker pool with properties. /// - internal class MatchmakerAddMessage + [DataContract] + public class MatchmakerAddMessage { - [DataMember(Name = "max_count"), Preserve] public int MaxCount { get; set; } - [DataMember(Name = "min_count"), Preserve] public int MinCount { get; set; } + [DataMember(Name = "min_count", Order = 1), Preserve] + public int MinCount { get; set; } - [DataMember(Name = "numeric_properties"), Preserve] - public Dictionary NumericProperties { get; set; } + [DataMember(Name = "max_count", Order = 2), Preserve] + public int MaxCount { get; set; } - [DataMember(Name = "query"), Preserve] public string Query { get; set; } + [DataMember(Name = "query", Order = 3), Preserve] + public string Query { get; set; } - [DataMember(Name = "string_properties"), Preserve] + [DataMember(Name = "string_properties", Order = 4), Preserve] public Dictionary StringProperties { get; set; } + [DataMember(Name = "numeric_properties", Order = 5), Preserve] + public Dictionary NumericProperties { get; set; } + public override string ToString() { return diff --git a/src/Nakama/SocketInternal/MatchmakerMatched.cs b/src/Nakama/SocketInternal/MatchmakerMatched.cs new file mode 100644 index 00000000..e412d363 --- /dev/null +++ b/src/Nakama/SocketInternal/MatchmakerMatched.cs @@ -0,0 +1,78 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class MatchmakerMatched : IMatchmakerMatched + { + [DataMember(Name = "match_id", Order = 2), Preserve] + public string MatchId { get; set; } + + [DataMember(Name = "ticket", Order = 1), Preserve] + public string Ticket { get; set; } + + [DataMember(Name = "token", Order = 3), Preserve] + public string Token { get; set; } + + public IEnumerable Users => _users ?? new List(0); + + [DataMember(Name = "users", Order = 4), Preserve] + private List _users { get; set; } + + public IMatchmakerUser Self => _self; + + [DataMember(Name = "self", Order = 5), Preserve] + private MatchmakerUser _self { get; set; } + + public override string ToString() + { + var users = string.Join(", ", Users); + return + $"MatchmakerMatched(MatchId='{MatchId}', Ticket='{Ticket}', Token='{Token}', Users=[{users}], Self={Self})"; + } + } + + /// + [DataContract] + public class MatchmakerUser : IMatchmakerUser + { + public IDictionary NumericProperties => _numericProperties ?? new Dictionary(); + + [DataMember(Name = "numeric_properties", Order = 1), Preserve] + private Dictionary _numericProperties { get; set; } + + public IUserPresence Presence => _presence; + + [DataMember(Name = "presence", Order = 2), Preserve] + private UserPresence _presence { get; set; } + + public IDictionary StringProperties => _stringProperties ?? new Dictionary(); + + [DataMember(Name = "string_properties", Order = 3), Preserve] + private Dictionary _stringProperties; + + public override string ToString() + { + return + $"MatchmakerUser(NumericProperties={NumericProperties}, Presence={Presence}, StringProperties={StringProperties})"; + } + } +} diff --git a/src/Nakama/MatchmakerRemoveMessage.cs b/src/Nakama/SocketInternal/MatchmakerRemoveMessage.cs similarity index 82% rename from src/Nakama/MatchmakerRemoveMessage.cs rename to src/Nakama/SocketInternal/MatchmakerRemoveMessage.cs index 01acebb8..a7d4ce6f 100644 --- a/src/Nakama/MatchmakerRemoveMessage.cs +++ b/src/Nakama/SocketInternal/MatchmakerRemoveMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// Remove the user from the matchmaker pool by ticket. /// - internal class MatchmakerRemoveMessage + [DataContract] + public class MatchmakerRemoveMessage { - [DataMember(Name="ticket"), Preserve] + [DataMember(Name="ticket", Order = 1), Preserve] public string Ticket { get; set; } public override string ToString() diff --git a/src/Nakama/SocketInternal/MatchmakerTicket.cs b/src/Nakama/SocketInternal/MatchmakerTicket.cs new file mode 100644 index 00000000..f88aa557 --- /dev/null +++ b/src/Nakama/SocketInternal/MatchmakerTicket.cs @@ -0,0 +1,33 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class MatchmakerTicket : IMatchmakerTicket + { + [DataMember(Name="ticket", Order = 1), Preserve] + public string Ticket { get; set; } + + public override string ToString() + { + return $"MatchmakerTicket(Ticket='{Ticket}')"; + } + } +} diff --git a/src/Nakama/SocketInternal/NotificationList.cs b/src/Nakama/SocketInternal/NotificationList.cs new file mode 100644 index 00000000..adcec009 --- /dev/null +++ b/src/Nakama/SocketInternal/NotificationList.cs @@ -0,0 +1,46 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class ApiNotificationList : IApiNotificationList + { + + /// + [DataMember(Name="cacheable_cursor", Order = 2), Preserve] + public string CacheableCursor { get; set; } + + /// + public IEnumerable Notifications => _notifications ?? new List(0); + + [DataMember(Name="notifications", Order = 1), Preserve] + private List _notifications; + + public override string ToString() + { + var output = ""; + output = string.Concat(output, "CacheableCursor: ", CacheableCursor, ", "); + output = string.Concat(output, "Notifications: [", string.Join(", ", Notifications), "], "); + return output; + } + } +} + diff --git a/src/Nakama/MatchCreateMessage.cs b/src/Nakama/SocketInternal/Status.cs similarity index 53% rename from src/Nakama/MatchCreateMessage.cs rename to src/Nakama/SocketInternal/Status.cs index 2223454b..148fd00b 100644 --- a/src/Nakama/MatchCreateMessage.cs +++ b/src/Nakama/SocketInternal/Status.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,24 @@ * limitations under the License. */ -namespace Nakama +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal { - /// - /// A create message for a match on the server. - /// - internal class MatchCreateMessage + /// + [DataContract] + public class Status : IStatus { + public IEnumerable Presences => _presences ?? UserPresence.NoPresences; + + [DataMember(Name="presences", Order = 1), Preserve] + private List _presences; + public override string ToString() { - return "MatchCreateMessage()"; + var presences = string.Join(", ", Presences); + return $"Status(Presences=[{presences}])"; } } } diff --git a/src/Nakama/StatusFollowMessage.cs b/src/Nakama/SocketInternal/StatusFollowMessage.cs similarity index 74% rename from src/Nakama/StatusFollowMessage.cs rename to src/Nakama/SocketInternal/StatusFollowMessage.cs index 80d63508..93f6c163 100644 --- a/src/Nakama/StatusFollowMessage.cs +++ b/src/Nakama/SocketInternal/StatusFollowMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,19 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// Follow one or more other users for status updates. /// - internal class StatusFollowMessage + [DataContract] + public class StatusFollowMessage { - [DataMember(Name = "user_ids"), Preserve] public List UserIds { get; set; } + [DataMember(Name = "user_ids", Order = 1), Preserve] + public List UserIds { get; set; } - [DataMember(Name = "usernames"), Preserve] public List Usernames { get; set; } + [DataMember(Name = "usernames", Order = 2), Preserve] + public List Usernames { get; set; } public override string ToString() { diff --git a/src/Nakama/SocketInternal/StatusPrescenceEvent.cs b/src/Nakama/SocketInternal/StatusPrescenceEvent.cs new file mode 100644 index 00000000..9ab78321 --- /dev/null +++ b/src/Nakama/SocketInternal/StatusPrescenceEvent.cs @@ -0,0 +1,43 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class StatusPresenceEvent : IStatusPresenceEvent + { + public IEnumerable Leaves => _leaves ?? UserPresence.NoPresences; + + [DataMember(Name = "leaves", Order = 3), Preserve] + private List _leaves; + + public IEnumerable Joins => _joins ?? UserPresence.NoPresences; + + [DataMember(Name = "joins", Order = 2), Preserve] + private List _joins; + + public override string ToString() + { + var joins = string.Join(", ", Joins); + var leaves = string.Join(", ", Leaves); + return $"StatusPresenceEvent(Leaves=[{leaves}], Joins=[{joins}])"; + } + } +} diff --git a/src/Nakama/StatusUnfollowMessage.cs b/src/Nakama/SocketInternal/StatusUnfollowMessage.cs similarity index 83% rename from src/Nakama/StatusUnfollowMessage.cs rename to src/Nakama/SocketInternal/StatusUnfollowMessage.cs index 58746bf1..38b49157 100644 --- a/src/Nakama/StatusUnfollowMessage.cs +++ b/src/Nakama/SocketInternal/StatusUnfollowMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,15 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// Unfollow one or more users on the server. /// - internal class StatusUnfollowMessage + [DataContract] + public class StatusUnfollowMessage { - [DataMember(Name="user_ids"), Preserve] + [DataMember(Name="user_ids", Order = 1), Preserve] public List UserIds { get; set; } public override string ToString() diff --git a/src/Nakama/StatusUpdateMessage.cs b/src/Nakama/SocketInternal/StatusUpdateMessage.cs similarity index 64% rename from src/Nakama/StatusUpdateMessage.cs rename to src/Nakama/SocketInternal/StatusUpdateMessage.cs index 37b20d91..dea34289 100644 --- a/src/Nakama/StatusUpdateMessage.cs +++ b/src/Nakama/SocketInternal/StatusUpdateMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,29 @@ */ using System.Runtime.Serialization; - -namespace Nakama +namespace Nakama.SocketInternal { /// /// Update the status of the current user. /// - internal class StatusUpdateMessage + [DataContract] + public class StatusUpdateMessage { + public string Status + { + get => _statusValue.Value ?? _status; + set + { + _status = value; + _statusValue.Value = value; + } + } + [DataMember(Name="status"), Preserve] - public string Status { get; set; } + private string _status; + + [DataMember(Order = 1), Preserve] + private StringValue _statusValue = new StringValue(); public override string ToString() { diff --git a/src/Nakama/SocketInternal/StreamPresenceEvent.cs b/src/Nakama/SocketInternal/StreamPresenceEvent.cs new file mode 100644 index 00000000..629854ab --- /dev/null +++ b/src/Nakama/SocketInternal/StreamPresenceEvent.cs @@ -0,0 +1,95 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Runtime.Serialization; +using System.Collections.Generic; + +namespace Nakama.SocketInternal +{ + /// + [DataContract] + public class StreamPresenceEvent : IStreamPresenceEvent + { + public IEnumerable Leaves => _leaves ?? UserPresence.NoPresences; + + [DataMember(Name = "leaves", Order = 1), Preserve] + private List _leaves; + + public IEnumerable Joins => _joins ?? UserPresence.NoPresences; + + [DataMember(Name = "joins", Order = 2), Preserve] + private List _joins; + + public IStream Stream => _stream; + + [DataMember(Name = "stream", Order = 3), Preserve] + private Stream _stream; + + public override string ToString() + { + var leaves = string.Join(", ", Leaves); + var joins = string.Join(", ", Joins); + return $"StreamPresenceEvent(Leaves=[{leaves}], Joins=[{joins}], Stream={Stream})"; + } + } + + /// + [DataContract] + public class StreamState : IStreamState + { + public IUserPresence Sender => _sender; + + [DataMember(Name = "sender", Order = 2), Preserve] + private UserPresence _sender; + + public string State => _state; + + [DataMember(Name = "data", Order = 3), Preserve] + public string _state; + + public IStream Stream => _stream; + + [DataMember(Name = "stream", Order = 1), Preserve] + private Stream _stream; + + [DataMember(Name = "reliable", Order = 4), Preserve] + public string Reliable { get; set; } + + public override string ToString() + { + return $"StreamState(Sender={Sender}, State='{_state}', Stream={Stream})"; + } + } + + /// + [DataContract] + public class Stream : IStream + { + [DataMember(Name = "descriptor", Order = 3), Preserve] public string Descriptor { get; set; } + + [DataMember(Name = "label", Order = 4), Preserve] public string Label { get; set; } + + [DataMember(Name = "mode", Order = 1), Preserve] public int Mode { get; set; } + + [DataMember(Name = "subject", Order = 2), Preserve] public string Subject { get; set; } + + public override string ToString() + { + return $"Stream(Descriptor='{Descriptor}', Label='{Label}', Mode={Mode}, Subject='{Subject}')"; + } + } +} + diff --git a/src/Nakama/SocketInternal/UserPresence.cs b/src/Nakama/SocketInternal/UserPresence.cs new file mode 100644 index 00000000..f8dfeed3 --- /dev/null +++ b/src/Nakama/SocketInternal/UserPresence.cs @@ -0,0 +1,76 @@ + + +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ +/// + [DataContract] + public class UserPresence : IUserPresence + { + public static readonly IReadOnlyList NoPresences = new List(0); + + [DataMember(Name = "persistence", Order = 4), Preserve] + public bool Persistence { get; set; } + + [DataMember(Name = "session_id", Order = 2), Preserve] + public string SessionId { get; set; } + + public string Status => _statusValue.Value ?? _status; + + [DataMember(Name = "username", Order = 3), Preserve] + public string Username { get; set; } + + [DataMember(Name = "user_id", Order = 1), Preserve] + public string UserId { get; set; } + + [DataMember(Name = "status"), Preserve] + private string _status; + + [DataMember(Order = 5), Preserve] + private StringValue _statusValue = new StringValue(); + + public override bool Equals(object obj) + { + if (!(obj is UserPresence item)) + { + return false; + } + return Equals(item); + } + + private bool Equals(IUserPresence other) => string.Equals(SessionId, other.SessionId) && string.Equals(UserId, other.UserId); + + public override int GetHashCode() + { + unchecked + { + // ReSharper disable twice NonReadonlyMemberInGetHashCode + return ((SessionId?.GetHashCode() ?? 0) * 397) ^ (UserId?.GetHashCode() ?? 0); + } + } + + public override string ToString() + { + return + $"UserPresence(Persistence={Persistence}, SessionId='{SessionId}', Status='{Status}', Username='{Username}', UserId='{UserId}')"; + } + } +} \ No newline at end of file diff --git a/src/Nakama/WebSocketAdapter.cs b/src/Nakama/SocketInternal/WebSocketAdapter.cs similarity index 86% rename from src/Nakama/WebSocketAdapter.cs rename to src/Nakama/SocketInternal/WebSocketAdapter.cs index eb716511..7c32ab2b 100644 --- a/src/Nakama/WebSocketAdapter.cs +++ b/src/Nakama/SocketInternal/WebSocketAdapter.cs @@ -20,11 +20,12 @@ using System.Threading; using System.Threading.Tasks; using Nakama.Ninja.WebSockets; +using Nakama.TinyJson; -namespace Nakama +namespace Nakama.SocketInternal { /// - /// An adapter which uses the WebSocket protocol with Nakama server. + /// A JSON-based adapter which uses the WebSocket protocol with Nakama server. /// public class WebSocketAdapter : ISocketAdapter { @@ -44,6 +45,15 @@ public class WebSocketAdapter : ISocketAdapter /// public event Action> Received; + /// + public string Format + { + get + { + return "json"; + } + } + /// /// If the WebSocket is connected. /// @@ -139,8 +149,14 @@ public void Dispose() _webSocket?.Dispose(); } + public WebSocketMessageEnvelope DeserializeEnvelope(ArraySegment buffer) + { + var contents = System.Text.Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); + return contents.FromJson(); + } + /// - public async void Send(ArraySegment buffer, CancellationToken cancellationToken, + public async void Send(WebSocketMessageEnvelope envelope, CancellationToken cancellationToken, bool reliable = true) { if (_webSocket == null) @@ -151,7 +167,9 @@ public async void Send(ArraySegment buffer, CancellationToken cancellation try { - var sendTask = _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, cancellationToken); + var json = envelope.ToJson(); + var buffer = System.Text.Encoding.UTF8.GetBytes(json); + var sendTask = _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); await Task.WhenAny(sendTask, Task.Delay(_sendTimeoutSec, cancellationToken)); } catch (Exception e) @@ -164,7 +182,7 @@ public async void Send(ArraySegment buffer, CancellationToken cancellation public override string ToString() { return - $"WebSocketDriver(IsConnected={IsConnected}, IsConnecting={IsConnecting}, MaxMessageSize={MaxMessageSize}, Uri='{_uri}')"; + $"WebSocketAdapter(IsConnected={IsConnected}, IsConnecting={IsConnecting}, MaxMessageSize={MaxMessageSize}, Uri='{_uri}')"; } private async Task ReceiveLoop(WebSocket webSocket, CancellationToken cancellationToken) diff --git a/src/Nakama/WebSocketErrorMessage.cs b/src/Nakama/SocketInternal/WebSocketErrorMessage.cs similarity index 66% rename from src/Nakama/WebSocketErrorMessage.cs rename to src/Nakama/SocketInternal/WebSocketErrorMessage.cs index 64a91f26..be00e815 100644 --- a/src/Nakama/WebSocketErrorMessage.cs +++ b/src/Nakama/SocketInternal/WebSocketErrorMessage.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,18 +17,22 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// A logical error received on the WebSocket connection. /// - internal class WebSocketErrorMessage + [DataContract] + public class WebSocketErrorMessage { - [DataMember(Name = "code"), Preserve] public int Code { get; set; } + [DataMember(Name = "code", Order = 1), Preserve] + public int Code { get; set; } - [DataMember(Name = "context"), Preserve] public Dictionary Context { get; set; } + [DataMember(Name = "context", Order = 3), Preserve] + public Dictionary Context { get; set; } - [DataMember(Name = "message"), Preserve] public string Message { get; set; } + [DataMember(Name = "message", Order = 2), Preserve] + public string Message { get; set; } public override string ToString() { diff --git a/src/Nakama/WebSocketMessageEnvelope.cs b/src/Nakama/SocketInternal/WebSocketMessageEnvelope.cs similarity index 55% rename from src/Nakama/WebSocketMessageEnvelope.cs rename to src/Nakama/SocketInternal/WebSocketMessageEnvelope.cs index 0f322dd1..8186746c 100644 --- a/src/Nakama/WebSocketMessageEnvelope.cs +++ b/src/Nakama/SocketInternal/WebSocketMessageEnvelope.cs @@ -1,5 +1,5 @@ /** - * Copyright 2018 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,104 +16,105 @@ using System.Runtime.Serialization; -namespace Nakama +namespace Nakama.SocketInternal { /// /// An envelope for messages received or sent on a WebSocket. /// - internal class WebSocketMessageEnvelope + [DataContract] + public class WebSocketMessageEnvelope { - [DataMember(Name="cid"), Preserve] + [DataMember(Name="cid", Order = 1), Preserve] public string Cid { get; set; } - [DataMember(Name="channel"), Preserve] + [DataMember(Name="channel", Order = 2), Preserve] public Channel Channel { get; set; } - [DataMember(Name="channel_join"), Preserve] + [DataMember(Name="channel_join", Order = 3), Preserve] public ChannelJoinMessage ChannelJoin { get; set; } - [DataMember(Name="channel_leave"), Preserve] + [DataMember(Name="channel_leave", Order = 4), Preserve] public ChannelLeaveMessage ChannelLeave { get; set; } - [DataMember(Name="channel_message"), Preserve] + [DataMember(Name="channel_message", Order = 5), Preserve] public ApiChannelMessage ChannelMessage { get; set; } - [DataMember(Name="channel_message_ack"), Preserve] + [DataMember(Name="channel_message_ack", Order = 6), Preserve] public ChannelMessageAck ChannelMessageAck { get; set; } - [DataMember(Name="channel_message_remove"), Preserve] + [DataMember(Name="channel_message_remove", Order = 9), Preserve] public ChannelRemoveMessage ChannelMessageRemove { get; set; } - [DataMember(Name="channel_message_send"), Preserve] + [DataMember(Name="channel_message_send", Order = 7), Preserve] public ChannelSendMessage ChannelMessageSend { get; set; } - [DataMember(Name="channel_message_update"), Preserve] + [DataMember(Name="channel_message_update", Order = 8), Preserve] public ChannelUpdateMessage ChannelMessageUpdate { get; set; } - [DataMember(Name="channel_presence_event"), Preserve] + [DataMember(Name="channel_presence_event", Order = 10), Preserve] public ChannelPresenceEvent ChannelPresenceEvent { get; set; } - [DataMember(Name="error"), Preserve] + [DataMember(Name="error", Order = 11), Preserve] public WebSocketErrorMessage Error { get; set; } - [DataMember(Name="matchmaker_add"), Preserve] + [DataMember(Name="matchmaker_add", Order = 19), Preserve] public MatchmakerAddMessage MatchmakerAdd { get; set; } - [DataMember(Name="matchmaker_matched"), Preserve] + [DataMember(Name="matchmaker_matched", Order = 20), Preserve] public MatchmakerMatched MatchmakerMatched { get; set; } - [DataMember(Name="matchmaker_remove"), Preserve] + [DataMember(Name="matchmaker_remove", Order = 21), Preserve] public MatchmakerRemoveMessage MatchmakerRemove { get; set; } - [DataMember(Name="matchmaker_ticket"), Preserve] + [DataMember(Name="matchmaker_ticket", Order = 22), Preserve] public MatchmakerTicket MatchmakerTicket { get; set; } - [DataMember(Name="match"), Preserve] + [DataMember(Name="match", Order = 12), Preserve] public Match Match { get; set; } - [DataMember(Name="match_create"), Preserve] + [DataMember(Name="match_create", Order = 13), Preserve] public MatchCreateMessage MatchCreate { get; set; } - [DataMember(Name="match_join"), Preserve] + [DataMember(Name="match_join", Order = 16), Preserve] public MatchJoinMessage MatchJoin { get; set; } - [DataMember(Name="match_leave"), Preserve] + [DataMember(Name="match_leave", Order = 17), Preserve] public MatchLeaveMessage MatchLeave { get; set; } - [DataMember(Name="match_presence_event"), Preserve] + [DataMember(Name="match_presence_event", Order = 18), Preserve] public MatchPresenceEvent MatchPresenceEvent { get; set; } - [DataMember(Name="match_data"), Preserve] + [DataMember(Name="match_data", Order = 14), Preserve] public MatchState MatchState { get; set; } - [DataMember(Name="match_data_send"), Preserve] + [DataMember(Name="match_data_send", Order = 15), Preserve] public MatchSendMessage MatchStateSend { get; set; } - [DataMember(Name="notifications"), Preserve] + [DataMember(Name="notifications", Order = 23), Preserve] public ApiNotificationList NotificationList { get; set; } - [DataMember(Name="rpc"), Preserve] + [DataMember(Name="rpc", Order = 24), Preserve] public ApiRpc Rpc { get; set; } - [DataMember(Name="status"), Preserve] + [DataMember(Name="status", Order = 25), Preserve] public Status Status { get; set; } - [DataMember(Name="status_follow"), Preserve] + [DataMember(Name="status_follow", Order = 26), Preserve] public StatusFollowMessage StatusFollow { get; set; } - [DataMember(Name="status_presence_event"), Preserve] + [DataMember(Name="status_presence_event", Order = 27), Preserve] public StatusPresenceEvent StatusPresenceEvent { get; set; } - [DataMember(Name="status_unfollow"), Preserve] + [DataMember(Name="status_unfollow", Order = 28), Preserve] public StatusUnfollowMessage StatusUnfollow { get; set; } - [DataMember(Name="status_update"), Preserve] + [DataMember(Name="status_update", Order = 29), Preserve] public StatusUpdateMessage StatusUpdate { get; set; } - [DataMember(Name="stream_presence_event"), Preserve] + [DataMember(Name="stream_presence_event", Order = 31), Preserve] public StreamPresenceEvent StreamPresenceEvent { get; set; } - [DataMember(Name="stream_data"), Preserve] + [DataMember(Name="stream_data", Order = 30), Preserve] public StreamState StreamState { get; set; } public override string ToString() diff --git a/src/Nakama/SocketInternal/WellKnownTypes/BoolValue.cs b/src/Nakama/SocketInternal/WellKnownTypes/BoolValue.cs new file mode 100644 index 00000000..bd342a98 --- /dev/null +++ b/src/Nakama/SocketInternal/WellKnownTypes/BoolValue.cs @@ -0,0 +1,46 @@ +/** + * Copyright 2020 The Nakama Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + /// A Protobuf-Net serializable class corresponding to the well-known + /// google.protobuf.BoolValue type. + /// + /// Keep in mind that grpc-gateway will automatically deserialize a vool into a BoolValue; + /// there is no need to for the client to utilize this for JSON. + /// + [DataContract] + public struct BoolValue + { + public bool HasValue => _nullable.HasValue; + public bool Value + { + get => _nullable.Value; + set => _nullable = value; + } + + [DataMember(Order = 1), Preserve] + private bool? _nullable { get; set; } + + public override string ToString() + { + return $"BoolValue(Value='{_nullable}')"; + } + } +} diff --git a/src/Nakama/SocketInternal/WellKnownTypes/IntValue.cs b/src/Nakama/SocketInternal/WellKnownTypes/IntValue.cs new file mode 100644 index 00000000..d74f9f6e --- /dev/null +++ b/src/Nakama/SocketInternal/WellKnownTypes/IntValue.cs @@ -0,0 +1,43 @@ +/** + * Copyright 2020 The Nakama Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + /// A Protobuf-Net serializable class corresponding to the well-known + /// google.protobuf.IntValue type. + /// + /// Keep in mind that grpc-gateway will automatically deserialize a vool into a IntValue; + /// there is no need to for the client to utilize this for JSON. + /// + [DataContract] + public struct IntValue + { + public bool HasValue => _nullable.HasValue; + public int Value => _nullable.Value; + + [DataMember(Order = 1), Preserve] + private int? _nullable { get; set; } + + public override string ToString() + { + return $"IntValue(Value='{_nullable}')"; + } + + } +} diff --git a/src/Nakama/SocketInternal/WellKnownTypes/StringValue.cs b/src/Nakama/SocketInternal/WellKnownTypes/StringValue.cs new file mode 100644 index 00000000..c300441b --- /dev/null +++ b/src/Nakama/SocketInternal/WellKnownTypes/StringValue.cs @@ -0,0 +1,50 @@ +/** + * Copyright 2020 The Nakama Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Runtime.Serialization; + +namespace Nakama.SocketInternal +{ + /// + /// A Protobuf-Net serializable class corresponding to the well-known + /// google.protobuf.StringValue type. + /// + /// Keep in mind that grpc-gateway will automatically deserialize a string into a StringValue; + /// there is no need to for the client to utilize this for JSON. + /// + [DataContract] + public struct StringValue + { + public string Value + { + get => _value; + set => _value = value; + } + + [DataMember(Order = 1), Preserve] + private string _value { get; set; } + + public override string ToString() + { + return $"StringValue(Value='{_value}')"; + } + + public static implicit operator StringValue(string value) + { + return new StringValue{_value = value}; + } + } +} diff --git a/src/Nakama/TinyJson/JsonParser.cs b/src/Nakama/TinyJson/JsonParser.cs index cda51d51..53741f31 100644 --- a/src/Nakama/TinyJson/JsonParser.cs +++ b/src/Nakama/TinyJson/JsonParser.cs @@ -375,16 +375,19 @@ private static Dictionary CreateMemberNameDictionary(IEnumerable(); foreach (var member in members) { - if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) - continue; - var name = member.Name; if (member.IsDefined(typeof(DataMemberAttribute), true)) { var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); - if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) - name = dataMemberAttribute.Name; + if (string.IsNullOrEmpty(dataMemberAttribute.Name)) + continue; + + name = dataMemberAttribute.Name; + } + else + { + continue; } nameToMember.Add(name, member); @@ -407,14 +410,14 @@ private static object ParseObject(Type type, string json) if (!_fieldInfoCache.TryGetValue(type, out nameToField)) { nameToField = CreateMemberNameDictionary( - type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic)); _fieldInfoCache.Add(type, nameToField); } if (!_propertyInfoCache.TryGetValue(type, out nameToProperty)) { nameToProperty = CreateMemberNameDictionary( - type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic)); _propertyInfoCache.Add(type, nameToProperty); } diff --git a/src/Nakama/TinyJson/JsonWriter.cs b/src/Nakama/TinyJson/JsonWriter.cs index b28dff55..741db69f 100644 --- a/src/Nakama/TinyJson/JsonWriter.cs +++ b/src/Nakama/TinyJson/JsonWriter.cs @@ -155,11 +155,22 @@ private static void AppendValue(StringBuilder stringBuilder, object item) var isFirst = true; var fieldInfos = - type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); + type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic); foreach (var t in fieldInfos) { - if (t.IsDefined(typeof(IgnoreDataMemberAttribute), true)) + if (t.IsDefined(typeof(DataMemberAttribute), true)) + { + var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(t, typeof(DataMemberAttribute), true); + + if (string.IsNullOrEmpty(dataMemberAttribute.Name)) + { + continue; + } + } + else + { continue; + } var value = t.GetValue(item); if (value == null) continue; @@ -174,11 +185,25 @@ private static void AppendValue(StringBuilder stringBuilder, object item) } var propertyInfo = - type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); + type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic); foreach (var t in propertyInfo) { - if (!t.CanRead || t.IsDefined(typeof(IgnoreDataMemberAttribute), true)) + if (!t.CanRead) + continue; + + if (t.IsDefined(typeof(DataMemberAttribute), true)) + { + var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(t, typeof(DataMemberAttribute), true); + + if (string.IsNullOrEmpty(dataMemberAttribute.Name)) + { + continue; + } + } + else + { continue; + } var value = t.GetValue(item, null); if (value == null) continue; @@ -198,10 +223,8 @@ private static void AppendValue(StringBuilder stringBuilder, object item) private static string GetMemberName(MemberInfo member) { - if (!member.IsDefined(typeof(DataMemberAttribute), true)) return member.Name; - var dataMemberAttribute = - (DataMemberAttribute) Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); - return !string.IsNullOrEmpty(dataMemberAttribute.Name) ? dataMemberAttribute.Name : member.Name; + var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); + return dataMemberAttribute.Name; } } } \ No newline at end of file diff --git a/tests/Nakama.Tests/AssemblyInfo.cs b/tests/Nakama.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..238fb56d --- /dev/null +++ b/tests/Nakama.Tests/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Xunit; + +// see: https://stackoverflow.com/questions/52389298/howto-resolve-net-test-hangs-on-starting-test-execution-please-wait +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/Nakama.Tests/GroupTest.cs b/tests/Nakama.Tests/GroupTest.cs index 3f05adbb..9da3cbac 100644 --- a/tests/Nakama.Tests/GroupTest.cs +++ b/tests/Nakama.Tests/GroupTest.cs @@ -25,12 +25,10 @@ namespace Nakama.Tests.Api public class GroupTest { private IClient _client; - private ISocket _socket; public GroupTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } [Fact] @@ -205,7 +203,7 @@ public async Task ShouldPromoteAndDemoteUsers() Assert.Equal(admins.GroupUsers.Count(), 2); - await _client.DemoteGroupUsersAsync(session1, group.Id, new string[]{session2.UserId, session3.UserId}); + await _client.DemoteGroupUsersAsync(session1, group.Id, new string[]{null}); admins = await _client.ListGroupUsersAsync(session1, group.Id, state: 1, limit: 2); Assert.Equal(admins.GroupUsers.Count(), 0); diff --git a/tests/Nakama.Tests/Nakama.Tests.csproj b/tests/Nakama.Tests/Nakama.Tests.csproj index ac5e80b6..05adc50d 100644 --- a/tests/Nakama.Tests/Nakama.Tests.csproj +++ b/tests/Nakama.Tests/Nakama.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/tests/Nakama.Tests/AwaitedSocketTaskTest.cs b/tests/Nakama.Tests/Socket/AwaitedSocketTaskTest.cs similarity index 57% rename from tests/Nakama.Tests/AwaitedSocketTaskTest.cs rename to tests/Nakama.Tests/Socket/AwaitedSocketTaskTest.cs index d0ca303d..7f48bce2 100644 --- a/tests/Nakama.Tests/AwaitedSocketTaskTest.cs +++ b/tests/Nakama.Tests/Socket/AwaitedSocketTaskTest.cs @@ -1,5 +1,5 @@ /** - * Copyright 2019 The Nakama Authors + * Copyright 2020 The Nakama Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,49 +18,48 @@ using System.Threading.Tasks; using Xunit; -namespace Nakama.Tests +namespace Nakama.Tests.Socket { public class AwaitedSocketTaskTest : IDisposable { private IClient _client; - private readonly ISocket _socket; public AwaitedSocketTaskTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } - public void Dispose() - { - _socket.Dispose(); - _client = null; - } + public void Dispose() { _client = null; } - [Fact] - public async void Socket_AwaitedTasks_AreCanceled() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void Socket_AwaitedTasks_AreCanceled(TestAdapterFactory adapterFactory) { + var socket = Nakama.Socket.From(_client, adapterFactory()); var id = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(id); - await _socket.ConnectAsync(session); + await socket.ConnectAsync(session); - var matchmakerTask1 = _socket.AddMatchmakerAsync("+label.foo:\"val\"", 15, 20); - var matchmakerTask2 = _socket.AddMatchmakerAsync("+label.bar:\"val\"", 15, 20); - await _socket.CloseAsync(); + var matchmakerTask1 = socket.AddMatchmakerAsync("+label.foo:\"val\"", 15, 20); + var matchmakerTask2 = socket.AddMatchmakerAsync("+label.bar:\"val\"", 15, 20); + await socket.CloseAsync(); await Assert.ThrowsAsync(() => Task.WhenAll(matchmakerTask1, matchmakerTask2)); } - [Fact] - public async void Socket_AwaitedTasksAfterDisconnect_AreCanceled() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void Socket_AwaitedTasksAfterDisconnect_AreCanceled(TestAdapterFactory adapterFactory) { var id = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(id); - await _socket.ConnectAsync(session); + var socket = Nakama.Socket.From(_client, adapterFactory()); + + await socket.ConnectAsync(session); + await socket.CloseAsync(); - await _socket.CloseAsync(); - var statusTask1 = _socket.FollowUsersAsync(new[] {session.UserId}); - var statusTask2 = _socket.FollowUsersAsync(new[] {session.UserId}); + var statusTask1 = socket.FollowUsersAsync(new[] {session.UserId}); + var statusTask2 = socket.FollowUsersAsync(new[] {session.UserId}); await Assert.ThrowsAsync(() => Task.WhenAll(statusTask1, statusTask2)); } diff --git a/tests/Nakama.Tests/Socket/WebSocketChannelTest.cs b/tests/Nakama.Tests/Socket/WebSocketChannelTest.cs index 19859d66..e0cbe8e5 100644 --- a/tests/Nakama.Tests/Socket/WebSocketChannelTest.cs +++ b/tests/Nakama.Tests/Socket/WebSocketChannelTest.cs @@ -22,43 +22,54 @@ namespace Nakama.Tests.Socket using Xunit; using TinyJson; - public class WebSocketChannelTest : IAsyncLifetime + public class WebSocketChannelTest { private IClient _client; - private ISocket _socket; public WebSocketChannelTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } - [Fact] - public async Task ShouldCreateRoomChannel() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldCreateRoomChannel(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - await _socket.ConnectAsync(session); - var channel = await _socket.JoinChatAsync("myroom", ChannelType.Room); + var socket = Nakama.Socket.From(_client, adapterFactory()); + socket.ReceivedError += e => System.Console.WriteLine(e.Message); + await socket.ConnectAsync(session); + + var channel = await socket.JoinChatAsync("myroom", ChannelType.Room); Assert.NotNull(channel); Assert.NotNull(channel.Id); Assert.Equal(channel.Self.UserId, session.UserId); Assert.Equal(channel.Self.Username, session.Username); + + await socket.CloseAsync(); } - [Fact] - public async Task ShouldSendMessageRoomChannel() + + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldSendMessageRoomChannel(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); + var socket = Nakama.Socket.From(_client, adapterFactory()); var completer = new TaskCompletionSource(); - _socket.ReceivedChannelMessage += (chatMessage) => completer.SetResult(chatMessage); - await _socket.ConnectAsync(session); - var channel = await _socket.JoinChatAsync("myroom", ChannelType.Room); + socket.ReceivedChannelMessage += (chatMessage) => completer.SetResult(chatMessage); + await socket.ConnectAsync(session); + socket.ReceivedError += e => System.Console.WriteLine(e.Message); + + var channel = await socket.JoinChatAsync("myroom", ChannelType.Room); // Send chat message. var content = new Dictionary {{"hello", "world"}}.ToJson(); - var sendAck = await _socket.WriteChatMessageAsync(channel, content); + + var sendAck = await socket.WriteChatMessageAsync(channel, content); + var message = await completer.Task.ConfigureAwait(false); Assert.NotNull(sendAck); @@ -66,10 +77,13 @@ public async Task ShouldSendMessageRoomChannel() Assert.Equal(sendAck.ChannelId, message.ChannelId); Assert.Equal(sendAck.MessageId, message.MessageId); Assert.Equal(sendAck.Username, message.Username); + + await socket.CloseAsync(); } - [Fact] - public async Task ShouldSendMessageDirectChannel() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldSendMessageDirectChannel(TestAdapterFactory adapterFactory) { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); @@ -77,17 +91,23 @@ public async Task ShouldSendMessageDirectChannel() await _client.AddFriendsAsync(session1, new[] {session2.UserId}); await _client.AddFriendsAsync(session2, new[] {session1.UserId}); + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + socket1.ReceivedError += e => Console.WriteLine(e.Message); + var completer = new TaskCompletionSource(); - _socket.ReceivedChannelMessage += (chatMessage) => completer.SetResult(chatMessage); - await _socket.ConnectAsync(session1); + socket1.ReceivedChannelMessage += (chatMessage) => completer.SetResult(chatMessage); + await socket1.ConnectAsync(session1); - var socket2 = Nakama.Socket.From(_client); + var socket2 = Nakama.Socket.From(_client, adapterFactory()); await socket2.ConnectAsync(session2); - var channel = await _socket.JoinChatAsync(session2.UserId, ChannelType.DirectMessage, false, false); + socket2.ReceivedError += e => Console.WriteLine(e.Message); + + var channel = await socket1.JoinChatAsync(session2.UserId, ChannelType.DirectMessage, false, false); // Send chat message. var content = new Dictionary {{"hello", "world"}}.ToJson(); - var sendAck = await _socket.WriteChatMessageAsync(channel, content); + + var sendAck = await socket1.WriteChatMessageAsync(channel, content); var message = await completer.Task.ConfigureAwait(false); Assert.NotNull(sendAck); @@ -95,16 +115,9 @@ public async Task ShouldSendMessageDirectChannel() Assert.Equal(sendAck.ChannelId, message.ChannelId); Assert.Equal(sendAck.MessageId, message.MessageId); Assert.Equal(sendAck.Username, message.Username); - } - Task IAsyncLifetime.InitializeAsync() - { - return Task.CompletedTask; - } - - Task IAsyncLifetime.DisposeAsync() - { - return _socket.CloseAsync(); + await socket1.CloseAsync(); + await socket2.CloseAsync(); } } } diff --git a/tests/Nakama.Tests/Socket/WebSocketMatchTest.cs b/tests/Nakama.Tests/Socket/WebSocketMatchTest.cs index bff23220..9340fce8 100644 --- a/tests/Nakama.Tests/Socket/WebSocketMatchTest.cs +++ b/tests/Nakama.Tests/Socket/WebSocketMatchTest.cs @@ -25,42 +25,49 @@ namespace Nakama.Tests.Socket using TinyJson; // "Flakey. Needs improvement." - public class WebSocketMatchTest : IAsyncLifetime + public class WebSocketMatchTest { private IClient _client; - private ISocket _socket; // ReSharper disable RedundantArgumentDefaultValue public WebSocketMatchTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } - [Fact] - public async Task ShouldCreateMatch() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldCreateMatch(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - await _socket.ConnectAsync(session); - var match = await _socket.CreateMatchAsync(); + var socket = Nakama.Socket.From(_client, adapterFactory()); + + await socket.ConnectAsync(session); + var match = await socket.CreateMatchAsync(); Assert.NotNull(match); Assert.NotNull(match.Id); Assert.NotEmpty(match.Id); Assert.False(match.Authoritative); Assert.True(match.Size > 0); + + await socket.CloseAsync(); } - [Fact] - public async Task ShouldCreateMatchAndSecondUserJoin() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldCreateMatchAndSecondUserJoin(TestAdapterFactory adapterFactory) { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - await _socket.ConnectAsync(session1); - var socket2 = Nakama.Socket.From(_client); + + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + await socket1.ConnectAsync(session1); + + var socket2 = Nakama.Socket.From(_client, adapterFactory()); await socket2.ConnectAsync(session2); - var match1 = await _socket.CreateMatchAsync(); + var match1 = await socket1.CreateMatchAsync(); var match2 = await socket2.JoinMatchAsync(match1.Id); Assert.NotNull(match1); @@ -71,54 +78,55 @@ public async Task ShouldCreateMatchAndSecondUserJoin() Assert.True(match1.Presences.Count() == 0 && match1.Self.UserId == session1.UserId); Assert.True(match2.Presences.Count() == 1); + await socket1.CloseAsync(); await socket2.CloseAsync(); } - [Fact] - public async Task ShouldCreateMatchAndLeave() + + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldCreateMatchAndLeave(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - await _socket.ConnectAsync(session); - var match = await _socket.CreateMatchAsync(); + + var socket = Nakama.Socket.From(_client, adapterFactory()); + await socket.ConnectAsync(session); + var match = await socket.CreateMatchAsync(); Assert.NotNull(match); Assert.NotNull(match.Id); - await _socket.LeaveMatchAsync(match.Id); + + await socket.LeaveMatchAsync(match.Id); + await socket.CloseAsync(); } - [Fact] - public async Task ShouldCreateMatchAndSendState() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldCreateMatchAndSendState(TestAdapterFactory adapterFactory) { var session1 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var session2 = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - _socket = Nakama.Socket.From(_client); - await _socket.ConnectAsync(session1); + var socket = Nakama.Socket.From(_client, adapterFactory()); + await socket.ConnectAsync(session1); - var socket2 = Nakama.Socket.From(_client); + var socket2 = Nakama.Socket.From(_client, adapterFactory()); var completer = new TaskCompletionSource(); socket2.ReceivedMatchState += (state) => completer.SetResult(state); await socket2.ConnectAsync(session2); - var match = await _socket.CreateMatchAsync(); + var match = await socket.CreateMatchAsync(); await socket2.JoinMatchAsync(match.Id); var newState = new Dictionary {{"hello", "world"}}.ToJson(); - await _socket.SendMatchStateAsync(match.Id, 0, newState); + await socket.SendMatchStateAsync(match.Id, 0, newState); var result = await completer.Task; Assert.NotNull(result); Assert.Equal(newState, Encoding.UTF8.GetString(result.State)); - } - - Task IAsyncLifetime.InitializeAsync() - { - return Task.CompletedTask; - } - Task IAsyncLifetime.DisposeAsync() - { - return _socket.CloseAsync(); + await socket.CloseAsync(); + await socket2.CloseAsync(); } } } diff --git a/tests/Nakama.Tests/Socket/WebSocketMatchmakerTest.cs b/tests/Nakama.Tests/Socket/WebSocketMatchmakerTest.cs index 86f0686d..a22a77c1 100644 --- a/tests/Nakama.Tests/Socket/WebSocketMatchmakerTest.cs +++ b/tests/Nakama.Tests/Socket/WebSocketMatchmakerTest.cs @@ -20,49 +20,47 @@ namespace Nakama.Tests.Socket using System.Threading.Tasks; using Xunit; - public class WebSocketMatchmakerTest : IAsyncLifetime + public class WebSocketMatchmakerTest { private IClient _client; - private ISocket _socket; public WebSocketMatchmakerTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } - [Fact] - public async Task ShouldJoinMatchmaker() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldJoinMatchmaker(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - await _socket.ConnectAsync(session); - var matchmakerTicket = await _socket.AddMatchmakerAsync("*"); + var socket = Nakama.Socket.From(_client, adapterFactory()); + + await socket.ConnectAsync(session); + var matchmakerTicket = await socket.AddMatchmakerAsync("*"); Assert.NotNull(matchmakerTicket); Assert.NotEmpty(matchmakerTicket.Ticket); + + await socket.CloseAsync(); } - // "Flakey. Needs improvement." - [Fact] - public async Task ShouldJoinAndLeaveMatchmaker() + // flakey, needs improvement + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldJoinAndLeaveMatchmaker(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - await _socket.ConnectAsync(session); - var matchmakerTicket = await _socket.AddMatchmakerAsync("*"); + var socket = Nakama.Socket.From(_client, adapterFactory()); + + await socket.ConnectAsync(session); + var matchmakerTicket = await socket.AddMatchmakerAsync("*"); Assert.NotNull(matchmakerTicket); Assert.NotEmpty(matchmakerTicket.Ticket); - await _socket.RemoveMatchmakerAsync(matchmakerTicket); - } + await socket.RemoveMatchmakerAsync(matchmakerTicket); - Task IAsyncLifetime.InitializeAsync() - { - return Task.CompletedTask; - } - - Task IAsyncLifetime.DisposeAsync() - { - return _socket.CloseAsync(); + await socket.CloseAsync(); } } } diff --git a/tests/Nakama.Tests/Socket/WebSocketNotificationTest.cs b/tests/Nakama.Tests/Socket/WebSocketNotificationTest.cs index faba66ae..102bba20 100644 --- a/tests/Nakama.Tests/Socket/WebSocketNotificationTest.cs +++ b/tests/Nakama.Tests/Socket/WebSocketNotificationTest.cs @@ -22,25 +22,27 @@ namespace Nakama.Tests.Socket using Xunit; using TinyJson; - public class WebSocketNotificationTest : IAsyncLifetime + public class WebSocketNotificationTest { private IClient _client; - private ISocket _socket; public WebSocketNotificationTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } - [Fact] - public async Task ShouldReceiveNotification() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldReceiveNotification(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var completer = new TaskCompletionSource(); - _socket.ReceivedNotification += (notification) => completer.SetResult(notification); - await _socket.ConnectAsync(session); + + var socket = Nakama.Socket.From(_client, adapterFactory()); + + socket.ReceivedNotification += (notification) => completer.SetResult(notification); + await socket.ConnectAsync(session); var payload = new Dictionary {{"user_id", session.UserId}}; var _ = _client.RpcAsync(session, "clientrpc.send_notification", payload.ToJson()); @@ -48,16 +50,8 @@ public async Task ShouldReceiveNotification() var result = await completer.Task; Assert.NotNull(result); Assert.Equal(session.UserId, result.SenderId); - } - Task IAsyncLifetime.InitializeAsync() - { - return Task.CompletedTask; - } - - Task IAsyncLifetime.DisposeAsync() - { - return _socket.CloseAsync(); + await socket.CloseAsync(); } } } diff --git a/tests/Nakama.Tests/Socket/WebSocketRpcTest.cs b/tests/Nakama.Tests/Socket/WebSocketRpcTest.cs index fee13eec..6f1c592b 100644 --- a/tests/Nakama.Tests/Socket/WebSocketRpcTest.cs +++ b/tests/Nakama.Tests/Socket/WebSocketRpcTest.cs @@ -22,40 +22,33 @@ namespace Nakama.Tests.Socket using Xunit; using TinyJson; - public class WebSocketRpcTest : IAsyncLifetime + public class WebSocketRpcTest { private IClient _client; - private ISocket _socket; public WebSocketRpcTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } - [Fact] - public async Task ShouldSendRpcRoundtrip() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldSendRpcRoundtrip(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - await _socket.ConnectAsync(session); + var socket = Nakama.Socket.From(_client, adapterFactory()); + + await socket.ConnectAsync(session); const string funcid = "clientrpc.rpc"; var payload = new Dictionary {{"hello", "world"}}.ToJson(); - var response = await _socket.RpcAsync(funcid, payload); + var response = await socket.RpcAsync(funcid, payload); Assert.NotNull(response); Assert.Equal(funcid, response.Id); Assert.Equal(payload, response.Payload); - } - - Task IAsyncLifetime.InitializeAsync() - { - return Task.CompletedTask; - } - Task IAsyncLifetime.DisposeAsync() - { - return _socket.CloseAsync(); + await socket.CloseAsync(); } } } diff --git a/tests/Nakama.Tests/Socket/WebSocketTest.cs b/tests/Nakama.Tests/Socket/WebSocketTest.cs index 2a618669..35773fe4 100644 --- a/tests/Nakama.Tests/Socket/WebSocketTest.cs +++ b/tests/Nakama.Tests/Socket/WebSocketTest.cs @@ -24,69 +24,99 @@ namespace Nakama.Tests.Socket public class WebSocketTest { private IClient _client; - private ISocket _socket; // ReSharper disable RedundantArgumentDefaultValue public WebSocketTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } - [Fact] - public void ShouldCreateSocket() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public void ShouldCreateSocket(TestAdapterFactory adapterFactory) { var client = ClientUtil.FromSettingsFile(); - var socket = Nakama.Socket.From(client); + var socket = Nakama.Socket.From(client, adapterFactory()); Assert.NotNull(socket); } - [Fact] - public async Task ShouldCreateSocketAndConnect() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldCreateSocketAndConnect(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var completer = new TaskCompletionSource(); - _socket.Connected += () => completer.SetResult(true); + var socket = Nakama.Socket.From(_client, adapterFactory()); - await _socket.ConnectAsync(session); + socket.Connected += () => completer.SetResult(true); + + await socket.ConnectAsync(session); Assert.True(await completer.Task); - await _socket.CloseAsync(); + await socket.CloseAsync(); } - [Fact] - public async Task ShouldCreateSocketAndDisconnect() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldCreateSocketAndDisconnect(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); var completer = new TaskCompletionSource(); - _socket.Closed += () => completer.SetResult(true); - await _socket.ConnectAsync(session); - await _socket.CloseAsync(); + var socket = Nakama.Socket.From(_client, adapterFactory()); + socket.Closed += () => completer.SetResult(true); + + await socket.ConnectAsync(session); + await socket.CloseAsync(); Assert.True(await completer.Task); } - [Fact] - public async Task ShouldCreateSocketAndDisconnectSilent() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task ShouldCreateSocketAndDisconnectSilent(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); + var socket = Nakama.Socket.From(_client, adapterFactory()); - await _socket.ConnectAsync(session); - Assert.True(_socket.IsConnected); + await socket.ConnectAsync(session); + Assert.True(socket.IsConnected); - await _socket.CloseAsync(); - Assert.False(_socket.IsConnected); + await socket.CloseAsync(); + Assert.False(socket.IsConnected); } - [Fact] - public async Task MultipleConnectAttemptsThrowException() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task MultipleConnectAttemptsThrowException(TestAdapterFactory adapterFactory) { var session = await _client.AuthenticateCustomAsync($"{Guid.NewGuid()}"); - await _socket.ConnectAsync(session); - Assert.True(_socket.IsConnected); - await Assert.ThrowsAsync(() => _socket.ConnectAsync(session)); + var socket = Nakama.Socket.From(_client, adapterFactory()); + + await socket.ConnectAsync(session); + Assert.True(socket.IsConnected); + await Assert.ThrowsAsync(() => socket.ConnectAsync(session)); + } + + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async Task MultipleSocketsDoNotThrowException(TestAdapterFactory adapterFactory) + { + var id1 = $"{Guid.NewGuid()}"; + var id2 = $"{Guid.NewGuid()}"; + + var session1 = await _client.AuthenticateCustomAsync(id1); + var session2 = await _client.AuthenticateCustomAsync(id2); + + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + var socket2 = Nakama.Socket.From(_client, adapterFactory()); + + await socket1.ConnectAsync(session1); + await socket2.ConnectAsync(session2); + + await socket1.CloseAsync(); + await socket2.CloseAsync(); } } } diff --git a/tests/Nakama.Tests/Socket/WebSocketTestData.cs b/tests/Nakama.Tests/Socket/WebSocketTestData.cs new file mode 100644 index 00000000..ecd5c792 --- /dev/null +++ b/tests/Nakama.Tests/Socket/WebSocketTestData.cs @@ -0,0 +1,39 @@ +/** +* Copyright 2020 The Nakama Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections; +using System.Collections.Generic; +using Nakama.SocketInternal; +using Nakama.Protobuf; + +namespace Nakama.Tests.Socket +{ + public delegate ISocketAdapter TestAdapterFactory(); + + public class WebSocketTestData : IEnumerable + { + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerator GetEnumerator() + { + TestAdapterFactory textAdapterFactory = () => new WebSocketAdapter(); + TestAdapterFactory protobufAdapterFactory = () => new ProtobufAdapter(); + + yield return new object[]{textAdapterFactory}; + yield return new object[]{protobufAdapterFactory}; + } + } +} diff --git a/tests/Nakama.Tests/Socket/WebSocketUserStatusTest.cs b/tests/Nakama.Tests/Socket/WebSocketUserStatusTest.cs index ad9d8058..2f39bf82 100644 --- a/tests/Nakama.Tests/Socket/WebSocketUserStatusTest.cs +++ b/tests/Nakama.Tests/Socket/WebSocketUserStatusTest.cs @@ -21,22 +21,20 @@ namespace Nakama.Tests.Socket { - // NOTE Test name patterns are: MethodName_StateUnderTest_ExpectedBehavior - public class WebSocketUserStatusTest : IAsyncLifetime + public class WebSocketUserStatusTest { private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); private IClient _client; - private readonly ISocket _socket; public WebSocketUserStatusTest() { _client = ClientUtil.FromSettingsFile(); - _socket = Nakama.Socket.From(_client); } - [Fact] - public async void FollowUsers_NoUsers_AnotherUser() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void FollowUsers_NoUsers_AnotherUser(TestAdapterFactory adapterFactory) { var id = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id); @@ -46,22 +44,29 @@ public async void FollowUsers_NoUsers_AnotherUser() var canceller = new CancellationTokenSource(); canceller.Token.Register(() => completer.TrySetCanceled()); canceller.CancelAfter(Timeout); - _socket.ReceivedStatusPresence += statuses => completer.SetResult(statuses); - _socket.ReceivedError += e => completer.TrySetException(e); - await _socket.ConnectAsync(session1); - await _socket.FollowUsersAsync(new[] {session2.UserId}); - var socket = Nakama.Socket.From(_client); - await socket.ConnectAsync(session2); - await socket.UpdateStatusAsync("new status change"); + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + + socket1.ReceivedStatusPresence += statuses => completer.SetResult(statuses); + socket1.ReceivedError += e => completer.TrySetException(e); + await socket1.ConnectAsync(session1); + await socket1.FollowUsersAsync(new[] {session2.UserId}); + + var socket2 = Nakama.Socket.From(_client, adapterFactory()); + await socket2.ConnectAsync(session2); + await socket2.UpdateStatusAsync("new status change"); var result = await completer.Task; Assert.NotNull(result); Assert.Contains(result.Joins, joined => joined.UserId.Equals(session2.UserId)); + + await socket1.CloseAsync(); + await socket2.CloseAsync(); } - [Fact] - public async void FollowUsers_NoUsers_AnotherUserByUsername() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void FollowUsers_NoUsers_AnotherUserByUsername(TestAdapterFactory adapterFactory) { var id = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id); @@ -71,34 +76,54 @@ public async void FollowUsers_NoUsers_AnotherUserByUsername() var canceller = new CancellationTokenSource(); canceller.Token.Register(() => completer.TrySetCanceled()); canceller.CancelAfter(Timeout); - _socket.ReceivedStatusPresence += statuses => completer.SetResult(statuses); - _socket.ReceivedError += e => completer.TrySetException(e); - await _socket.ConnectAsync(session1); - await _socket.FollowUsersAsync(new string[] { }, new[] {session2.Username}); + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + + socket1.ReceivedStatusPresence += statuses => completer.SetResult(statuses); + socket1.ReceivedError += e => + { + System.Console.WriteLine(e.Message); + completer.TrySetException(e); + }; - var socket = Nakama.Socket.From(_client); - await socket.ConnectAsync(session2); - await socket.UpdateStatusAsync("new status change"); + await socket1.ConnectAsync(session1); + + await socket1.FollowUsersAsync(new string[] { }, new[] {session2.Username}); + + var socket2 = Nakama.Socket.From(_client); + + socket2.ReceivedError += e => System.Console.WriteLine(e.Message); + await socket2.ConnectAsync(session2); + + await socket2.UpdateStatusAsync("new status change"); var result = await completer.Task; + Assert.NotNull(result); Assert.Contains(result.Joins, joined => joined.UserId.Equals(session2.UserId)); + + await socket1.CloseAsync(); + await socket2.CloseAsync(); } - [Fact] - public async void FollowUsers_NoUsers_FollowedSelf() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void FollowUsers_NoUsers_FollowedSelf(TestAdapterFactory adapterFactory) { var id = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(id); - await _socket.ConnectAsync(session); + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + await socket1.ConnectAsync(session); - var statuses = await _socket.FollowUsersAsync(new[] {session.UserId}); + var statuses = await socket1.FollowUsersAsync(new[] {session.UserId}); Assert.NotNull(statuses); Assert.Empty(statuses.Presences); + + await socket1.CloseAsync(); } - [Fact] - public async void FollowUsers_NoUsers_UserJoinsAndLeaves() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void FollowUsers_NoUsers_UserJoinsAndLeaves(TestAdapterFactory adapterFactory) { var id = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id); @@ -108,15 +133,17 @@ public async void FollowUsers_NoUsers_UserJoinsAndLeaves() var canceller = new CancellationTokenSource(); canceller.Token.Register(() => completer1.TrySetCanceled()); canceller.CancelAfter(Timeout); - _socket.ReceivedStatusPresence += statuses => completer1.TrySetResult(statuses); - _socket.ReceivedError += e => completer1.TrySetException(e); - await _socket.ConnectAsync(session1); - await _socket.FollowUsersAsync(new[] {session2.UserId}); + + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + socket1.ReceivedStatusPresence += statuses => completer1.TrySetResult(statuses); + socket1.ReceivedError += e => completer1.TrySetException(e); + await socket1.ConnectAsync(session1); + await socket1.FollowUsersAsync(new[] {session2.UserId}); // Second user comes online and sets status. - var socket = Nakama.Socket.From(_client); - await socket.ConnectAsync(session2); - await socket.UpdateStatusAsync("new status change"); + var socket2 = Nakama.Socket.From(_client, adapterFactory()); + await socket2.ConnectAsync(session2); + await socket2.UpdateStatusAsync("new status change"); var result1 = await completer1.Task; Assert.NotNull(result1); @@ -124,30 +151,32 @@ public async void FollowUsers_NoUsers_UserJoinsAndLeaves() Assert.Contains(result1.Joins, joined => joined.UserId.Equals(session2.UserId)); var completer2 = new TaskCompletionSource(); - _socket.ReceivedStatusPresence += statuses => completer2.SetResult(statuses); + socket1.ReceivedStatusPresence += statuses => completer2.SetResult(statuses); // Second user drops offline. - await socket.CloseAsync(); + await socket2.CloseAsync(); var result2 = await completer2.Task; Assert.NotNull(result2); Assert.Empty(result2.Joins); Assert.Contains(result2.Leaves, left => left.UserId.Equals(session2.UserId)); } - [Fact] - public async void FollowUsers_TwoSessions_HasTwoStatuses() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void FollowUsers_TwoSessions_HasTwoStatuses(TestAdapterFactory adapterFactory) { var id1 = Guid.NewGuid().ToString(); var session1 = await _client.AuthenticateCustomAsync(id1); - await _socket.ConnectAsync(session1); + + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + + await socket1.ConnectAsync(session1); var id2 = Guid.NewGuid().ToString(); var session2 = await _client.AuthenticateCustomAsync(id2); - var socket1 = Nakama.Socket.From(_client); - //socket1.ReceivedError - await socket1.ConnectAsync(session2); - var socket2 = Nakama.Socket.From(_client); - //socket2.ReceivedError + + var socket2 = Nakama.Socket.From(_client, adapterFactory()); + await socket2.ConnectAsync(session2); // Both sockets for single user set statuses. @@ -156,29 +185,31 @@ public async void FollowUsers_TwoSessions_HasTwoStatuses() const string status2 = "user 2 socket 2 status."; await socket2.UpdateStatusAsync(status2); - var statuses = await _socket.FollowUsersAsync(new[] {session2.UserId}); + var statuses = await socket1.FollowUsersAsync(new[] {session2.UserId}); Assert.NotNull(statuses); + Assert.NotNull(statuses.Presences); Assert.Contains(statuses.Presences, presence => presence.Status.Equals(status1) || presence.Status.Equals(status2)); await socket2.CloseAsync(); } - [Fact] - public async void FollowUsers_TwoUsers_ThirdUserFollowsBoth() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void FollowUsers_TwoUsers_ThirdUserFollowsBoth(TestAdapterFactory adapterFactory) { var id1 = Guid.NewGuid().ToString(); - var socket1 = Nakama.Socket.From(_client); + var socket1 = Nakama.Socket.From(_client, adapterFactory()); //socket1.ReceivedError var session1 = await _client.AuthenticateCustomAsync(id1); var id2 = Guid.NewGuid().ToString(); - var socket2 = Nakama.Socket.From(_client); + var socket2 = Nakama.Socket.From(_client, adapterFactory()); //socket2.ReceivedError var session2 = await _client.AuthenticateCustomAsync(id2); var id3 = Guid.NewGuid().ToString(); - var socket3 = Nakama.Socket.From(_client); + var socket3 = Nakama.Socket.From(_client, adapterFactory()); //socket3.ReceivedError var session3 = await _client.AuthenticateCustomAsync(id3); @@ -202,8 +233,9 @@ public async void FollowUsers_TwoUsers_ThirdUserFollowsBoth() await socket3.CloseAsync(); } - [Fact] - public async void UpdateStatus_NoStatus_HasStatus() + [Theory] + [ClassData(typeof(WebSocketTestData))] + public async void UpdateStatus_NoStatus_HasStatus(TestAdapterFactory adapterFactory) { var id = Guid.NewGuid().ToString(); var session = await _client.AuthenticateCustomAsync(id); @@ -212,24 +244,18 @@ public async void UpdateStatus_NoStatus_HasStatus() var canceller = new CancellationTokenSource(); canceller.Token.Register(() => completer.TrySetCanceled()); canceller.CancelAfter(Timeout); - _socket.ReceivedStatusPresence += statuses => completer.SetResult(statuses); - _socket.ReceivedError += e => completer.TrySetException(e); - await _socket.ConnectAsync(session); - await _socket.UpdateStatusAsync("super status change!"); + var socket1 = Nakama.Socket.From(_client, adapterFactory()); + socket1.ReceivedStatusPresence += statuses => completer.SetResult(statuses); + await socket1.ConnectAsync(session); + + await socket1.UpdateStatusAsync("super status change!"); var result = await completer.Task; + Assert.NotNull(result); Assert.Contains(result.Joins, joined => joined.UserId.Equals(session.UserId)); - } - Task IAsyncLifetime.InitializeAsync() - { - return Task.CompletedTask; - } - - Task IAsyncLifetime.DisposeAsync() - { - return _socket.CloseAsync(); + await socket1.CloseAsync(); } } }