// Notes: // In Mirror 89.11.0, the OnServerConnectedWithAddress event was added // https://github.com/MirrorNetworking/Mirror/releases/tag/v89.11.0 // OnServerConnected no longer seems to work? #if MIRROR using System; using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; using UnityEngine; using Mirror; using Adrenak.BRW; namespace Adrenak.UniVoice.Networks { /// /// Activate this class by including the UNIVOICE_MIRROR_NETWORK compilaton symbol /// in your project. /// This is an implementation of the interface for Mirror. /// It uses the Mirror transport to send and receive UniVoice audio data to and from clients. /// public class MirrorServer : IAudioServer { const string TAG = "[MirrorServer]"; public event Action OnServerStart; public event Action OnServerStop; public event Action OnClientVoiceSettingsUpdated; public List ClientIDs { get; private set; } public Dictionary ClientVoiceSettings { get; private set; } readonly MirrorModeObserver mirrorEvents; public MirrorServer() { ClientIDs = new List(); ClientVoiceSettings = new Dictionary(); mirrorEvents = MirrorModeObserver.New("for MirrorServer"); mirrorEvents.ModeChanged += OnModeChanged; NetworkServer.RegisterHandler(OnReceivedMessage, false); } public void Dispose() { mirrorEvents.ModeChanged -= OnModeChanged; NetworkServer.UnregisterHandler(); OnServerShutdown(); } void OnServerStarted() { #if MIRROR_89_OR_NEWER NetworkManager.singleton.transport.OnServerConnectedWithAddress += OnServerConnected; #else NetworkManager.singleton.transport.OnServerConnected += OnServerConnected; #endif NetworkManager.singleton.transport.OnServerDisconnected += OnServerDisconnected; OnServerStart?.Invoke(); } void OnServerShutdown() { #if MIRROR_89_OR_NEWER NetworkManager.singleton.transport.OnServerConnectedWithAddress -= OnServerConnected; #else NetworkManager.singleton.transport.OnServerConnected -= OnServerConnected; #endif NetworkManager.singleton.transport.OnServerDisconnected -= OnServerDisconnected; ClientIDs.Clear(); ClientVoiceSettings.Clear(); OnServerStop?.Invoke(); } void OnModeChanged(NetworkManagerMode oldMode, NetworkManagerMode newMode) { // For some reason, handlers don't always work as expected when the connection mode changes NetworkServer.ReplaceHandler(OnReceivedMessage, false); // If in Host mode, the server and internal client have both started and the client connects immediately. // The host client seems to have ID 0 always, so we trigger a new client connection using id 0. if (newMode == NetworkManagerMode.Host) { OnServerStarted(); OnServerConnected(0, "localhost"); } else if (newMode == NetworkManagerMode.ServerOnly) { // If a Host changes to ServerOnly, we disconnect the internal client if (oldMode == NetworkManagerMode.Host) OnServerDisconnected(0); // But if this machine is going from Offline to ServerOnly, only the server is starting else if (oldMode == NetworkManagerMode.Offline) OnServerStarted(); } // If a Host or ServerOnly goes offline else if (newMode == NetworkManagerMode.Offline && (oldMode == NetworkManagerMode.ServerOnly || oldMode == NetworkManagerMode.Host)) { // We check if it was a Host before and disconnect the internal client if (oldMode == NetworkManagerMode.Host) OnServerDisconnected(0); OnServerShutdown(); } } void OnReceivedMessage(NetworkConnectionToClient connection, MirrorMessage message) { var clientId = connection.connectionId; var reader = new BytesReader(message.data); var tag = reader.ReadString(); // Server forwards the received audio from a client to other clients based on voice settings. // Client can mute or deafen each other using IDs as well as tags if (tag.Equals(MirrorMessageTags.AUDIO_FRAME)) { // We start with all the peers except the one that's // sent the audio var peersToForwardAudioTo = ClientIDs .Where(x => x != clientId); // Check the voice settings of the sender and eliminate any peers the sender // may have deafened if (ClientVoiceSettings.TryGetValue(clientId, out var senderSettings)) { // If the client sending the audio has deafened everyone, // we simply return. Sender's audio should not be forwarded to anyone. if (senderSettings.deafenAll) return; // Filter the recipient list by removing all peers that the sender has // deafened using ID peersToForwardAudioTo = peersToForwardAudioTo .Where(x => !senderSettings.deafenedPeers.Contains(x)); // Further filter the recipient list by removing peers that the sender has // deafened using tags peersToForwardAudioTo = peersToForwardAudioTo.Where(peer => { // Get the voice settings of the peer if (ClientVoiceSettings.TryGetValue(peer, out VoiceSettings peerVoiceSettings)) { // Check if sender has not deafened peer using tag var hasDeafenedPeer = senderSettings.deafenedTags.Intersect(peerVoiceSettings.myTags).Count() > 0; return !hasDeafenedPeer; } // If peer doesn't have voice settings, we can keep the peer in the list else { return true; } }); } // We iterate through each recipient peer that the sender wants to send audio to, checking if // they have muted the sender, before forwarding the audio to them. foreach (var recipient in peersToForwardAudioTo) { // Get the settings of a potential recipient if (ClientVoiceSettings.TryGetValue(recipient, out var recipientSettings)) { // If a peer has muted everyone, don't send audio if (recipientSettings.muteAll) continue; // If the peers has muted the sender using ID, skip sending audio if (recipientSettings.mutedPeers.Contains(clientId)) continue; // If the peer has muted the sender using tag, skip sending audio if (senderSettings != null && recipientSettings.mutedTags.Intersect(senderSettings.myTags).Count() > 0) continue; } SendToClient(recipient, message.data, Channels.Unreliable); } } else if (tag.Equals(MirrorMessageTags.VOICE_SETTINGS)) { // We create the VoiceSettings object by reading from the reader // and update the peer voice settings map var muteAll = reader.ReadInt() == 1 ? true : false; var mutedPeers = reader.ReadIntArray().ToList(); var deafenAll = reader.ReadInt() == 1 ? true : false; var deafenedPeers = reader.ReadIntArray().ToList(); var myTags = reader.ReadStringArray().ToList(); var mutedTags = reader.ReadStringArray().ToList(); var deafenedTags = reader.ReadStringArray().ToList(); var voiceSettings = new VoiceSettings { muteAll = muteAll, mutedPeers = mutedPeers, deafenAll = deafenAll, deafenedPeers = deafenedPeers, myTags = myTags, mutedTags = mutedTags, deafenedTags = deafenedTags }; if (ClientVoiceSettings.ContainsKey(clientId)) ClientVoiceSettings[clientId] = voiceSettings; else ClientVoiceSettings.Add(clientId, voiceSettings); OnClientVoiceSettingsUpdated?.Invoke(); } } // When a new Mirror client connects #if MIRROR_89_OR_NEWER void OnServerConnected(int connId, string addr) { #else void OnServerConnected(int connId) { #endif // Not sure if this needs to be done, but being extra cautious here NetworkServer.ReplaceHandler(OnReceivedMessage, false); Debug.unityLogger.Log(LogType.Log, TAG, $"Client {connId} connected"); ClientIDs.Add(connId); foreach (var peer in ClientIDs) { // To the new peer, we send data to initialize it with. // This includes the following: // - its own ID (int) This tells the new peer its ID in the chatroom // - IDs of other peers (int[]) This tells the new peer the IDs of the // peers that are already in the chatroom if (peer == connId) { // Get all the existing peer IDs except that of the newly joined peer var otherPeerIDs = ClientIDs .Where(x => x != connId) .ToArray(); var newClientPacket = new BytesWriter() .WriteString(MirrorMessageTags.PEER_INIT) .WriteInt(connId) .WriteIntArray(otherPeerIDs); // We initialize the new client/peer with some delay. A delay here MAY not be // required but I faced some issues with immediate initialization earlier. SendToClientDelayed(connId, newClientPacket.Bytes, Channels.Reliable, 100); string log = $"Initializing new client with ID {connId}"; if (otherPeerIDs.Length > 0) log += $" and peer list {string.Join(", ", otherPeerIDs)}"; Debug.unityLogger.Log(LogType.Log, TAG, log); } // To the already existing peers, we let them know a new peer has joined // by sending the new peer ID to them. else { var newPeerNotifyPacked = new BytesWriter() .WriteString(MirrorMessageTags.PEER_JOINED) .WriteInt(connId); Debug.unityLogger.Log( LogType.Log, TAG, $"Notified client {peer} about new client {connId}"); SendToClient(peer, newPeerNotifyPacked.Bytes, Channels.Reliable); } } } void OnServerDisconnected(int connId) { // Not sure if this needs to be done, but being extra cautious here NetworkServer.ReplaceHandler(OnReceivedMessage, false); ClientIDs.Remove(connId); Debug.unityLogger.Log(LogType.Log, TAG, $"Client {connId} disconnected"); // Notify all remaining peers that a peer has left foreach (var peerId in ClientIDs) { var packet = new BytesWriter() .WriteString(MirrorMessageTags.PEER_LEFT) .WriteInt(connId); Debug.unityLogger.Log(LogType.Log, TAG, $"Notified client {peerId} about {connId} leaving"); SendToClient(peerId, packet.Bytes, Channels.Reliable); } } async void SendToClientDelayed(int peerID, byte[] bytes, int channel, int delayMS) { await Task.Delay(delayMS); SendToClient(peerID, bytes, channel); } void SendToClient(int clientConnId, byte[] bytes, int channel) { var message = new MirrorMessage { data = bytes }; var conn = GetConnectionToClient(clientConnId); if (conn != null) conn.Send(message, channel); } NetworkConnectionToClient GetConnectionToClient(int connId) { foreach (var conn in NetworkServer.connections) if (conn.Key == connId) return conn.Value; return null; } } } #endif