123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 |
- // ----------------------------------------------------------------------------
- // <copyright file="Room.cs" company="Exit Games GmbH">
- // Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH
- // </copyright>
- // <summary>
- // The Room class resembles the properties known about the room in which
- // a game/match happens.
- // </summary>
- // <author>developer@photonengine.com</author>
- // ----------------------------------------------------------------------------
- #if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER
- #define SUPPORTED_UNITY
- #endif
- namespace Photon.Realtime
- {
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using ExitGames.Client.Photon;
- #if SUPPORTED_UNITY || NETFX_CORE
- using Hashtable = ExitGames.Client.Photon.Hashtable;
- using SupportClass = ExitGames.Client.Photon.SupportClass;
- #endif
- /// <summary>
- /// This class represents a room a client joins/joined.
- /// </summary>
- /// <remarks>
- /// Contains a list of current players, their properties and those of this room, too.
- /// A room instance has a number of "well known" properties like IsOpen, MaxPlayers which can be changed.
- /// Your own, custom properties can be set via SetCustomProperties() while being in the room.
- ///
- /// Typically, this class should be extended by a game-specific implementation with logic and extra features.
- /// </remarks>
- public class Room : RoomInfo
- {
- /// <summary>
- /// A reference to the LoadBalancingClient which is currently keeping the connection and state.
- /// </summary>
- public LoadBalancingClient LoadBalancingClient { get; set; }
- /// <summary>The name of a room. Unique identifier (per region and virtual appid) for a room/match.</summary>
- /// <remarks>The name can't be changed once it's set by the server.</remarks>
- public new string Name
- {
- get
- {
- return this.name;
- }
- internal set
- {
- this.name = value;
- }
- }
- private bool isOffline;
- public bool IsOffline
- {
- get
- {
- return isOffline;
- }
- private set
- {
- isOffline = value;
- }
- }
- /// <summary>
- /// Defines if the room can be joined.
- /// </summary>
- /// <remarks>
- /// This does not affect listing in a lobby but joining the room will fail if not open.
- /// If not open, the room is excluded from random matchmaking.
- /// Due to racing conditions, found matches might become closed while users are trying to join.
- /// Simply re-connect to master and find another.
- /// Use property "IsVisible" to not list the room.
- ///
- /// As part of RoomInfo this can't be set.
- /// As part of a Room (which the player joined), the setter will update the server and all clients.
- /// </remarks>
- public new bool IsOpen
- {
- get
- {
- return this.isOpen;
- }
- set
- {
- if (value != this.isOpen)
- {
- if (!this.isOffline)
- {
- this.LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.IsOpen, value } });
- }
- }
- this.isOpen = value;
- }
- }
- /// <summary>
- /// Defines if the room is listed in its lobby.
- /// </summary>
- /// <remarks>
- /// Rooms can be created invisible, or changed to invisible.
- /// To change if a room can be joined, use property: open.
- ///
- /// As part of RoomInfo this can't be set.
- /// As part of a Room (which the player joined), the setter will update the server and all clients.
- /// </remarks>
- public new bool IsVisible
- {
- get
- {
- return this.isVisible;
- }
- set
- {
- if (value != this.isVisible)
- {
- if (!this.isOffline)
- {
- this.LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.IsVisible, value } });
- }
- }
- this.isVisible = value;
- }
- }
- /// <summary>
- /// Sets a limit of players to this room. This property is synced and shown in lobby, too.
- /// If the room is full (players count == maxplayers), joining this room will fail.
- /// </summary>
- /// <remarks>
- /// As part of RoomInfo this can't be set.
- /// As part of a Room (which the player joined), the setter will update the server and all clients.
- /// </remarks>
- public new int MaxPlayers
- {
- get
- {
- return this.maxPlayers;
- }
- set
- {
- if (value != this.maxPlayers)
- {
- byte maxPlayersPropValue = (byte)value; // TODO: when the server accepts int typed MaxPlayers, remove this
- if (!this.isOffline)
- {
- this.LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.MaxPlayers, maxPlayersPropValue } });
- }
- this.maxPlayers = maxPlayersPropValue;
- }
- }
- }
- /// <summary>The count of players in this Room (using this.Players.Count).</summary>
- public new int PlayerCount
- {
- get
- {
- if (this.Players == null)
- {
- return 0;
- }
- return (byte)this.Players.Count;
- }
- }
- /// <summary>While inside a Room, this is the list of players who are also in that room.</summary>
- private Dictionary<int, Player> players = new Dictionary<int, Player>();
- /// <summary>While inside a Room, this is the list of players who are also in that room.</summary>
- public Dictionary<int, Player> Players
- {
- get
- {
- return this.players;
- }
- private set
- {
- this.players = value;
- }
- }
- /// <summary>
- /// 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.
- /// </summary>
- /// <remarks>
- /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
- /// Define expected players in the methods: <see cref="LoadBalancingClient.OpCreateRoom"/>, <see cref="LoadBalancingClient.OpJoinRoom"/> and <see cref="LoadBalancingClient.OpJoinRandomRoom"/>.
- /// </remarks>
- public string[] ExpectedUsers
- {
- get { return this.expectedUsers; }
- }
- /// <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>
- public int PlayerTtl
- {
- get { return this.playerTtl; }
- set
- {
- if (value != this.playerTtl)
- {
- if (!this.isOffline)
- {
- this.LoadBalancingClient.OpSetPropertyOfRoom(GamePropertyKey.PlayerTtl, value); // TODO: implement Offline Mode
- }
- }
- this.playerTtl = value;
- }
- }
- /// <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>
- public int EmptyRoomTtl
- {
- get { return this.emptyRoomTtl; }
- set
- {
- if (value != this.emptyRoomTtl)
- {
- if (!this.isOffline)
- {
- this.LoadBalancingClient.OpSetPropertyOfRoom(GamePropertyKey.EmptyRoomTtl, value); // TODO: implement Offline Mode
- }
- }
- this.emptyRoomTtl = value;
- }
- }
- /// <summary>
- /// The ID (actorNumber, actorNumber) of the player who's the master of this Room.
- /// Note: This changes when the current master leaves the room.
- /// </summary>
- public int MasterClientId { get { return this.masterClientId; } }
- /// <summary>
- /// Gets a list of custom properties that are in the RoomInfo of the Lobby.
- /// This list is defined when creating the room and can't be changed afterwards. Compare: LoadBalancingClient.OpCreateRoom()
- /// </summary>
- /// <remarks>You could name properties that are not set from the beginning. Those will be synced with the lobby when added later on.</remarks>
- public string[] PropertiesListedInLobby
- {
- get
- {
- return this.propertiesListedInLobby;
- }
- private set
- {
- this.propertiesListedInLobby = value;
- }
- }
- /// <summary>
- /// Gets if this room cleans up the event cache when a player (actor) leaves.
- /// </summary>
- /// <remarks>
- /// This affects which events joining players get.
- ///
- /// Set in room creation via RoomOptions.CleanupCacheOnLeave.
- ///
- /// Within PUN, auto cleanup of events means that cached RPCs and instantiated networked objects are deleted from the room.
- /// </remarks>
- public bool AutoCleanUp
- {
- get
- {
- return this.autoCleanUp;
- }
- }
- /// <summary>Define if the client who calls SetProperties should receive the properties update event or not. </summary>
- public bool BroadcastPropertiesChangeToAll { get; private set; }
- /// <summary>Define if Join and Leave events should not be sent to clients in the room. </summary>
- public bool SuppressRoomEvents { get; private set; }
- /// <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>
- public bool SuppressPlayerInfo { get; private set; }
- /// <summary>Define if UserIds of the players are broadcast in the room. Useful for FindFriends and reserving slots for expected users.</summary>
- public bool PublishUserId { get; private set; }
- /// <summary>Define if actor or room properties with null values are removed on the server or kept.</summary>
- public bool DeleteNullProperties { get; private set; }
- #if SERVERSDK
- /// <summary>Define if rooms should have unique UserId per actor and that UserIds are used instead of actor number in rejoin.</summary>
- public bool CheckUserOnJoin { get; private set; }
- #endif
- /// <summary>Creates a Room (representation) with given name and properties and the "listing options" as provided by parameters.</summary>
- /// <param name="roomName">Name of the room (can be null until it's actually created on server).</param>
- /// <param name="options">Room options.</param>
- public Room(string roomName, RoomOptions options, bool isOffline = false) : base(roomName, options != null ? options.CustomRoomProperties : null)
- {
- // base() sets name and (custom)properties. here we set "well known" properties
- if (options != null)
- {
- this.isVisible = options.IsVisible;
- this.isOpen = options.IsOpen;
- this.maxPlayers = options.MaxPlayers;
- this.propertiesListedInLobby = options.CustomRoomPropertiesForLobby;
- //this.playerTtl = options.PlayerTtl; // set via well known properties
- //this.emptyRoomTtl = options.EmptyRoomTtl; // set via well known properties
- }
- this.isOffline = isOffline;
- }
- /// <summary>Read (received) room option flags into related bool parameters.</summary>
- /// <remarks>This is for internal use. The operation response for join and create room operations is read this way.</remarks>
- /// <param name="roomFlags"></param>
- internal void InternalCacheRoomFlags(int roomFlags)
- {
- this.BroadcastPropertiesChangeToAll = (roomFlags & (int)RoomOptionBit.BroadcastPropsChangeToAll) != 0;
- this.SuppressRoomEvents = (roomFlags & (int)RoomOptionBit.SuppressRoomEvents) != 0;
- this.SuppressPlayerInfo = (roomFlags & (int)RoomOptionBit.SuppressPlayerInfo) != 0;
- this.PublishUserId = (roomFlags & (int)RoomOptionBit.PublishUserId) != 0;
- this.DeleteNullProperties = (roomFlags & (int)RoomOptionBit.DeleteNullProps) != 0;
- #if SERVERSDK
- this.CheckUserOnJoin = (roomFlags & (int)RoomOptionBit.CheckUserOnJoin) != 0;
- #endif
- this.autoCleanUp = (roomFlags & (int)RoomOptionBit.DeleteCacheOnLeave) != 0;
- }
- protected internal override void InternalCacheProperties(Hashtable propertiesToCache)
- {
- int oldMasterId = this.masterClientId;
- base.InternalCacheProperties(propertiesToCache); // important: updating the properties fields has no way to do callbacks on change
- if (oldMasterId != 0 && this.masterClientId != oldMasterId)
- {
- this.LoadBalancingClient.InRoomCallbackTargets.OnMasterClientSwitched(this.GetPlayer(this.masterClientId));
- }
- }
- /// <summary>
- /// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition.
- /// </summary>
- /// <remarks>
- /// Custom Properties are a set of string keys and arbitrary values which is synchronized
- /// for the players in a Room. They are available when the client enters the room, as
- /// they are in the response of OpJoin and OpCreate.
- ///
- /// Custom Properties either relate to the (current) Room or a Player (in that Room).
- ///
- /// Both classes locally cache the current key/values and make them available as
- /// property: CustomProperties. This is provided only to read them.
- /// You must use the method SetCustomProperties to set/modify them.
- ///
- /// Any client can set any Custom Properties anytime (when in a room).
- /// It's up to the game logic to organize how they are best used.
- ///
- /// You should call SetCustomProperties only with key/values that are new or changed. This reduces
- /// traffic and performance.
- ///
- /// Unless you define some expectedProperties, setting key/values is always permitted.
- /// In this case, the property-setting client will not receive the new values from the server but
- /// instead update its local cache in SetCustomProperties.
- ///
- /// If you define expectedProperties, the server will skip updates if the server property-cache
- /// does not contain all expectedProperties with the same values.
- /// In this case, the property-setting client will get an update from the server and update it's
- /// cached key/values at about the same time as everyone else.
- ///
- /// The benefit of using expectedProperties can be only one client successfully sets a key from
- /// one known value to another.
- /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
- /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
- /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
- /// take the item will have it (and the others fail to set the ownership).
- ///
- /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
- /// </remarks>
- /// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
- /// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values. Client must be in room.</param>
- /// <param name="webFlags">Defines if this SetCustomProperties-operation gets forwarded to your WebHooks. Client must be in room.</param>
- /// <returns>
- /// False if propertiesToSet is null or empty or have zero string keys.
- /// True in offline mode even if expectedProperties or webFlags are used.
- /// Otherwise, returns if this operation could be sent to the server.
- /// </returns>
- public virtual bool SetCustomProperties(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
- {
- if (propertiesToSet == null || propertiesToSet.Count == 0)
- {
- return false;
- }
- Hashtable customProps = propertiesToSet.StripToStringKeys() as Hashtable;
- if (this.isOffline)
- {
- if (customProps.Count == 0)
- {
- return false;
- }
- // Merge and delete values.
- this.CustomProperties.Merge(customProps);
- this.CustomProperties.StripKeysWithNullValues();
- // invoking callbacks
- this.LoadBalancingClient.InRoomCallbackTargets.OnRoomPropertiesUpdate(propertiesToSet);
- }
- else
- {
- // send (sync) these new values if in online room
- return this.LoadBalancingClient.OpSetPropertiesOfRoom(customProps, expectedProperties, webFlags);
- }
- return true;
- }
- /// <summary>
- /// Enables you to define the properties available in the lobby if not all properties are needed to pick a room.
- /// </summary>
- /// <remarks>
- /// Limit the amount of properties sent to users in the lobby to improve speed and stability.
- /// </remarks>
- /// <param name="lobbyProps">An array of custom room property names to forward to the lobby.</param>
- /// <returns>If the operation could be sent to the server.</returns>
- public bool SetPropertiesListedInLobby(string[] lobbyProps)
- {
- if (this.isOffline)
- {
- return false;
- }
- Hashtable customProps = new Hashtable();
- customProps[GamePropertyKey.PropsListedInLobby] = lobbyProps;
- return this.LoadBalancingClient.OpSetPropertiesOfRoom(customProps);
- }
- /// <summary>
- /// Removes a player from this room's Players Dictionary.
- /// This is internally used by the LoadBalancing API. There is usually no need to remove players yourself.
- /// This is not a way to "kick" players.
- /// </summary>
- protected internal virtual void RemovePlayer(Player player)
- {
- this.Players.Remove(player.ActorNumber);
- player.RoomReference = null;
- }
- /// <summary>
- /// Removes a player from this room's Players Dictionary.
- /// </summary>
- protected internal virtual void RemovePlayer(int id)
- {
- this.RemovePlayer(this.GetPlayer(id));
- }
- /// <summary>
- /// Asks the server to assign another player as Master Client of your current room.
- /// </summary>
- /// <remarks>
- /// RaiseEvent has the option to send messages only to the Master Client of a room.
- /// SetMasterClient affects which client gets those messages.
- ///
- /// This method calls an operation on the server to set a new Master Client, which takes a roundtrip.
- /// In case of success, this client and the others get the new Master Client from the server.
- ///
- /// SetMasterClient tells the server which current Master Client should be replaced with the new one.
- /// It will fail, if anything switches the Master Client moments earlier. There is no callback for this
- /// error. All clients should get the new Master Client assigned by the server anyways.
- ///
- /// See also: MasterClientId
- /// </remarks>
- /// <param name="masterClientPlayer">The player to become the next Master Client.</param>
- /// <returns>False when this operation couldn't be done currently. Requires a v4 Photon Server.</returns>
- public bool SetMasterClient(Player masterClientPlayer)
- {
- if (this.isOffline)
- {
- return false;
- }
- Hashtable newProps = new Hashtable() { { GamePropertyKey.MasterClientId, masterClientPlayer.ActorNumber } };
- Hashtable prevProps = new Hashtable() { { GamePropertyKey.MasterClientId, this.MasterClientId } };
- return this.LoadBalancingClient.OpSetPropertiesOfRoom(newProps, prevProps);
- }
- /// <summary>
- /// Checks if the player is in the room's list already and calls StorePlayer() if not.
- /// </summary>
- /// <param name="player">The new player - identified by ID.</param>
- /// <returns>False if the player could not be added (cause it was in the list already).</returns>
- public virtual bool AddPlayer(Player player)
- {
- if (!this.Players.ContainsKey(player.ActorNumber))
- {
- this.StorePlayer(player);
- return true;
- }
- return false;
- }
- /// <summary>
- /// Updates a player reference in the Players dictionary (no matter if it existed before or not).
- /// </summary>
- /// <param name="player">The Player instance to insert into the room.</param>
- public virtual Player StorePlayer(Player player)
- {
- this.Players[player.ActorNumber] = player;
- player.RoomReference = this;
- //// while initializing the room, the players are not guaranteed to be added in-order
- //if (this.MasterClientId == 0 || player.ActorNumber < this.MasterClientId)
- //{
- // this.masterClientId = player.ActorNumber;
- //}
- return player;
- }
- /// <summary>
- /// Tries to find the player with given actorNumber (a.k.a. ID).
- /// Only useful when in a Room, as IDs are only valid per Room.
- /// </summary>
- /// <param name="id">ID to look for.</param>
- /// <param name="findMaster">If true, the Master Client is returned for ID == 0.</param>
- /// <returns>The player with the ID or null.</returns>
- public virtual Player GetPlayer(int id, bool findMaster = false)
- {
- int idToFind = (findMaster && id == 0) ? this.MasterClientId : id;
- Player result = null;
- this.Players.TryGetValue(idToFind, out result);
- return result;
- }
- /// <summary>
- /// Attempts to remove all current expected users from the server's Slot Reservation list.
- /// </summary>
- /// <remarks>
- /// Note that this operation can conflict with new/other users joining. They might be
- /// adding users to the list of expected users before or after this client called ClearExpectedUsers.
- ///
- /// This room's expectedUsers value will update, when the server sends a successful update.
- ///
- /// Internals: This methods wraps up setting the ExpectedUsers property of a room.
- /// </remarks>
- /// <returns>If the operation could be sent to the server.</returns>
- public bool ClearExpectedUsers()
- {
- if (this.ExpectedUsers == null || this.ExpectedUsers.Length == 0)
- {
- return false;
- }
- return this.SetExpectedUsers(new string[0], this.ExpectedUsers);
- }
- /// <summary>
- /// Attempts to update the expected users from the server's Slot Reservation list.
- /// </summary>
- /// <remarks>
- /// Note that this operation can conflict with new/other users joining. They might be
- /// adding users to the list of expected users before or after this client called SetExpectedUsers.
- ///
- /// This room's expectedUsers value will update, when the server sends a successful update.
- ///
- /// Internals: This methods wraps up setting the ExpectedUsers property of a room.
- /// </remarks>
- /// <param name="newExpectedUsers">The new array of UserIDs to be reserved in the room.</param>
- /// <returns>If the operation could be sent to the server.</returns>
- public bool SetExpectedUsers(string[] newExpectedUsers)
- {
- if (newExpectedUsers == null || newExpectedUsers.Length == 0)
- {
- this.LoadBalancingClient.DebugReturn(DebugLevel.ERROR, "newExpectedUsers array is null or empty, call Room.ClearExpectedUsers() instead if this is what you want.");
- return false;
- }
- return this.SetExpectedUsers(newExpectedUsers, this.ExpectedUsers);
- }
- private bool SetExpectedUsers(string[] newExpectedUsers, string[] oldExpectedUsers)
- {
- if (this.isOffline)
- {
- return false;
- }
- Hashtable gameProperties = new Hashtable(1);
- gameProperties.Add(GamePropertyKey.ExpectedUsers, newExpectedUsers);
- Hashtable expectedProperties = null;
- if (oldExpectedUsers != null)
- {
- expectedProperties = new Hashtable(1);
- expectedProperties.Add(GamePropertyKey.ExpectedUsers, oldExpectedUsers);
- }
- return this.LoadBalancingClient.OpSetPropertiesOfRoom(gameProperties, expectedProperties);
- }
- /// <summary>Returns a summary of this Room instance as string.</summary>
- /// <returns>Summary of this Room instance.</returns>
- public override string ToString()
- {
- return string.Format("Room: '{0}' {1},{2} {4}/{3} players.", this.name, this.isVisible ? "visible" : "hidden", this.isOpen ? "open" : "closed", this.maxPlayers, this.PlayerCount);
- }
- /// <summary>Returns a summary of this Room instance as longer string, including Custom Properties.</summary>
- /// <returns>Summary of this Room instance.</returns>
- public new string ToStringFull()
- {
- 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());
- }
- }
- }
|