#if FUSION_WEAVER namespace Photon.Voice.Fusion { using global::Fusion; using global::Fusion.Sockets; using PhotonAppSettings = global::Fusion.Photon.Realtime.PhotonAppSettings; using System.Collections.Generic; using Realtime; using ExitGames.Client.Photon; using UnityEngine; using Unity; using System; using LogLevel = Photon.Voice.LogLevel; [AddComponentMenu("Photon Voice/Fusion/Fusion Voice Client")] [RequireComponent(typeof(NetworkRunner))] public class FusionVoiceClient : VoiceFollowClient, INetworkRunnerCallbacks { // abstract VoiceFollowClient implementation protected override bool LeaderInRoom => this.networkRunner.SessionInfo.IsValid; protected override bool LeaderOfflineMode => networkRunner.GameMode == GameMode.Single; #region Private Fields private NetworkRunner networkRunner; private EnterRoomParams voiceRoomParams = new EnterRoomParams { RoomOptions = new RoomOptions { IsVisible = false } }; bool voiceFollowClientStarted = false; #endregion #region Properties /// /// Whether or not to use the Voice AppId and all the other AppSettings from Fusion's RealtimeAppSettings ScriptableObject singleton in the Voice client/app. /// [field: SerializeField] public bool UseFusionAppSettings = true; /// /// Whether or not to use the same AuthenticationValues used in Fusion client/app in Voice client/app as well. /// This means that the same UserID will be used in both clients. /// If custom authentication is used and setup in Fusion AppId from dashboard, the same configuration should be done for the Voice AppId. /// [field: SerializeField] public bool UseFusionAuthValues = true; #endregion #region Private Methods protected override void Start() { // skip "Temporary Runner Prefab" if (this.networkRunner.State == NetworkRunner.States.Shutdown) { return; } // Actual start code if the runner is already connecting VoiceFollowClientStart(); } // Starts the VoiceFollowClient and add the recorder. // Can be either be called from Start, or once the local player has joined the session, if the NetworkRunner was not yet starting during the Start() call void VoiceFollowClientStart() { if (voiceFollowClientStarted) return; voiceFollowClientStarted = true; base.Start(); if (this.UsePrimaryRecorder) { if (this.PrimaryRecorder != null) { AddRecorder(this.PrimaryRecorder); } else { this.Logger.Log(LogLevel.Error, "Primary Recorder is not set."); } } } protected override void Awake() { base.Awake(); this.networkRunner = this.GetComponent(); VoiceRegisterCustomTypes(); } protected override void OnDestroy() { base.OnDestroy(); } protected override Speaker InstantiateSpeakerForRemoteVoice(int playerId, byte voiceId, object userData) { if (userData == null) // Recorder w/o VoiceNetworkObject: probably created due to this.UsePrimaryRecorder = true { this.Logger.Log(LogLevel.Info, "Creating Speaker for remote voice {0}/{1} FusionVoiceClient Primary Recorder (userData == null).", playerId, voiceId); return this.InstantiateSpeakerPrefab(this.gameObject, true); } if (!(userData is NetworkId)) { this.Logger.Log(LogLevel.Warning, "UserData ({0}) is not of type NetworkId. Remote voice {1}/{2} not linked. Do you have a Recorder not used with a VoiceNetworkObject? is this expected?", userData == null ? "null" : userData.ToString(), playerId, voiceId); return null; } NetworkId networkId = (NetworkId)userData; if (!networkId.IsValid) { this.Logger.Log(LogLevel.Warning, "NetworkId is not valid ({0}). Remote voice {1}/{2} not linked.", networkId, playerId, voiceId); return null; } VoiceNetworkObject voiceNetworkObject = this.networkRunner.TryGetNetworkedBehaviourFromNetworkedObjectRef(networkId); if (ReferenceEquals(null, voiceNetworkObject) || !voiceNetworkObject) { this.Logger.Log(LogLevel.Warning, "No voiceNetworkObject found with ID {0}. Remote voice {1}/{2} not linked.", networkId, playerId, voiceId); return null; } this.Logger.Log(LogLevel.Info, "Using VoiceNetworkObject {0} Speaker for remote voice p#{1} v#{2}.", userData, playerId, voiceId); return voiceNetworkObject.SpeakerInUse; } private string fusionOfflineVoiceRoomName; private string FusionOfflineVoiceRoomName { get { if (fusionOfflineVoiceRoomName == null) { fusionOfflineVoiceRoomName = string.Format("fusion_offline_{0}_voice", Guid.NewGuid()); } return fusionOfflineVoiceRoomName; } } // abstract VoiceFollowClient implementation protected override string GetVoiceRoomName() { return networkRunner.GameMode == GameMode.Single || !this.networkRunner.SessionInfo.IsValid ? FusionOfflineVoiceRoomName : string.Format("{0}_voice", this.networkRunner.SessionInfo.Name); } // abstract VoiceFollowClient implementation protected override bool ConnectVoice() { AppSettings settings = new AppSettings(); if (this.UseFusionAppSettings) { #if FUSION2 settings.AppIdVoice = PhotonAppSettings.Global.AppSettings.AppIdVoice; settings.AppVersion = PhotonAppSettings.Global.AppSettings.AppVersion; settings.FixedRegion = PhotonAppSettings.Global.AppSettings.FixedRegion; settings.UseNameServer = PhotonAppSettings.Global.AppSettings.UseNameServer; settings.Server = PhotonAppSettings.Global.AppSettings.Server; settings.Port = PhotonAppSettings.Global.AppSettings.Port; settings.ProxyServer = PhotonAppSettings.Global.AppSettings.ProxyServer; settings.BestRegionSummaryFromStorage = PhotonAppSettings.Global.AppSettings.BestRegionSummaryFromStorage; settings.EnableLobbyStatistics = false; settings.EnableProtocolFallback = PhotonAppSettings.Global.AppSettings.EnableProtocolFallback; settings.Protocol = PhotonAppSettings.Global.AppSettings.Protocol; settings.AuthMode = (AuthModeOption)(int)PhotonAppSettings.Global.AppSettings.AuthMode; settings.NetworkLogging = PhotonAppSettings.Global.AppSettings.NetworkLogging; #else settings.AppIdVoice = PhotonAppSettings.Instance.AppSettings.AppIdVoice; settings.AppVersion = PhotonAppSettings.Instance.AppSettings.AppVersion; settings.FixedRegion = PhotonAppSettings.Instance.AppSettings.FixedRegion; settings.UseNameServer = PhotonAppSettings.Instance.AppSettings.UseNameServer; settings.Server = PhotonAppSettings.Instance.AppSettings.Server; settings.Port = PhotonAppSettings.Instance.AppSettings.Port; settings.ProxyServer = PhotonAppSettings.Instance.AppSettings.ProxyServer; settings.BestRegionSummaryFromStorage = PhotonAppSettings.Instance.AppSettings.BestRegionSummaryFromStorage; settings.EnableLobbyStatistics = false; settings.EnableProtocolFallback = PhotonAppSettings.Instance.AppSettings.EnableProtocolFallback; settings.Protocol = PhotonAppSettings.Instance.AppSettings.Protocol; settings.AuthMode = (AuthModeOption)(int)PhotonAppSettings.Instance.AppSettings.AuthMode; settings.NetworkLogging = PhotonAppSettings.Instance.AppSettings.NetworkLogging; #endif } else { this.Settings.CopyTo(settings); } string fusionRegion = this.networkRunner.SessionInfo.Region; if (string.IsNullOrEmpty(fusionRegion)) { this.Logger.Log(LogLevel.Warning, "Unexpected: fusion region is empty."); if (!string.IsNullOrEmpty(settings.FixedRegion)) { this.Logger.Log(LogLevel.Warning, "Unexpected: fusion region is empty while voice region is set to \"{0}\". Setting it to null now.", settings.FixedRegion); settings.FixedRegion = null; } } else if (!string.Equals(settings.FixedRegion, fusionRegion, StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrEmpty(settings.FixedRegion)) { this.Logger.Log(LogLevel.Info, "Setting voice region to \"{0}\" to match fusion region.", fusionRegion); } else { this.Logger.Log(LogLevel.Info, "Switching voice region to \"{0}\" from \"{1}\" to match fusion region.", fusionRegion, settings.FixedRegion); } settings.FixedRegion = fusionRegion; } if (this.UseFusionAuthValues && this.networkRunner.AuthenticationValues != null) { this.Client.AuthValues = new AuthenticationValues(this.networkRunner.AuthenticationValues.UserId) { AuthGetParameters = this.networkRunner.AuthenticationValues.AuthGetParameters, AuthType = (CustomAuthenticationType)(int)this.networkRunner.AuthenticationValues.AuthType }; if (this.networkRunner.AuthenticationValues.AuthPostData != null) { if (this.networkRunner.AuthenticationValues.AuthPostData is byte[] byteData) { this.Client.AuthValues.SetAuthPostData(byteData); } else if (this.networkRunner.AuthenticationValues.AuthPostData is string stringData) { this.Client.AuthValues.SetAuthPostData(stringData); } else if (this.networkRunner.AuthenticationValues.AuthPostData is Dictionary dictData) { this.Client.AuthValues.SetAuthPostData(dictData); } } } return this.ConnectUsingSettings(settings); } private static void VoiceRegisterCustomTypes() { PhotonPeer.RegisterType(typeof(NetworkId), FusionNetworkIdTypeCode, SerializeFusionNetworkId, DeserializeFusionNetworkId); } private const byte FusionNetworkIdTypeCode = 0; // we need to make sure this does not clash with other custom types? private static object DeserializeFusionNetworkId(StreamBuffer instream, short length) { NetworkId networkId = new NetworkId(); lock (memCompressedUInt64) { ulong ul = ReadCompressedUInt64(instream); networkId.Raw = (uint)ul; } return networkId; } private static ulong ReadCompressedUInt64(StreamBuffer stream) { ulong value = 0; int shift = 0; byte[] data = stream.GetBuffer(); int offset = stream.Position; while (shift != 70) { if (offset >= data.Length) { throw new System.IO.EndOfStreamException("Failed to read full ulong."); } byte b = data[offset]; offset++; value |= (ulong)(b & 0x7F) << shift; shift += 7; if ((b & 0x80) == 0) { break; } } stream.Position = offset; return value; } private static byte[] memCompressedUInt64 = new byte[10]; private static int WriteCompressedUInt64(StreamBuffer stream, ulong value) { int count = 0; lock (memCompressedUInt64) { // put values in an array of bytes with variable length encoding memCompressedUInt64[count] = (byte)(value & 0x7F); value = value >> 7; while (value > 0) { memCompressedUInt64[count] |= 0x80; memCompressedUInt64[++count] = (byte)(value & 0x7F); value = value >> 7; } count++; stream.Write(memCompressedUInt64, 0, count); } return count; } private static short SerializeFusionNetworkId(StreamBuffer outstream, object customobject) { NetworkId networkId = (NetworkId) customobject; return (short)WriteCompressedUInt64(outstream, networkId.Raw); } #endregion #region INetworkRunnerCallbacks void INetworkRunnerCallbacks.OnPlayerJoined(NetworkRunner runner, PlayerRef player) { this.Logger.Log(LogLevel.Info, "OnPlayerJoined {0}", player); if (runner.LocalPlayer == player) { // Will call the VoicefollowClient start code if the runner was not yet connecting during start (not needed in normal cases) VoiceFollowClientStart(); this.Logger.Log(LogLevel.Info, "Local player joined, calling VoiceConnectOrJoinRoom"); LeaderStateChanged(ClientState.Joined); } } void INetworkRunnerCallbacks.OnPlayerLeft(NetworkRunner runner, PlayerRef player) { this.Logger.Log(LogLevel.Info, "OnPlayerLeft {0}", player); if (runner.LocalPlayer == player) { this.Logger.Log(LogLevel.Info, "Local player left, calling VoiceDisconnect"); LeaderStateChanged(ClientState.Disconnected); } } void INetworkRunnerCallbacks.OnInput(NetworkRunner runner, NetworkInput input) { } void INetworkRunnerCallbacks.OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { } void INetworkRunnerCallbacks.OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) { } void INetworkRunnerCallbacks.OnConnectedToServer(NetworkRunner runner) { LeaderStateChanged(ClientState.ConnectedToMasterServer); } #if FUSION2 void INetworkRunnerCallbacks.OnDisconnectedFromServer(NetworkRunner runner, NetDisconnectReason reason) #else void INetworkRunnerCallbacks.OnDisconnectedFromServer(NetworkRunner runner) #endif { LeaderStateChanged(ClientState.Disconnected); } void INetworkRunnerCallbacks.OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { } void INetworkRunnerCallbacks.OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { } void INetworkRunnerCallbacks.OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { } void INetworkRunnerCallbacks.OnSessionListUpdated(NetworkRunner runner, List sessionList) { } void INetworkRunnerCallbacks.OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary data) { } void INetworkRunnerCallbacks.OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { } void INetworkRunnerCallbacks.OnSceneLoadDone(NetworkRunner runner) { } void INetworkRunnerCallbacks.OnSceneLoadStart(NetworkRunner runner) { } #if FUSION2 public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { } public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { } void INetworkRunnerCallbacks.OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey reliableKey, ArraySegment data) { } void INetworkRunnerCallbacks.OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey reliableKey, float progress) { } #else void INetworkRunnerCallbacks.OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ArraySegment data) { } #endif #endregion } } #endif