// ----------------------------------------------------------------------- // // Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH // // // Provides the operations and a state for games using the // Photon LoadBalancing server. // // developer@photonengine.com // ---------------------------------------------------------------------------- #if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER #define SUPPORTED_UNITY #endif namespace Photon.Realtime { using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using ExitGames.Client.Photon; #if SUPPORTED_UNITY using UnityEngine; using Debug = UnityEngine.Debug; #endif #if SUPPORTED_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, /// Not Used. Authenticated, /// The client sent an OpJoinLobby and if this was done on the Master Server, it will result in. Depending on the lobby, it gets room listings. JoiningLobby, /// 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, [Obsolete("Renamed to DisconnectingFromMasterServer")] DisconnectingFromMasterserver = DisconnectingFromMasterServer, /// Transition to GameServer (client authenticates and joins/creates a room). ConnectingToGameServer, [Obsolete("Renamed to ConnectingToGameServer")] ConnectingToGameserver = ConnectingToGameServer, /// Connected to GameServer (going to auth and join game). ConnectedToGameServer, [Obsolete("Renamed to ConnectedToGameServer")] ConnectedToGameserver = 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, [Obsolete("Renamed to DisconnectingFromGameServer")] DisconnectingFromGameserver = DisconnectingFromGameServer, /// Connecting to MasterServer (includes sending authentication values). ConnectingToMasterServer, [Obsolete("Renamed to ConnectingToMasterServer.")] ConnectingToMasterserver = 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, [Obsolete("Renamed to ConnectedToMasterServer.")] ConnectedToMasterserver = ConnectedToMasterServer, [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 encryption already. You should call OpGetRegions or ConnectToRegionMaster. ConnectedToNameServer, /// Clients disconnects (specifically) from the NameServer (usually to connect to the MasterServer). DisconnectingFromNameServer, /// Client was unable to connect to Name Server and will attempt to connect with an alternative network protocol (TCP). ConnectWithFallbackProtocol } /// /// 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, /// Done on Master Server and (if successful) followed by a Join or Create on Game Server. JoinRandomOrCreateRoom, /// Client is either joining or creating a room. On Master- and Game-Server. JoinOrCreateRoom } /// Enumeration 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 server is not available or the address is wrong. Make sure the port is provided and the server is up. ExceptionOnConnect, /// OnStatusChanged: Dns resolution for a hostname failed. The exception for this is being catched and logged with error level. DnsExceptionOnConnect, /// OnStatusChanged: The server address was parsed as IPv4 illegally. An illegal address would be e.g. 192.168.1.300. IPAddress.TryParse() will let this pass but our check won't. ServerAddressInvalid, /// OnStatusChanged: Some internal exception caused the socket code to fail. This may happen if you attempt to connect locally but the server is not available. In doubt: Contact Exit Games. Exception, /// OnStatusChanged: The server disconnected this client due to timing out (missing acknowledgement from the client). ServerTimeout, /// OnStatusChanged: This client detected that the server's responses are not received in due time. ClientTimeout, /// OnStatusChanged: The server disconnected this client from within the room's logic (the C# code). DisconnectByServerLogic, /// OnStatusChanged: The server disconnected this client for unknown reasons. DisconnectByServerReasonUnknown, /// OnOperationResponse: Authenticate in the Photon Cloud with invalid AppId. Update your subscription or contact Exit Games. InvalidAuthentication, /// OnOperationResponse: Authenticate in the Photon Cloud with invalid client values or custom authentication setup in Cloud Dashboard. CustomAuthenticationFailed, /// The authentication ticket should provide access to any Photon Cloud server without doing another authentication-service call. However, the ticket expired. AuthenticationTicketExpired, /// 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, /// OnStatusChanged: The client disconnected from within the logic (the C# code). DisconnectByClientLogic, /// The client called an operation too frequently and got disconnected due to hitting the OperationLimit. This triggers a client-side disconnect, too. /// To protect the server, some operations have a limit. When an OperationResponse fails with ErrorCode.OperationLimitReached, the client disconnects. DisconnectByOperationLimit, /// The client received a "Disconnect Message" from the server. Check the debug logs for details. DisconnectByDisconnectMessage } /// 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 which sort of app the LoadBalancingClient is used for: Realtime or Voice. public enum ClientAppType { /// Realtime apps are for gaming / interaction. Also used by PUN 2. Realtime, /// Voice apps stream audio. Voice, /// Fusion clients are for matchmaking and relay in Photon Fusion. Fusion } /// /// 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 unavailable. /// DatagramEncryption = 10, /// /// With this encryption mode for UDP, the connection gets setup with random sequence numbers and all further datagrams get encrypted almost entirely. On-demand message encryption (like in PayloadEncryption) is unavailable. /// DatagramEncryptionRandomSequence = 11, ///// ///// Same as above except that GCM mode is used to encrypt data. ///// //DatagramEncryptionGCMRandomSequence = 12, /// /// Datagram Encryption with GCM. /// DatagramEncryptionGCM = 13, } /// Container for port definitions. public struct PhotonPortDefinition { public static readonly PhotonPortDefinition AlternativeUdpPorts = new PhotonPortDefinition() { NameServerPort = 27000, MasterServerPort = 27001, GameServerPort = 27002}; /// Typical ports: UDP: 5058 or 27000, TCP: 4533, WSS: 19093 or 443. public ushort NameServerPort; /// Typical ports: UDP: 5056 or 27002, TCP: 4530, WSS: 19090 or 443. public ushort MasterServerPort; /// Typical ports: UDP: 5055 or 27001, TCP: 4531, WSS: 19091 or 443. public ushort GameServerPort; } #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 { get; private set; } /// /// Gets or sets the binary protocol version used by this client /// /// /// Use this always instead of setting it via /// () directly, especially when WSS protocol is used. /// public SerializationProtocol SerializationProtocol { get { return this.LoadBalancingPeer.SerializationProtocolType; } set { this.LoadBalancingPeer.SerializationProtocolType = value; } } /// 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; } /// The ClientAppType defines which sort of AppId should be expected. The LoadBalancingClient supports Realtime and Voice app types. Default: Realtime. public ClientAppType ClientType { get; set; } /// User authentication values to be sent to the Photon server right after connecting. /// 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; /// Optionally contains a protocol which will be used on Master- and GameServer. /// /// When using AuthMode = AuthModeOption.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. /// /// This is nullable by design. In many cases, the protocol on the NameServer is not different from the other servers. /// If set, the operation AuthOnce will contain this value and the OpAuth response on the NameServer will execute a protocol switch. /// public ConnectionProtocol? ExpectedProtocol { get; set; } ///Simplifies getting the token for connect/init requests, if this feature is enabled. private object TokenForInit { get { if (this.AuthMode == AuthModeOption.Auth) { return null; } return (this.AuthValues != null) ? this.AuthValues.Token : null; } } /// Internally used cache for the server's token. Identifies a user/session and can be used to rejoin. private object tokenCache; /// True if this client uses a NameServer to get the Master Server address. /// This value is public, despite being an internal value, which should only be set by this client. public bool IsUsingNameServer { get; set; } /// Name Server Host Name for Photon Cloud. Without port and without any prefix. public string NameServerHost = "ns.photonengine.io"; /// 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(); } } /// 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 } }; /// Replaced by ServerPortOverrides. [Obsolete("Set port overrides in ServerPortOverrides. Not used anymore!")] public bool UseAlternativeUdpPorts { get; set; } /// Defines overrides for server ports. Used per server-type if > 0. Important: You must change these when the protocol changes! /// /// Typical ports are listed in PhotonPortDefinition. /// /// Instead of using the port provided from the servers, the specified port is used (independent of the protocol). /// If a value is 0 (default), the port is not being replaced. /// /// Different protocols have different typical ports per server-type. /// https://doc.photonengine.com/en-us/pun/current/reference/tcp-and-udp-port-numbers /// /// In case of using the AuthMode AutOnceWss, the name server's protocol is wss, while udp or tcp will be used on the master server and game server. /// Set the ports accordingly per protocol and server. /// public PhotonPortDefinition ServerPortOverrides; /// Enables a fallback to another protocol in case a connect to the Name Server fails. /// /// When connecting to the Name Server fails for a first time, the client will select an alternative /// network protocol and re-try to connect. /// /// The fallback will use the default Name Server port as defined by ProtocolToNameServerPort. /// /// The fallback for TCP is UDP. All other protocols fallback to TCP. /// public bool EnableProtocolFallback { get; set; } /// 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; 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; } /// /// Defines a proxy URL for WebSocket connections. Can be the proxy or point to a .pac file. /// /// /// This URL supports various definitions: /// /// "user:pass@proxyaddress:port"
/// "proxyaddress:port"
/// "system:"
/// "pac:"
/// "pac:http://host/path/pacfile.pac"
/// /// Important: Don't define a protocol, except to point to a pac file. the proxy address should not begin with http:// or https://. ///
public string ProxyServerAddress; /// 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; } set { if (this.state == value) { return; } ClientState previousState = this.state; this.state = value; if (StateChanged != null) StateChanged(previousState, 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 is ready to send operations. /// /// /// Not all operations can be called on all types of servers. If an operation is unavailable on the currently connected server, /// this will result in a OperationResponse with ErrorCode != 0. /// /// Examples: 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). /// /// To check which server you are on, use: . /// public bool IsConnectedAndReady { get { if (this.LoadBalancingPeer == null) { return false; } switch (this.State) { case ClientState.PeerCreated: case ClientState.Disconnected: case ClientState.Disconnecting: case ClientState.DisconnectingFromGameServer: case ClientState.DisconnectingFromMasterServer: case ClientState.DisconnectingFromNameServer: 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 StateChanged; /// Register a method to be called when an event got dispatched. Gets called after the LoadBalancingClient handled the internal events first. /// /// This is an alternative to extending LoadBalancingClient to override OnEvent(). /// /// Note that OnEvent is calling EventReceived after it handled internal events first. /// 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 EventReceived; /// Register a method to be called when an operation response is received. /// /// 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 OpResponseReceived; /// Wraps up the target objects for a group of callbacks, so they can be called conveniently. /// By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks. public ConnectionCallbacksContainer ConnectionCallbackTargets; /// Wraps up the target objects for a group of callbacks, so they can be called conveniently. /// By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks. public MatchMakingCallbacksContainer MatchMakingCallbackTargets; /// Wraps up the target objects for a group of callbacks, so they can be called conveniently. /// By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks. internal InRoomCallbacksContainer InRoomCallbackTargets; /// Wraps up the target objects for a group of callbacks, so they can be called conveniently. /// By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks. internal LobbyCallbacksContainer LobbyCallbackTargets; /// Wraps up the target objects for a group of callbacks, so they can be called conveniently. /// By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks. internal WebRpcCallbacksContainer WebRpcCallbackTargets; /// Wraps up the target objects for a group of callbacks, so they can be called conveniently. /// By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks. internal ErrorInfoCallbacksContainer ErrorInfoCallbackTargets; /// 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. public bool InLobby { get { return this.State == ClientState.JoinedLobby; } } /// The lobby this client currently uses. Defined when joining a lobby or creating rooms public TypedLobby CurrentLobby { get; internal set; } /// /// If enabled, the client will get a list of available lobbies from the Master Server. /// /// /// Set this value before the client connects to the Master Server. While connected to the Master /// Server, a change has no effect. /// /// Implement OptionalInfoCallbacks.OnLobbyStatisticsUpdate, to get the list of used lobbies. /// /// The lobby statistics can be useful if your title dynamically uses lobbies, depending (e.g.) /// on current player activity or such. /// In this case, getting a list of available lobbies, their room-count and player-count can /// be useful info. /// /// ConnectUsingSettings sets this to the PhotonServerSettings value. /// public bool EnableLobbyStatistics; /// Internal lobby stats cache, used by LobbyStatistics. private readonly List lobbyStatistics = new List(); /// 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; } } /// The current room this client is connected to (null if none available). public Room CurrentRoom { get; set; } /// Is true while being in a room (this.state == ClientState.Joined). /// /// Aside from polling this value, game logic should implement IMatchmakingCallbacks in some class /// and react when that gets called.
/// OpRaiseEvent, OpLeave and some other operations can only be used (successfully) when the client is in a room.. ///
public bool InRoom { get { return this.state == ClientState.Joined && this.CurrentRoom != null; } } /// 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; /// Used when the client arrives on the GS, to join the room with the correct values. private EnterRoomParams enterRoomParamsCache; /// Used to cache a failed "enter room" operation on the Game Server, to return to the Master Server before calling a fail-callback. private OperationResponse failedRoomEntryOperation; /// Maximum of userIDs that can be sent in one friend list request. private const int FriendRequestListMax = 512; /// Contains the list of names of friends to look up their state on the server. private string[] friendListRequested; /// Internal flag to know if the client currently fetches a friend list. public bool IsFetchingFriendList { get { return this.friendListRequested != null; } } /// 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; } /// The cluster name provided by the Name Server. /// /// The value is provided by the OpResponse for OpAuthenticate/OpAuthenticateOnce. /// Default: null. This value only ever updates from the Name Server authenticate response. /// public string CurrentCluster { get; private set; } /// Contains the list if enabled regions this client may use. Null, unless the client got a response to OpGetRegions. public RegionHandler RegionHandler; /// Stores the best region summary of a previous session to speed up connecting. private string bestRegionSummaryFromStorage; /// Set when the best region pinging is done. public string SummaryToCache; /// Internal connection setting/flag. If the client should connect to the best region or not. /// /// It's set in the Connect...() methods. Only ConnectUsingSettings() sets it to true. /// If true, client will ping available regions and select the best. /// A bestRegionSummaryFromStorage can be used to cut the ping time short. /// private bool connectToBestRegion = true; /// Definition of parameters for encryption data (included in Authenticate operation response). private 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; } private class CallbackTargetChange { public readonly object Target; /// Add if true, remove if false. public readonly bool AddTarget; public CallbackTargetChange(object target, bool addTarget) { this.Target = target; this.AddTarget = addTarget; } } private readonly Queue callbackTargetChanges = new Queue(); private readonly HashSet callbackTargets = new HashSet(); /// 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.ConnectionCallbackTargets = new ConnectionCallbacksContainer(this); this.MatchMakingCallbackTargets = new MatchMakingCallbacksContainer(this); this.InRoomCallbackTargets = new InRoomCallbacksContainer(this); this.LobbyCallbackTargets = new LobbyCallbacksContainer(this); this.WebRpcCallbackTargets = new WebRpcCallbacksContainer(this); this.ErrorInfoCallbackTargets = new ErrorInfoCallbacksContainer(this); this.LoadBalancingPeer = new LoadBalancingPeer(this, protocol); this.LoadBalancingPeer.OnDisconnectMessage += this.OnDisconnectMessageReceived; this.SerializationProtocol = SerializationProtocol.GpBinaryV18; this.LocalPlayer = this.CreatePlayer(string.Empty, -1, true, null); //TODO: Check if we can do this later #if SUPPORTED_UNITY CustomTypesUnity.Register(); #endif #if UNITY_WEBGL if (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Tcp || this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Udp) { this.LoadBalancingPeer.Listener.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure."); this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; } #endif 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; } public int NameServerPortInAppSettings; /// /// 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); if (this.NameServerPortInAppSettings != 0) { this.DebugReturn(DebugLevel.INFO, string.Format("Using NameServerPortInAppSettings: {0}", this.NameServerPortInAppSettings)); protocolPort = this.NameServerPortInAppSettings; } if (this.ServerPortOverrides.NameServerPort > 0) { protocolPort = this.ServerPortOverrides.NameServerPort; } switch (this.LoadBalancingPeer.TransportProtocol) { case ConnectionProtocol.Udp: case ConnectionProtocol.Tcp: return string.Format("{0}:{1}", NameServerHost, protocolPort); 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 // needed connect variants: // connect to Name Server only (could include getregions) -> end after getregions // connect to Region Master via Name Server (specific region/cluster) -> no getregions! authenticates and ends after on connected to master // connect to Best Region via Name Server // connect to Master Server (no Name Server, no appid) public virtual bool ConnectUsingSettings(AppSettings appSettings) { if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected) { this.DebugReturn(DebugLevel.WARNING, "ConnectUsingSettings() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState); return false; } if (appSettings == null) { this.DebugReturn(DebugLevel.ERROR, "ConnectUsingSettings failed. The appSettings can't be null.'"); return false; } switch (this.ClientType) { case ClientAppType.Realtime: this.AppId = appSettings.AppIdRealtime; break; case ClientAppType.Voice: this.AppId = appSettings.AppIdVoice; break; case ClientAppType.Fusion: this.AppId = appSettings.AppIdFusion; break; } this.AppVersion = appSettings.AppVersion; this.IsUsingNameServer = appSettings.UseNameServer; this.CloudRegion = appSettings.FixedRegion; this.connectToBestRegion = string.IsNullOrEmpty(this.CloudRegion); this.EnableLobbyStatistics = appSettings.EnableLobbyStatistics; this.LoadBalancingPeer.DebugOut = appSettings.NetworkLogging; this.AuthMode = appSettings.AuthMode; if (appSettings.AuthMode == AuthModeOption.AuthOnceWss) { this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; this.ExpectedProtocol = appSettings.Protocol; } else { this.LoadBalancingPeer.TransportProtocol = appSettings.Protocol; this.ExpectedProtocol = null; } this.EnableProtocolFallback = appSettings.EnableProtocolFallback; this.bestRegionSummaryFromStorage = appSettings.BestRegionSummaryFromStorage; this.DisconnectedCause = DisconnectCause.None; this.CheckConnectSetupWebGl(); if (this.IsUsingNameServer) { this.Server = ServerConnection.NameServer; if (!appSettings.IsDefaultNameServer) { this.NameServerHost = appSettings.Server; } this.ProxyServerAddress = appSettings.ProxyServer; this.NameServerPortInAppSettings = appSettings.Port; if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit)) { return false; } this.State = ClientState.ConnectingToNameServer; } else { this.Server = ServerConnection.MasterServer; int portToUse = appSettings.IsDefaultPort ? 5055 : appSettings.Port; // TODO: setup new (default) port config this.MasterServerAddress = string.Format("{0}:{1}", appSettings.Server, portToUse); if (!this.LoadBalancingPeer.Connect(this.MasterServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit)) { return false; } this.State = ClientState.ConnectingToMasterServer; } return true; } [Obsolete("Use ConnectToMasterServer() instead.")] public bool Connect() { return this.ConnectToMasterServer(); } /// /// Starts the "process" to connect to a Master Server, using MasterServerAddress and AppId properties. /// /// /// To connect to the Photon Cloud, use ConnectUsingSettings() or ConnectToRegionMaster(). /// /// The process to connect includes several steps: the actual connecting, establishing encryption, authentification /// (of app and optionally the user) and connecting to the MasterServer /// /// 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) /// public virtual bool ConnectToMasterServer() { if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected) { this.DebugReturn(DebugLevel.WARNING, "ConnectToMasterServer() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState); return false; } // when using authMode AuthOnce or AuthOnceWSS, the token must be available for the init request. if it's null in that case, don't connect if (this.AuthMode != AuthModeOption.Auth && this.TokenForInit == null) { this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect to MasterServer with Token == null in AuthMode: " + this.AuthMode); return false; } this.CheckConnectSetupWebGl(); if (this.LoadBalancingPeer.Connect(this.MasterServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit)) { this.DisconnectedCause = DisconnectCause.None; this.connectToBestRegion = false; this.State = ClientState.ConnectingToMasterServer; this.Server = ServerConnection.MasterServer; 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() { if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected) { this.DebugReturn(DebugLevel.WARNING, "ConnectToNameServer() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState); return false; } this.IsUsingNameServer = true; this.CloudRegion = null; this.CheckConnectSetupWebGl(); if (this.AuthMode == AuthModeOption.AuthOnceWss) { if (this.ExpectedProtocol == null) { this.ExpectedProtocol = this.LoadBalancingPeer.TransportProtocol; } this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; } if (this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", this.TokenForInit)) { this.DisconnectedCause = DisconnectCause.None; this.connectToBestRegion = false; this.State = ClientState.ConnectingToNameServer; this.Server = ServerConnection.NameServer; return true; } return false; } /// /// Connects you to a specific region's Master Server, using the Name Server to find the IP. /// /// /// If the region is null or empty, no connection will be made. /// If the region (code) provided is not available, the connection process will fail on the Name Server. /// This method connects only to the region defined. No "Best Region" pinging will be done. /// /// If the region string does not contain a "/", this means no specific cluster is requested. /// To support "Sharding", the region gets a "/*" postfix in this case, to select a random cluster. /// /// If the operation could be sent. If false, no operation was sent. public bool ConnectToRegionMaster(string region) { if (string.IsNullOrEmpty(region)) { this.DebugReturn(DebugLevel.ERROR, "ConnectToRegionMaster() failed. The region can not be null or empty."); return false; } this.IsUsingNameServer = true; if (this.State == ClientState.Authenticating) { if (this.LoadBalancingPeer.DebugOut >= DebugLevel.INFO) { this.DebugReturn(DebugLevel.INFO, "ConnectToRegionMaster() will skip calling authenticate, as the current state is 'Authenticating'. Just wait for the result."); } return true; } if (this.State == ClientState.ConnectedToNameServer) { this.CloudRegion = region; bool authenticating = this.CallAuthenticate(); if (authenticating) { this.State = ClientState.Authenticating; } return authenticating; } this.LoadBalancingPeer.Disconnect(); if (!string.IsNullOrEmpty(region) && !region.Contains("/")) { region = region + "/*"; } this.CloudRegion = region; this.CheckConnectSetupWebGl(); if (this.AuthMode == AuthModeOption.AuthOnceWss) { if (this.ExpectedProtocol == null) { this.ExpectedProtocol = this.LoadBalancingPeer.TransportProtocol; } this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; } this.connectToBestRegion = false; this.DisconnectedCause = DisconnectCause.None; if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", null)) { return false; } this.State = ClientState.ConnectingToNameServer; this.Server = ServerConnection.NameServer; return true; } [Conditional("UNITY_WEBGL")] private void CheckConnectSetupWebGl() { #if UNITY_WEBGL if (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocket && this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure) { this.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure."); this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; } this.EnableProtocolFallback = false; // no fallback on WebGL #endif } /// /// Privately used only for reconnecting. /// private bool Connect(string serverAddress, string proxyServerAddress, ServerConnection serverType) { // TODO: Make sure app doesn't quit right now if (this.State == ClientState.Disconnecting) { this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect while disconnecting (still). Current state: " + this.State); return false; } // when using authMode AuthOnce or AuthOnceWSS, the token must be available for the init request. if it's null in that case, don't connect if (this.AuthMode != AuthModeOption.Auth && serverType != ServerConnection.NameServer && this.TokenForInit == null) { this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect to " + serverType + " with Token == null in AuthMode: " + this.AuthMode); return false; } // connect might fail, if the DNS name can't be resolved or if no network connection is available, etc. bool connecting = this.LoadBalancingPeer.Connect(serverAddress, proxyServerAddress, this.AppId, this.TokenForInit); if (connecting) { this.DisconnectedCause = DisconnectCause.None; this.Server = serverType; switch (serverType) { case ServerConnection.NameServer: State = ClientState.ConnectingToNameServer; break; case ServerConnection.MasterServer: State = ClientState.ConnectingToMasterServer; break; case ServerConnection.GameServer: State = ClientState.ConnectingToGameServer; break; } } return connecting; } /// Can be used to reconnect to the master server after a disconnect. /// Common use case: Press the Lock Button on a iOS device and you get disconnected immediately. public bool ReconnectToMaster() { if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected) { this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState); return false; } if (string.IsNullOrEmpty(this.MasterServerAddress)) { this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() failed. MasterServerAddress is null or empty."); return false; } if (this.tokenCache == null) { this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() failed. It seems the client doesn't have any previous authentication token to re-connect."); return false; } if (this.AuthValues == null) { this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() with AuthValues == null is not correct!"); this.AuthValues = new AuthenticationValues(); } this.AuthValues.Token = this.tokenCache; return this.Connect(this.MasterServerAddress, this.ProxyServerAddress, ServerConnection.MasterServer); } /// /// Can be used to return to a room quickly by directly reconnecting to a game server to rejoin a room. /// /// /// Rejoining room will not send any player properties. Instead client will receive up-to-date ones from server. /// If you want to set new player properties, do it once rejoined. /// /// False, if the conditions are not met. Then, this client does not attempt the ReconnectAndRejoin. public bool ReconnectAndRejoin() { if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected) { this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState); return false; } if (string.IsNullOrEmpty(this.GameServerAddress)) { this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client wasn't connected to a game server before (no address)."); return false; } if (this.enterRoomParamsCache == null) { this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client doesn't have any previous room to re-join."); return false; } if (this.tokenCache == null) { this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client doesn't have any previous authentication token to re-connect."); return false; } if (this.AuthValues == null) { this.AuthValues = new AuthenticationValues(); } this.AuthValues.Token = this.tokenCache; if (!string.IsNullOrEmpty(this.GameServerAddress) && this.enterRoomParamsCache != null) { this.lastJoinType = JoinType.JoinRoom; this.enterRoomParamsCache.JoinMode = JoinMode.RejoinOnly; return this.Connect(this.GameServerAddress, this.ProxyServerAddress, ServerConnection.GameServer); } return false; } /// Disconnects the peer from a server or stays disconnected. If the client / peer was connected, a callback will be triggered. /// /// Disconnect will attempt to notify the server of the client closing the connection. /// /// Clients that are in a room, will leave the room. If the room's playerTTL > 0, the player will just become inactive (and may rejoin). /// /// This method will not change the current State, if this client State is PeerCreated, Disconnecting or Disconnected. /// In those cases, there is also no callback for the disconnect. The DisconnectedCause will only change if the client was connected. /// public void Disconnect(DisconnectCause cause = DisconnectCause.DisconnectByClientLogic) { if (this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated) { this.DebugReturn(DebugLevel.INFO, "Disconnect() call gets skipped due to State " + this.State + ". DisconnectedCause: " + this.DisconnectedCause + " Parameter cause: " + cause); return; } if (this.State != ClientState.Disconnected) { this.State = ClientState.Disconnecting; this.DisconnectedCause = cause; this.LoadBalancingPeer.Disconnect(); } } /// /// 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(); } /// /// Useful to test loss of connection which will end in a client timeout. This modifies LoadBalancingPeer.NetworkSimulationSettings. Read remarks. /// /// /// Use with care as this sets LoadBalancingPeer.IsSimulationEnabled.
/// Read LoadBalancingPeer.IsSimulationEnabled to check if this is on or off, if needed.
/// /// If simulateTimeout is true, LoadBalancingPeer.NetworkSimulationSettings.IncomingLossPercentage and /// LoadBalancingPeer.NetworkSimulationSettings.OutgoingLossPercentage will be set to 100.
/// Obviously, this overrides any network simulation settings done before.
/// /// If you want fine-grained network simulation control, use the NetworkSimulationSettings.
/// /// The timeout will lead to a call to , as usual in a client timeout. /// /// You could modify this method (or use NetworkSimulationSettings) to deliberately run into a server timeout by /// just setting the OutgoingLossPercentage = 100 and the IncomingLossPercentage = 0. ///
/// If true, a connection loss is simulated. If false, the simulation ends. public void SimulateConnectionLoss(bool simulateTimeout) { this.DebugReturn(DebugLevel.WARNING, "SimulateConnectionLoss() set to: "+simulateTimeout); if (simulateTimeout) { this.LoadBalancingPeer.NetworkSimulationSettings.IncomingLossPercentage = 100; this.LoadBalancingPeer.NetworkSimulationSettings.OutgoingLossPercentage = 100; } this.LoadBalancingPeer.IsSimulationEnabled = simulateTimeout; } private bool CallAuthenticate() { if (this.IsUsingNameServer && this.Server != ServerConnection.NameServer && (this.AuthValues == null || this.AuthValues.Token == null)) { this.DebugReturn(DebugLevel.ERROR, "Authenticate without Token is only allowed on Name Server. Connecting to: " + this.Server + " on: " + this.CurrentServerAddress + ". State: " + this.State); return false; } if (this.AuthMode == AuthModeOption.Auth) { if (!this.CheckIfOpCanBeSent(OperationCode.Authenticate, this.Server, "Authenticate")) { return false; } return this.LoadBalancingPeer.OpAuthenticate(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, (this.EnableLobbyStatistics && this.Server == ServerConnection.MasterServer)); } else { if (!this.CheckIfOpCanBeSent(OperationCode.AuthenticateOnce, this.Server, "AuthenticateOnce")) { return false; } ConnectionProtocol targetProtocolPastNameServer = this.ExpectedProtocol != null ? (ConnectionProtocol) this.ExpectedProtocol : this.LoadBalancingPeer.TransportProtocol; return this.LoadBalancingPeer.OpAuthenticateOnce(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, this.EncryptionMode, targetProtocolPastNameServer); } } /// /// 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 (10..50 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(); } } /// /// 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). private bool OpGetRegions() { if (!this.CheckIfOpCanBeSent(OperationCode.GetRegions, this.Server, "GetRegions")) { return false; } bool sent = this.LoadBalancingPeer.OpGetRegions(this.AppId); 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)
///
/// The options may be used to define which state a room must match to be returned. ///
/// Array of friend's names (make sure they are unique). /// Options that affect the result of the FindFriends operation. /// If the operation could be sent (requires connection). public bool OpFindFriends(string[] friendsToFind, FindFriendsOptions options = null) { if (!this.CheckIfOpCanBeSent(OperationCode.FindFriends, this.Server, "FindFriends")) { return false; } if (this.IsFetchingFriendList) { this.DebugReturn(DebugLevel.WARNING, "OpFindFriends skipped: already fetching friends list."); return false; // fetching friends currently, so don't do it again (avoid changing the list while fetching friends) } if (friendsToFind == null || friendsToFind.Length == 0) { this.DebugReturn(DebugLevel.ERROR, "OpFindFriends skipped: friendsToFind array is null or empty."); return false; } if (friendsToFind.Length > FriendRequestListMax) { this.DebugReturn(DebugLevel.ERROR, string.Format("OpFindFriends skipped: friendsToFind array exceeds allowed length of {0}.", FriendRequestListMax)); return false; } List friendsList = new List(friendsToFind.Length); for (int i = 0; i < friendsToFind.Length; i++) { string friendUserId = friendsToFind[i]; if (string.IsNullOrEmpty(friendUserId)) { this.DebugReturn(DebugLevel.WARNING, string.Format( "friendsToFind array contains a null or empty UserId, element at position {0} skipped.", i)); } else if (friendUserId.Equals(UserId)) { this.DebugReturn(DebugLevel.WARNING, string.Format( "friendsToFind array contains local player's UserId \"{0}\", element at position {1} skipped.", friendUserId, i)); } else if (friendsList.Contains(friendUserId)) { this.DebugReturn(DebugLevel.WARNING, string.Format( "friendsToFind array contains duplicate UserId \"{0}\", element at position {1} skipped.", friendUserId, i)); } else { friendsList.Add(friendUserId); } } if (friendsList.Count == 0) { this.DebugReturn(DebugLevel.ERROR, "OpFindFriends skipped: friends list to find is empty."); return false; } string[] filteredArray = friendsList.ToArray(); bool sent = this.LoadBalancingPeer.OpFindFriends(filteredArray, options); this.friendListRequested = sent ? filteredArray : null; return sent; } /// If already connected to a Master Server, this joins the specified lobby. This request triggers an OnOperationResponse() call and the callback OnJoinedLobby(). /// The lobby to join. Use null for default lobby. /// If the operation could be sent. False, if the client is not IsConnectedAndReady or when it's not connected to a Master Server. public bool OpJoinLobby(TypedLobby lobby) { if (!this.CheckIfOpCanBeSent(OperationCode.JoinLobby, this.Server, "JoinLobby")) { return false; } if (lobby == null) { lobby = TypedLobby.Default; } bool sent = this.LoadBalancingPeer.OpJoinLobby(lobby); if (sent) { this.CurrentLobby = lobby; this.State = ClientState.JoiningLobby; } 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() { if (!this.CheckIfOpCanBeSent(OperationCode.LeaveLobby, this.Server, "LeaveLobby")) { return false; } return this.LoadBalancingPeer.OpLeaveLobby(); } /// /// Joins a random room that matches the filter. Will callback: OnJoinedRoom or OnJoinRandomFailed. /// /// /// Used for random matchmaking. You can join any room or one with specific properties defined in opJoinRandomRoomParams. /// /// 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). /// It may also fail when actually joining the room which was found. Rooms may close, become full or empty anytime. /// /// This method can only be called while the client is connected to a Master Server so you should /// implement the callback OnConnectedToMaster. /// Check the return value to make sure the operation will be called on the server. /// Note: There will be no callbacks if this method returned false. /// /// /// This client's State is set to ClientState.Joining immediately, when the operation could /// be called. In the background, the client will switch servers and call various related operations. /// /// When you're in the room, this client's State will become ClientState.Joined. /// /// /// When entering 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 are not wiped when leaving a room. /// /// More about matchmaking: /// https://doc.photonengine.com/en-us/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 definition of properties to filter rooms in random matchmaking. /// If the operation could be sent currently (requires connection to Master Server). public bool OpJoinRandomRoom(OpJoinRandomRoomParams opJoinRandomRoomParams = null) { if (!this.CheckIfOpCanBeSent(OperationCode.JoinRandomGame, this.Server, "JoinRandomGame")) { return false; } if (opJoinRandomRoomParams == null) { opJoinRandomRoomParams = new OpJoinRandomRoomParams(); } this.enterRoomParamsCache = new EnterRoomParams(); this.enterRoomParamsCache.Lobby = opJoinRandomRoomParams.TypedLobby; this.enterRoomParamsCache.ExpectedUsers = opJoinRandomRoomParams.ExpectedUsers; bool sending = this.LoadBalancingPeer.OpJoinRandomRoom(opJoinRandomRoomParams); if (sending) { this.lastJoinType = JoinType.JoinRandomRoom; this.State = ClientState.Joining; } return sending; } /// /// Attempts to join a room that matches the specified filter and creates a room if none found. /// /// /// This operation is a combination of filter-based random matchmaking with the option to create a new room, /// if no fitting room exists. /// The benefit of that is that the room creation is done by the same operation and the room can be found /// by the very next client, looking for similar rooms. /// /// There are separate parameters for joining and creating a room. /// /// This method can only be called while connected to a Master Server. /// This client's State is set to ClientState.Joining immediately. /// /// Either IMatchmakingCallbacks.OnJoinedRoom or IMatchmakingCallbacks.OnCreatedRoom get called. /// /// More about matchmaking: /// https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby /// /// Check the return value to make sure the operation will be called on the server. /// Note: There will be no callbacks if this method returned false. /// /// If the operation will be sent (requires connection to Master Server). public bool OpJoinRandomOrCreateRoom(OpJoinRandomRoomParams opJoinRandomRoomParams, EnterRoomParams createRoomParams) { if (!this.CheckIfOpCanBeSent(OperationCode.JoinRandomGame, this.Server, "OpJoinRandomOrCreateRoom")) { return false; } if (opJoinRandomRoomParams == null) { opJoinRandomRoomParams = new OpJoinRandomRoomParams(); } if (createRoomParams == null) { createRoomParams = new EnterRoomParams(); } createRoomParams.JoinMode = JoinMode.CreateIfNotExists; this.enterRoomParamsCache = createRoomParams; this.enterRoomParamsCache.Lobby = opJoinRandomRoomParams.TypedLobby; this.enterRoomParamsCache.ExpectedUsers = opJoinRandomRoomParams.ExpectedUsers; bool sending = this.LoadBalancingPeer.OpJoinRandomOrCreateRoom(opJoinRandomRoomParams, createRoomParams); if (sending) { this.lastJoinType = JoinType.JoinRandomOrCreateRoom; this.State = ClientState.Joining; } return sending; } /// /// Creates a new room. Will callback: OnCreatedRoom and OnJoinedRoom or OnCreateRoomFailed. /// /// /// When successful, the client will enter the specified room and callback both OnCreatedRoom and OnJoinedRoom. /// In all error cases, OnCreateRoomFailed gets called. /// /// Creating a room will fail if the room name is already in use or when the RoomOptions clashing /// with one another. Check the EnterRoomParams reference for the various room creation options. /// /// /// This method can only be called while the client is connected to a Master Server so you should /// implement the callback OnConnectedToMaster. /// Check the return value to make sure the operation will be called on the server. /// Note: There will be no callbacks if this method returned false. /// /// /// When you're in the room, this client's State will become ClientState.Joined. /// /// /// When entering 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 are not wiped when leaving a room. /// /// You can define an array of expectedUsers, to block player slots in the room for these users. /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. /// /// Definition of properties for the room to create. /// If the operation could be sent currently (requires connection to Master Server). public bool OpCreateRoom(EnterRoomParams enterRoomParams) { if (!this.CheckIfOpCanBeSent(OperationCode.CreateGame, this.Server, "CreateGame")) { return false; } bool onGameServer = this.Server == ServerConnection.GameServer; enterRoomParams.OnGameServer = onGameServer; if (!onGameServer) { this.enterRoomParamsCache = enterRoomParams; } bool sending = this.LoadBalancingPeer.OpCreateRoom(enterRoomParams); if (sending) { this.lastJoinType = JoinType.CreateRoom; this.State = ClientState.Joining; } return sending; } /// /// Joins a specific room by name and creates it on demand. Will callback: OnJoinedRoom or OnJoinRoomFailed. /// /// /// Useful when players make up a room name to meet in: /// All involved clients call the same method and whoever is first, also creates the room. /// /// When successful, the client will enter the specified room. /// The client which creates the room, will callback both OnCreatedRoom and OnJoinedRoom. /// Clients that join an existing room will only callback OnJoinedRoom. /// In all error cases, OnJoinRoomFailed gets called. /// /// Joining a room will fail, if the room is full, closed or when the user /// already is present in the room (checked by userId). /// /// To return to a room, use OpRejoinRoom. /// /// This method can only be called while the client is connected to a Master Server so you should /// implement the callback OnConnectedToMaster. /// Check the return value to make sure the operation will be called on the server. /// Note: There will be no callbacks if this method returned false. /// /// This client's State is set to ClientState.Joining immediately, when the operation could /// be called. In the background, the client will switch servers and call various related operations. /// /// When you're in the room, this client's State will become ClientState.Joined. /// /// /// 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. /// /// When entering 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 are not wiped when leaving a room. /// /// You can define an array of expectedUsers, to block player slots in the room for these users. /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. /// /// Definition of properties for the room to create or join. /// If the operation could be sent currently (requires connection to Master Server). public bool OpJoinOrCreateRoom(EnterRoomParams enterRoomParams) { if (!this.CheckIfOpCanBeSent(OperationCode.JoinGame, this.Server, "JoinOrCreateRoom")) { return false; } bool onGameServer = this.Server == ServerConnection.GameServer; enterRoomParams.JoinMode = JoinMode.CreateIfNotExists; enterRoomParams.OnGameServer = onGameServer; if (!onGameServer) { this.enterRoomParamsCache = enterRoomParams; } bool sending = this.LoadBalancingPeer.OpJoinRoom(enterRoomParams); if (sending) { this.lastJoinType = JoinType.JoinOrCreateRoom; this.State = ClientState.Joining; } return sending; } /// /// Joins a room by name. Will callback: OnJoinedRoom or OnJoinRoomFailed. /// /// /// Useful when using lobbies or when players follow friends or invite each other. /// /// When successful, the client will enter the specified room and callback via OnJoinedRoom. /// In all error cases, OnJoinRoomFailed gets called. /// /// Joining a room will fail if the room is full, closed, not existing or when the user /// already is present in the room (checked by userId). /// /// To return to a room, use OpRejoinRoom. /// When players invite each other and it's unclear who's first to respond, use OpJoinOrCreateRoom instead. /// /// This method can only be called while the client is connected to a Master Server so you should /// implement the callback OnConnectedToMaster. /// Check the return value to make sure the operation will be called on the server. /// Note: There will be no callbacks if this method returned false. /// /// A room's name has to be unique (per region, appid and gameversion). /// When your title uses a global matchmaking or invitations (e.g. an external solution), /// keep regions and the game versions in mind to join a room. /// /// /// This client's State is set to ClientState.Joining immediately, when the operation could /// be called. In the background, the client will switch servers and call various related operations. /// /// When you're in the room, this client's State will become ClientState.Joined. /// /// /// When entering 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 are not wiped when leaving a room. /// /// You can define an array of expectedUsers, to reserve player slots in the room for friends or party members. /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. /// /// Definition of properties for the room to join. /// If the operation could be sent currently (requires connection to Master Server). public bool OpJoinRoom(EnterRoomParams enterRoomParams) { if (!this.CheckIfOpCanBeSent(OperationCode.JoinGame, this.Server, "JoinRoom")) { return false; } bool onGameServer = this.Server == ServerConnection.GameServer; enterRoomParams.OnGameServer = onGameServer; if (!onGameServer) { this.enterRoomParamsCache = enterRoomParams; } bool sending = this.LoadBalancingPeer.OpJoinRoom(enterRoomParams); if (sending) { this.lastJoinType = enterRoomParams.JoinMode == JoinMode.CreateIfNotExists ? JoinType.JoinOrCreateRoom : JoinType.JoinRoom; this.State = ClientState.Joining; } return sending; } /// /// Rejoins a room by roomName (using the userID internally to return). Will callback: OnJoinedRoom or OnJoinRoomFailed. /// /// /// Used to return to a room, before this user was removed from the players list. /// Internally, the userID will be checked by the server, to make sure this user is in the room (active or inactice). /// /// In contrast to join, this operation never adds a players to a room. It will attempt to retake an existing /// spot in the playerlist or fail. This makes sure the client doean't accidentally join a room when the /// game logic meant to re-activate an existing actor in an existing room. /// /// This method will fail on the server, when the room does not exist, can't be loaded (persistent rooms) or /// when the userId is not in the player list of this room. This will lead to a callback OnJoinRoomFailed. /// /// Rejoining room will not send any player properties. Instead client will receive up-to-date ones from server. /// If you want to set new player properties, do it once rejoined. /// public bool OpRejoinRoom(string roomName) { if (!this.CheckIfOpCanBeSent(OperationCode.JoinGame, this.Server, "RejoinRoom")) { return false; } bool onGameServer = this.Server == ServerConnection.GameServer; EnterRoomParams opParams = new EnterRoomParams(); this.enterRoomParamsCache = opParams; opParams.RoomName = roomName; opParams.OnGameServer = onGameServer; opParams.JoinMode = JoinMode.RejoinOnly; bool sending = this.LoadBalancingPeer.OpJoinRoom(opParams); if (sending) { this.lastJoinType = JoinType.JoinRoom; this.State = ClientState.Joining; } return sending; } /// /// Leaves the current room, optionally telling the server that the user is just becoming inactive. Will callback: OnLeftRoom. /// /// /// /// 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 true, this player becomes inactive in the game and can return later (if PlayerTTL of the room is != 0). /// WebFlag: Securely transmit the encrypted object AuthCookie to the web service in PathLeave webhook when available /// If the current room could be left (impossible while not in a room). public bool OpLeaveRoom(bool becomeInactive, bool sendAuthCookie = false) { if (!this.CheckIfOpCanBeSent(OperationCode.Leave, this.Server, "LeaveRoom")) { return false; } this.State = ClientState.Leaving; this.GameServerAddress = String.Empty; this.enterRoomParamsCache = null; return this.LoadBalancingPeer.OpLeaveRoom(becomeInactive, sendAuthCookie); } /// Gets a list of rooms matching the (non empty) SQL filter for the given SQL-typed lobby. /// /// Operation is only available for lobbies of type SqlLobby and the filter can not be empty. /// It will check those conditions and fail locally, returning false. /// /// This is an async request which triggers a OnOperationResponse() call. /// /// /// 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) { if (!this.CheckIfOpCanBeSent(OperationCode.GetGameList, this.Server, "GetGameList")) { return false; } if (string.IsNullOrEmpty(sqlLobbyFilter)) { this.DebugReturn(DebugLevel.ERROR, "Operation GetGameList requires a filter."); return false; } if (typedLobby.Type != LobbyType.SqlLobby) { this.DebugReturn(DebugLevel.ERROR, "Operation GetGameList can only be used for lobbies of type SqlLobby."); return false; } 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. /// /// False if propertiesToSet is null or empty or have zero string keys. /// If not in a room, returns true if local player and expectedProperties and webFlags are null. /// False if actorNr is lower than or equal to zero. /// Otherwise, returns if the operation could be sent to the server. /// public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null) { if (propertiesToSet == null || propertiesToSet.Count == 0) { this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. propertiesToSet must not be null nor empty."); return false; } 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.ActorNumber == actorNr) { return this.LocalPlayer.SetCustomProperties(propertiesToSet); } 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); if (customActorProperties.Count == 0) { this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. Only string keys allowed for custom properties."); return false; } return this.OpSetPropertiesOfActor(actorNr, customActorProperties, expectedProperties, webFlags); } /// 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.CheckIfOpCanBeSent(OperationCode.SetProperties, this.Server, "SetProperties")) { return false; } if (actorProperties == null || actorProperties.Count == 0) { this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfActor() failed. actorProperties must not be null nor empty."); return false; } bool res = this.LoadBalancingPeer.OpSetPropertiesOfActor(actorNr, actorProperties, expectedProperties, webFlags); if (res && !this.CurrentRoom.BroadcastPropertiesChangeToAll && (expectedProperties == null || expectedProperties.Count == 0)) { Player target = this.CurrentRoom.GetPlayer(actorNr); if (target != null) { target.InternalCacheProperties(actorProperties); this.InRoomCallbackTargets.OnPlayerPropertiesUpdate(target, actorProperties); } } return res; } /// /// 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 web flags for an optional PathProperties webhook. /// /// False if propertiesToSet is null or empty or have zero string keys. /// Otherwise, returns if the operation could be sent to the server. /// public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null) { if (propertiesToSet == null || propertiesToSet.Count == 0) { this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfRoom() failed. propertiesToSet must not be null nor empty."); return false; } Hashtable customGameProps = new Hashtable(); customGameProps.MergeStringKeys(propertiesToSet); if (customGameProps.Count == 0) { this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfRoom() failed. Only string keys are allowed for custom properties."); return false; } return this.OpSetPropertiesOfRoom(customGameProps, expectedProperties, webFlags); } protected internal bool OpSetPropertyOfRoom(byte propCode, object value) { Hashtable properties = new Hashtable(); properties[propCode] = value; return this.OpSetPropertiesOfRoom(properties); } /// 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.CheckIfOpCanBeSent(OperationCode.SetProperties, this.Server, "SetProperties")) { return false; } if (gameProperties == null || gameProperties.Count == 0) { this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfRoom() failed. gameProperties must not be null nor empty."); return false; } bool res = this.LoadBalancingPeer.OpSetPropertiesOfRoom(gameProperties, expectedProperties, webFlags); if (res && !this.CurrentRoom.BroadcastPropertiesChangeToAll && (expectedProperties == null || expectedProperties.Count == 0)) { this.CurrentRoom.InternalCacheProperties(gameProperties); this.InRoomCallbackTargets.OnRoomPropertiesUpdate(gameProperties); } return res; } /// /// Send an event with custom code/type and any content to the other players in the same room. /// /// 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). /// Contains used send options. If you pass null, the default options will be used. /// Send options for reliable, encryption etc /// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands. public virtual bool OpRaiseEvent(byte eventCode, object customEventContent, RaiseEventOptions raiseEventOptions, SendOptions sendOptions) { if (!this.CheckIfOpCanBeSent(OperationCode.RaiseEvent, this.Server, "RaiseEvent")) { return false; } return this.LoadBalancingPeer.OpRaiseEvent(eventCode, customEventContent, raiseEventOptions, sendOptions); } /// /// 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.CheckIfOpCanBeSent(OperationCode.ChangeGroups, this.Server, "ChangeGroups")) { 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 (this.InRoom) { this.InRoomCallbackTargets.OnRoomPropertiesUpdate(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); this.InRoomCallbackTargets.OnPlayerPropertiesUpdate(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; if (actorNr == 0) { continue; } 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); } } } } /// /// 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. public 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); } } /// /// 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; // 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); object temp; if (operationResponse.Parameters.TryGetValue(ParameterCode.RoomOptionFlags, out temp)) { this.CurrentRoom.InternalCacheRoomFlags((int)temp); } this.State = ClientState.Joined; // the callbacks OnCreatedRoom and OnJoinedRoom are called in the event join. it contains important info about the room and players. // unless there will be no room events (RoomOptions.SuppressRoomEvents = true) if (this.CurrentRoom.SuppressRoomEvents) { if (this.lastJoinType == JoinType.CreateRoom || (this.lastJoinType == JoinType.JoinOrCreateRoom && this.LocalPlayer.ActorNumber == 1)) { this.MatchMakingCallbackTargets.OnCreatedRoom(); } this.MatchMakingCallbackTargets.OnJoinedRoom(); } } private void UpdatedActorList(int[] actorsInGame) { if (actorsInGame != null) { foreach (int actorNumber in actorsInGame) { if (actorNumber == 0) { continue; } Player target = this.CurrentRoom.GetPlayer(actorNumber); if (target == null) { this.CurrentRoom.StorePlayer(this.CreatePlayer(string.Empty, actorNumber, 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 bool CheckIfOpAllowedOnServer(byte opCode, ServerConnection serverConnection) { switch (serverConnection) { case ServerConnection.MasterServer: switch (opCode) { case OperationCode.CreateGame: case OperationCode.Authenticate: case OperationCode.AuthenticateOnce: case OperationCode.FindFriends: case OperationCode.GetGameList: case OperationCode.GetLobbyStats: case OperationCode.JoinGame: case OperationCode.JoinLobby: case OperationCode.LeaveLobby: case OperationCode.WebRpc: case OperationCode.ServerSettings: case OperationCode.JoinRandomGame: return true; } break; case ServerConnection.GameServer: switch (opCode) { case OperationCode.CreateGame: case OperationCode.Authenticate: case OperationCode.AuthenticateOnce: case OperationCode.ChangeGroups: case OperationCode.GetProperties: case OperationCode.JoinGame: case OperationCode.Leave: case OperationCode.WebRpc: case OperationCode.ServerSettings: case OperationCode.SetProperties: case OperationCode.RaiseEvent: return true; } break; case ServerConnection.NameServer: switch (opCode) { case OperationCode.Authenticate: case OperationCode.AuthenticateOnce: case OperationCode.GetRegions: case OperationCode.ServerSettings: return true; } break; default: throw new ArgumentOutOfRangeException("serverConnection", serverConnection, null); } return false; } private bool CheckIfOpCanBeSent(byte opCode, ServerConnection serverConnection, string opName) { if (this.LoadBalancingPeer == null) { this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) can't be sent because peer is null", opName, opCode)); return false; } if (!this.CheckIfOpAllowedOnServer(opCode, serverConnection)) { if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ERROR) { this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) not allowed on current server ({2})", opName, opCode, serverConnection)); } return false; } if (!this.CheckIfClientIsReadyToCallOperation(opCode)) { DebugLevel levelToReport = DebugLevel.ERROR; if (opCode == OperationCode.RaiseEvent && (this.State == ClientState.Leaving || this.State == ClientState.Disconnecting || this.State == ClientState.DisconnectingFromGameServer)) { levelToReport = DebugLevel.INFO; } if (this.LoadBalancingPeer.DebugOut >= levelToReport) { this.DebugReturn(levelToReport, string.Format("Operation {0} ({1}) not called because client is not connected or not ready yet, client state: {2}", opName, opCode, Enum.GetName(typeof(ClientState), this.State))); } return false; } if (this.LoadBalancingPeer.PeerState != PeerStateValue.Connected) { this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) can't be sent because peer is not connected, peer state: {2}", opName, opCode, this.LoadBalancingPeer.PeerState)); return false; } return true; } private bool CheckIfClientIsReadyToCallOperation(byte opCode) { switch (opCode) { //case OperationCode.ServerSettings: // ?? //case OperationCode.WebRpc: // WebRPC works on MS and GS and I think it does not need the client to be ready case OperationCode.Authenticate: case OperationCode.AuthenticateOnce: return this.IsConnectedAndReady || this.State == ClientState.ConnectingToNameServer || // this is required since we do not set state to ConnectedToNameServer before authentication this.State == ClientState.ConnectingToMasterServer || // this is required since we do not set state to ConnectedToMasterServer before authentication this.State == ClientState.ConnectingToGameServer; // this is required since we do not set state to ConnectedToGameServer before authentication case OperationCode.ChangeGroups: case OperationCode.GetProperties: case OperationCode.SetProperties: case OperationCode.RaiseEvent: case OperationCode.Leave: return this.InRoom; case OperationCode.JoinGame: case OperationCode.CreateGame: return this.State == ClientState.ConnectedToMasterServer || this.InLobby || this.State == ClientState.ConnectedToGameServer; // CurrentRoom can be not null in case of quick rejoin case OperationCode.LeaveLobby: return this.InLobby; case OperationCode.JoinRandomGame: case OperationCode.FindFriends: case OperationCode.GetGameList: case OperationCode.GetLobbyStats: // do we need to be inside lobby to call this? case OperationCode.JoinLobby: // You don't have to explicitly leave a lobby to join another (client can be in one max, at any time) return this.State == ClientState.ConnectedToMasterServer || this.InLobby; case OperationCode.GetRegions: return this.State == ClientState.ConnectedToNameServer; } return this.IsConnected; } #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 (this.LoadBalancingPeer.DebugOut != DebugLevel.ALL && level > this.LoadBalancingPeer.DebugOut) { return; } #if !SUPPORTED_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 } private void CallbackRoomEnterFailed(OperationResponse operationResponse) { if (operationResponse.ReturnCode != 0) { if (operationResponse.OperationCode == OperationCode.JoinGame) { this.MatchMakingCallbackTargets.OnJoinRoomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage); } else if (operationResponse.OperationCode == OperationCode.CreateGame) { this.MatchMakingCallbackTargets.OnCreateRoomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage); } else if (operationResponse.OperationCode == OperationCode.JoinRandomGame) { this.MatchMakingCallbackTargets.OnJoinRandomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage); } } } /// /// 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.Token)) { if (this.AuthValues == null) { this.AuthValues = new AuthenticationValues(); //this.DebugReturn(DebugLevel.ERROR, "Server returned secret. Created AuthValues."); } this.AuthValues.Token = operationResponse[ParameterCode.Token] as string; this.tokenCache = this.AuthValues.Token; } // if the operation limit was reached, disconnect (but still execute the operation response). if (operationResponse.ReturnCode == ErrorCode.OperationLimitReached) { this.Disconnect(DisconnectCause.DisconnectByOperationLimit); } 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; this.ConnectionCallbackTargets.OnCustomAuthenticationFailed(operationResponse.DebugMessage); break; case ErrorCode.InvalidRegion: this.DisconnectedCause = DisconnectCause.InvalidRegion; break; case ErrorCode.MaxCcuReached: this.DisconnectedCause = DisconnectCause.MaxCcuReached; break; case ErrorCode.InvalidOperation: case ErrorCode.OperationNotAllowedInCurrentState: this.DisconnectedCause = DisconnectCause.OperationNotAllowedInCurrentState; break; case ErrorCode.AuthenticationTicketExpired: this.DisconnectedCause = DisconnectCause.AuthenticationTicketExpired; break; } this.Disconnect(this.DisconnectedCause); 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.LocalPlayer.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) { string receivedCluster = operationResponse[ParameterCode.Cluster] as string; if (!string.IsNullOrEmpty(receivedCluster)) { this.CurrentCluster = receivedCluster; } // 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.ServerPortOverrides.MasterServerPort != 0) { //Debug.LogWarning("Incoming MasterServer Address: "+this.MasterServerAddress); this.MasterServerAddress = ReplacePortWithAlternative(this.MasterServerAddress, this.ServerPortOverrides.MasterServerPort); //Debug.LogWarning("New MasterServer Address: "+this.MasterServerAddress); } if (this.AuthMode == AuthModeOption.AuthOnceWss && this.ExpectedProtocol != null) { this.DebugReturn(DebugLevel.INFO, string.Format("AuthOnceWss mode. Auth response switches TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol)); this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol; this.ExpectedProtocol = null; } this.DisconnectToReconnect(); } else if (this.Server == ServerConnection.MasterServer) { this.State = ClientState.ConnectedToMasterServer; if (this.failedRoomEntryOperation == null) { this.ConnectionCallbackTargets.OnConnectedToMaster(); } else { this.CallbackRoomEnterFailed(this.failedRoomEntryOperation); this.failedRoomEntryOperation = null; } if (this.AuthMode != AuthModeOption.Auth) { this.LoadBalancingPeer.OpSettings(this.EnableLobbyStatistics); } } else if (this.Server == ServerConnection.GameServer) { this.State = ClientState.Joining; if (this.enterRoomParamsCache.JoinMode == JoinMode.RejoinOnly) { this.enterRoomParamsCache.PlayerProperties = null; } else { Hashtable allProps = new Hashtable(); allProps.Merge(this.LocalPlayer.CustomProperties); if (!string.IsNullOrEmpty(this.LocalPlayer.NickName)) { allProps[ActorProperties.PlayerName] = this.LocalPlayer.NickName; } this.enterRoomParamsCache.PlayerProperties = allProps; } this.enterRoomParamsCache.OnGameServer = true; if (this.lastJoinType == JoinType.JoinRoom || this.lastJoinType == JoinType.JoinRandomRoom || this.lastJoinType == JoinType.JoinRandomOrCreateRoom || this.lastJoinType == JoinType.JoinOrCreateRoom) { this.LoadBalancingPeer.OpJoinRoom(this.enterRoomParamsCache); } else if (this.lastJoinType == JoinType.CreateRoom) { this.LoadBalancingPeer.OpCreateRoom(this.enterRoomParamsCache); } break; } // optionally, OpAuth may return some data for the client to use. if it's available, call OnCustomAuthenticationResponse Dictionary data = (Dictionary)operationResponse[ParameterCode.Data]; if (data != null) { this.ConnectionCallbackTargets.OnCustomAuthenticationResponse(data); } break; } case OperationCode.GetRegions: // Debug.Log("GetRegions returned: " + operationResponse.ToStringFull()); if (operationResponse.ReturnCode == ErrorCode.InvalidAuthentication) { this.DebugReturn(DebugLevel.ERROR, string.Format("GetRegions failed. AppId is unknown on the (cloud) server. "+operationResponse.DebugMessage)); this.Disconnect(DisconnectCause.InvalidAuthentication); break; } if (operationResponse.ReturnCode != ErrorCode.Ok) { this.DebugReturn(DebugLevel.ERROR, "GetRegions failed. Can't provide regions list. ReturnCode: " + operationResponse.ReturnCode + ": " + operationResponse.DebugMessage); this.Disconnect(DisconnectCause.InvalidAuthentication); break; } if (this.RegionHandler == null) { this.RegionHandler = new RegionHandler(this.ServerPortOverrides.MasterServerPort); } if (this.RegionHandler.IsPinging) { this.DebugReturn(DebugLevel.WARNING, "Received an response for OpGetRegions while the RegionHandler is pinging regions already. Skipping this response in favor of completing the current region-pinging."); return; // in this particular case, we suppress the duplicate GetRegion response. we don't want a callback for this, cause there is a warning already. } this.RegionHandler.SetRegions(operationResponse); this.ConnectionCallbackTargets.OnRegionListReceived(this.RegionHandler); if (this.connectToBestRegion) { // ping minimal regions (if one is known) and connect this.RegionHandler.PingMinimumOfRegions(this.OnRegionPingCompleted, this.bestRegionSummaryFromStorage); } 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) { if (this.Server == ServerConnection.GameServer) { this.failedRoomEntryOperation = operationResponse; this.DisconnectToReconnect(); } else { this.State = (this.InLobby) ? ClientState.JoinedLobby : ClientState.ConnectedToMasterServer; this.CallbackRoomEnterFailed(operationResponse); } } else { if (this.Server == ServerConnection.GameServer) { this.GameEnteredOnGameServer(operationResponse); } else { this.GameServerAddress = (string)operationResponse[ParameterCode.Address]; if (this.ServerPortOverrides.GameServerPort != 0) { //Debug.LogWarning("Incoming GameServer Address: " + this.GameServerAddress); this.GameServerAddress = ReplacePortWithAlternative(this.GameServerAddress, this.ServerPortOverrides.GameServerPort); //Debug.LogWarning("New GameServer Address: " + this.GameServerAddress); } 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; } List _RoomInfoList = new List(); Hashtable games = (Hashtable)operationResponse[ParameterCode.GameList]; foreach (string gameName in games.Keys) { _RoomInfoList.Add(new RoomInfo(gameName, (Hashtable)games[gameName])); } this.LobbyCallbackTargets.OnRoomListUpdate(_RoomInfoList); break; case OperationCode.JoinLobby: this.State = ClientState.JoinedLobby; this.LobbyCallbackTargets.OnJoinedLobby(); break; case OperationCode.LeaveLobby: this.State = ClientState.ConnectedToMasterServer; this.LobbyCallbackTargets.OnLeftLobby(); break; case OperationCode.Leave: this.DisconnectToReconnect(); break; case OperationCode.FindFriends: if (operationResponse.ReturnCode != 0) { this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed: " + operationResponse.ToStringFull()); this.friendListRequested = null; break; } bool[] onlineList = operationResponse[ParameterCode.FindFriendsResponseOnlineList] as bool[]; string[] roomList = operationResponse[ParameterCode.FindFriendsResponseRoomIdList] as string[]; //if (onlineList == null || roomList == null || this.friendListRequested == null || onlineList.Length != this.friendListRequested.Length) //{ // // TODO: Check if we should handle this case better / more extensively // this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed. Some list is not set. OpResponse: " + operationResponse.ToStringFull()); // this.friendListRequested = null; // this.isFetchingFriendList = false; // break; //} List friendList = new List(this.friendListRequested.Length); for (int index = 0; index < this.friendListRequested.Length; index++) { FriendInfo friend = new FriendInfo(); friend.UserId = this.friendListRequested[index]; friend.Room = roomList[index]; friend.IsOnline = onlineList[index]; friendList.Insert(index, friend); } this.friendListRequested = null; this.MatchMakingCallbackTargets.OnFriendListUpdate(friendList); break; case OperationCode.WebRpc: this.WebRpcCallbackTargets.OnWebRpcResponse(operationResponse); break; } if (this.OpResponseReceived != null) this.OpResponseReceived(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: 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; this.ConnectionCallbackTargets.OnConnected(); // if initial connect } 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: if (this.Server == ServerConnection.NameServer) { this.State = ClientState.ConnectedToNameServer; // if there is no specific region to connect to, get available regions from the Name Server. the result triggers next actions in workflow if (string.IsNullOrEmpty(this.CloudRegion)) { this.OpGetRegions(); break; } } else { // auth AuthOnce, no explicit authentication is needed on Master Server and Game Server. this is done via token, so: break if (this.AuthMode == AuthModeOption.AuthOnce || this.AuthMode == AuthModeOption.AuthOnceWss) { break; } } // authenticate in all other cases (using the CloudRegion, if available) bool authenticating = this.CallAuthenticate(); if (authenticating) { this.State = ClientState.Authenticating; } else { this.DebugReturn(DebugLevel.ERROR, "OpAuthenticate failed. Check log output and AuthValues. 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.friendListRequested = null; bool wasInRoom = this.CurrentRoom != null; this.CurrentRoom = null; // players get cleaned up inside this, too, except LocalPlayer (which we keep) this.ChangeLocalID(-1); // depends on this.CurrentRoom, so it must be called after updating that if (this.Server == ServerConnection.GameServer && wasInRoom) { this.MatchMakingCallbackTargets.OnLeftRoom(); } if (this.ExpectedProtocol != null && this.LoadBalancingPeer.TransportProtocol != this.ExpectedProtocol) { this.DebugReturn(DebugLevel.INFO, string.Format("On disconnect switches TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol)); this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol; this.ExpectedProtocol = null; } switch (this.State) { case ClientState.ConnectWithFallbackProtocol: this.EnableProtocolFallback = false; // the client does a fallback only one time this.LoadBalancingPeer.TransportProtocol = (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Tcp) ? ConnectionProtocol.Udp : ConnectionProtocol.Tcp; this.NameServerPortInAppSettings = 0; // this does not affect the ServerSettings file, just a variable at runtime this.ServerPortOverrides = new PhotonPortDefinition(); // use default ports for the fallback if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit)) { return; } this.State = ClientState.ConnectingToNameServer; break; 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; this.ConnectionCallbackTargets.OnDisconnected(this.DisconnectedCause); break; case ClientState.DisconnectingFromGameServer: case ClientState.DisconnectingFromNameServer: this.ConnectToMasterServer(); // this gets the client back to the Master Server break; case ClientState.DisconnectingFromMasterServer: this.Connect(this.GameServerAddress, this.ProxyServerAddress, ServerConnection.GameServer); // this connects the client with the Game Server (when joining/creating a room) break; case ClientState.Disconnected: // this client is already Disconnected, so no further action is needed. // this.DebugReturn(DebugLevel.INFO, "LBC.OnStatusChanged(Disconnect) this.State: " + this.State + ". Server: " + this.Server); 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; this.ConnectionCallbackTargets.OnDisconnected(this.DisconnectedCause); break; } break; case StatusCode.DisconnectByServerUserLimit: this.DebugReturn(DebugLevel.ERROR, "This connection was rejected due to the apps CCU limit."); this.DisconnectedCause = DisconnectCause.MaxCcuReached; this.State = ClientState.Disconnecting; break; case StatusCode.DnsExceptionOnConnect: this.DisconnectedCause = DisconnectCause.DnsExceptionOnConnect; this.State = ClientState.Disconnecting; break; case StatusCode.ServerAddressInvalid: this.DisconnectedCause = DisconnectCause.ServerAddressInvalid; this.State = ClientState.Disconnecting; break; case StatusCode.ExceptionOnConnect: case StatusCode.SecurityExceptionOnConnect: case StatusCode.EncryptionFailedToEstablish: this.DisconnectedCause = DisconnectCause.ExceptionOnConnect; // if enabled, the client can attempt to connect with another networking-protocol to check if that connects if (this.EnableProtocolFallback && this.State == ClientState.ConnectingToNameServer) { this.State = ClientState.ConnectWithFallbackProtocol; } else { this.State = ClientState.Disconnecting; } break; case StatusCode.Exception: case StatusCode.ExceptionOnReceive: case StatusCode.SendError: this.DisconnectedCause = DisconnectCause.Exception; this.State = ClientState.Disconnecting; break; case StatusCode.DisconnectByServerTimeout: this.DisconnectedCause = DisconnectCause.ServerTimeout; this.State = ClientState.Disconnecting; break; case StatusCode.DisconnectByServerLogic: this.DisconnectedCause = DisconnectCause.DisconnectByServerLogic; this.State = ClientState.Disconnecting; break; case StatusCode.DisconnectByServerReasonUnknown: this.DisconnectedCause = DisconnectCause.DisconnectByServerReasonUnknown; this.State = ClientState.Disconnecting; break; case StatusCode.TimeoutDisconnect: this.DisconnectedCause = DisconnectCause.ClientTimeout; // if enabled, the client can attempt to connect with another networking-protocol to check if that connects if (this.EnableProtocolFallback && this.State == ClientState.ConnectingToNameServer) { this.State = ClientState.ConnectWithFallbackProtocol; } else { this.State = ClientState.Disconnecting; } 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 = photonEvent.Sender; Player originatingPlayer = (this.CurrentRoom != null) ? this.CurrentRoom.GetPlayer(actorNr) : null; switch (photonEvent.Code) { case EventCode.GameList: case EventCode.GameListUpdate: List _RoomInfoList = new List(); Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList]; foreach (string gameName in games.Keys) { _RoomInfoList.Add(new RoomInfo(gameName, (Hashtable)games[gameName])); } this.LobbyCallbackTargets.OnRoomListUpdate(_RoomInfoList); break; case EventCode.Join: Hashtable actorProperties = (Hashtable)photonEvent[ParameterCode.PlayerProperties]; if (originatingPlayer == null) { if (actorNr > 0) { originatingPlayer = this.CreatePlayer(string.Empty, actorNr, false, actorProperties); this.CurrentRoom.StorePlayer(originatingPlayer); } } else { originatingPlayer.InternalCacheProperties(actorProperties); originatingPlayer.IsInactive = false; originatingPlayer.HasRejoined = actorNr != this.LocalPlayer.ActorNumber; // event is for non-local player, who is known (by ActorNumber), so it's a returning player } if (actorNr == this.LocalPlayer.ActorNumber) { // 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); // any operation that does a "rejoin" will set this value to true. this can indicate if the local player returns to a room. originatingPlayer.HasRejoined = this.enterRoomParamsCache.JoinMode == JoinMode.RejoinOnly; // joinWithCreateOnDemand can turn an OpJoin into creating the room. Then actorNumber is 1 and callback: OnCreatedRoom() if (this.lastJoinType == JoinType.CreateRoom || (this.lastJoinType == JoinType.JoinOrCreateRoom && this.LocalPlayer.ActorNumber == 1)) { this.MatchMakingCallbackTargets.OnCreatedRoom(); } this.MatchMakingCallbackTargets.OnJoinedRoom(); } else { this.InRoomCallbackTargets.OnPlayerEnteredRoom(originatingPlayer); } break; case EventCode.Leave: if (originatingPlayer != null) { bool isInactive = false; if (photonEvent.Parameters.ContainsKey(ParameterCode.IsInactive)) { isInactive = (bool)photonEvent.Parameters[ParameterCode.IsInactive]; } if (isInactive) { originatingPlayer.IsInactive = true; } else { originatingPlayer.IsInactive = false; this.CurrentRoom.RemovePlayer(actorNr); } } if (photonEvent.Parameters.ContainsKey(ParameterCode.MasterClientId)) { int newMaster = (int)photonEvent[ParameterCode.MasterClientId]; if (newMaster != 0) { this.CurrentRoom.masterClientId = newMaster; this.InRoomCallbackTargets.OnMasterClientSwitched(this.CurrentRoom.GetPlayer(newMaster)); } } // finally, send notification that a player left this.InRoomCallbackTargets.OnPlayerLeftRoom(originatingPlayer); 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[]; int[] peers = photonEvent[ParameterCode.PeerCount] as int[]; int[] rooms = photonEvent[ParameterCode.GameCount] as int[]; byte[] types; ByteArraySlice slice = photonEvent[ParameterCode.LobbyType] as ByteArraySlice; bool useByteArraySlice = slice != null; if (useByteArraySlice) { types = slice.Buffer; } else { types = photonEvent[ParameterCode.LobbyType] as byte[]; } 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); } if (useByteArraySlice) { slice.Release(); } this.LobbyCallbackTargets.OnLobbyStatisticsUpdate(this.lobbyStatistics); break; case EventCode.ErrorInfo: this.ErrorInfoCallbackTargets.OnErrorInfo(new ErrorInfo(photonEvent)); break; case EventCode.AuthEvent: if (this.AuthValues == null) { this.AuthValues = new AuthenticationValues(); } this.AuthValues.Token = photonEvent[ParameterCode.Token] as string; this.tokenCache = this.AuthValues.Token; break; } this.UpdateCallbackTargets(); if (this.EventReceived != null) this.EventReceived(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 OnDisconnectMessageReceived(DisconnectMessage obj) { this.DebugReturn(DebugLevel.ERROR, string.Format("Got DisconnectMessage. Code: {0} Msg: \"{1}\". Debug Info: {2}", obj.Code, obj.DebugMessage, obj.Parameters.ToStringFull())); this.Disconnect(DisconnectCause.DisconnectByDisconnectMessage); } /// A callback of the RegionHandler, provided in OnRegionListReceived. /// The regionHandler wraps up best region and other region relevant info. private void OnRegionPingCompleted(RegionHandler regionHandler) { //Debug.Log("OnRegionPingCompleted " + regionHandler.BestRegion); //Debug.Log("RegionPingSummary: " + regionHandler.SummaryToCache); this.SummaryToCache = regionHandler.SummaryToCache; this.ConnectToRegionMaster(regionHandler.BestRegion.Code); } protected internal static string ReplacePortWithAlternative(string address, ushort replacementPort) { bool webSocket = address.StartsWith("ws"); if (webSocket) { UriBuilder urib = new UriBuilder(address); urib.Port = replacementPort; return urib.ToString(); } else { UriBuilder urib = new UriBuilder(string.Format("scheme://{0}", address)); return string.Format("{0}:{1}", urib.Host, replacementPort); } } 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: case EncryptionMode.DatagramEncryptionRandomSequence: { byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1]; byte[] secret2 = (byte[])encryptionData[EncryptionDataParameters.Secret2]; this.LoadBalancingPeer.InitDatagramEncryption(secret1, secret2, mode == EncryptionMode.DatagramEncryptionRandomSequence); } break; case EncryptionMode.DatagramEncryptionGCM: { byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1]; this.LoadBalancingPeer.InitDatagramEncryption(secret1, null, true, true); } break; default: throw new ArgumentOutOfRangeException(); } } /// /// This operation makes Photon call your custom web-service by path/name with the given parameters (converted into Json). /// Use as a callback. /// /// /// 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 class is a helper-class that extracts the most valuable content from the WebRPC /// 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) { if (string.IsNullOrEmpty(uriPath)) { this.DebugReturn(DebugLevel.ERROR, "WebRPC method name must not be null nor empty."); return false; } if (!this.CheckIfOpCanBeSent(OperationCode.WebRpc, this.Server, "WebRpc")) { return false; } Dictionary opParameters = new Dictionary(); opParameters.Add(ParameterCode.UriPath, uriPath); if (parameters != null) { opParameters.Add(ParameterCode.WebRpcParameters, parameters); } if (sendAuthCookie) { opParameters.Add(ParameterCode.EventForward, WebFlags.SendAuthCookieConst); } //return this.LoadBalancingPeer.OpCustom(OperationCode.WebRpc, opParameters, true); return this.LoadBalancingPeer.SendOperation(OperationCode.WebRpc, opParameters, SendOptions.SendReliable); } /// /// Registers an object for callbacks for the implemented callback-interfaces. /// /// /// Adding and removing callback targets is queued to not mess with callbacks in execution. /// Internally, this means that the addition/removal is done before the LoadBalancingClient /// calls the next callbacks. This detail should not affect a game's workflow. /// /// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks, /// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback. /// /// See: /// /// The object that registers to get callbacks from this client. public void AddCallbackTarget(object target) { this.callbackTargetChanges.Enqueue(new CallbackTargetChange(target, true)); } /// /// Unregisters an object from callbacks for the implemented callback-interfaces. /// /// /// Adding and removing callback targets is queued to not mess with callbacks in execution. /// Internally, this means that the addition/removal is done before the LoadBalancingClient /// calls the next callbacks. This detail should not affect a game's workflow. /// /// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks, /// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback. /// /// See: /// /// The object that unregisters from getting callbacks. public void RemoveCallbackTarget(object target) { this.callbackTargetChanges.Enqueue(new CallbackTargetChange(target, false)); } /// /// Applies queued callback cahnges from a queue to the actual containers. Will cause exceptions if used while callbacks execute. /// /// /// There is no explicit check that this is not called during callbacks, however the implemented, private logic takes care of this. /// protected internal void UpdateCallbackTargets() { while (this.callbackTargetChanges.Count > 0) { CallbackTargetChange change = this.callbackTargetChanges.Dequeue(); if (change.AddTarget) { if (this.callbackTargets.Contains(change.Target)) { //Debug.Log("UpdateCallbackTargets skipped adding a target, as the object is already registered. Target: " + change.Target); continue; } this.callbackTargets.Add(change.Target); } else { if (!this.callbackTargets.Contains(change.Target)) { //Debug.Log("UpdateCallbackTargets skipped removing a target, as the object is not registered. Target: " + change.Target); continue; } this.callbackTargets.Remove(change.Target); } this.UpdateCallbackTarget(change, this.InRoomCallbackTargets); this.UpdateCallbackTarget(change, this.ConnectionCallbackTargets); this.UpdateCallbackTarget(change, this.MatchMakingCallbackTargets); this.UpdateCallbackTarget(change, this.LobbyCallbackTargets); this.UpdateCallbackTarget(change, this.WebRpcCallbackTargets); this.UpdateCallbackTarget(change, this.ErrorInfoCallbackTargets); IOnEventCallback onEventCallback = change.Target as IOnEventCallback; if (onEventCallback != null) { if (change.AddTarget) { EventReceived += onEventCallback.OnEvent; } else { EventReceived -= onEventCallback.OnEvent; } } } } /// Helper method to cast and apply a target per (interface) type. /// Either of the interfaces for callbacks. /// The queued change to apply (add or remove) some target. /// The container that calls callbacks on it's list of targets. private void UpdateCallbackTarget(CallbackTargetChange change, List container) where T : class { T target = change.Target as T; if (target != null) { if (change.AddTarget) { container.Add(target); } else { container.Remove(target); } } } } /// /// Collection of "organizational" callbacks for the Realtime Api to cover: Connection and Regions. /// /// /// Classes that implement this interface must be registered to get callbacks for various situations. /// /// To register for callbacks, call and pass the class implementing this interface /// To stop getting callbacks, call and pass the class implementing this interface /// /// /// \ingroup callbacks public interface IConnectionCallbacks { /// /// Called to signal that the "low level connection" got established but before the client can call operation on the server. /// /// /// After the (low level transport) connection is established, the client will automatically send /// the Authentication operation, which needs to get a response before the client can call other operations. /// /// Your logic should wait for either: OnRegionListReceived or OnConnectedToMaster. /// /// This callback is useful to detect if the server can be reached at all (technically). /// Most often, it's enough to implement OnDisconnected(DisconnectCause cause) and check for the cause. /// /// This is not called for transitions from the masterserver to game servers. /// void OnConnected(); /// /// Called when the client is connected to the Master Server and ready for matchmaking and other tasks. /// /// /// The list of available rooms won't become available unless you join a lobby via LoadBalancingClient.OpJoinLobby. /// You can join rooms and create them even without being in a lobby. The default lobby is used in that case. /// void OnConnectedToMaster(); /// /// Called after disconnecting from the Photon server. It could be a failure or an explicit disconnect call /// /// /// The reason for this disconnect is provided as DisconnectCause. /// void OnDisconnected(DisconnectCause cause); /// /// Called when the Name Server provided a list of regions for your title. /// /// Check the RegionHandler class description, to make use of the provided values. /// The currently used RegionHandler. void OnRegionListReceived(RegionHandler regionHandler); /// /// Called when your Custom Authentication service responds with additional data. /// /// /// Custom Authentication services can include some custom data in their response. /// When present, that data is made available in this callback as Dictionary. /// While the keys of your data have to be strings, the values can be either string or a number (in Json). /// You need to make extra sure, that the value type is the one you expect. Numbers become (currently) int64. /// /// Example: void OnCustomAuthenticationResponse(Dictionary<string, object> data) { ... } /// /// void OnCustomAuthenticationResponse(Dictionary data); /// /// Called when the custom authentication failed. Followed by disconnect! /// /// /// Custom Authentication can fail due to user-input, bad tokens/secrets. /// If authentication is successful, this method is not called. Implement OnJoinedLobby() or OnConnectedToMaster() (as usual). /// /// During development of a game, it might also fail due to wrong configuration on the server side. /// In those cases, logging the debugMessage is very important. /// /// Unless you setup a custom authentication service for your app (in the [Dashboard](https://dashboard.photonengine.com)), /// this won't be called! /// /// Contains a debug message why authentication failed. This has to be fixed during development. void OnCustomAuthenticationFailed(string debugMessage); } /// /// Collection of "organizational" callbacks for the Realtime Api to cover the Lobby. /// /// /// Classes that implement this interface must be registered to get callbacks for various situations. /// /// To register for callbacks, call and pass the class implementing this interface /// To stop getting callbacks, call and pass the class implementing this interface /// /// /// \ingroup callbacks public interface ILobbyCallbacks { /// /// Called on entering a lobby on the Master Server. The actual room-list updates will call OnRoomListUpdate. /// /// /// While in the lobby, the roomlist is automatically updated in fixed intervals (which you can't modify in the public cloud). /// The room list gets available via OnRoomListUpdate. /// void OnJoinedLobby(); /// /// Called after leaving a lobby. /// /// /// When you leave a lobby, [OpCreateRoom](@ref OpCreateRoom) and [OpJoinRandomRoom](@ref OpJoinRandomRoom) /// automatically refer to the default lobby. /// void OnLeftLobby(); /// /// Called for any update of the room-listing while in a lobby (InLobby) on the Master Server. /// /// /// Each item is a RoomInfo which might include custom properties (provided you defined those as lobby-listed when creating a room). /// Not all types of lobbies provide a listing of rooms to the client. Some are silent and specialized for server-side matchmaking. /// void OnRoomListUpdate(List roomList); /// /// Called when the Master Server sent an update for the Lobby Statistics. /// /// /// This callback has two preconditions: /// EnableLobbyStatistics must be set to true, before this client connects. /// And the client has to be connected to the Master Server, which is providing the info about lobbies. /// void OnLobbyStatisticsUpdate(List lobbyStatistics); } /// /// Collection of "organizational" callbacks for the Realtime Api to cover Matchmaking. /// /// /// Classes that implement this interface must be registered to get callbacks for various situations. /// /// To register for callbacks, call and pass the class implementing this interface /// To stop getting callbacks, call and pass the class implementing this interface /// /// /// \ingroup callbacks public interface IMatchmakingCallbacks { /// /// Called when the server sent the response to a FindFriends request. /// /// /// After calling OpFindFriends, the Master Server will cache the friend list and send updates to the friend /// list. The friends includes the name, userId, online state and the room (if any) for each requested user/friend. /// /// Use the friendList to update your UI and store it, if the UI should highlight changes. /// void OnFriendListUpdate(List friendList); /// /// Called when this client created a room and entered it. OnJoinedRoom() will be called as well. /// /// /// This callback is only called on the client which created a room (see OpCreateRoom). /// /// As any client might close (or drop connection) anytime, there is a chance that the /// creator of a room does not execute OnCreatedRoom. /// /// If you need specific room properties or a "start signal", implement OnMasterClientSwitched() /// and make each new MasterClient check the room's state. /// void OnCreatedRoom(); /// /// Called when the server couldn't create a room (OpCreateRoom failed). /// /// /// Creating a room may fail for various reasons. Most often, the room already exists (roomname in use) or /// the RoomOptions clash and it's impossible to create the room. /// /// When creating a room fails on a Game Server: /// The client will cache the failure internally and returns to the Master Server before it calls the fail-callback. /// This way, the client is ready to find/create a room at the moment of the callback. /// In this case, the client skips calling OnConnectedToMaster but returning to the Master Server will still call OnConnected. /// Treat callbacks of OnConnected as pure information that the client could connect. /// /// Operation ReturnCode from the server. /// Debug message for the error. void OnCreateRoomFailed(short returnCode, string message); /// /// Called when the LoadBalancingClient entered a room, no matter if this client created it or simply joined. /// /// /// When this is called, you can access the existing players in Room.Players, their custom properties and Room.CustomProperties. /// /// In this callback, you could create player objects. For example in Unity, instantiate a prefab for the player. /// /// If you want a match to be started "actively", enable the user to signal "ready" (using OpRaiseEvent or a Custom Property). /// void OnJoinedRoom(); /// /// Called when a previous OpJoinRoom call failed on the server. /// /// /// Joining a room may fail for various reasons. Most often, the room is full or does not exist anymore /// (due to someone else being faster or closing the room). /// /// When joining a room fails on a Game Server: /// The client will cache the failure internally and returns to the Master Server before it calls the fail-callback. /// This way, the client is ready to find/create a room at the moment of the callback. /// In this case, the client skips calling OnConnectedToMaster but returning to the Master Server will still call OnConnected. /// Treat callbacks of OnConnected as pure information that the client could connect. /// /// Operation ReturnCode from the server. /// Debug message for the error. void OnJoinRoomFailed(short returnCode, string message); /// /// Called when a previous OpJoinRandom call failed on the server. /// /// /// The most common causes are that a room is full or does not exist (due to someone else being faster or closing the room). /// /// This operation is only ever sent to the Master Server. Once a room is found by the Master Server, the client will /// head off to the designated Game Server and use the operation Join on the Game Server. /// /// When using multiple lobbies (via OpJoinLobby or a TypedLobby parameter), another lobby might have more/fitting rooms.
///
/// Operation ReturnCode from the server. /// Debug message for the error. void OnJoinRandomFailed(short returnCode, string message); /// /// Called when the local user/client left a room, so the game's logic can clean up it's internal state. /// /// /// When leaving a room, the LoadBalancingClient will disconnect the Game Server and connect to the Master Server. /// This wraps up multiple internal actions. /// /// Wait for the callback OnConnectedToMaster, before you use lobbies and join or create rooms. /// void OnLeftRoom(); } /// /// Collection of "in room" callbacks for the Realtime Api to cover: Players entering or leaving, property updates and Master Client switching. /// /// /// Classes that implement this interface must be registered to get callbacks for various situations. /// /// To register for callbacks, call and pass the class implementing this interface /// To stop getting callbacks, call and pass the class implementing this interface /// /// /// \ingroup callbacks public interface IInRoomCallbacks { /// /// Called when a remote player entered the room. This Player is already added to the playerlist. /// /// /// If your game starts with a certain number of players, this callback can be useful to check the /// Room.playerCount and find out if you can start. /// void OnPlayerEnteredRoom(Player newPlayer); /// /// Called when a remote player left the room or became inactive. Check otherPlayer.IsInactive. /// /// /// If another player leaves the room or if the server detects a lost connection, this callback will /// be used to notify your game logic. /// /// Depending on the room's setup, players may become inactive, which means they may return and retake /// their spot in the room. In such cases, the Player stays in the Room.Players dictionary. /// /// If the player is not just inactive, it gets removed from the Room.Players dictionary, before /// the callback is called. /// void OnPlayerLeftRoom(Player otherPlayer); /// /// Called when a room's custom properties changed. The propertiesThatChanged contains all that was set via Room.SetCustomProperties. /// /// /// Since v1.25 this method has one parameter: Hashtable propertiesThatChanged.
/// Changing properties must be done by Room.SetCustomProperties, which causes this callback locally, too. ///
/// void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged); /// /// Called when custom player-properties are changed. Player and the changed properties are passed as object[]. /// /// /// Changing properties must be done by Player.SetCustomProperties, which causes this callback locally, too. /// /// Contains Player that changed. /// Contains the properties that changed. void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps); /// /// Called after switching to a new MasterClient when the current one leaves. /// /// /// This is not called when this client enters a room. /// The former MasterClient is still in the player list when this method get called. /// void OnMasterClientSwitched(Player newMasterClient); } /// /// Event callback for the Realtime Api. Covers events from the server and those sent by clients via OpRaiseEvent. /// /// /// Classes that implement this interface must be registered to get callbacks for various situations. /// /// To register for callbacks, call and pass the class implementing this interface /// To stop getting callbacks, call and pass the class implementing this interface /// /// /// \ingroup callbacks public interface IOnEventCallback { /// Called for any incoming events. /// /// To receive events, implement IOnEventCallback in any class and register it via AddCallbackTarget /// (either in LoadBalancingClient or PhotonNetwork). /// /// With the EventData.Sender you can look up the Player who sent the event. /// /// It is best practice to assign an eventCode for each different type of content and action, so the Code /// will be essential to read the incoming events. /// void OnEvent(EventData photonEvent); } /// /// Interface for "WebRpc" callbacks for the Realtime Api. Currently includes only responses for Web RPCs. /// /// /// Classes that implement this interface must be registered to get callbacks for various situations. /// /// To register for callbacks, call and pass the class implementing this interface /// To stop getting callbacks, call and pass the class implementing this interface /// /// /// \ingroup callbacks public interface IWebRpcCallback { /// /// Called when the response to a WebRPC is available. See . /// /// /// Important: The response.ReturnCode is 0 if Photon was able to reach your web-service.
/// The content of the response is what your web-service sent. You can create a WebRpcResponse from it.
/// Example: WebRpcResponse webResponse = new WebRpcResponse(operationResponse);
/// /// Please note: Class OperationResponse is in a namespace which needs to be "used":
/// using ExitGames.Client.Photon; // includes OperationResponse (and other classes) ///
/// /// public void OnWebRpcResponse(OperationResponse response) /// { /// Debug.LogFormat("WebRPC operation response {0}", response.ToStringFull()); /// switch (response.ReturnCode) /// { /// case ErrorCode.Ok: /// WebRpcResponse webRpcResponse = new WebRpcResponse(response); /// Debug.LogFormat("Parsed WebRPC response {0}", response.ToStringFull()); /// if (string.IsNullOrEmpty(webRpcResponse.Name)) /// { /// Debug.LogError("Unexpected: WebRPC response did not contain WebRPC method name"); /// } /// if (webRpcResponse.ResultCode == 0) // success /// { /// switch (webRpcResponse.Name) /// { /// // todo: add your code here /// case GetGameListWebRpcMethodName: // example /// // ... /// break; /// } /// } /// else if (webRpcResponse.ResultCode == -1) /// { /// Debug.LogErrorFormat("Web server did not return ResultCode for WebRPC method=\"{0}\", Message={1}", webRpcResponse.Name, webRpcResponse.Message); /// } /// else /// { /// Debug.LogErrorFormat("Web server returned ResultCode={0} for WebRPC method=\"{1}\", Message={2}", webRpcResponse.ResultCode, webRpcResponse.Name, webRpcResponse.Message); /// } /// break; /// case ErrorCode.ExternalHttpCallFailed: // web service unreachable /// Debug.LogErrorFormat("WebRPC call failed as request could not be sent to the server. {0}", response.DebugMessage); /// break; /// case ErrorCode.HttpLimitReached: // too many WebRPCs in a short period of time /// // the debug message should contain the limit exceeded /// Debug.LogErrorFormat("WebRPCs rate limit exceeded: {0}", response.DebugMessage); /// break; /// case ErrorCode.InvalidOperation: // WebRPC not configured at all OR not configured properly OR trying to send on name server /// if (PhotonNetwork.Server == ServerConnection.NameServer) /// { /// Debug.LogErrorFormat("WebRPC not supported on NameServer. {0}", response.DebugMessage); /// } /// else /// { /// Debug.LogErrorFormat("WebRPC not properly configured or not configured at all. {0}", response.DebugMessage); /// } /// break; /// default: /// // other unknown error, unexpected /// Debug.LogErrorFormat("Unexpected error, {0} {1}", response.ReturnCode, response.DebugMessage); /// break; /// } /// } /// /// void OnWebRpcResponse(OperationResponse response); } /// /// Interface for event callback for the Realtime Api. /// /// /// Classes that implement this interface must be registered to get callbacks for various situations. /// /// To register for callbacks, call and pass the class implementing this interface /// To stop getting callbacks, call and pass the class implementing this interface /// /// /// \ingroup callbacks public interface IErrorInfoCallback { /// /// Called when the client receives an event from the server indicating that an error happened there. /// /// /// In most cases this could be either: /// 1. an error from webhooks plugin (if HasErrorInfo is enabled), read more here: /// https://doc.photonengine.com/en-us/realtime/current/gameplay/web-extensions/webhooks#options /// 2. an error sent from a custom server plugin via PluginHost.BroadcastErrorInfoEvent, see example here: /// https://doc.photonengine.com/en-us/server/current/plugins/manual#handling_http_response /// 3. an error sent from the server, for example, when the limit of cached events has been exceeded in the room /// (all clients will be disconnected and the room will be closed in this case) /// read more here: https://doc.photonengine.com/en-us/realtime/current/gameplay/cached-events#special_considerations /// /// If you implement or you will also get this event. /// /// Object containing information about the error void OnErrorInfo(ErrorInfo errorInfo); } /// /// Container type for callbacks defined by IConnectionCallbacks. See LoadBalancingCallbackTargets. /// /// /// While the interfaces of callbacks wrap up the methods that will be called, /// the container classes implement a simple way to call a method on all registered objects. /// public class ConnectionCallbacksContainer : List, IConnectionCallbacks { private readonly LoadBalancingClient client; public ConnectionCallbacksContainer(LoadBalancingClient client) { this.client = client; } public void OnConnected() { this.client.UpdateCallbackTargets(); foreach (IConnectionCallbacks target in this) { target.OnConnected(); } } public void OnConnectedToMaster() { this.client.UpdateCallbackTargets(); foreach (IConnectionCallbacks target in this) { target.OnConnectedToMaster(); } } public void OnRegionListReceived(RegionHandler regionHandler) { this.client.UpdateCallbackTargets(); foreach (IConnectionCallbacks target in this) { target.OnRegionListReceived(regionHandler); } } public void OnDisconnected(DisconnectCause cause) { this.client.UpdateCallbackTargets(); foreach (IConnectionCallbacks target in this) { target.OnDisconnected(cause); } } public void OnCustomAuthenticationResponse(Dictionary data) { this.client.UpdateCallbackTargets(); foreach (IConnectionCallbacks target in this) { target.OnCustomAuthenticationResponse(data); } } public void OnCustomAuthenticationFailed(string debugMessage) { this.client.UpdateCallbackTargets(); foreach (IConnectionCallbacks target in this) { target.OnCustomAuthenticationFailed(debugMessage); } } } /// /// Container type for callbacks defined by IMatchmakingCallbacks. See MatchMakingCallbackTargets. /// /// /// While the interfaces of callbacks wrap up the methods that will be called, /// the container classes implement a simple way to call a method on all registered objects. /// public class MatchMakingCallbacksContainer : List, IMatchmakingCallbacks { private readonly LoadBalancingClient client; public MatchMakingCallbacksContainer(LoadBalancingClient client) { this.client = client; } public void OnCreatedRoom() { this.client.UpdateCallbackTargets(); foreach (IMatchmakingCallbacks target in this) { target.OnCreatedRoom(); } } public void OnJoinedRoom() { this.client.UpdateCallbackTargets(); foreach (IMatchmakingCallbacks target in this) { target.OnJoinedRoom(); } } public void OnCreateRoomFailed(short returnCode, string message) { this.client.UpdateCallbackTargets(); foreach (IMatchmakingCallbacks target in this) { target.OnCreateRoomFailed(returnCode, message); } } public void OnJoinRandomFailed(short returnCode, string message) { this.client.UpdateCallbackTargets(); foreach (IMatchmakingCallbacks target in this) { target.OnJoinRandomFailed(returnCode, message); } } public void OnJoinRoomFailed(short returnCode, string message) { this.client.UpdateCallbackTargets(); foreach (IMatchmakingCallbacks target in this) { target.OnJoinRoomFailed(returnCode, message); } } public void OnLeftRoom() { this.client.UpdateCallbackTargets(); foreach (IMatchmakingCallbacks target in this) { target.OnLeftRoom(); } } public void OnFriendListUpdate(List friendList) { this.client.UpdateCallbackTargets(); foreach (IMatchmakingCallbacks target in this) { target.OnFriendListUpdate(friendList); } } } /// /// Container type for callbacks defined by IInRoomCallbacks. See InRoomCallbackTargets. /// /// /// While the interfaces of callbacks wrap up the methods that will be called, /// the container classes implement a simple way to call a method on all registered objects. /// internal class InRoomCallbacksContainer : List, IInRoomCallbacks { private readonly LoadBalancingClient client; public InRoomCallbacksContainer(LoadBalancingClient client) { this.client = client; } public void OnPlayerEnteredRoom(Player newPlayer) { this.client.UpdateCallbackTargets(); foreach (IInRoomCallbacks target in this) { target.OnPlayerEnteredRoom(newPlayer); } } public void OnPlayerLeftRoom(Player otherPlayer) { this.client.UpdateCallbackTargets(); foreach (IInRoomCallbacks target in this) { target.OnPlayerLeftRoom(otherPlayer); } } public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged) { this.client.UpdateCallbackTargets(); foreach (IInRoomCallbacks target in this) { target.OnRoomPropertiesUpdate(propertiesThatChanged); } } public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProp) { this.client.UpdateCallbackTargets(); foreach (IInRoomCallbacks target in this) { target.OnPlayerPropertiesUpdate(targetPlayer, changedProp); } } public void OnMasterClientSwitched(Player newMasterClient) { this.client.UpdateCallbackTargets(); foreach (IInRoomCallbacks target in this) { target.OnMasterClientSwitched(newMasterClient); } } } /// /// Container type for callbacks defined by ILobbyCallbacks. See LobbyCallbackTargets. /// /// /// While the interfaces of callbacks wrap up the methods that will be called, /// the container classes implement a simple way to call a method on all registered objects. /// internal class LobbyCallbacksContainer : List, ILobbyCallbacks { private readonly LoadBalancingClient client; public LobbyCallbacksContainer(LoadBalancingClient client) { this.client = client; } public void OnJoinedLobby() { this.client.UpdateCallbackTargets(); foreach (ILobbyCallbacks target in this) { target.OnJoinedLobby(); } } public void OnLeftLobby() { this.client.UpdateCallbackTargets(); foreach (ILobbyCallbacks target in this) { target.OnLeftLobby(); } } public void OnRoomListUpdate(List roomList) { this.client.UpdateCallbackTargets(); foreach (ILobbyCallbacks target in this) { target.OnRoomListUpdate(roomList); } } public void OnLobbyStatisticsUpdate(List lobbyStatistics) { this.client.UpdateCallbackTargets(); foreach (ILobbyCallbacks target in this) { target.OnLobbyStatisticsUpdate(lobbyStatistics); } } } /// /// Container type for callbacks defined by IWebRpcCallback. See WebRpcCallbackTargets. /// /// /// While the interfaces of callbacks wrap up the methods that will be called, /// the container classes implement a simple way to call a method on all registered objects. /// internal class WebRpcCallbacksContainer : List, IWebRpcCallback { private LoadBalancingClient client; public WebRpcCallbacksContainer(LoadBalancingClient client) { this.client = client; } public void OnWebRpcResponse(OperationResponse response) { this.client.UpdateCallbackTargets(); foreach (IWebRpcCallback target in this) { target.OnWebRpcResponse(response); } } } /// /// Container type for callbacks defined by . See . /// /// /// While the interfaces of callbacks wrap up the methods that will be called, /// the container classes implement a simple way to call a method on all registered objects. /// internal class ErrorInfoCallbacksContainer : List, IErrorInfoCallback { private LoadBalancingClient client; public ErrorInfoCallbacksContainer(LoadBalancingClient client) { this.client = client; } public void OnErrorInfo(ErrorInfo errorInfo) { this.client.UpdateCallbackTargets(); foreach (IErrorInfoCallback target in this) { target.OnErrorInfo(errorInfo); } } } /// /// Class wrapping the received event. /// /// /// This is passed inside callback. /// If you implement or you will also get but not parsed. /// /// In most cases this could be either: /// 1. an error from webhooks plugin (if HasErrorInfo is enabled), read more here: /// https://doc.photonengine.com/en-us/realtime/current/gameplay/web-extensions/webhooks#options /// 2. an error sent from a custom server plugin via PluginHost.BroadcastErrorInfoEvent, see example here: /// https://doc.photonengine.com/en-us/server/current/plugins/manual#handling_http_response /// 3. an error sent from the server, for example, when the limit of cached events has been exceeded in the room /// (all clients will be disconnected and the room will be closed in this case) /// read more here: https://doc.photonengine.com/en-us/realtime/current/gameplay/cached-events#special_considerations /// public class ErrorInfo { /// /// String containing information about the error. /// public readonly string Info; public ErrorInfo(EventData eventData) { this.Info = eventData[ParameterCode.Info] as string; } public override string ToString() { return string.Format("ErrorInfo: {0}", this.Info); } } }