#if UNITY_NETCODE_GAMEOBJECTS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Netcode;
using Adrenak.BRW;
using UnityEngine;
using System.Security.Cryptography;
namespace Adrenak.UniVoice.Networks {
///
/// This is an implementation of the interface for Netcode for GameObjects.
/// It uses NGO CustomMessagingManager to send and receive UniVoice audio data to and from clients.
/// Client IDs (ulong) are cast to int for compatibility with VoiceSettings.
///
public class NGOServer : IAudioServer {
private const string TAG = "[NGOServer]";
private const string MESSAGE_NAME = "UniVoice_NGO";
public event Action OnServerStart;
public event Action OnServerStop;
public event Action OnClientVoiceSettingsUpdated;
public List ClientIDs { get; private set; }
public Dictionary ClientVoiceSettings { get; private set; }
private NetworkManager _networkManager;
private NamedMessagePublisher _publisher;
private bool _isServerStarted;
private bool _isDisposed;
private NGOClient _localClient;
public NGOServer(NGOClient localClient) {
_localClient = localClient;
ClientIDs = new List();
ClientVoiceSettings = new Dictionary();
_networkManager = NetworkManager.Singleton;
if (_networkManager == null) {
Debug.LogError($"{TAG} NetworkManager.Singleton is null. Ensure Netcode for GameObjects is set up.");
return;
}
_networkManager.OnClientConnectedCallback += OnServerClientConnected;
_networkManager.OnClientDisconnectCallback += OnServerClientDisconnected;
_networkManager.OnServerStarted += OnServerStarted;
_networkManager.OnServerStopped += OnServerShutdown;
}
private void OnServerShutdown(bool wasHost) {
OnServerShutdown();
}
public void Dispose() {
if (_isDisposed) return;
_isDisposed = true;
if (_networkManager != null) {
_networkManager.OnClientConnectedCallback -= OnServerClientConnected;
_networkManager.OnClientDisconnectCallback -= OnServerClientDisconnected;
_networkManager.OnServerStarted -= OnServerStarted;
_networkManager.OnServerStopped -= OnServerShutdown;
}
OnServerShutdown();
}
private void OnServerStarted() {
if (_isServerStarted) return;
_isServerStarted = true;
if (_networkManager.CustomMessagingManager != null) {
_publisher = _networkManager.CustomMessagingManager.GetPublisher();
_publisher.Subscribe(MESSAGE_NAME, OnReceivedMessage);
}
OnServerStart?.Invoke();
}
private void OnServerShutdown() {
if (_networkManager.CustomMessagingManager != null) {
_publisher.Unsubscribe(MESSAGE_NAME, OnReceivedMessage);
}
_isServerStarted = false;
ClientIDs.Clear();
ClientVoiceSettings.Clear();
OnServerStop?.Invoke();
}
private void OnServerClientConnected(ulong clientId) {
if (!_networkManager.IsServer) return;
var connId = (int)clientId;
if (ClientIDs.Contains(connId)) return;
OnServerStarted();
ClientIDs.Add(connId);
Debug.unityLogger.Log(LogType.Log, TAG, $"Client {connId} connected. IDs now: {string.Join(", ", ClientIDs)}");
foreach (var peerId in ClientIDs) {
if (peerId == connId) {
var otherPeerIDs = ClientIDs.Where(x => x != connId).ToArray();
var writer = new BytesWriter()
.WriteString(NGOMessageTags.PEER_INIT)
.WriteInt(connId)
.WriteIntArray(otherPeerIDs);
var 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);
SendToClientDelayed(connId, writer.Bytes, NetworkDelivery.Reliable, 100);
}
else {
var writer = new BytesWriter()
.WriteString(NGOMessageTags.PEER_JOINED)
.WriteInt(connId);
Debug.unityLogger.Log(LogType.Log, TAG, $"Notified client {peerId} about new client {connId}");
SendToClient(peerId, writer.Bytes, NetworkDelivery.Reliable);
}
}
}
private void OnServerClientDisconnected(ulong clientId) {
if (!_networkManager.IsServer) return;
var connId = (int)clientId;
ClientIDs.Remove(connId);
ClientVoiceSettings.Remove(connId);
Debug.unityLogger.Log(LogType.Log, TAG, $"Client {connId} disconnected. IDs now: {string.Join(", ", ClientIDs)}");
foreach (var peerId in ClientIDs) {
var writer = new BytesWriter()
.WriteString(NGOMessageTags.PEER_LEFT)
.WriteInt(connId);
Debug.unityLogger.Log(LogType.Log, TAG, $"Notified client {peerId} about {connId} leaving");
SendToClient(peerId, writer.Bytes, NetworkDelivery.Reliable);
}
if (ClientIDs.Count == 0)
OnServerShutdown();
}
private void OnReceivedMessage(ulong senderClientId, FastBufferReader reader) {
if (!_networkManager.IsServer) return;
var clientId = (int)senderClientId;
var length = reader.Length;
if (length <= 0) return;
if (!reader.TryBeginRead(4)) return;
reader.ReadValueSafe(out int payloadLength);
if (!reader.TryBeginRead(payloadLength)) {
Debug.LogError($"{TAG} TryBeginRead failed - not enough data in buffer. Needed {payloadLength}");
return;
}
var data = new byte[payloadLength];
reader.ReadBytes(ref data, payloadLength);
var msgReader = new BytesReader(data);
var tag = msgReader.ReadString();
if (tag.Equals(NGOMessageTags.AUDIO_FRAME)) {
var peersToForwardAudioTo = ClientIDs.Where(x => x != clientId);
if (ClientVoiceSettings.TryGetValue(clientId, out var senderSettings)) {
if (senderSettings.deafenAll)
return;
peersToForwardAudioTo = peersToForwardAudioTo
.Where(x => !senderSettings.deafenedPeers.Contains(x));
peersToForwardAudioTo = peersToForwardAudioTo.Where(peer => {
if (ClientVoiceSettings.TryGetValue(peer, out var peerVoiceSettings)) {
var hasDeafenedPeer = senderSettings.deafenedTags.Intersect(peerVoiceSettings.myTags).Any();
return !hasDeafenedPeer;
}
return true;
});
}
var sender = msgReader.ReadInt();
var frame = new AudioFrame {
timestamp = msgReader.ReadLong(),
frequency = msgReader.ReadInt(),
channelCount = msgReader.ReadInt(),
samples = msgReader.ReadByteArray()
};
foreach (var recipient in peersToForwardAudioTo) {
if (ClientVoiceSettings.TryGetValue(recipient, out var recipientSettings)) {
if (recipientSettings.muteAll)
continue;
if (recipientSettings.mutedPeers.Contains(clientId))
continue;
if (senderSettings != null && recipientSettings.mutedTags.Intersect(senderSettings.myTags).Any())
continue;
}
SendToClient(recipient, data, NetworkDelivery.Unreliable);
}
}
else if (tag.Equals(NGOMessageTags.VOICE_SETTINGS)) {
var muteAll = msgReader.ReadInt() == 1;
var mutedPeers = msgReader.ReadIntArray().ToList();
var deafenAll = msgReader.ReadInt() == 1;
var deafenedPeers = msgReader.ReadIntArray().ToList();
var myTags = msgReader.ReadStringArray().ToList();
var mutedTags = msgReader.ReadStringArray().ToList();
var deafenedTags = msgReader.ReadStringArray().ToList();
var voiceSettings = new VoiceSettings {
muteAll = muteAll,
mutedPeers = mutedPeers,
deafenAll = deafenAll,
deafenedPeers = deafenedPeers,
myTags = myTags,
mutedTags = mutedTags,
deafenedTags = deafenedTags
};
ClientVoiceSettings[clientId] = voiceSettings;
OnClientVoiceSettingsUpdated?.Invoke();
}
}
private async void SendToClientDelayed(int clientId, byte[] bytes, NetworkDelivery delivery, int delayMs) {
await Task.Delay(delayMs);
SendToClient(clientId, bytes, delivery);
}
private void SendToClient(int clientId, byte[] bytes, NetworkDelivery delivery) {
if (_networkManager == null || !_networkManager.IsServer) return;
var writer = new FastBufferWriter(bytes.Length + 4, Allocator.Temp);
try {
writer.WriteValueSafe(bytes.Length);
writer.WriteBytesSafe(bytes);
if (clientId == 0)
_localClient.OnReceivedMessage(0, new FastBufferReader(writer, Allocator.Temp));
else if (_networkManager.CustomMessagingManager != null)
_networkManager.CustomMessagingManager.SendNamedMessage(MESSAGE_NAME, (ulong)clientId, writer, delivery);
}
finally {
writer.Dispose();
}
}
}
}
#endif