// ----------------------------------------------------------------------------
//
// Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH
//
//
// Implements callbacks of the Realtime API to logs selected information
// for support cases.
//
// developer@photonengine.com
// ----------------------------------------------------------------------------
#if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER
#define SUPPORTED_UNITY
#endif
namespace Photon.Realtime
{
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using Stopwatch = System.Diagnostics.Stopwatch;
using ExitGames.Client.Photon;
using System.Threading;
#if SUPPORTED_UNITY
using UnityEngine;
#endif
#if SUPPORTED_UNITY || NETFX_CORE
using Hashtable = ExitGames.Client.Photon.Hashtable;
using SupportClass = ExitGames.Client.Photon.SupportClass;
#endif
///
/// Helper class to debug log basic information about Photon client and vital traffic statistics.
///
///
/// Set SupportLogger.Client for this to work.
///
#if SUPPORTED_UNITY
[DisallowMultipleComponent]
#if PUN_2_OR_NEWER || FUSION_UNITY
[AddComponentMenu("")] // hide from Unity Menus and searches
#endif
public class SupportLogger : MonoBehaviour, IConnectionCallbacks , IMatchmakingCallbacks , IInRoomCallbacks, ILobbyCallbacks, IErrorInfoCallback
#else
public class SupportLogger : IConnectionCallbacks, IInRoomCallbacks, IMatchmakingCallbacks , ILobbyCallbacks
#endif
{
///
/// Toggle to enable or disable traffic statistics logging.
///
public bool LogTrafficStats = true;
//private bool loggedStillOfflineMessage;
private LoadBalancingClient client;
private Stopwatch startStopwatch;
/// helps skip the initial OnApplicationPause call, which is not really of interest on start
private bool initialOnApplicationPauseSkipped = false;
private int pingMax;
private int pingMin;
///
/// Photon client to log information and statistics from.
///
public LoadBalancingClient Client
{
get { return this.client; }
set
{
if (this.client != value)
{
if (this.client != null)
{
this.client.RemoveCallbackTarget(this);
}
this.client = value;
if (this.client != null)
{
this.client.AddCallbackTarget(this);
}
}
}
}
#if SUPPORTED_UNITY
protected void OnDestroy()
{
this.Client = null; // will remove this SupportLogger as callback target
}
protected void OnApplicationPause(bool pause)
{
if (!this.initialOnApplicationPauseSkipped)
{
this.initialOnApplicationPauseSkipped = true;
return;
}
Debug.Log(string.Format("{0} SupportLogger OnApplicationPause({1}). Client: {2}.", this.GetFormattedTimestamp(), pause, this.client == null ? "null" : this.client.State.ToString()));
}
protected void OnApplicationQuit()
{
this.CancelInvoke();
}
#else
// non-Unity implementation uses Timer instances
private Timer trackingTimer;
private Timer logTimer;
[Obsolete]
public SupportLogger()
{
}
public SupportLogger(LoadBalancingClient client)
{
this.Client = client;
}
// called by Timer which requires a state object parameter
private void TrackValues(object o)
{
this.TrackValues();
}
// called by Timer which requires a state object parameter
private void LogStats(object o)
{
this.LogStats();
}
#endif
/// Seconds between logging network stats.
public float LogStatsInterval = 5;
/// Seconds between internally tracking network stats.
public float TrackValuesInterval = 0.05f;
public void StartLogStats()
{
#if SUPPORTED_UNITY
this.InvokeRepeating(nameof(this.LogStats), this.LogStatsInterval, this.LogStatsInterval);
#else
int logStatsMs = (int)(this.LogStatsInterval * 1000);
this.logTimer = new Timer(this.LogStats, null, logStatsMs, logStatsMs);
#endif
}
public void StopLogStats()
{
#if SUPPORTED_UNITY
this.CancelInvoke(nameof(this.LogStats));
#else
this.logTimer.Dispose();
this.logTimer = null;
#endif
}
private void StartTrackValues()
{
#if SUPPORTED_UNITY
this.InvokeRepeating(nameof(this.TrackValues), this.TrackValuesInterval, this.TrackValuesInterval);
#else
int trackValuesMs = (int)(this.TrackValuesInterval * 1000);
this.trackingTimer = new Timer(this.TrackValues, null, trackValuesMs, trackValuesMs);
#endif
}
private void StopTrackValues()
{
#if SUPPORTED_UNITY
this.CancelInvoke(nameof(this.TrackValues));
#else
this.trackingTimer.Dispose();
this.trackingTimer = null;
#endif
}
// called via Timer or InvokeRepeating (Unity)
private void TrackValues()
{
if (this.client == null)
{
return;
}
int currentRtt = this.client.LoadBalancingPeer.RoundTripTime;
if (currentRtt > this.pingMax)
{
this.pingMax = currentRtt;
}
if (currentRtt < this.pingMin)
{
this.pingMin = currentRtt;
}
}
///
/// Debug logs vital traffic statistics about the attached Photon Client.
///
public void LogStats()
{
if (this.client == null || this.client.State == ClientState.PeerCreated)
{
return;
}
if (this.LogTrafficStats)
{
Debug.Log(string.Format("{0} SupportLogger {1} Ping min/max: {2}/{3}", this.GetFormattedTimestamp() , this.client.LoadBalancingPeer.VitalStatsToString(false) , this.pingMin , this.pingMax));
}
}
///
/// Debug logs basic information (AppId, AppVersion, PeerID, Server address, Region) about the attached Photon Client.
///
private void LogBasics()
{
if (this.client == null)
{
return;
}
List buildProperties = new List(10);
#if SUPPORTED_UNITY
buildProperties.Add(Application.unityVersion);
buildProperties.Add(Application.platform.ToString());
#endif
#if ENABLE_IL2CPP
buildProperties.Add("ENABLE_IL2CPP");
#endif
#if ENABLE_MONO
buildProperties.Add("ENABLE_MONO");
#endif
#if DEBUG
buildProperties.Add("DEBUG");
#endif
#if MASTER
buildProperties.Add("MASTER");
#endif
#if NET_4_6
buildProperties.Add("NET_4_6");
#endif
#if NET_STANDARD_2_0
buildProperties.Add("NET_STANDARD_2_0");
#endif
#if NET_STANDARD_2_1
buildProperties.Add("NET_STANDARD_2_1");
#endif
#if NETFX_CORE
buildProperties.Add("NETFX_CORE");
#endif
#if NET_LEGACY
buildProperties.Add("NET_LEGACY");
#endif
#if UNITY_64
buildProperties.Add("UNITY_64");
#endif
#if UNITY_FUSION
buildProperties.Add("UNITY_FUSION");
#endif
StringBuilder sb = new StringBuilder();
string appIdShort = string.IsNullOrEmpty(this.client.AppId) || this.client.AppId.Length < 8 ? this.client.AppId : string.Concat(this.client.AppId.Substring(0, 8), "***");
sb.AppendFormat("{0} SupportLogger Info: ", this.GetFormattedTimestamp());
sb.AppendFormat("AppID: \"{0}\" AppVersion: \"{1}\" Client: v{2} ({4}) Build: {3} ", appIdShort, this.client.AppVersion, PhotonPeer.Version, string.Join(", ", buildProperties.ToArray()), this.client.LoadBalancingPeer.TargetFramework);
if (this.client != null && this.client.LoadBalancingPeer != null && this.client.LoadBalancingPeer.SocketImplementation != null)
{
sb.AppendFormat("Socket: {0} ", this.client.LoadBalancingPeer.SocketImplementation.Name);
}
sb.AppendFormat("UserId: \"{0}\" AuthType: {1} AuthMode: {2} {3} ", this.client.UserId, (this.client.AuthValues != null) ? this.client.AuthValues.AuthType.ToString() : "N/A", this.client.AuthMode, this.client.EncryptionMode);
sb.AppendFormat("State: {0} ", this.client.State);
sb.AppendFormat("PeerID: {0} ", this.client.LoadBalancingPeer.PeerID);
sb.AppendFormat("NameServer: {0} Current Server: {1} IP: {2} Region: {3} ", this.client.NameServerHost, this.client.CurrentServerAddress, this.client.LoadBalancingPeer.ServerIpAddress, this.client.CloudRegion);
sb.AppendFormat("{0} UTC", DateTime.UtcNow.ToString(CultureInfo.InvariantCulture));
Debug.Log(sb.ToString());
}
public void OnConnected()
{
this.pingMax = 0;
this.pingMin = this.client.LoadBalancingPeer.RoundTripTime;
this.LogBasics();
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnConnected().");
if (this.LogTrafficStats)
{
this.client.LoadBalancingPeer.TrafficStatsEnabled = false;
this.client.LoadBalancingPeer.TrafficStatsEnabled = true;
this.StartLogStats();
}
this.StartTrackValues();
}
public void OnDisconnected(DisconnectCause cause)
{
this.StopLogStats();
this.StopTrackValues();
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnDisconnected(" + cause + ").");
this.LogBasics();
this.LogStats();
}
public void OnConnectedToMaster()
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnConnectedToMaster().");
}
public void OnFriendListUpdate(List friendList)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnFriendListUpdate(friendList).");
}
public void OnJoinedLobby()
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnJoinedLobby(" + this.client.CurrentLobby + ").");
}
public void OnLeftLobby()
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnLeftLobby().");
}
public void OnCreateRoomFailed(short returnCode, string message)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnCreateRoomFailed(" + returnCode+","+message+").");
}
public void OnJoinedRoom()
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnJoinedRoom(" + this.client.CurrentRoom + "). " + this.client.CurrentLobby + " GameServer:" + this.client.GameServerAddress);
}
public void OnJoinRoomFailed(short returnCode, string message)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnJoinRoomFailed(" + returnCode+","+message+").");
}
public void OnJoinRandomFailed(short returnCode, string message)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnJoinRandomFailed(" + returnCode+","+message+").");
}
public void OnCreatedRoom()
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnCreatedRoom(" + this.client.CurrentRoom + "). " + this.client.CurrentLobby + " GameServer:" + this.client.GameServerAddress);
}
public void OnLeftRoom()
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnLeftRoom().");
}
public void OnRegionListReceived(RegionHandler regionHandler)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnRegionListReceived(regionHandler).");
}
public void OnRoomListUpdate(List roomList)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnRoomListUpdate(roomList). roomList.Count: " + roomList.Count);
}
public void OnPlayerEnteredRoom(Player newPlayer)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnPlayerEnteredRoom(" + newPlayer+").");
}
public void OnPlayerLeftRoom(Player otherPlayer)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnPlayerLeftRoom(" + otherPlayer+").");
}
public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnRoomPropertiesUpdate(propertiesThatChanged).");
}
public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnPlayerPropertiesUpdate(targetPlayer,changedProps).");
}
public void OnMasterClientSwitched(Player newMasterClient)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnMasterClientSwitched(" + newMasterClient+").");
}
public void OnCustomAuthenticationResponse(Dictionary data)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnCustomAuthenticationResponse(" + data.ToStringFull()+").");
}
public void OnCustomAuthenticationFailed (string debugMessage)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnCustomAuthenticationFailed(" + debugMessage+").");
}
public void OnLobbyStatisticsUpdate(List lobbyStatistics)
{
Debug.Log(this.GetFormattedTimestamp() + " SupportLogger OnLobbyStatisticsUpdate(lobbyStatistics).");
}
public void OnErrorInfo(ErrorInfo errorInfo)
{
Debug.LogError(this.GetFormattedTimestamp() + " SupportLogger OnErrorInfo(): " + errorInfo.ToString());
}
private string GetFormattedTimestamp()
{
if (this.startStopwatch == null)
{
this.startStopwatch = new Stopwatch();
this.startStopwatch.Start();
}
TimeSpan span = this.startStopwatch.Elapsed;
if (span.Minutes > 0)
{
return string.Format("[{0}:{1}.{1:000}]", span.Minutes, span.Seconds, span.Milliseconds);
}
return string.Format("[{0}.{1:000}]", span.Seconds, span.Milliseconds);
}
#if !SUPPORTED_UNITY
private static class Debug
{
public static void Log(string msg)
{
System.Diagnostics.Debug.WriteLine(msg);
}
public static void LogWarning(string msg)
{
System.Diagnostics.Debug.WriteLine(msg);
}
public static void LogError(string msg)
{
System.Diagnostics.Debug.WriteLine(msg);
}
}
#endif
}
}