// --------------------------------------------------------------------------------------------------------------------
// <copyright file="PhotonNetwork.cs" company="Exit Games GmbH">
//   Part of: Photon Unity Networking
// </copyright>
// --------------------------------------------------------------------------------------------------------------------


using System.Diagnostics;
using UnityEngine;
using System;
using System.Collections.Generic;
using ExitGames.Client.Photon;
using UnityEngine.SceneManagement;
using Debug = UnityEngine.Debug;
using Hashtable = ExitGames.Client.Photon.Hashtable;

#if UNITY_EDITOR
using UnityEditor;
using System.IO;
#endif


/// <summary>
/// The main class to use the PhotonNetwork plugin.
/// This class is static.
/// </summary>
/// \ingroup publicApi
public static class PhotonNetwork
{
    /// <summary>Version number of PUN. Also used in GameVersion to separate client version from each other.</summary>
    public const string versionPUN = "1.105";

    /// <summary>Version string for your this build. Can be used to separate incompatible clients. Sent during connect.</summary>
    /// <remarks>This is only sent when you connect so that is also the place you set it usually (e.g. in ConnectUsingSettings).</remarks>
    public static string gameVersion { get; set; }

    /// <summary>
    /// This Monobehaviour allows Photon to run an Update loop.
    /// </summary>
    internal static readonly PhotonHandler photonMono;

    /// <summary>
    /// Photon peer class that implements LoadBalancing in PUN.
    /// Primary use is internal (by PUN itself).
    /// </summary>
    internal static NetworkingPeer networkingPeer;

    /// <summary>
    /// The maximum number of assigned PhotonViews <i>per player</i> (or scene). See the [General Documentation](@ref general) topic "Limitations" on how to raise this limitation.
    /// </summary>
    public static readonly int MAX_VIEW_IDS = 1000; // VIEW & PLAYER LIMIT CAN BE EASILY CHANGED, SEE DOCS


    /// <summary>Name of the PhotonServerSettings file (used to load and by PhotonEditor to save new files).</summary>
    internal const string serverSettingsAssetFile = "PhotonServerSettings";

    /// <summary>Serialized server settings, written by the Setup Wizard for use in ConnectUsingSettings.</summary>
    public static ServerSettings PhotonServerSettings = (ServerSettings)Resources.Load(PhotonNetwork.serverSettingsAssetFile, typeof(ServerSettings));

    /// <summary>Currently used server address (no matter if master or game server).</summary>
    public static string ServerAddress { get { return (networkingPeer != null) ? networkingPeer.ServerAddress : "<not connected>"; } }

    /// <summary>Currently used Cloud Region (if any). As long as the client is not on a Master Server or Game Server, the region is not yet defined.</summary>
    public static CloudRegionCode CloudRegion { get { return (networkingPeer != null && connected && Server!=ServerConnection.NameServer) ? networkingPeer.CloudRegion : CloudRegionCode.none; } }

    /// <summary>The cluster name provided by the Name Server.</summary>
    /// <remarks>
    /// The value is provided by the OpResponse for OpAuthenticate/OpAuthenticateOnce. See ConnectToRegion.
    /// 
    /// Null until set.
    ///
    /// Note that the Name Server may assign another cluster, if the requested one is not configured or available.
    /// </remarks>
    public static string CurrentCluster { get { return (networkingPeer != null ) ? networkingPeer.CurrentCluster : null; } }

    /// <summary>
    /// False until you connected to Photon initially. True in offline mode, while connected to any server and even while switching servers.
    /// </summary>
    public static bool connected
    {
        get
        {
            if (offlineMode)
            {
                return true;
            }

            if (networkingPeer == null)
            {
                return false;
            }

            return !networkingPeer.IsInitialConnect && networkingPeer.State != ClientState.PeerCreated && networkingPeer.State != ClientState.Disconnected && networkingPeer.State != ClientState.Disconnecting && networkingPeer.State != ClientState.ConnectingToNameServer;
        }
    }

    /// <summary>
    /// True when you called ConnectUsingSettings (or similar) until the low level connection to Photon gets established.
    /// </summary>
    public static bool connecting
    {
        get { return networkingPeer.IsInitialConnect && !offlineMode; }
    }

    /// <summary>
    /// A refined version of connected which is true only if your connection to the server is ready to accept operations like join, leave, etc.
    /// </summary>
    public static bool connectedAndReady
    {
        get
        {
            // connected property will check offlineMode and networkingPeer being null
            if (!connected)
            {
                return false;
            }

            if (offlineMode)
            {
                return true;
            }

            switch (connectionStateDetailed)
            {
                case ClientState.PeerCreated:
                case ClientState.Disconnected:
                case ClientState.Disconnecting:
                case ClientState.Authenticating:
                case ClientState.ConnectingToGameserver:
                case ClientState.ConnectingToMasterserver:
                case ClientState.ConnectingToNameServer:
                case ClientState.Joining:
                    return false;   // we are not ready to execute any operations
            }

            return true;
        }
    }

    /// <summary>
    /// Simplified connection state
    /// </summary>
    public static ConnectionState connectionState
    {
        get
        {
            if (offlineMode)
            {
                return ConnectionState.Connected;
            }

            if (networkingPeer == null)
            {
                return ConnectionState.Disconnected;
            }

            switch (networkingPeer.PeerState)
            {
                case PeerStateValue.Disconnected:
                    return ConnectionState.Disconnected;
                case PeerStateValue.Connecting:
                    return ConnectionState.Connecting;
                case PeerStateValue.Connected:
                    return ConnectionState.Connected;
                case PeerStateValue.Disconnecting:
                    return ConnectionState.Disconnecting;
                case PeerStateValue.InitializingApplication:
                    return ConnectionState.InitializingApplication;
            }

            return ConnectionState.Disconnected;
        }
    }

    /// <summary>
    /// Detailed connection state (ignorant of PUN, so it can be "disconnected" while switching servers).
    /// </summary>
    /// <remarks>
    /// In OfflineMode, this is ClientState.Joined (after create/join) or it is ConnectedToMaster in all other cases.
    /// </remarks>
    public static ClientState connectionStateDetailed
    {
        get
        {
            if (offlineMode)
            {
                return (offlineModeRoom != null) ? ClientState.Joined : ClientState.ConnectedToMaster;
            }

            if (networkingPeer == null)
            {
                return ClientState.Disconnected;
            }

            return networkingPeer.State;
        }
    }

    /// <summary>The server (type) this client is currently connected or connecting to.</summary>
    /// <remarks>Photon uses 3 different roles of servers: Name Server, Master Server and Game Server.</remarks>
    public static ServerConnection Server { get { return (PhotonNetwork.networkingPeer != null) ? PhotonNetwork.networkingPeer.Server : ServerConnection.NameServer; } }

    /// <summary>
    /// A user's authentication values used during connect.
    /// </summary>
    /// <remarks>
    /// Set these before calling Connect if you want custom authentication.
    /// These values set the userId, if and how that userId gets verified (server-side), etc..
    ///
    /// If authentication fails for any values, PUN will call your implementation of OnCustomAuthenticationFailed(string debugMsg).
    /// See: PhotonNetworkingMessage.OnCustomAuthenticationFailed
    /// </remarks>
    public static AuthenticationValues AuthValues
    {
        get { return (networkingPeer != null) ? networkingPeer.AuthValues : null; }
        set { if (networkingPeer != null) networkingPeer.AuthValues = value; }
    }

    /// <summary>
    /// Get the room we're currently in. Null if we aren't in any room.
    /// </summary>
    public static Room room
    {
        get
        {
            if (isOfflineMode)
            {
                return offlineModeRoom;
            }

            return networkingPeer.CurrentRoom;
        }
    }

    /// <summary>If true, Instantiate methods will check if you are in a room and fail if you are not.</summary>
    /// <remarks>
    /// Instantiating anything outside of a specific room is very likely to break things.
    /// Turn this off only if you know what you do.</remarks>
    public static bool InstantiateInRoomOnly = true;

    /// <summary>
    /// Network log level. Controls how verbose PUN is.
    /// </summary>
    public static PhotonLogLevel logLevel = PhotonLogLevel.ErrorsOnly;

    /// <summary>
    /// The local PhotonPlayer. Always available and represents this player.
    /// CustomProperties can be set before entering a room and will be synced as well.
    /// </summary>
    public static PhotonPlayer player
    {
        get
        {
            if (networkingPeer == null)
            {
                return null; // Surpress ExitApplication errors
            }

            return networkingPeer.LocalPlayer;
        }
    }

    /// <summary>
    /// The Master Client of the current room or null (outside of rooms).
    /// </summary>
    /// <remarks>
    /// Can be used as "authoritative" client/player to make descisions, run AI or other.
    ///
    /// If the current Master Client leaves the room (leave/disconnect), the server will quickly assign someone else.
    /// If the current Master Client times out (closed app, lost connection, etc), messages sent to this client are
    /// effectively lost for the others! A timeout can take 10 seconds in which no Master Client is active.
    ///
    /// Implement the method IPunCallbacks.OnMasterClientSwitched to be called when the Master Client switched.
    ///
    /// Use PhotonNetwork.SetMasterClient, to switch manually to some other player / client.
    ///
    /// With offlineMode == true, this always returns the PhotonNetwork.player.
    /// </remarks>
    public static PhotonPlayer masterClient
    {
        get
        {
            if (offlineMode)
            {
                return PhotonNetwork.player;
            }

            if (networkingPeer == null)
            {
                return null;
            }

            return networkingPeer.GetPlayerWithId(networkingPeer.mMasterClientId);
        }
    }

    /// <summary>
    /// Set to synchronize the player's nickname with everyone in the room(s) you enter. This sets PhotonNetwork.player.NickName.
    /// </summary>
    /// <remarks>
    /// The playerName is just a nickname and does not have to be unique or backed up with some account.<br/>
    /// Set the value any time (e.g. before you connect) and it will be available to everyone you play with.<br/>
    /// Access the names of players by: PhotonPlayer.NickName. <br/>
    /// PhotonNetwork.otherPlayers is a list of other players - each contains the playerName the remote player set.
    /// </remarks>
    public static string playerName
    {
        get
        {
            return networkingPeer.PlayerName;
        }

        set
        {
            networkingPeer.PlayerName = value;
        }
    }

    /// <summary>The list of players in the current room, including the local player.</summary>
    /// <remarks>
    /// This list is only valid, while the client is in a room.
    /// It automatically gets updated when someone joins or leaves.
    ///
    /// This can be used to list all players in a room.
    /// Each player's PhotonPlayer.customProperties are accessible (set and synchronized via
    /// PhotonPlayer.SetCustomProperties).
    ///
    /// You can use a PhotonPlayer.TagObject to store an arbitrary object for reference.
    /// That is not synchronized via the network.
    /// </remarks>
    public static PhotonPlayer[] playerList
    {
        get
        {
            if (networkingPeer == null)
                return new PhotonPlayer[0];

            return networkingPeer.mPlayerListCopy;
        }
    }

    /// <summary>The list of players in the current room, excluding the local player.</summary>
    /// <remarks>
    /// This list is only valid, while the client is in a room.
    /// It automatically gets updated when someone joins or leaves.
    ///
    /// This can be used to list all other players in a room.
    /// Each player's PhotonPlayer.customProperties are accessible (set and synchronized via
    /// PhotonPlayer.SetCustomProperties).
    ///
    /// You can use a PhotonPlayer.TagObject to store an arbitrary object for reference.
    /// That is not synchronized via the network.
    /// </remarks>
    public static PhotonPlayer[] otherPlayers
    {
        get
        {
            if (networkingPeer == null)
                return new PhotonPlayer[0];

            return networkingPeer.mOtherPlayerListCopy;
        }
    }

    /// <summary>
    /// Read-only list of friends, their online status and the room they are in. Null until initialized by a FindFriends call.
    /// </summary>
    /// <remarks>
    /// Do not modify this list!
    /// It is internally handled by FindFriends and only available to read the values.
    /// The value of FriendListAge tells you how old the data is in milliseconds.
    ///
    /// Don't get this list more often than useful (> 10 seconds). In best case, keep the list you fetch really short.
    /// You could (e.g.) get the full list only once, then request a few updates only for friends who are online.
    /// After a while (e.g. 1 minute), you can get the full list again (to update online states).
    /// </remarks>
    public static List<FriendInfo> Friends { get; internal set; }

    /// <summary>
    /// Age of friend list info (in milliseconds). It's 0 until a friend list is fetched.
    /// </summary>
    public static int FriendsListAge
    {
        get { return (networkingPeer != null) ? networkingPeer.FriendListAge : 0; }
    }

    /// <summary>
    /// The minimum difference that a Vector2 or Vector3(e.g. a transforms rotation) needs to change before we send it via a PhotonView's OnSerialize/ObservingComponent.
    /// </summary>
    /// <remarks>
    /// Note that this is the sqrMagnitude. E.g. to send only after a 0.01 change on the Y-axix, we use 0.01f*0.01f=0.0001f. As a remedy against float inaccuracy we use 0.000099f instead of 0.0001f.
    /// </remarks>
    public static float precisionForVectorSynchronization = 0.000099f;

    /// <summary>
    /// The minimum angle that a rotation needs to change before we send it via a PhotonView's OnSerialize/ObservingComponent.
    /// </summary>
    public static float precisionForQuaternionSynchronization = 1.0f;

    /// <summary>
    /// The minimum difference between floats before we send it via a PhotonView's OnSerialize/ObservingComponent.
    /// </summary>
    public static float precisionForFloatSynchronization = 0.01f;

    /// <summary>
    /// While enabled, the MonoBehaviours on which we call RPCs are cached, avoiding costly GetComponents<MonoBehaviour>() calls.
    /// </summary>
    /// <remarks>
    /// RPCs are called on the MonoBehaviours of a target PhotonView. Those have to be found via GetComponents.
    ///
    /// When set this to true, the list of MonoBehaviours gets cached in each PhotonView.
    /// You can use photonView.RefreshRpcMonoBehaviourCache() to manually refresh a PhotonView's
    /// list of MonoBehaviours on demand (when a new MonoBehaviour gets added to a networked GameObject, e.g.).
    /// </remarks>
    public static bool UseRpcMonoBehaviourCache;

    /// <summary>
    /// While enabled (true), Instantiate uses PhotonNetwork.PrefabCache to keep game objects in memory (improving instantiation of the same prefab).
    /// </summary>
    /// <remarks>
    /// Setting UsePrefabCache to false during runtime will not clear PrefabCache but will ignore it right away.
    /// You could clean and modify the cache yourself. Read its comments.
    /// </remarks>
    public static bool UsePrefabCache = true;

    /// <summary>
    /// An Object Pool can be used to keep and reuse instantiated object instances. It replaced Unity's default Instantiate and Destroy methods.
    /// </summary>
    /// <remarks>
    /// To use a GameObject pool, implement IPunPrefabPool and assign it here.
    /// Prefabs are identified by name.
    /// </remarks>
    public static IPunPrefabPool PrefabPool { get { return networkingPeer.ObjectPool; } set { networkingPeer.ObjectPool = value; }}

    /// <summary>
    /// Keeps references to GameObjects for frequent instantiation (out of memory instead of loading the Resources).
    /// </summary>
    /// <remarks>
    /// You should be able to modify the cache anytime you like, except while Instantiate is used. Best do it only in the main-Thread.
    /// </remarks>
    public static Dictionary<string, GameObject> PrefabCache = new Dictionary<string, GameObject>();

    /// <summary>
    /// If not null, this is the (exclusive) list of GameObjects that get called by PUN SendMonoMessage().
    /// </summary>
    /// <remarks>
    /// For all callbacks defined in PhotonNetworkingMessage, PUN will use SendMonoMessage and
    /// call FindObjectsOfType() to find all scripts and GameObjects that might want a callback by PUN.
    ///
    /// PUN callbacks are not very frequent (in-game, property updates are most frequent) but
    /// FindObjectsOfType is time consuming and with a large number of GameObjects, performance might
    /// suffer.
    ///
    /// Optionally, SendMonoMessageTargets can be used to supply a list of target GameObjects. This
    /// skips the FindObjectsOfType() but any GameObject that needs callbacks will have to Add itself
    /// to this list.
    ///
    /// If null, the default behaviour is to do a SendMessage on each GameObject with a MonoBehaviour.
    /// </remarks>
    public static HashSet<GameObject> SendMonoMessageTargets;


    /// <summary>
    /// Defines which classes can contain PUN Callback implementations.
    /// </summary>
    /// <remarks>
    /// This provides the option to optimize your runtime for speed.<br/>
    /// The more specific this Type is, the fewer classes will be checked with reflection for callback methods.
    /// </remarks>
    public static Type SendMonoMessageTargetType = typeof(MonoBehaviour);

    /// <summary>
    /// Can be used to skip starting RPCs as Coroutine, which can be a performance issue.
    /// </summary>
    public static bool StartRpcsAsCoroutine = true;

