Seven is the number.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4413 lines
210 KiB

4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
3 years ago
4 years ago
  1. // -----------------------------------------------------------------------
  2. // <copyright file="LoadBalancingClient.cs" company="Exit Games GmbH">
  3. // Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH
  4. // </copyright>
  5. // <summary>
  6. // Provides the operations and a state for games using the
  7. // Photon LoadBalancing server.
  8. // </summary>
  9. // <author>developer@photonengine.com</author>
  10. // ----------------------------------------------------------------------------
  11. #if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER
  12. #define SUPPORTED_UNITY
  13. #endif
  14. namespace Photon.Realtime
  15. {
  16. using System;
  17. using System.Collections;
  18. using System.Collections.Generic;
  19. using System.Diagnostics;
  20. using ExitGames.Client.Photon;
  21. #if SUPPORTED_UNITY
  22. using UnityEngine;
  23. using Debug = UnityEngine.Debug;
  24. #endif
  25. #if SUPPORTED_UNITY || NETFX_CORE
  26. using Hashtable = ExitGames.Client.Photon.Hashtable;
  27. using SupportClass = ExitGames.Client.Photon.SupportClass;
  28. #endif
  29. #region Enums
  30. /// <summary>
  31. /// State values for a client, which handles switching Photon server types, some operations, etc.
  32. /// </summary>
  33. /// \ingroup publicApi
  34. public enum ClientState
  35. {
  36. /// <summary>Peer is created but not used yet.</summary>
  37. PeerCreated,
  38. /// <summary>Transition state while connecting to a server. On the Photon Cloud this sends the AppId and AuthenticationValues (UserID).</summary>
  39. Authenticating,
  40. /// <summary>Not Used.</summary>
  41. Authenticated,
  42. /// <summary>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.</summary>
  43. JoiningLobby,
  44. /// <summary>The client is in a lobby, connected to the MasterServer. Depending on the lobby, it gets room listings.</summary>
  45. JoinedLobby,
  46. /// <summary>Transition from MasterServer to GameServer.</summary>
  47. DisconnectingFromMasterServer,
  48. [Obsolete("Renamed to DisconnectingFromMasterServer")]
  49. DisconnectingFromMasterserver = DisconnectingFromMasterServer,
  50. /// <summary>Transition to GameServer (client authenticates and joins/creates a room).</summary>
  51. ConnectingToGameServer,
  52. [Obsolete("Renamed to ConnectingToGameServer")]
  53. ConnectingToGameserver = ConnectingToGameServer,
  54. /// <summary>Connected to GameServer (going to auth and join game).</summary>
  55. ConnectedToGameServer,
  56. [Obsolete("Renamed to ConnectedToGameServer")]
  57. ConnectedToGameserver = ConnectedToGameServer,
  58. /// <summary>Transition state while joining or creating a room on GameServer.</summary>
  59. Joining,
  60. /// <summary>The client entered a room. The CurrentRoom and Players are known and you can now raise events.</summary>
  61. Joined,
  62. /// <summary>Transition state when leaving a room.</summary>
  63. Leaving,
  64. /// <summary>Transition from GameServer to MasterServer (after leaving a room/game).</summary>
  65. DisconnectingFromGameServer,
  66. [Obsolete("Renamed to DisconnectingFromGameServer")]
  67. DisconnectingFromGameserver = DisconnectingFromGameServer,
  68. /// <summary>Connecting to MasterServer (includes sending authentication values).</summary>
  69. ConnectingToMasterServer,
  70. [Obsolete("Renamed to ConnectingToMasterServer.")]
  71. ConnectingToMasterserver = ConnectingToMasterServer,
  72. /// <summary>The client disconnects (from any server). This leads to state Disconnected.</summary>
  73. Disconnecting,
  74. /// <summary>The client is no longer connected (to any server). Connect to MasterServer to go on.</summary>
  75. Disconnected,
  76. /// <summary>Connected to MasterServer. You might use matchmaking or join a lobby now.</summary>
  77. ConnectedToMasterServer,
  78. [Obsolete("Renamed to ConnectedToMasterServer.")]
  79. ConnectedToMasterserver = ConnectedToMasterServer,
  80. [Obsolete("Renamed to ConnectedToMasterServer.")]
  81. ConnectedToMaster = ConnectedToMasterServer,
  82. /// <summary>Client connects to the NameServer. This process includes low level connecting and setting up encryption. When done, state becomes ConnectedToNameServer.</summary>
  83. ConnectingToNameServer,
  84. /// <summary>Client is connected to the NameServer and established encryption already. You should call OpGetRegions or ConnectToRegionMaster.</summary>
  85. ConnectedToNameServer,
  86. /// <summary>Clients disconnects (specifically) from the NameServer (usually to connect to the MasterServer).</summary>
  87. DisconnectingFromNameServer,
  88. /// <summary>Client was unable to connect to Name Server and will attempt to connect with an alternative network protocol (TCP).</summary>
  89. ConnectWithFallbackProtocol
  90. }
  91. /// <summary>
  92. /// Internal state, how this peer gets into a particular room (joining it or creating it).
  93. /// </summary>
  94. internal enum JoinType
  95. {
  96. /// <summary>This client creates a room, gets into it (no need to join) and can set room properties.</summary>
  97. CreateRoom,
  98. /// <summary>The room existed already and we join into it (not setting room properties).</summary>
  99. JoinRoom,
  100. /// <summary>Done on Master Server and (if successful) followed by a Join on Game Server.</summary>
  101. JoinRandomRoom,
  102. /// <summary>Done on Master Server and (if successful) followed by a Join or Create on Game Server.</summary>
  103. JoinRandomOrCreateRoom,
  104. /// <summary>Client is either joining or creating a room. On Master- and Game-Server.</summary>
  105. JoinOrCreateRoom
  106. }
  107. /// <summary>Enumeration of causes for Disconnects (used in LoadBalancingClient.DisconnectedCause).</summary>
  108. /// <remarks>Read the individual descriptions to find out what to do about this type of disconnect.</remarks>
  109. public enum DisconnectCause
  110. {
  111. /// <summary>No error was tracked.</summary>
  112. None,
  113. /// <summary>OnStatusChanged: The server is not available or the address is wrong. Make sure the port is provided and the server is up.</summary>
  114. ExceptionOnConnect,
  115. /// <summary>OnStatusChanged: Dns resolution for a hostname failed. The exception for this is being catched and logged with error level.</summary>
  116. DnsExceptionOnConnect,
  117. /// <summary>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.</summary>
  118. ServerAddressInvalid,
  119. /// <summary>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.</summary>
  120. Exception,
  121. /// <summary>OnStatusChanged: The server disconnected this client due to timing out (missing acknowledgement from the client).</summary>
  122. ServerTimeout,
  123. /// <summary>OnStatusChanged: This client detected that the server's responses are not received in due time.</summary>
  124. ClientTimeout,
  125. /// <summary>OnStatusChanged: The server disconnected this client from within the room's logic (the C# code).</summary>
  126. DisconnectByServerLogic,
  127. /// <summary>OnStatusChanged: The server disconnected this client for unknown reasons.</summary>
  128. DisconnectByServerReasonUnknown,
  129. /// <summary>OnOperationResponse: Authenticate in the Photon Cloud with invalid AppId. Update your subscription or contact Exit Games.</summary>
  130. InvalidAuthentication,
  131. /// <summary>OnOperationResponse: Authenticate in the Photon Cloud with invalid client values or custom authentication setup in Cloud Dashboard.</summary>
  132. CustomAuthenticationFailed,
  133. /// <summary>The authentication ticket should provide access to any Photon Cloud server without doing another authentication-service call. However, the ticket expired.</summary>
  134. AuthenticationTicketExpired,
  135. /// <summary>OnOperationResponse: Authenticate (temporarily) failed when using a Photon Cloud subscription without CCU Burst. Update your subscription.</summary>
  136. MaxCcuReached,
  137. /// <summary>OnOperationResponse: Authenticate when the app's Photon Cloud subscription is locked to some (other) region(s). Update your subscription or master server address.</summary>
  138. InvalidRegion,
  139. /// <summary>OnOperationResponse: Operation that's (currently) not available for this client (not authorized usually). Only tracked for op Authenticate.</summary>
  140. OperationNotAllowedInCurrentState,
  141. /// <summary>OnStatusChanged: The client disconnected from within the logic (the C# code).</summary>
  142. DisconnectByClientLogic,
  143. /// <summary>The client called an operation too frequently and got disconnected due to hitting the OperationLimit. This triggers a client-side disconnect, too.</summary>
  144. /// <remarks>To protect the server, some operations have a limit. When an OperationResponse fails with ErrorCode.OperationLimitReached, the client disconnects.</remarks>
  145. DisconnectByOperationLimit,
  146. /// <summary>The client received a "Disconnect Message" from the server. Check the debug logs for details.</summary>
  147. DisconnectByDisconnectMessage
  148. }
  149. /// <summary>Available server (types) for internally used field: server.</summary>
  150. /// <remarks>Photon uses 3 different roles of servers: Name Server, Master Server and Game Server.</remarks>
  151. public enum ServerConnection
  152. {
  153. /// <summary>This server is where matchmaking gets done and where clients can get lists of rooms in lobbies.</summary>
  154. MasterServer,
  155. /// <summary>This server handles a number of rooms to execute and relay the messages between players (in a room).</summary>
  156. GameServer,
  157. /// <summary>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).</summary>
  158. NameServer
  159. }
  160. /// <summary>Defines which sort of app the LoadBalancingClient is used for: Realtime or Voice.</summary>
  161. public enum ClientAppType
  162. {
  163. /// <summary>Realtime apps are for gaming / interaction. Also used by PUN 2.</summary>
  164. Realtime,
  165. /// <summary>Voice apps stream audio.</summary>
  166. Voice
  167. }
  168. /// <summary>
  169. /// Defines how the communication gets encrypted.
  170. /// </summary>
  171. public enum EncryptionMode
  172. {
  173. /// <summary>
  174. /// This is the default encryption mode: Messages get encrypted only on demand (when you send operations with the "encrypt" parameter set to true).
  175. /// </summary>
  176. PayloadEncryption,
  177. /// <summary>
  178. /// 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.
  179. /// </summary>
  180. DatagramEncryption = 10,
  181. /// <summary>
  182. /// 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.
  183. /// </summary>
  184. DatagramEncryptionRandomSequence = 11,
  185. ///// <summary>
  186. ///// Same as above except that GCM mode is used to encrypt data.
  187. ///// </summary>
  188. //DatagramEncryptionGCMRandomSequence = 12,
  189. /// <summary>
  190. /// Datagram Encryption with GCM.
  191. /// </summary>
  192. DatagramEncryptionGCM = 13,
  193. }
  194. /// <summary>Container for port definitions.</summary>
  195. public struct PhotonPortDefinition
  196. {
  197. public static readonly PhotonPortDefinition AlternativeUdpPorts = new PhotonPortDefinition() { NameServerPort = 27000, MasterServerPort = 27001, GameServerPort = 27002};
  198. /// <summary>Typical ports: UDP: 5058 or 27000, TCP: 4533, WSS: 19093 or 443.</summary>
  199. public ushort NameServerPort;
  200. /// <summary>Typical ports: UDP: 5056 or 27002, TCP: 4530, WSS: 19090 or 443.</summary>
  201. public ushort MasterServerPort;
  202. /// <summary>Typical ports: UDP: 5055 or 27001, TCP: 4531, WSS: 19091 or 443.</summary>
  203. public ushort GameServerPort;
  204. }
  205. #endregion
  206. /// <summary>
  207. /// This class implements the Photon LoadBalancing workflow by using a LoadBalancingPeer.
  208. /// It keeps a state and will automatically execute transitions between the Master and Game Servers.
  209. /// </summary>
  210. /// <remarks>
  211. /// This class (and the Player class) should be extended to implement your own game logic.
  212. /// You can override CreatePlayer as "factory" method for Players and return your own Player instances.
  213. /// The State of this class is essential to know when a client is in a lobby (or just on the master)
  214. /// and when in a game where the actual gameplay should take place.
  215. /// Extension notes:
  216. /// An extension of this class should override the methods of the IPhotonPeerListener, as they
  217. /// are called when the state changes. Call base.method first, then pick the operation or state you
  218. /// want to react to and put it in a switch-case.
  219. /// We try to provide demo to each platform where this api can be used, so lookout for those.
  220. /// </remarks>
  221. public class LoadBalancingClient : IPhotonPeerListener
  222. {
  223. /// <summary>
  224. /// The client uses a LoadBalancingPeer as API to communicate with the server.
  225. /// This is public for ease-of-use: Some methods like OpRaiseEvent are not relevant for the connection state and don't need a override.
  226. /// </summary>
  227. public LoadBalancingPeer LoadBalancingPeer { get; private set; }
  228. /// <summary>
  229. /// Gets or sets the binary protocol version used by this client
  230. /// </summary>
  231. /// <remarks>
  232. /// Use this always instead of setting it via <see cref="LoadBalancingClient.LoadBalancingPeer"/>
  233. /// (<see cref="PhotonPeer.SerializationProtocolType"/>) directly, especially when WSS protocol is used.
  234. /// </remarks>
  235. public SerializationProtocol SerializationProtocol
  236. {
  237. get
  238. {
  239. return this.LoadBalancingPeer.SerializationProtocolType;
  240. }
  241. set
  242. {
  243. this.LoadBalancingPeer.SerializationProtocolType = value;
  244. }
  245. }
  246. /// <summary>The version of your client. A new version also creates a new "virtual app" to separate players from older client versions.</summary>
  247. public string AppVersion { get; set; }
  248. /// <summary>The AppID as assigned from the Photon Cloud. If you host yourself, this is the "regular" Photon Server Application Name (most likely: "LoadBalancing").</summary>
  249. public string AppId { get; set; }
  250. /// <summary>The ClientAppType defines which sort of AppId should be expected. The LoadBalancingClient supports Realtime and Voice app types. Default: Realtime.</summary>
  251. public ClientAppType ClientType { get; set; }
  252. /// <summary>User authentication values to be sent to the Photon server right after connecting.</summary>
  253. /// <remarks>Set this property or pass AuthenticationValues by Connect(..., authValues).</remarks>
  254. public AuthenticationValues AuthValues { get; set; }
  255. /// <summary>Enables the new Authentication workflow.</summary>
  256. public AuthModeOption AuthMode = AuthModeOption.Auth;
  257. /// <summary>Defines how the communication gets encrypted.</summary>
  258. public EncryptionMode EncryptionMode = EncryptionMode.PayloadEncryption;
  259. /// <summary>Optionally contains a protocol which will be used on Master- and GameServer. </summary>
  260. /// <remarks>
  261. /// When using AuthMode = AuthModeOption.AuthOnceWss, the client uses a wss-connection on the NameServer but another protocol on the other servers.
  262. /// As the NameServer sends an address, which is different per protocol, it needs to know the expected protocol.
  263. ///
  264. /// This is nullable by design. In many cases, the protocol on the NameServer is not different from the other servers.
  265. /// If set, the operation AuthOnce will contain this value and the OpAuth response on the NameServer will execute a protocol switch.
  266. /// </remarks>
  267. public ConnectionProtocol? ExpectedProtocol { get; private set; }
  268. ///<summary>Simplifies getting the token for connect/init requests, if this feature is enabled.</summary>
  269. private object TokenForInit
  270. {
  271. get
  272. {
  273. if (this.AuthMode == AuthModeOption.Auth)
  274. {
  275. return null;
  276. }
  277. return (this.AuthValues != null) ? this.AuthValues.Token : null;
  278. }
  279. }
  280. /// <summary>Internally used cache for the server's token. Identifies a user/session and can be used to rejoin.</summary>
  281. private object tokenCache;
  282. /// <summary>True if this client uses a NameServer to get the Master Server address.</summary>
  283. /// <remarks>This value is public, despite being an internal value, which should only be set by this client.</remarks>
  284. public bool IsUsingNameServer { get; set; }
  285. /// <summary>Name Server Host Name for Photon Cloud. Without port and without any prefix.</summary>
  286. public string NameServerHost = "ns.exitgames.com";
  287. /// <summary>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.</summary>
  288. public string NameServerAddress { get { return this.GetNameServerAddress(); } }
  289. /// <summary>Name Server port per protocol (the UDP port is different than TCP, etc).</summary>
  290. private static readonly Dictionary<ConnectionProtocol, int> ProtocolToNameServerPort = new Dictionary<ConnectionProtocol, int>() { { ConnectionProtocol.Udp, 5058 }, { ConnectionProtocol.Tcp, 4533 }, { ConnectionProtocol.WebSocket, 9093 }, { ConnectionProtocol.WebSocketSecure, 19093 } }; //, { ConnectionProtocol.RHttp, 6063 } };
  291. /// <summary>Replaced by ServerPortOverrides.</summary>
  292. [Obsolete("Set port overrides in ServerPortOverrides. Not used anymore!")]
  293. public bool UseAlternativeUdpPorts { get; set; }
  294. /// <summary>Defines overrides for server ports. Used per server-type if > 0. Important: You must change these when the protocol changes!</summary>
  295. /// <remarks>
  296. /// Typical ports are listed in PhotonPortDefinition.
  297. ///
  298. /// Instead of using the port provided from the servers, the specified port is used (independent of the protocol).
  299. /// If a value is 0 (default), the port is not being replaced.
  300. ///
  301. /// Different protocols have different typical ports per server-type.
  302. /// https://doc.photonengine.com/en-us/pun/current/reference/tcp-and-udp-port-numbers
  303. ///
  304. /// 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.
  305. /// Set the ports accordingly per protocol and server.
  306. /// </remarks>
  307. public PhotonPortDefinition ServerPortOverrides;
  308. /// <summary>Enables a fallback to another protocol in case a connect to the Name Server fails.</summary>
  309. /// <remarks>
  310. /// When connecting to the Name Server fails for a first time, the client will select an alternative
  311. /// network protocol and re-try to connect.
  312. ///
  313. /// The fallback will use the default Name Server port as defined by ProtocolToNameServerPort.
  314. ///
  315. /// The fallback for TCP is UDP. All other protocols fallback to TCP.
  316. /// </remarks>
  317. public bool EnableProtocolFallback { get; set; }
  318. /// <summary>The currently used server address (if any). The type of server is define by Server property.</summary>
  319. public string CurrentServerAddress { get { return this.LoadBalancingPeer.ServerAddress; } }
  320. /// <summary>Your Master Server address. In PhotonCloud, call ConnectToRegionMaster() to find your Master Server.</summary>
  321. /// <remarks>
  322. /// In the Photon Cloud, explicit definition of a Master Server Address is not best practice.
  323. /// The Photon Cloud has a "Name Server" which redirects clients to a specific Master Server (per Region and AppId).
  324. /// </remarks>
  325. public string MasterServerAddress { get; set; }
  326. /// <summary>The game server's address for a particular room. In use temporarily, as assigned by master.</summary>
  327. public string GameServerAddress { get; protected internal set; }
  328. /// <summary>The server this client is currently connected or connecting to.</summary>
  329. /// <remarks>
  330. /// Each server (NameServer, MasterServer, GameServer) allow some operations and reject others.
  331. /// </remarks>
  332. public ServerConnection Server { get; private set; }
  333. /// <summary>
  334. /// Defines a proxy URL for WebSocket connections. Can be the proxy or point to a .pac file.
  335. /// </summary>
  336. /// <remarks>
  337. /// This URL supports various definitions:
  338. ///
  339. /// "user:pass@proxyaddress:port"<br/>
  340. /// "proxyaddress:port"<br/>
  341. /// "system:"<br/>
  342. /// "pac:"<br/>
  343. /// "pac:http://host/path/pacfile.pac"<br/>
  344. ///
  345. /// Important: Don't define a protocol, except to point to a pac file. the proxy address should not begin with http:// or https://.
  346. /// </remarks>
  347. public string ProxyServerAddress;
  348. /// <summary>Backing field for property.</summary>
  349. private ClientState state = ClientState.PeerCreated;
  350. /// <summary>Current state this client is in. Careful: several states are "transitions" that lead to other states.</summary>
  351. public ClientState State
  352. {
  353. get
  354. {
  355. return this.state;
  356. }
  357. set
  358. {
  359. if (this.state == value)
  360. {
  361. return;
  362. }
  363. ClientState previousState = this.state;
  364. this.state = value;
  365. if (StateChanged != null) StateChanged(previousState, this.state);
  366. }
  367. }
  368. /// <summary>Returns if this client is currently connected or connecting to some type of server.</summary>
  369. /// <remarks>This is even true while switching servers. Use IsConnectedAndReady to check only for those states that enable you to send Operations.</remarks>
  370. public bool IsConnected { get { return this.LoadBalancingPeer != null && this.State != ClientState.PeerCreated && this.State != ClientState.Disconnected; } }
  371. /// <summary>
  372. /// A refined version of IsConnected which is true only if your connection is ready to send operations.
  373. /// </summary>
  374. /// <remarks>
  375. /// Not all operations can be called on all types of servers. If an operation is unavailable on the currently connected server,
  376. /// this will result in a OperationResponse with ErrorCode != 0.
  377. ///
  378. /// Examples: The NameServer allows OpGetRegions which is not available anywhere else.
  379. /// The MasterServer does not allow you to send events (OpRaiseEvent) and on the GameServer you are unable to join a lobby (OpJoinLobby).
  380. ///
  381. /// To check which server you are on, use: <see cref="Server"/>.
  382. /// </remarks>
  383. public bool IsConnectedAndReady
  384. {
  385. get
  386. {
  387. if (this.LoadBalancingPeer == null)
  388. {
  389. return false;
  390. }
  391. switch (this.State)
  392. {
  393. case ClientState.PeerCreated:
  394. case ClientState.Disconnected:
  395. case ClientState.Disconnecting:
  396. case ClientState.DisconnectingFromGameServer:
  397. case ClientState.DisconnectingFromMasterServer:
  398. case ClientState.DisconnectingFromNameServer:
  399. case ClientState.Authenticating:
  400. case ClientState.ConnectingToGameServer:
  401. case ClientState.ConnectingToMasterServer:
  402. case ClientState.ConnectingToNameServer:
  403. case ClientState.Joining:
  404. case ClientState.Leaving:
  405. return false; // we are not ready to execute any operations
  406. }
  407. return true;
  408. }
  409. }
  410. /// <summary>Register a method to be called when this client's ClientState gets set.</summary>
  411. /// <remarks>This can be useful to react to being connected, joined into a room, etc.</remarks>
  412. public event Action<ClientState, ClientState> StateChanged;
  413. /// <summary>Register a method to be called when an event got dispatched. Gets called after the LoadBalancingClient handled the internal events first.</summary>
  414. /// <remarks>
  415. /// This is an alternative to extending LoadBalancingClient to override OnEvent().
  416. ///
  417. /// Note that OnEvent is calling EventReceived after it handled internal events first.
  418. /// That means for example: Joining players will already be in the player list but leaving
  419. /// players will already be removed from the room.
  420. /// </remarks>
  421. public event Action<EventData> EventReceived;
  422. /// <summary>Register a method to be called when an operation response is received.</summary>
  423. /// <remarks>
  424. /// This is an alternative to extending LoadBalancingClient to override OnOperationResponse().
  425. ///
  426. /// Note that OnOperationResponse gets executed before your Action is called.
  427. /// That means for example: The OpJoinLobby response already set the state to "JoinedLobby"
  428. /// and the response to OpLeave already triggered the Disconnect before this is called.
  429. /// </remarks>
  430. public event Action<OperationResponse> OpResponseReceived;
  431. /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
  432. /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
  433. public ConnectionCallbacksContainer ConnectionCallbackTargets;
  434. /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
  435. /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
  436. public MatchMakingCallbacksContainer MatchMakingCallbackTargets;
  437. /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
  438. /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
  439. internal InRoomCallbacksContainer InRoomCallbackTargets;
  440. /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
  441. /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
  442. internal LobbyCallbacksContainer LobbyCallbackTargets;
  443. /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
  444. /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
  445. internal WebRpcCallbacksContainer WebRpcCallbackTargets;
  446. /// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
  447. /// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
  448. internal ErrorInfoCallbacksContainer ErrorInfoCallbackTargets;
  449. /// <summary>Summarizes (aggregates) the different causes for disconnects of a client.</summary>
  450. /// <remarks>
  451. /// A disconnect can be caused by: errors in the network connection or some vital operation failing
  452. /// (which is considered "high level"). While operations always trigger a call to OnOperationResponse,
  453. /// connection related changes are treated in OnStatusChanged.
  454. /// The DisconnectCause is set in either case and summarizes the causes for any disconnect in a single
  455. /// state value which can be used to display (or debug) the cause for disconnection.
  456. /// </remarks>
  457. public DisconnectCause DisconnectedCause { get; protected set; }
  458. /// <summary>Internal value if the client is in a lobby.</summary>
  459. /// <remarks>This is used to re-set this.State, when joining/creating a room fails.</remarks>
  460. public bool InLobby
  461. {
  462. get { return this.State == ClientState.JoinedLobby; }
  463. }
  464. /// <summary>The lobby this client currently uses. Defined when joining a lobby or creating rooms</summary>
  465. public TypedLobby CurrentLobby { get; internal set; }
  466. /// <summary>
  467. /// If enabled, the client will get a list of available lobbies from the Master Server.
  468. /// </summary>
  469. /// <remarks>
  470. /// Set this value before the client connects to the Master Server. While connected to the Master
  471. /// Server, a change has no effect.
  472. ///
  473. /// Implement OptionalInfoCallbacks.OnLobbyStatisticsUpdate, to get the list of used lobbies.
  474. ///
  475. /// The lobby statistics can be useful if your title dynamically uses lobbies, depending (e.g.)
  476. /// on current player activity or such.
  477. /// In this case, getting a list of available lobbies, their room-count and player-count can
  478. /// be useful info.
  479. ///
  480. /// ConnectUsingSettings sets this to the PhotonServerSettings value.
  481. /// </remarks>
  482. public bool EnableLobbyStatistics;
  483. /// <summary>Internal lobby stats cache, used by LobbyStatistics.</summary>
  484. private readonly List<TypedLobbyInfo> lobbyStatistics = new List<TypedLobbyInfo>();
  485. /// <summary>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.</summary>
  486. public Player LocalPlayer { get; internal set; }
  487. /// <summary>
  488. /// The nickname of the player (synced with others). Same as client.LocalPlayer.NickName.
  489. /// </summary>
  490. public string NickName
  491. {
  492. get
  493. {
  494. return this.LocalPlayer.NickName;
  495. }
  496. set
  497. {
  498. if (this.LocalPlayer == null)
  499. {
  500. return;
  501. }
  502. this.LocalPlayer.NickName = value;
  503. }
  504. }
  505. /// <summary>An ID for this user. Sent in OpAuthenticate when you connect. If not set, the PlayerName is applied during connect.</summary>
  506. /// <remarks>
  507. /// On connect, if the UserId is null or empty, the client will copy the PlayName to UserId. If PlayerName is not set either
  508. /// (before connect), the server applies a temporary ID which stays unknown to this client and other clients.
  509. ///
  510. /// The UserId is what's used in FindFriends and for fetching data for your account (with WebHooks e.g.).
  511. ///
  512. /// By convention, set this ID before you connect, not while being connected.
  513. /// There is no error but the ID won't change while being connected.
  514. /// </remarks>
  515. public string UserId
  516. {
  517. get
  518. {
  519. if (this.AuthValues != null)
  520. {
  521. return this.AuthValues.UserId;
  522. }
  523. return null;
  524. }
  525. set
  526. {
  527. if (this.AuthValues == null)
  528. {
  529. this.AuthValues = new AuthenticationValues();
  530. }
  531. this.AuthValues.UserId = value;
  532. }
  533. }
  534. /// <summary>The current room this client is connected to (null if none available).</summary>
  535. public Room CurrentRoom { get; set; }
  536. /// <summary>Is true while being in a room (this.state == ClientState.Joined).</summary>
  537. /// <remarks>
  538. /// Aside from polling this value, game logic should implement IMatchmakingCallbacks in some class
  539. /// and react when that gets called.<br/>
  540. /// OpRaiseEvent, OpLeave and some other operations can only be used (successfully) when the client is in a room..
  541. /// </remarks>
  542. public bool InRoom
  543. {
  544. get
  545. {
  546. return this.state == ClientState.Joined && this.CurrentRoom != null;
  547. }
  548. }
  549. /// <summary>Statistic value available on master server: Players on master (looking for games).</summary>
  550. public int PlayersOnMasterCount { get; internal set; }
  551. /// <summary>Statistic value available on master server: Players in rooms (playing).</summary>
  552. public int PlayersInRoomsCount { get; internal set; }
  553. /// <summary>Statistic value available on master server: Rooms currently created.</summary>
  554. public int RoomsCount { get; internal set; }
  555. /// <summary>Internally used to decide if a room must be created or joined on game server.</summary>
  556. private JoinType lastJoinType;
  557. /// <summary>Used when the client arrives on the GS, to join the room with the correct values.</summary>
  558. private EnterRoomParams enterRoomParamsCache;
  559. /// <summary>Used to cache a failed "enter room" operation on the Game Server, to return to the Master Server before calling a fail-callback.</summary>
  560. private OperationResponse failedRoomEntryOperation;
  561. /// <summary>Maximum of userIDs that can be sent in one friend list request.</summary>
  562. private const int FriendRequestListMax = 512;
  563. /// <summary>Contains the list of names of friends to look up their state on the server.</summary>
  564. private string[] friendListRequested;
  565. /// <summary>Internal flag to know if the client currently fetches a friend list.</summary>
  566. public bool IsFetchingFriendList { get { return this.friendListRequested != null; } }
  567. /// <summary>The cloud region this client connects to. Set by ConnectToRegionMaster(). Not set if you don't use a NameServer!</summary>
  568. public string CloudRegion { get; private set; }
  569. /// <summary>The cluster name provided by the Name Server.</summary>
  570. /// <remarks>
  571. /// The value is provided by the OpResponse for OpAuthenticate/OpAuthenticateOnce.
  572. /// Default: null. This value only ever updates from the Name Server authenticate response.
  573. /// </remarks>
  574. public string CurrentCluster { get; private set; }
  575. /// <summary>Contains the list if enabled regions this client may use. Null, unless the client got a response to OpGetRegions.</summary>
  576. public RegionHandler RegionHandler;
  577. /// <summary>Stores the best region summary of a previous session to speed up connecting.</summary>
  578. private string bestRegionSummaryFromStorage;
  579. /// <summary>Set when the best region pinging is done.</summary>
  580. public string SummaryToCache;
  581. /// <summary>Internal connection setting/flag. If the client should connect to the best region or not.</summary>
  582. /// <remarks>
  583. /// It's set in the Connect...() methods. Only ConnectUsingSettings() sets it to true.
  584. /// If true, client will ping available regions and select the best.
  585. /// A bestRegionSummaryFromStorage can be used to cut the ping time short.
  586. /// </remarks>
  587. private bool connectToBestRegion = true;
  588. /// <summary>Definition of parameters for encryption data (included in Authenticate operation response).</summary>
  589. private class EncryptionDataParameters
  590. {
  591. /// <summary>
  592. /// Key for encryption mode
  593. /// </summary>
  594. public const byte Mode = 0;
  595. /// <summary>
  596. /// Key for first secret
  597. /// </summary>
  598. public const byte Secret1 = 1;
  599. /// <summary>
  600. /// Key for second secret
  601. /// </summary>
  602. public const byte Secret2 = 2;
  603. }
  604. private class CallbackTargetChange
  605. {
  606. public readonly object Target;
  607. /// <summary>Add if true, remove if false.</summary>
  608. public readonly bool AddTarget;
  609. public CallbackTargetChange(object target, bool addTarget)
  610. {
  611. this.Target = target;
  612. this.AddTarget = addTarget;
  613. }
  614. }
  615. private readonly Queue<CallbackTargetChange> callbackTargetChanges = new Queue<CallbackTargetChange>();
  616. private readonly HashSet<object> callbackTargets = new HashSet<object>();
  617. /// <summary>Creates a LoadBalancingClient with UDP protocol or the one specified.</summary>
  618. /// <param name="protocol">Specifies the network protocol to use for connections.</param>
  619. public LoadBalancingClient(ConnectionProtocol protocol = ConnectionProtocol.Udp)
  620. {
  621. this.ConnectionCallbackTargets = new ConnectionCallbacksContainer(this);
  622. this.MatchMakingCallbackTargets = new MatchMakingCallbacksContainer(this);
  623. this.InRoomCallbackTargets = new InRoomCallbacksContainer(this);
  624. this.LobbyCallbackTargets = new LobbyCallbacksContainer(this);
  625. this.WebRpcCallbackTargets = new WebRpcCallbacksContainer(this);
  626. this.ErrorInfoCallbackTargets = new ErrorInfoCallbacksContainer(this);
  627. this.LoadBalancingPeer = new LoadBalancingPeer(this, protocol);
  628. this.LoadBalancingPeer.OnDisconnectMessage += this.OnDisconnectMessageReceived;
  629. this.SerializationProtocol = SerializationProtocol.GpBinaryV18;
  630. this.LocalPlayer = this.CreatePlayer(string.Empty, -1, true, null); //TODO: Check if we can do this later
  631. #if SUPPORTED_UNITY
  632. CustomTypesUnity.Register();
  633. #endif
  634. #if UNITY_WEBGL
  635. if (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Tcp || this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Udp)
  636. {
  637. this.LoadBalancingPeer.Listener.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
  638. this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
  639. }
  640. #endif
  641. this.State = ClientState.PeerCreated;
  642. }
  643. /// <summary>Creates a LoadBalancingClient, setting various values needed before connecting.</summary>
  644. /// <param name="masterAddress">The Master Server's address to connect to. Used in Connect.</param>
  645. /// <param name="appId">The AppId of this title. Needed for the Photon Cloud. Find it in the Dashboard.</param>
  646. /// <param name="gameVersion">A version for this client/build. In the Photon Cloud, players are separated by AppId, GameVersion and Region.</param>
  647. /// <param name="protocol">Specifies the network protocol to use for connections.</param>
  648. public LoadBalancingClient(string masterAddress, string appId, string gameVersion, ConnectionProtocol protocol = ConnectionProtocol.Udp) : this(protocol)
  649. {
  650. this.MasterServerAddress = masterAddress;
  651. this.AppId = appId;
  652. this.AppVersion = gameVersion;
  653. }
  654. public int NameServerPortInAppSettings;
  655. /// <summary>
  656. /// Gets the NameServer Address (with prefix and port), based on the set protocol (this.LoadBalancingPeer.UsedProtocol).
  657. /// </summary>
  658. /// <returns>NameServer Address (with prefix and port).</returns>
  659. private string GetNameServerAddress()
  660. {
  661. var protocolPort = 0;
  662. ProtocolToNameServerPort.TryGetValue(this.LoadBalancingPeer.TransportProtocol, out protocolPort);
  663. if (this.NameServerPortInAppSettings != 0)
  664. {
  665. this.DebugReturn(DebugLevel.INFO, string.Format("Using NameServerPortInAppSettings: {0}", this.NameServerPortInAppSettings));
  666. protocolPort = this.NameServerPortInAppSettings;
  667. }
  668. if (this.ServerPortOverrides.NameServerPort > 0)
  669. {
  670. protocolPort = this.ServerPortOverrides.NameServerPort;
  671. }
  672. switch (this.LoadBalancingPeer.TransportProtocol)
  673. {
  674. case ConnectionProtocol.Udp:
  675. case ConnectionProtocol.Tcp:
  676. return string.Format("{0}:{1}", NameServerHost, protocolPort);
  677. case ConnectionProtocol.WebSocket:
  678. return string.Format("ws://{0}:{1}", NameServerHost, protocolPort);
  679. case ConnectionProtocol.WebSocketSecure:
  680. return string.Format("wss://{0}:{1}", NameServerHost, protocolPort);
  681. default:
  682. throw new ArgumentOutOfRangeException();
  683. }
  684. }
  685. #region Operations and Commands
  686. // needed connect variants:
  687. // connect to Name Server only (could include getregions) -> end after getregions
  688. // connect to Region Master via Name Server (specific region/cluster) -> no getregions! authenticates and ends after on connected to master
  689. // connect to Best Region via Name Server
  690. // connect to Master Server (no Name Server, no appid)
  691. public virtual bool ConnectUsingSettings(AppSettings appSettings)
  692. {
  693. if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
  694. {
  695. Debug.LogWarning("ConnectUsingSettings() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
  696. return false;
  697. }
  698. if (appSettings == null)
  699. {
  700. this.DebugReturn(DebugLevel.ERROR, "ConnectUsingSettings failed. The appSettings can't be null.'");
  701. return false;
  702. }
  703. this.AppId = this.ClientType == ClientAppType.Realtime ? appSettings.AppIdRealtime : appSettings.AppIdVoice;
  704. this.AppVersion = appSettings.AppVersion;
  705. this.IsUsingNameServer = appSettings.UseNameServer;
  706. this.CloudRegion = appSettings.FixedRegion;
  707. this.connectToBestRegion = string.IsNullOrEmpty(this.CloudRegion);
  708. this.EnableLobbyStatistics = appSettings.EnableLobbyStatistics;
  709. this.LoadBalancingPeer.DebugOut = appSettings.NetworkLogging;
  710. this.AuthMode = appSettings.AuthMode;
  711. this.LoadBalancingPeer.TransportProtocol = (this.AuthMode == AuthModeOption.AuthOnceWss) ? ConnectionProtocol.WebSocketSecure : appSettings.Protocol;
  712. this.ExpectedProtocol = appSettings.Protocol;
  713. this.EnableProtocolFallback = appSettings.EnableProtocolFallback;
  714. this.bestRegionSummaryFromStorage = appSettings.BestRegionSummaryFromStorage;
  715. this.DisconnectedCause = DisconnectCause.None;
  716. this.CheckConnectSetupWebGl();
  717. this.CheckConnectSetupXboxOne(); // may throw an exception if there are issues that can not be corrected
  718. if (this.IsUsingNameServer)
  719. {
  720. this.Server = ServerConnection.NameServer;
  721. if (!appSettings.IsDefaultNameServer)
  722. {
  723. this.NameServerHost = appSettings.Server;
  724. }
  725. this.ProxyServerAddress = appSettings.ProxyServer;
  726. this.NameServerPortInAppSettings = appSettings.Port;
  727. if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
  728. {
  729. return false;
  730. }
  731. this.State = ClientState.ConnectingToNameServer;
  732. }
  733. else
  734. {
  735. this.Server = ServerConnection.MasterServer;
  736. int portToUse = appSettings.IsDefaultPort ? 5055 : appSettings.Port; // TODO: setup new (default) port config
  737. this.MasterServerAddress = string.Format("{0}:{1}", appSettings.Server, portToUse);
  738. if (!this.LoadBalancingPeer.Connect(this.MasterServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
  739. {
  740. return false;
  741. }
  742. this.State = ClientState.ConnectingToMasterServer;
  743. }
  744. return true;
  745. }
  746. [Obsolete("Use ConnectToMasterServer() instead.")]
  747. public bool Connect()
  748. {
  749. return this.ConnectToMasterServer();
  750. }
  751. /// <summary>
  752. /// Starts the "process" to connect to a Master Server, using MasterServerAddress and AppId properties.
  753. /// </summary>
  754. /// <remarks>
  755. /// To connect to the Photon Cloud, use ConnectUsingSettings() or ConnectToRegionMaster().
  756. ///
  757. /// The process to connect includes several steps: the actual connecting, establishing encryption, authentification
  758. /// (of app and optionally the user) and connecting to the MasterServer
  759. ///
  760. /// Users can connect either anonymously or use "Custom Authentication" to verify each individual player's login.
  761. /// Custom Authentication in Photon uses external services and communities to verify users. While the client provides a user's info,
  762. /// the service setup is done in the Photon Cloud Dashboard.
  763. /// The parameter authValues will set this.AuthValues and use them in the connect process.
  764. ///
  765. /// Connecting to the Photon Cloud might fail due to:
  766. /// - Network issues (OnStatusChanged() StatusCode.ExceptionOnConnect)
  767. /// - Region not available (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.InvalidRegion)
  768. /// - Subscription CCU limit reached (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.MaxCcuReached)
  769. /// </remarks>
  770. public virtual bool ConnectToMasterServer()
  771. {
  772. if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
  773. {
  774. Debug.LogWarning("ConnectToMasterServer() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
  775. return false;
  776. }
  777. // 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
  778. if (this.AuthMode != AuthModeOption.Auth && this.TokenForInit == null)
  779. {
  780. this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect to MasterServer with Token == null in AuthMode: " + this.AuthMode);
  781. return false;
  782. }
  783. this.CheckConnectSetupWebGl();
  784. this.CheckConnectSetupXboxOne(); // may throw an exception if there are issues that can not be corrected
  785. if (this.LoadBalancingPeer.Connect(this.MasterServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
  786. {
  787. this.DisconnectedCause = DisconnectCause.None;
  788. this.connectToBestRegion = false;
  789. this.State = ClientState.ConnectingToMasterServer;
  790. this.Server = ServerConnection.MasterServer;
  791. return true;
  792. }
  793. return false;
  794. }
  795. /// <summary>
  796. /// Connects to the NameServer for Photon Cloud, where a region and server list can be obtained.
  797. /// </summary>
  798. /// <see cref="OpGetRegions"/>
  799. /// <returns>If the workflow was started or failed right away.</returns>
  800. public bool ConnectToNameServer()
  801. {
  802. if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
  803. {
  804. Debug.LogWarning("ConnectToNameServer() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
  805. return false;
  806. }
  807. this.IsUsingNameServer = true;
  808. this.CloudRegion = null;
  809. this.CheckConnectSetupWebGl();
  810. this.CheckConnectSetupXboxOne(); // may throw an exception if there are issues that can not be corrected
  811. if (this.AuthMode == AuthModeOption.AuthOnceWss)
  812. {
  813. if (this.ExpectedProtocol == null)
  814. {
  815. this.ExpectedProtocol = this.LoadBalancingPeer.TransportProtocol;
  816. }
  817. this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
  818. }
  819. if (this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", this.TokenForInit))
  820. {
  821. this.DisconnectedCause = DisconnectCause.None;
  822. this.connectToBestRegion = false;
  823. this.State = ClientState.ConnectingToNameServer;
  824. this.Server = ServerConnection.NameServer;
  825. return true;
  826. }
  827. return false;
  828. }
  829. /// <summary>
  830. /// Connects you to a specific region's Master Server, using the Name Server to find the IP.
  831. /// </summary>
  832. /// <remarks>
  833. /// If the region is null or empty, no connection will be made.
  834. /// If the region (code) provided is not available, the connection process will fail on the Name Server.
  835. /// This method connects only to the region defined. No "Best Region" pinging will be done.
  836. ///
  837. /// If the region string does not contain a "/", this means no specific cluster is requested.
  838. /// To support "Sharding", the region gets a "/*" postfix in this case, to select a random cluster.
  839. /// </remarks>
  840. /// <returns>If the operation could be sent. If false, no operation was sent.</returns>
  841. public bool ConnectToRegionMaster(string region)
  842. {
  843. if (string.IsNullOrEmpty(region))
  844. {
  845. this.DebugReturn(DebugLevel.ERROR, "ConnectToRegionMaster() failed. The region can not be null or empty.");
  846. return false;
  847. }
  848. this.IsUsingNameServer = true;
  849. if (this.State == ClientState.Authenticating)
  850. {
  851. if (this.LoadBalancingPeer.DebugOut >= DebugLevel.INFO)
  852. {
  853. this.DebugReturn(DebugLevel.INFO, "ConnectToRegionMaster() will skip calling authenticate, as the current state is 'Authenticating'. Just wait for the result.");
  854. }
  855. return true;
  856. }
  857. if (this.State == ClientState.ConnectedToNameServer)
  858. {
  859. this.CloudRegion = region;
  860. bool authenticating = this.CallAuthenticate();
  861. if (authenticating)
  862. {
  863. this.State = ClientState.Authenticating;
  864. }
  865. return authenticating;
  866. }
  867. this.LoadBalancingPeer.Disconnect();
  868. if (!string.IsNullOrEmpty(region) && !region.Contains("/"))
  869. {
  870. region = region + "/*";
  871. }
  872. this.CloudRegion = region;
  873. this.CheckConnectSetupWebGl();
  874. this.CheckConnectSetupXboxOne(); // may throw an exception if there are issues that can not be corrected
  875. if (this.AuthMode == AuthModeOption.AuthOnceWss)
  876. {
  877. if (this.ExpectedProtocol == null)
  878. {
  879. this.ExpectedProtocol = this.LoadBalancingPeer.TransportProtocol;
  880. }
  881. this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
  882. }
  883. this.connectToBestRegion = false;
  884. this.DisconnectedCause = DisconnectCause.None;
  885. if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", null))
  886. {
  887. return false;
  888. }
  889. this.State = ClientState.ConnectingToNameServer;
  890. this.Server = ServerConnection.NameServer;
  891. return true;
  892. }
  893. [Conditional("UNITY_WEBGL")]
  894. private void CheckConnectSetupWebGl()
  895. {
  896. #if UNITY_WEBGL
  897. if (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocket && this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure)
  898. {
  899. this.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
  900. this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
  901. }
  902. this.EnableProtocolFallback = false; // no fallback on WebGL
  903. #endif
  904. }
  905. [Conditional("UNITY_XBOXONE"), Conditional("UNITY_GAMECORE")]
  906. private void CheckConnectSetupXboxOne()
  907. {
  908. #if (UNITY_XBOXONE || UNITY_GAMECORE) && !UNITY_EDITOR
  909. this.AuthMode = AuthModeOption.Auth;
  910. if (this.AuthValues == null)
  911. {
  912. this.DebugReturn(DebugLevel.ERROR, "XBOX builds must set AuthValues. Set this before calling any Connect method. Refer to the online docs for guidance.");
  913. throw new Exception("XBOX builds must set AuthValues.");
  914. }
  915. if (this.AuthValues.AuthPostData == null)
  916. {
  917. this.DebugReturn(DebugLevel.ERROR,"XBOX builds must use Photon's XBox Authentication and set the XSTS token by calling: PhotonNetwork.AuthValues.SetAuthPostData(xstsToken). Refer to the online docs for guidance.");
  918. throw new Exception("XBOX builds must use Photon's XBox Authentication.");
  919. }
  920. if (this.AuthValues.AuthType != CustomAuthenticationType.Xbox)
  921. {
  922. this.DebugReturn(DebugLevel.WARNING, "XBOX builds must use AuthValues.AuthType \"CustomAuthenticationType.Xbox\". PUN sets this value now. Refer to the online docs to avoid this warning.");
  923. this.AuthValues.AuthType = CustomAuthenticationType.Xbox;
  924. }
  925. if (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure)
  926. {
  927. this.DebugReturn(DebugLevel.INFO, "XBOX builds must use WSS (Secure WebSockets) as Transport Protocol. Changing the protocol now.");
  928. this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
  929. }
  930. this.EnableProtocolFallback = false; // no transport protocol fallback on XBOX
  931. #endif
  932. }
  933. /// <summary>
  934. /// Privately used only for reconnecting.
  935. /// </summary>
  936. private bool Connect(string serverAddress, string proxyServerAddress, ServerConnection serverType)
  937. {
  938. // TODO: Make sure app doesn't quit right now
  939. if (this.State == ClientState.Disconnecting)
  940. {
  941. this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect while disconnecting (still). Current state: " + this.State);
  942. return false;
  943. }
  944. // 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
  945. if (this.AuthMode != AuthModeOption.Auth && serverType != ServerConnection.NameServer && this.TokenForInit == null)
  946. {
  947. this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect to " + serverType + " with Token == null in AuthMode: " + this.AuthMode);
  948. return false;
  949. }
  950. // connect might fail, if the DNS name can't be resolved or if no network connection is available, etc.
  951. bool connecting = this.LoadBalancingPeer.Connect(serverAddress, proxyServerAddress, this.AppId, this.TokenForInit);
  952. if (connecting)
  953. {
  954. this.DisconnectedCause = DisconnectCause.None;
  955. this.Server = serverType;
  956. switch (serverType)
  957. {
  958. case ServerConnection.NameServer:
  959. State = ClientState.ConnectingToNameServer;
  960. break;
  961. case ServerConnection.MasterServer:
  962. State = ClientState.ConnectingToMasterServer;
  963. break;
  964. case ServerConnection.GameServer:
  965. State = ClientState.ConnectingToGameServer;
  966. break;
  967. }
  968. }
  969. return connecting;
  970. }
  971. /// <summary>Can be used to reconnect to the master server after a disconnect.</summary>
  972. /// <remarks>Common use case: Press the Lock Button on a iOS device and you get disconnected immediately.</remarks>
  973. public bool ReconnectToMaster()
  974. {
  975. if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
  976. {
  977. Debug.LogWarning("ReconnectToMaster() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
  978. return false;
  979. }
  980. if (string.IsNullOrEmpty(this.MasterServerAddress))
  981. {
  982. this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() failed. MasterServerAddress is null or empty.");
  983. return false;
  984. }
  985. if (this.tokenCache == null)
  986. {
  987. this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() failed. It seems the client doesn't have any previous authentication token to re-connect.");
  988. return false;
  989. }
  990. if (this.AuthValues == null)
  991. {
  992. this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() with AuthValues == null is not correct!");
  993. this.AuthValues = new AuthenticationValues();
  994. }
  995. this.AuthValues.Token = this.tokenCache;
  996. return this.Connect(this.MasterServerAddress, this.ProxyServerAddress, ServerConnection.MasterServer);
  997. }
  998. /// <summary>
  999. /// Can be used to return to a room quickly by directly reconnecting to a game server to rejoin a room.
  1000. /// </summary>
  1001. /// <remarks>
  1002. /// Rejoining room will not send any player properties. Instead client will receive up-to-date ones from server.
  1003. /// If you want to set new player properties, do it once rejoined.
  1004. /// </remarks>
  1005. /// <returns>False, if the conditions are not met. Then, this client does not attempt the ReconnectAndRejoin.</returns>
  1006. public bool ReconnectAndRejoin()
  1007. {
  1008. if (this.LoadBalancingPeer.PeerState != PeerStateValue.Disconnected)
  1009. {
  1010. Debug.LogWarning("ReconnectAndRejoin() failed. Can only connect while in state 'Disconnected'. Current state: " + this.LoadBalancingPeer.PeerState);
  1011. return false;
  1012. }
  1013. if (string.IsNullOrEmpty(this.GameServerAddress))
  1014. {
  1015. this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client wasn't connected to a game server before (no address).");
  1016. return false;
  1017. }
  1018. if (this.enterRoomParamsCache == null)
  1019. {
  1020. this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client doesn't have any previous room to re-join.");
  1021. return false;
  1022. }
  1023. if (this.tokenCache == null)
  1024. {
  1025. this.DebugReturn(DebugLevel.WARNING, "ReconnectAndRejoin() failed. It seems the client doesn't have any previous authentication token to re-connect.");
  1026. return false;
  1027. }
  1028. if (this.AuthValues == null)
  1029. {
  1030. this.AuthValues = new AuthenticationValues();
  1031. }
  1032. this.AuthValues.Token = this.tokenCache;
  1033. if (!string.IsNullOrEmpty(this.GameServerAddress) && this.enterRoomParamsCache != null)
  1034. {
  1035. this.lastJoinType = JoinType.JoinRoom;
  1036. this.enterRoomParamsCache.JoinMode = JoinMode.RejoinOnly;
  1037. return this.Connect(this.GameServerAddress, this.ProxyServerAddress, ServerConnection.GameServer);
  1038. }
  1039. return false;
  1040. }
  1041. /// <summary>Disconnects the peer from a server or stays disconnected. If the client / peer was connected, a callback will be triggered.</summary>
  1042. /// <remarks>
  1043. /// This method will not change the current State, if this client State is PeerCreated, Disconnecting or Disconnected.
  1044. /// In those cases, there is also no callback for the disconnect. The DisconnectedCause will only change if the client was connected.
  1045. /// </remarks>
  1046. public void Disconnect(DisconnectCause cause = DisconnectCause.DisconnectByClientLogic)
  1047. {
  1048. if (this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated)
  1049. {
  1050. this.DebugReturn(DebugLevel.INFO, "Disconnect() call gets skipped due to State " + this.State + ". DisconnectedCause: " + this.DisconnectedCause + " Parameter cause: " + cause);
  1051. return;
  1052. }
  1053. if (this.State != ClientState.Disconnected)
  1054. {
  1055. this.State = ClientState.Disconnecting;
  1056. this.DisconnectedCause = cause;
  1057. this.LoadBalancingPeer.Disconnect();
  1058. }
  1059. }
  1060. /// <summary>
  1061. /// Private Disconnect variant that sets the state, too.
  1062. /// </summary>
  1063. private void DisconnectToReconnect()
  1064. {
  1065. switch (this.Server)
  1066. {
  1067. case ServerConnection.NameServer:
  1068. this.State = ClientState.DisconnectingFromNameServer;
  1069. break;
  1070. case ServerConnection.MasterServer:
  1071. this.State = ClientState.DisconnectingFromMasterServer;
  1072. break;
  1073. case ServerConnection.GameServer:
  1074. this.State = ClientState.DisconnectingFromGameServer;
  1075. break;
  1076. }
  1077. this.LoadBalancingPeer.Disconnect();
  1078. }
  1079. /// <summary>
  1080. /// Useful to test loss of connection which will end in a client timeout. This modifies LoadBalancingPeer.NetworkSimulationSettings. Read remarks.
  1081. /// </summary>
  1082. /// <remarks>
  1083. /// Use with care as this sets LoadBalancingPeer.IsSimulationEnabled.<br/>
  1084. /// Read LoadBalancingPeer.IsSimulationEnabled to check if this is on or off, if needed.<br/>
  1085. ///
  1086. /// If simulateTimeout is true, LoadBalancingPeer.NetworkSimulationSettings.IncomingLossPercentage and
  1087. /// LoadBalancingPeer.NetworkSimulationSettings.OutgoingLossPercentage will be set to 100.<br/>
  1088. /// Obviously, this overrides any network simulation settings done before.<br/>
  1089. ///
  1090. /// If you want fine-grained network simulation control, use the NetworkSimulationSettings.<br/>
  1091. ///
  1092. /// The timeout will lead to a call to <see cref="IConnectionCallbacks.OnDisconnected"/>, as usual in a client timeout.
  1093. ///
  1094. /// You could modify this method (or use NetworkSimulationSettings) to deliberately run into a server timeout by
  1095. /// just setting the OutgoingLossPercentage = 100 and the IncomingLossPercentage = 0.
  1096. /// </remarks>
  1097. /// <param name="simulateTimeout">If true, a connection loss is simulated. If false, the simulation ends.</param>
  1098. public void SimulateConnectionLoss(bool simulateTimeout)
  1099. {
  1100. this.DebugReturn(DebugLevel.WARNING, "SimulateConnectionLoss() set to: "+simulateTimeout);
  1101. if (simulateTimeout)
  1102. {
  1103. this.LoadBalancingPeer.NetworkSimulationSettings.IncomingLossPercentage = 100;
  1104. this.LoadBalancingPeer.NetworkSimulationSettings.OutgoingLossPercentage = 100;
  1105. }
  1106. this.LoadBalancingPeer.IsSimulationEnabled = simulateTimeout;
  1107. }
  1108. private bool CallAuthenticate()
  1109. {
  1110. if (this.IsUsingNameServer && this.Server != ServerConnection.NameServer && (this.AuthValues == null || this.AuthValues.Token == null))
  1111. {
  1112. this.DebugReturn(DebugLevel.ERROR, "Authenticate without Token is only allowed on Name Server. Connecting to: " + this.Server + " on: " + this.CurrentServerAddress + ". State: " + this.State);
  1113. }
  1114. if (this.AuthMode == AuthModeOption.Auth)
  1115. {
  1116. if (!this.CheckIfOpCanBeSent(OperationCode.Authenticate, this.Server, "Authenticate"))
  1117. {
  1118. return false;
  1119. }
  1120. return this.LoadBalancingPeer.OpAuthenticate(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, (this.EnableLobbyStatistics && this.Server == ServerConnection.MasterServer));
  1121. }
  1122. else
  1123. {
  1124. if (!this.CheckIfOpCanBeSent(OperationCode.AuthenticateOnce, this.Server, "AuthenticateOnce"))
  1125. {
  1126. return false;
  1127. }
  1128. ConnectionProtocol targetProtocolPastNameServer = this.ExpectedProtocol != null ? (ConnectionProtocol) this.ExpectedProtocol : this.LoadBalancingPeer.TransportProtocol;
  1129. return this.LoadBalancingPeer.OpAuthenticateOnce(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, this.EncryptionMode, targetProtocolPastNameServer);
  1130. }
  1131. }
  1132. /// <summary>
  1133. /// This method dispatches all available incoming commands and then sends this client's outgoing commands.
  1134. /// It uses DispatchIncomingCommands and SendOutgoingCommands to do that.
  1135. /// </summary>
  1136. /// <remarks>
  1137. /// The Photon client libraries are designed to fit easily into a game or application. The application
  1138. /// is in control of the context (thread) in which incoming events and responses are executed and has
  1139. /// full control of the creation of UDP/TCP packages.
  1140. ///
  1141. /// Sending packages and dispatching received messages are two separate tasks. Service combines them
  1142. /// into one method at the cost of control. It calls DispatchIncomingCommands and SendOutgoingCommands.
  1143. ///
  1144. /// Call this method regularly (10..50 times a second).
  1145. ///
  1146. /// This will Dispatch ANY received commands (unless a reliable command in-order is still missing) and
  1147. /// events AND will send queued outgoing commands. Fewer calls might be more effective if a device
  1148. /// cannot send many packets per second, as multiple operations might be combined into one package.
  1149. /// </remarks>
  1150. /// <example>
  1151. /// You could replace Service by:
  1152. ///
  1153. /// while (DispatchIncomingCommands()); //Dispatch until everything is Dispatched...
  1154. /// SendOutgoingCommands(); //Send a UDP/TCP package with outgoing messages
  1155. /// </example>
  1156. /// <seealso cref="PhotonPeer.DispatchIncomingCommands"/>
  1157. /// <seealso cref="PhotonPeer.SendOutgoingCommands"/>
  1158. public void Service()
  1159. {
  1160. if (this.LoadBalancingPeer != null)
  1161. {
  1162. this.LoadBalancingPeer.Service();
  1163. }
  1164. }
  1165. /// <summary>
  1166. /// While on the NameServer, this gets you the list of regional servers (short names and their IPs to ping them).
  1167. /// </summary>
  1168. /// <returns>If the operation could be sent. If false, no operation was sent (e.g. while not connected to the NameServer).</returns>
  1169. private bool OpGetRegions()
  1170. {
  1171. if (!this.CheckIfOpCanBeSent(OperationCode.GetRegions, this.Server, "GetRegions"))
  1172. {
  1173. return false;
  1174. }
  1175. bool sent = this.LoadBalancingPeer.OpGetRegions(this.AppId);
  1176. return sent;
  1177. }
  1178. /// <summary>
  1179. /// 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.
  1180. /// </summary>
  1181. /// <remarks>
  1182. /// Used on Master Server to find the rooms played by a selected list of users.
  1183. /// The result will be stored in LoadBalancingClient.FriendList, which is null before the first server response.
  1184. ///
  1185. /// Users identify themselves by setting a UserId in the LoadBalancingClient instance.
  1186. /// This will send the ID in OpAuthenticate during connect (to master and game servers).
  1187. /// Note: Changing a player's name doesn't make sense when using a friend list.
  1188. ///
  1189. /// The list of usernames must be fetched from some other source (not provided by Photon).
  1190. ///
  1191. ///
  1192. /// Internal:<br/>
  1193. /// The server response includes 2 arrays of info (each index matching a friend from the request):<br/>
  1194. /// ParameterCode.FindFriendsResponseOnlineList = bool[] of online states<br/>
  1195. /// ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room)<br/>
  1196. /// <br/>
  1197. /// The options may be used to define which state a room must match to be returned.
  1198. /// </remarks>
  1199. /// <param name="friendsToFind">Array of friend's names (make sure they are unique).</param>
  1200. /// <param name="options">Options that affect the result of the FindFriends operation.</param>
  1201. /// <returns>If the operation could be sent (requires connection).</returns>
  1202. public bool OpFindFriends(string[] friendsToFind, FindFriendsOptions options = null)
  1203. {
  1204. if (!this.CheckIfOpCanBeSent(OperationCode.FindFriends, this.Server, "FindFriends"))
  1205. {
  1206. return false;
  1207. }
  1208. if (this.IsFetchingFriendList)
  1209. {
  1210. this.DebugReturn(DebugLevel.WARNING, "OpFindFriends skipped: already fetching friends list.");
  1211. return false; // fetching friends currently, so don't do it again (avoid changing the list while fetching friends)
  1212. }
  1213. if (friendsToFind == null || friendsToFind.Length == 0)
  1214. {
  1215. this.DebugReturn(DebugLevel.ERROR, "OpFindFriends skipped: friendsToFind array is null or empty.");
  1216. return false;
  1217. }
  1218. if (friendsToFind.Length > FriendRequestListMax)
  1219. {
  1220. this.DebugReturn(DebugLevel.ERROR, string.Format("OpFindFriends skipped: friendsToFind array exceeds allowed length of {0}.", FriendRequestListMax));
  1221. return false;
  1222. }
  1223. List<string> friendsList = new List<string>(friendsToFind.Length);
  1224. for (int i = 0; i < friendsToFind.Length; i++)
  1225. {
  1226. string friendUserId = friendsToFind[i];
  1227. if (string.IsNullOrEmpty(friendUserId))
  1228. {
  1229. this.DebugReturn(DebugLevel.WARNING,
  1230. string.Format(
  1231. "friendsToFind array contains a null or empty UserId, element at position {0} skipped.",
  1232. i));
  1233. }
  1234. else if (friendUserId.Equals(UserId))
  1235. {
  1236. this.DebugReturn(DebugLevel.WARNING,
  1237. string.Format(
  1238. "friendsToFind array contains local player's UserId \"{0}\", element at position {1} skipped.",
  1239. friendUserId,
  1240. i));
  1241. }
  1242. else if (friendsList.Contains(friendUserId))
  1243. {
  1244. this.DebugReturn(DebugLevel.WARNING,
  1245. string.Format(
  1246. "friendsToFind array contains duplicate UserId \"{0}\", element at position {1} skipped.",
  1247. friendUserId,
  1248. i));
  1249. }
  1250. else
  1251. {
  1252. friendsList.Add(friendUserId);
  1253. }
  1254. }
  1255. if (friendsList.Count == 0)
  1256. {
  1257. this.DebugReturn(DebugLevel.ERROR, "OpFindFriends skipped: friends list to find is empty.");
  1258. return false;
  1259. }
  1260. string[] filteredArray = friendsList.ToArray();
  1261. bool sent = this.LoadBalancingPeer.OpFindFriends(filteredArray, options);
  1262. this.friendListRequested = sent ? filteredArray : null;
  1263. return sent;
  1264. }
  1265. /// <summary>If already connected to a Master Server, this joins the specified lobby. This request triggers an OnOperationResponse() call and the callback OnJoinedLobby().</summary>
  1266. /// <param name="lobby">The lobby to join. Use null for default lobby.</param>
  1267. /// <returns>If the operation could be sent. False, if the client is not IsConnectedAndReady or when it's not connected to a Master Server.</returns>
  1268. public bool OpJoinLobby(TypedLobby lobby)
  1269. {
  1270. if (!this.CheckIfOpCanBeSent(OperationCode.JoinLobby, this.Server, "JoinLobby"))
  1271. {
  1272. return false;
  1273. }
  1274. if (lobby == null)
  1275. {
  1276. lobby = TypedLobby.Default;
  1277. }
  1278. bool sent = this.LoadBalancingPeer.OpJoinLobby(lobby);
  1279. if (sent)
  1280. {
  1281. this.CurrentLobby = lobby;
  1282. this.State = ClientState.JoiningLobby;
  1283. }
  1284. return sent;
  1285. }
  1286. /// <summary>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).</summary>
  1287. /// <returns>If the operation could be sent (has to be connected).</returns>
  1288. public bool OpLeaveLobby()
  1289. {
  1290. if (!this.CheckIfOpCanBeSent(OperationCode.LeaveLobby, this.Server, "LeaveLobby"))
  1291. {
  1292. return false;
  1293. }
  1294. return this.LoadBalancingPeer.OpLeaveLobby();
  1295. }
  1296. /// <summary>
  1297. /// Joins a random room that matches the filter. Will callback: OnJoinedRoom or OnJoinRandomFailed.
  1298. /// </summary>
  1299. /// <remarks>
  1300. /// Used for random matchmaking. You can join any room or one with specific properties defined in opJoinRandomRoomParams.
  1301. ///
  1302. /// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms.
  1303. /// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties.
  1304. /// You need to define which Custom Room Properties will be available for matchmaking when you create a room.
  1305. /// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby)
  1306. ///
  1307. /// This operation fails if no rooms are fitting or available (all full, closed or not visible).
  1308. /// It may also fail when actually joining the room which was found. Rooms may close, become full or empty anytime.
  1309. ///
  1310. /// This method can only be called while the client is connected to a Master Server so you should
  1311. /// implement the callback OnConnectedToMaster.
  1312. /// Check the return value to make sure the operation will be called on the server.
  1313. /// Note: There will be no callbacks if this method returned false.
  1314. ///
  1315. ///
  1316. /// This client's State is set to ClientState.Joining immediately, when the operation could
  1317. /// be called. In the background, the client will switch servers and call various related operations.
  1318. ///
  1319. /// When you're in the room, this client's State will become ClientState.Joined.
  1320. ///
  1321. ///
  1322. /// When entering a room, this client's Player Custom Properties will be sent to the room.
  1323. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
  1324. /// Note that the player properties will be cached locally and are not wiped when leaving a room.
  1325. ///
  1326. /// More about matchmaking:
  1327. /// https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby
  1328. ///
  1329. /// You can define an array of expectedUsers, to block player slots in the room for these users.
  1330. /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
  1331. /// </remarks>
  1332. /// <param name="opJoinRandomRoomParams">Optional definition of properties to filter rooms in random matchmaking.</param>
  1333. /// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
  1334. public bool OpJoinRandomRoom(OpJoinRandomRoomParams opJoinRandomRoomParams = null)
  1335. {
  1336. if (!this.CheckIfOpCanBeSent(OperationCode.JoinRandomGame, this.Server, "JoinRandomGame"))
  1337. {
  1338. return false;
  1339. }
  1340. if (opJoinRandomRoomParams == null)
  1341. {
  1342. opJoinRandomRoomParams = new OpJoinRandomRoomParams();
  1343. }
  1344. this.enterRoomParamsCache = new EnterRoomParams();
  1345. this.enterRoomParamsCache.Lobby = opJoinRandomRoomParams.TypedLobby;
  1346. this.enterRoomParamsCache.ExpectedUsers = opJoinRandomRoomParams.ExpectedUsers;
  1347. bool sending = this.LoadBalancingPeer.OpJoinRandomRoom(opJoinRandomRoomParams);
  1348. if (sending)
  1349. {
  1350. this.lastJoinType = JoinType.JoinRandomRoom;
  1351. this.State = ClientState.Joining;
  1352. }
  1353. return sending;
  1354. }
  1355. /// <summary>
  1356. /// Attempts to join a room that matches the specified filter and creates a room if none found.
  1357. /// </summary>
  1358. /// <remarks>
  1359. /// This operation is a combination of filter-based random matchmaking with the option to create a new room,
  1360. /// if no fitting room exists.
  1361. /// The benefit of that is that the room creation is done by the same operation and the room can be found
  1362. /// by the very next client, looking for similar rooms.
  1363. ///
  1364. /// There are separate parameters for joining and creating a room.
  1365. ///
  1366. /// This method can only be called while connected to a Master Server.
  1367. /// This client's State is set to ClientState.Joining immediately.
  1368. ///
  1369. /// Either IMatchmakingCallbacks.OnJoinedRoom or IMatchmakingCallbacks.OnCreatedRoom get called.
  1370. ///
  1371. /// More about matchmaking:
  1372. /// https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby
  1373. ///
  1374. /// Check the return value to make sure the operation will be called on the server.
  1375. /// Note: There will be no callbacks if this method returned false.
  1376. /// </remarks>
  1377. /// <returns>If the operation will be sent (requires connection to Master Server).</returns>
  1378. public bool OpJoinRandomOrCreateRoom(OpJoinRandomRoomParams opJoinRandomRoomParams, EnterRoomParams createRoomParams)
  1379. {
  1380. if (!this.CheckIfOpCanBeSent(OperationCode.JoinRandomGame, this.Server, "OpJoinRandomOrCreateRoom"))
  1381. {
  1382. return false;
  1383. }
  1384. if (opJoinRandomRoomParams == null)
  1385. {
  1386. opJoinRandomRoomParams = new OpJoinRandomRoomParams();
  1387. }
  1388. if (createRoomParams == null)
  1389. {
  1390. createRoomParams = new EnterRoomParams();
  1391. }
  1392. createRoomParams.JoinMode = JoinMode.CreateIfNotExists;
  1393. this.enterRoomParamsCache = createRoomParams;
  1394. this.enterRoomParamsCache.Lobby = opJoinRandomRoomParams.TypedLobby;
  1395. this.enterRoomParamsCache.ExpectedUsers = opJoinRandomRoomParams.ExpectedUsers;
  1396. bool sending = this.LoadBalancingPeer.OpJoinRandomOrCreateRoom(opJoinRandomRoomParams, createRoomParams);
  1397. if (sending)
  1398. {
  1399. this.lastJoinType = JoinType.JoinRandomOrCreateRoom;
  1400. this.State = ClientState.Joining;
  1401. }
  1402. return sending;
  1403. }
  1404. /// <summary>
  1405. /// Creates a new room. Will callback: OnCreatedRoom and OnJoinedRoom or OnCreateRoomFailed.
  1406. /// </summary>
  1407. /// <remarks>
  1408. /// When successful, the client will enter the specified room and callback both OnCreatedRoom and OnJoinedRoom.
  1409. /// In all error cases, OnCreateRoomFailed gets called.
  1410. ///
  1411. /// Creating a room will fail if the room name is already in use or when the RoomOptions clashing
  1412. /// with one another. Check the EnterRoomParams reference for the various room creation options.
  1413. ///
  1414. ///
  1415. /// This method can only be called while the client is connected to a Master Server so you should
  1416. /// implement the callback OnConnectedToMaster.
  1417. /// Check the return value to make sure the operation will be called on the server.
  1418. /// Note: There will be no callbacks if this method returned false.
  1419. ///
  1420. ///
  1421. /// When you're in the room, this client's State will become ClientState.Joined.
  1422. ///
  1423. ///
  1424. /// When entering a room, this client's Player Custom Properties will be sent to the room.
  1425. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
  1426. /// Note that the player properties will be cached locally and are not wiped when leaving a room.
  1427. ///
  1428. /// You can define an array of expectedUsers, to block player slots in the room for these users.
  1429. /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
  1430. /// </remarks>
  1431. /// <param name="enterRoomParams">Definition of properties for the room to create.</param>
  1432. /// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
  1433. public bool OpCreateRoom(EnterRoomParams enterRoomParams)
  1434. {
  1435. if (!this.CheckIfOpCanBeSent(OperationCode.CreateGame, this.Server, "CreateGame"))
  1436. {
  1437. return false;
  1438. }
  1439. bool onGameServer = this.Server == ServerConnection.GameServer;
  1440. enterRoomParams.OnGameServer = onGameServer;
  1441. if (!onGameServer)
  1442. {
  1443. this.enterRoomParamsCache = enterRoomParams;
  1444. }
  1445. bool sending = this.LoadBalancingPeer.OpCreateRoom(enterRoomParams);
  1446. if (sending)
  1447. {
  1448. this.lastJoinType = JoinType.CreateRoom;
  1449. this.State = ClientState.Joining;
  1450. }
  1451. return sending;
  1452. }
  1453. /// <summary>
  1454. /// Joins a specific room by name and creates it on demand. Will callback: OnJoinedRoom or OnJoinRoomFailed.
  1455. /// </summary>
  1456. /// <remarks>
  1457. /// Useful when players make up a room name to meet in:
  1458. /// All involved clients call the same method and whoever is first, also creates the room.
  1459. ///
  1460. /// When successful, the client will enter the specified room.
  1461. /// The client which creates the room, will callback both OnCreatedRoom and OnJoinedRoom.
  1462. /// Clients that join an existing room will only callback OnJoinedRoom.
  1463. /// In all error cases, OnJoinRoomFailed gets called.
  1464. ///
  1465. /// Joining a room will fail, if the room is full, closed or when the user
  1466. /// already is present in the room (checked by userId).
  1467. ///
  1468. /// To return to a room, use OpRejoinRoom.
  1469. ///
  1470. /// This method can only be called while the client is connected to a Master Server so you should
  1471. /// implement the callback OnConnectedToMaster.
  1472. /// Check the return value to make sure the operation will be called on the server.
  1473. /// Note: There will be no callbacks if this method returned false.
  1474. ///
  1475. /// This client's State is set to ClientState.Joining immediately, when the operation could
  1476. /// be called. In the background, the client will switch servers and call various related operations.
  1477. ///
  1478. /// When you're in the room, this client's State will become ClientState.Joined.
  1479. ///
  1480. ///
  1481. /// If you set room properties in roomOptions, they get ignored when the room is existing already.
  1482. /// This avoids changing the room properties by late joining players.
  1483. ///
  1484. /// When entering a room, this client's Player Custom Properties will be sent to the room.
  1485. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
  1486. /// Note that the player properties will be cached locally and are not wiped when leaving a room.
  1487. ///
  1488. /// You can define an array of expectedUsers, to block player slots in the room for these users.
  1489. /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
  1490. /// </remarks>
  1491. /// <param name="enterRoomParams">Definition of properties for the room to create or join.</param>
  1492. /// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
  1493. public bool OpJoinOrCreateRoom(EnterRoomParams enterRoomParams)
  1494. {
  1495. bool onGameServer = this.Server == ServerConnection.GameServer;
  1496. enterRoomParams.JoinMode = JoinMode.CreateIfNotExists;
  1497. enterRoomParams.OnGameServer = onGameServer;
  1498. if (!onGameServer)
  1499. {
  1500. this.enterRoomParamsCache = enterRoomParams;
  1501. }
  1502. bool sending = this.LoadBalancingPeer.OpJoinRoom(enterRoomParams);
  1503. if (sending)
  1504. {
  1505. this.lastJoinType = JoinType.JoinOrCreateRoom;
  1506. this.State = ClientState.Joining;
  1507. }
  1508. return sending;
  1509. }
  1510. /// <summary>
  1511. /// Joins a room by name. Will callback: OnJoinedRoom or OnJoinRoomFailed.
  1512. /// </summary>
  1513. /// <remarks>
  1514. /// Useful when using lobbies or when players follow friends or invite each other.
  1515. ///
  1516. /// When successful, the client will enter the specified room and callback via OnJoinedRoom.
  1517. /// In all error cases, OnJoinRoomFailed gets called.
  1518. ///
  1519. /// Joining a room will fail if the room is full, closed, not existing or when the user
  1520. /// already is present in the room (checked by userId).
  1521. ///
  1522. /// To return to a room, use OpRejoinRoom.
  1523. /// When players invite each other and it's unclear who's first to respond, use OpJoinOrCreateRoom instead.
  1524. ///
  1525. /// This method can only be called while the client is connected to a Master Server so you should
  1526. /// implement the callback OnConnectedToMaster.
  1527. /// Check the return value to make sure the operation will be called on the server.
  1528. /// Note: There will be no callbacks if this method returned false.
  1529. ///
  1530. /// A room's name has to be unique (per region, appid and gameversion).
  1531. /// When your title uses a global matchmaking or invitations (e.g. an external solution),
  1532. /// keep regions and the game versions in mind to join a room.
  1533. ///
  1534. ///
  1535. /// This client's State is set to ClientState.Joining immediately, when the operation could
  1536. /// be called. In the background, the client will switch servers and call various related operations.
  1537. ///
  1538. /// When you're in the room, this client's State will become ClientState.Joined.
  1539. ///
  1540. ///
  1541. /// When entering a room, this client's Player Custom Properties will be sent to the room.
  1542. /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
  1543. /// Note that the player properties will be cached locally and are not wiped when leaving a room.
  1544. ///
  1545. /// You can define an array of expectedUsers, to reserve player slots in the room for friends or party members.
  1546. /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
  1547. /// </remarks>
  1548. /// <param name="enterRoomParams">Definition of properties for the room to join.</param>
  1549. /// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
  1550. public bool OpJoinRoom(EnterRoomParams enterRoomParams)
  1551. {
  1552. if (!this.CheckIfOpCanBeSent(OperationCode.JoinGame, this.Server, "JoinRoom"))
  1553. {
  1554. return false;
  1555. }
  1556. bool onGameServer = this.Server == ServerConnection.GameServer;
  1557. enterRoomParams.OnGameServer = onGameServer;
  1558. if (!onGameServer)
  1559. {
  1560. this.enterRoomParamsCache = enterRoomParams;
  1561. }
  1562. bool sending = this.LoadBalancingPeer.OpJoinRoom(enterRoomParams);
  1563. if (sending)
  1564. {
  1565. this.lastJoinType = enterRoomParams.JoinMode == JoinMode.CreateIfNotExists ? JoinType.JoinOrCreateRoom : JoinType.JoinRoom;
  1566. this.State = ClientState.Joining;
  1567. }
  1568. return sending;
  1569. }
  1570. /// <summary>
  1571. /// Rejoins a room by roomName (using the userID internally to return). Will callback: OnJoinedRoom or OnJoinRoomFailed.
  1572. /// </summary>
  1573. /// <remarks>
  1574. /// Used to return to a room, before this user was removed from the players list.
  1575. /// Internally, the userID will be checked by the server, to make sure this user is in the room (active or inactice).
  1576. ///
  1577. /// In contrast to join, this operation never adds a players to a room. It will attempt to retake an existing
  1578. /// spot in the playerlist or fail. This makes sure the client doean't accidentally join a room when the
  1579. /// game logic meant to re-activate an existing actor in an existing room.
  1580. ///
  1581. /// This method will fail on the server, when the room does not exist, can't be loaded (persistent rooms) or
  1582. /// when the userId is not in the player list of this room. This will lead to a callback OnJoinRoomFailed.
  1583. ///
  1584. /// Rejoining room will not send any player properties. Instead client will receive up-to-date ones from server.
  1585. /// If you want to set new player properties, do it once rejoined.
  1586. /// </remarks>
  1587. public bool OpRejoinRoom(string roomName)
  1588. {
  1589. bool onGameServer = this.Server == ServerConnection.GameServer;
  1590. EnterRoomParams opParams = new EnterRoomParams();
  1591. this.enterRoomParamsCache = opParams;
  1592. opParams.RoomName = roomName;
  1593. opParams.OnGameServer = onGameServer;
  1594. opParams.JoinMode = JoinMode.RejoinOnly;
  1595. bool sending = this.LoadBalancingPeer.OpJoinRoom(opParams);
  1596. if (sending)
  1597. {
  1598. this.lastJoinType = JoinType.JoinRoom;
  1599. this.State = ClientState.Joining;
  1600. }
  1601. return sending;
  1602. }
  1603. /// <summary>
  1604. /// Leaves the current room, optionally telling the server that the user is just becoming inactive. Will callback: OnLeftRoom.
  1605. /// </summary>
  1606. ///
  1607. /// <remarks>
  1608. /// OpLeaveRoom skips execution when the room is null or the server is not GameServer or the client is disconnecting from GS already.
  1609. /// OpLeaveRoom returns false in those cases and won't change the state, so check return of this method.
  1610. ///
  1611. /// In some cases, this method will skip the OpLeave call and just call Disconnect(),
  1612. /// which not only leaves the room but also the server. Disconnect also triggers a leave and so that workflow is is quicker.
  1613. /// </remarks>
  1614. /// <param name="becomeInactive">If true, this player becomes inactive in the game and can return later (if PlayerTTL of the room is != 0).</param>
  1615. /// <param name="sendAuthCookie">WebFlag: Securely transmit the encrypted object AuthCookie to the web service in PathLeave webhook when available</param>
  1616. /// <returns>If the current room could be left (impossible while not in a room).</returns>
  1617. public bool OpLeaveRoom(bool becomeInactive, bool sendAuthCookie = false)
  1618. {
  1619. if (!this.CheckIfOpCanBeSent(OperationCode.Leave, this.Server, "LeaveRoom"))
  1620. {
  1621. return false;
  1622. }
  1623. this.State = ClientState.Leaving;
  1624. this.GameServerAddress = String.Empty;
  1625. this.enterRoomParamsCache = null;
  1626. return this.LoadBalancingPeer.OpLeaveRoom(becomeInactive, sendAuthCookie);
  1627. }
  1628. /// <summary>Gets a list of rooms matching the (non empty) SQL filter for the given SQL-typed lobby.</summary>
  1629. /// <remarks>
  1630. /// Operation is only available for lobbies of type SqlLobby and the filter can not be empty.
  1631. /// It will check those conditions and fail locally, returning false.
  1632. ///
  1633. /// This is an async request which triggers a OnOperationResponse() call.
  1634. /// </remarks>
  1635. /// <see cref="https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby#sql_lobby_type"/>
  1636. /// <param name="typedLobby">The lobby to query. Has to be of type SqlLobby.</param>
  1637. /// <param name="sqlLobbyFilter">The sql query statement.</param>
  1638. /// <returns>If the operation could be sent (has to be connected).</returns>
  1639. public bool OpGetGameList(TypedLobby typedLobby, string sqlLobbyFilter)
  1640. {
  1641. if (!this.CheckIfOpCanBeSent(OperationCode.GetGameList, this.Server, "GetGameList"))
  1642. {
  1643. return false;
  1644. }
  1645. if (string.IsNullOrEmpty(sqlLobbyFilter))
  1646. {
  1647. this.DebugReturn(DebugLevel.ERROR, "Operation GetGameList requires a filter.");
  1648. return false;
  1649. }
  1650. if (typedLobby.Type != LobbyType.SqlLobby)
  1651. {
  1652. this.DebugReturn(DebugLevel.ERROR, "Operation GetGameList can only be used for lobbies of type SqlLobby.");
  1653. return false;
  1654. }
  1655. return this.LoadBalancingPeer.OpGetGameList(typedLobby, sqlLobbyFilter);
  1656. }
  1657. /// <summary>
  1658. /// Updates and synchronizes a Player's Custom Properties. Optionally, expectedProperties can be provided as condition.
  1659. /// </summary>
  1660. /// <remarks>
  1661. /// Custom Properties are a set of string keys and arbitrary values which is synchronized
  1662. /// for the players in a Room. They are available when the client enters the room, as
  1663. /// they are in the response of OpJoin and OpCreate.
  1664. ///
  1665. /// Custom Properties either relate to the (current) Room or a Player (in that Room).
  1666. ///
  1667. /// Both classes locally cache the current key/values and make them available as
  1668. /// property: CustomProperties. This is provided only to read them.
  1669. /// You must use the method SetCustomProperties to set/modify them.
  1670. ///
  1671. /// Any client can set any Custom Properties anytime (when in a room).
  1672. /// It's up to the game logic to organize how they are best used.
  1673. ///
  1674. /// You should call SetCustomProperties only with key/values that are new or changed. This reduces
  1675. /// traffic and performance.
  1676. ///
  1677. /// Unless you define some expectedProperties, setting key/values is always permitted.
  1678. /// In this case, the property-setting client will not receive the new values from the server but
  1679. /// instead update its local cache in SetCustomProperties.
  1680. ///
  1681. /// If you define expectedProperties, the server will skip updates if the server property-cache
  1682. /// does not contain all expectedProperties with the same values.
  1683. /// In this case, the property-setting client will get an update from the server and update it's
  1684. /// cached key/values at about the same time as everyone else.
  1685. ///
  1686. /// The benefit of using expectedProperties can be only one client successfully sets a key from
  1687. /// one known value to another.
  1688. /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
  1689. /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
  1690. /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
  1691. /// take the item will have it (and the others fail to set the ownership).
  1692. ///
  1693. /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
  1694. /// </remarks>
  1695. /// <param name="actorNr">Defines which player the Custom Properties belong to. ActorID of a player.</param>
  1696. /// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
  1697. /// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values. Client must be in room.</param>
  1698. /// <param name="webFlags">Defines if the set properties should be forwarded to a WebHook. Client must be in room.</param>
  1699. /// <returns>
  1700. /// False if propertiesToSet is null or empty or have zero string keys.
  1701. /// If not in a room, returns true if local player and expectedProperties and webFlags are null.
  1702. /// False if actorNr is lower than or equal to zero.
  1703. /// Otherwise, returns if the operation could be sent to the server.
  1704. /// </returns>
  1705. public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
  1706. {
  1707. if (propertiesToSet == null || propertiesToSet.Count == 0)
  1708. {
  1709. this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. propertiesToSet must not be null nor empty.");
  1710. return false;
  1711. }
  1712. if (this.CurrentRoom == null)
  1713. {
  1714. // if you attempt to set this player's values without conditions, then fine:
  1715. if (expectedProperties == null && webFlags == null && this.LocalPlayer != null && this.LocalPlayer.ActorNumber == actorNr)
  1716. {
  1717. return this.LocalPlayer.SetCustomProperties(propertiesToSet);
  1718. }
  1719. if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ERROR)
  1720. {
  1721. this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. To use expectedProperties or webForward, you have to be in a room. State: " + this.State);
  1722. }
  1723. return false;
  1724. }
  1725. Hashtable customActorProperties = new Hashtable();
  1726. customActorProperties.MergeStringKeys(propertiesToSet);
  1727. if (customActorProperties.Count == 0)
  1728. {
  1729. this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. Only string keys allowed for custom properties.");
  1730. return false;
  1731. }
  1732. return this.OpSetPropertiesOfActor(actorNr, customActorProperties, expectedProperties, webFlags);
  1733. }
  1734. /// <summary>Internally used to cache and set properties (including well known properties).</summary>
  1735. /// <remarks>Requires being in a room (because this attempts to send an operation which will fail otherwise).</remarks>
  1736. protected internal bool OpSetPropertiesOfActor(int actorNr, Hashtable actorProperties, Hashtable expectedProperties = null, WebFlags webFlags = null)
  1737. {
  1738. if (!this.CheckIfOpCanBeSent(OperationCode.SetProperties, this.Server, "SetProperties"))
  1739. {
  1740. return false;
  1741. }
  1742. if (actorProperties == null || actorProperties.Count == 0)
  1743. {
  1744. this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfActor() failed. actorProperties must not be null nor empty.");
  1745. return false;
  1746. }
  1747. bool res = this.LoadBalancingPeer.OpSetPropertiesOfActor(actorNr, actorProperties, expectedProperties, webFlags);
  1748. if (res && !this.CurrentRoom.BroadcastPropertiesChangeToAll && (expectedProperties == null || expectedProperties.Count == 0))
  1749. {
  1750. Player target = this.CurrentRoom.GetPlayer(actorNr);
  1751. if (target != null)
  1752. {
  1753. target.InternalCacheProperties(actorProperties);
  1754. this.InRoomCallbackTargets.OnPlayerPropertiesUpdate(target, actorProperties);
  1755. }
  1756. }
  1757. return res;
  1758. }
  1759. /// <summary>
  1760. /// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition.
  1761. /// </summary>
  1762. /// <remarks>
  1763. /// Custom Properties are a set of string keys and arbitrary values which is synchronized
  1764. /// for the players in a Room. They are available when the client enters the room, as
  1765. /// they are in the response of OpJoin and OpCreate.
  1766. ///
  1767. /// Custom Properties either relate to the (current) Room or a Player (in that Room).
  1768. ///
  1769. /// Both classes locally cache the current key/values and make them available as
  1770. /// property: CustomProperties. This is provided only to read them.
  1771. /// You must use the method SetCustomProperties to set/modify them.
  1772. ///
  1773. /// Any client can set any Custom Properties anytime (when in a room).
  1774. /// It's up to the game logic to organize how they are best used.
  1775. ///
  1776. /// You should call SetCustomProperties only with key/values that are new or changed. This reduces
  1777. /// traffic and performance.
  1778. ///
  1779. /// Unless you define some expectedProperties, setting key/values is always permitted.
  1780. /// In this case, the property-setting client will not receive the new values from the server but
  1781. /// instead update its local cache in SetCustomProperties.
  1782. ///
  1783. /// If you define expectedProperties, the server will skip updates if the server property-cache
  1784. /// does not contain all expectedProperties with the same values.
  1785. /// In this case, the property-setting client will get an update from the server and update it's
  1786. /// cached key/values at about the same time as everyone else.
  1787. ///
  1788. /// The benefit of using expectedProperties can be only one client successfully sets a key from
  1789. /// one known value to another.
  1790. /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
  1791. /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
  1792. /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
  1793. /// take the item will have it (and the others fail to set the ownership).
  1794. ///
  1795. /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
  1796. /// </remarks>
  1797. /// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
  1798. /// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values.</param>
  1799. /// <param name="webFlags">Defines web flags for an optional PathProperties webhook.</param>
  1800. /// <returns>
  1801. /// False if propertiesToSet is null or empty or have zero string keys.
  1802. /// Otherwise, returns if the operation could be sent to the server.
  1803. /// </returns>
  1804. public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
  1805. {
  1806. if (propertiesToSet == null || propertiesToSet.Count == 0)
  1807. {
  1808. this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfRoom() failed. propertiesToSet must not be null nor empty.");
  1809. return false;
  1810. }
  1811. Hashtable customGameProps = new Hashtable();
  1812. customGameProps.MergeStringKeys(propertiesToSet);
  1813. if (customGameProps.Count == 0)
  1814. {
  1815. this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfRoom() failed. Only string keys are allowed for custom properties.");
  1816. return false;
  1817. }
  1818. return this.OpSetPropertiesOfRoom(customGameProps, expectedProperties, webFlags);
  1819. }
  1820. protected internal bool OpSetPropertyOfRoom(byte propCode, object value)
  1821. {
  1822. Hashtable properties = new Hashtable();
  1823. properties[propCode] = value;
  1824. return this.OpSetPropertiesOfRoom(properties);
  1825. }
  1826. /// <summary>Internally used to cache and set properties (including well known properties).</summary>
  1827. /// <remarks>Requires being in a room (because this attempts to send an operation which will fail otherwise).</remarks>
  1828. protected internal bool OpSetPropertiesOfRoom(Hashtable gameProperties, Hashtable expectedProperties = null, WebFlags webFlags = null)
  1829. {
  1830. if (!this.CheckIfOpCanBeSent(OperationCode.SetProperties, this.Server, "SetProperties"))
  1831. {
  1832. return false;
  1833. }
  1834. if (gameProperties == null || gameProperties.Count == 0)
  1835. {
  1836. this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfRoom() failed. gameProperties must not be null nor empty.");
  1837. return false;
  1838. }
  1839. bool res = this.LoadBalancingPeer.OpSetPropertiesOfRoom(gameProperties, expectedProperties, webFlags);
  1840. if (res && !this.CurrentRoom.BroadcastPropertiesChangeToAll && (expectedProperties == null || expectedProperties.Count == 0))
  1841. {
  1842. this.CurrentRoom.InternalCacheProperties(gameProperties);
  1843. this.InRoomCallbackTargets.OnRoomPropertiesUpdate(gameProperties);
  1844. }
  1845. return res;
  1846. }
  1847. /// <summary>
  1848. /// Send an event with custom code/type and any content to the other players in the same room.
  1849. /// </summary>
  1850. /// <param name="eventCode">Identifies this type of event (and the content). Your game's event codes can start with 0.</param>
  1851. /// <param name="customEventContent">Any serializable datatype (including Hashtable like the other OpRaiseEvent overloads).</param>
  1852. /// <param name="raiseEventOptions">Contains used send options. If you pass null, the default options will be used.</param>
  1853. /// <param name="sendOptions">Send options for reliable, encryption etc</param>
  1854. /// <returns>If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands.</returns>
  1855. public virtual bool OpRaiseEvent(byte eventCode, object customEventContent, RaiseEventOptions raiseEventOptions, SendOptions sendOptions)
  1856. {
  1857. if (this.LoadBalancingPeer == null)
  1858. {
  1859. return false;
  1860. }
  1861. if (!this.CheckIfOpCanBeSent(OperationCode.RaiseEvent, this.Server, "RaiseEvent"))
  1862. {
  1863. return false;
  1864. }
  1865. return this.LoadBalancingPeer.OpRaiseEvent(eventCode, customEventContent, raiseEventOptions, sendOptions);
  1866. }
  1867. /// <summary>
  1868. /// Operation to handle this client's interest groups (for events in room).
  1869. /// </summary>
  1870. /// <remarks>
  1871. /// Note the difference between passing null and byte[0]:
  1872. /// null won't add/remove any groups.
  1873. /// byte[0] will add/remove all (existing) groups.
  1874. /// First, removing groups is executed. This way, you could leave all groups and join only the ones provided.
  1875. ///
  1876. /// Changes become active not immediately but when the server executes this operation (approximately RTT/2).
  1877. /// </remarks>
  1878. /// <param name="groupsToRemove">Groups to remove from interest. Null will not remove any. A byte[0] will remove all.</param>
  1879. /// <param name="groupsToAdd">Groups to add to interest. Null will not add any. A byte[0] will add all current.</param>
  1880. /// <returns>If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands.</returns>
  1881. public virtual bool OpChangeGroups(byte[] groupsToRemove, byte[] groupsToAdd)
  1882. {
  1883. if (this.LoadBalancingPeer == null)
  1884. {
  1885. return false;
  1886. }
  1887. if (!this.CheckIfOpCanBeSent(OperationCode.ChangeGroups, this.Server, "ChangeGroups"))
  1888. {
  1889. return false;
  1890. }
  1891. return this.LoadBalancingPeer.OpChangeGroups(groupsToRemove, groupsToAdd);
  1892. }
  1893. #endregion
  1894. #region Helpers
  1895. /// <summary>
  1896. /// Privately used to read-out properties coming from the server in events and operation responses (which might be a bit tricky).
  1897. /// </summary>
  1898. private void ReadoutProperties(Hashtable gameProperties, Hashtable actorProperties, int targetActorNr)
  1899. {
  1900. // read game properties and cache them locally
  1901. if (this.CurrentRoom != null && gameProperties != null)
  1902. {
  1903. this.CurrentRoom.InternalCacheProperties(gameProperties);
  1904. if (this.InRoom)
  1905. {
  1906. this.InRoomCallbackTargets.OnRoomPropertiesUpdate(gameProperties);
  1907. }
  1908. }
  1909. if (actorProperties != null && actorProperties.Count > 0)
  1910. {
  1911. if (targetActorNr > 0)
  1912. {
  1913. // we have a single entry in the actorProperties with one user's name
  1914. // targets MUST exist before you set properties
  1915. Player target = this.CurrentRoom.GetPlayer(targetActorNr);
  1916. if (target != null)
  1917. {
  1918. Hashtable props = this.ReadoutPropertiesForActorNr(actorProperties, targetActorNr);
  1919. target.InternalCacheProperties(props);
  1920. this.InRoomCallbackTargets.OnPlayerPropertiesUpdate(target, props);
  1921. }
  1922. }
  1923. else
  1924. {
  1925. // in this case, we've got a key-value pair per actor (each
  1926. // value is a hashtable with the actor's properties then)
  1927. int actorNr;
  1928. Hashtable props;
  1929. string newName;
  1930. Player target;
  1931. foreach (object key in actorProperties.Keys)
  1932. {
  1933. actorNr = (int)key;
  1934. props = (Hashtable)actorProperties[key];
  1935. newName = (string)props[ActorProperties.PlayerName];
  1936. target = this.CurrentRoom.GetPlayer(actorNr);
  1937. if (target == null)
  1938. {
  1939. target = this.CreatePlayer(newName, actorNr, false, props);
  1940. this.CurrentRoom.StorePlayer(target);
  1941. }
  1942. target.InternalCacheProperties(props);
  1943. }
  1944. }
  1945. }
  1946. }
  1947. /// <summary>
  1948. /// Privately used only to read properties for a distinct actor (which might be the hashtable OR a key-pair value IN the actorProperties).
  1949. /// </summary>
  1950. private Hashtable ReadoutPropertiesForActorNr(Hashtable actorProperties, int actorNr)
  1951. {
  1952. if (actorProperties.ContainsKey(actorNr))
  1953. {
  1954. return (Hashtable)actorProperties[actorNr];
  1955. }
  1956. return actorProperties;
  1957. }
  1958. /// <summary>
  1959. /// Internally used to set the LocalPlayer's ID (from -1 to the actual in-room ID).
  1960. /// </summary>
  1961. /// <param name="newID">New actor ID (a.k.a actorNr) assigned when joining a room.</param>
  1962. public void ChangeLocalID(int newID)
  1963. {
  1964. if (this.LocalPlayer == null)
  1965. {
  1966. 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));
  1967. }
  1968. if (this.CurrentRoom == null)
  1969. {
  1970. // change to new actor/player ID and make sure the player does not have a room reference left
  1971. this.LocalPlayer.ChangeLocalID(newID);
  1972. this.LocalPlayer.RoomReference = null;
  1973. }
  1974. else
  1975. {
  1976. // remove old actorId from actor list
  1977. this.CurrentRoom.RemovePlayer(this.LocalPlayer);
  1978. // change to new actor/player ID
  1979. this.LocalPlayer.ChangeLocalID(newID);
  1980. // update the room's list with the new reference
  1981. this.CurrentRoom.StorePlayer(this.LocalPlayer);
  1982. }
  1983. }
  1984. /// <summary>
  1985. /// Called internally, when a game was joined or created on the game server successfully.
  1986. /// </summary>
  1987. /// <remarks>
  1988. /// This reads the response, finds out the local player's actorNumber (a.k.a. Player.ID) and applies properties of the room and players.
  1989. /// Errors for these operations are to be handled before this method is called.
  1990. /// </remarks>
  1991. /// <param name="operationResponse">Contains the server's response for an operation called by this peer.</param>
  1992. private void GameEnteredOnGameServer(OperationResponse operationResponse)
  1993. {
  1994. this.CurrentRoom = this.CreateRoom(this.enterRoomParamsCache.RoomName, this.enterRoomParamsCache.RoomOptions);
  1995. this.CurrentRoom.LoadBalancingClient = this;
  1996. // first change the local id, instead of first updating the actorList since actorList uses ID to update itself
  1997. // the local player's actor-properties are not returned in join-result. add this player to the list
  1998. int localActorNr = (int)operationResponse[ParameterCode.ActorNr];
  1999. this.ChangeLocalID(localActorNr);
  2000. if (operationResponse.Parameters.ContainsKey(ParameterCode.ActorList))
  2001. {
  2002. int[] actorsInRoom = (int[])operationResponse.Parameters[ParameterCode.ActorList];
  2003. this.UpdatedActorList(actorsInRoom);
  2004. }
  2005. Hashtable actorProperties = (Hashtable)operationResponse[ParameterCode.PlayerProperties];
  2006. Hashtable gameProperties = (Hashtable)operationResponse[ParameterCode.GameProperties];
  2007. this.ReadoutProperties(gameProperties, actorProperties, 0);
  2008. object temp;
  2009. if (operationResponse.Parameters.TryGetValue(ParameterCode.RoomOptionFlags, out temp))
  2010. {
  2011. this.CurrentRoom.InternalCacheRoomFlags((int)temp);
  2012. }
  2013. this.State = ClientState.Joined;
  2014. // the callbacks OnCreatedRoom and OnJoinedRoom are called in the event join. it contains important info about the room and players.
  2015. // unless there will be no room events (RoomOptions.SuppressRoomEvents = true)
  2016. if (this.CurrentRoom.SuppressRoomEvents)
  2017. {
  2018. if (this.lastJoinType == JoinType.CreateRoom || (this.lastJoinType == JoinType.JoinOrCreateRoom && this.LocalPlayer.ActorNumber == 1))
  2019. {
  2020. this.MatchMakingCallbackTargets.OnCreatedRoom();
  2021. }
  2022. this.MatchMakingCallbackTargets.OnJoinedRoom();
  2023. }
  2024. }
  2025. private void UpdatedActorList(int[] actorsInGame)
  2026. {
  2027. if (actorsInGame != null)
  2028. {
  2029. foreach (int actorNumber in actorsInGame)
  2030. {
  2031. Player target = this.CurrentRoom.GetPlayer(actorNumber);
  2032. if (target == null)
  2033. {
  2034. this.CurrentRoom.StorePlayer(this.CreatePlayer(string.Empty, actorNumber, false, null));
  2035. }
  2036. }
  2037. }
  2038. }
  2039. /// <summary>
  2040. /// Factory method to create a player instance - override to get your own player-type with custom features.
  2041. /// </summary>
  2042. /// <param name="actorName">The name of the player to be created. </param>
  2043. /// <param name="actorNumber">The player ID (a.k.a. actorNumber) of the player to be created.</param>
  2044. /// <param name="isLocal">Sets the distinction if the player to be created is your player or if its assigned to someone else.</param>
  2045. /// <param name="actorProperties">The custom properties for this new player</param>
  2046. /// <returns>The newly created player</returns>
  2047. protected internal virtual Player CreatePlayer(string actorName, int actorNumber, bool isLocal, Hashtable actorProperties)
  2048. {
  2049. Player newPlayer = new Player(actorName, actorNumber, isLocal, actorProperties);
  2050. return newPlayer;
  2051. }
  2052. /// <summary>Internal "factory" method to create a room-instance.</summary>
  2053. protected internal virtual Room CreateRoom(string roomName, RoomOptions opt)
  2054. {
  2055. Room r = new Room(roomName, opt);
  2056. return r;
  2057. }
  2058. private bool CheckIfOpAllowedOnServer(byte opCode, ServerConnection serverConnection)
  2059. {
  2060. switch (serverConnection)
  2061. {
  2062. case ServerConnection.MasterServer:
  2063. switch (opCode)
  2064. {
  2065. case OperationCode.CreateGame:
  2066. case OperationCode.Authenticate:
  2067. case OperationCode.AuthenticateOnce:
  2068. case OperationCode.FindFriends:
  2069. case OperationCode.GetGameList:
  2070. case OperationCode.GetLobbyStats:
  2071. case OperationCode.JoinGame:
  2072. case OperationCode.JoinLobby:
  2073. case OperationCode.LeaveLobby:
  2074. case OperationCode.WebRpc:
  2075. case OperationCode.ServerSettings:
  2076. case OperationCode.JoinRandomGame:
  2077. return true;
  2078. }
  2079. break;
  2080. case ServerConnection.GameServer:
  2081. switch (opCode)
  2082. {
  2083. case OperationCode.CreateGame:
  2084. case OperationCode.Authenticate:
  2085. case OperationCode.AuthenticateOnce:
  2086. case OperationCode.ChangeGroups:
  2087. case OperationCode.GetProperties:
  2088. case OperationCode.JoinGame:
  2089. case OperationCode.Leave:
  2090. case OperationCode.WebRpc:
  2091. case OperationCode.ServerSettings:
  2092. case OperationCode.SetProperties:
  2093. case OperationCode.RaiseEvent:
  2094. return true;
  2095. }
  2096. break;
  2097. case ServerConnection.NameServer:
  2098. switch (opCode)
  2099. {
  2100. case OperationCode.Authenticate:
  2101. case OperationCode.AuthenticateOnce:
  2102. case OperationCode.GetRegions:
  2103. case OperationCode.ServerSettings:
  2104. return true;
  2105. }
  2106. break;
  2107. default:
  2108. throw new ArgumentOutOfRangeException("serverConnection", serverConnection, null);
  2109. }
  2110. return false;
  2111. }
  2112. private bool CheckIfOpCanBeSent(byte opCode, ServerConnection serverConnection, string opName)
  2113. {
  2114. if (this.LoadBalancingPeer == null)
  2115. {
  2116. this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) can't be sent because peer is null", opName, opCode));
  2117. return false;
  2118. }
  2119. if (!this.CheckIfOpAllowedOnServer(opCode, serverConnection))
  2120. {
  2121. if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ERROR)
  2122. {
  2123. this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) not allowed on current server ({2})", opName, opCode, serverConnection));
  2124. }
  2125. return false;
  2126. }
  2127. if (!this.CheckIfClientIsReadyToCallOperation(opCode))
  2128. {
  2129. DebugLevel levelToReport = DebugLevel.ERROR;
  2130. if (opCode == OperationCode.RaiseEvent && (this.State == ClientState.Leaving || this.State == ClientState.Disconnecting || this.State == ClientState.DisconnectingFromGameServer))
  2131. {
  2132. levelToReport = DebugLevel.INFO;
  2133. }
  2134. if (this.LoadBalancingPeer.DebugOut >= levelToReport)
  2135. {
  2136. 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)));
  2137. }
  2138. return false;
  2139. }
  2140. if (this.LoadBalancingPeer.PeerState != PeerStateValue.Connected)
  2141. {
  2142. 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));
  2143. return false;
  2144. }
  2145. return true;
  2146. }
  2147. private bool CheckIfClientIsReadyToCallOperation(byte opCode)
  2148. {
  2149. switch (opCode)
  2150. {
  2151. //case OperationCode.ServerSettings: // ??
  2152. //case OperationCode.WebRpc: // WebRPC works on MS and GS and I think it does not need the client to be ready
  2153. case OperationCode.Authenticate:
  2154. case OperationCode.AuthenticateOnce:
  2155. return this.IsConnectedAndReady ||
  2156. this.State == ClientState.ConnectingToNameServer || // this is required since we do not set state to ConnectedToNameServer before authentication
  2157. this.State == ClientState.ConnectingToMasterServer || // this is required since we do not set state to ConnectedToMasterServer before authentication
  2158. this.State == ClientState.ConnectingToGameServer; // this is required since we do not set state to ConnectedToGameServer before authentication
  2159. case OperationCode.ChangeGroups:
  2160. case OperationCode.GetProperties:
  2161. case OperationCode.SetProperties:
  2162. case OperationCode.RaiseEvent:
  2163. case OperationCode.Leave:
  2164. return this.InRoom;
  2165. case OperationCode.JoinGame:
  2166. case OperationCode.CreateGame:
  2167. return this.State == ClientState.ConnectedToMasterServer || this.InLobby || this.State == ClientState.ConnectedToGameServer; // CurrentRoom can be not null in case of quick rejoin
  2168. case OperationCode.LeaveLobby:
  2169. return this.InLobby;
  2170. case OperationCode.JoinRandomGame:
  2171. case OperationCode.FindFriends:
  2172. case OperationCode.GetGameList:
  2173. case OperationCode.GetLobbyStats: // do we need to be inside lobby to call this?
  2174. case OperationCode.JoinLobby: // You don't have to explicitly leave a lobby to join another (client can be in one max, at any time)
  2175. return this.State == ClientState.ConnectedToMasterServer || this.InLobby;
  2176. case OperationCode.GetRegions:
  2177. return this.State == ClientState.ConnectedToNameServer;
  2178. }
  2179. return this.IsConnected;
  2180. }
  2181. #endregion
  2182. #region Implementation of IPhotonPeerListener
  2183. /// <summary>Debug output of low level api (and this client).</summary>
  2184. /// <remarks>This method is not responsible to keep up the state of a LoadBalancingClient. Calling base.DebugReturn on overrides is optional.</remarks>
  2185. public virtual void DebugReturn(DebugLevel level, string message)
  2186. {
  2187. if (this.LoadBalancingPeer.DebugOut != DebugLevel.ALL && level > this.LoadBalancingPeer.DebugOut)
  2188. {
  2189. return;
  2190. }
  2191. #if !SUPPORTED_UNITY
  2192. Debug.WriteLine(message);
  2193. #else
  2194. if (level == DebugLevel.ERROR)
  2195. {
  2196. Debug.LogError(message);
  2197. }
  2198. else if (level == DebugLevel.WARNING)
  2199. {
  2200. Debug.LogWarning(message);
  2201. }
  2202. else if (level == DebugLevel.INFO)
  2203. {
  2204. Debug.Log(message);
  2205. }
  2206. else if (level == DebugLevel.ALL)
  2207. {
  2208. Debug.Log(message);
  2209. }
  2210. #endif
  2211. }
  2212. private void CallbackRoomEnterFailed(OperationResponse operationResponse)
  2213. {
  2214. if (operationResponse.ReturnCode != 0)
  2215. {
  2216. if (operationResponse.OperationCode == OperationCode.JoinGame)
  2217. {
  2218. this.MatchMakingCallbackTargets.OnJoinRoomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
  2219. }
  2220. else if (operationResponse.OperationCode == OperationCode.CreateGame)
  2221. {
  2222. this.MatchMakingCallbackTargets.OnCreateRoomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
  2223. }
  2224. else if (operationResponse.OperationCode == OperationCode.JoinRandomGame)
  2225. {
  2226. this.MatchMakingCallbackTargets.OnJoinRandomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
  2227. }
  2228. }
  2229. }
  2230. /// <summary>
  2231. /// Uses the OperationResponses provided by the server to advance the internal state and call ops as needed.
  2232. /// </summary>
  2233. /// <remarks>
  2234. /// When this method finishes, it will call your OnOpResponseAction (if any). This way, you can get any
  2235. /// operation response without overriding this class.
  2236. ///
  2237. /// To implement a more complex game/app logic, you should implement your own class that inherits the
  2238. /// LoadBalancingClient. Override this method to use your own operation-responses easily.
  2239. ///
  2240. /// This method is essential to update the internal state of a LoadBalancingClient, so overriding methods
  2241. /// must call base.OnOperationResponse().
  2242. /// </remarks>
  2243. /// <param name="operationResponse">Contains the server's response for an operation called by this peer.</param>
  2244. public virtual void OnOperationResponse(OperationResponse operationResponse)
  2245. {
  2246. // if (operationResponse.ReturnCode != 0) this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull());
  2247. // use the "secret" or "token" whenever we get it. doesn't really matter if it's in AuthResponse.
  2248. if (operationResponse.Parameters.ContainsKey(ParameterCode.Token))
  2249. {
  2250. if (this.AuthValues == null)
  2251. {
  2252. this.AuthValues = new AuthenticationValues();
  2253. //this.DebugReturn(DebugLevel.ERROR, "Server returned secret. Created AuthValues.");
  2254. }
  2255. this.AuthValues.Token = operationResponse[ParameterCode.Token] as string;
  2256. this.tokenCache = this.AuthValues.Token;
  2257. }
  2258. // if the operation limit was reached, disconnect (but still execute the operation response).
  2259. if (operationResponse.ReturnCode == ErrorCode.OperationLimitReached)
  2260. {
  2261. this.Disconnect(DisconnectCause.DisconnectByOperationLimit);
  2262. }
  2263. switch (operationResponse.OperationCode)
  2264. {
  2265. case OperationCode.Authenticate:
  2266. case OperationCode.AuthenticateOnce:
  2267. {
  2268. if (operationResponse.ReturnCode != 0)
  2269. {
  2270. this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull() + " Server: " + this.Server + " Address: " + this.LoadBalancingPeer.ServerAddress);
  2271. switch (operationResponse.ReturnCode)
  2272. {
  2273. case ErrorCode.InvalidAuthentication:
  2274. this.DisconnectedCause = DisconnectCause.InvalidAuthentication;
  2275. break;
  2276. case ErrorCode.CustomAuthenticationFailed:
  2277. this.DisconnectedCause = DisconnectCause.CustomAuthenticationFailed;
  2278. this.ConnectionCallbackTargets.OnCustomAuthenticationFailed(operationResponse.DebugMessage);
  2279. break;
  2280. case ErrorCode.InvalidRegion:
  2281. this.DisconnectedCause = DisconnectCause.InvalidRegion;
  2282. break;
  2283. case ErrorCode.MaxCcuReached:
  2284. this.DisconnectedCause = DisconnectCause.MaxCcuReached;
  2285. break;
  2286. case ErrorCode.OperationNotAllowedInCurrentState:
  2287. this.DisconnectedCause = DisconnectCause.OperationNotAllowedInCurrentState;
  2288. break;
  2289. case ErrorCode.AuthenticationTicketExpired:
  2290. this.DisconnectedCause = DisconnectCause.AuthenticationTicketExpired;
  2291. break;
  2292. }
  2293. this.Disconnect(this.DisconnectedCause);
  2294. break; // if auth didn't succeed, we disconnect (above) and exit this operation's handling
  2295. }
  2296. if (this.Server == ServerConnection.NameServer || this.Server == ServerConnection.MasterServer)
  2297. {
  2298. if (operationResponse.Parameters.ContainsKey(ParameterCode.UserId))
  2299. {
  2300. string incomingId = (string)operationResponse.Parameters[ParameterCode.UserId];
  2301. if (!string.IsNullOrEmpty(incomingId))
  2302. {
  2303. this.UserId = incomingId;
  2304. this.LocalPlayer.UserId = incomingId;
  2305. this.DebugReturn(DebugLevel.INFO, string.Format("Received your UserID from server. Updating local value to: {0}", this.UserId));
  2306. }
  2307. }
  2308. if (operationResponse.Parameters.ContainsKey(ParameterCode.NickName))
  2309. {
  2310. this.NickName = (string)operationResponse.Parameters[ParameterCode.NickName];
  2311. this.DebugReturn(DebugLevel.INFO, string.Format("Received your NickName from server. Updating local value to: {0}", this.NickName));
  2312. }
  2313. if (operationResponse.Parameters.ContainsKey(ParameterCode.EncryptionData))
  2314. {
  2315. this.SetupEncryption((Dictionary<byte, object>)operationResponse.Parameters[ParameterCode.EncryptionData]);
  2316. }
  2317. }
  2318. if (this.Server == ServerConnection.NameServer)
  2319. {
  2320. string receivedCluster = operationResponse[ParameterCode.Cluster] as string;
  2321. if (!string.IsNullOrEmpty(receivedCluster))
  2322. {
  2323. this.CurrentCluster = receivedCluster;
  2324. }
  2325. // on the NameServer, authenticate returns the MasterServer address for a region and we hop off to there
  2326. this.MasterServerAddress = operationResponse[ParameterCode.Address] as string;
  2327. if (this.ServerPortOverrides.MasterServerPort != 0)
  2328. {
  2329. //Debug.LogWarning("Incoming MasterServer Address: "+this.MasterServerAddress);
  2330. this.MasterServerAddress = ReplacePortWithAlternative(this.MasterServerAddress, this.ServerPortOverrides.MasterServerPort);
  2331. //Debug.LogWarning("New MasterServer Address: "+this.MasterServerAddress);
  2332. }
  2333. if (this.AuthMode == AuthModeOption.AuthOnceWss && this.ExpectedProtocol != null)
  2334. {
  2335. this.DebugReturn(DebugLevel.INFO, string.Format("AuthOnceWss mode. Auth response switches TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol));
  2336. this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol;
  2337. this.ExpectedProtocol = null;
  2338. }
  2339. this.DisconnectToReconnect();
  2340. }
  2341. else if (this.Server == ServerConnection.MasterServer)
  2342. {
  2343. this.State = ClientState.ConnectedToMasterServer;
  2344. if (this.failedRoomEntryOperation == null)
  2345. {
  2346. this.ConnectionCallbackTargets.OnConnectedToMaster();
  2347. }
  2348. else
  2349. {
  2350. this.CallbackRoomEnterFailed(this.failedRoomEntryOperation);
  2351. this.failedRoomEntryOperation = null;
  2352. }
  2353. if (this.AuthMode != AuthModeOption.Auth)
  2354. {
  2355. this.LoadBalancingPeer.OpSettings(this.EnableLobbyStatistics);
  2356. }
  2357. }
  2358. else if (this.Server == ServerConnection.GameServer)
  2359. {
  2360. this.State = ClientState.Joining;
  2361. if (this.enterRoomParamsCache.JoinMode == JoinMode.RejoinOnly)
  2362. {
  2363. this.enterRoomParamsCache.PlayerProperties = null;
  2364. }
  2365. else
  2366. {
  2367. Hashtable allProps = new Hashtable();
  2368. allProps.Merge(this.LocalPlayer.CustomProperties);
  2369. if (!string.IsNullOrEmpty(this.LocalPlayer.NickName))
  2370. {
  2371. allProps[ActorProperties.PlayerName] = this.LocalPlayer.NickName;
  2372. }
  2373. this.enterRoomParamsCache.PlayerProperties = allProps;
  2374. }
  2375. this.enterRoomParamsCache.OnGameServer = true;
  2376. if (this.lastJoinType == JoinType.JoinRoom || this.lastJoinType == JoinType.JoinRandomRoom || this.lastJoinType == JoinType.JoinRandomOrCreateRoom || this.lastJoinType == JoinType.JoinOrCreateRoom)
  2377. {
  2378. this.LoadBalancingPeer.OpJoinRoom(this.enterRoomParamsCache);
  2379. }
  2380. else if (this.lastJoinType == JoinType.CreateRoom)
  2381. {
  2382. this.LoadBalancingPeer.OpCreateRoom(this.enterRoomParamsCache);
  2383. }
  2384. break;
  2385. }
  2386. // optionally, OpAuth may return some data for the client to use. if it's available, call OnCustomAuthenticationResponse
  2387. Dictionary<string, object> data = (Dictionary<string, object>)operationResponse[ParameterCode.Data];
  2388. if (data != null)
  2389. {
  2390. this.ConnectionCallbackTargets.OnCustomAuthenticationResponse(data);
  2391. }
  2392. break;
  2393. }
  2394. case OperationCode.GetRegions:
  2395. // Debug.Log("GetRegions returned: " + operationResponse.ToStringFull());
  2396. if (operationResponse.ReturnCode == ErrorCode.InvalidAuthentication)
  2397. {
  2398. this.DebugReturn(DebugLevel.ERROR, string.Format("GetRegions failed. AppId is unknown on the (cloud) server. "+operationResponse.DebugMessage));
  2399. this.Disconnect(DisconnectCause.InvalidAuthentication);
  2400. break;
  2401. }
  2402. if (operationResponse.ReturnCode != ErrorCode.Ok)
  2403. {
  2404. this.DebugReturn(DebugLevel.ERROR, "GetRegions failed. Can't provide regions list. ReturnCode: " + operationResponse.ReturnCode + ": " + operationResponse.DebugMessage);
  2405. this.Disconnect(DisconnectCause.InvalidAuthentication);
  2406. break;
  2407. }
  2408. if (this.RegionHandler == null)
  2409. {
  2410. this.RegionHandler = new RegionHandler(this.ServerPortOverrides.MasterServerPort);
  2411. }
  2412. if (this.RegionHandler.IsPinging)
  2413. {
  2414. 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.");
  2415. 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.
  2416. }
  2417. this.RegionHandler.SetRegions(operationResponse);
  2418. this.ConnectionCallbackTargets.OnRegionListReceived(this.RegionHandler);
  2419. if (this.connectToBestRegion)
  2420. {
  2421. // ping minimal regions (if one is known) and connect
  2422. this.RegionHandler.PingMinimumOfRegions(this.OnRegionPingCompleted, this.bestRegionSummaryFromStorage);
  2423. }
  2424. break;
  2425. case OperationCode.JoinRandomGame: // this happens only on the master server. on gameserver this is a "regular" join
  2426. case OperationCode.CreateGame:
  2427. case OperationCode.JoinGame:
  2428. if (operationResponse.ReturnCode != 0)
  2429. {
  2430. if (this.Server == ServerConnection.GameServer)
  2431. {
  2432. this.failedRoomEntryOperation = operationResponse;
  2433. this.DisconnectToReconnect();
  2434. }
  2435. else
  2436. {
  2437. this.State = (this.InLobby) ? ClientState.JoinedLobby : ClientState.ConnectedToMasterServer;
  2438. this.CallbackRoomEnterFailed(operationResponse);
  2439. }
  2440. }
  2441. else
  2442. {
  2443. if (this.Server == ServerConnection.GameServer)
  2444. {
  2445. this.GameEnteredOnGameServer(operationResponse);
  2446. }
  2447. else
  2448. {
  2449. this.GameServerAddress = (string)operationResponse[ParameterCode.Address];
  2450. if (this.ServerPortOverrides.GameServerPort != 0)
  2451. {
  2452. //Debug.LogWarning("Incoming GameServer Address: " + this.GameServerAddress);
  2453. this.GameServerAddress = ReplacePortWithAlternative(this.GameServerAddress, this.ServerPortOverrides.GameServerPort);
  2454. //Debug.LogWarning("New GameServer Address: " + this.GameServerAddress);
  2455. }
  2456. string roomName = operationResponse[ParameterCode.RoomName] as string;
  2457. if (!string.IsNullOrEmpty(roomName))
  2458. {
  2459. this.enterRoomParamsCache.RoomName = roomName;
  2460. }
  2461. this.DisconnectToReconnect();
  2462. }
  2463. }
  2464. break;
  2465. case OperationCode.GetGameList:
  2466. if (operationResponse.ReturnCode != 0)
  2467. {
  2468. this.DebugReturn(DebugLevel.ERROR, "GetGameList failed: " + operationResponse.ToStringFull());
  2469. break;
  2470. }
  2471. List<RoomInfo> _RoomInfoList = new List<RoomInfo>();
  2472. Hashtable games = (Hashtable)operationResponse[ParameterCode.GameList];
  2473. foreach (string gameName in games.Keys)
  2474. {
  2475. _RoomInfoList.Add(new RoomInfo(gameName, (Hashtable)games[gameName]));
  2476. }
  2477. this.LobbyCallbackTargets.OnRoomListUpdate(_RoomInfoList);
  2478. break;
  2479. case OperationCode.JoinLobby:
  2480. this.State = ClientState.JoinedLobby;
  2481. this.LobbyCallbackTargets.OnJoinedLobby();
  2482. break;
  2483. case OperationCode.LeaveLobby:
  2484. this.State = ClientState.ConnectedToMasterServer;
  2485. this.LobbyCallbackTargets.OnLeftLobby();
  2486. break;
  2487. case OperationCode.Leave:
  2488. this.DisconnectToReconnect();
  2489. break;
  2490. case OperationCode.FindFriends:
  2491. if (operationResponse.ReturnCode != 0)
  2492. {
  2493. this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed: " + operationResponse.ToStringFull());
  2494. this.friendListRequested = null;
  2495. break;
  2496. }
  2497. bool[] onlineList = operationResponse[ParameterCode.FindFriendsResponseOnlineList] as bool[];
  2498. string[] roomList = operationResponse[ParameterCode.FindFriendsResponseRoomIdList] as string[];
  2499. //if (onlineList == null || roomList == null || this.friendListRequested == null || onlineList.Length != this.friendListRequested.Length)
  2500. //{
  2501. // // TODO: Check if we should handle this case better / more extensively
  2502. // this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed. Some list is not set. OpResponse: " + operationResponse.ToStringFull());
  2503. // this.friendListRequested = null;
  2504. // this.isFetchingFriendList = false;
  2505. // break;
  2506. //}
  2507. List<FriendInfo> friendList = new List<FriendInfo>(this.friendListRequested.Length);
  2508. for (int index = 0; index < this.friendListRequested.Length; index++)
  2509. {
  2510. FriendInfo friend = new FriendInfo();
  2511. friend.UserId = this.friendListRequested[index];
  2512. friend.Room = roomList[index];
  2513. friend.IsOnline = onlineList[index];
  2514. friendList.Insert(index, friend);
  2515. }
  2516. this.friendListRequested = null;
  2517. this.MatchMakingCallbackTargets.OnFriendListUpdate(friendList);
  2518. break;
  2519. case OperationCode.WebRpc:
  2520. this.WebRpcCallbackTargets.OnWebRpcResponse(operationResponse);
  2521. break;
  2522. }
  2523. if (this.OpResponseReceived != null) this.OpResponseReceived(operationResponse);
  2524. }
  2525. /// <summary>
  2526. /// Uses the connection's statusCodes to advance the internal state and call operations as needed.
  2527. /// </summary>
  2528. /// <remarks>This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnStatusChanged.</remarks>
  2529. public virtual void OnStatusChanged(StatusCode statusCode)
  2530. {
  2531. switch (statusCode)
  2532. {
  2533. case StatusCode.Connect:
  2534. if (this.State == ClientState.ConnectingToNameServer)
  2535. {
  2536. if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ALL)
  2537. {
  2538. this.DebugReturn(DebugLevel.ALL, "Connected to nameserver.");
  2539. }
  2540. this.Server = ServerConnection.NameServer;
  2541. if (this.AuthValues != null)
  2542. {
  2543. this.AuthValues.Token = null; // when connecting to NameServer, invalidate the secret (only)
  2544. }
  2545. }
  2546. if (this.State == ClientState.ConnectingToGameServer)
  2547. {
  2548. if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ALL)
  2549. {
  2550. this.DebugReturn(DebugLevel.ALL, "Connected to gameserver.");
  2551. }
  2552. this.Server = ServerConnection.GameServer;
  2553. }
  2554. if (this.State == ClientState.ConnectingToMasterServer)
  2555. {
  2556. if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ALL)
  2557. {
  2558. this.DebugReturn(DebugLevel.ALL, "Connected to masterserver.");
  2559. }
  2560. this.Server = ServerConnection.MasterServer;
  2561. this.ConnectionCallbackTargets.OnConnected(); // if initial connect
  2562. }
  2563. if (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure)
  2564. {
  2565. if (this.Server == ServerConnection.NameServer || this.AuthMode == AuthModeOption.Auth)
  2566. {
  2567. this.LoadBalancingPeer.EstablishEncryption();
  2568. }
  2569. }
  2570. else
  2571. {
  2572. goto case StatusCode.EncryptionEstablished;
  2573. }
  2574. break;
  2575. case StatusCode.EncryptionEstablished:
  2576. if (this.Server == ServerConnection.NameServer)
  2577. {
  2578. this.State = ClientState.ConnectedToNameServer;
  2579. // if there is no specific region to connect to, get available regions from the Name Server. the result triggers next actions in workflow
  2580. if (string.IsNullOrEmpty(this.CloudRegion))
  2581. {
  2582. this.OpGetRegions();
  2583. break;
  2584. }
  2585. }
  2586. else
  2587. {
  2588. // auth AuthOnce, no explicit authentication is needed on Master Server and Game Server. this is done via token, so: break
  2589. if (this.AuthMode == AuthModeOption.AuthOnce || this.AuthMode == AuthModeOption.AuthOnceWss)
  2590. {
  2591. break;
  2592. }
  2593. }
  2594. // authenticate in all other cases (using the CloudRegion, if available)
  2595. bool authenticating = this.CallAuthenticate();
  2596. if (authenticating)
  2597. {
  2598. this.State = ClientState.Authenticating;
  2599. }
  2600. else
  2601. {
  2602. this.DebugReturn(DebugLevel.ERROR, "OpAuthenticate failed. Check log output and AuthValues. State: " + this.State);
  2603. }
  2604. break;
  2605. case StatusCode.Disconnect:
  2606. // disconnect due to connection exception is handled below (don't connect to GS or master in that case)
  2607. this.friendListRequested = null;
  2608. bool wasInRoom = this.CurrentRoom != null;
  2609. this.CurrentRoom = null; // players get cleaned up inside this, too, except LocalPlayer (which we keep)
  2610. this.ChangeLocalID(-1); // depends on this.CurrentRoom, so it must be called after updating that
  2611. if (this.Server == ServerConnection.GameServer && wasInRoom)
  2612. {
  2613. this.MatchMakingCallbackTargets.OnLeftRoom();
  2614. }
  2615. if (this.ExpectedProtocol != null && this.LoadBalancingPeer.TransportProtocol != this.ExpectedProtocol)
  2616. {
  2617. this.DebugReturn(DebugLevel.INFO, string.Format("On disconnect switches TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol));
  2618. this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol;
  2619. this.ExpectedProtocol = null;
  2620. }
  2621. switch (this.State)
  2622. {
  2623. case ClientState.ConnectWithFallbackProtocol:
  2624. this.EnableProtocolFallback = false; // the client does a fallback only one time
  2625. this.LoadBalancingPeer.TransportProtocol = (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Tcp) ? ConnectionProtocol.Udp : ConnectionProtocol.Tcp;
  2626. this.NameServerPortInAppSettings = 0; // this does not affect the ServerSettings file, just a variable at runtime
  2627. this.ServerPortOverrides = new PhotonPortDefinition(); // use default ports for the fallback
  2628. if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
  2629. {
  2630. return;
  2631. }
  2632. this.State = ClientState.ConnectingToNameServer;
  2633. break;
  2634. case ClientState.PeerCreated:
  2635. case ClientState.Disconnecting:
  2636. if (this.AuthValues != null)
  2637. {
  2638. this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
  2639. }
  2640. this.State = ClientState.Disconnected;
  2641. this.ConnectionCallbackTargets.OnDisconnected(this.DisconnectedCause);
  2642. break;
  2643. case ClientState.DisconnectingFromGameServer:
  2644. case ClientState.DisconnectingFromNameServer:
  2645. this.ConnectToMasterServer(); // this gets the client back to the Master Server
  2646. break;
  2647. case ClientState.DisconnectingFromMasterServer:
  2648. this.Connect(this.GameServerAddress, this.ProxyServerAddress, ServerConnection.GameServer); // this connects the client with the Game Server (when joining/creating a room)
  2649. break;
  2650. case ClientState.Disconnected:
  2651. // this client is already Disconnected, so no further action is needed.
  2652. // this.DebugReturn(DebugLevel.INFO, "LBC.OnStatusChanged(Disconnect) this.State: " + this.State + ". Server: " + this.Server);
  2653. break;
  2654. default:
  2655. string stacktrace = "";
  2656. #if DEBUG && !NETFX_CORE
  2657. stacktrace = new System.Diagnostics.StackTrace(true).ToString();
  2658. #endif
  2659. this.DebugReturn(DebugLevel.WARNING, "Got a unexpected Disconnect in LoadBalancingClient State: " + this.State + ". Server: " + this.Server + " Trace: " + stacktrace);
  2660. if (this.AuthValues != null)
  2661. {
  2662. this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
  2663. }
  2664. this.State = ClientState.Disconnected;
  2665. this.ConnectionCallbackTargets.OnDisconnected(this.DisconnectedCause);
  2666. break;
  2667. }
  2668. break;
  2669. case StatusCode.DisconnectByServerUserLimit:
  2670. this.DebugReturn(DebugLevel.ERROR, "This connection was rejected due to the apps CCU limit.");
  2671. this.DisconnectedCause = DisconnectCause.MaxCcuReached;
  2672. this.State = ClientState.Disconnecting;
  2673. break;
  2674. case StatusCode.DnsExceptionOnConnect:
  2675. this.DisconnectedCause = DisconnectCause.DnsExceptionOnConnect;
  2676. this.State = ClientState.Disconnecting;
  2677. break;
  2678. case StatusCode.ServerAddressInvalid:
  2679. this.DisconnectedCause = DisconnectCause.ServerAddressInvalid;
  2680. this.State = ClientState.Disconnecting;
  2681. break;
  2682. case StatusCode.ExceptionOnConnect:
  2683. case StatusCode.SecurityExceptionOnConnect:
  2684. case StatusCode.EncryptionFailedToEstablish:
  2685. this.DisconnectedCause = DisconnectCause.ExceptionOnConnect;
  2686. // if enabled, the client can attempt to connect with another networking-protocol to check if that connects
  2687. if (this.EnableProtocolFallback && this.State == ClientState.ConnectingToNameServer)
  2688. {
  2689. this.State = ClientState.ConnectWithFallbackProtocol;
  2690. }
  2691. else
  2692. {
  2693. this.State = ClientState.Disconnecting;
  2694. }
  2695. break;
  2696. case StatusCode.Exception:
  2697. case StatusCode.ExceptionOnReceive:
  2698. case StatusCode.SendError:
  2699. this.DisconnectedCause = DisconnectCause.Exception;
  2700. this.State = ClientState.Disconnecting;
  2701. break;
  2702. case StatusCode.DisconnectByServerTimeout:
  2703. this.DisconnectedCause = DisconnectCause.ServerTimeout;
  2704. this.State = ClientState.Disconnecting;
  2705. break;
  2706. case StatusCode.DisconnectByServerLogic:
  2707. this.DisconnectedCause = DisconnectCause.DisconnectByServerLogic;
  2708. this.State = ClientState.Disconnecting;
  2709. break;
  2710. case StatusCode.DisconnectByServerReasonUnknown:
  2711. this.DisconnectedCause = DisconnectCause.DisconnectByServerReasonUnknown;
  2712. this.State = ClientState.Disconnecting;
  2713. break;
  2714. case StatusCode.TimeoutDisconnect:
  2715. this.DisconnectedCause = DisconnectCause.ClientTimeout;
  2716. // if enabled, the client can attempt to connect with another networking-protocol to check if that connects
  2717. if (this.EnableProtocolFallback && this.State == ClientState.ConnectingToNameServer)
  2718. {
  2719. this.State = ClientState.ConnectWithFallbackProtocol;
  2720. }
  2721. else
  2722. {
  2723. this.State = ClientState.Disconnecting;
  2724. }
  2725. break;
  2726. }
  2727. }
  2728. /// <summary>
  2729. /// Uses the photonEvent's provided by the server to advance the internal state and call ops as needed.
  2730. /// </summary>
  2731. /// <remarks>This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnEvent.</remarks>
  2732. public virtual void OnEvent(EventData photonEvent)
  2733. {
  2734. int actorNr = photonEvent.Sender;
  2735. Player originatingPlayer = (this.CurrentRoom != null) ? this.CurrentRoom.GetPlayer(actorNr) : null;
  2736. switch (photonEvent.Code)
  2737. {
  2738. case EventCode.GameList:
  2739. case EventCode.GameListUpdate:
  2740. List<RoomInfo> _RoomInfoList = new List<RoomInfo>();
  2741. Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList];
  2742. foreach (string gameName in games.Keys)
  2743. {
  2744. _RoomInfoList.Add(new RoomInfo(gameName, (Hashtable)games[gameName]));
  2745. }
  2746. this.LobbyCallbackTargets.OnRoomListUpdate(_RoomInfoList);
  2747. break;
  2748. case EventCode.Join:
  2749. Hashtable actorProperties = (Hashtable)photonEvent[ParameterCode.PlayerProperties];
  2750. if (originatingPlayer == null)
  2751. {
  2752. originatingPlayer = this.CreatePlayer(string.Empty, actorNr, false, actorProperties);
  2753. this.CurrentRoom.StorePlayer(originatingPlayer);
  2754. }
  2755. else
  2756. {
  2757. originatingPlayer.InternalCacheProperties(actorProperties);
  2758. originatingPlayer.IsInactive = false;
  2759. originatingPlayer.HasRejoined = actorNr != this.LocalPlayer.ActorNumber; // event is for non-local player, who is known (by ActorNumber), so it's a returning player
  2760. }
  2761. if (actorNr == this.LocalPlayer.ActorNumber)
  2762. {
  2763. // 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
  2764. int[] actorsInRoom = (int[])photonEvent[ParameterCode.ActorList];
  2765. this.UpdatedActorList(actorsInRoom);
  2766. // any operation that does a "rejoin" will set this value to true. this can indicate if the local player returns to a room.
  2767. originatingPlayer.HasRejoined = this.enterRoomParamsCache.JoinMode == JoinMode.RejoinOnly;
  2768. // joinWithCreateOnDemand can turn an OpJoin into creating the room. Then actorNumber is 1 and callback: OnCreatedRoom()
  2769. if (this.lastJoinType == JoinType.CreateRoom || (this.lastJoinType == JoinType.JoinOrCreateRoom && this.LocalPlayer.ActorNumber == 1))
  2770. {
  2771. this.MatchMakingCallbackTargets.OnCreatedRoom();
  2772. }
  2773. this.MatchMakingCallbackTargets.OnJoinedRoom();
  2774. }
  2775. else
  2776. {
  2777. this.InRoomCallbackTargets.OnPlayerEnteredRoom(originatingPlayer);
  2778. }
  2779. break;
  2780. case EventCode.Leave:
  2781. if (originatingPlayer != null)
  2782. {
  2783. bool isInactive = false;
  2784. if (photonEvent.Parameters.ContainsKey(ParameterCode.IsInactive))
  2785. {
  2786. isInactive = (bool)photonEvent.Parameters[ParameterCode.IsInactive];
  2787. }
  2788. if (isInactive)
  2789. {
  2790. originatingPlayer.IsInactive = true;
  2791. }
  2792. else
  2793. {
  2794. originatingPlayer.IsInactive = false;
  2795. this.CurrentRoom.RemovePlayer(actorNr);
  2796. }
  2797. }
  2798. if (photonEvent.Parameters.ContainsKey(ParameterCode.MasterClientId))
  2799. {
  2800. int newMaster = (int)photonEvent[ParameterCode.MasterClientId];
  2801. if (newMaster != 0)
  2802. {
  2803. this.CurrentRoom.masterClientId = newMaster;
  2804. this.InRoomCallbackTargets.OnMasterClientSwitched(this.CurrentRoom.GetPlayer(newMaster));
  2805. }
  2806. }
  2807. // finally, send notification that a player left
  2808. this.InRoomCallbackTargets.OnPlayerLeftRoom(originatingPlayer);
  2809. break;
  2810. case EventCode.PropertiesChanged:
  2811. // whenever properties are sent in-room, they can be broadcasted as event (which we handle here)
  2812. // we get PLAYERproperties if actorNr > 0 or ROOMproperties if actorNumber is not set or 0
  2813. int targetActorNr = 0;
  2814. if (photonEvent.Parameters.ContainsKey(ParameterCode.TargetActorNr))
  2815. {
  2816. targetActorNr = (int)photonEvent[ParameterCode.TargetActorNr];
  2817. }
  2818. Hashtable gameProperties = null;
  2819. Hashtable actorProps = null;
  2820. if (targetActorNr == 0)
  2821. {
  2822. gameProperties = (Hashtable)photonEvent[ParameterCode.Properties];
  2823. }
  2824. else
  2825. {
  2826. actorProps = (Hashtable)photonEvent[ParameterCode.Properties];
  2827. }
  2828. this.ReadoutProperties(gameProperties, actorProps, targetActorNr);
  2829. break;
  2830. case EventCode.AppStats:
  2831. // only the master server sends these in (1 minute) intervals
  2832. this.PlayersInRoomsCount = (int)photonEvent[ParameterCode.PeerCount];
  2833. this.RoomsCount = (int)photonEvent[ParameterCode.GameCount];
  2834. this.PlayersOnMasterCount = (int)photonEvent[ParameterCode.MasterPeerCount];
  2835. break;
  2836. case EventCode.LobbyStats:
  2837. string[] names = photonEvent[ParameterCode.LobbyName] as string[];
  2838. int[] peers = photonEvent[ParameterCode.PeerCount] as int[];
  2839. int[] rooms = photonEvent[ParameterCode.GameCount] as int[];
  2840. byte[] types;
  2841. ByteArraySlice slice = photonEvent[ParameterCode.LobbyType] as ByteArraySlice;
  2842. bool useByteArraySlice = slice != null;
  2843. if (useByteArraySlice)
  2844. {
  2845. types = slice.Buffer;
  2846. }
  2847. else
  2848. {
  2849. types = photonEvent[ParameterCode.LobbyType] as byte[];
  2850. }
  2851. this.lobbyStatistics.Clear();
  2852. for (int i = 0; i < names.Length; i++)
  2853. {
  2854. TypedLobbyInfo info = new TypedLobbyInfo();
  2855. info.Name = names[i];
  2856. info.Type = (LobbyType)types[i];
  2857. info.PlayerCount = peers[i];
  2858. info.RoomCount = rooms[i];
  2859. this.lobbyStatistics.Add(info);
  2860. }
  2861. if (useByteArraySlice)
  2862. {
  2863. slice.Release();
  2864. }
  2865. this.LobbyCallbackTargets.OnLobbyStatisticsUpdate(this.lobbyStatistics);
  2866. break;
  2867. case EventCode.ErrorInfo:
  2868. this.ErrorInfoCallbackTargets.OnErrorInfo(new ErrorInfo(photonEvent));
  2869. break;
  2870. case EventCode.AuthEvent:
  2871. if (this.AuthValues == null)
  2872. {
  2873. this.AuthValues = new AuthenticationValues();
  2874. }
  2875. this.AuthValues.Token = photonEvent[ParameterCode.Token] as string;
  2876. this.tokenCache = this.AuthValues.Token;
  2877. break;
  2878. }
  2879. this.UpdateCallbackTargets();
  2880. if (this.EventReceived != null) this.EventReceived(photonEvent);
  2881. }
  2882. /// <summary>In Photon 4, "raw messages" will get their own callback method in the interface. Not used yet.</summary>
  2883. public virtual void OnMessage(object message)
  2884. {
  2885. this.DebugReturn(DebugLevel.ALL, string.Format("got OnMessage {0}", message));
  2886. }
  2887. #endregion
  2888. private void OnDisconnectMessageReceived(DisconnectMessage obj)
  2889. {
  2890. this.DebugReturn(DebugLevel.ERROR, string.Format("Got DisconnectMessage. Code: {0} Msg: \"{1}\". Debug Info: {2}", obj.Code, obj.DebugMessage, obj.Parameters.ToStringFull()));
  2891. this.Disconnect(DisconnectCause.DisconnectByDisconnectMessage);
  2892. }
  2893. /// <summary>A callback of the RegionHandler, provided in OnRegionListReceived.</summary>
  2894. /// <param name="regionHandler">The regionHandler wraps up best region and other region relevant info.</param>
  2895. private void OnRegionPingCompleted(RegionHandler regionHandler)
  2896. {
  2897. //Debug.Log("OnRegionPingCompleted " + regionHandler.BestRegion);
  2898. //Debug.Log("RegionPingSummary: " + regionHandler.SummaryToCache);
  2899. this.SummaryToCache = regionHandler.SummaryToCache;
  2900. this.ConnectToRegionMaster(regionHandler.BestRegion.Code);
  2901. }
  2902. protected internal static string ReplacePortWithAlternative(string address, ushort replacementPort)
  2903. {
  2904. bool webSocket = address.StartsWith("ws");
  2905. if (webSocket)
  2906. {
  2907. UriBuilder urib = new UriBuilder(address);
  2908. urib.Port = replacementPort;
  2909. return urib.ToString();
  2910. }
  2911. else
  2912. {
  2913. UriBuilder urib = new UriBuilder(string.Format("scheme://{0}", address));
  2914. return string.Format("{0}:{1}", urib.Host, replacementPort);
  2915. }
  2916. }
  2917. private void SetupEncryption(Dictionary<byte, object> encryptionData)
  2918. {
  2919. var mode = (EncryptionMode)(byte)encryptionData[EncryptionDataParameters.Mode];
  2920. switch (mode)
  2921. {
  2922. case EncryptionMode.PayloadEncryption:
  2923. byte[] encryptionSecret = (byte[])encryptionData[EncryptionDataParameters.Secret1];
  2924. this.LoadBalancingPeer.InitPayloadEncryption(encryptionSecret);
  2925. break;
  2926. case EncryptionMode.DatagramEncryption:
  2927. case EncryptionMode.DatagramEncryptionRandomSequence:
  2928. {
  2929. byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1];
  2930. byte[] secret2 = (byte[])encryptionData[EncryptionDataParameters.Secret2];
  2931. this.LoadBalancingPeer.InitDatagramEncryption(secret1, secret2, mode == EncryptionMode.DatagramEncryptionRandomSequence);
  2932. }
  2933. break;
  2934. case EncryptionMode.DatagramEncryptionGCM:
  2935. {
  2936. byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1];
  2937. this.LoadBalancingPeer.InitDatagramEncryption(secret1, null, true, true);
  2938. }
  2939. break;
  2940. default:
  2941. throw new ArgumentOutOfRangeException();
  2942. }
  2943. }
  2944. /// <summary>
  2945. /// This operation makes Photon call your custom web-service by path/name with the given parameters (converted into Json).
  2946. /// Use <see cref="IWebRpcCallback.OnWebRpcResponse"/> as a callback.
  2947. /// </summary>
  2948. /// <remarks>
  2949. /// A WebRPC calls a custom, http-based function on a server you provide. The uriPath is relative to a "base path"
  2950. /// which is configured server-side. The sent parameters get converted from C# types to Json. Vice versa, the response
  2951. /// of the web-service will be converted to C# types and sent back as normal operation response.
  2952. ///
  2953. /// To use this feature, you have to setup your server:
  2954. ///
  2955. /// For a Photon Cloud application, <a href="https://doc.photonengine.com/en-us/realtime/current/reference/webhooks">
  2956. /// visit the Dashboard </a> and setup "WebHooks". The BaseUrl is used for WebRPCs as well.
  2957. ///
  2958. /// The class <see cref="WebRpcResponse"/> is a helper-class that extracts the most valuable content from the WebRPC
  2959. /// response.
  2960. /// </remarks>
  2961. /// <param name="uriPath">The url path to call, relative to the baseUrl configured on Photon's server-side.</param>
  2962. /// <param name="parameters">The parameters to send to the web-service method.</param>
  2963. /// <param name="sendAuthCookie">Defines if the authentication cookie gets sent to a WebHook (if setup).</param>
  2964. public bool OpWebRpc(string uriPath, object parameters, bool sendAuthCookie = false)
  2965. {
  2966. if (string.IsNullOrEmpty(uriPath))
  2967. {
  2968. this.DebugReturn(DebugLevel.ERROR, "WebRPC method name must not be null nor empty.");
  2969. return false;
  2970. }
  2971. if (!this.CheckIfOpCanBeSent(OperationCode.WebRpc, this.Server, "WebRpc"))
  2972. {
  2973. return false;
  2974. }
  2975. Dictionary<byte, object> opParameters = new Dictionary<byte, object>();
  2976. opParameters.Add(ParameterCode.UriPath, uriPath);
  2977. if (parameters != null)
  2978. {
  2979. opParameters.Add(ParameterCode.WebRpcParameters, parameters);
  2980. }
  2981. if (sendAuthCookie)
  2982. {
  2983. opParameters.Add(ParameterCode.EventForward, WebFlags.SendAuthCookieConst);
  2984. }
  2985. //return this.LoadBalancingPeer.OpCustom(OperationCode.WebRpc, opParameters, true);
  2986. return this.LoadBalancingPeer.SendOperation(OperationCode.WebRpc, opParameters, SendOptions.SendReliable);
  2987. }
  2988. /// <summary>
  2989. /// Registers an object for callbacks for the implemented callback-interfaces.
  2990. /// </summary>
  2991. /// <remarks>
  2992. /// Adding and removing callback targets is queued to not mess with callbacks in execution.
  2993. /// Internally, this means that the addition/removal is done before the LoadBalancingClient
  2994. /// calls the next callbacks. This detail should not affect a game's workflow.
  2995. ///
  2996. /// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks,
  2997. /// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback.
  2998. ///
  2999. /// See: <a href="https://doc.photonengine.com/en-us/realtime/current/reference/dotnet-callbacks"/>
  3000. /// </remarks>
  3001. /// <param name="target">The object that registers to get callbacks from this client.</param>
  3002. public void AddCallbackTarget(object target)
  3003. {
  3004. this.callbackTargetChanges.Enqueue(new CallbackTargetChange(target, true));
  3005. }
  3006. /// <summary>
  3007. /// Unregisters an object from callbacks for the implemented callback-interfaces.
  3008. /// </summary>
  3009. /// <remarks>
  3010. /// Adding and removing callback targets is queued to not mess with callbacks in execution.
  3011. /// Internally, this means that the addition/removal is done before the LoadBalancingClient
  3012. /// calls the next callbacks. This detail should not affect a game's workflow.
  3013. ///
  3014. /// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks,
  3015. /// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback.
  3016. ///
  3017. /// See: <a href="https://doc.photonengine.com/en-us/realtime/current/reference/dotnet-callbacks"></a>
  3018. /// </remarks>
  3019. /// <param name="target">The object that unregisters from getting callbacks.</param>
  3020. public void RemoveCallbackTarget(object target)
  3021. {
  3022. this.callbackTargetChanges.Enqueue(new CallbackTargetChange(target, false));
  3023. }
  3024. /// <summary>
  3025. /// Applies queued callback cahnges from a queue to the actual containers. Will cause exceptions if used while callbacks execute.
  3026. /// </summary>
  3027. /// <remarks>
  3028. /// There is no explicit check that this is not called during callbacks, however the implemented, private logic takes care of this.
  3029. /// </remarks>
  3030. protected internal void UpdateCallbackTargets()
  3031. {
  3032. while (this.callbackTargetChanges.Count > 0)
  3033. {
  3034. CallbackTargetChange change = this.callbackTargetChanges.Dequeue();
  3035. if (change.AddTarget)
  3036. {
  3037. if (this.callbackTargets.Contains(change.Target))
  3038. {
  3039. //Debug.Log("UpdateCallbackTargets skipped adding a target, as the object is already registered. Target: " + change.Target);
  3040. continue;
  3041. }
  3042. this.callbackTargets.Add(change.Target);
  3043. }
  3044. else
  3045. {
  3046. if (!this.callbackTargets.Contains(change.Target))
  3047. {
  3048. //Debug.Log("UpdateCallbackTargets skipped removing a target, as the object is not registered. Target: " + change.Target);
  3049. continue;
  3050. }
  3051. this.callbackTargets.Remove(change.Target);
  3052. }
  3053. this.UpdateCallbackTarget<IInRoomCallbacks>(change, this.InRoomCallbackTargets);
  3054. this.UpdateCallbackTarget<IConnectionCallbacks>(change, this.ConnectionCallbackTargets);
  3055. this.UpdateCallbackTarget<IMatchmakingCallbacks>(change, this.MatchMakingCallbackTargets);
  3056. this.UpdateCallbackTarget<ILobbyCallbacks>(change, this.LobbyCallbackTargets);
  3057. this.UpdateCallbackTarget<IWebRpcCallback>(change, this.WebRpcCallbackTargets);
  3058. this.UpdateCallbackTarget<IErrorInfoCallback>(change, this.ErrorInfoCallbackTargets);
  3059. IOnEventCallback onEventCallback = change.Target as IOnEventCallback;
  3060. if (onEventCallback != null)
  3061. {
  3062. if (change.AddTarget)
  3063. {
  3064. EventReceived += onEventCallback.OnEvent;
  3065. }
  3066. else
  3067. {
  3068. EventReceived -= onEventCallback.OnEvent;
  3069. }
  3070. }
  3071. }
  3072. }
  3073. /// <summary>Helper method to cast and apply a target per (interface) type.</summary>
  3074. /// <typeparam name="T">Either of the interfaces for callbacks.</typeparam>
  3075. /// <param name="change">The queued change to apply (add or remove) some target.</param>
  3076. /// <param name="container">The container that calls callbacks on it's list of targets.</param>
  3077. private void UpdateCallbackTarget<T>(CallbackTargetChange change, List<T> container) where T : class
  3078. {
  3079. T target = change.Target as T;
  3080. if (target != null)
  3081. {
  3082. if (change.AddTarget)
  3083. {
  3084. container.Add(target);
  3085. }
  3086. else
  3087. {
  3088. container.Remove(target);
  3089. }
  3090. }
  3091. }
  3092. }
  3093. /// <summary>
  3094. /// Collection of "organizational" callbacks for the Realtime Api to cover: Connection and Regions.
  3095. /// </summary>
  3096. /// <remarks>
  3097. /// Classes that implement this interface must be registered to get callbacks for various situations.
  3098. ///
  3099. /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
  3100. /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
  3101. ///
  3102. /// </remarks>
  3103. /// \ingroup callbacks
  3104. public interface IConnectionCallbacks
  3105. {
  3106. /// <summary>
  3107. /// Called to signal that the "low level connection" got established but before the client can call operation on the server.
  3108. /// </summary>
  3109. /// <remarks>
  3110. /// After the (low level transport) connection is established, the client will automatically send
  3111. /// the Authentication operation, which needs to get a response before the client can call other operations.
  3112. ///
  3113. /// Your logic should wait for either: OnRegionListReceived or OnConnectedToMaster.
  3114. ///
  3115. /// This callback is useful to detect if the server can be reached at all (technically).
  3116. /// Most often, it's enough to implement OnDisconnected(DisconnectCause cause) and check for the cause.
  3117. ///
  3118. /// This is not called for transitions from the masterserver to game servers.
  3119. /// </remarks>
  3120. void OnConnected();
  3121. /// <summary>
  3122. /// Called when the client is connected to the Master Server and ready for matchmaking and other tasks.
  3123. /// </summary>
  3124. /// <remarks>
  3125. /// The list of available rooms won't become available unless you join a lobby via LoadBalancingClient.OpJoinLobby.
  3126. /// You can join rooms and create them even without being in a lobby. The default lobby is used in that case.
  3127. /// </remarks>
  3128. void OnConnectedToMaster();
  3129. /// <summary>
  3130. /// Called after disconnecting from the Photon server. It could be a failure or an explicit disconnect call
  3131. /// </summary>
  3132. /// <remarks>
  3133. /// The reason for this disconnect is provided as DisconnectCause.
  3134. /// </remarks>
  3135. void OnDisconnected(DisconnectCause cause);
  3136. /// <summary>
  3137. /// Called when the Name Server provided a list of regions for your title.
  3138. /// </summary>
  3139. /// <remarks>Check the RegionHandler class description, to make use of the provided values.</remarks>
  3140. /// <param name="regionHandler">The currently used RegionHandler.</param>
  3141. void OnRegionListReceived(RegionHandler regionHandler);
  3142. /// <summary>
  3143. /// Called when your Custom Authentication service responds with additional data.
  3144. /// </summary>
  3145. /// <remarks>
  3146. /// Custom Authentication services can include some custom data in their response.
  3147. /// When present, that data is made available in this callback as Dictionary.
  3148. /// While the keys of your data have to be strings, the values can be either string or a number (in Json).
  3149. /// You need to make extra sure, that the value type is the one you expect. Numbers become (currently) int64.
  3150. ///
  3151. /// Example: void OnCustomAuthenticationResponse(Dictionary&lt;string, object&gt; data) { ... }
  3152. /// </remarks>
  3153. /// <see cref="https://doc.photonengine.com/en-us/realtime/current/reference/custom-authentication"/>
  3154. void OnCustomAuthenticationResponse(Dictionary<string, object> data);
  3155. /// <summary>
  3156. /// Called when the custom authentication failed. Followed by disconnect!
  3157. /// </summary>
  3158. /// <remarks>
  3159. /// Custom Authentication can fail due to user-input, bad tokens/secrets.
  3160. /// If authentication is successful, this method is not called. Implement OnJoinedLobby() or OnConnectedToMaster() (as usual).
  3161. ///
  3162. /// During development of a game, it might also fail due to wrong configuration on the server side.
  3163. /// In those cases, logging the debugMessage is very important.
  3164. ///
  3165. /// Unless you setup a custom authentication service for your app (in the [Dashboard](https://dashboard.photonengine.com)),
  3166. /// this won't be called!
  3167. /// </remarks>
  3168. /// <param name="debugMessage">Contains a debug message why authentication failed. This has to be fixed during development.</param>
  3169. void OnCustomAuthenticationFailed(string debugMessage);
  3170. }
  3171. /// <summary>
  3172. /// Collection of "organizational" callbacks for the Realtime Api to cover the Lobby.
  3173. /// </summary>
  3174. /// <remarks>
  3175. /// Classes that implement this interface must be registered to get callbacks for various situations.
  3176. ///
  3177. /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
  3178. /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
  3179. ///
  3180. /// </remarks>
  3181. /// \ingroup callbacks
  3182. public interface ILobbyCallbacks
  3183. {
  3184. /// <summary>
  3185. /// Called on entering a lobby on the Master Server. The actual room-list updates will call OnRoomListUpdate.
  3186. /// </summary>
  3187. /// <remarks>
  3188. /// While in the lobby, the roomlist is automatically updated in fixed intervals (which you can't modify in the public cloud).
  3189. /// The room list gets available via OnRoomListUpdate.
  3190. /// </remarks>
  3191. void OnJoinedLobby();
  3192. /// <summary>
  3193. /// Called after leaving a lobby.
  3194. /// </summary>
  3195. /// <remarks>
  3196. /// When you leave a lobby, [OpCreateRoom](@ref OpCreateRoom) and [OpJoinRandomRoom](@ref OpJoinRandomRoom)
  3197. /// automatically refer to the default lobby.
  3198. /// </remarks>
  3199. void OnLeftLobby();
  3200. /// <summary>
  3201. /// Called for any update of the room-listing while in a lobby (InLobby) on the Master Server.
  3202. /// </summary>
  3203. /// <remarks>
  3204. /// Each item is a RoomInfo which might include custom properties (provided you defined those as lobby-listed when creating a room).
  3205. /// Not all types of lobbies provide a listing of rooms to the client. Some are silent and specialized for server-side matchmaking.
  3206. /// </remarks>
  3207. void OnRoomListUpdate(List<RoomInfo> roomList);
  3208. /// <summary>
  3209. /// Called when the Master Server sent an update for the Lobby Statistics.
  3210. /// </summary>
  3211. /// <remarks>
  3212. /// This callback has two preconditions:
  3213. /// EnableLobbyStatistics must be set to true, before this client connects.
  3214. /// And the client has to be connected to the Master Server, which is providing the info about lobbies.
  3215. /// </remarks>
  3216. void OnLobbyStatisticsUpdate(List<TypedLobbyInfo> lobbyStatistics);
  3217. }
  3218. /// <summary>
  3219. /// Collection of "organizational" callbacks for the Realtime Api to cover Matchmaking.
  3220. /// </summary>
  3221. /// <remarks>
  3222. /// Classes that implement this interface must be registered to get callbacks for various situations.
  3223. ///
  3224. /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
  3225. /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
  3226. ///
  3227. /// </remarks>
  3228. /// \ingroup callbacks
  3229. public interface IMatchmakingCallbacks
  3230. {
  3231. /// <summary>
  3232. /// Called when the server sent the response to a FindFriends request.
  3233. /// </summary>
  3234. /// <remarks>
  3235. /// After calling OpFindFriends, the Master Server will cache the friend list and send updates to the friend
  3236. /// list. The friends includes the name, userId, online state and the room (if any) for each requested user/friend.
  3237. ///
  3238. /// Use the friendList to update your UI and store it, if the UI should highlight changes.
  3239. /// </remarks>
  3240. void OnFriendListUpdate(List<FriendInfo> friendList);
  3241. /// <summary>
  3242. /// Called when this client created a room and entered it. OnJoinedRoom() will be called as well.
  3243. /// </summary>
  3244. /// <remarks>
  3245. /// This callback is only called on the client which created a room (see OpCreateRoom).
  3246. ///
  3247. /// As any client might close (or drop connection) anytime, there is a chance that the
  3248. /// creator of a room does not execute OnCreatedRoom.
  3249. ///
  3250. /// If you need specific room properties or a "start signal", implement OnMasterClientSwitched()
  3251. /// and make each new MasterClient check the room's state.
  3252. /// </remarks>
  3253. void OnCreatedRoom();
  3254. /// <summary>
  3255. /// Called when the server couldn't create a room (OpCreateRoom failed).
  3256. /// </summary>
  3257. /// <remarks>
  3258. /// Creating a room may fail for various reasons. Most often, the room already exists (roomname in use) or
  3259. /// the RoomOptions clash and it's impossible to create the room.
  3260. ///
  3261. /// When creating a room fails on a Game Server:
  3262. /// The client will cache the failure internally and returns to the Master Server before it calls the fail-callback.
  3263. /// This way, the client is ready to find/create a room at the moment of the callback.
  3264. /// In this case, the client skips calling OnConnectedToMaster but returning to the Master Server will still call OnConnected.
  3265. /// Treat callbacks of OnConnected as pure information that the client could connect.
  3266. /// </remarks>
  3267. /// <param name="returnCode">Operation ReturnCode from the server.</param>
  3268. /// <param name="message">Debug message for the error.</param>
  3269. void OnCreateRoomFailed(short returnCode, string message);
  3270. /// <summary>
  3271. /// Called when the LoadBalancingClient entered a room, no matter if this client created it or simply joined.
  3272. /// </summary>
  3273. /// <remarks>
  3274. /// When this is called, you can access the existing players in Room.Players, their custom properties and Room.CustomProperties.
  3275. ///
  3276. /// In this callback, you could create player objects. For example in Unity, instantiate a prefab for the player.
  3277. ///
  3278. /// If you want a match to be started "actively", enable the user to signal "ready" (using OpRaiseEvent or a Custom Property).
  3279. /// </remarks>
  3280. void OnJoinedRoom();
  3281. /// <summary>
  3282. /// Called when a previous OpJoinRoom call failed on the server.
  3283. /// </summary>
  3284. /// <remarks>
  3285. /// Joining a room may fail for various reasons. Most often, the room is full or does not exist anymore
  3286. /// (due to someone else being faster or closing the room).
  3287. ///
  3288. /// When joining a room fails on a Game Server:
  3289. /// The client will cache the failure internally and returns to the Master Server before it calls the fail-callback.
  3290. /// This way, the client is ready to find/create a room at the moment of the callback.
  3291. /// In this case, the client skips calling OnConnectedToMaster but returning to the Master Server will still call OnConnected.
  3292. /// Treat callbacks of OnConnected as pure information that the client could connect.
  3293. /// </remarks>
  3294. /// <param name="returnCode">Operation ReturnCode from the server.</param>
  3295. /// <param name="message">Debug message for the error.</param>
  3296. void OnJoinRoomFailed(short returnCode, string message);
  3297. /// <summary>
  3298. /// Called when a previous OpJoinRandom call failed on the server.
  3299. /// </summary>
  3300. /// <remarks>
  3301. /// The most common causes are that a room is full or does not exist (due to someone else being faster or closing the room).
  3302. ///
  3303. /// This operation is only ever sent to the Master Server. Once a room is found by the Master Server, the client will
  3304. /// head off to the designated Game Server and use the operation Join on the Game Server.
  3305. ///
  3306. /// When using multiple lobbies (via OpJoinLobby or a TypedLobby parameter), another lobby might have more/fitting rooms.<br/>
  3307. /// </remarks>
  3308. /// <param name="returnCode">Operation ReturnCode from the server.</param>
  3309. /// <param name="message">Debug message for the error.</param>
  3310. void OnJoinRandomFailed(short returnCode, string message);
  3311. /// <summary>
  3312. /// Called when the local user/client left a room, so the game's logic can clean up it's internal state.
  3313. /// </summary>
  3314. /// <remarks>
  3315. /// When leaving a room, the LoadBalancingClient will disconnect the Game Server and connect to the Master Server.
  3316. /// This wraps up multiple internal actions.
  3317. ///
  3318. /// Wait for the callback OnConnectedToMaster, before you use lobbies and join or create rooms.
  3319. /// </remarks>
  3320. void OnLeftRoom();
  3321. }
  3322. /// <summary>
  3323. /// Collection of "in room" callbacks for the Realtime Api to cover: Players entering or leaving, property updates and Master Client switching.
  3324. /// </summary>
  3325. /// <remarks>
  3326. /// Classes that implement this interface must be registered to get callbacks for various situations.
  3327. ///
  3328. /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
  3329. /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
  3330. ///
  3331. /// </remarks>
  3332. /// \ingroup callbacks
  3333. public interface IInRoomCallbacks
  3334. {
  3335. /// <summary>
  3336. /// Called when a remote player entered the room. This Player is already added to the playerlist.
  3337. /// </summary>
  3338. /// <remarks>
  3339. /// If your game starts with a certain number of players, this callback can be useful to check the
  3340. /// Room.playerCount and find out if you can start.
  3341. /// </remarks>
  3342. void OnPlayerEnteredRoom(Player newPlayer);
  3343. /// <summary>
  3344. /// Called when a remote player left the room or became inactive. Check otherPlayer.IsInactive.
  3345. /// </summary>
  3346. /// <remarks>
  3347. /// If another player leaves the room or if the server detects a lost connection, this callback will
  3348. /// be used to notify your game logic.
  3349. ///
  3350. /// Depending on the room's setup, players may become inactive, which means they may return and retake
  3351. /// their spot in the room. In such cases, the Player stays in the Room.Players dictionary.
  3352. ///
  3353. /// If the player is not just inactive, it gets removed from the Room.Players dictionary, before
  3354. /// the callback is called.
  3355. /// </remarks>
  3356. void OnPlayerLeftRoom(Player otherPlayer);
  3357. /// <summary>
  3358. /// Called when a room's custom properties changed. The propertiesThatChanged contains all that was set via Room.SetCustomProperties.
  3359. /// </summary>
  3360. /// <remarks>
  3361. /// Since v1.25 this method has one parameter: Hashtable propertiesThatChanged.<br/>
  3362. /// Changing properties must be done by Room.SetCustomProperties, which causes this callback locally, too.
  3363. /// </remarks>
  3364. /// <param name="propertiesThatChanged"></param>
  3365. void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged);
  3366. /// <summary>
  3367. /// Called when custom player-properties are changed. Player and the changed properties are passed as object[].
  3368. /// </summary>
  3369. /// <remarks>
  3370. /// Changing properties must be done by Player.SetCustomProperties, which causes this callback locally, too.
  3371. /// </remarks>
  3372. /// <param name="targetPlayer">Contains Player that changed.</param>
  3373. /// <param name="changedProps">Contains the properties that changed.</param>
  3374. void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps);
  3375. /// <summary>
  3376. /// Called after switching to a new MasterClient when the current one leaves.
  3377. /// </summary>
  3378. /// <remarks>
  3379. /// This is not called when this client enters a room.
  3380. /// The former MasterClient is still in the player list when this method get called.
  3381. /// </remarks>
  3382. void OnMasterClientSwitched(Player newMasterClient);
  3383. }
  3384. /// <summary>
  3385. /// Event callback for the Realtime Api. Covers events from the server and those sent by clients via OpRaiseEvent.
  3386. /// </summary>
  3387. /// <remarks>
  3388. /// Classes that implement this interface must be registered to get callbacks for various situations.
  3389. ///
  3390. /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
  3391. /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
  3392. ///
  3393. /// </remarks>
  3394. /// \ingroup callbacks
  3395. public interface IOnEventCallback
  3396. {
  3397. /// <summary>Called for any incoming events.</summary>
  3398. /// <remarks>
  3399. /// To receive events, implement IOnEventCallback in any class and register it via AddCallbackTarget
  3400. /// (either in LoadBalancingClient or PhotonNetwork).
  3401. ///
  3402. /// With the EventData.Sender you can look up the Player who sent the event.
  3403. ///
  3404. /// It is best practice to assign an eventCode for each different type of content and action, so the Code
  3405. /// will be essential to read the incoming events.
  3406. /// </remarks>
  3407. void OnEvent(EventData photonEvent);
  3408. }
  3409. /// <summary>
  3410. /// Interface for "WebRpc" callbacks for the Realtime Api. Currently includes only responses for Web RPCs.
  3411. /// </summary>
  3412. /// <remarks>
  3413. /// Classes that implement this interface must be registered to get callbacks for various situations.
  3414. ///
  3415. /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
  3416. /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
  3417. ///
  3418. /// </remarks>
  3419. /// \ingroup callbacks
  3420. public interface IWebRpcCallback
  3421. {
  3422. /// <summary>
  3423. /// Called when the response to a WebRPC is available. See <see cref="LoadBalancingClient.OpWebRpc"/>.
  3424. /// </summary>
  3425. /// <remarks>
  3426. /// Important: The response.ReturnCode is 0 if Photon was able to reach your web-service.<br/>
  3427. /// The content of the response is what your web-service sent. You can create a WebRpcResponse from it.<br/>
  3428. /// Example: WebRpcResponse webResponse = new WebRpcResponse(operationResponse);<br/>
  3429. ///
  3430. /// Please note: Class OperationResponse is in a namespace which needs to be "used":<br/>
  3431. /// using ExitGames.Client.Photon; // includes OperationResponse (and other classes)
  3432. /// </remarks>
  3433. /// <example>
  3434. /// public void OnWebRpcResponse(OperationResponse response)
  3435. /// {
  3436. /// Debug.LogFormat("WebRPC operation response {0}", response.ToStringFull());
  3437. /// switch (response.ReturnCode)
  3438. /// {
  3439. /// case ErrorCode.Ok:
  3440. /// WebRpcResponse webRpcResponse = new WebRpcResponse(response);
  3441. /// Debug.LogFormat("Parsed WebRPC response {0}", response.ToStringFull());
  3442. /// if (string.IsNullOrEmpty(webRpcResponse.Name))
  3443. /// {
  3444. /// Debug.LogError("Unexpected: WebRPC response did not contain WebRPC method name");
  3445. /// }
  3446. /// if (webRpcResponse.ResultCode == 0) // success
  3447. /// {
  3448. /// switch (webRpcResponse.Name)
  3449. /// {
  3450. /// // todo: add your code here
  3451. /// case GetGameListWebRpcMethodName: // example
  3452. /// // ...
  3453. /// break;
  3454. /// }
  3455. /// }
  3456. /// else if (webRpcResponse.ResultCode == -1)
  3457. /// {
  3458. /// Debug.LogErrorFormat("Web server did not return ResultCode for WebRPC method=\"{0}\", Message={1}", webRpcResponse.Name, webRpcResponse.Message);
  3459. /// }
  3460. /// else
  3461. /// {
  3462. /// Debug.LogErrorFormat("Web server returned ResultCode={0} for WebRPC method=\"{1}\", Message={2}", webRpcResponse.ResultCode, webRpcResponse.Name, webRpcResponse.Message);
  3463. /// }
  3464. /// break;
  3465. /// case ErrorCode.ExternalHttpCallFailed: // web service unreachable
  3466. /// Debug.LogErrorFormat("WebRPC call failed as request could not be sent to the server. {0}", response.DebugMessage);
  3467. /// break;
  3468. /// case ErrorCode.HttpLimitReached: // too many WebRPCs in a short period of time
  3469. /// // the debug message should contain the limit exceeded
  3470. /// Debug.LogErrorFormat("WebRPCs rate limit exceeded: {0}", response.DebugMessage);
  3471. /// break;
  3472. /// case ErrorCode.InvalidOperation: // WebRPC not configured at all OR not configured properly OR trying to send on name server
  3473. /// if (PhotonNetwork.Server == ServerConnection.NameServer)
  3474. /// {
  3475. /// Debug.LogErrorFormat("WebRPC not supported on NameServer. {0}", response.DebugMessage);
  3476. /// }
  3477. /// else
  3478. /// {
  3479. /// Debug.LogErrorFormat("WebRPC not properly configured or not configured at all. {0}", response.DebugMessage);
  3480. /// }
  3481. /// break;
  3482. /// default:
  3483. /// // other unknown error, unexpected
  3484. /// Debug.LogErrorFormat("Unexpected error, {0} {1}", response.ReturnCode, response.DebugMessage);
  3485. /// break;
  3486. /// }
  3487. /// }
  3488. ///
  3489. /// </example>
  3490. void OnWebRpcResponse(OperationResponse response);
  3491. }
  3492. /// <summary>
  3493. /// Interface for <see cref="EventCode.ErrorInfo"/> event callback for the Realtime Api.
  3494. /// </summary>
  3495. /// <remarks>
  3496. /// Classes that implement this interface must be registered to get callbacks for various situations.
  3497. ///
  3498. /// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
  3499. /// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
  3500. ///
  3501. /// </remarks>
  3502. /// \ingroup callbacks
  3503. public interface IErrorInfoCallback
  3504. {
  3505. /// <summary>
  3506. /// Called when the client receives an event from the server indicating that an error happened there.
  3507. /// </summary>
  3508. /// <remarks>
  3509. /// In most cases this could be either:
  3510. /// 1. an error from webhooks plugin (if HasErrorInfo is enabled), read more here:
  3511. /// https://doc.photonengine.com/en-us/realtime/current/gameplay/web-extensions/webhooks#options
  3512. /// 2. an error sent from a custom server plugin via PluginHost.BroadcastErrorInfoEvent, see example here:
  3513. /// https://doc.photonengine.com/en-us/server/current/plugins/manual#handling_http_response
  3514. /// 3. an error sent from the server, for example, when the limit of cached events has been exceeded in the room
  3515. /// (all clients will be disconnected and the room will be closed in this case)
  3516. /// read more here: https://doc.photonengine.com/en-us/realtime/current/gameplay/cached-events#special_considerations
  3517. ///
  3518. /// If you implement <see cref="IOnEventCallback.OnEvent"/> or <see cref="LoadBalancingClient.EventReceived"/> you will also get this event.
  3519. /// </remarks>
  3520. /// <param name="errorInfo">Object containing information about the error</param>
  3521. void OnErrorInfo(ErrorInfo errorInfo);
  3522. }
  3523. /// <summary>
  3524. /// Container type for callbacks defined by IConnectionCallbacks. See LoadBalancingCallbackTargets.
  3525. /// </summary>
  3526. /// <remarks>
  3527. /// While the interfaces of callbacks wrap up the methods that will be called,
  3528. /// the container classes implement a simple way to call a method on all registered objects.
  3529. /// </remarks>
  3530. public class ConnectionCallbacksContainer : List<IConnectionCallbacks>, IConnectionCallbacks
  3531. {
  3532. private readonly LoadBalancingClient client;
  3533. public ConnectionCallbacksContainer(LoadBalancingClient client)
  3534. {
  3535. this.client = client;
  3536. }
  3537. public void OnConnected()
  3538. {
  3539. this.client.UpdateCallbackTargets();
  3540. foreach (IConnectionCallbacks target in this)
  3541. {
  3542. target.OnConnected();
  3543. }
  3544. }
  3545. public void OnConnectedToMaster()
  3546. {
  3547. this.client.UpdateCallbackTargets();
  3548. foreach (IConnectionCallbacks target in this)
  3549. {
  3550. target.OnConnectedToMaster();
  3551. }
  3552. }
  3553. public void OnRegionListReceived(RegionHandler regionHandler)
  3554. {
  3555. this.client.UpdateCallbackTargets();
  3556. foreach (IConnectionCallbacks target in this)
  3557. {
  3558. target.OnRegionListReceived(regionHandler);
  3559. }
  3560. }
  3561. public void OnDisconnected(DisconnectCause cause)
  3562. {
  3563. this.client.UpdateCallbackTargets();
  3564. foreach (IConnectionCallbacks target in this)
  3565. {
  3566. target.OnDisconnected(cause);
  3567. }
  3568. }
  3569. public void OnCustomAuthenticationResponse(Dictionary<string, object> data)
  3570. {
  3571. this.client.UpdateCallbackTargets();
  3572. foreach (IConnectionCallbacks target in this)
  3573. {
  3574. target.OnCustomAuthenticationResponse(data);
  3575. }
  3576. }
  3577. public void OnCustomAuthenticationFailed(string debugMessage)
  3578. {
  3579. this.client.UpdateCallbackTargets();
  3580. foreach (IConnectionCallbacks target in this)
  3581. {
  3582. target.OnCustomAuthenticationFailed(debugMessage);
  3583. }
  3584. }
  3585. }
  3586. /// <summary>
  3587. /// Container type for callbacks defined by IMatchmakingCallbacks. See MatchMakingCallbackTargets.
  3588. /// </summary>
  3589. /// <remarks>
  3590. /// While the interfaces of callbacks wrap up the methods that will be called,
  3591. /// the container classes implement a simple way to call a method on all registered objects.
  3592. /// </remarks>
  3593. public class MatchMakingCallbacksContainer : List<IMatchmakingCallbacks>, IMatchmakingCallbacks
  3594. {
  3595. private readonly LoadBalancingClient client;
  3596. public MatchMakingCallbacksContainer(LoadBalancingClient client)
  3597. {
  3598. this.client = client;
  3599. }
  3600. public void OnCreatedRoom()
  3601. {
  3602. this.client.UpdateCallbackTargets();
  3603. foreach (IMatchmakingCallbacks target in this)
  3604. {
  3605. target.OnCreatedRoom();
  3606. }
  3607. }
  3608. public void OnJoinedRoom()
  3609. {
  3610. this.client.UpdateCallbackTargets();
  3611. foreach (IMatchmakingCallbacks target in this)
  3612. {
  3613. target.OnJoinedRoom();
  3614. }
  3615. }
  3616. public void OnCreateRoomFailed(short returnCode, string message)
  3617. {
  3618. this.client.UpdateCallbackTargets();
  3619. foreach (IMatchmakingCallbacks target in this)
  3620. {
  3621. target.OnCreateRoomFailed(returnCode, message);
  3622. }
  3623. }
  3624. public void OnJoinRandomFailed(short returnCode, string message)
  3625. {
  3626. this.client.UpdateCallbackTargets();
  3627. foreach (IMatchmakingCallbacks target in this)
  3628. {
  3629. target.OnJoinRandomFailed(returnCode, message);
  3630. }
  3631. }
  3632. public void OnJoinRoomFailed(short returnCode, string message)
  3633. {
  3634. this.client.UpdateCallbackTargets();
  3635. foreach (IMatchmakingCallbacks target in this)
  3636. {
  3637. target.OnJoinRoomFailed(returnCode, message);
  3638. }
  3639. }
  3640. public void OnLeftRoom()
  3641. {
  3642. this.client.UpdateCallbackTargets();
  3643. foreach (IMatchmakingCallbacks target in this)
  3644. {
  3645. target.OnLeftRoom();
  3646. }
  3647. }
  3648. public void OnFriendListUpdate(List<FriendInfo> friendList)
  3649. {
  3650. this.client.UpdateCallbackTargets();
  3651. foreach (IMatchmakingCallbacks target in this)
  3652. {
  3653. target.OnFriendListUpdate(friendList);
  3654. }
  3655. }
  3656. }
  3657. /// <summary>
  3658. /// Container type for callbacks defined by IInRoomCallbacks. See InRoomCallbackTargets.
  3659. /// </summary>
  3660. /// <remarks>
  3661. /// While the interfaces of callbacks wrap up the methods that will be called,
  3662. /// the container classes implement a simple way to call a method on all registered objects.
  3663. /// </remarks>
  3664. internal class InRoomCallbacksContainer : List<IInRoomCallbacks>, IInRoomCallbacks
  3665. {
  3666. private readonly LoadBalancingClient client;
  3667. public InRoomCallbacksContainer(LoadBalancingClient client)
  3668. {
  3669. this.client = client;
  3670. }
  3671. public void OnPlayerEnteredRoom(Player newPlayer)
  3672. {
  3673. this.client.UpdateCallbackTargets();
  3674. foreach (IInRoomCallbacks target in this)
  3675. {
  3676. target.OnPlayerEnteredRoom(newPlayer);
  3677. }
  3678. }
  3679. public void OnPlayerLeftRoom(Player otherPlayer)
  3680. {
  3681. this.client.UpdateCallbackTargets();
  3682. foreach (IInRoomCallbacks target in this)
  3683. {
  3684. target.OnPlayerLeftRoom(otherPlayer);
  3685. }
  3686. }
  3687. public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
  3688. {
  3689. this.client.UpdateCallbackTargets();
  3690. foreach (IInRoomCallbacks target in this)
  3691. {
  3692. target.OnRoomPropertiesUpdate(propertiesThatChanged);
  3693. }
  3694. }
  3695. public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProp)
  3696. {
  3697. this.client.UpdateCallbackTargets();
  3698. foreach (IInRoomCallbacks target in this)
  3699. {
  3700. target.OnPlayerPropertiesUpdate(targetPlayer, changedProp);
  3701. }
  3702. }
  3703. public void OnMasterClientSwitched(Player newMasterClient)
  3704. {
  3705. this.client.UpdateCallbackTargets();
  3706. foreach (IInRoomCallbacks target in this)
  3707. {
  3708. target.OnMasterClientSwitched(newMasterClient);
  3709. }
  3710. }
  3711. }
  3712. /// <summary>
  3713. /// Container type for callbacks defined by ILobbyCallbacks. See LobbyCallbackTargets.
  3714. /// </summary>
  3715. /// <remarks>
  3716. /// While the interfaces of callbacks wrap up the methods that will be called,
  3717. /// the container classes implement a simple way to call a method on all registered objects.
  3718. /// </remarks>
  3719. internal class LobbyCallbacksContainer : List<ILobbyCallbacks>, ILobbyCallbacks
  3720. {
  3721. private readonly LoadBalancingClient client;
  3722. public LobbyCallbacksContainer(LoadBalancingClient client)
  3723. {
  3724. this.client = client;
  3725. }
  3726. public void OnJoinedLobby()
  3727. {
  3728. this.client.UpdateCallbackTargets();
  3729. foreach (ILobbyCallbacks target in this)
  3730. {
  3731. target.OnJoinedLobby();
  3732. }
  3733. }
  3734. public void OnLeftLobby()
  3735. {
  3736. this.client.UpdateCallbackTargets();
  3737. foreach (ILobbyCallbacks target in this)
  3738. {
  3739. target.OnLeftLobby();
  3740. }
  3741. }
  3742. public void OnRoomListUpdate(List<RoomInfo> roomList)
  3743. {
  3744. this.client.UpdateCallbackTargets();
  3745. foreach (ILobbyCallbacks target in this)
  3746. {
  3747. target.OnRoomListUpdate(roomList);
  3748. }
  3749. }
  3750. public void OnLobbyStatisticsUpdate(List<TypedLobbyInfo> lobbyStatistics)
  3751. {
  3752. this.client.UpdateCallbackTargets();
  3753. foreach (ILobbyCallbacks target in this)
  3754. {
  3755. target.OnLobbyStatisticsUpdate(lobbyStatistics);
  3756. }
  3757. }
  3758. }
  3759. /// <summary>
  3760. /// Container type for callbacks defined by IWebRpcCallback. See WebRpcCallbackTargets.
  3761. /// </summary>
  3762. /// <remarks>
  3763. /// While the interfaces of callbacks wrap up the methods that will be called,
  3764. /// the container classes implement a simple way to call a method on all registered objects.
  3765. /// </remarks>
  3766. internal class WebRpcCallbacksContainer : List<IWebRpcCallback>, IWebRpcCallback
  3767. {
  3768. private LoadBalancingClient client;
  3769. public WebRpcCallbacksContainer(LoadBalancingClient client)
  3770. {
  3771. this.client = client;
  3772. }
  3773. public void OnWebRpcResponse(OperationResponse response)
  3774. {
  3775. this.client.UpdateCallbackTargets();
  3776. foreach (IWebRpcCallback target in this)
  3777. {
  3778. target.OnWebRpcResponse(response);
  3779. }
  3780. }
  3781. }
  3782. /// <summary>
  3783. /// Container type for callbacks defined by <see cref="IErrorInfoCallback"/>. See <see cref="LoadBalancingClient.ErrorInfoCallbackTargets"/>.
  3784. /// </summary>
  3785. /// <remarks>
  3786. /// While the interfaces of callbacks wrap up the methods that will be called,
  3787. /// the container classes implement a simple way to call a method on all registered objects.
  3788. /// </remarks>
  3789. internal class ErrorInfoCallbacksContainer : List<IErrorInfoCallback>, IErrorInfoCallback
  3790. {
  3791. private LoadBalancingClient client;
  3792. public ErrorInfoCallbacksContainer(LoadBalancingClient client)
  3793. {
  3794. this.client = client;
  3795. }
  3796. public void OnErrorInfo(ErrorInfo errorInfo)
  3797. {
  3798. this.client.UpdateCallbackTargets();
  3799. foreach (IErrorInfoCallback target in this)
  3800. {
  3801. target.OnErrorInfo(errorInfo);
  3802. }
  3803. }
  3804. }
  3805. /// <summary>
  3806. /// Class wrapping the received <see cref="EventCode.ErrorInfo"/> event.
  3807. /// </summary>
  3808. /// <remarks>
  3809. /// This is passed inside <see cref="IErrorInfoCallback.OnErrorInfo"/> callback.
  3810. /// If you implement <see cref="IOnEventCallback.OnEvent"/> or <see cref="LoadBalancingClient.EventReceived"/> you will also get <see cref="EventCode.ErrorInfo"/> but not parsed.
  3811. ///
  3812. /// In most cases this could be either:
  3813. /// 1. an error from webhooks plugin (if HasErrorInfo is enabled), read more here:
  3814. /// https://doc.photonengine.com/en-us/realtime/current/gameplay/web-extensions/webhooks#options
  3815. /// 2. an error sent from a custom server plugin via PluginHost.BroadcastErrorInfoEvent, see example here:
  3816. /// https://doc.photonengine.com/en-us/server/current/plugins/manual#handling_http_response
  3817. /// 3. an error sent from the server, for example, when the limit of cached events has been exceeded in the room
  3818. /// (all clients will be disconnected and the room will be closed in this case)
  3819. /// read more here: https://doc.photonengine.com/en-us/realtime/current/gameplay/cached-events#special_considerations
  3820. /// </remarks>
  3821. public class ErrorInfo
  3822. {
  3823. /// <summary>
  3824. /// String containing information about the error.
  3825. /// </summary>
  3826. public readonly string Info;
  3827. public ErrorInfo(EventData eventData)
  3828. {
  3829. this.Info = eventData[ParameterCode.Info] as string;
  3830. }
  3831. public override string ToString()
  3832. {
  3833. return string.Format("ErrorInfo: {0}", this.Info);
  3834. }
  3835. }
  3836. }