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.

629 lines
27 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
  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 cleans up the event cache when a player (actor) leaves.
  242. /// </summary>
  243. /// <remarks>
  244. /// This affects which events joining players get.
  245. ///
  246. /// Set in room creation via RoomOptions.CleanupCacheOnLeave.
  247. ///
  248. /// Within PUN, auto cleanup of events means that cached RPCs and instantiated networked objects are deleted from the room.
  249. /// </remarks>
  250. public bool AutoCleanUp
  251. {
  252. get
  253. {
  254. return this.autoCleanUp;
  255. }
  256. }
  257. /// <summary>Define if the client who calls SetProperties should receive the properties update event or not. </summary>
  258. public bool BroadcastPropertiesChangeToAll { get; private set; }
  259. /// <summary>Define if Join and Leave events should not be sent to clients in the room. </summary>
  260. public bool SuppressRoomEvents { get; private set; }
  261. /// <summary>Extends SuppressRoomEvents: Define if Join and Leave events but also the actors' list and their respective properties should not be sent to clients. </summary>
  262. public bool SuppressPlayerInfo { get; private set; }
  263. /// <summary>Define if UserIds of the players are broadcast in the room. Useful for FindFriends and reserving slots for expected users.</summary>
  264. public bool PublishUserId { get; private set; }
  265. /// <summary>Define if actor or room properties with null values are removed on the server or kept.</summary>
  266. public bool DeleteNullProperties { get; private set; }
  267. #if SERVERSDK
  268. /// <summary>Define if rooms should have unique UserId per actor and that UserIds are used instead of actor number in rejoin.</summary>
  269. public bool CheckUserOnJoin { get; private set; }
  270. #endif
  271. /// <summary>Creates a Room (representation) with given name and properties and the "listing options" as provided by parameters.</summary>
  272. /// <param name="roomName">Name of the room (can be null until it's actually created on server).</param>
  273. /// <param name="options">Room options.</param>
  274. public Room(string roomName, RoomOptions options, bool isOffline = false) : base(roomName, options != null ? options.CustomRoomProperties : null)
  275. {
  276. // base() sets name and (custom)properties. here we set "well known" properties
  277. if (options != null)
  278. {
  279. this.isVisible = options.IsVisible;
  280. this.isOpen = options.IsOpen;
  281. this.maxPlayers = options.MaxPlayers;
  282. this.propertiesListedInLobby = options.CustomRoomPropertiesForLobby;
  283. //this.playerTtl = options.PlayerTtl; // set via well known properties
  284. //this.emptyRoomTtl = options.EmptyRoomTtl; // set via well known properties
  285. }
  286. this.isOffline = isOffline;
  287. }
  288. /// <summary>Read (received) room option flags into related bool parameters.</summary>
  289. /// <remarks>This is for internal use. The operation response for join and create room operations is read this way.</remarks>
  290. /// <param name="roomFlags"></param>
  291. internal void InternalCacheRoomFlags(int roomFlags)
  292. {
  293. this.BroadcastPropertiesChangeToAll = (roomFlags & (int)RoomOptionBit.BroadcastPropsChangeToAll) != 0;
  294. this.SuppressRoomEvents = (roomFlags & (int)RoomOptionBit.SuppressRoomEvents) != 0;
  295. this.SuppressPlayerInfo = (roomFlags & (int)RoomOptionBit.SuppressPlayerInfo) != 0;
  296. this.PublishUserId = (roomFlags & (int)RoomOptionBit.PublishUserId) != 0;
  297. this.DeleteNullProperties = (roomFlags & (int)RoomOptionBit.DeleteNullProps) != 0;
  298. #if SERVERSDK
  299. this.CheckUserOnJoin = (roomFlags & (int)RoomOptionBit.CheckUserOnJoin) != 0;
  300. #endif
  301. this.autoCleanUp = (roomFlags & (int)RoomOptionBit.DeleteCacheOnLeave) != 0;
  302. }
  303. protected internal override void InternalCacheProperties(Hashtable propertiesToCache)
  304. {
  305. int oldMasterId = this.masterClientId;
  306. base.InternalCacheProperties(propertiesToCache); // important: updating the properties fields has no way to do callbacks on change
  307. if (oldMasterId != 0 && this.masterClientId != oldMasterId)
  308. {
  309. this.LoadBalancingClient.InRoomCallbackTargets.OnMasterClientSwitched(this.GetPlayer(this.masterClientId));
  310. }
  311. }
  312. /// <summary>
  313. /// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition.
  314. /// </summary>
  315. /// <remarks>
  316. /// Custom Properties are a set of string keys and arbitrary values which is synchronized
  317. /// for the players in a Room. They are available when the client enters the room, as
  318. /// they are in the response of OpJoin and OpCreate.
  319. ///
  320. /// Custom Properties either relate to the (current) Room or a Player (in that Room).
  321. ///
  322. /// Both classes locally cache the current key/values and make them available as
  323. /// property: CustomProperties. This is provided only to read them.
  324. /// You must use the method SetCustomProperties to set/modify them.
  325. ///
  326. /// Any client can set any Custom Properties anytime (when in a room).
  327. /// It's up to the game logic to organize how they are best used.
  328. ///
  329. /// You should call SetCustomProperties only with key/values that are new or changed. This reduces
  330. /// traffic and performance.
  331. ///
  332. /// Unless you define some expectedProperties, setting key/values is always permitted.
  333. /// In this case, the property-setting client will not receive the new values from the server but
  334. /// instead update its local cache in SetCustomProperties.
  335. ///
  336. /// If you define expectedProperties, the server will skip updates if the server property-cache
  337. /// does not contain all expectedProperties with the same values.
  338. /// In this case, the property-setting client will get an update from the server and update it's
  339. /// cached key/values at about the same time as everyone else.
  340. ///
  341. /// The benefit of using expectedProperties can be only one client successfully sets a key from
  342. /// one known value to another.
  343. /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
  344. /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
  345. /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
  346. /// take the item will have it (and the others fail to set the ownership).
  347. ///
  348. /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
  349. /// </remarks>
  350. /// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
  351. /// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values. Client must be in room.</param>
  352. /// <param name="webFlags">Defines if this SetCustomProperties-operation gets forwarded to your WebHooks. Client must be in room.</param>
  353. /// <returns>
  354. /// False if propertiesToSet is null or empty or have zero string keys.
  355. /// True in offline mode even if expectedProperties or webFlags are used.
  356. /// Otherwise, returns if this operation could be sent to the server.
  357. /// </returns>
  358. public virtual bool SetCustomProperties(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
  359. {
  360. if (propertiesToSet == null || propertiesToSet.Count == 0)
  361. {
  362. return false;
  363. }
  364. Hashtable customProps = propertiesToSet.StripToStringKeys() as Hashtable;
  365. if (this.isOffline)
  366. {
  367. if (customProps.Count == 0)
  368. {
  369. return false;
  370. }
  371. // Merge and delete values.
  372. this.CustomProperties.Merge(customProps);
  373. this.CustomProperties.StripKeysWithNullValues();
  374. // invoking callbacks
  375. this.LoadBalancingClient.InRoomCallbackTargets.OnRoomPropertiesUpdate(propertiesToSet);
  376. }
  377. else
  378. {
  379. // send (sync) these new values if in online room
  380. return this.LoadBalancingClient.OpSetPropertiesOfRoom(customProps, expectedProperties, webFlags);
  381. }
  382. return true;
  383. }
  384. /// <summary>
  385. /// Enables you to define the properties available in the lobby if not all properties are needed to pick a room.
  386. /// </summary>
  387. /// <remarks>
  388. /// Limit the amount of properties sent to users in the lobby to improve speed and stability.
  389. /// </remarks>
  390. /// <param name="lobbyProps">An array of custom room property names to forward to the lobby.</param>
  391. /// <returns>If the operation could be sent to the server.</returns>
  392. public bool SetPropertiesListedInLobby(string[] lobbyProps)
  393. {
  394. if (this.isOffline)
  395. {
  396. return false;
  397. }
  398. Hashtable customProps = new Hashtable();
  399. customProps[GamePropertyKey.PropsListedInLobby] = lobbyProps;
  400. return this.LoadBalancingClient.OpSetPropertiesOfRoom(customProps);
  401. }
  402. /// <summary>
  403. /// Removes a player from this room's Players Dictionary.
  404. /// This is internally used by the LoadBalancing API. There is usually no need to remove players yourself.
  405. /// This is not a way to "kick" players.
  406. /// </summary>
  407. protected internal virtual void RemovePlayer(Player player)
  408. {
  409. this.Players.Remove(player.ActorNumber);
  410. player.RoomReference = null;
  411. }
  412. /// <summary>
  413. /// Removes a player from this room's Players Dictionary.
  414. /// </summary>
  415. protected internal virtual void RemovePlayer(int id)
  416. {
  417. this.RemovePlayer(this.GetPlayer(id));
  418. }
  419. /// <summary>
  420. /// Asks the server to assign another player as Master Client of your current room.
  421. /// </summary>
  422. /// <remarks>
  423. /// RaiseEvent has the option to send messages only to the Master Client of a room.
  424. /// SetMasterClient affects which client gets those messages.
  425. ///
  426. /// This method calls an operation on the server to set a new Master Client, which takes a roundtrip.
  427. /// In case of success, this client and the others get the new Master Client from the server.
  428. ///
  429. /// SetMasterClient tells the server which current Master Client should be replaced with the new one.
  430. /// It will fail, if anything switches the Master Client moments earlier. There is no callback for this
  431. /// error. All clients should get the new Master Client assigned by the server anyways.
  432. ///
  433. /// See also: MasterClientId
  434. /// </remarks>
  435. /// <param name="masterClientPlayer">The player to become the next Master Client.</param>
  436. /// <returns>False when this operation couldn't be done currently. Requires a v4 Photon Server.</returns>
  437. public bool SetMasterClient(Player masterClientPlayer)
  438. {
  439. if (this.isOffline)
  440. {
  441. return false;
  442. }
  443. Hashtable newProps = new Hashtable() { { GamePropertyKey.MasterClientId, masterClientPlayer.ActorNumber } };
  444. Hashtable prevProps = new Hashtable() { { GamePropertyKey.MasterClientId, this.MasterClientId } };
  445. return this.LoadBalancingClient.OpSetPropertiesOfRoom(newProps, prevProps);
  446. }
  447. /// <summary>
  448. /// Checks if the player is in the room's list already and calls StorePlayer() if not.
  449. /// </summary>
  450. /// <param name="player">The new player - identified by ID.</param>
  451. /// <returns>False if the player could not be added (cause it was in the list already).</returns>
  452. public virtual bool AddPlayer(Player player)
  453. {
  454. if (!this.Players.ContainsKey(player.ActorNumber))
  455. {
  456. this.StorePlayer(player);
  457. return true;
  458. }
  459. return false;
  460. }
  461. /// <summary>
  462. /// Updates a player reference in the Players dictionary (no matter if it existed before or not).
  463. /// </summary>
  464. /// <param name="player">The Player instance to insert into the room.</param>
  465. public virtual Player StorePlayer(Player player)
  466. {
  467. this.Players[player.ActorNumber] = player;
  468. player.RoomReference = this;
  469. //// while initializing the room, the players are not guaranteed to be added in-order
  470. //if (this.MasterClientId == 0 || player.ActorNumber < this.MasterClientId)
  471. //{
  472. // this.masterClientId = player.ActorNumber;
  473. //}
  474. return player;
  475. }
  476. /// <summary>
  477. /// Tries to find the player with given actorNumber (a.k.a. ID).
  478. /// Only useful when in a Room, as IDs are only valid per Room.
  479. /// </summary>
  480. /// <param name="id">ID to look for.</param>
  481. /// <param name="findMaster">If true, the Master Client is returned for ID == 0.</param>
  482. /// <returns>The player with the ID or null.</returns>
  483. public virtual Player GetPlayer(int id, bool findMaster = false)
  484. {
  485. int idToFind = (findMaster && id == 0) ? this.MasterClientId : id;
  486. Player result = null;
  487. this.Players.TryGetValue(idToFind, out result);
  488. return result;
  489. }
  490. /// <summary>
  491. /// Attempts to remove all current expected users from the server's Slot Reservation list.
  492. /// </summary>
  493. /// <remarks>
  494. /// Note that this operation can conflict with new/other users joining. They might be
  495. /// adding users to the list of expected users before or after this client called ClearExpectedUsers.
  496. ///
  497. /// This room's expectedUsers value will update, when the server sends a successful update.
  498. ///
  499. /// Internals: This methods wraps up setting the ExpectedUsers property of a room.
  500. /// </remarks>
  501. /// <returns>If the operation could be sent to the server.</returns>
  502. public bool ClearExpectedUsers()
  503. {
  504. if (this.ExpectedUsers == null || this.ExpectedUsers.Length == 0)
  505. {
  506. return false;
  507. }
  508. return this.SetExpectedUsers(new string[0], this.ExpectedUsers);
  509. }
  510. /// <summary>
  511. /// Attempts to update the expected users from the server's Slot Reservation list.
  512. /// </summary>
  513. /// <remarks>
  514. /// Note that this operation can conflict with new/other users joining. They might be
  515. /// adding users to the list of expected users before or after this client called SetExpectedUsers.
  516. ///
  517. /// This room's expectedUsers value will update, when the server sends a successful update.
  518. ///
  519. /// Internals: This methods wraps up setting the ExpectedUsers property of a room.
  520. /// </remarks>
  521. /// <param name="newExpectedUsers">The new array of UserIDs to be reserved in the room.</param>
  522. /// <returns>If the operation could be sent to the server.</returns>
  523. public bool SetExpectedUsers(string[] newExpectedUsers)
  524. {
  525. if (newExpectedUsers == null || newExpectedUsers.Length == 0)
  526. {
  527. this.LoadBalancingClient.DebugReturn(DebugLevel.ERROR, "newExpectedUsers array is null or empty, call Room.ClearExpectedUsers() instead if this is what you want.");
  528. return false;
  529. }
  530. return this.SetExpectedUsers(newExpectedUsers, this.ExpectedUsers);
  531. }
  532. private bool SetExpectedUsers(string[] newExpectedUsers, string[] oldExpectedUsers)
  533. {
  534. if (this.isOffline)
  535. {
  536. return false;
  537. }
  538. Hashtable gameProperties = new Hashtable(1);
  539. gameProperties.Add(GamePropertyKey.ExpectedUsers, newExpectedUsers);
  540. Hashtable expectedProperties = null;
  541. if (oldExpectedUsers != null)
  542. {
  543. expectedProperties = new Hashtable(1);
  544. expectedProperties.Add(GamePropertyKey.ExpectedUsers, oldExpectedUsers);
  545. }
  546. return this.LoadBalancingClient.OpSetPropertiesOfRoom(gameProperties, expectedProperties);
  547. }
  548. /// <summary>Returns a summary of this Room instance as string.</summary>
  549. /// <returns>Summary of this Room instance.</returns>
  550. public override string ToString()
  551. {
  552. return string.Format("Room: '{0}' {1},{2} {4}/{3} players.", this.name, this.isVisible ? "visible" : "hidden", this.isOpen ? "open" : "closed", this.maxPlayers, this.PlayerCount);
  553. }
  554. /// <summary>Returns a summary of this Room instance as longer string, including Custom Properties.</summary>
  555. /// <returns>Summary of this Room instance.</returns>
  556. public new string ToStringFull()
  557. {
  558. 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());
  559. }
  560. }
  561. }