    /// <summary>
    /// Offline mode can be set to re-use your multiplayer code in singleplayer game modes.
    /// When this is on PhotonNetwork will not create any connections and there is near to
    /// no overhead. Mostly usefull for reusing RPC's and PhotonNetwork.Instantiate
    /// </summary>
    public static bool offlineMode
    {
        get
        {
            return isOfflineMode;
        }

        set
        {
            if (value == isOfflineMode)
            {
                return;
            }

            if (value && connected)
            {
                Debug.LogError("Can't start OFFLINE mode while connected!");
                return;
            }

            if (networkingPeer.PeerState != PeerStateValue.Disconnected)
            {
                networkingPeer.Disconnect(); // Cleanup (also calls OnLeftRoom to reset stuff)
            }
            isOfflineMode = value;
            if (isOfflineMode)
            {
                networkingPeer.ChangeLocalID(-1);
                NetworkingPeer.SendMonoMessage(PhotonNetworkingMessage.OnConnectedToMaster);
            }
            else
            {
                offlineModeRoom = null;
                networkingPeer.ChangeLocalID(-1);
            }
        }
    }

    private static bool isOfflineMode = false;
    private static Room offlineModeRoom = null;


    /// <summary>Only used in Unity Networking. In PUN, set the number of players in PhotonNetwork.CreateRoom.</summary>
    [Obsolete("Used for compatibility with Unity networking only.")]
    public static int maxConnections;

    /// <summary>Defines if all clients in a room should load the same level as the Master Client (if that used PhotonNetwork.LoadLevel).</summary>
    /// <remarks>
    /// To synchronize the loaded level, the Master Client should use PhotonNetwork.LoadLevel.
    /// All clients will load the new scene when they get the update or when they join.
    ///
    /// Internally, a Custom Room Property is set for the loaded scene. When a client reads that
    /// and is not in the same scene yet, it will immediately pause the Message Queue
    /// (PhotonNetwork.isMessageQueueRunning = false) and load. When the scene finished loading,
    /// PUN will automatically re-enable the Message Queue.
    /// </remarks>
    public static bool automaticallySyncScene
    {
        get
        {
            return _mAutomaticallySyncScene;
        }
        set
        {
            _mAutomaticallySyncScene = value;
            if (_mAutomaticallySyncScene && room != null)
            {
                networkingPeer.LoadLevelIfSynced();
            }
        }
    }

    private static bool _mAutomaticallySyncScene = false;

    /// <summary>
    /// This setting defines per room, if network-instantiated GameObjects (with PhotonView) get cleaned up when the creator of it leaves.
    /// </summary>
    /// <remarks>
    /// This setting is done per room. It can't be changed in the room and it will override the settings of individual clients.
    ///
    /// If room.AutoCleanUp is enabled in a room, the PUN clients will destroy a player's GameObjects on leave.
    /// This includes GameObjects manually instantiated (via RPCs, e.g.).
    /// When enabled, the server will clean RPCs, instantiated GameObjects and PhotonViews of the leaving player, too. and
    /// Players who join after someone left, won't get the events of that player anymore.
    ///
    /// Under the hood, this setting is stored as a Custom Room Property.
    /// Enabled by default.
    /// </remarks>
    public static bool autoCleanUpPlayerObjects
    {
        get
        {
            return m_autoCleanUpPlayerObjects;
        }
        set
        {
            if (room != null)
                Debug.LogError("Setting autoCleanUpPlayerObjects while in a room is not supported.");
            else m_autoCleanUpPlayerObjects = value;
        }
    }

    private static bool m_autoCleanUpPlayerObjects = true;

    /// <summary>
    /// Set in PhotonServerSettings asset. Defines if the PhotonNetwork should join the "lobby" when connected to the Master server.
    /// </summary>
    /// <remarks>
    /// If this is false, OnConnectedToMaster() will be called when connection to the Master is available.
    /// OnJoinedLobby() will NOT be called if this is false.
    ///
    /// Enabled by default.
    ///
    /// The room listing will not become available.
    /// Rooms can be created and joined (randomly) without joining the lobby (and getting sent the room list).
    /// </remarks>
    public static bool autoJoinLobby
    {
        get
        {
            return PhotonNetwork.PhotonServerSettings.JoinLobby;
        }
        set
        {
            PhotonNetwork.PhotonServerSettings.JoinLobby = value;
        }
    }


    /// <summary>
    /// Set in PhotonServerSettings asset. Enable to get a list of active lobbies from the Master Server.
    /// </summary>
    /// <remarks>
    /// Lobby Statistics can be useful if a game uses multiple lobbies and you want
    /// to show activity of each to players.
    ///
    /// This value is stored in PhotonServerSettings.
    ///
    /// PhotonNetwork.LobbyStatistics is updated when you connect to the Master Server.
    /// There is also a callback PunBehaviour.
    /// </remarks>
    public static bool EnableLobbyStatistics
    {
        get
        {
            return PhotonNetwork.PhotonServerSettings.EnableLobbyStatistics;
        }
        set
        {
            PhotonNetwork.PhotonServerSettings.EnableLobbyStatistics = value;
        }
    }

    /// <summary>
    /// If turned on, the Master Server will provide information about active lobbies for this application.
    /// </summary>
    /// <remarks>
    /// Lobby Statistics can be useful if a game uses multiple lobbies and you want
    /// to show activity of each to players. Per lobby, you get: name, type, room- and player-count.
    ///
    /// PhotonNetwork.LobbyStatistics is updated when you connect to the Master Server.
    /// There is also a callback PunBehaviour.OnLobbyStatisticsUpdate, which you should implement
    /// to update your UI (e.g.).
    ///
    /// Lobby Statistics are not turned on by default.
    /// Enable them in the PhotonServerSettings file of the project.
    /// </remarks>
    public static List<TypedLobbyInfo> LobbyStatistics
    {
        get { return PhotonNetwork.networkingPeer.LobbyStatistics; }
        // only available to reset the state conveniently. done by state updates of PUN
        private set { PhotonNetwork.networkingPeer.LobbyStatistics = value; }
    }


    /// <summary>True while this client is in a lobby.</summary>
    /// <remarks>
    /// Implement IPunCallbacks.OnReceivedRoomListUpdate() for a notification when the list of rooms
    /// becomes available or updated.
    ///
    /// You are automatically leaving any lobby when you join a room!
    /// Lobbies only exist on the Master Server (whereas rooms are handled by Game Servers).
    /// </remarks>
    public static bool insideLobby
    {
        get
        {
            return networkingPeer.insideLobby;
        }
    }

    /// <summary>
    /// The lobby that will be used when PUN joins a lobby or creates a game.
    /// </summary>
    /// <remarks>
    /// The default lobby uses an empty string as name.
    /// PUN will enter a lobby on the Master Server if autoJoinLobby is set to true.
    /// So when you connect or leave a room, PUN automatically gets you into a lobby again.
    ///
    /// Check PhotonNetwork.insideLobby if the client is in a lobby.
    /// (@ref masterServerAndLobby)
    /// </remarks>
    public static TypedLobby lobby
    {
        get { return networkingPeer.lobby; }
        set { networkingPeer.lobby = value; }
    }

    /// <summary>
    /// Defines how many times per second PhotonNetwork should send a package. If you change
    /// this, do not forget to also change 'sendRateOnSerialize'.
    /// </summary>
    /// <remarks>
    /// Less packages are less overhead but more delay.
    /// Setting the sendRate to 50 will create up to 50 packages per second (which is a lot!).
    /// Keep your target platform in mind: mobile networks are slower and less reliable.
    /// </remarks>
    public static int sendRate
    {
        get
        {
            return 1000 / sendInterval;
        }

        set
        {
            sendInterval = 1000 / value;
            if (photonMono != null)
            {
                photonMono.updateInterval = sendInterval;
            }

            if (value < sendRateOnSerialize)
            {
                // sendRateOnSerialize needs to be <= sendRate
                sendRateOnSerialize = value;
            }
        }
    }

    /// <summary>
    /// Defines how many times per second OnPhotonSerialize should be called on PhotonViews.
    /// </summary>
    /// <remarks>
    /// Choose this value in relation to PhotonNetwork.sendRate. OnPhotonSerialize will create updates and messages to be sent.<br/>
    /// A lower rate takes up less performance but will cause more lag.
    /// </remarks>
    public static int sendRateOnSerialize
    {
        get
        {
            return 1000 / sendIntervalOnSerialize;
        }

        set
        {
            if (value > sendRate)
            {
                Debug.LogError("Error: Can not set the OnSerialize rate higher than the overall SendRate.");
                value = sendRate;
            }

            sendIntervalOnSerialize = 1000 / value;
            if (photonMono != null)
            {
                photonMono.updateIntervalOnSerialize = sendIntervalOnSerialize;
            }
        }
    }

    private static int sendInterval = 50; // in miliseconds.

    private static int sendIntervalOnSerialize = 100; // in miliseconds. I.e. 100 = 100ms which makes 10 times/second

    /// <summary>
    /// Can be used to pause dispatching of incoming evtents (RPCs, Instantiates and anything else incoming).
    /// </summary>
    /// <remarks>
    /// While IsMessageQueueRunning == false, the OnPhotonSerializeView calls are not done and nothing is sent by
    /// a client. Also, incoming messages will be queued until you re-activate the message queue.
    ///
    /// This can be useful if you first want to load a level, then go on receiving data of PhotonViews and RPCs.
    /// The client will go on receiving and sending acknowledgements for incoming packages and your RPCs/Events.
    /// This adds "lag" and can cause issues when the pause is longer, as all incoming messages are just queued.
    /// </remarks>
    public static bool isMessageQueueRunning
    {
        get
        {
            return m_isMessageQueueRunning;
        }

        set
        {
            if (value) PhotonHandler.StartFallbackSendAckThread();
            m_isMessageQueueRunning = value;
        }
    }

    /// <summary>Backup for property isMessageQueueRunning.</summary>
    private static bool m_isMessageQueueRunning = true;

    /// <summary>
    /// Photon network time, synched with the server.
    /// </summary>
    /// <remarks>
    /// v1.55<br/>
    /// This time value depends on the server's Environment.TickCount. It is different per server
    /// but inside a Room, all clients should have the same value (Rooms are on one server only).<br/>
    /// This is not a DateTime!<br/>
    ///
    /// Use this value with care: <br/>
    /// It can start with any positive value.<br/>
    /// It will "wrap around" from 4294967.295 to 0!
    /// </remarks>
    public static double time
    {
        get
        {
            uint u = (uint)ServerTimestamp;
            double t = u;
            return t / 1000;
        }
    }

    /// <summary>
    /// The current server's millisecond timestamp.
    /// </summary>
    /// <remarks>
    /// This can be useful to sync actions and events on all clients in one room.
    ///
    /// It will overflow from a positive to a negative value every so often, so
    /// be careful to use only time-differences to check the time delta when things
    /// happen.
    ///
    /// This is the basis for PhotonNetwork.time.
    /// </remarks>
    public static int ServerTimestamp
    {
        get
        {
            if (offlineMode)
            {
                return (int)startupStopwatch.ElapsedMilliseconds;   
            }

            return networkingPeer.ServerTimeInMilliSeconds;
        }
    }

	static Stopwatch startupStopwatch;

    /// <summary>
    /// Defines how many seconds PUN keeps the connection, after Unity's OnApplicationPause(true) call. Default: 60 seconds.
    /// </summary>
    /// <remarks>
    /// It's best practice to disconnect inactive apps/connections after a while but to also allow users to take calls, etc..
    /// We think a reasonable backgroung timeout is 60 seconds.
    ///
    /// To handle the timeout, implement: OnDisconnectedFromPhoton(), as usual.
    /// Your application will "notice" the background disconnect when it becomes active again (running the Update() loop).
    ///
    /// If you need to separate this case from others, you need to track if the app was in the background
    /// (there is no special callback by PUN).
    ///
    /// A value below 0.1 seconds will disable this timeout (careful: connections can be kept indefinitely).
    ///
    ///
    /// Info:
    /// PUN is running a "fallback thread" to send ACKs to the server, even when Unity is not calling Update() regularly.
    /// This helps keeping the connection while loading scenes and assets and when the app is in the background.
    ///
    /// Note:
    /// Some platforms (e.g. iOS) don't allow to keep a connection while the app is in background.
    /// In those cases, this value does not change anything, the app immediately loses connection in background.
    ///
    /// Unity's OnApplicationPause() callback is broken in some exports (Android) of some Unity versions.
    /// Make sure OnApplicationPause() gets the callbacks you'd expect on the platform you target!
    /// Check PhotonHandler.OnApplicationPause(bool pause), to see the implementation.
    /// </remarks>
    public static float BackgroundTimeout = 60.0f;

    /// <summary>
    /// Are we the master client?
    /// </summary>
    public static bool isMasterClient
    {
        get
        {
            if (offlineMode)
            {
                return true;
            }
            else
            {
                return networkingPeer.mMasterClientId == player.ID;
            }
        }
    }

    /// <summary>Is true while being in a room (connectionStateDetailed == ClientState.Joined).</summary>
    /// <remarks>
    /// Many actions can only be executed in a room, like Instantiate or Leave, etc.
    /// You can join a room in offline mode, too.
    /// </remarks>
    public static bool inRoom
    {
        get
        {
            // in offline mode, you can be in a room too and connectionStateDetailed then returns Joined like on online mode!
            return connectionStateDetailed == ClientState.Joined;
        }
    }

    /// <summary>
    /// True if we are in a room (client) and NOT the room's masterclient
    /// </summary>
    public static bool isNonMasterClientInRoom
    {
        get
        {
            return !isMasterClient && room != null;
        }
    }

    /// <summary>
    /// The count of players currently looking for a room (available on MasterServer in 5sec intervals).
    /// </summary>
    public static int countOfPlayersOnMaster
    {
        get
        {
            return networkingPeer.PlayersOnMasterCount;
        }
    }

    /// <summary>
    /// Count of users currently playing your app in some room (sent every 5sec by Master Server).
    /// Use PhotonNetwork.playerList.Length or PhotonNetwork.room.PlayerCount to get the count of players in the room you're in!
    /// </summary>
    public static int countOfPlayersInRooms
    {
        get
        {
            return networkingPeer.PlayersInRoomsCount;
        }
    }

    /// <summary>
    /// The count of players currently using this application (available on MasterServer in 5sec intervals).
    /// </summary>
    public static int countOfPlayers
    {
        get
        {
            return networkingPeer.PlayersInRoomsCount + networkingPeer.PlayersOnMasterCount;
        }
    }

    /// <summary>
    /// The count of rooms currently in use (available on MasterServer in 5sec intervals).
    /// </summary>
    /// <remarks>
    /// While inside the lobby you can also check the count of listed rooms as: PhotonNetwork.GetRoomList().Length.
    /// Since PUN v1.25 this is only based on the statistic event Photon sends (counting all rooms).
    /// </remarks>
    public static int countOfRooms
    {
        get
        {
            return networkingPeer.RoomsCount;
        }
    }

    /// <summary>
    /// Enables or disables the collection of statistics about this client's traffic.
    /// </summary>
    /// <remarks>
    /// If you encounter issues with clients, the traffic stats are a good starting point to find solutions.
    /// Only with enabled stats, you can use GetVitalStats
    /// </remarks>
    public static bool NetworkStatisticsEnabled
    {
        get
        {
            return networkingPeer.TrafficStatsEnabled;
        }

        set
        {
            networkingPeer.TrafficStatsEnabled = value;
        }
    }

    /// <summary>
    /// Count of commands that got repeated (due to local repeat-timing before an ACK was received).
    /// </summary>
    /// <remarks>
    /// If this value increases a lot, there is a good chance that a timeout disconnect will happen due to bad conditions.
    /// </remarks>
    public static int ResentReliableCommands
    {
        get { return networkingPeer.ResentReliableCommands; }
    }

    /// <summary>Crc checks can be useful to detect and avoid issues with broken datagrams. Can be enabled while not connected.</summary>
    public static bool CrcCheckEnabled
    {
        get { return networkingPeer.CrcEnabled; }
        set
        {
            if (!connected && !connecting)
            {
                networkingPeer.CrcEnabled = value;
            }
            else
            {
                Debug.Log("Can't change CrcCheckEnabled while being connected. CrcCheckEnabled stays " + networkingPeer.CrcEnabled);
            }
        }
    }

    /// <summary>If CrcCheckEnabled, this counts the incoming packages that don't have a valid CRC checksum and got rejected.</summary>
    public static int PacketLossByCrcCheck
    {
        get { return networkingPeer.PacketLossByCrc; }
    }

    /// <summary>Defines the number of times a reliable message can be resent before not getting an ACK for it will trigger a disconnect. Default: 5.</summary>
    /// <remarks>Less resends mean quicker disconnects, while more can lead to much more lag without helping. Min: 3. Max: 10.</remarks>
    public static int MaxResendsBeforeDisconnect
    {
        get { return networkingPeer.SentCountAllowance; }
        set
        {
            if (value < 3) value = 3;
            if (value > 10) value = 10;
            networkingPeer.SentCountAllowance = value;
        }
    }

