holopy3/Assets/Normal/Realtime/Room/Room.cs

584 lines
24 KiB
C#
Raw Normal View History

2020-12-10 14:25:12 +00:00
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Normal.Realtime.Native;
using Normal.Realtime.Serialization;
// Represents a room on the server. Manages the connection to the server and the data model.
namespace Normal.Realtime {
public class Room {
public delegate void ConnectionStateChanged(Room room, ConnectionState previousConnectionState, ConnectionState connectionState);
public event ConnectionStateChanged connectionStateChanged;
public delegate void RPCMessageReceived(Room room, byte[] data, bool reliable);
public event RPCMessageReceived rpcMessageReceived;
// Connection state
public enum ConnectionState {
Error = -1,
Disconnected = 0,
ConnectingToMatcher = 1,
PingingRegions = 2,
FinishedMatching = 3,
ConnectingToRealtime = 4,
ConnectedToRealtime = 5,
Ready = 6, // Datastore is ready
}
private ConnectionState _connectionState = ConnectionState.Disconnected;
public ConnectionState connectionState { get { return _connectionState; } }
public bool connecting {
get {
return _connectionState == ConnectionState.ConnectingToMatcher ||
_connectionState == ConnectionState.PingingRegions ||
_connectionState == ConnectionState.FinishedMatching ||
_connectionState == ConnectionState.ConnectingToRealtime ||
_connectionState == ConnectionState.ConnectedToRealtime;
}
}
public bool connected { get { return _connectionState == ConnectionState.Ready; } }
public bool disconnected { get { return _connectionState == ConnectionState.Disconnected || _connectionState == ConnectionState.Error; } }
// Matcher
private Matcher _matcher;
private List<PingResult> _pingResults;
private int _ipsLeftToPing = 0;
private UInt64 _clientIdentifier;
private byte[] _connectToken;
public int clientID { get { return !sessionCapturePlayback ? _client.ClientIndex() : 255; } }
private double _time;
public double time { get { return _time; } }
// Datastore
private Datastore _datastore;
public Datastore datastore { get { return _datastore; } }
public double datastoreFrameDuration = 1.0/20.0;
private double _deltaTime;
private uint _nextUpdateID = 1; // TODO: Convert to zero when 0 isn't used to signal the initial state of the room
// Native API
private Client _client;
// Realtime
private Realtime _realtime;
public Realtime realtime { get { return _realtime; } }
private IModel _roomModel;
// Debug
private bool _debugLogging = false;
public bool debugLogging { get { return _debugLogging; } set { if (value == _debugLogging) return; Plugin.logLevel = value ? Plugin.LogLevel.LogLevelInfo : Plugin.LogLevel.LogLevelError; _debugLogging = value; } }
// Session capture / playback
private SessionCapture _sessionCapture;
private bool sessionCaptureRecord { get { return _sessionCapture != null && _sessionCapture.mode == SessionCapture.Mode.Record; } }
private bool sessionCapturePlayback { get { return _sessionCapture != null && _sessionCapture.mode == SessionCapture.Mode.Playback; } }
public Room() : this(null) {
}
public Room(SessionCapture sessionCapture) {
_sessionCapture = sessionCapture;
_deltaTime = 0.0;
_datastore = new Datastore();
if (!sessionCapturePlayback) {
_client = new Client();
_client.persistenceMessageReceived += ReceivedPersistenceMessage;
_client.rpcMessageReceived += ReceivedRPCMessage;
}
}
~Room() {
if (_client != null) {
_client.persistenceMessageReceived -= ReceivedPersistenceMessage;
_client.rpcMessageReceived -= ReceivedRPCMessage;
_client.Dispose();
}
DisposeMatcher();
}
// State
private void SetConnectionState(ConnectionState connectionState) {
if (connectionState == _connectionState)
return;
ConnectionState previousConnectionState = _connectionState;
_connectionState = connectionState;
if (connectionStateChanged != null) {
try {
connectionStateChanged(this, previousConnectionState, connectionState);
} catch (Exception exception) {
Debug.LogException(exception);
}
}
switch (connectionState) {
case ConnectionState.Error:
// Stop recording
if (sessionCaptureRecord)
_sessionCapture.StopRecording();
// Clear datastore
_datastore.Reset(null);
Debug.LogError("Realtime: Connection failed" + (!_debugLogging ? ". Turn on debug logging for more details." : ""));
break;
case ConnectionState.Disconnected:
// Stop recording
if (sessionCaptureRecord)
_sessionCapture.StopRecording();
// Clear datastore
_datastore.Reset(null);
Debug.Log("Realtime: Disconnected");
break;
case ConnectionState.ConnectingToMatcher:
if (_debugLogging)
Debug.Log("Realtime: Connecting to matcher.");
break;
case ConnectionState.FinishedMatching:
// Reset datastore
_datastore.Reset(_roomModel);
// Set state to connecting to realtime
SetConnectionState(ConnectionState.ConnectingToRealtime);
_client.Connect(_clientIdentifier, _connectToken);
break;
case ConnectionState.Ready:
Debug.Log("Realtime: Connected");
break;
case ConnectionState.PingingRegions:
Debug.Log("Realtime: Pinging regions to determine server with lowest latency for this client.");
string[] IPsToPing = _matcher.GetIPsToPing();
PingServers(IPsToPing);
break;
}
}
// Realtime
public void _SetRealtime(Realtime realtime) {
_realtime = realtime;
}
// Network Statistics
public NetworkInfo GetNetworkStatistics() {
if (_client == null)
return new NetworkInfo();
return _client.GetNetworkStatistics();
}
// Connect / Disconnect
public void Connect(string roomName, string appKey, IModel roomModel = null) {
if (!sessionCapturePlayback) {
ConnectToServer(roomName, appKey, roomModel);
} else {
Debug.Log("Realtime: Connect() called on Room with session capture override. Will begin playing back session capture now.");
StartSessonCapturePlayback(roomModel);
}
}
private void ConnectToServer(string roomName, string appKey, IModel roomModel = null) {
if (connectionState != ConnectionState.Disconnected && connectionState != ConnectionState.Error) {
Debug.LogError("Room asked to connect to server, but room is already connecting or connected (" + connectionState + "). Ignoring.");
return;
}
Debug.Log("Realtime: Connecting to room \"" + roomName + "\"");
SetConnectionState(ConnectionState.ConnectingToMatcher);
_roomModel = roomModel;
_clientIdentifier = Client.GenerateSecureClientIdentifier();
// Begin connecting to the matcher
JoinRoom(roomName, appKey, _clientIdentifier);
}
private void StartSessonCapturePlayback(IModel roomModel = null) {
// Reset datastore
_datastore.Reset(roomModel);
// Fire connecting / connected to Realtime events
SetConnectionState(ConnectionState.ConnectingToRealtime);
SetConnectionState(ConnectionState.ConnectedToRealtime);
// Start session capture playback
byte[] initialDatastore = _sessionCapture.StartPlayback();
// Initialize datastore
_datastore.Deserialize(initialDatastore);
// Reset _deltaTime
_deltaTime = 0.0;
// We're ready!
SetConnectionState(ConnectionState.Ready);
}
public void Disconnect() {
Disconnect(false);
}
private void Disconnect(bool error) {
// Disconnect client
if (_client != null)
_client.Disconnect();
DisposeMatcher();
// Stop session capture / playback
if (_sessionCapture != null) {
_sessionCapture.StopRecording();
_sessionCapture.StopPlayback();
}
// Set connection state
if (error)
SetConnectionState(ConnectionState.Error);
else
SetConnectionState(ConnectionState.Disconnected);
}
// Tick
public void Tick(double deltaTime) {
if (!sessionCapturePlayback)
RoomTick(deltaTime);
else
PlaybackTick(deltaTime);
}
private void RoomTick(double deltaTime) {
// Connect to a room on the Matcher if needed.
MatcherTick();
// Info
// We grab time here because we want to have the same timestamp on deserialize/serialize operations called by ClienTick()/PersistenceTick().
_time = _client.RoomTime();
// Process incoming network packets and send out anything that's been sitting around.
// TODO: Given that this is going to send/receive packets all at once. Maybe we should call Tick twice? Call it once here to receive packets + dispatch messages, and again at the end to send them right away.
// TODO: We can't do the above TODO until _client.Tick() does its own delta time calculation
ClientTick();
// Persistence
PersistenceTick(deltaTime);
}
private void MatcherTick() {
if (_matcher == null)
return;
int matcherState = _matcher.Tick();
switch (matcherState) {
case (int)Matcher.State.ReadyToPingRegions:
SetConnectionState(ConnectionState.PingingRegions);
break;
case (int)Matcher.State.Disconnected:
if (_debugLogging)
Debug.LogError("Realtime: The matcher has disconnected.");
SetConnectionState(ConnectionState.Disconnected);
DisposeMatcher();
break;
case (int)Matcher.State.Done:
_connectToken = _matcher.GetConnectToken();
DisposeMatcher();
SetConnectionState(ConnectionState.FinishedMatching);
break;
case (int)Matcher.State.Error:
Debug.LogError("Realtime: There was an issue connecting to a room on the matcher. (" + _matcher.GetServerError() + ")");
SetConnectionState(ConnectionState.Error);
DisposeMatcher();
break;
default:
break;
}
}
// Matcher
/// <summary>
/// Join a realtime room. If one with the given roomID is not currently running, it will ping
/// servers around the world to find the closest region to start up a new room.
/// <param name="roomID">The UUID of a room to join.</param>
/// <param name="appKey">Normal Developer App Key.</param>
/// <param name="clientIdentifier">Secure random UInt64 used to identify this client</param>
/// </summary>
private void JoinRoom(string roomID, string appKey, UInt64 clientIdentifier) {
if (_matcher != null) {
DisposeMatcher();
}
SetConnectionState(ConnectionState.ConnectingToMatcher);
_matcher = new Matcher();
_matcher.Connect(roomID, appKey, clientIdentifier);
}
/// <summary>
/// Ping an array of IPs and return the IP with the lowest ping time.
/// <param name="servers">An array of IP strings.</param>
/// </summary>
private void PingServers(string[] servers) {
SetConnectionState(ConnectionState.PingingRegions);
_pingResults = new List<PingResult>();
_ipsLeftToPing = servers.Length;
foreach (string ip in servers) {
_realtime.StartCoroutine(StartPing(ip));
}
}
/// <summary>
/// Starts a new ping operation for a given IP.
/// </summary>
IEnumerator StartPing(string address) {
Ping ping = new Ping(address);
float startTime = Time.realtimeSinceStartup;
// Wait for up to 8 seconds for the ping to finish.
while (!ping.isDone && (Time.realtimeSinceStartup - startTime) < 8.0f) {
yield return null;
}
PingFinished(ping);
}
/// <summary>
/// Each ping will call this when it's done or has failed. When we have all the pings
/// we call the completion handler with the ping with the lowest time.
/// <param name="p">The ping that just completed.</param>
/// </summary>
private void PingFinished(Ping p) {
PingResult pr = new PingResult(p.ip, p.time);
_pingResults.Add(pr);
// Log an error if a ping failed, but we need to respond to the matcher with the time regardless, which will be -1.
if (!p.isDone && _debugLogging) {
Debug.LogError(string.Format("Ping failed for address: {0} with ping time: {1}", p.ip, p.time));
}
_ipsLeftToPing--;
if (_ipsLeftToPing <= 0) {
foreach (PingResult sPing in _pingResults) {
if (_debugLogging)
Debug.Log(string.Format("Realtime: IP: {0} Ping Time: {1}", sPing.address, sPing.time));
}
if (_debugLogging && (_connectionState != ConnectionState.PingingRegions || _matcher == null)) {
Debug.LogWarning("Realtime: Yo, Jeff here. If you see this log, lmk, you can safely ignore it, but I want to know what's happening. The client (" + _clientIdentifier + ") sent back ping results in the state (" + connectionState + ") which was not PingingRegions.");
} else {
_matcher.ContinueConnectionWithPingTimes(_pingResults.ToArray());
}
}
}
private void ClientTick() {
if (connectionState != ConnectionState.ConnectingToRealtime &&
connectionState != ConnectionState.ConnectedToRealtime &&
connectionState != ConnectionState.Ready)
return;
int clientConnectionState = _client.Tick();
// Update connection state using native client
switch (clientConnectionState) {
case -1: // Error
SetConnectionState(ConnectionState.Error);
break;
case 0: // Disconnected
SetConnectionState(ConnectionState.Disconnected);
break;
case 1: // Connecting
SetConnectionState(ConnectionState.ConnectingToRealtime);
break;
case 2: // Connected
if (connectionState == ConnectionState.ConnectingToRealtime)
SetConnectionState(ConnectionState.ConnectedToRealtime);
break;
default:
Debug.LogError("NativeClient returned unknown client state: " + clientConnectionState + ". This is a bug.");
break;
}
}
// Persistence
private void PersistenceTick(double deltaTime) {
// Wait until we're in sync with the server
if (_connectionState != ConnectionState.Ready)
return;
_deltaTime += deltaTime;
if (_deltaTime >= datastoreFrameDuration) {
_deltaTime -= datastoreFrameDuration;
try {
WriteBuffer writeBuffer = _datastore.writeBuffer;
// Unreliable
_datastore.SerializeDeltaUpdates(false);
if (writeBuffer.bytesWritten > 0) {
_client.SendPersistenceMessage(writeBuffer.GetBuffer(), writeBuffer.bytesWritten, false);
// Record delta update if local session capture is enabled and set to record
if (sessionCaptureRecord)
_sessionCapture.WriteDeltaUpdate(_time, clientID, writeBuffer.GetBuffer(), writeBuffer.bytesWritten, false, 0, false);
}
// Reliable
_datastore.SerializeDeltaUpdates(true, _nextUpdateID);
if (writeBuffer.bytesWritten > 0) {
_client.SendPersistenceMessage(writeBuffer.GetBuffer(), writeBuffer.bytesWritten, true);
// Record delta update if local session capture is enabled and set to record
if (sessionCaptureRecord)
_sessionCapture.WriteDeltaUpdate(_time, clientID, writeBuffer.GetBuffer(), writeBuffer.bytesWritten, true, _nextUpdateID, false);
// Only increment if data was written
_nextUpdateID++;
}
} catch (Exception exception) {
Debug.LogException(exception);
Debug.LogError("Failed to serialize datastore message. Disconnecting.");
Disconnect(true);
}
}
}
void ReceivedPersistenceMessage(Client client, int sender, byte[] data, bool reliable) {
if (connectionState != ConnectionState.ConnectingToRealtime &&
connectionState != ConnectionState.ConnectedToRealtime &&
connectionState != ConnectionState.Ready) {
Debug.LogError("Room received persistence message while not in a connection state where it would be expecting one (" + connectionState + "). This is a bug! Ignoring.");
return;
}
bool updateIsFromUs = sender == clientID;
// TODO: We need to add try catches around all native -> managed callbacks (ideally in Plugin.cs or Client.cs if we have to). If C# throws an exception, it skips the native code that runs too which can lead to leaks.
bool isInitialRoomMessage = connectionState != ConnectionState.Ready;
try {
// If we're not in a ready state, try to initialize the datastore with this message.
if (connectionState != ConnectionState.Ready) {
// Make sure we didn't accidentally receive an unreliable update that arrived out of order from the original room setup message...
if (reliable) {
// Retrieve the room time now that the client has calculated it
_time = _client.RoomTime();
// Initialize datastore
_datastore.Deserialize(data);
// Reset _deltaTime
_deltaTime = 0.0;
// We're ready!
SetConnectionState(ConnectionState.Ready);
// Start session capture if local session capture is enabled and set to record
if (sessionCaptureRecord)
_sessionCapture.StartRecording(clientID, time, data);
}
} else {
// Apply delta update to the datastore
uint updateID = _datastore.DeserializeDeltaUpdates(data, reliable, updateIsFromUs);
// Record delta update if local session capture is enabled and set to record
if (sessionCaptureRecord)
_sessionCapture.WriteDeltaUpdate(_time, sender, data, data.Length, reliable, updateID, true);
}
} catch (Exception e) {
bool readUpdateID = reliable;
if (isInitialRoomMessage)
readUpdateID = false;
Debug.LogException(e);
BufferAnalyzer bufferAnalyzer = new BufferAnalyzer();
Debug.LogError("Failed to deserialize incoming datastore message. Disconnecting. (" + isInitialRoomMessage + ") (" + reliable + "): " + bufferAnalyzer.AnalyzeBuffer(new ReadBuffer(data), readUpdateID));
Disconnect(true);
}
}
private void PlaybackTick(double deltaTime) {
if (!_sessionCapture.playing)
return;
// Tick
_sessionCapture.PlaybackTick(deltaTime);
// Read updates until we've read up to the current playback time
SessionCapture.DeltaUpdate deltaUpdate = _sessionCapture.ReadDeltaUpdate();
while (deltaUpdate != null) {
// Update room time
_time = deltaUpdate.timestamp;
// Apply delta update
if (deltaUpdate.data.Length > 0)
_datastore.DeserializeDeltaUpdates(deltaUpdate.data, deltaUpdate.reliable, false);
// Read next update
deltaUpdate = _sessionCapture.ReadDeltaUpdate();
}
if (!_sessionCapture.playing)
SetConnectionState(ConnectionState.Disconnected);
}
private void DisposeMatcher() {
if (_matcher != null) {
if (_debugLogging)
Debug.Log("Disposing Matcher.");
_matcher.Dispose();
_matcher = null;
}
}
// RPC
public bool SendRPCMessage(byte[] data, bool reliable) {
return _client.SendRPCMessage(data, data.Length, reliable);
}
public bool SendRPCMessage(byte[] data, int dataLength, bool reliable) {
return _client.SendRPCMessage(data, dataLength, reliable);
}
private void ReceivedRPCMessage(Client client, byte[] data, bool reliable) {
try {
if (rpcMessageReceived != null)
rpcMessageReceived(this, data, reliable);
} catch (Exception exception) {
Debug.LogException(exception);
}
}
// Audio
public AudioInputStream CreateAudioInputStream(bool voice, int sampleRate, int channels) {
if (sessionCapturePlayback)
return null; // TODO: Implement audio stream support
return _client.CreateAudioInputStream(voice, sampleRate, channels);
}
public AudioOutputStream GetAudioOutputStream(int clientID, int streamID) {
if (sessionCapturePlayback)
return null; // TODO: Implement audio stream support
return _client.GetAudioOutputStream(clientID, streamID);
}
}
}