using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Text; using System.Linq; using System.Reflection; using IotWeb.Common.Http; using Newtonsoft.Json; using SBRL.Utilities; using Nibriboard.Client.Messages; using RippleSpace; namespace Nibriboard.Client { /// /// A delegate that is used in the event that is fired when a nibri client disconnects. /// public delegate void NibriDisconnectedEvent(NibriClient disconnectedClient); /// /// Represents a single client connected to the ripple-space on this Nibriboard server. /// public class NibriClient { private static int nextId = 1; private static int getNextId() { return nextId++; } /// /// This client's unique id. /// public readonly int Id = getNextId(); /// /// The nibri client manager /// private readonly NibriClientManager manager; /// /// The underlying websocket connection to the client. /// Please try not to call the send method on here - use the NibriClient Send() method instead. /// private readonly WebSocket client; private static readonly Dictionary messageEventTypes = new Dictionary() { ["HandshakeRequest"] = typeof(HandshakeRequestMessage), ["CursorPosition"] = typeof(CursorPositionMessage) }; /// /// Whether this nibri client is still connected. /// public bool Connected = true; public event NibriDisconnectedEvent Disconnected; /// /// Whether this client has completed the handshake yet or not. /// public bool HandshakeCompleted = false; /// /// The name this client has assignedto themselves. /// /// The name. public string Name { get; private set; } /// /// The current area that this client is looking at. /// /// The current view port. public Rectangle CurrentViewPort { get; private set; } = Rectangle.Zero; /// /// The absolute position in plane-space of this client's cursor. /// /// The absolute cursor position. public Vector2 AbsoluteCursorPosition { get; private set; } = Vector2.Zero; /// /// This client's colour. Used to tell multiple clients apart visually. /// public readonly ColourHSL Colour = ColourHSL.RandomSaturated(); #region Core Setup & Message Routing Logic public NibriClient(NibriClientManager inManager, WebSocket inClient) { manager = inManager; client = inClient; client.DataReceived += async (WebSocket clientSocket, string frame) => { try { await handleMessage(frame); } catch (Exception error) { await Console.Error.WriteLineAsync(error.ToString()); throw; } //Task.Run(async () => await onMessage(frame)).Wait(); }; // Store whether this NibriClient is still connected or not client.ConnectionClosed += (WebSocket socket) => { Connected = false; Disconnected(this); }; } private async Task handleMessage(string frame) { string eventName = JsonUtilities.DeserializeProperty(frame, "Event"); if(eventName == null) { Log.WriteLine("[NibriClient#{0}] Received message that didn't have an event.", Id); return; } if (!messageEventTypes.ContainsKey(eventName)) { Log.WriteLine("[NibriClient#{0}] Received message with invalid event {1}.", Id, eventName); return; } Type messageType = messageEventTypes[eventName]; Type jsonNet = typeof(JsonConvert); MethodInfo deserialiserInfo = jsonNet.GetMethods().First(method => method.Name == "DeserializeObject" && method.IsGenericMethod); MethodInfo genericInfo = deserialiserInfo.MakeGenericMethod(messageType); var decodedMessage = genericInfo.Invoke(null, new object[] { frame }); string handlerMethodName = "handle" + decodedMessage.GetType().Name; Type clientType = this.GetType(); MethodInfo handlerInfo = clientType.GetMethod(handlerMethodName, BindingFlags.Instance | BindingFlags.NonPublic); await (Task)handlerInfo.Invoke(this, new object[] { decodedMessage }); } #endregion /// /// Sends a to the client. /// If you *really* need to send a raw message to the client, you can do so with the SendRawa() method. /// /// The message to send. public void Send(Message message) { SendRaw(JsonConvert.SerializeObject(message)); } /// /// Sends a raw string to the client. Don't use unnless you know what you're doing! /// Use the regular Send() method if you can possibly help it. /// /// The message to send. public void SendRaw(string message) { if (!Connected) throw new InvalidOperationException($"[NibriClient]{Id}] Can't send a message as the client has disconnected."); client.Send(message); } /// /// Generates a new ClientState object representing this client's state at the current time. /// public ClientState GenerateStateSnapshot() { ClientState result = new ClientState(); result.Id = Id; result.Name = Name; result.Colour = Colour; result.AbsCursorPosition = AbsoluteCursorPosition; result.Viewport = CurrentViewPort; return result; } #region Message Handlers /// /// Handles an incoming handshake request. We should only receive one of these! /// protected Task handleHandshakeRequestMessage(HandshakeRequestMessage message) { CurrentViewPort = message.InitialViewport; AbsoluteCursorPosition = message.InitialAbsCursorPosition; // Tell everyone else about the new client ClientStatesMessage newClientNotification = new ClientStatesMessage(); newClientNotification.ClientStates.Add(GenerateStateSnapshot()); manager.Broadcast(this, newClientNotification); // Send the new client a response to their handshake request HandshakeResponseMessage handshakeResponse = new HandshakeResponseMessage(); handshakeResponse.Id = Id; handshakeResponse.Colour = Colour; Send(handshakeResponse); // Tell the new client about everyone else who's connected // FUTURE: If we need to handle a large number of connections, we should generate this message based on the chunks surrounding the client Send(GenerateClientStateUpdate()); return Task.CompletedTask; } /// /// Handles an incoming cursor position message from the client.. /// protected Task handleCursorPositionMessage(CursorPositionMessage message) { AbsoluteCursorPosition = message.AbsCursorPosition; // Send the update to the other clients // TODO: Buffer these updates and send them about 5 times a second ClientStatesMessage updateMessage = new ClientStatesMessage(); updateMessage.ClientStates.Add(this.GenerateStateSnapshot()); manager.Broadcast(this, updateMessage); return Task.CompletedTask; } #endregion /// /// Generates an update message that contains information about the locations and states of all connected clients. /// Automatically omits information about the current client. /// /// The client state update message. protected ClientStatesMessage GenerateClientStateUpdate() { ClientStatesMessage result = new ClientStatesMessage(); foreach (NibriClient client in manager.Clients) { // Don't include ourselves in the update message! if (client == this) continue; result.ClientStates.Add(client.GenerateStateSnapshot()); } return result; } } }