    /// <summary>In case of network loss, reliable messages can be repeated quickly up to 3 times.</summary>
    /// <remarks>
    /// When reliable messages get lost more than once, subsequent repeats are delayed a bit
    /// to allow the network to recover.<br/>
    /// With this option, the repeats 2 and 3 can be sped up. This can help avoid timeouts but
    /// also it increases the speed in which gaps are closed.<br/>
    /// When you set this, increase PhotonNetwork.MaxResendsBeforeDisconnect to 6 or 7.
    /// </remarks>
    public static int QuickResends
    {
        get { return networkingPeer.QuickResendAttempts; }
        set
        {
            if (value < 0) value = 0;
            if (value > 3) value = 3;
            networkingPeer.QuickResendAttempts = (byte)value;
        }
    }

    /// <summary>Switch to alternative ports for a UDP connection to the Public Cloud.</summary>
    /// <remarks>
    /// This should be used when a customer has issues with connection stability. Some players
    /// reported better connectivity for Steam games. The effect might vary, which is why the
    /// alternative ports are not the new default.
    ///
    /// The alternative (server) ports are 27000 up to 27003.
    ///
    /// The values are appplied by replacing any incoming server-address string accordingly.
    /// You only need to set this to true though.
    ///
    /// This value does not affect TCP or WebSocket connections.
    /// </remarks>
    public static bool UseAlternativeUdpPorts { get; set; }


    /// <summary>
    /// Defines the delegate usable in OnEventCall.
    /// </summary>
    /// <remarks>Any eventCode &lt; 200 will be forwarded to your delegate(s).</remarks>
    /// <param name="eventCode">The code assigend to the incoming event.</param>
    /// <param name="content">The content the sender put into the event.</param>
    /// <param name="senderId">The ID of the player who sent the event. It might be 0, if the "room" sent the event.</param>
    public delegate void EventCallback(byte eventCode, object content, int senderId);

    /// <summary>Register your RaiseEvent handling methods here by using "+=".</summary>
    /// <remarks>Any eventCode &lt; 200 will be forwarded to your delegate(s).</remarks>
    /// <see cref="RaiseEvent"/>
	public static event EventCallback OnEventCall;


    internal static int lastUsedViewSubId = 0;  // each player only needs to remember it's own (!) last used subId to speed up assignment
    internal static int lastUsedViewSubIdStatic = 0;  // per room, the master is able to instantiate GOs. the subId for this must be unique too
    internal static List<int> manuallyAllocatedViewIds = new List<int>();

    /// <summary>
    /// Static constructor used for basic setup.
    /// </summary>
    static PhotonNetwork()
    {
        #if UNITY_EDITOR
        if (PhotonServerSettings == null)
        {
            // create PhotonServerSettings
            CreateSettings();
        }

        if (!EditorApplication.isPlaying && !EditorApplication.isPlayingOrWillChangePlaymode)
        {
            //Debug.Log(string.Format("PhotonNetwork.ctor() Not playing {0} {1}", UnityEditor.EditorApplication.isPlaying, UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode));
            return;
        }

        // This can happen when you recompile a script IN play made
        // This helps to surpress some errors, but will not fix breaking
        PhotonHandler[] photonHandlers = GameObject.FindObjectsOfType(typeof(PhotonHandler)) as PhotonHandler[];
        if (photonHandlers != null && photonHandlers.Length > 0)
        {
            Debug.LogWarning("Unity recompiled. Connection gets closed and replaced. You can connect as 'new' client.");
            foreach (PhotonHandler photonHandler in photonHandlers)
            {
                //Debug.Log("Handler: " + photonHandler + " photonHandler.gameObject: " + photonHandler.gameObject);
                photonHandler.gameObject.hideFlags = 0;
                GameObject.DestroyImmediate(photonHandler.gameObject);
                Component.DestroyImmediate(photonHandler);
            }
        }
        #endif

		if (PhotonServerSettings != null)
		{
			Application.runInBackground = PhotonServerSettings.RunInBackground;
		}

        // Set up a MonoBehaviour to run Photon, and hide it
        GameObject photonGO = new GameObject();
        photonMono = (PhotonHandler)photonGO.AddComponent<PhotonHandler>();
        photonGO.name = "PhotonMono";
        photonGO.hideFlags = HideFlags.HideInHierarchy;


        // Set up the NetworkingPeer and use protocol of PhotonServerSettings
        ConnectionProtocol protocol = PhotonNetwork.PhotonServerSettings.Protocol;
        networkingPeer = new NetworkingPeer(string.Empty, protocol);
        networkingPeer.QuickResendAttempts = 2;
        networkingPeer.SentCountAllowance = 7;


        #if !UNITY_EDITOR && UNITY_XBOXONE
        networkingPeer.AuthMode = AuthModeOption.Auth;
        #endif

        startupStopwatch = new Stopwatch();
        startupStopwatch.Start();

        // Local player
        CustomTypes.Register();
    }

    /// <summary>
    /// While offline, the network protocol can be switched (which affects the ports you can use to connect).
    /// </summary>
    /// <remarks>
    /// When you switch the protocol, make sure to also switch the port for the master server. Default ports are:
    /// TCP: 4530
    /// UDP: 5055
    ///
    /// This could look like this:<br/>
    /// Connect(serverAddress, <udpport|tcpport>, appID, gameVersion)
    ///
    /// Or when you use ConnectUsingSettings(), the PORT in the settings can be switched like so:<br/>
    /// PhotonNetwork.PhotonServerSettings.ServerPort = 4530;
    ///
    /// The current protocol can be read this way:<br/>
    /// PhotonNetwork.networkingPeer.UsedProtocol
    ///
    /// This does not work with the native socket plugin of PUN+ on mobile!
    /// </remarks>
    /// <param name="cp">Network protocol to use as low level connection. UDP is default. TCP is not available on all platforms (see remarks).</param>
    public static void SwitchToProtocol(ConnectionProtocol cp)
    {
        // Debug.Log("SwitchToProtocol: " + cp + " PhotonNetwork.connected: " + PhotonNetwork.connected);
        networkingPeer.TransportProtocol = cp;
    }


    /// <summary>Connect to Photon as configured in the editor (saved in PhotonServerSettings file).</summary>
    /// <remarks>
    /// This method will disable offlineMode (which won't destroy any instantiated GOs) and it
    /// will set isMessageQueueRunning to true.
    ///
    /// Your server configuration is created by the PUN Wizard and contains the AppId and
    /// region for Photon Cloud games and the server address if you host Photon yourself.
    /// These settings usually don't change often.
    ///
    /// To ignore the config file and connect anywhere call: PhotonNetwork.ConnectToMaster.
    ///
    /// To connect to the Photon Cloud, a valid AppId must be in the settings file (shown in the Photon Cloud Dashboard).
    /// https://www.photonengine.com/dashboard
    ///
    /// Connecting to the Photon Cloud might fail due to:
    /// - Invalid AppId (calls: OnFailedToConnectToPhoton(). check exact AppId value)
    /// - Network issues (calls: OnFailedToConnectToPhoton())
    /// - Invalid region (calls: OnConnectionFail() with DisconnectCause.InvalidRegion)
    /// - Subscription CCU limit reached (calls: OnConnectionFail() with DisconnectCause.MaxCcuReached. also calls: OnPhotonMaxCccuReached())
    ///
    /// </remarks>
    /// <param name="gameVersion">This client's version number. Users are separated from each other by gameversion (which allows you to make breaking changes).</param>
    public static bool ConnectUsingSettings(string gameVersion)
    {
        if (networkingPeer.PeerState != PeerStateValue.Disconnected)
        {
            Debug.LogWarning("ConnectUsingSettings() failed. Can only connect while in state 'Disconnected'. Current state: " + networkingPeer.PeerState);
            return false;
        }
        if (PhotonServerSettings == null)
        {
            Debug.LogError("Can't connect: Loading settings failed. ServerSettings asset must be in any 'Resources' folder as: " + serverSettingsAssetFile);
            return false;
        }
        if (PhotonServerSettings.HostType == ServerSettings.HostingOption.NotSet)
        {
            Debug.LogError("You did not select a Hosting Type in your PhotonServerSettings. Please set it up or don't use ConnectUsingSettings().");
            return false;
        }

		// only apply Settings if logLevel is default ( see ServerSettings.cs), else it means it's been set programmatically
		if (PhotonNetwork.logLevel == PhotonLogLevel.ErrorsOnly)
		{
        	PhotonNetwork.logLevel = PhotonServerSettings.PunLogging;
		}

		// only apply Settings if logLevel is default ( see ServerSettings.cs), else it means it's been set programmatically
		if (PhotonNetwork.networkingPeer.DebugOut == DebugLevel.ERROR)
		{
        	PhotonNetwork.networkingPeer.DebugOut = PhotonServerSettings.NetworkLogging;
		}


        SwitchToProtocol(PhotonServerSettings.Protocol);
        networkingPeer.SetApp(PhotonServerSettings.AppID, gameVersion);

        if (PhotonServerSettings.HostType == ServerSettings.HostingOption.OfflineMode)
        {
            offlineMode = true;
            return true;
        }

        if (offlineMode)
        {
            // someone can set offlineMode in code and then call ConnectUsingSettings() with non-offline settings. Warning for that case:
            Debug.LogWarning("ConnectUsingSettings() disabled the offline mode. No longer offline.");
        }

        offlineMode = false; // Cleanup offline mode
        isMessageQueueRunning = true;
        networkingPeer.IsInitialConnect = true;

        if (PhotonServerSettings.HostType == ServerSettings.HostingOption.SelfHosted)
        {
            networkingPeer.IsUsingNameServer = false;
            networkingPeer.MasterServerAddress = (PhotonServerSettings.ServerPort == 0) ? PhotonServerSettings.ServerAddress : PhotonServerSettings.ServerAddress + ":" + PhotonServerSettings.ServerPort;

            return networkingPeer.Connect(networkingPeer.MasterServerAddress, ServerConnection.MasterServer);
        }

        if (PhotonServerSettings.HostType == ServerSettings.HostingOption.BestRegion)
        {
            return ConnectToBestCloudServer(gameVersion);
        }

        return networkingPeer.ConnectToRegionMaster(PhotonServerSettings.PreferredRegion);
    }

    /// <summary>Connect to a Photon Master Server by address, port, appID and game(client) version.</summary>
    /// <remarks>
    /// To connect to the Photon Cloud, a valid AppId must be in the settings file (shown in the Photon Cloud Dashboard).
    /// https://www.photonengine.com/dashboard
    ///
    /// Connecting to the Photon Cloud might fail due to:
    /// - Invalid AppId (calls: OnFailedToConnectToPhoton(). check exact AppId value)
    /// - Network issues (calls: OnFailedToConnectToPhoton())
    /// - Invalid region (calls: OnConnectionFail() with DisconnectCause.InvalidRegion)
    /// - Subscription CCU limit reached (calls: OnConnectionFail() with DisconnectCause.MaxCcuReached. also calls: OnPhotonMaxCccuReached())
    ///
    /// </remarks>
    /// <param name="masterServerAddress">The server's address (either your own or Photon Cloud address).</param>
    /// <param name="port">The server's port to connect to.</param>
    /// <param name="appID">Your application ID (Photon Cloud provides you with a GUID for your game).</param>
    /// <param name="gameVersion">This client's version number. Users are separated by gameversion (which allows you to make breaking changes).</param>
    public static bool ConnectToMaster(string masterServerAddress, int port, string appID, string gameVersion)
    {
        if (networkingPeer.PeerState != PeerStateValue.Disconnected)
        {
            Debug.LogWarning("ConnectToMaster() failed. Can only connect while in state 'Disconnected'. Current state: " + networkingPeer.PeerState);
            return false;
        }

        if (offlineMode)
        {
            offlineMode = false; // Cleanup offline mode
            Debug.LogWarning("ConnectToMaster() disabled the offline mode. No longer offline.");
        }

        if (!isMessageQueueRunning)
        {
            isMessageQueueRunning = true;
            Debug.LogWarning("ConnectToMaster() enabled isMessageQueueRunning. Needs to be able to dispatch incoming messages.");
        }

        networkingPeer.SetApp(appID, gameVersion);
        networkingPeer.IsUsingNameServer = false;
        networkingPeer.IsInitialConnect = true;
        networkingPeer.MasterServerAddress = (port == 0) ? masterServerAddress : masterServerAddress + ":" + port;

        return networkingPeer.Connect(networkingPeer.MasterServerAddress, ServerConnection.MasterServer);
    }

	/// <summary>Can be used to reconnect to the master server after a disconnect.</summary>
	/// <remarks>
	/// After losing connection, you can use this to connect a client to the region Master Server again.
	/// Cache the room name you're in and use ReJoin(roomname) to return to a game.
	/// Common use case: Press the Lock Button on a iOS device and you get disconnected immediately.
	/// </remarks>
    public static bool Reconnect()
    {
        if (string.IsNullOrEmpty(networkingPeer.MasterServerAddress))
        {
            Debug.LogWarning("Reconnect() failed. It seems the client wasn't connected before?! Current state: " + networkingPeer.PeerState);
            return false;
        }

        if (networkingPeer.PeerState != PeerStateValue.Disconnected)
        {
            Debug.LogWarning("Reconnect() failed. Can only connect while in state 'Disconnected'. Current state: " + networkingPeer.PeerState);
            return false;
        }

        if (offlineMode)
        {
            offlineMode = false; // Cleanup offline mode
            Debug.LogWarning("Reconnect() disabled the offline mode. No longer offline.");
        }

        if (!isMessageQueueRunning)
        {
            isMessageQueueRunning = true;
            Debug.LogWarning("Reconnect() enabled isMessageQueueRunning. Needs to be able to dispatch incoming messages.");
        }

        networkingPeer.IsUsingNameServer = false;
        networkingPeer.IsInitialConnect = false;
        return networkingPeer.ReconnectToMaster();
    }


    /// <summary>When the client lost connection during gameplay, this method attempts to reconnect and rejoin the room.</summary>
    /// <remarks>
    /// This method re-connects directly to the game server which was hosting the room PUN was in before.
    /// If the room was shut down in the meantime, PUN will call OnPhotonJoinRoomFailed and return this client to the Master Server.
    ///
    /// Check the return value, if this client will attempt a reconnect and rejoin (if the conditions are met).
    /// If ReconnectAndRejoin returns false, you can still attempt a Reconnect and ReJoin.
    ///
    /// Similar to PhotonNetwork.ReJoin, this requires you to use unique IDs per player (the UserID).
    /// </remarks>
    /// <returns>False, if there is no known room or game server to return to. Then, this client does not attempt the ReconnectAndRejoin.</returns>
    public static bool ReconnectAndRejoin()
    {
        if (networkingPeer.PeerState != PeerStateValue.Disconnected)
        {
            Debug.LogWarning("ReconnectAndRejoin() failed. Can only connect while in state 'Disconnected'. Current state: " + networkingPeer.PeerState);
            return false;
        }
        if (offlineMode)
        {
            offlineMode = false; // Cleanup offline mode
            Debug.LogWarning("ReconnectAndRejoin() disabled the offline mode. No longer offline.");
        }

        if (string.IsNullOrEmpty(networkingPeer.GameServerAddress))
        {
            Debug.LogWarning("ReconnectAndRejoin() failed. It seems the client wasn't connected to a game server before (no address).");
            return false;
        }
        if (networkingPeer.enterRoomParamsCache == null)
        {
            Debug.LogWarning("ReconnectAndRejoin() failed. It seems the client doesn't have any previous room to re-join.");
            return false;
        }

        if (!isMessageQueueRunning)
        {
            isMessageQueueRunning = true;
            Debug.LogWarning("ReconnectAndRejoin() enabled isMessageQueueRunning. Needs to be able to dispatch incoming messages.");
        }

        networkingPeer.IsUsingNameServer = false;
        networkingPeer.IsInitialConnect = false;
        return networkingPeer.ReconnectAndRejoin();
    }


