// ----------------------------------------------------------------------- // // Loadbalancing Framework for Photon - Copyright (C) 2011 Exit Games GmbH // // // Provides the operations and a state for games using the // Photon LoadBalancing server. // // developer@photonengine.com // ---------------------------------------------------------------------------- #if UNITY_4_7_OR_NEWER #define UNITY #endif namespace ExitGames.Client.Photon.LoadBalancing { using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; #if UNITY using UnityEngine; using Debug = UnityEngine.Debug; #endif #if UNITY || NETFX_CORE using Hashtable = ExitGames.Client.Photon.Hashtable; using SupportClass = ExitGames.Client.Photon.SupportClass; #endif #region Enums /// /// State values for a client, which handles switching Photon server types, some operations, etc. /// /// \ingroup publicApi public enum ClientState { /// Peer is created but not used yet. PeerCreated, /// Transition state while connecting to a server. On the Photon Cloud this sends the AppId and AuthenticationValues (UserID). Authenticating, /// Transition state while connecting to a server. Leads to state ConnectedToMasterserver or JoinedLobby. Authenticated, /// The client is in a lobby, connected to the MasterServer. Depending on the lobby, it gets room listings. JoinedLobby, /// Transition from MasterServer to GameServer. DisconnectingFromMasterserver, /// Transition to GameServer (client authenticates and joins/creates a room). ConnectingToGameserver, /// Connected to GameServer (going to auth and join game). ConnectedToGameserver, /// Transition state while joining or creating a room on GameServer. Joining, /// The client entered a room. The CurrentRoom and Players are known and you can now raise events. Joined, /// Transition state when leaving a room. Leaving, /// Transition from GameServer to MasterServer (after leaving a room/game). DisconnectingFromGameserver, /// Connecting to MasterServer (includes sending authentication values). ConnectingToMasterserver, /// The client disconnects (from any server). This leads to state Disconnected. Disconnecting, /// The client is no longer connected (to any server). Connect to MasterServer to go on. Disconnected, /// Connected to MasterServer. You might use matchmaking or join a lobby now. ConnectedToMasterserver, /// Connected to MasterServer. You might use matchmaking or join a lobby now. [Obsolete("Renamed to ConnectedToMasterserver.")] ConnectedToMaster = ConnectedToMasterserver, /// Client connects to the NameServer. This process includes low level connecting and setting up encryption. When done, state becomes ConnectedToNameServer. ConnectingToNameServer, /// Client is connected to the NameServer and established enctryption already. You should call OpGetRegions or ConnectToRegionMaster. ConnectedToNameServer, /// Clients disconnects (specifically) from the NameServer (usually to connect to the MasterServer). DisconnectingFromNameServer } /// /// Internal state, how this peer gets into a particular room (joining it or creating it). /// internal enum JoinType { /// This client creates a room, gets into it (no need to join) and can set room properties. CreateRoom, /// The room existed already and we join into it (not setting room properties). JoinRoom, /// Done on Master Server and (if successful) followed by a Join on Game Server. JoinRandomRoom, /// Client is either joining or creating a room. On Master- and Game-Server. JoinOrCreateRoom } /// Enumaration of causes for Disconnects (used in LoadBalancingClient.DisconnectedCause). /// Read the individual descriptions to find out what to do about this type of disconnect. public enum DisconnectCause { /// No error was tracked. None, /// OnStatusChanged: The CCUs count of your Photon Server License is exausted (temporarily). DisconnectByServerUserLimit, /// OnStatusChanged: The server is not available or the address is wrong. Make sure the port is provided and the server is up. ExceptionOnConnect, /// OnStatusChanged: The server disconnected this client. Most likely the server's send buffer is full (receiving too much from other clients). DisconnectByServer, /// OnStatusChanged: This client detected that the server's responses are not received in due time. Maybe you send / receive too much? TimeoutDisconnect, /// OnStatusChanged: Some internal exception caused the socket code to fail. Contact Exit Games. Exception, /// OnOperationResponse: Authenticate in the Photon Cloud with invalid AppId. Update your subscription or contact Exit Games. InvalidAuthentication, /// OnOperationResponse: Authenticate (temporarily) failed when using a Photon Cloud subscription without CCU Burst. Update your subscription. MaxCcuReached, /// OnOperationResponse: Authenticate when the app's Photon Cloud subscription is locked to some (other) region(s). Update your subscription or master server address. InvalidRegion, /// OnOperationResponse: Operation that's (currently) not available for this client (not authorized usually). Only tracked for op Authenticate. OperationNotAllowedInCurrentState, /// OnOperationResponse: Authenticate in the Photon Cloud with invalid client values or custom authentication setup in Cloud Dashboard. CustomAuthenticationFailed, /// OnStatusChanged: The server disconnected this client from within the room's logic (the C# code). DisconnectByServerLogic, /// The authentication ticket should provide access to any Photon Cloud server without doing another authentication-service call. However, the ticket expired. AuthenticationTicketExpired } /// Available server (types) for internally used field: server. /// Photon uses 3 different roles of servers: Name Server, Master Server and Game Server. public enum ServerConnection { /// This server is where matchmaking gets done and where clients can get lists of rooms in lobbies. MasterServer, /// This server handles a number of rooms to execute and relay the messages between players (in a room). GameServer, /// This server is used initially to get the address (IP) of a Master Server for a specific region. Not used for Photon OnPremise (self hosted). NameServer } /// /// Defines how the communication gets encrypted. /// public enum EncryptionMode { /// /// This is the default encryption mode: Messages get encrypted only on demand (when you send operations with the "encrypt" parameter set to true). /// PayloadEncryption, /// /// With this encryption mode for UDP, the connection gets setup and all further datagrams get encrypted almost entirely. On-demand message encryption (like in PayloadEncryption) is skipped. /// DatagramEncryption = 10, } public static class EncryptionDataParameters { /// /// Key for encryption mode /// public const byte Mode = 0; /// /// Key for first secret /// public const byte Secret1 = 1; /// /// Key for second secret /// public const byte Secret2 = 2; } #endregion /// /// This class implements the Photon LoadBalancing workflow by using a LoadBalancingPeer. /// It keeps a state and will automatically execute transitions between the Master and Game Servers. /// /// /// This class (and the Player class) should be extended to implement your own game logic. /// You can override CreatePlayer as "factory" method for Players and return your own Player instances. /// The State of this class is essential to know when a client is in a lobby (or just on the master) /// and when in a game where the actual gameplay should take place. /// Extension notes: /// An extension of this class should override the methods of the IPhotonPeerListener, as they /// are called when the state changes. Call base.method first, then pick the operation or state you /// want to react to and put it in a switch-case. /// We try to provide demo to each platform where this api can be used, so lookout for those. /// public class LoadBalancingClient : IPhotonPeerListener { /// /// The client uses a LoadBalancingPeer as API to communicate with the server. /// This is public for ease-of-use: Some methods like OpRaiseEvent are not relevant for the connection state and don't need a override. /// public LoadBalancingPeer loadBalancingPeer; /// The version of your client. A new version also creates a new "virtual app" to separate players from older client versions. public string AppVersion { get; set; } /// The AppID as assigned from the Photon Cloud. If you host yourself, this is the "regular" Photon Server Application Name (most likely: "LoadBalancing"). public string AppId { get; set; } /// A user's authentication values for authentication in Photon. /// Set this property or pass AuthenticationValues by Connect(..., authValues). public AuthenticationValues AuthValues { get; set; } /// Enables the new Authentication workflow. public AuthModeOption AuthMode = AuthModeOption.Auth; /// Defines how the communication gets encrypted. public EncryptionMode EncryptionMode = EncryptionMode.PayloadEncryption; /// The protocol which will be used on Master- and Gameserver. /// /// When using AuthOnceWss, the client uses a wss-connection on the Nameserver but another protocol on the other servers. /// As the Nameserver sends an address, which is different per protocol, it needs to know the expected protocol. /// private ConnectionProtocol ExpectedProtocol = ConnectionProtocol.Udp; /// Exposes the TransportProtocol of the used PhotonPeer. Settable while not connected. public ConnectionProtocol TransportProtocol { get { return this.loadBalancingPeer.TransportProtocol; } set { if (this.loadBalancingPeer == null || this.loadBalancingPeer.PeerState != PeerStateValue.Disconnected) { this.DebugReturn(DebugLevel.WARNING, "Can't set TransportProtocol. Disconnect first! " + ((this.loadBalancingPeer != null) ? "PeerState: " + this.loadBalancingPeer.PeerState : "loadBalancingPeer is null.")); return; } this.loadBalancingPeer.TransportProtocol = value; } } /// Defines which IPhotonSocket class to use per ConnectionProtocol. /// /// Several platforms have special Socket implementations and slightly different APIs. /// To accomodate this, switching the socket implementation for a network protocol was made available. /// By default, UDP and TCP have socket implementations assigned. /// /// You only need to set the SocketImplementationConfig once, after creating a PhotonPeer /// and before connecting. If you switch the TransportProtocol, the correct implementation is being used. /// public Dictionary SocketImplementationConfig { get { return this.loadBalancingPeer.SocketImplementationConfig; } } ///Simplifies getting the token for connect/init requests, if this feature is enabled. private string TokenForInit { get { if (this.AuthMode == AuthModeOption.Auth) { return null; } return (this.AuthValues != null) ? this.AuthValues.Token : null; } } /// True if this client uses a NameServer to get the Master Server address. public bool IsUsingNameServer { get; protected internal set; } /// Name Server Host Name for Photon Cloud. Without port and without any prefix. public string NameServerHost = "ns.exitgames.com"; /// Name Server for HTTP connections to the Photon Cloud. Includes prefix and port. public string NameServerHttp = "http://ns.exitgames.com:80/photon/n"; /// Name Server port per protocol (the UDP port is different than TCP, etc). private static readonly Dictionary ProtocolToNameServerPort = new Dictionary() { { ConnectionProtocol.Udp, 5058 }, { ConnectionProtocol.Tcp, 4533 }, { ConnectionProtocol.WebSocket, 9093 }, { ConnectionProtocol.WebSocketSecure, 19093 } }; //, { ConnectionProtocol.RHttp, 6063 } }; /// Name Server Address for Photon Cloud (based on current protocol). You can use the default values and usually won't have to set this value. public string NameServerAddress { get { return this.GetNameServerAddress(); } } /// The currently used server address (if any). The type of server is define by Server property. public string CurrentServerAddress { get { return this.loadBalancingPeer.ServerAddress; } } /// Your Master Server address. In PhotonCloud, call ConnectToRegionMaster() to find your Master Server. /// /// In the Photon Cloud, explicit definition of a Master Server Address is not best practice. /// The Photon Cloud has a "Name Server" which redirects clients to a specific Master Server (per Region and AppId). /// public string MasterServerAddress { get; protected internal set; } /// The game server's address for a particular room. In use temporarily, as assigned by master. public string GameServerAddress { get; protected internal set; } /// The server this client is currently connected or connecting to. /// /// Each server (NameServer, MasterServer, GameServer) allow some operations and reject others. /// public ServerConnection Server { get; private set; } /// Backing field for property. private ClientState state = ClientState.PeerCreated; /// Current state this client is in. Careful: several states are "transitions" that lead to other states. public ClientState State { get { return this.state; } protected internal set { this.state = value; if (OnStateChangeAction != null) OnStateChangeAction(this.state); } } /// Returns if this client is currently connected or connecting to some type of server. /// This is even true while switching servers. Use IsConnectedAndReady to check only for those states that enable you to send Operations. public bool IsConnected { get { return this.loadBalancingPeer != null && this.State != ClientState.PeerCreated && this.State != ClientState.Disconnected; } } /// /// A refined version of IsConnected which is true only if your connection to the server is ready to accept operations. /// /// /// Which operations are available, depends on the Server. For example, the NameServer allows OpGetRegions which is not available anywhere else. /// The MasterServer does not allow you to send events (OpRaiseEvent) and on the GameServer you are unable to join a lobby (OpJoinLobby). /// Check which server you are on with PhotonNetwork.Server. /// public bool IsConnectedAndReady { get { if (this.loadBalancingPeer == null) { return false; } switch (this.State) { case ClientState.PeerCreated: case ClientState.Disconnected: case ClientState.Disconnecting: case ClientState.Authenticating: case ClientState.ConnectingToGameserver: case ClientState.ConnectingToMasterserver: case ClientState.ConnectingToNameServer: case ClientState.Joining: case ClientState.Leaving: return false; // we are not ready to execute any operations } return true; } } /// Register a method to be called when this client's ClientState gets set. /// This can be useful to react to being connected, joined into a room, etc. public event Action OnStateChangeAction; /// Register a method to be called when an event got dispatched. Gets called at the end of OnEvent(). /// /// This is an alternative to extending LoadBalancingClient to override OnEvent(). /// /// Note that OnEvent is executing before your Action is called. /// That means for example: Joining players will already be in the player list but leaving /// players will already be removed from the room. /// public event Action OnEventAction; /// Register a method to be called when this client's ClientState gets set. /// /// This is an alternative to extending LoadBalancingClient to override OnOperationResponse(). /// /// Note that OnOperationResponse gets executed before your Action is called. /// That means for example: The OpJoinLobby response already set the state to "JoinedLobby" /// and the response to OpLeave already triggered the Disconnect before this is called. /// public event Action OnOpResponseAction; /// Summarizes (aggregates) the different causes for disconnects of a client. /// /// A disconnect can be caused by: errors in the network connection or some vital operation failing /// (which is considered "high level"). While operations always trigger a call to OnOperationResponse, /// connection related changes are treated in OnStatusChanged. /// The DisconnectCause is set in either case and summarizes the causes for any disconnect in a single /// state value which can be used to display (or debug) the cause for disconnection. /// public DisconnectCause DisconnectedCause { get; protected set; } /// Internal value if the client is in a lobby. /// This is used to re-set this.State, when joining/creating a room fails. private bool inLobby; /// The lobby this client currently uses. public TypedLobby CurrentLobby { get; protected internal set; } /// Backing field for property. private bool autoJoinLobby = true; /// If your client should join random games, you can skip joining the lobby. Call OpJoinRandomRoom and create a room if that fails. public bool AutoJoinLobby { get { return this.autoJoinLobby; } set { this.autoJoinLobby = value; } } /// /// If set to true, the Master Server will report the list of used lobbies to the client. This sets and updates LobbyStatistics. /// /// /// Lobby Statistics can be useful if a game uses multiple lobbies and you want /// to show activity of each to players. /// /// LobbyStatistics are updated when you connect to the Master Server. /// public bool EnableLobbyStatistics; /// Internal lobby stats cache, used by LobbyStatistics. private List lobbyStatistics = new List(); /// /// If RequestLobbyStatistics is true, this provides a list of used lobbies (their name, type, room- and player-count) of this application, while on the Master Server. /// /// /// If turned on, the Master Server will provide information about active lobbies for this application. /// /// 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. /// /// Lobby Statistics are not turned on by default. /// Enable them by setting RequestLobbyStatistics to true before you connect. /// /// LobbyStatistics are updated when you connect to the Master Server. /// You can check in OnEvent if EventCode.LobbyStats arrived. This the updates. /// public List LobbyStatistics { get { return this.lobbyStatistics; } private set { this.lobbyStatistics = value; } } /// The local player is never null but not valid unless the client is in a room, too. The ID will be -1 outside of rooms. public Player LocalPlayer { get; internal set; } /// /// The nickname of the player (synced with others). Same as client.LocalPlayer.NickName. /// public string NickName { get { return this.LocalPlayer.NickName; } set { if (this.LocalPlayer == null) { return; } this.LocalPlayer.NickName = value; } } /// An ID for this user. Sent in OpAuthenticate when you connect. If not set, the PlayerName is applied during connect. /// /// On connect, if the UserId is null or empty, the client will copy the PlayName to UserId. If PlayerName is not set either /// (before connect), the server applies a temporary ID which stays unknown to this client and other clients. /// /// The UserId is what's used in FindFriends and for fetching data for your account (with WebHooks e.g.). /// /// By convention, set this ID before you connect, not while being connected. /// There is no error but the ID won't change while being connected. /// public string UserId { get { if (this.AuthValues != null) { return this.AuthValues.UserId; } return null; } set { if (this.AuthValues == null) { this.AuthValues = new AuthenticationValues(); } this.AuthValues.UserId = value; } } /// This "list" is populated while being in the lobby of the Master. It contains RoomInfo per roomName (keys). public Dictionary RoomInfoList = new Dictionary(); /// The current room this client is connected to (null if none available). public Room CurrentRoom; /// Statistic value available on master server: Players on master (looking for games). public int PlayersOnMasterCount { get; internal set; } /// Statistic value available on master server: Players in rooms (playing). public int PlayersInRoomsCount { get; internal set; } /// Statistic value available on master server: Rooms currently created. public int RoomsCount { get; internal set; } /// Internally used to decide if a room must be created or joined on game server. private JoinType lastJoinType; protected internal EnterRoomParams enterRoomParamsCache; /// Internally used to trigger OpAuthenticate when encryption was established after a connect. private bool didAuthenticate; /// /// List of friends, their online status and the room they are in. Null until initialized by OpFindFriends response. /// /// /// Do not modify this list! It's internally handled by OpFindFriends and meant as read-only. /// The value of FriendListAge gives you a hint how old the data is. 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. /// public List FriendList { get; private set; } /// Contains the list of names of friends to look up their state on the server. private string[] friendListRequested; /// /// Age of friend list info (in milliseconds). It's 0 until a friend list is fetched. /// public int FriendListAge { get { return (this.isFetchingFriendList || this.friendListTimestamp == 0) ? 0 : Environment.TickCount - this.friendListTimestamp; } } /// Private timestamp (in ms) of the last friendlist update. private int friendListTimestamp; /// Internal flag to know if the client currently fetches a friend list. private bool isFetchingFriendList; /// Internally used to check if a "Secret" is available to use. Sent by Photon Cloud servers, it simplifies authentication when switching servers. protected bool IsAuthorizeSecretAvailable { get { return this.AuthValues != null && !string.IsNullOrEmpty(this.AuthValues.Token); } } /// A list of region names for the Photon Cloud. Set by the result of OpGetRegions(). /// Put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available. public string[] AvailableRegions { get; private set; } /// A list of region server (IP addresses with port) for the Photon Cloud. Set by the result of OpGetRegions(). /// Put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available. public string[] AvailableRegionsServers { get; private set; } /// The cloud region this client connects to. Set by ConnectToRegionMaster(). Not set if you don't use a NameServer! public string CloudRegion { get; private set; } /// Creates a LoadBalancingClient with UDP protocol or the one specified. /// Specifies the network protocol to use for connections. public LoadBalancingClient(ConnectionProtocol protocol = ConnectionProtocol.Udp) { this.loadBalancingPeer = new LoadBalancingPeer(this, protocol); this.LocalPlayer = this.CreatePlayer(string.Empty, -1, true, null); this.State = ClientState.PeerCreated; } /// Creates a LoadBalancingClient, setting various values needed before connecting. /// The Master Server's address to connect to. Used in Connect. /// The AppId of this title. Needed for the Photon Cloud. Find it in the Dashboard. /// A version for this client/build. In the Photon Cloud, players are separated by AppId, GameVersion and Region. /// Specifies the network protocol to use for connections. public LoadBalancingClient(string masterAddress, string appId, string gameVersion, ConnectionProtocol protocol = ConnectionProtocol.Udp) : this(protocol) { this.MasterServerAddress = masterAddress; this.AppId = appId; this.AppVersion = gameVersion; } /// /// Gets the NameServer Address (with prefix and port), based on the set protocol (this.loadBalancingPeer.UsedProtocol). /// /// NameServer Address (with prefix and port). private string GetNameServerAddress() { var protocolPort = 0; ProtocolToNameServerPort.TryGetValue(this.loadBalancingPeer.TransportProtocol, out protocolPort); switch (this.loadBalancingPeer.TransportProtocol) { case ConnectionProtocol.Udp: case ConnectionProtocol.Tcp: return string.Format("{0}:{1}", NameServerHost, protocolPort); #if RHTTP case ConnectionProtocol.RHttp: return NameServerHttp; #endif case ConnectionProtocol.WebSocket: return string.Format("ws://{0}:{1}", NameServerHost, protocolPort); case ConnectionProtocol.WebSocketSecure: return string.Format("wss://{0}:{1}", NameServerHost, protocolPort); default: throw new ArgumentOutOfRangeException(); } } #region Operations and Commands /// /// Starts the "process" to connect to the master server. Relevant connection-values parameters can be set via parameters. /// /// /// The process to connect includes several steps: the actual connecting, establishing encryption, authentification /// (of app and optionally the user) and joining a lobby (if AutoJoinLobby is true). /// /// Instead of providing all these parameters, you can also set the individual properties of a client before calling Connect(). /// /// Users can connect either anonymously or use "Custom Authentication" to verify each individual player's login. /// Custom Authentication in Photon uses external services and communities to verify users. While the client provides a user's info, /// the service setup is done in the Photon Cloud Dashboard. /// The parameter authValues will set this.AuthValues and use them in the connect process. /// /// To connect to the Photon Cloud, a valid AppId must be provided. This is shown in the Photon Cloud Dashboard. /// https://cloud.photonengine.com/dashboard /// Connecting to the Photon Cloud might fail due to: /// - Network issues (OnStatusChanged() StatusCode.ExceptionOnConnect) /// - Region not available (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.InvalidRegion) /// - Subscription CCU limit reached (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.MaxCcuReached) /// More about the connection limitations: /// http://doc.photonengine.com/photon-cloud/SubscriptionErrorCases/#cat-references /// /// Set a master server address instead of using the default. Uses default if null or empty. /// Your application's name or the AppID assigned by Photon Cloud (as listed in Dashboard). Uses default if null or empty. /// Can be used to separate users by their client's version (useful to add features without breaking older clients). Uses default if null or empty. /// Optional name for this player. /// Authentication values for this user. Optional. If you provide a unique userID it is used for FindFriends. /// If the operation could be send (can be false for bad server urls). public bool Connect(string masterServerAddress, string appId, string appVersion, string nickName, AuthenticationValues authValues) { if (!string.IsNullOrEmpty(masterServerAddress)) { this.MasterServerAddress = masterServerAddress; } if (!string.IsNullOrEmpty(appId)) { this.AppId = appId; } if (!string.IsNullOrEmpty(appVersion)) { this.AppVersion = appVersion; } if (!string.IsNullOrEmpty(nickName)) { this.NickName = nickName; } this.AuthValues = authValues; // as this.Connect() checks usage of WebSockets for WebGL exports, this method doesn't return this.Connect(); } /// /// Starts the "process" to connect to a Master Server, using MasterServerAddress and AppId properties. /// /// /// To connect to the Photon Cloud, use ConnectToRegionMaster(). /// /// The process to connect includes several steps: the actual connecting, establishing encryption, authentification /// (of app and optionally the user) and joining a lobby (if AutoJoinLobby is true). /// /// Users can connect either anonymously or use "Custom Authentication" to verify each individual player's login. /// Custom Authentication in Photon uses external services and communities to verify users. While the client provides a user's info, /// the service setup is done in the Photon Cloud Dashboard. /// The parameter authValues will set this.AuthValues and use them in the connect process. /// /// Connecting to the Photon Cloud might fail due to: /// - Network issues (OnStatusChanged() StatusCode.ExceptionOnConnect) /// - Region not available (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.InvalidRegion) /// - Subscription CCU limit reached (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.MaxCcuReached) /// More about the connection limitations: /// http://doc.photonengine.com/photon-cloud/SubscriptionErrorCases/#cat-references /// public virtual bool Connect() { this.DisconnectedCause = DisconnectCause.None; #if UNITY_WEBGL if (this.TransportProtocol == ConnectionProtocol.Tcp || this.TransportProtocol == ConnectionProtocol.Udp) { this.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure."); this.TransportProtocol = ConnectionProtocol.WebSocketSecure; } #endif if (this.loadBalancingPeer.Connect(this.MasterServerAddress, this.AppId, this.TokenForInit)) { this.State = ClientState.ConnectingToMasterserver; return true; } return false; } /// /// Connects to the NameServer for Photon Cloud, where a region and server list can be obtained. /// /// /// If the workflow was started or failed right away. public bool ConnectToNameServer() { this.IsUsingNameServer = true; this.CloudRegion = null; if (this.AuthMode == AuthModeOption.AuthOnceWss) { this.ExpectedProtocol = this.loadBalancingPeer.TransportProtocol; this.loadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; } if (!this.loadBalancingPeer.Connect(NameServerAddress, "NameServer", this.TokenForInit)) { return false; } this.State = ClientState.ConnectingToNameServer; return true; } /// /// Connects you to a specific region's Master Server, using the Name Server to find the IP. /// /// If the operation could be sent. If false, no operation was sent. public bool ConnectToRegionMaster(string region) { this.IsUsingNameServer = true; if (this.State == ClientState.ConnectedToNameServer) { this.CloudRegion = region; return this.CallAuthenticate(); } this.loadBalancingPeer.Disconnect(); this.CloudRegion = region; if (this.AuthMode == AuthModeOption.AuthOnceWss) { this.ExpectedProtocol = this.loadBalancingPeer.TransportProtocol; this.loadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; } if (!this.loadBalancingPeer.Connect(this.NameServerAddress, "NameServer", null)) { return false; } this.State = ClientState.ConnectingToNameServer; return true; } /// Disconnects this client from any server and sets this.State if the connection is successfuly closed. public void Disconnect() { if (this.State != ClientState.Disconnected) { this.State = ClientState.Disconnecting; this.loadBalancingPeer.Disconnect(); //// we can set this high-level state if the low-level (connection)state is "disconnected" //if (this.loadBalancingPeer.PeerState == PeerStateValue.Disconnected || this.loadBalancingPeer.PeerState == PeerStateValue.InitializingApplication) //{ // this.State = ClientState.Disconnected; //} } } private bool CallAuthenticate() { if (this.AuthMode == AuthModeOption.Auth) { return this.loadBalancingPeer.OpAuthenticate(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, (this.EnableLobbyStatistics && this.Server == ServerConnection.MasterServer)); } else { return this.loadBalancingPeer.OpAuthenticateOnce(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, this.EncryptionMode, this.ExpectedProtocol); } } /// /// This method dispatches all available incoming commands and then sends this client's outgoing commands. /// It uses DispatchIncomingCommands and SendOutgoingCommands to do that. /// /// /// The Photon client libraries are designed to fit easily into a game or application. The application /// is in control of the context (thread) in which incoming events and responses are executed and has /// full control of the creation of UDP/TCP packages. /// /// Sending packages and dispatching received messages are two separate tasks. Service combines them /// into one method at the cost of control. It calls DispatchIncomingCommands and SendOutgoingCommands. /// /// Call this method regularly (2..20 times a second). /// /// This will Dispatch ANY received commands (unless a reliable command in-order is still missing) and /// events AND will send queued outgoing commands. Fewer calls might be more effective if a device /// cannot send many packets per second, as multiple operations might be combined into one package. /// /// /// You could replace Service by: /// /// while (DispatchIncomingCommands()); //Dispatch until everything is Dispatched... /// SendOutgoingCommands(); //Send a UDP/TCP package with outgoing messages /// /// /// public void Service() { if (this.loadBalancingPeer != null) { this.loadBalancingPeer.Service(); } } /// /// Private Disconnect variant that sets the state, too. /// private void DisconnectToReconnect() { switch (this.Server) { case ServerConnection.NameServer: this.State = ClientState.DisconnectingFromNameServer; break; case ServerConnection.MasterServer: this.State = ClientState.DisconnectingFromMasterserver; break; case ServerConnection.GameServer: this.State = ClientState.DisconnectingFromGameserver; break; } this.loadBalancingPeer.Disconnect(); } /// /// Privately used only. /// Starts the "process" to connect to the game server (connect before a game is joined). /// private bool ConnectToGameServer() { if (this.loadBalancingPeer.Connect(this.GameServerAddress, this.AppId, this.TokenForInit)) { this.State = ClientState.ConnectingToGameserver; return true; } // TODO: handle error "cant connect to GS" return false; } /// /// While on the NameServer, this gets you the list of regional servers (short names and their IPs to ping them). /// /// If the operation could be sent. If false, no operation was sent (e.g. while not connected to the NameServer). public bool OpGetRegions() { if (this.Server != ServerConnection.NameServer) { return false; } bool sent = this.loadBalancingPeer.OpGetRegions(this.AppId); if (sent) { this.AvailableRegions = null; } return sent; } /// /// Request the rooms and online status for a list of friends. All clients should set a unique UserId before connecting. The result is available in this.FriendList. /// /// /// Used on Master Server to find the rooms played by a selected list of users. /// The result will be stored in LoadBalancingClient.FriendList, which is null before the first server response. /// /// Users identify themselves by setting a UserId in the LoadBalancingClient instance. /// This will send the ID in OpAuthenticate during connect (to master and game servers). /// Note: Changing a player's name doesn't make sense when using a friend list. /// /// The list of usernames 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) /// /// Array of friend's names (make sure they are unique). /// If the operation could be sent (requires connection). public bool OpFindFriends(string[] friendsToFind) { if (this.loadBalancingPeer == null) { return false; } if (this.isFetchingFriendList || this.Server != ServerConnection.MasterServer) { return false; // fetching friends currently, so don't do it again (avoid changing the list while fetching friends) } this.isFetchingFriendList = true; this.friendListRequested = friendsToFind; return this.loadBalancingPeer.OpFindFriends(friendsToFind); } /// /// Joins the lobby on the Master Server, where you get a list of RoomInfos of currently open rooms. /// This is an async request which triggers a OnOperationResponse() call. /// /// The lobby join to. Use null for default lobby. /// If the operation could be sent (has to be connected). public bool OpJoinLobby(TypedLobby lobby) { if (lobby == null) { lobby = TypedLobby.Default; } bool sent = this.loadBalancingPeer.OpJoinLobby(lobby); if (sent) { this.CurrentLobby = lobby; } return sent; } /// Opposite of joining a lobby. You don't have to explicitly leave a lobby to join another (client can be in one max, at any time). /// If the operation could be sent (has to be connected). public bool OpLeaveLobby() { return this.loadBalancingPeer.OpLeaveLobby(); } /// Operation to join a random room if available. You can use room properties to filter accepted rooms. /// /// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms. /// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties. /// You need to define which Custom Room Properties will be available for matchmaking when you create a room. /// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby) /// /// This operation fails if no rooms are fitting or available (all full, closed or not visible). /// Override this class and implement OnOperationResponse(OperationResponse operationResponse). /// /// OpJoinRandomRoom can only be called while the client is connected to a Master Server. /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. /// Alternatively, check the returned bool value. /// /// While the server is looking for a game, the State will be Joining. It's set immediately when this method sent the Operation. /// /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically /// to switch servers and join the room. When you're in the room, this client's State will become /// ClientState.Joined (both, for joining or creating it). /// Set a OnStateChangeAction method to check for states. /// /// When joining a room, this client's Player Custom Properties will be sent to the room. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. /// Note that the player properties will be cached locally and sent to any next room you would join, too. /// /// More about matchmaking: /// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby /// /// 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. /// /// Optional. A room will only be joined, if it matches these custom properties (with string keys). /// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value. /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. /// If the operation could be sent currently (requires connection to Master Server). public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, string[] expectedUsers = null) { return OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, MatchmakingMode.FillRoom, TypedLobby.Default, null, expectedUsers); } /// Operation to join a random room if available. You can use room properties to filter accepted rooms. /// /// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms. /// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties. /// You need to define which Custom Room Properties will be available for matchmaking when you create a room. /// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby) /// /// This operation fails if no rooms are fitting or available (all full, closed or not visible). /// Override this class and implement OnOperationResponse(OperationResponse operationResponse). /// /// OpJoinRandomRoom can only be called while the client is connected to a Master Server. /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. /// Alternatively, check the returned bool value. /// /// While the server is looking for a game, the State will be Joining. It's set immediately when this method sent the Operation. /// /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically /// to switch servers and join the room. When you're in the room, this client's State will become /// ClientState.Joined (both, for joining or creating it). /// Set a OnStateChangeAction method to check for states. /// /// When joining a room, this client's Player Custom Properties will be sent to the room. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. /// Note that the player properties will be cached locally and sent to any next room you would join, too. /// /// More about matchmaking: /// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby /// /// Optional. A room will only be joined, if it matches these custom properties (with string keys). /// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value. /// Selects one of the available matchmaking algorithms. See MatchmakingMode enum for options. /// If the operation could be sent currently (requires connection to Master Server). public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, MatchmakingMode matchmakingMode) { return this.OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, matchmakingMode, TypedLobby.Default, null); } /// Operation to join a random room if available. You can use room properties to filter accepted rooms. /// /// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms. /// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties. /// You need to define which Custom Room Properties will be available for matchmaking when you create a room. /// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby) /// /// This operation fails if no rooms are fitting or available (all full, closed or not visible). /// Override this class and implement OnOperationResponse(OperationResponse operationResponse). /// /// OpJoinRandomRoom can only be called while the client is connected to a Master Server. /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. /// Alternatively, check the returned bool value. /// /// While the server is looking for a game, the State will be Joining. /// It's set immediately when this method sent the Operation. /// /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically /// to switch servers and join the room. When you're in the room, this client's State will become /// ClientState.Joined (both, for joining or creating it). /// Set a OnStateChangeAction method to check for states. /// /// When joining a room, this client's Player Custom Properties will be sent to the room. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. /// Note that the player properties will be cached locally and sent to any next room you would join, too. /// /// The parameter lobby can be null (using the defaul lobby) or a typed lobby you make up. /// Lobbies are created on the fly, as required by the clients. If you organize matchmaking with lobbies, /// keep in mind that they also fragment your matchmaking. Using more lobbies will put less rooms in each. /// /// The parameter sqlLobbyFilter can only be combined with the LobbyType.SqlLobby. In that case, it's used /// to define a sql-like "WHERE" clause for filtering rooms. This is useful for skill-based matchmaking e.g.. /// /// More about matchmaking: /// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby /// /// 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. /// /// Optional. A room will only be joined, if it matches these custom properties (with string keys). /// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value. /// Selects one of the available matchmaking algorithms. See MatchmakingMode enum for options. /// The lobby in which to find a room. Use null for default lobby. /// Can be used with LobbyType.SqlLobby only. This is a "where" clause of a sql statement. Use null for random game. /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. /// If the operation could be sent currently (requires connection to Master Server). public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, MatchmakingMode matchmakingMode, TypedLobby lobby, string sqlLobbyFilter, string[] expectedUsers = null) { if (lobby == null) { lobby = TypedLobby.Default; } this.State = ClientState.Joining; this.lastJoinType = JoinType.JoinRandomRoom; this.CurrentLobby = lobby; this.enterRoomParamsCache = new EnterRoomParams(); this.enterRoomParamsCache.Lobby = lobby; this.enterRoomParamsCache.ExpectedUsers = expectedUsers; OpJoinRandomRoomParams opParams = new OpJoinRandomRoomParams(); opParams.ExpectedCustomRoomProperties = expectedCustomRoomProperties; opParams.ExpectedMaxPlayers = expectedMaxPlayers; opParams.MatchingType = matchmakingMode; opParams.TypedLobby = lobby; opParams.SqlLobbyFilter = sqlLobbyFilter; opParams.ExpectedUsers = expectedUsers; return this.loadBalancingPeer.OpJoinRandomRoom(opParams); //return this.loadBalancingPeer.OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, playerPropsToSend, matchmakingMode, lobby, sqlLobbyFilter); } /// /// Joins a room by roomName. Useful when using room lists in lobbies or when you know the name otherwise. /// /// /// This method is useful when you are using a lobby to list rooms and know their names. /// A room's name has to be unique (per region and game version), so it does not matter which lobby it's in. /// /// If the room is full, closed or not existing, this will fail. Override this class and implement /// OnOperationResponse(OperationResponse operationResponse) to get the errors. /// /// OpJoinRoom can only be called while the client is connected to a Master Server. /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. /// Alternatively, check the returned bool value. /// /// While the server is joining the game, the State will be ClientState.Joining. /// It's set immediately when this method sends the Operation. /// /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically /// to switch servers and join the room. When you're in the room, this client's State will become /// ClientState.Joined (both, for joining or creating it). /// Set a OnStateChangeAction method to check for states. /// /// When joining a room, this client's Player Custom Properties will be sent to the room. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. /// Note that the player properties will be cached locally and sent to any next room you would join, too. /// /// It's usually better to use OpJoinOrCreateRoom for invitations. /// Then it does not matter if the room is already setup. /// /// 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. /// /// The name of the room to join. Must be existing already, open and non-full or can't be joined. /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. /// If the operation could be sent currently (requires connection to Master Server). public bool OpJoinRoom(string roomName, string[] expectedUsers = null) { this.State = ClientState.Joining; this.lastJoinType = JoinType.JoinRoom; bool onGameServer = this.Server == ServerConnection.GameServer; EnterRoomParams opParams = new EnterRoomParams(); this.enterRoomParamsCache = opParams; opParams.RoomName = roomName; opParams.OnGameServer = onGameServer; opParams.ExpectedUsers = expectedUsers; return this.loadBalancingPeer.OpJoinRoom(opParams); } /// /// Rejoins a room by roomName (using the userID internally to return). Useful to return to a persisted room or after temporarily losing connection. /// public bool OpReJoinRoom(string roomName) { this.State = ClientState.Joining; this.lastJoinType = JoinType.JoinRoom; bool onGameServer = this.Server == ServerConnection.GameServer; EnterRoomParams opParams = new EnterRoomParams(); this.enterRoomParamsCache = opParams; opParams.RoomName = roomName; opParams.OnGameServer = onGameServer; opParams.RejoinOnly = true; return this.loadBalancingPeer.OpJoinRoom(opParams); } /// /// Joins a specific room by name. If the room does not exist (yet), it will be created implicitly. /// /// /// Unlike OpJoinRoom, this operation does not fail if the room does not exist. /// This can be useful when you send invitations to a room before actually creating it: /// Any invited player (whoever is first) can call this and on demand, the room gets created implicitly. /// /// This operation does not allow you to re-join a game. To return to a room, use OpJoinRoom with /// the actorNumber which was assigned previously. /// /// If you set room properties in RoomOptions, they get ignored when the room is existing already. /// This avoids changing the room properties by late joining players. Only when the room gets created, /// the RoomOptions are set in this case. /// /// If the room is full or closed, this will fail. Override this class and implement /// OnOperationResponse(OperationResponse operationResponse) to get the errors. /// /// This method can only be called while the client is connected to a Master Server. /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before /// calling this method. Alternatively, check the returned bool value. /// /// While the server is joining the game, the State will be ClientState.Joining. /// It's set immediately when this method sends the Operation. /// /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically /// to switch servers and join the room. When you're in the room, this client's State will become /// ClientState.Joined (both, for joining or creating it). /// Set a OnStateChangeAction method to check for states. /// /// When entering the room, this client's Player Custom Properties will be sent to the room. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. /// Note that the player properties will be cached locally and sent to any next room you would join, too. /// /// 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. /// /// The name of the room to join (might be created implicitly). /// Contains the parameters and properties of the new room. See RoomOptions class for a description of each. /// Typed lobby to be used if the roomname is not in use (and room gets created). If != null, it will also set CurrentLobby. /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. /// If the operation could be sent currently (requires connection to Master Server). public bool OpJoinOrCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby, string[] expectedUsers = null) { this.State = ClientState.Joining; this.lastJoinType = JoinType.JoinOrCreateRoom; this.CurrentLobby = lobby; bool onGameServer = this.Server == ServerConnection.GameServer; EnterRoomParams opParams = new EnterRoomParams(); this.enterRoomParamsCache = opParams; opParams.RoomName = roomName; opParams.RoomOptions = roomOptions; opParams.Lobby = lobby; opParams.CreateIfNotExists = true; opParams.OnGameServer = onGameServer; opParams.ExpectedUsers = expectedUsers; return this.loadBalancingPeer.OpJoinRoom(opParams); } /// /// Creates a new room on the server (or fails if the name is already in use). /// /// /// 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). Room names are unique. /// /// A room will be attached to the specified lobby. Use null as lobby to attach the /// room to the lobby you are now in. If you are in no lobby, the default lobby is used. /// /// Multiple lobbies can help separate players by map or skill or game type. Each room can only be found /// in one lobby (no matter if defined by name and type or as default). /// /// This method can only be called while the client is connected to a Master Server. /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. /// Alternatively, check the returned bool value. /// /// Even when sent, the Operation will fail (on the server) if the roomName is in use. /// Override this class and implement OnOperationResponse(OperationResponse operationResponse) to get the errors. /// /// /// While the server is creating the game, the State will be ClientState.Joining. /// The state Joining is used because the client is on the way to enter a room (no matter if joining or creating). /// It's set immediately when this method sends the Operation. /// /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically /// to switch servers and enter the room. When you're in the room, this client's State will become /// ClientState.Joined (both, for joining or creating it). /// Set a OnStateChangeAction method to check for states. /// /// When entering the room, this client's Player Custom Properties will be sent to the room. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. /// Note that the player properties will be cached locally and sent to any next room you would join, too. /// /// 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. /// /// The name to create a room with. Must be unique and not in use or can't be created. If null, the server will assign a GUID as name. /// Contains the parameters and properties of the new room. See RoomOptions class for a description of each. /// The lobby (name and type) in which to create the room. Null uses the current lobby or the default lobby (if not in a lobby). /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. /// If the operation could be sent currently (requires connection to Master Server). public bool OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby, string[] expectedUsers = null) { this.State = ClientState.Joining; this.lastJoinType = JoinType.CreateRoom; this.CurrentLobby = lobby; bool onGameServer = this.Server == ServerConnection.GameServer; EnterRoomParams opParams = new EnterRoomParams(); this.enterRoomParamsCache = opParams; opParams.RoomName = roomName; opParams.RoomOptions = roomOptions; opParams.Lobby = lobby; opParams.OnGameServer = onGameServer; opParams.ExpectedUsers = expectedUsers; return this.loadBalancingPeer.OpCreateRoom(opParams); //return this.loadBalancingPeer.OpCreateRoom(roomName, roomOptions, lobby, playerPropsToSend, onGameServer); } /// /// Leaves the CurrentRoom and returns to the Master server (back to the lobby). /// OpLeaveRoom skips execution when the room is null or the server is not GameServer or the client is disconnecting from GS already. /// OpLeaveRoom returns false in those cases and won't change the state, so check return of this method. /// /// /// This method actually is not an operation per se. It sets a state and calls Disconnect(). /// This is is quicker than calling OpLeave and then disconnect (which also triggers a leave). /// /// If the current room could be left (impossible while not in a room). public bool OpLeaveRoom() { return OpLeaveRoom(false); //TURNBASED } /// /// Leaves the current room, optionally telling the server that the user is just becoming inactive. /// /// /// If true, this player becomes inactive in the game and can return later (if PlayerTTL of the room is > 0). /// /// OpLeaveRoom skips execution when the room is null or the server is not GameServer or the client is disconnecting from GS already. /// OpLeaveRoom returns false in those cases and won't change the state, so check return of this method. /// /// In some cases, this method will skip the OpLeave call and just call Disconnect(), /// which not only leaves the room but also the server. Disconnect also triggers a leave and so that workflow is is quicker. /// /// If the current room could be left (impossible while not in a room). public bool OpLeaveRoom(bool becomeInactive) { if (this.CurrentRoom == null || this.Server != ServerConnection.GameServer || this.State == ClientState.DisconnectingFromGameserver) { return false; } if (becomeInactive) { this.State = ClientState.DisconnectingFromGameserver; this.loadBalancingPeer.Disconnect(); } else { this.State = ClientState.Leaving; this.loadBalancingPeer.OpLeaveRoom(false); //TURNBASED users can leave a room forever or return later } return true; } /// Gets a list of games matching a SQL-like where clause. /// /// Operation is only available for lobbies of type SqlLobby. /// This is an async request which triggers a OnOperationResponse() call. /// Returned game list is stored in RoomInfoList. /// /// /// The lobby to query. Has to be of type SqlLobby. /// The sql query statement. /// If the operation could be sent (has to be connected). public bool OpGetGameList(TypedLobby typedLobby, string sqlLobbyFilter) { return this.loadBalancingPeer.OpGetGameList(typedLobby, sqlLobbyFilter); } /// /// Updates and synchronizes a Player's Custom Properties. Optionally, expectedProperties can be provided as condition. /// /// /// Custom Properties are a set of string keys and arbitrary values which is synchronized /// for the players in a Room. They are available when the client enters the room, as /// they are in the response of OpJoin and OpCreate. /// /// Custom Properties either relate to the (current) Room or a Player (in that Room). /// /// Both classes locally cache the current key/values and make them available as /// property: CustomProperties. This is provided only to read them. /// You must use the method SetCustomProperties to set/modify them. /// /// Any client can set any Custom Properties anytime (when in a room). /// It's up to the game logic to organize how they are best used. /// /// You should call SetCustomProperties only with key/values that are new or changed. This reduces /// traffic and performance. /// /// Unless you define some expectedProperties, setting key/values is always permitted. /// In this case, the property-setting client will not receive the new values from the server but /// instead update its local cache in SetCustomProperties. /// /// If you define expectedProperties, the server will skip updates if the server property-cache /// does not contain all expectedProperties with the same values. /// In this case, the property-setting client will get an update from the server and update it's /// cached key/values at about the same time as everyone else. /// /// The benefit of using expectedProperties can be only one client successfully sets a key from /// one known value to another. /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally. /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to /// take the item will have it (and the others fail to set the ownership). /// /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true). /// /// Defines which player the Custom Properties belong to. ActorID of a player. /// Hashtable of Custom Properties that changes. /// Provide some keys/values to use as condition for setting the new values. Client must be in room. /// Defines if the set properties should be forwarded to a WebHook. Client must be in room. public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null) { if (this.CurrentRoom == null) { // if you attempt to set this player's values without conditions, then fine: if (expectedProperties == null && webFlags == null && this.LocalPlayer != null && this.LocalPlayer.ID == actorNr) { this.LocalPlayer.SetCustomProperties(propertiesToSet); return true; } if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR) { this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. To use expectedProperties or webForward, you have to be in a room. State: " + this.State); } return false; } Hashtable customActorProperties = new Hashtable(); customActorProperties.MergeStringKeys(propertiesToSet); return this.OpSetPropertiesOfActor(actorNr, customActorProperties, expectedProperties, webFlags); } /// Replaced by a newer version with WebFlags. [Obsolete("Use the overload with WebFlags.")] public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties, bool webForward) { return this.OpSetCustomPropertiesOfActor(actorNr, propertiesToSet, expectedProperties, (webForward ? new WebFlags(WebFlags.HttpForwardConst) : null)); } /// Internally used to cache and set properties (including well known properties). /// Requires being in a room (because this attempts to send an operation which will fail otherwise). protected internal bool OpSetPropertiesOfActor(int actorNr, Hashtable actorProperties, Hashtable expectedProperties = null, WebFlags webFlags = null) { if (this.CurrentRoom == null) { if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR) { this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfActor() failed because this client is not in a room currently. State: " + this.State); } return false; } if (expectedProperties == null || expectedProperties.Count == 0) { Player target = this.CurrentRoom.GetPlayer(actorNr); if (target != null) { target.InternalCacheProperties(actorProperties); } } return this.loadBalancingPeer.OpSetPropertiesOfActor(actorNr, actorProperties, expectedProperties, webFlags); } /// /// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition. /// /// /// Custom Properties are a set of string keys and arbitrary values which is synchronized /// for the players in a Room. They are available when the client enters the room, as /// they are in the response of OpJoin and OpCreate. /// /// Custom Properties either relate to the (current) Room or a Player (in that Room). /// /// Both classes locally cache the current key/values and make them available as /// property: CustomProperties. This is provided only to read them. /// You must use the method SetCustomProperties to set/modify them. /// /// Any client can set any Custom Properties anytime (when in a room). /// It's up to the game logic to organize how they are best used. /// /// You should call SetCustomProperties only with key/values that are new or changed. This reduces /// traffic and performance. /// /// Unless you define some expectedProperties, setting key/values is always permitted. /// In this case, the property-setting client will not receive the new values from the server but /// instead update its local cache in SetCustomProperties. /// /// If you define expectedProperties, the server will skip updates if the server property-cache /// does not contain all expectedProperties with the same values. /// In this case, the property-setting client will get an update from the server and update it's /// cached key/values at about the same time as everyone else. /// /// The benefit of using expectedProperties can be only one client successfully sets a key from /// one known value to another. /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally. /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to /// take the item will have it (and the others fail to set the ownership). /// /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true). /// /// Hashtable of Custom Properties that changes. /// Provide some keys/values to use as condition for setting the new values. /// Defines if the set properties should be forwarded to a WebHook. public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null) { Hashtable customGameProps = new Hashtable(); customGameProps.MergeStringKeys(propertiesToSet); return this.OpSetPropertiesOfRoom(customGameProps, expectedProperties, webFlags); } /// Replaced by a newer version with WebFlags. [Obsolete("Use the overload with WebFlags.")] public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties, bool webForward) { return this.OpSetCustomPropertiesOfRoom(propertiesToSet, expectedProperties, (webForward ? new WebFlags(WebFlags.HttpForwardConst) : null)); } /// Internally used to cache and set properties (including well known properties). /// Requires being in a room (because this attempts to send an operation which will fail otherwise). protected internal bool OpSetPropertiesOfRoom(Hashtable gameProperties, Hashtable expectedProperties = null, WebFlags webFlags = null) { if (this.CurrentRoom == null) { if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR) { this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfRoom() failed because this client is not in a room currently. State: " + this.State); } return false; } if (expectedProperties == null || expectedProperties.Count == 0) { this.CurrentRoom.InternalCacheProperties(gameProperties); } return this.loadBalancingPeer.OpSetPropertiesOfRoom(gameProperties, expectedProperties, webFlags); } /// /// Send an event with custom code/type and any content to the other players in the same room. /// /// This override explicitly uses another parameter order to not mix it up with the implementation for Hashtable only. /// Identifies this type of event (and the content). Your game's event codes can start with 0. /// Any serializable datatype (including Hashtable like the other OpRaiseEvent overloads). /// If this event has to arrive reliably (potentially repeated if it's lost). /// Contains (slightly) less often used options. If you pass null, the default options will be used. /// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands. public virtual bool OpRaiseEvent(byte eventCode, object customEventContent, bool sendReliable, RaiseEventOptions raiseEventOptions) { if (this.loadBalancingPeer == null) { return false; } return this.loadBalancingPeer.OpRaiseEvent(eventCode, customEventContent, sendReliable, raiseEventOptions); } /// /// Operation to handle this client's interest groups (for events in room). /// /// /// Note the difference between passing null and byte[0]: /// null won't add/remove any groups. /// byte[0] will add/remove all (existing) groups. /// First, removing groups is executed. This way, you could leave all groups and join only the ones provided. /// /// Changes become active not immediately but when the server executes this operation (approximately RTT/2). /// /// Groups to remove from interest. Null will not remove any. A byte[0] will remove all. /// Groups to add to interest. Null will not add any. A byte[0] will add all current. /// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands. public virtual bool OpChangeGroups(byte[] groupsToRemove, byte[] groupsToAdd) { if (this.loadBalancingPeer == null) { return false; } return this.loadBalancingPeer.OpChangeGroups(groupsToRemove, groupsToAdd); } #endregion #region Helpers /// /// Privately used to read-out properties coming from the server in events and operation responses (which might be a bit tricky). /// private void ReadoutProperties(Hashtable gameProperties, Hashtable actorProperties, int targetActorNr) { // read game properties and cache them locally if (this.CurrentRoom != null && gameProperties != null) { this.CurrentRoom.InternalCacheProperties(gameProperties); } if (actorProperties != null && actorProperties.Count > 0) { if (targetActorNr > 0) { // we have a single entry in the actorProperties with one user's name // targets MUST exist before you set properties Player target = this.CurrentRoom.GetPlayer(targetActorNr); if (target != null) { Hashtable props = this.ReadoutPropertiesForActorNr(actorProperties, targetActorNr); target.InternalCacheProperties(props); //SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerPropertiesChanged, target, props); } } else { // in this case, we've got a key-value pair per actor (each // value is a hashtable with the actor's properties then) int actorNr; Hashtable props; string newName; Player target; foreach (object key in actorProperties.Keys) { actorNr = (int)key; props = (Hashtable)actorProperties[key]; newName = (string)props[ActorProperties.PlayerName]; target = this.CurrentRoom.GetPlayer(actorNr); if (target == null) { target = this.CreatePlayer(newName, actorNr, false, props); this.CurrentRoom.StorePlayer(target); } target.InternalCacheProperties(props); //SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerPropertiesChanged, target, props); } } } } /// /// Privately used only to read properties for a distinct actor (which might be the hashtable OR a key-pair value IN the actorProperties). /// private Hashtable ReadoutPropertiesForActorNr(Hashtable actorProperties, int actorNr) { if (actorProperties.ContainsKey(actorNr)) { return (Hashtable)actorProperties[actorNr]; } return actorProperties; } /// /// Internally used to set the LocalPlayer's ID (from -1 to the actual in-room ID). /// /// New actor ID (a.k.a actorNr) assigned when joining a room. protected internal void ChangeLocalID(int newID) { if (this.LocalPlayer == null) { this.DebugReturn(DebugLevel.WARNING, string.Format("Local actor is null or not in mActors! mLocalActor: {0} mActors==null: {1} newID: {2}", this.LocalPlayer, this.CurrentRoom.Players == null, newID)); } if (this.CurrentRoom == null) { // change to new actor/player ID and make sure the player does not have a room reference left this.LocalPlayer.ChangeLocalID(newID); this.LocalPlayer.RoomReference = null; } else { // remove old actorId from actor list this.CurrentRoom.RemovePlayer(this.LocalPlayer); // change to new actor/player ID this.LocalPlayer.ChangeLocalID(newID); // update the room's list with the new reference this.CurrentRoom.StorePlayer(this.LocalPlayer); } } /// /// Internally used to clean up local instances of players and room. /// private void CleanCachedValues() { this.ChangeLocalID(-1); this.isFetchingFriendList = false; // if this is called on the gameserver, we clean the room we were in. on the master, we keep the room to get into it if (this.Server == ServerConnection.GameServer || this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated) { this.CurrentRoom = null; // players get cleaned up inside this, too, except LocalPlayer (which we keep) } // when we leave the master, we clean up the rooms list (which might be updated by the lobby when we join again) if (this.Server == ServerConnection.MasterServer || this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated) { this.RoomInfoList.Clear(); } } /// /// Called internally, when a game was joined or created on the game server successfully. /// /// /// This reads the response, finds out the local player's actorNumber (a.k.a. Player.ID) and applies properties of the room and players. /// Errors for these operations are to be handled before this method is called. /// /// Contains the server's response for an operation called by this peer. private void GameEnteredOnGameServer(OperationResponse operationResponse) { this.CurrentRoom = this.CreateRoom(this.enterRoomParamsCache.RoomName, this.enterRoomParamsCache.RoomOptions); this.CurrentRoom.LoadBalancingClient = this; this.CurrentRoom.IsLocalClientInside = true; // first change the local id, instead of first updating the actorList since actorList uses ID to update itself // the local player's actor-properties are not returned in join-result. add this player to the list int localActorNr = (int)operationResponse[ParameterCode.ActorNr]; this.ChangeLocalID(localActorNr); if (operationResponse.Parameters.ContainsKey(ParameterCode.ActorList)) { int[] actorsInRoom = (int[])operationResponse.Parameters[ParameterCode.ActorList]; this.UpdatedActorList(actorsInRoom); } Hashtable actorProperties = (Hashtable)operationResponse[ParameterCode.PlayerProperties]; Hashtable gameProperties = (Hashtable)operationResponse[ParameterCode.GameProperties]; this.ReadoutProperties(gameProperties, actorProperties, 0); this.State = ClientState.Joined; switch (operationResponse.OperationCode) { case OperationCode.CreateGame: // TODO: add callback "game created" break; case OperationCode.JoinGame: case OperationCode.JoinRandomGame: // "game joined" should be called in another place (the join ev contains important info for the room). break; } } private void UpdatedActorList(int[] actorsInGame) { if (actorsInGame != null) { foreach (int userId in actorsInGame) { Player target = this.CurrentRoom.GetPlayer(userId); if (target == null) { this.CurrentRoom.StorePlayer(this.CreatePlayer(string.Empty, userId, false, null)); } } } } /// /// Factory method to create a player instance - override to get your own player-type with custom features. /// /// The name of the player to be created. /// The player ID (a.k.a. actorNumber) of the player to be created. /// Sets the distinction if the player to be created is your player or if its assigned to someone else. /// The custom properties for this new player /// The newly created player protected internal virtual Player CreatePlayer(string actorName, int actorNumber, bool isLocal, Hashtable actorProperties) { Player newPlayer = new Player(actorName, actorNumber, isLocal, actorProperties); return newPlayer; } /// Internal "factory" method to create a room-instance. protected internal virtual Room CreateRoom(string roomName, RoomOptions opt) { Room r = new Room(roomName, opt); return r; } private byte[] encryptionSecret; #endregion #region Implementation of IPhotonPeerListener /// Debug output of low level api (and this client). /// This method is not responsible to keep up the state of a LoadBalancingClient. Calling base.DebugReturn on overrides is optional. public virtual void DebugReturn(DebugLevel level, string message) { #if !UNITY Debug.WriteLine(message); #else if (level == DebugLevel.ERROR) { Debug.LogError(message); } else if (level == DebugLevel.WARNING) { Debug.LogWarning(message); } else if (level == DebugLevel.INFO) { Debug.Log(message); } else if (level == DebugLevel.ALL) { Debug.Log(message); } #endif } /// /// Uses the OperationResponses provided by the server to advance the internal state and call ops as needed. /// /// /// When this method finishes, it will call your OnOpResponseAction (if any). This way, you can get any /// operation response without overriding this class. /// /// To implement a more complex game/app logic, you should implement your own class that inherits the /// LoadBalancingClient. Override this method to use your own operation-responses easily. /// /// This method is essential to update the internal state of a LoadBalancingClient, so overriding methods /// must call base.OnOperationResponse(). /// /// Contains the server's response for an operation called by this peer. public virtual void OnOperationResponse(OperationResponse operationResponse) { // if (operationResponse.ReturnCode != 0) this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull()); // use the "secret" or "token" whenever we get it. doesn't really matter if it's in AuthResponse. if (operationResponse.Parameters.ContainsKey(ParameterCode.Secret)) { if (this.AuthValues == null) { this.AuthValues = new AuthenticationValues(); //this.DebugReturn(DebugLevel.ERROR, "Server returned secret. Created AuthValues."); } this.AuthValues.Token = operationResponse[ParameterCode.Secret] as string; } switch (operationResponse.OperationCode) { case OperationCode.Authenticate: case OperationCode.AuthenticateOnce: { if (operationResponse.ReturnCode != 0) { this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull() + " Server: " + this.Server + " Address: " + this.loadBalancingPeer.ServerAddress); switch (operationResponse.ReturnCode) { case ErrorCode.InvalidAuthentication: this.DisconnectedCause = DisconnectCause.InvalidAuthentication; break; case ErrorCode.CustomAuthenticationFailed: this.DisconnectedCause = DisconnectCause.CustomAuthenticationFailed; break; case ErrorCode.InvalidRegion: this.DisconnectedCause = DisconnectCause.InvalidRegion; break; case ErrorCode.MaxCcuReached: this.DisconnectedCause = DisconnectCause.MaxCcuReached; break; case ErrorCode.OperationNotAllowedInCurrentState: this.DisconnectedCause = DisconnectCause.OperationNotAllowedInCurrentState; break; } this.State = ClientState.Disconnecting; this.Disconnect(); break; // if auth didn't succeed, we disconnect (above) and exit this operation's handling } if (this.Server == ServerConnection.NameServer || this.Server == ServerConnection.MasterServer) { if (operationResponse.Parameters.ContainsKey(ParameterCode.UserId)) { string incomingId = (string)operationResponse.Parameters[ParameterCode.UserId]; if (!string.IsNullOrEmpty(incomingId)) { this.UserId = incomingId; this.DebugReturn(DebugLevel.INFO, string.Format("Received your UserID from server. Updating local value to: {0}", this.UserId)); } } if (operationResponse.Parameters.ContainsKey(ParameterCode.NickName)) { this.NickName = (string)operationResponse.Parameters[ParameterCode.NickName]; this.DebugReturn(DebugLevel.INFO, string.Format("Received your NickName from server. Updating local value to: {0}", this.NickName)); } if (operationResponse.Parameters.ContainsKey(ParameterCode.EncryptionData)) { this.SetupEncryption((Dictionary)operationResponse.Parameters[ParameterCode.EncryptionData]); } } if (this.Server == ServerConnection.NameServer) { // on the NameServer, authenticate returns the MasterServer address for a region and we hop off to there this.MasterServerAddress = operationResponse[ParameterCode.Address] as string; if (this.AuthMode == AuthModeOption.AuthOnceWss) { this.DebugReturn(DebugLevel.INFO, string.Format("Due to AuthOnceWss, switching TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol)); this.loadBalancingPeer.TransportProtocol = this.ExpectedProtocol; } this.DisconnectToReconnect(); } else if (this.Server == ServerConnection.MasterServer) { this.State = ClientState.ConnectedToMasterserver; if (this.AuthMode != AuthModeOption.Auth) { this.loadBalancingPeer.OpSettings(this.EnableLobbyStatistics); } if (this.AutoJoinLobby) { this.loadBalancingPeer.OpJoinLobby(this.CurrentLobby); } } else if (this.Server == ServerConnection.GameServer) { this.State = ClientState.Joining; this.enterRoomParamsCache.PlayerProperties = this.LocalPlayer.AllProperties; this.enterRoomParamsCache.OnGameServer = true; if (this.lastJoinType == JoinType.JoinRoom || this.lastJoinType == JoinType.JoinRandomRoom || this.lastJoinType == JoinType.JoinOrCreateRoom) { this.loadBalancingPeer.OpJoinRoom(this.enterRoomParamsCache); } else if (this.lastJoinType == JoinType.CreateRoom) { this.loadBalancingPeer.OpCreateRoom(this.enterRoomParamsCache); } break; } break; } case OperationCode.GetRegions: this.AvailableRegions = operationResponse[ParameterCode.Region] as string[]; this.AvailableRegionsServers = operationResponse[ParameterCode.Address] as string[]; break; case OperationCode.JoinRandomGame: // this happens only on the master server. on gameserver this is a "regular" join case OperationCode.CreateGame: case OperationCode.JoinGame: if (operationResponse.ReturnCode != 0) { //PhotonNetworkingMessage callback = PhotonNetworkingMessage.OnPhotonRandomJoinFailed; //if (operationResponse.OperationCode == OperationCode.CreateGame) callback = PhotonNetworkingMessage.OnPhotonCreateRoomFailed; //if (operationResponse.OperationCode == OperationCode.JoinGame) callback = PhotonNetworkingMessage.OnPhotonJoinRoomFailed; //SendMonoMessage(callback, operationResponse.ReturnCode, operationResponse.DebugMessage); if (this.Server == ServerConnection.GameServer) { this.DisconnectToReconnect(); } else { this.State = (this.inLobby) ? ClientState.JoinedLobby : ClientState.ConnectedToMasterserver; } } else { if (this.Server == ServerConnection.GameServer) { this.GameEnteredOnGameServer(operationResponse); } else { this.GameServerAddress = (string) operationResponse[ParameterCode.Address]; string roomName = operationResponse[ParameterCode.RoomName] as string; if (!string.IsNullOrEmpty(roomName)) { this.enterRoomParamsCache.RoomName = roomName; } this.DisconnectToReconnect(); } } break; case OperationCode.GetGameList: if (operationResponse.ReturnCode != 0) { this.DebugReturn(DebugLevel.ERROR, "GetGameList failed: " + operationResponse.ToStringFull()); break; } this.RoomInfoList = new Dictionary(); Hashtable games = (Hashtable)operationResponse[ParameterCode.GameList]; foreach (string gameName in games.Keys) { RoomInfo game = new RoomInfo(gameName, (Hashtable)games[gameName]); this.RoomInfoList[gameName] = game; } // TODO: OnRoomListUpdate break; case OperationCode.JoinLobby: this.State = ClientState.JoinedLobby; this.inLobby = true; // TODO: OnJoinedLobby break; case OperationCode.LeaveLobby: this.State = ClientState.ConnectedToMasterserver; this.inLobby = false; break; case OperationCode.Leave: //this.CleanCachedValues(); // this is done in status change on "disconnect" this.DisconnectToReconnect(); break; case OperationCode.FindFriends: if (operationResponse.ReturnCode != 0) { this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed: " + operationResponse.ToStringFull()); this.isFetchingFriendList = false; break; } bool[] onlineList = operationResponse[ParameterCode.FindFriendsResponseOnlineList] as bool[]; string[] roomList = operationResponse[ParameterCode.FindFriendsResponseRoomIdList] as string[]; List friendList = new List(this.friendListRequested.Length); for (int index = 0; index < this.friendListRequested.Length; index++) { FriendInfo friend = new FriendInfo(); friend.Name = this.friendListRequested[index]; friend.Room = roomList[index]; friend.IsOnline = onlineList[index]; friendList.Insert(index, friend); } this.FriendList = friendList; this.friendListRequested = null; this.isFetchingFriendList = false; this.friendListTimestamp = Environment.TickCount; if (this.friendListTimestamp == 0) { this.friendListTimestamp = 1; // makes sure the timestamp is not accidentally 0 } break; } if (this.OnOpResponseAction != null) this.OnOpResponseAction(operationResponse); } /// /// Uses the connection's statusCodes to advance the internal state and call operations as needed. /// /// This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnStatusChanged. public virtual void OnStatusChanged(StatusCode statusCode) { switch (statusCode) { case StatusCode.Connect: this.inLobby = false; if (this.State == ClientState.ConnectingToNameServer) { if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL) { this.DebugReturn(DebugLevel.ALL, "Connected to nameserver."); } this.Server = ServerConnection.NameServer; if (this.AuthValues != null) { this.AuthValues.Token = null; // when connecting to NameServer, invalidate the secret (only) } } if (this.State == ClientState.ConnectingToGameserver) { if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL) { this.DebugReturn(DebugLevel.ALL, "Connected to gameserver."); } this.Server = ServerConnection.GameServer; } if (this.State == ClientState.ConnectingToMasterserver) { if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL) { this.DebugReturn(DebugLevel.ALL, "Connected to masterserver."); } this.Server = ServerConnection.MasterServer; } if (this.loadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure) { if (this.Server == ServerConnection.NameServer || this.AuthMode == AuthModeOption.Auth) { this.loadBalancingPeer.EstablishEncryption(); } } else { goto case StatusCode.EncryptionEstablished; } break; case StatusCode.EncryptionEstablished: // on nameserver, the "process" is stopped here, so the developer/game can either get regions or authenticate with a specific region if (this.Server == ServerConnection.NameServer) { this.State = ClientState.ConnectedToNameServer; // TODO: should we automatically get the regions?! } if (this.Server != ServerConnection.NameServer && (this.AuthMode == AuthModeOption.AuthOnce || this.AuthMode == AuthModeOption.AuthOnceWss)) { // AuthMode "Once" means we only authenticate on the NameServer break; } // on any other server we might now have to authenticate still, so the client can do anything at all if (!this.didAuthenticate && (!this.IsUsingNameServer || this.CloudRegion != null)) { // once encryption is availble, the client should send one (secure) authenticate. it includes the AppId (which identifies your app on the Photon Cloud) this.didAuthenticate = this.CallAuthenticate(); if (this.didAuthenticate) { this.State = ClientState.Authenticating; } else { this.DebugReturn(DebugLevel.ERROR, "Error calling OpAuthenticate! Did not work. Check log output, AuthValues and if you're connected. State: " + this.State); } } break; case StatusCode.Disconnect: // disconnect due to connection exception is handled below (don't connect to GS or master in that case) this.CleanCachedValues(); this.didAuthenticate = false; // on connect, we know that we didn't this.inLobby = false; switch (this.State) { case ClientState.PeerCreated: case ClientState.Disconnecting: if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.State = ClientState.Disconnected; break; case ClientState.DisconnectingFromGameserver: case ClientState.DisconnectingFromNameServer: this.Connect(); // this gets the client back to the Master Server break; case ClientState.DisconnectingFromMasterserver: this.ConnectToGameServer(); // this connects the client with the Game Server (when joining/creating a room) break; default: string stacktrace = ""; #if DEBUG && !NETFX_CORE stacktrace = new System.Diagnostics.StackTrace(true).ToString(); #endif this.DebugReturn(DebugLevel.WARNING, "Got a unexpected Disconnect in LoadBalancingClient State: " + this.State + ". Server: " + this.Server+ " Trace: " + stacktrace); if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.State = ClientState.Disconnected; break; } break; case StatusCode.DisconnectByServerUserLimit: this.DebugReturn(DebugLevel.ERROR, "The Photon license's CCU Limit was reached. Server rejected this connection. Wait and re-try."); if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.DisconnectedCause = DisconnectCause.DisconnectByServerUserLimit; this.State = ClientState.Disconnected; break; case StatusCode.ExceptionOnConnect: case StatusCode.SecurityExceptionOnConnect: if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.DisconnectedCause = DisconnectCause.ExceptionOnConnect; this.State = ClientState.Disconnected; break; case StatusCode.DisconnectByServer: if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.DisconnectedCause = DisconnectCause.DisconnectByServer; this.State = ClientState.Disconnected; break; case StatusCode.DisconnectByServerLogic: if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.DisconnectedCause = DisconnectCause.DisconnectByServerLogic; this.State = ClientState.Disconnected; break; case StatusCode.TimeoutDisconnect: if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.DisconnectedCause = DisconnectCause.TimeoutDisconnect; this.State = ClientState.Disconnected; break; case StatusCode.Exception: case StatusCode.ExceptionOnReceive: if (this.AuthValues != null) { this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) } this.DisconnectedCause = DisconnectCause.Exception; this.State = ClientState.Disconnected; break; } } /// /// Uses the photonEvent's provided by the server to advance the internal state and call ops as needed. /// /// This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnEvent. public virtual void OnEvent(EventData photonEvent) { int actorNr = 0; Player originatingPlayer = null; if (photonEvent.Parameters.ContainsKey(ParameterCode.ActorNr)) { actorNr = (int) photonEvent[ParameterCode.ActorNr]; if (this.CurrentRoom != null) { originatingPlayer = this.CurrentRoom.GetPlayer(actorNr); } } switch (photonEvent.Code) { case EventCode.GameList: case EventCode.GameListUpdate: if (photonEvent.Code == EventCode.GameList) { this.RoomInfoList = new Dictionary(); } Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList]; foreach (string gameName in games.Keys) { RoomInfo game = new RoomInfo(gameName, (Hashtable)games[gameName]); if (game.removedFromList) { this.RoomInfoList.Remove(gameName); } else { this.RoomInfoList[gameName] = game; } } //SendMonoMessage(PhotonNetworkingMessage.OnReceivedRoomListUpdate); break; case EventCode.Join: Hashtable actorProperties = (Hashtable)photonEvent[ParameterCode.PlayerProperties]; if (originatingPlayer == null) { Player newPlayer = this.CreatePlayer(string.Empty, actorNr, false, actorProperties); this.CurrentRoom.StorePlayer(newPlayer); } else { originatingPlayer.InternalCacheProperties(actorProperties); originatingPlayer.IsInactive = false; } if (actorNr == this.LocalPlayer.ID) { // in this player's own join event, we get a complete list of players in the room, so check if we know each of the int[] actorsInRoom = (int[])photonEvent[ParameterCode.ActorList]; this.UpdatedActorList(actorsInRoom); } break; case EventCode.Leave: bool isInactive = false; if (photonEvent.Parameters.ContainsKey(ParameterCode.IsInactive)) { isInactive = (bool)photonEvent.Parameters[ParameterCode.IsInactive]; } if (isInactive) { originatingPlayer.IsInactive = true; } else { this.CurrentRoom.RemovePlayer(actorNr); } if (photonEvent.Parameters.ContainsKey(ParameterCode.MasterClientId)) { int newMaster = (int)photonEvent[ParameterCode.MasterClientId]; if (newMaster != 0) { this.CurrentRoom.masterClientId = newMaster; } } break; case EventCode.PropertiesChanged: // whenever properties are sent in-room, they can be broadcasted as event (which we handle here) // we get PLAYERproperties if actorNr > 0 or ROOMproperties if actorNumber is not set or 0 int targetActorNr = 0; if (photonEvent.Parameters.ContainsKey(ParameterCode.TargetActorNr)) { targetActorNr = (int)photonEvent[ParameterCode.TargetActorNr]; } Hashtable gameProperties = null; Hashtable actorProps = null; if (targetActorNr == 0) { gameProperties = (Hashtable)photonEvent[ParameterCode.Properties]; } else { actorProps = (Hashtable)photonEvent[ParameterCode.Properties]; } this.ReadoutProperties(gameProperties, actorProps, targetActorNr); break; case EventCode.AppStats: // only the master server sends these in (1 minute) intervals this.PlayersInRoomsCount = (int)photonEvent[ParameterCode.PeerCount]; this.RoomsCount = (int)photonEvent[ParameterCode.GameCount]; this.PlayersOnMasterCount = (int)photonEvent[ParameterCode.MasterPeerCount]; break; case EventCode.LobbyStats: string[] names = photonEvent[ParameterCode.LobbyName] as string[]; byte[] types = photonEvent[ParameterCode.LobbyType] as byte[]; int[] peers = photonEvent[ParameterCode.PeerCount] as int[]; int[] rooms = photonEvent[ParameterCode.GameCount] as int[]; this.lobbyStatistics.Clear(); for (int i = 0; i < names.Length; i++) { TypedLobbyInfo info = new TypedLobbyInfo(); info.Name = names[i]; info.Type = (LobbyType)types[i]; info.PlayerCount = peers[i]; info.RoomCount = rooms[i]; this.lobbyStatistics.Add(info); } //SendMonoMessage(PhotonNetworkingMessage.OnLobbyStatisticsUpdate); break; case EventCode.ErrorInfo: if (this.OnEventAction != null) { this.OnEventAction(photonEvent); } break; case EventCode.AuthEvent: if (this.AuthValues == null) { this.AuthValues = new AuthenticationValues(); } this.AuthValues.Token = photonEvent[ParameterCode.Secret] as string; break; } if (this.OnEventAction != null) this.OnEventAction(photonEvent); } /// In Photon 4, "raw messages" will get their own callback method in the interface. Not used yet. public virtual void OnMessage(object message) { this.DebugReturn(DebugLevel.ALL, string.Format("got OnMessage {0}", message)); } #endregion private void SetupEncryption(Dictionary encryptionData) { var mode = (EncryptionMode)(byte)encryptionData[EncryptionDataParameters.Mode]; switch (mode) { case EncryptionMode.PayloadEncryption: byte[] encryptionSecret = (byte[])encryptionData[EncryptionDataParameters.Secret1]; this.loadBalancingPeer.InitPayloadEncryption(encryptionSecret); break; case EncryptionMode.DatagramEncryption: { byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1]; byte[] secret2 = (byte[])encryptionData[EncryptionDataParameters.Secret2]; this.loadBalancingPeer.InitDatagramEncryption(secret1, secret2); } break; default: throw new ArgumentOutOfRangeException(); } } /// /// This operation makes Photon call your custom web-service by path/name with the given parameters (converted into Json). /// /// /// A WebRPC calls a custom, http-based function on a server you provide. The uriPath is relative to a "base path" /// which is configured server-side. The sent parameters get converted from C# types to Json. Vice versa, the response /// of the web-service will be converted to C# types and sent back as normal operation response. /// /// /// To use this feature, you have to setup your server: /// /// For a Photon Cloud application, /// visit the Dashboard and setup "WebHooks". The BaseUrl is used for WebRPCs as well. /// /// /// The response by Photon will call OnOperationResponse() with Code: OperationCode.WebRpc. /// To get this response, you can derive the LoadBalancingClient, or (much easier) you set a suitable /// OnOpResponseAction to be called. /// /// /// It's important to understand that the OperationResponse tells you if the WebRPC could be called or not /// but the content of the response will contain the values the web-service sent (if any). /// If the web-service could not execute the request, it might return another error and a message. This is /// inside the OperationResponse. /// /// The class WebRpcResponse is a helper-class that extracts the most valuable content from the WebRPC /// response. /// /// /// To get a WebRPC response, set a OnOpResponseAction: /// /// this.OnOpResponseAction = this.OpResponseHandler; /// /// It could look like this: /// /// public void OpResponseHandler(OperationResponse operationResponse) /// { /// if (operationResponse.OperationCode == OperationCode.WebRpc) /// { /// if (operationResponse.ReturnCode != 0) /// { /// Console.WriteLine("WebRpc failed. Response: " + operationResponse.ToStringFull()); /// } /// else /// { /// WebRpcResponse webResponse = new WebRpcResponse(operationResponse); /// Console.WriteLine(webResponse.DebugMessage); // message from the webserver /// /// // do something with the response... /// } /// } /// } /// /// The url path to call, relative to the baseUrl configured on Photon's server-side. /// The parameters to send to the web-service method. /// Defines if the authentication cookie gets sent to a WebHook (if setup). public bool OpWebRpc(string uriPath, object parameters, bool sendAuthCookie = false) { Dictionary opParameters = new Dictionary(); opParameters.Add(ParameterCode.UriPath, uriPath); opParameters.Add(ParameterCode.WebRpcParameters, parameters); if (sendAuthCookie) { opParameters.Add(ParameterCode.EventForward, WebFlags.SendAuthCookieConst); } return this.loadBalancingPeer.OpCustom(OperationCode.WebRpc, opParameters, true); } } }