// ---------------------------------------------------------------------------- // // Photon Voice - Copyright (C) 2018 Exit Games GmbH // // // This class can be used to automatically join/leave Voice rooms when // another network clinet (Leader) joins or leaves its rooms. // // developer@photonengine.com // ---------------------------------------------------------------------------- using ExitGames.Client.Photon; using UnityEngine; using Photon.Realtime; using Photon.Voice.Unity; using System; namespace Photon.Voice { /// /// This class can be used to automatically sync client states between Leader and Voice clients. /// abstract public class VoiceFollowClient : VoiceConnection { abstract protected bool LeaderInRoom { get; } abstract protected bool LeaderOfflineMode { get; } abstract protected string GetVoiceRoomName(); abstract protected bool ConnectVoice(); #region Public Fields /// Auto connect voice client and join a voice room when Leader client is joined to a Leader room public bool AutoConnectAndJoin = true; #endregion #region Private Fields /// Used as deliberate disconnect / prevents automatic (re)connect when moving to state disconnected. /// /// After a manualDisconnect, the VoiceFollowClient will go online at the next state change of Leader. /// To prevent that, set AutoConnectAndJoin to false. /// private bool manualDisconnect; private bool errAuthOrJoin; #endregion #region Public Methods /// /// Connect voice client to Photon servers and join a Voice room /// /// If true, connection command send from client public bool ConnectAndJoinRoom() { if (!LeaderInRoom) { this.Logger.Log(LogLevel.Error, "Cannot connect and join if Leader is not joined."); return false; } if (this.ConnectVoice()) { this.manualDisconnect = false; return true; } this.Logger.Log(LogLevel.Error, "Connecting to server failed."); return false; } /// /// Disconnect voice client from all Photon servers /// public void Disconnect() { if (!this.Client.IsConnected) { this.Logger.Log(LogLevel.Error, "Cannot Disconnect if not connected."); return; } this.manualDisconnect = true; this.Client.Disconnect(); } #endregion #region Private Methods protected virtual void Start() { this.manualDisconnect = false; this.FollowLeader(); // in case this is enabled or activated late } protected override void OnDestroy() { base.OnDestroy(); } protected override void OnOperationResponseReceived(OperationResponse operationResponse) { // the base method only logs some error cases. this class re-implements that, so we deliberately skip calling the base method //base.OnOperationResponseReceived(operationResponse); if (operationResponse.ReturnCode != ErrorCode.Ok) { switch (operationResponse.OperationCode) { case OperationCode.Authenticate: case OperationCode.AuthenticateOnce: this.Logger.Log(LogLevel.Error, "Setting AutoConnectAndJoin to false because authentication failed. Error: {0}. Message: {1}.", operationResponse.ReturnCode, operationResponse.DebugMessage); this.errAuthOrJoin = true; break; case OperationCode.JoinGame: this.Logger.Log(LogLevel.Error, "Failed to join room. RoomName: '{2}' Region: {3} Error: {0}. Message: {1}.", operationResponse.ReturnCode, operationResponse.DebugMessage, GetVoiceRoomName(), this.Client.CloudRegion); // TODO: replace the following with a cooldown time. check error code if this is a temporary issue and if so, the client can try again this.errAuthOrJoin = true; // prevents re-connecting without game logic doing something this.manualDisconnect = true; // do a deliberate (manual) disconnect now as the client is already online this.Disconnect(); break; default: this.Logger.Log(LogLevel.Error, "Operation {0} response error code {1} message {2}", operationResponse.OperationCode, operationResponse.ReturnCode, operationResponse.DebugMessage); break; } } } protected void LeaderStateChanged(ClientState toState) { this.Logger.Log(LogLevel.Info, "OnLeaderStateChanged to {0}", toState); if (toState == ClientState.Joined) { //clear the error state so Voice can try to connect once this.errAuthOrJoin = false; } this.FollowLeader(toState); } protected override void OnVoiceStateChanged(ClientState fromState, ClientState toState) { base.OnVoiceStateChanged(fromState, toState); if (toState == ClientState.Disconnected) { // for a manual / deliberate disconnect, skip this specific voice-client disconnected state-change (to avoid re-connect) if (this.manualDisconnect) { this.manualDisconnect = false; return; // skipping the FollowLeader()-call actually keeps this from immediate re-connect. } // TODO test rejoins and maybe add more cases we could recover if (this.Client.DisconnectedCause == DisconnectCause.ClientTimeout) { bool result = this.Client.ReconnectAndRejoin(); if (result) { return; } } if (this.Client.DisconnectedCause == DisconnectCause.DnsExceptionOnConnect) { Debug.LogWarning($"Voice Disconnected and will not immediately reconnect. Cause: {this.Client.DisconnectedCause}"); return; } } this.Logger.Log(LogLevel.Debug, "OnVoiceStateChanged from {0} to {1}", fromState, toState); this.FollowLeader(toState); } private void ConnectOrJoinVoice() { switch (this.ClientState) { case ClientState.PeerCreated: case ClientState.Disconnected: this.Logger.Log(LogLevel.Info, "Leader joined room, now connecting Voice client"); if (!this.ConnectVoice()) { this.Logger.Log(LogLevel.Error, "Connecting to server failed."); } break; case ClientState.ConnectedToMasterServer: this.Logger.Log(LogLevel.Info, "Leader joined room, now joining Voice room"); if (!this.JoinVoiceRoom(GetVoiceRoomName())) { this.Logger.Log(LogLevel.Error, "Joining a voice room failed."); } break; default: this.Logger.Log(LogLevel.Warning, "Leader joined room, Voice client is busy ({0}). Is this expected?", this.ClientState); break; } } protected virtual bool JoinVoiceRoom(string voiceRoomName) { if (string.IsNullOrEmpty(voiceRoomName)) { this.Logger.Log(LogLevel.Error, "Voice room name is null or empty."); return false; } var roomParams = new EnterRoomParams { RoomOptions = new RoomOptions { IsVisible = false, PlayerTtl = 2000 }, RoomName = voiceRoomName }; Debug.Log($"Calling OpJoinOrCreateRoom for room name '{voiceRoomName}' region {this.Client.CloudRegion}."); // TODO: remove when done debugging VoiceFollowClient return this.Client.OpJoinOrCreateRoom(roomParams); } private void FollowLeader(ClientState toState) { switch (toState) { case ClientState.Joined: case ClientState.Disconnected: case ClientState.ConnectedToMasterServer: this.Logger.Log(LogLevel.Debug, $"FollowLeader for state {toState}"); this.FollowLeader(); break; } } private void FollowLeader() { // no matter what usually happens, if there was a deliberate disconnect call, don't react to state changes if (this.manualDisconnect) { return; } // setting errAuthOrJoin to true should keep the client from automatically joining the lead room (unless this client is already on the way). if (this.AutoConnectAndJoin && !this.errAuthOrJoin || this.Client.IsConnected) { // if Leader is NOT in an online room, voice should disconnect if (!LeaderInRoom || LeaderOfflineMode) { // voice client always disconnects for Leader's offline mode if (this.Client.IsConnected && this.Client.State != ClientState.Disconnecting) { // this.Client.Disconnect(); // Remove the player from the room to avoid an error when reconnecting without rejoining with the same UserId this.Client.OpLeaveRoom(false); } return; } // as Leader is in an online room (checked above), voice might have to follow (and check if in the correct room) if (!this.Client.InRoom) { // Leader is in a room but the voice client not. follow with next steps! this.ConnectOrJoinVoice(); return; } // if Leader is in a room and voice, too, make sure the voice room has the expected room name string expectedRoomName = GetVoiceRoomName(); string currentRoomName = this.Client.CurrentRoom.Name; if (string.IsNullOrEmpty(currentRoomName) || !currentRoomName.Equals(expectedRoomName)) { this.Logger.Log(LogLevel.Warning, "Voice room mismatch: Expected:\"{0}\" Current:\"{1}\", leaving the second to join the first.", expectedRoomName, currentRoomName); if (!this.Client.OpLeaveRoom(false)) { this.Logger.Log(LogLevel.Error, "Leaving the current voice room failed."); } } // if both clients are in matching rooms, everything is fine. } } #endregion } }