    /// <summary>
    /// Connect to the Photon Cloud region with the lowest ping (on platforms that support Unity's Ping).
    /// </summary>
    /// <remarks>
    /// Will save the result of pinging all cloud servers in PlayerPrefs. Calling this the first time can take +-2 seconds.
    /// The ping result can be overridden via PhotonNetwork.OverrideBestCloudServer(..)
    /// This call can take up to 2 seconds if it is the first time you are using this, all cloud servers will be pinged to check for the best region.
    ///
    /// The PUN Setup Wizard stores your appID in a settings file and applies a server address/port.
    /// To connect to the Photon Cloud, a valid AppId must be in the settings file (shown in the Photon Cloud Dashboard).
    /// https://www.photonengine.com/dashboard
    ///
    /// Connecting to the Photon Cloud might fail due to:
    /// - Invalid AppId (calls: OnFailedToConnectToPhoton(). check exact AppId value)
    /// - Network issues (calls: OnFailedToConnectToPhoton())
    /// - Invalid region (calls: OnConnectionFail() with DisconnectCause.InvalidRegion)
    /// - Subscription CCU limit reached (calls: OnConnectionFail() with DisconnectCause.MaxCcuReached. also calls: OnPhotonMaxCccuReached())
    ///
    /// </remarks>
    /// <param name="gameVersion">This client's version number. Users are separated from each other by gameversion (which allows you to make breaking changes).</param>
    /// <returns>If this client is going to connect to cloud server based on ping. Even if true, this does not guarantee a connection but the attempt is being made.</returns>
    public static bool ConnectToBestCloudServer(string gameVersion)
    {
        if (networkingPeer.PeerState != PeerStateValue.Disconnected)
        {
            Debug.LogWarning("ConnectToBestCloudServer() failed. Can only connect while in state 'Disconnected'. Current state: " + networkingPeer.PeerState);
            return false;
        }

        if (PhotonServerSettings == null)
        {
            Debug.LogError("Can't connect: Loading settings failed. ServerSettings asset must be in any 'Resources' folder as: " + PhotonNetwork.serverSettingsAssetFile);
            return false;
        }

        if (PhotonServerSettings.HostType == ServerSettings.HostingOption.OfflineMode)
        {
            return PhotonNetwork.ConnectUsingSettings(gameVersion);
        }

        networkingPeer.IsInitialConnect = true;
        networkingPeer.SetApp(PhotonServerSettings.AppID, gameVersion);

        bool couldConnect = PhotonNetwork.networkingPeer.ConnectToNameServer();
        return couldConnect;
    }


    /// <summary>
    /// Connects to the Photon Cloud region and cluster of choice.
    /// </summary>
    /// <remarks>
    /// Connecting to a specific cluster may be necessary, when regions get sharded and you support friends.
    /// 
    /// In all other cases, you should not define a cluster as this allows the Name Server to distribute
    /// clients as needed. A random, load balanced cluster will be selected if available.
    ///
    /// The Name Server has the final say to assign a cluster as available.
    /// Once connected, check the value of CurrentCluster.
    /// </remarks>
    public static bool ConnectToRegion(CloudRegionCode region, string gameVersion, string cluster = null)
    {
        if (networkingPeer.PeerState != PeerStateValue.Disconnected)
        {
            Debug.LogWarning("ConnectToRegion() failed. Can only connect while in state 'Disconnected'. Current state: " + networkingPeer.PeerState);
            return false;
        }

        if (PhotonServerSettings == null)
        {
            Debug.LogError("Can't connect: ServerSettings asset must be in any 'Resources' folder as: " + PhotonNetwork.serverSettingsAssetFile);
            return false;
        }

        if (PhotonServerSettings.HostType == ServerSettings.HostingOption.OfflineMode)
        {
            return PhotonNetwork.ConnectUsingSettings(gameVersion);
        }

        networkingPeer.IsInitialConnect = true;
        networkingPeer.SetApp(PhotonServerSettings.AppID, gameVersion);
        if (region != CloudRegionCode.none)
        {
            Debug.Log("ConnectToRegion: " + region);
            return networkingPeer.ConnectToRegionMaster(region, cluster);
        }

        return false;
    }

    /// <summary>Overwrites the region that is used for ConnectToBestCloudServer(string gameVersion).</summary>
    /// <remarks>
    /// This will overwrite the result of pinging all cloud servers.<br/>
    /// Use this to allow your users to save a manually selected region in the player preferences.<br/>
    /// Note: You can also use PhotonNetwork.ConnectToRegion to (temporarily) connect to a specific region.
    /// </remarks>
    public static void OverrideBestCloudServer(CloudRegionCode region)
    {
        PhotonHandler.BestRegionCodeInPreferences = region;
    }

    /// <summary>Pings all cloud servers again to find the one with best ping (currently).</summary>
    public static void RefreshCloudServerRating()
    {
        throw new NotImplementedException("not available at the moment");
    }


    /// <summary>
    /// Resets the traffic stats and re-enables them.
    /// </summary>
    public static void NetworkStatisticsReset()
    {
        networkingPeer.TrafficStatsReset();
    }


    /// <summary>
    /// Only available when NetworkStatisticsEnabled was used to gather some stats.
    /// </summary>
    /// <returns>A string with vital networking statistics.</returns>
    public static string NetworkStatisticsToString()
    {
        if (networkingPeer == null || offlineMode)
        {
            return "Offline or in OfflineMode. No VitalStats available.";
        }

        return networkingPeer.VitalStatsToString(false);
    }

    /// <summary>
    /// Used for compatibility with Unity networking only. Encryption is automatically initialized while connecting.
    /// </summary>
    [Obsolete("Used for compatibility with Unity networking only. Encryption is automatically initialized while connecting.")]
    public static void InitializeSecurity()
    {
        return;
    }

    /// <summary>
    /// Helper function which is called inside this class to erify if certain functions can be used (e.g. RPC when not connected)
    /// </summary>
    /// <returns></returns>
    private static bool VerifyCanUseNetwork()
    {
        if (connected)
        {
            return true;
        }

        Debug.LogError("Cannot send messages when not connected. Either connect to Photon OR use offline mode!");
        return false;
    }

    /// <summary>
    /// Makes this client disconnect from the photon server, a process that leaves any room and calls OnDisconnectedFromPhoton on completion.
    /// </summary>
    /// <remarks>
    /// When you disconnect, the client will send a "disconnecting" message to the server. This speeds up leave/disconnect
    /// messages for players in the same room as you (otherwise the server would timeout this client's connection).
    /// When used in offlineMode, the state-change and event-call OnDisconnectedFromPhoton are immediate.
    /// Offline mode is set to false as well.
    /// Once disconnected, the client can connect again. Use ConnectUsingSettings.
    /// </remarks>
    public static void Disconnect()
    {
        if (offlineMode)
        {
            offlineMode = false;
            offlineModeRoom = null;
            networkingPeer.State = ClientState.Disconnecting;
            networkingPeer.OnStatusChanged(StatusCode.Disconnect);
            return;
        }

        if (networkingPeer == null)
        {
            return; // Surpress error when quitting playmode in the editor
        }

        networkingPeer.Disconnect();
    }

    /// <summary>
    /// Requests the rooms and online status for a list of friends and saves the result in PhotonNetwork.Friends.
    /// </summary>
    /// <remarks>
    /// Works only on Master Server to find the rooms played by a selected list of users.
    ///
    /// The result will be stored in PhotonNetwork.Friends when available.
    /// That list is initialized on first use of OpFindFriends (before that, it is null).
    /// To refresh the list, call FindFriends again (in 5 seconds or 10 or 20).
    ///
    /// Users identify themselves by setting a unique userId in the PhotonNetwork.AuthValues.
    /// See remarks of AuthenticationValues for info about how this is set and used.
    ///
    /// The list of friends must be fetched from some other source (not provided by Photon).
    ///
    ///
    /// Internal:
    /// The server response includes 2 arrays of info (each index matching a friend from the request):
    /// ParameterCode.FindFriendsResponseOnlineList = bool[] of online states
    /// ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room)
    /// </remarks>
    /// <param name="friendsToFind">Array of friend (make sure to use unique playerName or AuthValues).</param>
    /// <param name="options">Options that affect the result of the FindFriends operation.</param>
    /// <returns>If the operation could be sent (requires connection, only one request is allowed at any time). Always false in offline mode.</returns>
    public static bool FindFriends(string[] friendsToFind, FindFriendsOptions options = null)
    {
        if (networkingPeer == null || isOfflineMode)
        {
            return false;
        }

        return networkingPeer.OpFindFriends(friendsToFind, options);
    }


    /// <summary>
    /// Creates a room with given name but fails if this room(name) is existing already. Creates random name for roomName null.
    /// </summary>
    /// <remarks>
    /// If you don't want to create a unique room-name, pass null or "" as name and the server will assign a roomName (a GUID as string).
    ///
    /// The created room is automatically placed in the currently used lobby (if any) or the default-lobby if you didn't explicitly join one.
    ///
    /// Call this only on the master server.
    /// Internally, the master will respond with a server-address (and roomName, if needed). Both are used internally
    /// to switch to the assigned game server and roomName.
    ///
    /// PhotonNetwork.autoCleanUpPlayerObjects will become this room's AutoCleanUp property and that's used by all clients that join this room.
    /// </remarks>
    /// <param name="roomName">Unique name of the room to create.</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool CreateRoom(string roomName)
    {
        return CreateRoom(roomName, null, null, null);
    }

    /// <summary>
    /// Creates a room but fails if this room is existing already. Can only be called on Master Server.
    /// </summary>
    /// <remarks>
    /// When successful, this calls the callbacks OnCreatedRoom and OnJoinedRoom (the latter, cause you join as first player).
    /// If the room can't be created (because it exists already), OnPhotonCreateRoomFailed gets called.
    ///
    /// If you don't want to create a unique room-name, pass null or "" as name and the server will assign a roomName (a GUID as string).
    ///
    /// Rooms can be created in any number of lobbies. Those don't have to exist before you create a room in them (they get
    /// auto-created on demand). Lobbies can be useful to split room lists on the server-side already. That can help keep the room
    /// lists short and manageable.
    /// If you set a typedLobby parameter, the room will be created in that lobby (no matter if you are active in any).
    /// If you don't set a typedLobby, the room is automatically placed in the currently active lobby (if any) or the
    /// default-lobby.
    ///
    /// Call this only on the master server.
    /// Internally, the master will respond with a server-address (and roomName, if needed). Both are used internally
    /// to switch to the assigned game server and roomName.
    ///
    /// PhotonNetwork.autoCleanUpPlayerObjects will become this room's autoCleanUp property and that's used by all clients that join this room.
    /// </remarks>
    /// <param name="roomName">Unique name of the room to create. Pass null or "" to make the server generate a name.</param>
    /// <param name="roomOptions">Common options for the room like MaxPlayers, initial custom room properties and similar. See RoomOptions type..</param>
    /// <param name="typedLobby">If null, the room is automatically created in the currently used lobby (which is "default" when you didn't join one explicitly).</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool CreateRoom(string roomName, RoomOptions roomOptions, TypedLobby typedLobby)
    {
        return CreateRoom(roomName, roomOptions, typedLobby, null);
    }

    /// <summary>
    /// Creates a room but fails if this room is existing already. Can only be called on Master Server.
    /// </summary>
    /// <remarks>
    /// When successful, this calls the callbacks OnCreatedRoom and OnJoinedRoom (the latter, cause you join as first player).
    /// If the room can't be created (because it exists already), OnPhotonCreateRoomFailed gets called.
    ///
    /// If you don't want to create a unique room-name, pass null or "" as name and the server will assign a roomName (a GUID as string).
    ///
    /// Rooms can be created in any number of lobbies. Those don't have to exist before you create a room in them (they get
    /// auto-created on demand). Lobbies can be useful to split room lists on the server-side already. That can help keep the room
    /// lists short and manageable.
    /// If you set a typedLobby parameter, the room will be created in that lobby (no matter if you are active in any).
    /// If you don't set a typedLobby, the room is automatically placed in the currently active lobby (if any) or the
    /// default-lobby.
    ///
    /// Call this only on the master server.
    /// Internally, the master will respond with a server-address (and roomName, if needed). Both are used internally
    /// to switch to the assigned game server and roomName.
    ///
    /// PhotonNetwork.autoCleanUpPlayerObjects will become this room's autoCleanUp property and that's used by all clients that join this room.
    ///
    /// You can define an array of expectedUsers, to block player slots in the room for these users.
    /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
    /// </remarks>
    /// <param name="roomName">Unique name of the room to create. Pass null or "" to make the server generate a name.</param>
    /// <param name="roomOptions">Common options for the room like MaxPlayers, initial custom room properties and similar. See RoomOptions type..</param>
    /// <param name="typedLobby">If null, the room is automatically created in the currently used lobby (which is "default" when you didn't join one explicitly).</param>
    /// <param name="expectedUsers">Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool CreateRoom(string roomName, RoomOptions roomOptions, TypedLobby typedLobby, string[] expectedUsers)
    {
        if (offlineMode)
        {
            if (offlineModeRoom != null)
            {
                Debug.LogError("CreateRoom failed. In offline mode you still have to leave a room to enter another.");
                return false;
            }
            EnterOfflineRoom(roomName, roomOptions, true);
            return true;
        }
        if (networkingPeer.Server != ServerConnection.MasterServer || !connectedAndReady)
        {
            Debug.LogError("CreateRoom failed. Client is not on Master Server or not yet ready to call operations. Wait for callback: OnJoinedLobby or OnConnectedToMaster.");
            return false;
        }

        typedLobby = typedLobby ?? ((networkingPeer.insideLobby) ? networkingPeer.lobby : null);  // use given lobby, or active lobby (if any active) or none

        EnterRoomParams opParams = new EnterRoomParams();
        opParams.RoomName = roomName;
        opParams.RoomOptions = roomOptions;
        opParams.Lobby = typedLobby;
        opParams.ExpectedUsers = expectedUsers;

        return networkingPeer.OpCreateGame(opParams);
    }


    /// <summary>Join room by roomname and on success calls OnJoinedRoom(). This is not affected by lobbies.</summary>
    /// <remarks>
    /// On success, the method OnJoinedRoom() is called on any script. You can implement it to react to joining a room.
    ///
    /// JoinRoom fails if the room is either full or no longer available (it might become empty while you attempt to join).
    /// Implement OnPhotonJoinRoomFailed() to get a callback in error case.
    ///
    /// To join a room from the lobby's listing, use RoomInfo.Name as roomName here.
    /// Despite using multiple lobbies, a roomName is always "global" for your application and so you don't
    /// have to specify which lobby it's in. The Master Server will find the room.
    /// In the Photon Cloud, an application is defined by AppId, Game- and PUN-version.
    /// </remarks>
    /// <see cref="PhotonNetworkingMessage.OnPhotonJoinRoomFailed"/>
    /// <see cref="PhotonNetworkingMessage.OnJoinedRoom"/>
    /// <param name="roomName">Unique name of the room to join.</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool JoinRoom(string roomName)
    {
        return JoinRoom(roomName, null);
    }

    /// <summary>Join room by roomname and on success calls OnJoinedRoom(). This is not affected by lobbies.</summary>
    /// <remarks>
    /// On success, the method OnJoinedRoom() is called on any script. You can implement it to react to joining a room.
    ///
    /// JoinRoom fails if the room is either full or no longer available (it might become empty while you attempt to join).
    /// Implement OnPhotonJoinRoomFailed() to get a callback in error case.
    ///
    /// To join a room from the lobby's listing, use RoomInfo.Name as roomName here.
    /// Despite using multiple lobbies, a roomName is always "global" for your application and so you don't
    /// have to specify which lobby it's in. The Master Server will find the room.
    /// In the Photon Cloud, an application is defined by AppId, Game- and PUN-version.
    ///
    /// You can define an array of expectedUsers, to block player slots in the room for these users.
    /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
    /// </remarks>
    /// <see cref="PhotonNetworkingMessage.OnPhotonJoinRoomFailed"/>
    /// <see cref="PhotonNetworkingMessage.OnJoinedRoom"/>
    /// <param name="roomName">Unique name of the room to join.</param>
    /// <param name="expectedUsers">Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool JoinRoom(string roomName, string[] expectedUsers)
    {
        if (offlineMode)
        {
            if (offlineModeRoom != null)
            {
                Debug.LogError("JoinRoom failed. In offline mode you still have to leave a room to enter another.");
                return false;
            }
            EnterOfflineRoom(roomName, null, true);
            return true;
        }
        if (networkingPeer.Server != ServerConnection.MasterServer || !connectedAndReady)
        {
            Debug.LogError("JoinRoom failed. Client is not on Master Server or not yet ready to call operations. Wait for callback: OnJoinedLobby or OnConnectedToMaster.");
            return false;
        }
        if (string.IsNullOrEmpty(roomName))
        {
            Debug.LogError("JoinRoom failed. A roomname is required. If you don't know one, how will you join?");
            return false;
        }


        EnterRoomParams opParams = new EnterRoomParams();
        opParams.RoomName = roomName;
        opParams.ExpectedUsers = expectedUsers;

        return networkingPeer.OpJoinRoom(opParams);
    }


    /// <summary>Lets you either join a named room or create it on the fly - you don't have to know if someone created the room already.</summary>
    /// <remarks>
    /// This makes it easier for groups of players to get into the same room. Once the group
    /// exchanged a roomName, any player can call JoinOrCreateRoom and it doesn't matter who
    /// actually joins or creates the room.
    ///
    /// The parameters roomOptions and typedLobby are only used when the room actually gets created by this client.
    /// You know if this client created a room, if you get a callback OnCreatedRoom (before OnJoinedRoom gets called as well).
    /// </remarks>
    /// <param name="roomName">Name of the room to join. Must be non null.</param>
    /// <param name="roomOptions">Options for the room, in case it does not exist yet. Else these values are ignored.</param>
    /// <param name="typedLobby">Lobby you want a new room to be listed in. Ignored if the room was existing and got joined.</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool JoinOrCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby typedLobby)
    {
        return JoinOrCreateRoom(roomName, roomOptions, typedLobby, null);
    }

