#if UNITY_NETCODE_GAMEOBJECTS using System; using System.Collections.Generic; using Unity.Collections; using Unity.Netcode; using Adrenak.BRW; using UnityEngine; using System.Linq; namespace Adrenak.UniVoice.Networks { /// /// This is the implementation of interface for Netcode for GameObjects. /// It uses NGO CustomMessagingManager to send and receive UniVoice data to the server. /// Client IDs are cast from ulong to int for compatibility with VoiceSettings. /// public class NGOClient : IAudioClient { private const string TAG = "[NGOClient]"; private const string MESSAGE_NAME = "UniVoice_NGO"; public int ID { get; private set; } = -1; public List PeerIDs { get; private set; } public VoiceSettings YourVoiceSettings { get; private set; } public event Action> OnJoined; public event Action OnLeft; public event Action OnPeerJoined; public event Action OnPeerLeft; public event Action OnReceivedPeerAudioFrame; private NetworkManager _networkManager; private NamedMessagePublisher _publisher; private bool _isDisposed; public NGOClient() { PeerIDs = new List(); YourVoiceSettings = new VoiceSettings(); _networkManager = NetworkManager.Singleton; if (_networkManager == null) { Debug.LogError($"{TAG} NetworkManager.Singleton is null. Ensure Netcode for GameObjects is set up."); return; } _networkManager.OnClientConnectedCallback += OnClientConnected; _networkManager.OnClientDisconnectCallback += OnClientDisconnected; } public void Dispose() { if (_isDisposed) return; _isDisposed = true; if (_networkManager != null) { _networkManager.OnClientConnectedCallback -= OnClientConnected; _networkManager.OnClientDisconnectCallback -= OnClientDisconnected; } PeerIDs.Clear(); } private void OnClientConnected(ulong clientId) { if (!_networkManager.IsClient) return; // Client receives this when they connect; ID and peers come from server via PEER_INIT // No action needed - server will send PEER_INIT if (clientId != _networkManager.LocalClientId) return; if (_networkManager.CustomMessagingManager != null) { _publisher = _networkManager.CustomMessagingManager.GetPublisher(); _publisher.Subscribe(MESSAGE_NAME, OnReceivedMessage); } } private void OnClientDisconnected(ulong clientId) { if (!_networkManager.IsClient) return; // Only process our own disconnection if (clientId != _networkManager.LocalClientId) return; if (_networkManager.CustomMessagingManager != null) { _publisher.Unsubscribe(MESSAGE_NAME, OnReceivedMessage); } OnClientDisconnectedLocal(); } private void OnClientDisconnectedLocal() { YourVoiceSettings = new VoiceSettings(); var oldPeerIds = new List(PeerIDs); PeerIDs.Clear(); ID = -1; foreach (var peerId in oldPeerIds) OnPeerLeft?.Invoke(peerId); OnLeft?.Invoke(); } internal void OnReceivedMessage(ulong senderClientId, FastBufferReader reader) { // If we're not not a client, return if (!_networkManager.IsClient) return; if (senderClientId != 0) return; // Else, check if we're also a server which means our device is the host. // On a host, both NGOClient and NGOServer will end up having their OnReceivedMessage // handlers invoked. We need to dedupe this by early exiting when the message is // not from the server. //if (_networkManager.IsServer && senderClientId != _networkManager.LocalClientId) { // Debug.unityLogger.Log(TAG, $"OnReceivedMessage from " + senderClientId + ". But local is " + _networkManager.LocalClientId); // return; //} 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(); switch (tag) { case NGOMessageTags.PEER_INIT: ID = msgReader.ReadInt(); PeerIDs = msgReader.ReadIntArray().ToList(); var log = $"Initialized with ID {ID}. "; if (PeerIDs.Count > 0) log += $"Peer list: {string.Join(", ", PeerIDs)}"; else log += "There are currently no peers."; Debug.unityLogger.Log(LogType.Log, TAG, log); OnJoined?.Invoke(ID, PeerIDs); foreach (var peerId in PeerIDs) OnPeerJoined?.Invoke(peerId); break; case NGOMessageTags.PEER_JOINED: var newPeerID = msgReader.ReadInt(); if (!PeerIDs.Contains(newPeerID)) { PeerIDs.Add(newPeerID); Debug.unityLogger.Log(LogType.Log, TAG, $"Peer {newPeerID} joined. Peer list is now {string.Join(", ", PeerIDs)}"); OnPeerJoined?.Invoke(newPeerID); } break; case NGOMessageTags.PEER_LEFT: var leftPeerID = msgReader.ReadInt(); if (PeerIDs.Contains(leftPeerID)) { PeerIDs.Remove(leftPeerID); var log2 = $"Peer {leftPeerID} left. "; if (PeerIDs.Count == 0) log2 += "There are no peers anymore."; else log2 += $"Peer list is now {string.Join(", ", PeerIDs)}"; Debug.unityLogger.Log(LogType.Log, TAG, log2); OnPeerLeft?.Invoke(leftPeerID); } break; case NGOMessageTags.AUDIO_FRAME: var sender = msgReader.ReadInt(); if (sender == ID || !PeerIDs.Contains(sender)) return; var frame = new AudioFrame { timestamp = msgReader.ReadLong(), frequency = msgReader.ReadInt(), channelCount = msgReader.ReadInt(), samples = msgReader.ReadByteArray() }; OnReceivedPeerAudioFrame?.Invoke(sender, frame); break; } } public void SendAudioFrame(AudioFrame frame) { if (ID == -1 || _networkManager == null || !_networkManager.IsClient) return; var writer = new BytesWriter(); writer.WriteString(NGOMessageTags.AUDIO_FRAME); writer.WriteInt(ID); writer.WriteLong(frame.timestamp); writer.WriteInt(frame.frequency); writer.WriteInt(frame.channelCount); writer.WriteByteArray(frame.samples); SendToServer(writer.Bytes, NetworkDelivery.Unreliable); } public void SubmitVoiceSettings() { if (ID == -1 || _networkManager == null || !_networkManager.IsClient) return; Debug.unityLogger.Log(TAG, "Submitting : " + YourVoiceSettings); var writer = new BytesWriter(); writer.WriteString(NGOMessageTags.VOICE_SETTINGS); writer.WriteInt(YourVoiceSettings.muteAll ? 1 : 0); writer.WriteIntArray(YourVoiceSettings.mutedPeers.ToArray()); writer.WriteInt(YourVoiceSettings.deafenAll ? 1 : 0); writer.WriteIntArray(YourVoiceSettings.deafenedPeers.ToArray()); writer.WriteStringArray(YourVoiceSettings.myTags.ToArray()); writer.WriteStringArray(YourVoiceSettings.mutedTags.ToArray()); writer.WriteStringArray(YourVoiceSettings.deafenedTags.ToArray()); SendToServer(writer.Bytes, NetworkDelivery.Reliable); } public void UpdateVoiceSettings(Action modification) { modification?.Invoke(YourVoiceSettings); SubmitVoiceSettings(); } private void SendToServer(byte[] data, NetworkDelivery delivery) { if (_networkManager == null || !_networkManager.IsClient) return; var writer = new FastBufferWriter(data.Length + 4, Allocator.Temp); try { writer.WriteValueSafe(data.Length); writer.WriteBytesSafe(data); if (_networkManager.CustomMessagingManager != null) _networkManager.CustomMessagingManager.SendNamedMessage(MESSAGE_NAME, NetworkManager.ServerClientId, writer, delivery); } catch (Exception e) { Debug.unityLogger.LogError(TAG, e); } finally { writer.Dispose(); } } } } #endif