mirror of
https://github.com/sbrl/Nibriboard.git
synced 2018-01-10 21:33:49 +00:00
278 lines
8.8 KiB
C#
278 lines
8.8 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// A delegate that is used in the event that is fired when a nibri client disconnects.
|
|
/// </summary>
|
|
public delegate void NibriDisconnectedEvent(NibriClient disconnectedClient);
|
|
|
|
/// <summary>
|
|
/// Represents a single client connected to the ripple-space on this Nibriboard server.
|
|
/// </summary>
|
|
public class NibriClient
|
|
{
|
|
private static int nextId = 1;
|
|
private static int getNextId() { return nextId++; }
|
|
|
|
/// <summary>
|
|
/// This client's unique id.
|
|
/// </summary>
|
|
public readonly int Id = getNextId();
|
|
/// <summary>
|
|
/// The nibri client manager
|
|
/// </summary>
|
|
private readonly NibriClientManager manager;
|
|
/// <summary>
|
|
/// The underlying websocket connection to the client.
|
|
/// Please try not to call the send method on here - use the NibriClient Send() method instead.
|
|
/// </summary>
|
|
private readonly WebSocket client;
|
|
|
|
private static readonly Dictionary<string, Type> messageEventTypes = new Dictionary<string, Type>()
|
|
{
|
|
["HandshakeRequest"] = typeof(HandshakeRequestMessage),
|
|
["CursorPosition"] = typeof(CursorPositionMessage)
|
|
};
|
|
|
|
/// <summary>
|
|
/// Whether this nibri client is still connected.
|
|
/// </summary>
|
|
public bool Connected = true;
|
|
public event NibriDisconnectedEvent Disconnected;
|
|
|
|
/// <summary>
|
|
/// The date and time at which the last message was received from this client.
|
|
/// </summary>
|
|
public DateTime LastMessageTime = DateTime.Now;
|
|
/// <summary>
|
|
/// The number of milliseconds since we last received a message from this client.
|
|
/// </summary>
|
|
/// <value>The milliseconds since last message.</value>
|
|
public int MillisecondsSinceLastMessage {
|
|
get {
|
|
return (int)((DateTime.Now - LastMessageTime).TotalMilliseconds);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this client has completed the handshake yet or not.
|
|
/// </summary>
|
|
public bool HandshakeCompleted = false;
|
|
|
|
|
|
/// <summary>
|
|
/// The name this client has assignedto themselves.
|
|
/// </summary>
|
|
/// <value>The name.</value>
|
|
public string Name { get; private set; }
|
|
/// <summary>
|
|
/// The current area that this client is looking at.
|
|
/// </summary>
|
|
/// <value>The current view port.</value>
|
|
public Rectangle CurrentViewPort { get; private set; } = Rectangle.Zero;
|
|
/// <summary>
|
|
/// The absolute position in plane-space of this client's cursor.
|
|
/// </summary>
|
|
/// <value>The absolute cursor position.</value>
|
|
public Vector2 AbsoluteCursorPosition { get; private set; } = Vector2.Zero;
|
|
/// <summary>
|
|
/// This client's colour. Used to tell multiple clients apart visually.
|
|
/// </summary>
|
|
public readonly ColourHSL Colour = ColourHSL.RandomSaturated();
|
|
|
|
#region Core Setup & Message Routing Logic
|
|
|
|
public NibriClient(NibriClientManager inManager, WebSocket inClient)
|
|
{
|
|
Log.WriteLine("[Nibriboard/WebSocket] New NibriClient connected with id #{0}.", Id);
|
|
|
|
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);
|
|
Log.WriteLine("[NibriClient] Client #{0} disconnected.", Id);
|
|
};
|
|
}
|
|
|
|
private async Task handleMessage(string frame)
|
|
{
|
|
// Updatet he last time we received a message from the client
|
|
LastMessageTime = DateTime.Now;
|
|
|
|
// Extract the event name from the message that the client sent.
|
|
string eventName = JsonUtilities.DeserializeProperty<string>(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
|
|
|
|
/// <summary>
|
|
/// Sends a <see cref="Nibriboard.Client.Messages.Message"/> to the client.
|
|
/// If you *really* need to send a raw message to the client, you can do so with the SendRawa() method.
|
|
/// </summary>
|
|
/// <param name="message">The message to send.</param>
|
|
public void Send(Message message)
|
|
{
|
|
SendRaw(JsonConvert.SerializeObject(message));
|
|
}
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="message">The message to send.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a heartbeat message to this client.
|
|
/// </summary>
|
|
public void SendHeartbeat()
|
|
{
|
|
Send(new HeartbeatMessage());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the connection to the client gracefully.
|
|
/// </summary>
|
|
public void CloseConnection(Message lastMessage)
|
|
{
|
|
if (!Connected)
|
|
return;
|
|
|
|
// Tell the client that we're shutting down
|
|
Send(lastMessage);
|
|
|
|
client.Close();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a new ClientState object representing this client's state at the current time.
|
|
/// </summary>
|
|
public ClientState GenerateStateSnapshot()
|
|
{
|
|
ClientState result = new ClientState();
|
|
result.Id = Id;
|
|
result.Name = Name;
|
|
result.Colour = Colour;
|
|
result.CursorPosition = AbsoluteCursorPosition;
|
|
result.Viewport = CurrentViewPort;
|
|
|
|
return result;
|
|
}
|
|
|
|
#region Message Handlers
|
|
/// <summary>
|
|
/// Handles an incoming handshake request. We should only receive one of these!
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles an incoming cursor position message from the client..
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Generates an update message that contains information about the locations and states of all connected clients.
|
|
/// Automatically omits information about the current client.
|
|
/// </summary>
|
|
/// <returns>The client state update message.</returns>
|
|
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;
|
|
}
|
|
}
|
|
}
|