    /// <summary>Lets you either join a named room or create it on the fly - you don't have to know if someone created the room already.</summary>
    /// <remarks>
    /// This makes it easier for groups of players to get into the same room. Once the group
    /// exchanged a roomName, any player can call JoinOrCreateRoom and it doesn't matter who
    /// actually joins or creates the room.
    ///
    /// The parameters roomOptions and typedLobby are only used when the room actually gets created by this client.
    /// You know if this client created a room, if you get a callback OnCreatedRoom (before OnJoinedRoom gets called as well).
    ///
    /// You can define an array of expectedUsers, to block player slots in the room for these users.
    /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
    /// </remarks>
    /// <param name="roomName">Name of the room to join. Must be non null.</param>
    /// <param name="roomOptions">Options for the room, in case it does not exist yet. Else these values are ignored.</param>
    /// <param name="typedLobby">Lobby you want a new room to be listed in. Ignored if the room was existing and got joined.</param>
    /// <param name="expectedUsers">Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool JoinOrCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby typedLobby, string[] expectedUsers)
    {
        if (offlineMode)
        {
            if (offlineModeRoom != null)
            {
                Debug.LogError("JoinOrCreateRoom failed. In offline mode you still have to leave a room to enter another.");
                return false;
            }
            EnterOfflineRoom(roomName, roomOptions, true);  // in offline mode, JoinOrCreateRoom assumes you create the room
            return true;
        }
        if (networkingPeer.Server != ServerConnection.MasterServer || !connectedAndReady)
        {
            Debug.LogError("JoinOrCreateRoom failed. Client is not on Master Server or not yet ready to call operations. Wait for callback: OnJoinedLobby or OnConnectedToMaster.");
            return false;
        }
        if (string.IsNullOrEmpty(roomName))
        {
            Debug.LogError("JoinOrCreateRoom failed. A roomname is required. If you don't know one, how will you join?");
            return false;
        }

        typedLobby = typedLobby ?? ((networkingPeer.insideLobby) ? networkingPeer.lobby : null);  // use given lobby, or active lobby (if any active) or none

        EnterRoomParams opParams = new EnterRoomParams();
        opParams.RoomName = roomName;
        opParams.RoomOptions = roomOptions;
        opParams.Lobby = typedLobby;
        opParams.CreateIfNotExists = true;
        opParams.PlayerProperties = player.CustomProperties;
        opParams.ExpectedUsers = expectedUsers;

        return networkingPeer.OpJoinRoom(opParams);
    }

    /// <summary>
    /// Joins any available room of the currently used lobby and fails if none is available.
    /// </summary>
    /// <remarks>
    /// Rooms can be created in arbitrary lobbies which get created on demand.
    /// You can join rooms from any lobby without actually joining the lobby.
    /// Use the JoinRandomRoom overload with TypedLobby parameter.
    ///
    /// This method will only match rooms attached to one lobby! If you use many lobbies, you
    /// might have to repeat JoinRandomRoom, to find some fitting room.
    /// This method looks up a room in the currently active lobby or (if no lobby is joined)
    /// in the default lobby.
    ///
    /// If this fails, you can still create a room (and make this available for the next who uses JoinRandomRoom).
    /// Alternatively, try again in a moment.
    /// </remarks>
    public static bool JoinRandomRoom()
    {
        return JoinRandomRoom(null, 0, MatchmakingMode.FillRoom, null, null);
    }

