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.

536 lines
22 KiB

4 years ago
  1. // ----------------------------------------------------------------------------
  2. // <copyright file="Room.cs" company="Exit Games GmbH">
  3. // Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH
  4. // </copyright>
  5. // <summary>
  6. // The Room class resembles the properties known about the room in which
  7. // a game/match happens.
  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 ExitGames.Client.Photon;
  20. #if SUPPORTED_UNITY || NETFX_CORE
  21. using Hashtable = ExitGames.Client.Photon.Hashtable;
  22. using SupportClass = ExitGames.Client.Photon.SupportClass;
  23. #endif
  24. /// <summary>
  25. /// This class represents a room a client joins/joined.
  26. /// </summary>
  27. /// <remarks>
  28. /// Contains a list of current players, their properties and those of this room, too.
  29. /// A room instance has a number of "well known" properties like IsOpen, MaxPlayers which can be changed.
  30. /// Your own, custom properties can be set via SetCustomProperties() while being in the room.
  31. ///
  32. /// Typically, this class should be extended by a game-specific implementation with logic and extra features.
  33. /// </remarks>
  34. public class Room : RoomInfo
  35. {
  36. /// <summary>
  37. /// A reference to the LoadBalancingClient which is currently keeping the connection and state.
  38. /// </summary>
  39. public LoadBalancingClient LoadBalancingClient { get; set; }
  40. /// <summary>The name of a room. Unique identifier (per region and virtual appid) for a room/match.</summary>
  41. /// <remarks>The name can't be changed once it's set by the server.</remarks>
  42. public new string Name
  43. {
  44. get
  45. {
  46. return this.name;
  47. }
  48. internal set
  49. {
  50. this.name = value;
  51. }
  52. }
  53. private bool isOffline;
  54. public bool IsOffline
  55. {
  56. get
  57. {
  58. return isOffline;
  59. }
  60. private set
  61. {
  62. isOffline = value;
  63. }
  64. }
  65. /// <summary>
  66. /// Defines if the room can be joined.
  67. /// </summary>
  68. /// <remarks>
  69. /// This does not affect listing in a lobby but joining the room will fail if not open.
  70. /// If not open, the room is excluded from random matchmaking.
  71. /// Due to racing conditions, found matches might become closed while users are trying to join.
  72. /// Simply re-connect to master and find another.
  73. /// Use property "IsVisible" to not list the room.
  74. ///
  75. /// As part of RoomInfo this can't be set.
  76. /// As part of a Room (which the player joined), the setter will update the server and all clients.
  77. /// </remarks>
  78. public new bool IsOpen
  79. {
  80. get
  81. {
  82. return this.isOpen;
  83. }
  84. set
  85. {
  86. if (value != this.isOpen)
  87. {
  88. if (!this.isOffline)
  89. {
  90. this.LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.IsOpen, value } });
  91. }
  92. }
  93. this.isOpen = value;
  94. }
  95. }
  96. /// <summary>
  97. /// Defines if the room is listed in its lobby.
  98. /// </summary>
  99. /// <remarks>
  100. /// Rooms can be created invisible, or changed to invisible.
  101. /// To change if a room can be joined, use property: open.
  102. ///
  103. /// As part of RoomInfo this can't be set.
  104. /// As part of a Room (which the player joined), the setter will update the server and all clients.
  105. /// </remarks>
  106. public new bool IsVisible
  107. {
  108. get
  109. {
  110. return this.isVisible;
  111. }
  112. set
  113. {
  114. if (value != this.isVisible)
  115. {
  116. if (!this.isOffline)
  117. {
  118. this.LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.IsVisible, value } });
  119. }
  120. }
  121. this.isVisible = value;
  122. }
  123. }
  124. /// <summary>
  125. /// Sets a limit of players to this room. This property is synced and shown in lobby, too.
  126. /// If the room is full (players count == maxplayers), joining this room will fail.
  127. /// </summary>
  128. /// <remarks>
  129. /// As part of RoomInfo this can't be set.
  130. /// As part of a Room (which the player joined), the setter will update the server and all clients.
  131. /// </remarks>
  132. public new byte MaxPlayers
  133. {
  134. get
  135. {
  136. return this.maxPlayers;
  137. }
  138. set
  139. {
  140. if (value != this.maxPlayers)
  141. {
  142. if (!this.isOffline)
  143. {
  144. this.LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.MaxPlayers, value } });
  145. }
  146. }
  147. this.maxPlayers = value;
  148. }
  149. }
  150. /// <summary>The count of players in this Room (using this.Players.Count).</summary>
  151. public new byte PlayerCount
  152. {
  153. get
  154. {
  155. if (this.Players == null)
  156. {
  157. return 0;
  158. }
  159. return (byte)this.Players.Count;
  160. }
  161. }
  162. /// <summary>While inside a Room, this is the list of players who are also in that room.</summary>
  163. private Dictionary<int, Player> players = new Dictionary<int, Player>();
  164. /// <summary>While inside a Room, this is the list of players who are also in that room.</summary>
  165. public Dictionary<int, Player> Players
  166. {
  167. get
  168. {
  169. return this.players;
  170. }
  171. private set
  172. {
  173. this.players = value;
  174. }
  175. }
  176. /// <summary>
  177. /// List of users who are expected to join this room. In matchmaking, Photon blocks a slot for each of these UserIDs out of the MaxPlayers.
  178. /// </summary>
  179. /// <remarks>
  180. /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
  181. /// Define expected players in the methods: <see cref="LoadBalancingClient.OpCreateRoom"/>, <see cref="LoadBalancingClient.OpJoinRoom"/> and <see cref="LoadBalancingClient.OpJoinRandomRoom"/>.
  182. /// </remarks>
  183. public string[] ExpectedUsers
  184. {
  185. get { return this.expectedUsers; }
  186. }
  187. /// <summary>Player Time To Live. How long any player can be inactive (due to disconnect or leave) before the user gets removed from the playerlist (freeing a slot).</summary>
  188. public int PlayerTtl
  189. {
  190. get { return this.playerTtl; }
  191. set
  192. {
  193. if (value != this.playerTtl)
  194. {
  195. if (!this.isOffline)
  196. {
  197. this.LoadBalancingClient.OpSetPropertyOfRoom(GamePropertyKey.PlayerTtl, value); // TODO: implement Offline Mode
  198. }
  199. }
  200. this.playerTtl = value;
  201. }
  202. }
  203. /// <summary>Room Time To Live. How long a room stays available (and in server-memory), after the last player becomes inactive. After this time, the room gets persisted or destroyed.</summary>
  204. public int EmptyRoomTtl
  205. {
  206. get { return this.emptyRoomTtl; }
  207. set
  208. {
  209. if (value != this.emptyRoomTtl)
  210. {
  211. if (!this.isOffline)
  212. {
  213. this.LoadBalancingClient.OpSetPropertyOfRoom(GamePropertyKey.EmptyRoomTtl, value); // TODO: implement Offline Mode
  214. }
  215. }
  216. this.emptyRoomTtl = value;
  217. }
  218. }
  219. /// <summary>
  220. /// The ID (actorNumber, actorNumber) of the player who's the master of this Room.
  221. /// Note: This changes when the current master leaves the room.
  222. /// </summary>
  223. public int MasterClientId { get { return this.masterClientId; } }
  224. /// <summary>
  225. /// Gets a list of custom properties that are in the RoomInfo of the Lobby.
  226. /// This list is defined when creating the room and can't be changed afterwards. Compare: LoadBalancingClient.OpCreateRoom()
  227. /// </summary>
  228. /// <remarks>You could name properties that are not set from the beginning. Those will be synced with the lobby when added later on.</remarks>
  229. public string[] PropertiesListedInLobby
  230. {
  231. get
  232. {
  233. return this.propertiesListedInLobby;
  234. }
  235. private set
  236. {
  237. this.propertiesListedInLobby = value;
  238. }
  239. }
  240. /// <summary>
  241. /// Gets if this room uses autoCleanUp to remove all (buffered) RPCs and instantiated GameObjects when a player leaves.
  242. /// </summary>
  243. public bool AutoCleanUp
  244. {
  245. get
  246. {
  247. return this.autoCleanUp;
  248. }
  249. }
  250. /// <summary>Creates a Room (representation) with given name and properties and the "listing options" as provided by parameters.</summary>
  251. /// <param name="roomName">Name of the room (can be null until it's actually created on server).</param>
  252. /// <param name="options">Room options.</param>
  253. public Room(string roomName, RoomOptions options, bool isOffline = false) : base(roomName, options != null ? options.CustomRoomProperties : null)
  254. {
  255. // base() sets name and (custom)properties. here we set "well known" properties
  256. if (options != null)
  257. {
  258. this.isVisible = options.IsVisible;
  259. this.isOpen = options.IsOpen;
  260. this.maxPlayers = options.MaxPlayers;
  261. this.propertiesListedInLobby = options.CustomRoomPropertiesForLobby;
  262. //this.playerTtl = options.PlayerTtl; // set via well known properties
  263. //this.emptyRoomTtl = options.EmptyRoomTtl; // set via well known properties
  264. }
  265. this.isOffline = isOffline;
  266. }
  267. protected internal override void InternalCacheProperties(Hashtable propertiesToCache)
  268. {
  269. int oldMasterId = this.masterClientId;
  270. base.InternalCacheProperties(propertiesToCache); // important: updating the properties fields has no way to do callbacks on change
  271. if (oldMasterId != 0 && this.masterClientId != oldMasterId)
  272. {
  273. this.LoadBalancingClient.InRoomCallbackTargets.OnMasterClientSwitched(this.GetPlayer(this.masterClientId));
  274. }
  275. }
  276. /// <summary>
  277. /// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition.
  278. /// </summary>
  279. /// <remarks>
  280. /// Custom Properties are a set of string keys and arbitrary values which is synchronized
  281. /// for the players in a Room. They are available when the client enters the room, as
  282. /// they are in the response of OpJoin and OpCreate.
  283. ///
  284. /// Custom Properties either relate to the (current) Room or a Player (in that Room).
  285. ///
  286. /// Both classes locally cache the current key/values and make them available as
  287. /// property: CustomProperties. This is provided only to read them.
  288. /// You must use the method SetCustomProperties to set/modify them.
  289. ///
  290. /// Any client can set any Custom Properties anytime (when in a room).
  291. /// It's up to the game logic to organize how they are best used.
  292. ///
  293. /// You should call SetCustomProperties only with key/values that are new or changed. This reduces
  294. /// traffic and performance.
  295. ///
  296. /// Unless you define some expectedProperties, setting key/values is always permitted.
  297. /// In this case, the property-setting client will not receive the new values from the server but
  298. /// instead update its local cache in SetCustomProperties.
  299. ///
  300. /// If you define expectedProperties, the server will skip updates if the server property-cache
  301. /// does not contain all expectedProperties with the same values.
  302. /// In this case, the property-setting client will get an update from the server and update it's
  303. /// cached key/values at about the same time as everyone else.
  304. ///
  305. /// The benefit of using expectedProperties can be only one client successfully sets a key from
  306. /// one known value to another.
  307. /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
  308. /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
  309. /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
  310. /// take the item will have it (and the others fail to set the ownership).
  311. ///
  312. /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
  313. /// </remarks>
  314. /// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
  315. /// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values. Client must be in room.</param>
  316. /// <param name="webFlags">Defines if this SetCustomProperties-operation gets forwarded to your WebHooks. Client must be in room.</param>
  317. public virtual void SetCustomProperties(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
  318. {
  319. Hashtable customProps = propertiesToSet.StripToStringKeys() as Hashtable;
  320. if (this.isOffline)
  321. {
  322. // Merge and delete values.
  323. this.CustomProperties.Merge(customProps);
  324. this.CustomProperties.StripKeysWithNullValues();
  325. // invoking callbacks
  326. this.LoadBalancingClient.InRoomCallbackTargets.OnRoomPropertiesUpdate(propertiesToSet);
  327. }
  328. else
  329. {
  330. // merge (and delete null-values), unless we use CAS (expected props)
  331. if (expectedProperties == null || expectedProperties.Count == 0)
  332. {
  333. this.CustomProperties.Merge(customProps);
  334. this.CustomProperties.StripKeysWithNullValues();
  335. }
  336. // send (sync) these new values if in online room
  337. this.LoadBalancingClient.LoadBalancingPeer.OpSetPropertiesOfRoom(customProps, expectedProperties, webFlags);
  338. }
  339. }
  340. /// <summary>
  341. /// Enables you to define the properties available in the lobby if not all properties are needed to pick a room.
  342. /// </summary>
  343. /// <remarks>
  344. /// Limit the amount of properties sent to users in the lobby to improve speed and stability.
  345. /// </remarks>
  346. /// <param name="propertiesListedInLobby">An array of custom room property names to forward to the lobby.</param>
  347. public void SetPropertiesListedInLobby(string[] propertiesListedInLobby)
  348. {
  349. Hashtable customProps = new Hashtable();
  350. customProps[GamePropertyKey.PropsListedInLobby] = propertiesListedInLobby;
  351. bool sent = this.LoadBalancingClient.OpSetPropertiesOfRoom(customProps);
  352. if (sent)
  353. {
  354. this.propertiesListedInLobby = propertiesListedInLobby;
  355. }
  356. }
  357. /// <summary>
  358. /// Removes a player from this room's Players Dictionary.
  359. /// This is internally used by the LoadBalancing API. There is usually no need to remove players yourself.
  360. /// This is not a way to "kick" players.
  361. /// </summary>
  362. protected internal virtual void RemovePlayer(Player player)
  363. {
  364. this.Players.Remove(player.ActorNumber);
  365. player.RoomReference = null;
  366. }
  367. /// <summary>
  368. /// Removes a player from this room's Players Dictionary.
  369. /// </summary>
  370. protected internal virtual void RemovePlayer(int id)
  371. {
  372. this.RemovePlayer(this.GetPlayer(id));
  373. }
  374. /// <summary>
  375. /// Asks the server to assign another player as Master Client of your current room.
  376. /// </summary>
  377. /// <remarks>
  378. /// RaiseEvent has the option to send messages only to the Master Client of a room.
  379. /// SetMasterClient affects which client gets those messages.
  380. ///
  381. /// This method calls an operation on the server to set a new Master Client, which takes a roundtrip.
  382. /// In case of success, this client and the others get the new Master Client from the server.
  383. ///
  384. /// SetMasterClient tells the server which current Master Client should be replaced with the new one.
  385. /// It will fail, if anything switches the Master Client moments earlier. There is no callback for this
  386. /// error. All clients should get the new Master Client assigned by the server anyways.
  387. ///
  388. /// See also: MasterClientId
  389. /// </remarks>
  390. /// <param name="masterClientPlayer">The player to become the next Master Client.</param>
  391. /// <returns>False when this operation couldn't be done currently. Requires a v4 Photon Server.</returns>
  392. public bool SetMasterClient(Player masterClientPlayer)
  393. {
  394. Hashtable newProps = new Hashtable() { { GamePropertyKey.MasterClientId, masterClientPlayer.ActorNumber } };
  395. Hashtable prevProps = new Hashtable() { { GamePropertyKey.MasterClientId, this.MasterClientId } };
  396. return this.LoadBalancingClient.OpSetPropertiesOfRoom(newProps, prevProps);
  397. }
  398. /// <summary>
  399. /// Checks if the player is in the room's list already and calls StorePlayer() if not.
  400. /// </summary>
  401. /// <param name="player">The new player - identified by ID.</param>
  402. /// <returns>False if the player could not be added (cause it was in the list already).</returns>
  403. public virtual bool AddPlayer(Player player)
  404. {
  405. if (!this.Players.ContainsKey(player.ActorNumber))
  406. {
  407. this.StorePlayer(player);
  408. return true;
  409. }
  410. return false;
  411. }
  412. /// <summary>
  413. /// Updates a player reference in the Players dictionary (no matter if it existed before or not).
  414. /// </summary>
  415. /// <param name="player">The Player instance to insert into the room.</param>
  416. public virtual Player StorePlayer(Player player)
  417. {
  418. this.Players[player.ActorNumber] = player;
  419. player.RoomReference = this;
  420. // while initializing the room, the players are not guaranteed to be added in-order
  421. if (this.MasterClientId == 0 || player.ActorNumber < this.MasterClientId)
  422. {
  423. this.masterClientId = player.ActorNumber;
  424. }
  425. return player;
  426. }
  427. /// <summary>
  428. /// Tries to find the player with given actorNumber (a.k.a. ID).
  429. /// Only useful when in a Room, as IDs are only valid per Room.
  430. /// </summary>
  431. /// <param name="id">ID to look for.</param>
  432. /// <returns>The player with the ID or null.</returns>
  433. public virtual Player GetPlayer(int id)
  434. {
  435. Player result = null;
  436. this.Players.TryGetValue(id, out result);
  437. return result;
  438. }
  439. /// <summary>
  440. /// Attempts to remove all current expected users from the server's Slot Reservation list.
  441. /// </summary>
  442. /// <remarks>
  443. /// Note that this operation can conflict with new/other users joining. They might be
  444. /// adding users to the list of expected users before or after this client called ClearExpectedUsers.
  445. ///
  446. /// This room's expectedUsers value will update, when the server sends a successful update.
  447. ///
  448. /// Internals: This methods wraps up setting the ExpectedUsers property of a room.
  449. /// </remarks>
  450. public void ClearExpectedUsers()
  451. {
  452. Hashtable props = new Hashtable();
  453. props[GamePropertyKey.ExpectedUsers] = new string[0];
  454. Hashtable expected = new Hashtable();
  455. expected[GamePropertyKey.ExpectedUsers] = this.ExpectedUsers;
  456. this.LoadBalancingClient.OpSetPropertiesOfRoom(props, expected);
  457. }
  458. /// <summary>Returns a summary of this Room instance as string.</summary>
  459. /// <returns>Summary of this Room instance.</returns>
  460. public override string ToString()
  461. {
  462. return string.Format("Room: '{0}' {1},{2} {4}/{3} players.", this.name, this.isVisible ? "visible" : "hidden", this.isOpen ? "open" : "closed", this.maxPlayers, this.PlayerCount);
  463. }
  464. /// <summary>Returns a summary of this Room instance as longer string, including Custom Properties.</summary>
  465. /// <returns>Summary of this Room instance.</returns>
  466. public new string ToStringFull()
  467. {
  468. return string.Format("Room: '{0}' {1},{2} {4}/{3} players.\ncustomProps: {5}", this.name, this.isVisible ? "visible" : "hidden", this.isOpen ? "open" : "closed", this.maxPlayers, this.PlayerCount, this.CustomProperties.ToStringFull());
  469. }
  470. }
  471. }