    /// <summary>
    /// Attempts to join an open room with fitting, custom properties but fails if none is currently available.
    /// </summary>
    /// <remarks>
    /// Rooms can be created in arbitrary lobbies which get created on demand.
    /// You can join rooms from any lobby without actually joining the lobby.
    /// Use the JoinRandomRoom overload with TypedLobby parameter.
    ///
    /// This method will only match rooms attached to one lobby! If you use many lobbies, you
    /// might have to repeat JoinRandomRoom, to find some fitting room.
    /// This method looks up a room in the currently active lobby or (if no lobby is joined)
    /// in the default lobby.
    ///
    /// If this fails, you can still create a room (and make this available for the next who uses JoinRandomRoom).
    /// Alternatively, try again in a moment.
    /// </remarks>
    /// <param name="expectedCustomRoomProperties">Filters for rooms that match these custom properties (string keys and values). To ignore, pass null.</param>
    /// <param name="expectedMaxPlayers">Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value.</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool JoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers)
    {
        return JoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, MatchmakingMode.FillRoom, null, null);
    }

    /// <summary>
    /// Attempts to join an open room with fitting, custom properties but fails if none is currently available.
    /// </summary>
    /// <remarks>
    /// Rooms can be created in arbitrary lobbies which get created on demand.
    /// You can join rooms from any lobby without actually joining the lobby with this overload.
    ///
    /// This method will only match rooms attached to one lobby! If you use many lobbies, you
    /// might have to repeat JoinRandomRoom, to find some fitting room.
    /// This method looks up a room in the specified lobby or the currently active lobby (if none specified)
    /// or in the default lobby (if none active).
    ///
    /// If this fails, you can still create a room (and make this available for the next who uses JoinRandomRoom).
    /// Alternatively, try again in a moment.
    ///
    /// In offlineMode, a room will be created but no properties will be set and all parameters of this
    /// JoinRandomRoom call are ignored. The event/callback OnJoinedRoom gets called (see enum PhotonNetworkingMessage).
    ///
    /// You can define an array of expectedUsers, to block player slots in the room for these users.
    /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
    /// </remarks>
    /// <param name="expectedCustomRoomProperties">Filters for rooms that match these custom properties (string keys and values). To ignore, pass null.</param>
    /// <param name="expectedMaxPlayers">Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value.</param>
    /// <param name="matchingType">Selects one of the available matchmaking algorithms. See MatchmakingMode enum for options.</param>
    /// <param name="typedLobby">The lobby in which you want to lookup a room. Pass null, to use the default lobby. This does not join that lobby and neither sets the lobby property.</param>
    /// <param name="sqlLobbyFilter">A filter-string for SQL-typed lobbies.</param>
    /// <param name="expectedUsers">Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.</param>
    /// <returns>If the operation got queued and will be sent.</returns>
    public static bool JoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, MatchmakingMode matchingType, TypedLobby typedLobby, string sqlLobbyFilter, string[] expectedUsers = null)
    {
        if (offlineMode)
        {
            if (offlineModeRoom != null)
            {
                Debug.LogError("JoinRandomRoom failed. In offline mode you still have to leave a room to enter another.");
                return false;
            }
            EnterOfflineRoom("offline room", null, true);
            return true;
        }
        if (networkingPeer.Server != ServerConnection.MasterServer || !connectedAndReady)
        {
            Debug.LogError("JoinRandomRoom failed. Client is not on Master Server or not yet ready to call operations. Wait for callback: OnJoinedLobby or OnConnectedToMaster.");
            return false;
        }

        typedLobby = typedLobby ?? ((networkingPeer.insideLobby) ? networkingPeer.lobby : null);  // use given lobby, or active lobby (if any active) or none

        OpJoinRandomRoomParams opParams = new OpJoinRandomRoomParams();
        opParams.ExpectedCustomRoomProperties = expectedCustomRoomProperties;
        opParams.ExpectedMaxPlayers = expectedMaxPlayers;
        opParams.MatchingType = matchingType;
        opParams.TypedLobby = typedLobby;
        opParams.SqlLobbyFilter = sqlLobbyFilter;
        opParams.ExpectedUsers = expectedUsers;

        return networkingPeer.OpJoinRandomRoom(opParams);
    }


	/// <summary>Can be used to return to a room after a disconnect and reconnect.</summary>
	/// <remarks>
	/// After losing connection, you might be able to return to a room and continue playing,
	/// if the client is reconnecting fast enough. Use Reconnect() and this method.
	/// Cache the room name you're in and use ReJoin(roomname) to return to a game.
	///
	/// Note: To be able to ReJoin any room, you need to use UserIDs!
	/// You also need to set RoomOptions.PlayerTtl.
	///
	/// <b>Important: Instantiate() and use of RPCs is not yet supported.</b>
	/// The ownership rules of PhotonViews prevent a seamless return to a game.
	/// Use Custom Properties and RaiseEvent with event caching instead.
	///
	/// Common use case: Press the Lock Button on a iOS device and you get disconnected immediately.
	/// </remarks>
    public static bool ReJoinRoom(string roomName)
    {
        if (offlineMode)
        {
            Debug.LogError("ReJoinRoom failed due to offline mode.");
            return false;
        }
        if (networkingPeer.Server != ServerConnection.MasterServer || !connectedAndReady)
        {
            Debug.LogError("ReJoinRoom failed. Client is not on Master Server or not yet ready to call operations. Wait for callback: OnJoinedLobby or OnConnectedToMaster.");
            return false;
        }
        if (string.IsNullOrEmpty(roomName))
        {
            Debug.LogError("ReJoinRoom failed. A roomname is required. If you don't know one, how will you join?");
            return false;
        }

        EnterRoomParams opParams = new EnterRoomParams();
        opParams.RoomName = roomName;
        opParams.RejoinOnly = true;
        opParams.PlayerProperties = player.CustomProperties;

        return networkingPeer.OpJoinRoom(opParams);
    }


    /// <summary>
    /// Internally used helper-method to setup an offline room, the numbers for actor and master-client and to do the callbacks.
    /// </summary>
    private static void EnterOfflineRoom(string roomName, RoomOptions roomOptions, bool createdRoom)
    {
        offlineModeRoom = new Room(roomName, roomOptions);
        networkingPeer.ChangeLocalID(1);
        networkingPeer.State = ClientState.ConnectingToGameserver;
        networkingPeer.OnStatusChanged(StatusCode.Connect);
        offlineModeRoom.MasterClientId = 1;

        if (createdRoom)
        {
            NetworkingPeer.SendMonoMessage(PhotonNetworkingMessage.OnCreatedRoom);
        }
        NetworkingPeer.SendMonoMessage(PhotonNetworkingMessage.OnJoinedRoom);
    }

    /// <summary>On MasterServer this joins the default lobby which list rooms currently in use.</summary>
    /// <remarks>
    /// The room list is sent and refreshed by the server. You can access this cached list by
    /// PhotonNetwork.GetRoomList().
    ///
    /// Per room you should check if it's full or not before joining. Photon also lists rooms that are
    /// full, unless you close and hide them (room.open = false and room.visible = false).
    ///
    /// In best case, you make your clients join random games, as described here:
    /// https://doc.photonengine.com/en-us/pun/current/lobby-and-matchmaking/matchmaking-and-lobby
    ///
    /// You can show your current players and room count without joining a lobby (but you must
    /// be on the master server). Use: countOfPlayers, countOfPlayersOnMaster, countOfPlayersInRooms and
    /// countOfRooms.
    ///
    /// You can use more than one lobby to keep the room lists shorter. See JoinLobby(TypedLobby lobby).
    /// When creating new rooms, they will be "attached" to the currently used lobby or the default lobby.
    ///
    /// You can use JoinRandomRoom without being in a lobby!
    /// Set autoJoinLobby = false before you connect, to not join a lobby. In that case, the
    /// connect-workflow will call OnConnectedToMaster (if you implement it) when it's done.
    /// </remarks>
    public static bool JoinLobby()
    {
        return JoinLobby(null);
    }

    /// <summary>On a Master Server you can join a lobby to get lists of available rooms.</summary>
    /// <remarks>
    /// The room list is sent and refreshed by the server. You can access this cached list by
    /// PhotonNetwork.GetRoomList().
    ///
    /// Any client can "make up" any lobby on the fly. Splitting rooms into multiple lobbies will
    /// keep each list shorter. However, having too many lists might ruin the matchmaking experience.
    ///
    /// In best case, you create a limited number of lobbies. For example, create a lobby per
    /// game-mode: "koth" for king of the hill and "ffa" for free for all, etc.
    ///
    /// There is no listing of lobbies at the moment.
    ///
    /// Sql-typed lobbies offer a different filtering model for random matchmaking. This might be more
    /// suited for skillbased-games. However, you will also need to follow the conventions for naming
    /// filterable properties in sql-lobbies! Both is explained in the matchmaking doc linked below.
    ///
    /// In best case, you make your clients join random games, as described here:
    /// http://confluence.exitgames.com/display/PTN/Op+JoinRandomGame
    ///
    ///
    /// Per room you should check if it's full or not before joining. Photon does list rooms that are
    /// full, unless you close and hide them (room.open = false and room.visible = false).
    ///
    /// You can show your games current players and room count without joining a lobby (but you must
    /// be on the master server). Use: countOfPlayers, countOfPlayersOnMaster, countOfPlayersInRooms and
    /// countOfRooms.
    ///
    /// When creating new rooms, they will be "attached" to the currently used lobby or the default lobby.
    ///
    /// You can use JoinRandomRoom without being in a lobby!
    /// Set autoJoinLobby = false before you connect, to not join a lobby. In that case, the
    /// connect-workflow will call OnConnectedToMaster (if you implement it) when it's done.
    /// </remarks>
    /// <param name="typedLobby">A typed lobby to join (must have name and type).</param>
    public static bool JoinLobby(TypedLobby typedLobby)
    {
        if (PhotonNetwork.connected && PhotonNetwork.Server == ServerConnection.MasterServer)
        {
            if (typedLobby == null)
            {
                typedLobby = TypedLobby.Default;
            }

            bool sending = networkingPeer.OpJoinLobby(typedLobby);
            if (sending)
            {
                networkingPeer.lobby = typedLobby;

            }
            return sending;
        }

        return false;
    }

    /// <summary>Leave a lobby to stop getting updates about available rooms.</summary>
    /// <remarks>
    /// This does not reset PhotonNetwork.lobby! This allows you to join this particular lobby later
    /// easily.
    ///
    /// The values countOfPlayers, countOfPlayersOnMaster, countOfPlayersInRooms and countOfRooms
    /// are received even without being in a lobby.
    ///
    /// You can use JoinRandomRoom without being in a lobby.
    /// Use autoJoinLobby to not join a lobby when you connect.
    /// </remarks>
    public static bool LeaveLobby()
    {
        if (PhotonNetwork.connected && PhotonNetwork.Server == ServerConnection.MasterServer)
        {
            return networkingPeer.OpLeaveLobby();
        }

        return false;
    }

    /// <summary>Leave the current room and return to the Master Server where you can join or create rooms (see remarks).</summary>
    /// <remarks>
    /// This will clean up all (network) GameObjects with a PhotonView, unless you changed autoCleanUp to false.
    /// Returns to the Master Server.
    ///
    /// In OfflineMode, the local "fake" room gets cleaned up and OnLeftRoom gets called immediately.
    ///
    /// In a room with playerTTL &lt; 0, LeaveRoom just turns a client inactive. The player stays in the room's player list
    /// and can return later on. Setting becomeInactive to false deliberately, means to "abandon" the room, despite the
    /// playerTTL allowing you to come back.
    ///
    /// In a room with playerTTL == 0, become inactive has no effect (clients are removed from the room right away).
    /// </remarks>
    /// <param name="becomeInactive">If this client becomes inactive in a room with playerTTL &lt; 0. Defaults to true.</param>
    public static bool LeaveRoom(bool becomeInactive = true)
    {
        if (offlineMode)
        {
            offlineModeRoom = null;
            networkingPeer.State = ClientState.PeerCreated;
            NetworkingPeer.SendMonoMessage(PhotonNetworkingMessage.OnLeftRoom);
        }
        else
        {
            if (room == null)
            {
                Debug.LogWarning("PhotonNetwork.room is null. You don't have to call LeaveRoom() when you're not in one. State: " + PhotonNetwork.connectionStateDetailed);
            }
            else
            {
                becomeInactive = becomeInactive && room.PlayerTtl != 0; // in a room with playerTTL == 0, the operation "leave" will never turn a client inactive
            }
            return networkingPeer.OpLeaveRoom(becomeInactive);
        }

        return true;
    }

    /// <summary>Fetches a custom list of games from the server, matching a SQL-like "where" clause, then triggers OnReceivedRoomListUpdate callback.</summary>
    /// <remarks>
    /// Operation is only available for lobbies of type SqlLobby. Note: You don't have to join that lobby.
    /// This is an async request.
    ///
    /// When done, OnReceivedRoomListUpdate gets called. Use GetRoomList() to access it.
    /// </remarks>
    /// <see cref="https://doc.photonengine.com/en-us/pun/current/lobby-and-matchmaking/matchmaking-and-lobby#sql_lobby_type"/>
    /// <param name="typedLobby">The lobby to query. Has to be of type SqlLobby.</param>
    /// <param name="sqlLobbyFilter">The sql query statement.</param>
    /// <returns>If the operation could be sent (has to be connected).</returns>
    public static bool GetCustomRoomList(TypedLobby typedLobby, string sqlLobbyFilter)
    {
        return networkingPeer.OpGetGameList(typedLobby, sqlLobbyFilter);
    }

    /// <summary>
    /// Gets currently cached rooms of the last rooms list sent by the server as RoomInfo array.
    /// This list is either available and updated automatically and periodically while in a lobby (check insideLobby) or
    /// received as a response to PhotonNetwork.GetCustomRoomList().
    /// </summary>
    /// <remarks>
    /// This list is a cached copy of the internal rooms list so it can be accessed each frame if needed.
    /// Per RoomInfo you can check if the room is full by comparing playerCount and MaxPlayers before you allow a join.
    ///
    /// The name of a room must be used to join it (via JoinRoom).
    ///
    /// Closed rooms are also listed by lobbies but they can't be joined. While in a room, any player can set
    /// Room.visible and Room.open to hide rooms from matchmaking and close them.
    /// </remarks>
    /// <returns>Cached RoomInfo[] of last room list sent by the server.</returns>
    public static RoomInfo[] GetRoomList()
    {
        if (offlineMode || networkingPeer == null)
        {
            return new RoomInfo[0];
        }

        return networkingPeer.mGameListCopy;
    }

    /// <summary>
    /// Sets this (local) player's properties and synchronizes them to the other players (don't modify them directly).
    /// </summary>
    /// <remarks>
    /// While in a room, your properties are synced with the other players.
    /// CreateRoom, JoinRoom and JoinRandomRoom will all apply your player's custom properties when you enter the room.
    /// The whole Hashtable will get sent. Minimize the traffic by setting only updated key/values.
    ///
    /// If the Hashtable is null, the custom properties will be cleared.
    /// Custom properties are never cleared automatically, so they carry over to the next room, if you don't change them.
    ///
    /// Don't set properties by modifying PhotonNetwork.player.customProperties!
    /// </remarks>
    /// <param name="customProperties">Only string-typed keys will be used from this hashtable. If null, custom properties are all deleted.</param>
    public static void SetPlayerCustomProperties(Hashtable customProperties)
    {
        if (customProperties == null)
        {
            customProperties = new Hashtable();
            foreach (object k in player.CustomProperties.Keys)
            {
                customProperties[(string)k] = null;
            }
        }

        if (room != null && room.IsLocalClientInside)
        {
            player.SetCustomProperties(customProperties);
        }
        else
        {
            player.InternalCacheProperties(customProperties);
        }
    }

    /// <summary>
    /// Locally removes Custom Properties of "this" player. Important: This does not synchronize the change! Useful when you switch rooms.
    /// </summary>
    /// <remarks>
    /// Use this method with care. It can create inconsistencies of state between players!
    /// This only changes the player.customProperties locally. This can be useful to clear your
    /// Custom Properties between games (let's say they store which turn you made, kills, etc).
    ///
    /// SetPlayerCustomProperties() syncs and can be used to set values to null while in a room.
    /// That can be considered "removed" while in a room.
    ///
    /// If customPropertiesToDelete is null or has 0 entries, all Custom Properties are deleted (replaced with a new Hashtable).
    /// If you specify keys to remove, those will be removed from the Hashtable but other keys are unaffected.
    /// </remarks>
    /// <param name="customPropertiesToDelete">List of Custom Property keys to remove. See remarks.</param>
    public static void RemovePlayerCustomProperties(string[] customPropertiesToDelete)
    {
        if (customPropertiesToDelete == null || customPropertiesToDelete.Length == 0 || player.CustomProperties == null)
        {
            player.CustomProperties = new Hashtable();
            return;
        }

        // if a specific list of props should be deleted, we do that here
        for (int i = 0; i < customPropertiesToDelete.Length; i++)
        {
            string key = customPropertiesToDelete[i];
            if (player.CustomProperties.ContainsKey(key))
            {
                player.CustomProperties.Remove(key);
            }
        }
    }

    /// <summary>
    /// Sends fully customizable events in a room. Events consist of at least an EventCode (0..199) and can have content.
    /// </summary>
    /// <remarks>
    /// To receive the events someone sends, register your handling method in PhotonNetwork.OnEventCall.
    ///
    /// Example:
    /// private void OnEventHandler(byte eventCode, object content, int senderId)
    /// { Debug.Log("OnEventHandler"); }
    ///
    /// PhotonNetwork.OnEventCall += this.OnEventHandler;
    ///
    /// With the senderId, you can look up the PhotonPlayer who sent the event.
    /// It is best practice to assign a eventCode for each different type of content and action. You have to cast the content.
    ///
    /// The eventContent is optional. To be able to send something, it must be a "serializable type", something that
    /// the client can turn into a byte[] basically. Most basic types and arrays of them are supported, including
    /// Unity's Vector2, Vector3, Quaternion. Transforms or classes some project defines are NOT supported!
    /// You can make your own class a "serializable type" by following the example in CustomTypes.cs.
    ///
    ///
    /// The RaiseEventOptions have some (less intuitive) combination rules:
    /// If you set targetActors (an array of PhotonPlayer.ID values), the receivers parameter gets ignored.
    /// When using event caching, the targetActors, receivers and interestGroup can't be used. Buffered events go to all.
    /// When using cachingOption removeFromRoomCache, the eventCode and content are actually not sent but used as filter.
    /// </remarks>
    /// <param name="eventCode">A byte identifying the type of event. You might want to use a code per action or to signal which content can be expected. Allowed: 0..199.</param>
    /// <param name="eventContent">Some serializable object like string, byte, integer, float (etc) and arrays of those. Hashtables with byte keys are good to send variable content.</param>
    /// <param name="sendReliable">Makes sure this event reaches all players. It gets acknowledged, which requires bandwidth and it can't be skipped (might add lag in case of loss).</param>
    /// <param name="options">Allows more complex usage of events. If null, RaiseEventOptions.Default will be used (which is fine).</param>
    /// <returns>False if event could not be sent</returns>
    public static bool RaiseEvent(byte eventCode, object eventContent, bool sendReliable, RaiseEventOptions options)
    {
        if (!inRoom || eventCode >= 200)
        {
            Debug.LogWarning("RaiseEvent() failed. Your event is not being sent! Check if your are in a Room and the eventCode must be less than 200 (0..199).");
            return false;
        }

        return networkingPeer.OpRaiseEvent(eventCode, eventContent, sendReliable, options);
    }

    #if PHOTON_LIB_MIN_4_1_2
    public static bool RaiseEvent(byte eventCode, object eventContent, RaiseEventOptions raiseEventOptions, SendOptions sendOptions)
    {
        if (!inRoom || eventCode >= 200)
        {
            Debug.LogWarning("RaiseEvent() failed. Your event is not being sent! Check if your are in a Room and the eventCode must be less than 200 (0..199).");
            return false;
        }

        return networkingPeer.OpRaiseEvent(eventCode, eventContent, raiseEventOptions, sendOptions);
    }
    #endif

    /// <summary>
    /// Allocates a viewID that's valid for the current/local player.
    /// </summary>
    /// <returns>A viewID that can be used for a new PhotonView.</returns>
    public static int AllocateViewID()
    {
        int manualId = AllocateViewID(player.ID);
        manuallyAllocatedViewIds.Add(manualId);
        return manualId;
    }


    /// <summary>
    /// Enables the Master Client to allocate a viewID that is valid for scene objects.
    /// </summary>
    /// <returns>A viewID that can be used for a new PhotonView or -1 in case of an error.</returns>
    public static int AllocateSceneViewID()
    {
        if (!PhotonNetwork.isMasterClient)
        {
            Debug.LogError("Only the Master Client can AllocateSceneViewID(). Check PhotonNetwork.isMasterClient!");
            return -1;
        }

        int manualId = AllocateViewID(0);
        manuallyAllocatedViewIds.Add(manualId);
        return manualId;
    }

    // use 0 for scene-targetPhotonView-ids
    // returns viewID (combined owner and sub id)
    private static int AllocateViewID(int ownerId)
    {
        if (ownerId == 0)
        {
            // we look up a fresh subId for the owner "room" (mind the "sub" in subId)
            int newSubId = lastUsedViewSubIdStatic;
            int newViewId;
            int ownerIdOffset = ownerId * MAX_VIEW_IDS;
            for (int i = 1; i < MAX_VIEW_IDS; i++)
            {
                newSubId = (newSubId + 1) % MAX_VIEW_IDS;
                if (newSubId == 0)
                {
                    continue;   // avoid using subID 0
                }

                newViewId = newSubId + ownerIdOffset;
                if (!networkingPeer.photonViewList.ContainsKey(newViewId))
                {
                    lastUsedViewSubIdStatic = newSubId;
                    return newViewId;
                }
            }

            // this is the error case: we didn't find any (!) free subId for this user
            throw new Exception(string.Format("AllocateViewID() failed. Room (user {0}) is out of 'scene' viewIDs. It seems all available are in use.", ownerId));
        }
        else
        {
            // we look up a fresh SUBid for the owner
            int newSubId = lastUsedViewSubId;
            int newViewId;
            int ownerIdOffset = ownerId * MAX_VIEW_IDS;
            for (int i = 1; i < MAX_VIEW_IDS; i++)
            {
                newSubId = (newSubId + 1) % MAX_VIEW_IDS;
                if (newSubId == 0)
                {
                    continue;   // avoid using subID 0
                }

                newViewId = newSubId + ownerIdOffset;
                if (!networkingPeer.photonViewList.ContainsKey(newViewId) && !manuallyAllocatedViewIds.Contains(newViewId))
                {
                    lastUsedViewSubId = newSubId;
                    return newViewId;
                }
            }

            throw new Exception(string.Format("AllocateViewID() failed. User {0} is out of subIds, as all viewIDs are used.", ownerId));
        }
    }

    private static int[] AllocateSceneViewIDs(int countOfNewViews)
    {
        int[] viewIDs = new int[countOfNewViews];
        for (int view = 0; view < countOfNewViews; view++)
        {
            viewIDs[view] = AllocateViewID(0);
        }

        return viewIDs;
    }

    /// <summary>
    /// Unregister a viewID (of manually instantiated and destroyed networked objects).
    /// </summary>
    /// <param name="viewID">A viewID manually allocated by this player.</param>
    public static void UnAllocateViewID(int viewID)
    {
        manuallyAllocatedViewIds.Remove(viewID);

        if (networkingPeer.photonViewList.ContainsKey(viewID))
        {
            Debug.LogWarning(string.Format("UnAllocateViewID() should be called after the PhotonView was destroyed (GameObject.Destroy()). ViewID: {0} still found in: {1}", viewID, networkingPeer.photonViewList[viewID]));
        }
    }

    /// <summary>
    /// Instantiate a prefab over the network. This prefab needs to be located in the root of a "Resources" folder.
    /// </summary>
    /// <remarks>
    /// Instead of using prefabs in the Resources folder, you can manually Instantiate and assign PhotonViews. See doc.
    /// </remarks>
    /// <param name="prefabName">Name of the prefab to instantiate.</param>
    /// <param name="position">Position Vector3 to apply on instantiation.</param>
    /// <param name="rotation">Rotation Quaternion to apply on instantiation.</param>
    /// <param name="group">The group for this PhotonView.</param>
    /// <returns>The new instance of a GameObject with initialized PhotonView.</returns>
    public static GameObject Instantiate(string prefabName, Vector3 position, Quaternion rotation, byte group)
    {
        return Instantiate(prefabName, position, rotation, group, null);
    }

    /// <summary>
    /// Instantiate a prefab over the network. This prefab needs to be located in the root of a "Resources" folder.
    /// </summary>
    /// <remarks>Instead of using prefabs in the Resources folder, you can manually Instantiate and assign PhotonViews. See doc.</remarks>
    /// <param name="prefabName">Name of the prefab to instantiate.</param>
    /// <param name="position">Position Vector3 to apply on instantiation.</param>
    /// <param name="rotation">Rotation Quaternion to apply on instantiation.</param>
    /// <param name="group">The group for this PhotonView.</param>
    /// <param name="data">Optional instantiation data. This will be saved to it's PhotonView.instantiationData.</param>
    /// <returns>The new instance of a GameObject with initialized PhotonView.</returns>
    public static GameObject Instantiate(string prefabName, Vector3 position, Quaternion rotation, byte group, object[] data)
    {
        if (!connected || (InstantiateInRoomOnly && !inRoom))
        {
            Debug.LogError("Failed to Instantiate prefab: " + prefabName + ". Client should be in a room. Current connectionStateDetailed: " + PhotonNetwork.connectionStateDetailed);
            return null;
        }

        GameObject prefabGo;
        if (!UsePrefabCache || !PrefabCache.TryGetValue(prefabName, out prefabGo))
        {
            prefabGo = (GameObject)Resources.Load(prefabName, typeof(GameObject));
            if (UsePrefabCache)
            {
                PrefabCache.Add(prefabName, prefabGo);
            }
        }

        if (prefabGo == null)
        {
            Debug.LogError("Failed to Instantiate prefab: " + prefabName + ". Verify the Prefab is in a Resources folder (and not in a subfolder)");
            return null;
        }

        // a scene object instantiated with network visibility has to contain a PhotonView
        if (prefabGo.GetComponent<PhotonView>() == null)
        {
            Debug.LogError("Failed to Instantiate prefab:" + prefabName + ". Prefab must have a PhotonView component.");
            return null;
        }

        Component[] views = (Component[])prefabGo.GetPhotonViewsInChildren();
        int[] viewIDs = new int[views.Length];
        for (int i = 0; i < viewIDs.Length; i++)
        {
            //Debug.Log("Instantiate prefabName: " + prefabName + " player.ID: " + player.ID);
            viewIDs[i] = AllocateViewID(player.ID);
        }

        // Send to others, create info
        Hashtable instantiateEvent = networkingPeer.SendInstantiate(prefabName, position, rotation, group, viewIDs, data, false);

        // Instantiate the GO locally (but the same way as if it was done via event). This will also cache the instantiationId
        return networkingPeer.DoInstantiate(instantiateEvent, networkingPeer.LocalPlayer, prefabGo);
    }


    /// <summary>
    /// Instantiate a scene-owned prefab over the network. The PhotonViews will be controllable by the MasterClient. This prefab needs to be located in the root of a "Resources" folder.
    /// </summary>
    /// <remarks>
    /// Only the master client can Instantiate scene objects.
    /// Instead of using prefabs in the Resources folder, you can manually Instantiate and assign PhotonViews. See doc.
    /// </remarks>
    /// <param name="prefabName">Name of the prefab to instantiate.</param>
    /// <param name="position">Position Vector3 to apply on instantiation.</param>
    /// <param name="rotation">Rotation Quaternion to apply on instantiation.</param>
    /// <param name="group">The group for this PhotonView.</param>
    /// <param name="data">Optional instantiation data. This will be saved to it's PhotonView.instantiationData.</param>
    /// <returns>The new instance of a GameObject with initialized PhotonView.</returns>
    public static GameObject InstantiateSceneObject(string prefabName, Vector3 position, Quaternion rotation, byte group, object[] data)
    {
        if (!connected || (InstantiateInRoomOnly && !inRoom))
        {
            Debug.LogError("Failed to InstantiateSceneObject prefab: " + prefabName + ". Client should be in a room. Current connectionStateDetailed: " + PhotonNetwork.connectionStateDetailed);
            return null;
        }

        if (!isMasterClient)
        {
            Debug.LogError("Failed to InstantiateSceneObject prefab: " + prefabName + ". Client is not the MasterClient in this room.");
            return null;
        }

        GameObject prefabGo;
        if (!UsePrefabCache || !PrefabCache.TryGetValue(prefabName, out prefabGo))
        {
            prefabGo = (GameObject)Resources.Load(prefabName, typeof(GameObject));
            if (UsePrefabCache)
            {
                PrefabCache.Add(prefabName, prefabGo);
            }
        }

        if (prefabGo == null)
        {
            Debug.LogError("Failed to InstantiateSceneObject prefab: " + prefabName + ". Verify the Prefab is in a Resources folder (and not in a subfolder)");
            return null;
        }

        // a scene object instantiated with network visibility has to contain a PhotonView
        if (prefabGo.GetComponent<PhotonView>() == null)
        {
            Debug.LogError("Failed to InstantiateSceneObject prefab:" + prefabName + ". Prefab must have a PhotonView component.");
            return null;
        }

        Component[] views = (Component[])prefabGo.GetPhotonViewsInChildren();
        int[] viewIDs = AllocateSceneViewIDs(views.Length);

        if (viewIDs == null)
        {
            Debug.LogError("Failed to InstantiateSceneObject prefab: " + prefabName + ". No ViewIDs are free to use. Max is: " + MAX_VIEW_IDS);
            return null;
        }

        // Send to others, create info
        Hashtable instantiateEvent = networkingPeer.SendInstantiate(prefabName, position, rotation, group, viewIDs, data, true);

        // Instantiate the GO locally (but the same way as if it was done via event). This will also cache the instantiationId
        return networkingPeer.DoInstantiate(instantiateEvent, networkingPeer.LocalPlayer, prefabGo);
    }

    /// <summary>
    /// The current roundtrip time to the photon server.
    /// </summary>
    /// <returns>Roundtrip time (to server and back).</returns>
    public static int GetPing()
    {
        return networkingPeer.RoundTripTime;
    }

    /// <summary>Refreshes the server timestamp (async operation, takes a roundtrip).</summary>
    /// <remarks>Can be useful if a bad connection made the timestamp unusable or imprecise.</remarks>
    public static void FetchServerTimestamp()
    {
        if (networkingPeer != null)
        {
            networkingPeer.FetchServerTimestamp();
        }
    }

    /// <summary>
    /// Can be used to immediately send the RPCs and Instantiates just called, so they are on their way to the other players.
    /// </summary>
    /// <remarks>
    /// This could be useful if you do a RPC to load a level and then load it yourself.
    /// While loading, no RPCs are sent to others, so this would delay the "load" RPC.
    /// You can send the RPC to "others", use this method, disable the message queue
    /// (by isMessageQueueRunning) and then load.
    /// </remarks>
    public static void SendOutgoingCommands()
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        while (networkingPeer.SendOutgoingCommands())
        {
        }
    }

    /// <summary>Request a client to disconnect (KICK). Only the master client can do this</summary>
    /// <remarks>Only the target player gets this event. That player will disconnect automatically, which is what the others will notice, too.</remarks>
    /// <param name="kickPlayer">The PhotonPlayer to kick.</param>
    public static bool CloseConnection(PhotonPlayer kickPlayer)
    {
        if (!VerifyCanUseNetwork())
        {
            return false;
        }

        if (!player.IsMasterClient)
        {
            Debug.LogError("CloseConnection: Only the masterclient can kick another player.");
            return false;
        }

        if (kickPlayer == null)
        {
            Debug.LogError("CloseConnection: No such player connected!");
            return false;
        }

        RaiseEventOptions options = new RaiseEventOptions() { TargetActors = new int[] { kickPlayer.ID } };
        return networkingPeer.OpRaiseEvent(PunEvent.CloseConnection, null, true, options);
    }

    /// <summary>
    /// Asks the server to assign another player as Master Client of your current room.
    /// </summary>
    /// <remarks>
    /// RPCs and RaiseEvent have the option to send messages only to the Master Client of a room.
    /// SetMasterClient affects which client gets those messages.
    ///
    /// This method calls an operation on the server to set a new Master Client, which takes a roundtrip.
    /// In case of success, this client and the others get the new Master Client from the server.
    ///
    /// SetMasterClient tells the server which current Master Client should be replaced with the new one.
    /// It will fail, if anything switches the Master Client moments earlier. There is no callback for this
    /// error. All clients should get the new Master Client assigned by the server anyways.
    ///
    /// See also: PhotonNetwork.masterClient
    ///
    /// On v3 servers:
    /// The ReceiverGroup.MasterClient (usable in RPCs) is not affected by this (still points to lowest player.ID in room).
    /// Avoid using this enum value (and send to a specific player instead).
    ///
    /// If the current Master Client leaves, PUN will detect a new one by "lowest player ID". Implement OnMasterClientSwitched
    /// to get a callback in this case. The PUN-selected Master Client might assign a new one.
    ///
    /// Make sure you don't create an endless loop of Master-assigning! When selecting a custom Master Client, all clients
    /// should point to the same player, no matter who actually assigns this player.
    ///
    /// Locally the Master Client is immediately switched, while remote clients get an event. This means the game
    /// is tempoarily without Master Client like when a current Master Client leaves.
    ///
    /// When switching the Master Client manually, keep in mind that this user might leave and not do it's work, just like
    /// any Master Client.
    ///
    /// </remarks>
    /// <param name="masterClientPlayer">The player to become the next Master Client.</param>
    /// <returns>False when this operation couldn't be done. Must be in a room (not in offlineMode).</returns>
    public static bool SetMasterClient(PhotonPlayer masterClientPlayer)
    {
        if (!inRoom || !VerifyCanUseNetwork() || offlineMode)
        {
            if (logLevel == PhotonLogLevel.Informational) Debug.Log("Can not SetMasterClient(). Not in room or in offlineMode.");
            return false;
        }

        if (room.serverSideMasterClient)
        {
            Hashtable newProps = new Hashtable() { { GamePropertyKey.MasterClientId, masterClientPlayer.ID } };
            Hashtable prevProps = new Hashtable() { { GamePropertyKey.MasterClientId, networkingPeer.mMasterClientId } };
            return networkingPeer.OpSetPropertiesOfRoom(newProps, expectedProperties: prevProps, webForward: false);
        }
        else
        {
            if (!isMasterClient)
            {
                return false;
            }
            return networkingPeer.SetMasterClient(masterClientPlayer.ID, true);
        }
    }

    /// <summary>
    /// Network-Destroy the GameObject associated with the PhotonView, unless the PhotonView is static or not under this client's control.
    /// </summary>
    /// <remarks>
    /// Destroying a networked GameObject while in a Room includes:
    /// - Removal of the Instantiate call from the server's room buffer.
    /// - Removing RPCs buffered for PhotonViews that got created indirectly with the PhotonNetwork.Instantiate call.
    /// - Sending a message to other clients to remove the GameObject also (affected by network lag).
    ///
    /// Usually, when you leave a room, the GOs get destroyed automatically.
    /// If you have to destroy a GO while not in a room, the Destroy is only done locally.
    ///
    /// Destroying networked objects works only if they got created with PhotonNetwork.Instantiate().
    /// Objects loaded with a scene are ignored, no matter if they have PhotonView components.
    ///
    /// The GameObject must be under this client's control:
    /// - Instantiated and owned by this client.
    /// - Instantiated objects of players who left the room are controlled by the Master Client.
    /// - Scene-owned game objects are controlled by the Master Client.
    /// - GameObject can be destroyed while client is not in a room.
    /// </remarks>
    /// <returns>Nothing. Check error debug log for any issues.</returns>
    public static void Destroy(PhotonView targetView)
    {
        if (targetView != null)
        {
            networkingPeer.RemoveInstantiatedGO(targetView.gameObject, !inRoom);
        }
        else
        {
            Debug.LogError("Destroy(targetPhotonView) failed, cause targetPhotonView is null.");
        }
    }

    /// <summary>
    /// Network-Destroy the GameObject, unless it is static or not under this client's control.
    /// </summary>
    /// <remarks>
    /// Destroying a networked GameObject includes:
    /// - Removal of the Instantiate call from the server's room buffer.
    /// - Removing RPCs buffered for PhotonViews that got created indirectly with the PhotonNetwork.Instantiate call.
    /// - Sending a message to other clients to remove the GameObject also (affected by network lag).
    ///
    /// Usually, when you leave a room, the GOs get destroyed automatically.
    /// If you have to destroy a GO while not in a room, the Destroy is only done locally.
    ///
    /// Destroying networked objects works only if they got created with PhotonNetwork.Instantiate().
    /// Objects loaded with a scene are ignored, no matter if they have PhotonView components.
    ///
    /// The GameObject must be under this client's control:
    /// - Instantiated and owned by this client.
    /// - Instantiated objects of players who left the room are controlled by the Master Client.
    /// - Scene-owned game objects are controlled by the Master Client.
    /// - GameObject can be destroyed while client is not in a room.
    /// </remarks>
    /// <returns>Nothing. Check error debug log for any issues.</returns>
    public static void Destroy(GameObject targetGo)
    {
        networkingPeer.RemoveInstantiatedGO(targetGo, !inRoom);
    }

    /// <summary>
    /// Network-Destroy all GameObjects, PhotonViews and their RPCs of targetPlayer. Can only be called on local player (for "self") or Master Client (for anyone).
    /// </summary>
    /// <remarks>
    /// Destroying a networked GameObject includes:
    /// - Removal of the Instantiate call from the server's room buffer.
    /// - Removing RPCs buffered for PhotonViews that got created indirectly with the PhotonNetwork.Instantiate call.
    /// - Sending a message to other clients to remove the GameObject also (affected by network lag).
    ///
    /// Destroying networked objects works only if they got created with PhotonNetwork.Instantiate().
    /// Objects loaded with a scene are ignored, no matter if they have PhotonView components.
    /// </remarks>
    /// <returns>Nothing. Check error debug log for any issues.</returns>
    public static void DestroyPlayerObjects(PhotonPlayer targetPlayer)
    {
        if (player == null)
        {
            Debug.LogError("DestroyPlayerObjects() failed, cause parameter 'targetPlayer' was null.");
        }

        DestroyPlayerObjects(targetPlayer.ID);
    }

    /// <summary>
    /// Network-Destroy all GameObjects, PhotonViews and their RPCs of this player (by ID). Can only be called on local player (for "self") or Master Client (for anyone).
    /// </summary>
    /// <remarks>
    /// Destroying a networked GameObject includes:
    /// - Removal of the Instantiate call from the server's room buffer.
    /// - Removing RPCs buffered for PhotonViews that got created indirectly with the PhotonNetwork.Instantiate call.
    /// - Sending a message to other clients to remove the GameObject also (affected by network lag).
    ///
    /// Destroying networked objects works only if they got created with PhotonNetwork.Instantiate().
    /// Objects loaded with a scene are ignored, no matter if they have PhotonView components.
    /// </remarks>
    /// <returns>Nothing. Check error debug log for any issues.</returns>
    public static void DestroyPlayerObjects(int targetPlayerId)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }
        if (player.IsMasterClient || targetPlayerId == player.ID)
        {
            networkingPeer.DestroyPlayerObjects(targetPlayerId, false);
        }
        else
        {
            Debug.LogError("DestroyPlayerObjects() failed, cause players can only destroy their own GameObjects. A Master Client can destroy anyone's. This is master: " + PhotonNetwork.isMasterClient);
        }
    }

    /// <summary>
    /// Network-Destroy all GameObjects, PhotonViews and their RPCs in the room. Removes anything buffered from the server. Can only be called by Master Client (for anyone).
    /// </summary>
    /// <remarks>
    /// Can only be called by Master Client (for anyone).
    /// Unlike the Destroy methods, this will remove anything from the server's room buffer. If your game
    /// buffers anything beyond Instantiate and RPC calls, that will be cleaned as well from server.
    ///
    /// Destroying all includes:
    /// - Remove anything from the server's room buffer (Instantiate, RPCs, anything buffered).
    /// - Sending a message to other clients to destroy everything locally, too (affected by network lag).
    ///
    /// Destroying networked objects works only if they got created with PhotonNetwork.Instantiate().
    /// Objects loaded with a scene are ignored, no matter if they have PhotonView components.
    /// </remarks>
    /// <returns>Nothing. Check error debug log for any issues.</returns>
    public static void DestroyAll()
    {
        if (isMasterClient)
        {
            networkingPeer.DestroyAll(false);
        }
        else
        {
            Debug.LogError("Couldn't call DestroyAll() as only the master client is allowed to call this.");
        }
    }

    /// <summary>
    /// Remove all buffered RPCs from server that were sent by targetPlayer. Can only be called on local player (for "self") or Master Client (for anyone).
    /// </summary>
    /// <remarks>
    /// This method requires either:
    /// - This is the targetPlayer's client.
    /// - This client is the Master Client (can remove any PhotonPlayer's RPCs).
    ///
    /// If the targetPlayer calls RPCs at the same time that this is called,
    /// network lag will determine if those get buffered or cleared like the rest.
    /// </remarks>
    /// <param name="targetPlayer">This player's buffered RPCs get removed from server buffer.</param>
    public static void RemoveRPCs(PhotonPlayer targetPlayer)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        if (!targetPlayer.IsLocal && !isMasterClient)
        {
            Debug.LogError("Error; Only the MasterClient can call RemoveRPCs for other players.");
            return;
        }

        networkingPeer.OpCleanRpcBuffer(targetPlayer.ID);
    }

    /// <summary>
    /// Remove all buffered RPCs from server that were sent via targetPhotonView. The Master Client and the owner of the targetPhotonView may call this.
    /// </summary>
    /// <remarks>
    /// This method requires either:
    /// - The targetPhotonView is owned by this client (Instantiated by it).
    /// - This client is the Master Client (can remove any PhotonView's RPCs).
    /// </remarks>
    /// <param name="targetPhotonView">RPCs buffered for this PhotonView get removed from server buffer.</param>
    public static void RemoveRPCs(PhotonView targetPhotonView)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        networkingPeer.CleanRpcBufferIfMine(targetPhotonView);
    }

    /// <summary>
    /// Remove all buffered RPCs from server that were sent in the targetGroup, if this is the Master Client or if this controls the individual PhotonView.
    /// </summary>
    /// <remarks>
    /// This method requires either:
    /// - This client is the Master Client (can remove any RPCs per group).
    /// - Any other client: each PhotonView is checked if it is under this client's control. Only those RPCs are removed.
    /// </remarks>
    /// <param name="targetGroup">Interest group that gets all RPCs removed.</param>
    public static void RemoveRPCsInGroup(int targetGroup)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        networkingPeer.RemoveRPCsInGroup(targetGroup);
    }

    /// <summary>
    /// Internal to send an RPC on given PhotonView. Do not call this directly but use: PhotonView.RPC!
    /// </summary>
    internal static void RPC(PhotonView view, string methodName, PhotonTargets target, bool encrypt, params object[] parameters)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        if (room == null)
        {
            Debug.LogWarning("RPCs can only be sent in rooms. Call of \"" + methodName + "\" gets executed locally only, if at all.");
            return;
        }

        if (networkingPeer != null)
        {
            if (PhotonNetwork.room.serverSideMasterClient)
            {
                networkingPeer.RPC(view, methodName, target, null, encrypt, parameters);
            }
            else
            {
                if (PhotonNetwork.networkingPeer.hasSwitchedMC && target == PhotonTargets.MasterClient)
                {
                    networkingPeer.RPC(view, methodName, PhotonTargets.Others, PhotonNetwork.masterClient, encrypt, parameters);
                }
                else
                {
                    networkingPeer.RPC(view, methodName, target, null, encrypt, parameters);
                }
            }
        }
        else
        {
            Debug.LogWarning("Could not execute RPC " + methodName + ". Possible scene loading in progress?");
        }
    }

    /// <summary>
    /// Internal to send an RPC on given PhotonView. Do not call this directly but use: PhotonView.RPC!
    /// </summary>
    internal static void RPC(PhotonView view, string methodName, PhotonPlayer targetPlayer, bool encrpyt, params object[] parameters)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        if (room == null)
        {
            Debug.LogWarning("RPCs can only be sent in rooms. Call of \"" + methodName + "\" gets executed locally only, if at all.");
            return;
        }

        if (player == null)
        {
            Debug.LogError("RPC can't be sent to target PhotonPlayer being null! Did not send \"" + methodName + "\" call.");
        }

        if (networkingPeer != null)
        {
            networkingPeer.RPC(view, methodName, PhotonTargets.Others, targetPlayer, encrpyt, parameters);
        }
        else
        {
            Debug.LogWarning("Could not execute RPC " + methodName + ". Possible scene loading in progress?");
        }
    }

    /// <summary>
    /// Populates SendMonoMessageTargets with currently existing GameObjects that have a Component of type.
    /// </summary>
    /// <param name="type">If null, this will use SendMonoMessageTargets as component-type (MonoBehaviour by default).</param>
    public static void CacheSendMonoMessageTargets(Type type)
    {
        if (type == null) type = SendMonoMessageTargetType;
        PhotonNetwork.SendMonoMessageTargets = FindGameObjectsWithComponent(type);
    }

    /// <summary>Finds the GameObjects with Components of a specific type (using FindObjectsOfType).</summary>
    /// <param name="type">Type must be a Component</param>
    /// <returns>HashSet with GameObjects that have a specific type of Component.</returns>
    public static HashSet<GameObject> FindGameObjectsWithComponent(Type type)
    {
        HashSet<GameObject> objectsWithComponent = new HashSet<GameObject>();

        Component[] targetComponents = (Component[]) GameObject.FindObjectsOfType(type);
        for (int index = 0; index < targetComponents.Length; index++)
        {
            if (targetComponents[index] != null)
            {
                objectsWithComponent.Add(targetComponents[index].gameObject);
            }
        }

        return objectsWithComponent;
    }



    [Obsolete("Use SetInterestGroups(byte group, bool enabled) instead.")]
    public static void SetReceivingEnabled(int group, bool enabled)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        SetInterestGroups((byte)group, enabled);
    }

    /// <summary>Enable/disable receiving events from a given Interest Group.</summary>
    /// <remarks>
    /// A client can tell the server which Interest Groups it's interested in.
    /// The server will only forward events for those Interest Groups to that client (saving bandwidth and performance).
    ///
    /// See: https://doc.photonengine.com/en-us/pun/current/gameplay/interestgroups
    ///
    /// See: https://doc.photonengine.com/en-us/pun/current/demos-and-tutorials/package-demos/culling-demo
    /// </remarks>
    /// <param name="group">The interest group to affect.</param>
    /// <param name="enabled">Sets if receiving from group to enabled (or not).</param>
    public static void SetInterestGroups(byte group, bool enabled)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        if (enabled)
        {
            byte[] groups = new byte[1] { (byte)group };
            networkingPeer.SetInterestGroups(null, groups);
        }
        else
        {
            byte[] groups = new byte[1] { (byte)group };
            networkingPeer.SetInterestGroups(groups, null);
        }
    }


    [Obsolete("Use SetInterestGroups(byte[] disableGroups, byte[] enableGroups) instead. Mind the parameter order!")]
    public static void SetReceivingEnabled(int[] enableGroups, int[] disableGroups)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        byte[] disableByteGroups = null;
        byte[] enableByteGroups = null;

        if (enableGroups != null)
        {
            enableByteGroups = new byte[enableGroups.Length];
            Array.Copy(enableGroups, enableByteGroups, enableGroups.Length);
        }
        if (disableGroups != null)
        {
            disableByteGroups = new byte[disableGroups.Length];
            Array.Copy(disableGroups, disableByteGroups, disableGroups.Length);
        }

        networkingPeer.SetInterestGroups(disableByteGroups, enableByteGroups);
    }

    /// <summary>Enable/disable receiving on given Interest Groups (applied to PhotonViews).</summary>
    /// <remarks>
    /// A client can tell the server which Interest Groups it's interested in.
    /// The server will only forward events for those Interest Groups to that client (saving bandwidth and performance).
    ///
    /// See: https://doc.photonengine.com/en-us/pun/current/gameplay/interestgroups
    ///
    /// See: https://doc.photonengine.com/en-us/pun/current/demos-and-tutorials/package-demos/culling-demo
    /// </remarks>
    /// <param name="disableGroups">The interest groups to disable (or null).</param>
    /// <param name="enableGroups">The interest groups to enable (or null).</param>
    public static void SetInterestGroups(byte[] disableGroups, byte[] enableGroups)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }
        networkingPeer.SetInterestGroups(disableGroups, enableGroups);
    }



    [Obsolete("Use SetSendingEnabled(byte group, bool enabled). Interest Groups have a byte-typed ID. Mind the parameter order.")]
    public static void SetSendingEnabled(int group, bool enabled)
    {
        SetSendingEnabled((byte)group, enabled);
    }

    /// <summary>Enable/disable sending on given group (applied to PhotonViews)</summary>
    /// <remarks>
    /// This does not interact with the Photon server-side.
    /// It's just a client-side setting to suppress updates, should they be sent to one of the blocked groups.
    ///
    /// This setting is not particularly useful, as it means that updates literally never reach the server or anyone else.
    /// Use with care.
    /// </remarks>
    /// <param name="group">The interest group to affect.</param>
    /// <param name="enabled">Sets if sending to group is enabled (or not).</param>
    public static void SetSendingEnabled(byte group, bool enabled)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        networkingPeer.SetSendingEnabled(group, enabled);
    }


    [Obsolete("Use SetSendingEnabled(byte group, bool enabled). Interest Groups have a byte-typed ID. Mind the parameter order.")]
    public static void SetSendingEnabled(int[] enableGroups, int[] disableGroups)
    {
        byte[] disableByteGroups = null;
        byte[] enableByteGroups = null;

        if (enableGroups != null)
        {
            enableByteGroups = new byte[enableGroups.Length];
            Array.Copy(enableGroups, enableByteGroups, enableGroups.Length);
        }
        if (disableGroups != null)
        {
            disableByteGroups = new byte[disableGroups.Length];
            Array.Copy(disableGroups, disableByteGroups, disableGroups.Length);
        }

        SetSendingEnabled(disableByteGroups, enableByteGroups);
    }

    /// <summary>Enable/disable sending on given groups (applied to PhotonViews)</summary>
    /// <remarks>
    /// This does not interact with the Photon server-side.
    /// It's just a client-side setting to suppress updates, should they be sent to one of the blocked groups.
    ///
    /// This setting is not particularly useful, as it means that updates literally never reach the server or anyone else.
    /// Use with care.
    /// <param name="enableGroups">The interest groups to enable sending on (or null).</param>
    /// <param name="disableGroups">The interest groups to disable sending on (or null).</param>
    public static void SetSendingEnabled(byte[] disableGroups, byte[] enableGroups)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        networkingPeer.SetSendingEnabled(disableGroups, enableGroups);
    }



    /// <summary>
    /// Sets level prefix for PhotonViews instantiated later on. Don't set it if you need only one!
    /// </summary>
    /// <remarks>
    /// Important: If you don't use multiple level prefixes, simply don't set this value. The
    /// default value is optimized out of the traffic.
    ///
    /// This won't affect existing PhotonViews (they can't be changed yet for existing PhotonViews).
    ///
    /// Messages sent with a different level prefix will be received but not executed. This affects
    /// RPCs, Instantiates and synchronization.
    ///
    /// Be aware that PUN never resets this value, you'll have to do so yourself.
    /// </remarks>
    /// <param name="prefix">Max value is short.MaxValue = 32767</param>
    public static void SetLevelPrefix(short prefix)
    {
        if (!VerifyCanUseNetwork())
        {
            return;
        }

        networkingPeer.SetLevelPrefix(prefix);
    }


    /// <summary>Wraps loading a level to pause the network message-queue. Optionally syncs the loaded level in a room.</summary>
    /// <remarks>
    /// To sync the loaded level in a room, set PhotonNetwork.automaticallySyncScene to true.
    /// The Master Client of a room will then sync the loaded level with every other player in the room.
    ///
    /// While loading levels, it makes sense to not dispatch messages received by other players.
    /// This method takes care of that by setting PhotonNetwork.isMessageQueueRunning = false and enabling
    /// the queue when the level was loaded.
    ///
    /// You should make sure you don't fire RPCs before you load another scene (which doesn't contain
    /// the same GameObjects and PhotonViews). You can call this in OnJoinedRoom.
    ///
    /// This uses Application.LoadLevel in Unity version not yet featuring the SceneManager API.
    /// </remarks>
    /// <param name='levelNumber'>
    /// Number of the level to load. When using level numbers, make sure they are identical on all clients.
    /// </param>
    public static void LoadLevel(int levelNumber)
    {
        networkingPeer.AsynchLevelLoadCall = false;

		if (PhotonNetwork.automaticallySyncScene) {
			networkingPeer.SetLevelInPropsIfSynced (levelNumber,true);
		}

        PhotonNetwork.isMessageQueueRunning = false;
        networkingPeer.loadingLevelAndPausedNetwork = true;
        SceneManager.LoadScene(levelNumber);
    }


	/// <summary>Wraps single asynchronous loading of a level to pause the network message-queue. Optionally syncs the loaded level in a room.</summary>
	/// <remarks>
	/// To sync the loaded level in a room, set PhotonNetwork.automaticallySyncScene to true.
	/// The Master Client of a room will then sync the loaded level with every other player in the room.
	///
	/// While loading levels, it makes sense to not dispatch messages received by other players.
	/// This method takes care of that by setting PhotonNetwork.isMessageQueueRunning = false and enabling
	/// the queue when the level was loaded.
	///
	/// You should make sure you don't fire RPCs before you load another scene (which doesn't contain
	/// the same GameObjects and PhotonViews). You can call this in OnJoinedRoom.
	///
	/// This uses Application.LoadLevel in Unity version not yet featuring the SceneManager API.
	/// </remarks>
	/// <returns>The async operation.</returns>
	/// <param name='levelNumber'>
	/// Number of the level to load. When using level numbers, make sure they are identical on all clients.
	/// </param>
	public static AsyncOperation LoadLevelAsync(int levelNumber)
	{
        networkingPeer.AsynchLevelLoadCall = true;

		if (PhotonNetwork.automaticallySyncScene) {
			networkingPeer.SetLevelInPropsIfSynced (levelNumber,true);
		}

		PhotonNetwork.isMessageQueueRunning = false;
		networkingPeer.loadingLevelAndPausedNetwork = true;
		return SceneManager.LoadSceneAsync(levelNumber,LoadSceneMode.Single);
	}


    /// <summary>Wraps loading a level to pause the network message-queue. Optionally syncs the loaded level in a room.</summary>
    /// <remarks>
    /// While loading levels, it makes sense to not dispatch messages received by other players.
    /// This method takes care of that by setting PhotonNetwork.isMessageQueueRunning = false and enabling
    /// the queue when the level was loaded.
    ///
    /// To sync the loaded level in a room, set PhotonNetwork.automaticallySyncScene to true.
    /// The Master Client of a room will then sync the loaded level with every other player in the room.
    ///
    /// You should make sure you don't fire RPCs before you load another scene (which doesn't contain
    /// the same GameObjects and PhotonViews). You can call this in OnJoinedRoom.
    ///
	/// This uses Application.LoadLevel in Unity version not yet featuring the SceneManager API.
    /// </remarks>
    /// <param name='levelName'>
    /// Name of the level to load. Make sure it's available to all clients in the same room.
    /// </param>
    public static void LoadLevel(string levelName)
    {
        networkingPeer.AsynchLevelLoadCall = false;

		if (PhotonNetwork.automaticallySyncScene) {
			networkingPeer.SetLevelInPropsIfSynced (levelName,true);
		}

        PhotonNetwork.isMessageQueueRunning = false;
        networkingPeer.loadingLevelAndPausedNetwork = true;
        SceneManager.LoadScene(levelName);
    }


	/// <summary>Wraps single asynchronous loading of a level to pause the network message-queue. Optionally syncs the loaded level in a room.</summary>
	/// <remarks>
	/// While loading levels, it makes sense to not dispatch messages received by other players.
	/// This method takes care of that by setting PhotonNetwork.isMessageQueueRunning = false and enabling
	/// the queue when the level was loaded.
	///
	/// To sync the loaded level in a room, set PhotonNetwork.automaticallySyncScene to true.
	/// The Master Client of a room will then sync the loaded level with every other player in the room.
	///
	/// You should make sure you don't fire RPCs before you load another scene (which doesn't contain
	/// the same GameObjects and PhotonViews). You can call this in OnJoinedRoom.
	///
	/// This uses Application.LoadLevel in Unity version not yet featuring the SceneManager API.
	/// </remarks>
	/// <returns>The async operation.</returns>
	/// <param name='levelName'>
	/// Name of the level to load. Make sure it's available to all clients in the same room.
	/// </param>
	/// <param name="mode">LoadSceneMode either single or additive</param>
	public static AsyncOperation LoadLevelAsync(string levelName)
	{
        networkingPeer.AsynchLevelLoadCall = true;

		if (PhotonNetwork.automaticallySyncScene) {
			networkingPeer.SetLevelInPropsIfSynced (levelName,true);
		}

		PhotonNetwork.isMessageQueueRunning = false;
		networkingPeer.loadingLevelAndPausedNetwork = true;
		return SceneManager.LoadSceneAsync(levelName,LoadSceneMode.Single);
	}


    /// <summary>
    /// This operation makes Photon call your custom web-service by name (path) with the given parameters.
    /// </summary>
    /// <remarks>
    /// This is a server-side feature which must be setup in the Photon Cloud Dashboard prior to use.
    /// <see cref="https://doc.photonengine.com/en-us/pun/current/gameplay/web-extensions/webrpc"/>
    /// The Parameters will be converted into JSon format, so make sure your parameters are compatible.
    ///
    /// See PhotonNetworkingMessage.OnWebRpcResponse on how to get a response.
    ///
    /// It's important to understand that the OperationResponse only tells if the WebRPC could be called.
    /// The content of the response contains any values your web-service sent and the error/success code.
    /// In case the web-service failed, an error code and a debug message are usually inside the
    /// OperationResponse.
    ///
    /// The class WebRpcResponse is a helper-class that extracts the most valuable content from the WebRPC
    /// response.
    /// </remarks>
    /// <example>
    /// Example callback implementation:<pre>
    ///
    /// public void OnWebRpcResponse(OperationResponse response)
    /// {
    ///     WebRpcResponse webResponse = new WebRpcResponse(operationResponse);
    ///     if (webResponse.ReturnCode != 0) { //...
    ///     }
    ///
    ///     switch (webResponse.Name) { //...
    ///     }
    ///     // and so on
    /// }</pre>
    /// </example>
    public static bool WebRpc(string name, object parameters)
    {
        return networkingPeer.WebRpc(name, parameters);
    }

	public static bool CallEvent(byte eventCode, object content, int senderId)
	{
		if (PhotonNetwork.OnEventCall != null)
		{
			PhotonNetwork.OnEventCall(eventCode, content, senderId);
			return true;
		}

		return false;
	}


#if UNITY_EDITOR


	/// <summary>
	/// Finds the asset path base on its name or search query: https://docs.unity3d.com/ScriptReference/AssetDatabase.FindAssets.html
	/// </summary>
	/// <returns>The asset path.</returns>
	/// <param name="asset">Asset.</param>
	public static string FindAssetPath(string asset)
	{
		string[] guids = AssetDatabase.FindAssets (asset, null);
		if (guids.Length != 1)
		{
			return string.Empty;
		} else
		{
			return AssetDatabase.GUIDToAssetPath (guids [0]);
		}
	}


	/// <summary>
	/// Finds the pun asset folder. Something like Assets/Photon Unity Networking/Resources/
	/// </summary>
	/// <returns>The pun asset folder.</returns>
	public static string FindPunAssetFolder()
	{
		string _thisPath =	FindAssetPath("PhotonClasses");
		string _PunFolderPath = string.Empty;

		_PunFolderPath = GetParent(_thisPath,"Photon Unity Networking");

		if (_PunFolderPath != null)
		{
			return "Assets" + _PunFolderPath.Substring(Application.dataPath.Length)+"/";
		}

		return "Assets/Photon Unity Networking/";
	}

	/// <summary>
	/// Gets the parent directory of a path. Recursive Function, will return null if parentName not found
	/// </summary>
	/// <returns>The parent directory</returns>
	/// <param name="path">Path.</param>
	/// <param name="parentName">Parent name.</param>
	public static string GetParent(string path, string parentName)
	{
		var dir = new DirectoryInfo(path);

		if (dir.Parent == null)
		{
			return null;
		}

		if (string.IsNullOrEmpty(parentName))
		{
			return  dir.Parent.FullName;
		}

		if (dir.Parent.Name == parentName)
		{
			return dir.Parent.FullName;
		}

		return GetParent(dir.Parent.FullName, parentName);
	}


    [Conditional("UNITY_EDITOR")]
    public static void CreateSettings()
    {
        PhotonNetwork.PhotonServerSettings = (ServerSettings)Resources.Load(PhotonNetwork.serverSettingsAssetFile, typeof(ServerSettings));
        if (PhotonNetwork.PhotonServerSettings != null)
        {
            return;
        }

        // find out if ServerSettings can be instantiated (existing script check)
        ScriptableObject serverSettingTest = ScriptableObject.CreateInstance("ServerSettings");
        if (serverSettingTest == null)
        {
            Debug.LogError("missing settings script");
            return;
        }
        UnityEngine.Object.DestroyImmediate(serverSettingTest);


        // if still not loaded, create one
        if (PhotonNetwork.PhotonServerSettings == null)
        {
			string _PunResourcesPath = PhotonNetwork.FindPunAssetFolder();

			_PunResourcesPath += "Resources/";


			string serverSettingsAssetPath = _PunResourcesPath+ PhotonNetwork.serverSettingsAssetFile + ".asset";
			string settingsPath = Path.GetDirectoryName(serverSettingsAssetPath);
            if (!Directory.Exists(settingsPath))
            {
                Directory.CreateDirectory(settingsPath);
                AssetDatabase.ImportAsset(settingsPath);
            }

            PhotonNetwork.PhotonServerSettings = (ServerSettings)ScriptableObject.CreateInstance("ServerSettings");
            if (PhotonNetwork.PhotonServerSettings != null)
            {
				AssetDatabase.CreateAsset(PhotonNetwork.PhotonServerSettings, serverSettingsAssetPath);
            }
            else
            {
                Debug.LogError("PUN failed creating a settings file. ScriptableObject.CreateInstance(\"ServerSettings\") returned null. Will try again later.");
            }
        }
    }


    /// <summary>
    /// Internally used by Editor scripts, called on Hierarchy change (includes scene save) to remove surplus hidden PhotonHandlers.
    /// </summary>
    public static void InternalCleanPhotonMonoFromSceneIfStuck()
    {
        PhotonHandler[] photonHandlers = GameObject.FindObjectsOfType(typeof(PhotonHandler)) as PhotonHandler[];
        if (photonHandlers != null && photonHandlers.Length > 0)
        {
            Debug.Log("Cleaning up hidden PhotonHandler instances in scene. Please save it. This is not an issue.");
            foreach (PhotonHandler photonHandler in photonHandlers)
            {
                // Debug.Log("Removing Handler: " + photonHandler + " photonHandler.gameObject: " + photonHandler.gameObject);
                photonHandler.gameObject.hideFlags = 0;

                if (photonHandler.gameObject != null && photonHandler.gameObject.name == "PhotonMono")
                {
                    GameObject.DestroyImmediate(photonHandler.gameObject);
                }

                Component.DestroyImmediate(photonHandler);
            }
        }
    }
#endif

}
