// -----------------------------------------------------------------------
//
// Loadbalancing Framework for Photon - Copyright (C) 2011 Exit Games GmbH
//
//
// Provides the operations and a state for games using the
// Photon LoadBalancing server.
//
// developer@photonengine.com
// ----------------------------------------------------------------------------
#if UNITY_4_7_OR_NEWER
#define UNITY
#endif
namespace ExitGames.Client.Photon.LoadBalancing
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
#if UNITY
using UnityEngine;
using Debug = UnityEngine.Debug;
#endif
#if UNITY || NETFX_CORE
using Hashtable = ExitGames.Client.Photon.Hashtable;
using SupportClass = ExitGames.Client.Photon.SupportClass;
#endif
#region Enums
///
/// State values for a client, which handles switching Photon server types, some operations, etc.
///
/// \ingroup publicApi
public enum ClientState
{
/// Peer is created but not used yet.
PeerCreated,
/// Transition state while connecting to a server. On the Photon Cloud this sends the AppId and AuthenticationValues (UserID).
Authenticating,
/// Transition state while connecting to a server. Leads to state ConnectedToMasterserver or JoinedLobby.
Authenticated,
/// The client is in a lobby, connected to the MasterServer. Depending on the lobby, it gets room listings.
JoinedLobby,
/// Transition from MasterServer to GameServer.
DisconnectingFromMasterserver,
/// Transition to GameServer (client authenticates and joins/creates a room).
ConnectingToGameserver,
/// Connected to GameServer (going to auth and join game).
ConnectedToGameserver,
/// Transition state while joining or creating a room on GameServer.
Joining,
/// The client entered a room. The CurrentRoom and Players are known and you can now raise events.
Joined,
/// Transition state when leaving a room.
Leaving,
/// Transition from GameServer to MasterServer (after leaving a room/game).
DisconnectingFromGameserver,
/// Connecting to MasterServer (includes sending authentication values).
ConnectingToMasterserver,
/// The client disconnects (from any server). This leads to state Disconnected.
Disconnecting,
/// The client is no longer connected (to any server). Connect to MasterServer to go on.
Disconnected,
/// Connected to MasterServer. You might use matchmaking or join a lobby now.
ConnectedToMasterserver,
/// Connected to MasterServer. You might use matchmaking or join a lobby now.
[Obsolete("Renamed to ConnectedToMasterserver.")]
ConnectedToMaster = ConnectedToMasterserver,
/// Client connects to the NameServer. This process includes low level connecting and setting up encryption. When done, state becomes ConnectedToNameServer.
ConnectingToNameServer,
/// Client is connected to the NameServer and established enctryption already. You should call OpGetRegions or ConnectToRegionMaster.
ConnectedToNameServer,
/// Clients disconnects (specifically) from the NameServer (usually to connect to the MasterServer).
DisconnectingFromNameServer
}
///
/// Internal state, how this peer gets into a particular room (joining it or creating it).
///
internal enum JoinType
{
/// This client creates a room, gets into it (no need to join) and can set room properties.
CreateRoom,
/// The room existed already and we join into it (not setting room properties).
JoinRoom,
/// Done on Master Server and (if successful) followed by a Join on Game Server.
JoinRandomRoom,
/// Client is either joining or creating a room. On Master- and Game-Server.
JoinOrCreateRoom
}
/// Enumaration of causes for Disconnects (used in LoadBalancingClient.DisconnectedCause).
/// Read the individual descriptions to find out what to do about this type of disconnect.
public enum DisconnectCause
{
/// No error was tracked.
None,
/// OnStatusChanged: The CCUs count of your Photon Server License is exausted (temporarily).
DisconnectByServerUserLimit,
/// OnStatusChanged: The server is not available or the address is wrong. Make sure the port is provided and the server is up.
ExceptionOnConnect,
/// OnStatusChanged: The server disconnected this client. Most likely the server's send buffer is full (receiving too much from other clients).
DisconnectByServer,
/// OnStatusChanged: This client detected that the server's responses are not received in due time. Maybe you send / receive too much?
TimeoutDisconnect,
/// OnStatusChanged: Some internal exception caused the socket code to fail. Contact Exit Games.
Exception,
/// OnOperationResponse: Authenticate in the Photon Cloud with invalid AppId. Update your subscription or contact Exit Games.
InvalidAuthentication,
/// OnOperationResponse: Authenticate (temporarily) failed when using a Photon Cloud subscription without CCU Burst. Update your subscription.
MaxCcuReached,
/// OnOperationResponse: Authenticate when the app's Photon Cloud subscription is locked to some (other) region(s). Update your subscription or master server address.
InvalidRegion,
/// OnOperationResponse: Operation that's (currently) not available for this client (not authorized usually). Only tracked for op Authenticate.
OperationNotAllowedInCurrentState,
/// OnOperationResponse: Authenticate in the Photon Cloud with invalid client values or custom authentication setup in Cloud Dashboard.
CustomAuthenticationFailed,
/// OnStatusChanged: The server disconnected this client from within the room's logic (the C# code).
DisconnectByServerLogic,
/// The authentication ticket should provide access to any Photon Cloud server without doing another authentication-service call. However, the ticket expired.
AuthenticationTicketExpired
}
/// Available server (types) for internally used field: server.
/// Photon uses 3 different roles of servers: Name Server, Master Server and Game Server.
public enum ServerConnection
{
/// This server is where matchmaking gets done and where clients can get lists of rooms in lobbies.
MasterServer,
/// This server handles a number of rooms to execute and relay the messages between players (in a room).
GameServer,
/// This server is used initially to get the address (IP) of a Master Server for a specific region. Not used for Photon OnPremise (self hosted).
NameServer
}
///
/// Defines how the communication gets encrypted.
///
public enum EncryptionMode
{
///
/// This is the default encryption mode: Messages get encrypted only on demand (when you send operations with the "encrypt" parameter set to true).
///
PayloadEncryption,
///
/// With this encryption mode for UDP, the connection gets setup and all further datagrams get encrypted almost entirely. On-demand message encryption (like in PayloadEncryption) is skipped.
///
DatagramEncryption = 10,
}
public static class EncryptionDataParameters
{
///
/// Key for encryption mode
///
public const byte Mode = 0;
///
/// Key for first secret
///
public const byte Secret1 = 1;
///
/// Key for second secret
///
public const byte Secret2 = 2;
}
#endregion
///
/// This class implements the Photon LoadBalancing workflow by using a LoadBalancingPeer.
/// It keeps a state and will automatically execute transitions between the Master and Game Servers.
///
///
/// This class (and the Player class) should be extended to implement your own game logic.
/// You can override CreatePlayer as "factory" method for Players and return your own Player instances.
/// The State of this class is essential to know when a client is in a lobby (or just on the master)
/// and when in a game where the actual gameplay should take place.
/// Extension notes:
/// An extension of this class should override the methods of the IPhotonPeerListener, as they
/// are called when the state changes. Call base.method first, then pick the operation or state you
/// want to react to and put it in a switch-case.
/// We try to provide demo to each platform where this api can be used, so lookout for those.
///
public class LoadBalancingClient : IPhotonPeerListener
{
///
/// The client uses a LoadBalancingPeer as API to communicate with the server.
/// This is public for ease-of-use: Some methods like OpRaiseEvent are not relevant for the connection state and don't need a override.
///
public LoadBalancingPeer loadBalancingPeer;
/// The version of your client. A new version also creates a new "virtual app" to separate players from older client versions.
public string AppVersion { get; set; }
/// The AppID as assigned from the Photon Cloud. If you host yourself, this is the "regular" Photon Server Application Name (most likely: "LoadBalancing").
public string AppId { get; set; }
/// A user's authentication values for authentication in Photon.
/// Set this property or pass AuthenticationValues by Connect(..., authValues).
public AuthenticationValues AuthValues { get; set; }
/// Enables the new Authentication workflow.
public AuthModeOption AuthMode = AuthModeOption.Auth;
/// Defines how the communication gets encrypted.
public EncryptionMode EncryptionMode = EncryptionMode.PayloadEncryption;
/// The protocol which will be used on Master- and Gameserver.
///
/// When using AuthOnceWss, the client uses a wss-connection on the Nameserver but another protocol on the other servers.
/// As the Nameserver sends an address, which is different per protocol, it needs to know the expected protocol.
///
private ConnectionProtocol ExpectedProtocol = ConnectionProtocol.Udp;
/// Exposes the TransportProtocol of the used PhotonPeer. Settable while not connected.
public ConnectionProtocol TransportProtocol
{
get { return this.loadBalancingPeer.TransportProtocol; }
set
{
if (this.loadBalancingPeer == null || this.loadBalancingPeer.PeerState != PeerStateValue.Disconnected)
{
this.DebugReturn(DebugLevel.WARNING, "Can't set TransportProtocol. Disconnect first! " + ((this.loadBalancingPeer != null) ? "PeerState: " + this.loadBalancingPeer.PeerState : "loadBalancingPeer is null."));
return;
}
this.loadBalancingPeer.TransportProtocol = value;
}
}
/// Defines which IPhotonSocket class to use per ConnectionProtocol.
///
/// Several platforms have special Socket implementations and slightly different APIs.
/// To accomodate this, switching the socket implementation for a network protocol was made available.
/// By default, UDP and TCP have socket implementations assigned.
///
/// You only need to set the SocketImplementationConfig once, after creating a PhotonPeer
/// and before connecting. If you switch the TransportProtocol, the correct implementation is being used.
///
public Dictionary SocketImplementationConfig
{
get { return this.loadBalancingPeer.SocketImplementationConfig; }
}
///Simplifies getting the token for connect/init requests, if this feature is enabled.
private string TokenForInit
{
get
{
if (this.AuthMode == AuthModeOption.Auth)
{
return null;
}
return (this.AuthValues != null) ? this.AuthValues.Token : null;
}
}
/// True if this client uses a NameServer to get the Master Server address.
public bool IsUsingNameServer { get; protected internal set; }
/// Name Server Host Name for Photon Cloud. Without port and without any prefix.
public string NameServerHost = "ns.exitgames.com";
/// Name Server for HTTP connections to the Photon Cloud. Includes prefix and port.
public string NameServerHttp = "http://ns.exitgames.com:80/photon/n";
/// Name Server port per protocol (the UDP port is different than TCP, etc).
private static readonly Dictionary ProtocolToNameServerPort = new Dictionary() { { ConnectionProtocol.Udp, 5058 }, { ConnectionProtocol.Tcp, 4533 }, { ConnectionProtocol.WebSocket, 9093 }, { ConnectionProtocol.WebSocketSecure, 19093 } }; //, { ConnectionProtocol.RHttp, 6063 } };
/// Name Server Address for Photon Cloud (based on current protocol). You can use the default values and usually won't have to set this value.
public string NameServerAddress { get { return this.GetNameServerAddress(); } }
/// The currently used server address (if any). The type of server is define by Server property.
public string CurrentServerAddress { get { return this.loadBalancingPeer.ServerAddress; } }
/// Your Master Server address. In PhotonCloud, call ConnectToRegionMaster() to find your Master Server.
///
/// In the Photon Cloud, explicit definition of a Master Server Address is not best practice.
/// The Photon Cloud has a "Name Server" which redirects clients to a specific Master Server (per Region and AppId).
///
public string MasterServerAddress { get; protected internal set; }
/// The game server's address for a particular room. In use temporarily, as assigned by master.
public string GameServerAddress { get; protected internal set; }
/// The server this client is currently connected or connecting to.
///
/// Each server (NameServer, MasterServer, GameServer) allow some operations and reject others.
///
public ServerConnection Server { get; private set; }
/// Backing field for property.
private ClientState state = ClientState.PeerCreated;
/// Current state this client is in. Careful: several states are "transitions" that lead to other states.
public ClientState State
{
get
{
return this.state;
}
protected internal set
{
this.state = value;
if (OnStateChangeAction != null) OnStateChangeAction(this.state);
}
}
/// Returns if this client is currently connected or connecting to some type of server.
/// This is even true while switching servers. Use IsConnectedAndReady to check only for those states that enable you to send Operations.
public bool IsConnected { get { return this.loadBalancingPeer != null && this.State != ClientState.PeerCreated && this.State != ClientState.Disconnected; } }
///
/// A refined version of IsConnected which is true only if your connection to the server is ready to accept operations.
///
///
/// Which operations are available, depends on the Server. For example, the NameServer allows OpGetRegions which is not available anywhere else.
/// The MasterServer does not allow you to send events (OpRaiseEvent) and on the GameServer you are unable to join a lobby (OpJoinLobby).
/// Check which server you are on with PhotonNetwork.Server.
///
public bool IsConnectedAndReady
{
get
{
if (this.loadBalancingPeer == null)
{
return false;
}
switch (this.State)
{
case ClientState.PeerCreated:
case ClientState.Disconnected:
case ClientState.Disconnecting:
case ClientState.Authenticating:
case ClientState.ConnectingToGameserver:
case ClientState.ConnectingToMasterserver:
case ClientState.ConnectingToNameServer:
case ClientState.Joining:
case ClientState.Leaving:
return false; // we are not ready to execute any operations
}
return true;
}
}
/// Register a method to be called when this client's ClientState gets set.
/// This can be useful to react to being connected, joined into a room, etc.
public event Action OnStateChangeAction;
/// Register a method to be called when an event got dispatched. Gets called at the end of OnEvent().
///
/// This is an alternative to extending LoadBalancingClient to override OnEvent().
///
/// Note that OnEvent is executing before your Action is called.
/// That means for example: Joining players will already be in the player list but leaving
/// players will already be removed from the room.
///
public event Action OnEventAction;
/// Register a method to be called when this client's ClientState gets set.
///
/// This is an alternative to extending LoadBalancingClient to override OnOperationResponse().
///
/// Note that OnOperationResponse gets executed before your Action is called.
/// That means for example: The OpJoinLobby response already set the state to "JoinedLobby"
/// and the response to OpLeave already triggered the Disconnect before this is called.
///
public event Action OnOpResponseAction;
/// Summarizes (aggregates) the different causes for disconnects of a client.
///
/// A disconnect can be caused by: errors in the network connection or some vital operation failing
/// (which is considered "high level"). While operations always trigger a call to OnOperationResponse,
/// connection related changes are treated in OnStatusChanged.
/// The DisconnectCause is set in either case and summarizes the causes for any disconnect in a single
/// state value which can be used to display (or debug) the cause for disconnection.
///
public DisconnectCause DisconnectedCause { get; protected set; }
/// Internal value if the client is in a lobby.
/// This is used to re-set this.State, when joining/creating a room fails.
private bool inLobby;
/// The lobby this client currently uses.
public TypedLobby CurrentLobby { get; protected internal set; }
/// Backing field for property.
private bool autoJoinLobby = true;
/// If your client should join random games, you can skip joining the lobby. Call OpJoinRandomRoom and create a room if that fails.
public bool AutoJoinLobby
{
get
{
return this.autoJoinLobby;
}
set
{
this.autoJoinLobby = value;
}
}
///
/// If set to true, the Master Server will report the list of used lobbies to the client. This sets and updates LobbyStatistics.
///
///
/// Lobby Statistics can be useful if a game uses multiple lobbies and you want
/// to show activity of each to players.
///
/// LobbyStatistics are updated when you connect to the Master Server.
///
public bool EnableLobbyStatistics;
/// Internal lobby stats cache, used by LobbyStatistics.
private List lobbyStatistics = new List();
///
/// If RequestLobbyStatistics is true, this provides a list of used lobbies (their name, type, room- and player-count) of this application, while on the Master Server.
///
///
/// If turned on, the Master Server will provide information about active lobbies for this application.
///
/// Lobby Statistics can be useful if a game uses multiple lobbies and you want
/// to show activity of each to players. Per lobby, you get: name, type, room- and player-count.
///
/// Lobby Statistics are not turned on by default.
/// Enable them by setting RequestLobbyStatistics to true before you connect.
///
/// LobbyStatistics are updated when you connect to the Master Server.
/// You can check in OnEvent if EventCode.LobbyStats arrived. This the updates.
///
public List LobbyStatistics
{
get { return this.lobbyStatistics; }
private set { this.lobbyStatistics = value; }
}
/// The local player is never null but not valid unless the client is in a room, too. The ID will be -1 outside of rooms.
public Player LocalPlayer { get; internal set; }
///
/// The nickname of the player (synced with others). Same as client.LocalPlayer.NickName.
///
public string NickName
{
get
{
return this.LocalPlayer.NickName;
}
set
{
if (this.LocalPlayer == null)
{
return;
}
this.LocalPlayer.NickName = value;
}
}
/// An ID for this user. Sent in OpAuthenticate when you connect. If not set, the PlayerName is applied during connect.
///
/// On connect, if the UserId is null or empty, the client will copy the PlayName to UserId. If PlayerName is not set either
/// (before connect), the server applies a temporary ID which stays unknown to this client and other clients.
///
/// The UserId is what's used in FindFriends and for fetching data for your account (with WebHooks e.g.).
///
/// By convention, set this ID before you connect, not while being connected.
/// There is no error but the ID won't change while being connected.
///
public string UserId {
get
{
if (this.AuthValues != null)
{
return this.AuthValues.UserId;
}
return null;
}
set
{
if (this.AuthValues == null)
{
this.AuthValues = new AuthenticationValues();
}
this.AuthValues.UserId = value;
}
}
/// This "list" is populated while being in the lobby of the Master. It contains RoomInfo per roomName (keys).
public Dictionary RoomInfoList = new Dictionary();
/// The current room this client is connected to (null if none available).
public Room CurrentRoom;
/// Statistic value available on master server: Players on master (looking for games).
public int PlayersOnMasterCount { get; internal set; }
/// Statistic value available on master server: Players in rooms (playing).
public int PlayersInRoomsCount { get; internal set; }
/// Statistic value available on master server: Rooms currently created.
public int RoomsCount { get; internal set; }
/// Internally used to decide if a room must be created or joined on game server.
private JoinType lastJoinType;
protected internal EnterRoomParams enterRoomParamsCache;
/// Internally used to trigger OpAuthenticate when encryption was established after a connect.
private bool didAuthenticate;
///
/// List of friends, their online status and the room they are in. Null until initialized by OpFindFriends response.
///
///
/// Do not modify this list! It's internally handled by OpFindFriends and meant as read-only.
/// The value of FriendListAge gives you a hint how old the data is. Don't get this list more often than useful (> 10 seconds).
/// In best case, keep the list you fetch really short. You could (e.g.) get the full list only once, then request a few updates
/// only for friends who are online. After a while (e.g. 1 minute), you can get the full list again.
///
public List FriendList { get; private set; }
/// Contains the list of names of friends to look up their state on the server.
private string[] friendListRequested;
///
/// Age of friend list info (in milliseconds). It's 0 until a friend list is fetched.
///
public int FriendListAge { get { return (this.isFetchingFriendList || this.friendListTimestamp == 0) ? 0 : Environment.TickCount - this.friendListTimestamp; } }
/// Private timestamp (in ms) of the last friendlist update.
private int friendListTimestamp;
/// Internal flag to know if the client currently fetches a friend list.
private bool isFetchingFriendList;
/// Internally used to check if a "Secret" is available to use. Sent by Photon Cloud servers, it simplifies authentication when switching servers.
protected bool IsAuthorizeSecretAvailable
{
get
{
return this.AuthValues != null && !string.IsNullOrEmpty(this.AuthValues.Token);
}
}
/// A list of region names for the Photon Cloud. Set by the result of OpGetRegions().
/// Put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available.
public string[] AvailableRegions { get; private set; }
/// A list of region server (IP addresses with port) for the Photon Cloud. Set by the result of OpGetRegions().
/// Put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available.
public string[] AvailableRegionsServers { get; private set; }
/// The cloud region this client connects to. Set by ConnectToRegionMaster(). Not set if you don't use a NameServer!
public string CloudRegion { get; private set; }
/// Creates a LoadBalancingClient with UDP protocol or the one specified.
/// Specifies the network protocol to use for connections.
public LoadBalancingClient(ConnectionProtocol protocol = ConnectionProtocol.Udp)
{
this.loadBalancingPeer = new LoadBalancingPeer(this, protocol);
this.LocalPlayer = this.CreatePlayer(string.Empty, -1, true, null);
this.State = ClientState.PeerCreated;
}
/// Creates a LoadBalancingClient, setting various values needed before connecting.
/// The Master Server's address to connect to. Used in Connect.
/// The AppId of this title. Needed for the Photon Cloud. Find it in the Dashboard.
/// A version for this client/build. In the Photon Cloud, players are separated by AppId, GameVersion and Region.
/// Specifies the network protocol to use for connections.
public LoadBalancingClient(string masterAddress, string appId, string gameVersion, ConnectionProtocol protocol = ConnectionProtocol.Udp) : this(protocol)
{
this.MasterServerAddress = masterAddress;
this.AppId = appId;
this.AppVersion = gameVersion;
}
///
/// Gets the NameServer Address (with prefix and port), based on the set protocol (this.loadBalancingPeer.UsedProtocol).
///
/// NameServer Address (with prefix and port).
private string GetNameServerAddress()
{
var protocolPort = 0;
ProtocolToNameServerPort.TryGetValue(this.loadBalancingPeer.TransportProtocol, out protocolPort);
switch (this.loadBalancingPeer.TransportProtocol)
{
case ConnectionProtocol.Udp:
case ConnectionProtocol.Tcp:
return string.Format("{0}:{1}", NameServerHost, protocolPort);
#if RHTTP
case ConnectionProtocol.RHttp:
return NameServerHttp;
#endif
case ConnectionProtocol.WebSocket:
return string.Format("ws://{0}:{1}", NameServerHost, protocolPort);
case ConnectionProtocol.WebSocketSecure:
return string.Format("wss://{0}:{1}", NameServerHost, protocolPort);
default:
throw new ArgumentOutOfRangeException();
}
}
#region Operations and Commands
///
/// Starts the "process" to connect to the master server. Relevant connection-values parameters can be set via parameters.
///
///
/// The process to connect includes several steps: the actual connecting, establishing encryption, authentification
/// (of app and optionally the user) and joining a lobby (if AutoJoinLobby is true).
///
/// Instead of providing all these parameters, you can also set the individual properties of a client before calling Connect().
///
/// Users can connect either anonymously or use "Custom Authentication" to verify each individual player's login.
/// Custom Authentication in Photon uses external services and communities to verify users. While the client provides a user's info,
/// the service setup is done in the Photon Cloud Dashboard.
/// The parameter authValues will set this.AuthValues and use them in the connect process.
///
/// To connect to the Photon Cloud, a valid AppId must be provided. This is shown in the Photon Cloud Dashboard.
/// https://cloud.photonengine.com/dashboard
/// Connecting to the Photon Cloud might fail due to:
/// - Network issues (OnStatusChanged() StatusCode.ExceptionOnConnect)
/// - Region not available (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.InvalidRegion)
/// - Subscription CCU limit reached (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.MaxCcuReached)
/// More about the connection limitations:
/// http://doc.photonengine.com/photon-cloud/SubscriptionErrorCases/#cat-references
///
/// Set a master server address instead of using the default. Uses default if null or empty.
/// Your application's name or the AppID assigned by Photon Cloud (as listed in Dashboard). Uses default if null or empty.
/// Can be used to separate users by their client's version (useful to add features without breaking older clients). Uses default if null or empty.
/// Optional name for this player.
/// Authentication values for this user. Optional. If you provide a unique userID it is used for FindFriends.
/// If the operation could be send (can be false for bad server urls).
public bool Connect(string masterServerAddress, string appId, string appVersion, string nickName, AuthenticationValues authValues)
{
if (!string.IsNullOrEmpty(masterServerAddress))
{
this.MasterServerAddress = masterServerAddress;
}
if (!string.IsNullOrEmpty(appId))
{
this.AppId = appId;
}
if (!string.IsNullOrEmpty(appVersion))
{
this.AppVersion = appVersion;
}
if (!string.IsNullOrEmpty(nickName))
{
this.NickName = nickName;
}
this.AuthValues = authValues;
// as this.Connect() checks usage of WebSockets for WebGL exports, this method doesn't
return this.Connect();
}
///
/// Starts the "process" to connect to a Master Server, using MasterServerAddress and AppId properties.
///
///
/// To connect to the Photon Cloud, use ConnectToRegionMaster().
///
/// The process to connect includes several steps: the actual connecting, establishing encryption, authentification
/// (of app and optionally the user) and joining a lobby (if AutoJoinLobby is true).
///
/// Users can connect either anonymously or use "Custom Authentication" to verify each individual player's login.
/// Custom Authentication in Photon uses external services and communities to verify users. While the client provides a user's info,
/// the service setup is done in the Photon Cloud Dashboard.
/// The parameter authValues will set this.AuthValues and use them in the connect process.
///
/// Connecting to the Photon Cloud might fail due to:
/// - Network issues (OnStatusChanged() StatusCode.ExceptionOnConnect)
/// - Region not available (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.InvalidRegion)
/// - Subscription CCU limit reached (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.MaxCcuReached)
/// More about the connection limitations:
/// http://doc.photonengine.com/photon-cloud/SubscriptionErrorCases/#cat-references
///
public virtual bool Connect()
{
this.DisconnectedCause = DisconnectCause.None;
#if UNITY_WEBGL
if (this.TransportProtocol == ConnectionProtocol.Tcp || this.TransportProtocol == ConnectionProtocol.Udp)
{
this.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
this.TransportProtocol = ConnectionProtocol.WebSocketSecure;
}
#endif
if (this.loadBalancingPeer.Connect(this.MasterServerAddress, this.AppId, this.TokenForInit))
{
this.State = ClientState.ConnectingToMasterserver;
return true;
}
return false;
}
///
/// Connects to the NameServer for Photon Cloud, where a region and server list can be obtained.
///
///
/// If the workflow was started or failed right away.
public bool ConnectToNameServer()
{
this.IsUsingNameServer = true;
this.CloudRegion = null;
if (this.AuthMode == AuthModeOption.AuthOnceWss)
{
this.ExpectedProtocol = this.loadBalancingPeer.TransportProtocol;
this.loadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
}
if (!this.loadBalancingPeer.Connect(NameServerAddress, "NameServer", this.TokenForInit))
{
return false;
}
this.State = ClientState.ConnectingToNameServer;
return true;
}
///
/// Connects you to a specific region's Master Server, using the Name Server to find the IP.
///
/// If the operation could be sent. If false, no operation was sent.
public bool ConnectToRegionMaster(string region)
{
this.IsUsingNameServer = true;
if (this.State == ClientState.ConnectedToNameServer)
{
this.CloudRegion = region;
return this.CallAuthenticate();
}
this.loadBalancingPeer.Disconnect();
this.CloudRegion = region;
if (this.AuthMode == AuthModeOption.AuthOnceWss)
{
this.ExpectedProtocol = this.loadBalancingPeer.TransportProtocol;
this.loadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
}
if (!this.loadBalancingPeer.Connect(this.NameServerAddress, "NameServer", null))
{
return false;
}
this.State = ClientState.ConnectingToNameServer;
return true;
}
/// Disconnects this client from any server and sets this.State if the connection is successfuly closed.
public void Disconnect()
{
if (this.State != ClientState.Disconnected)
{
this.State = ClientState.Disconnecting;
this.loadBalancingPeer.Disconnect();
//// we can set this high-level state if the low-level (connection)state is "disconnected"
//if (this.loadBalancingPeer.PeerState == PeerStateValue.Disconnected || this.loadBalancingPeer.PeerState == PeerStateValue.InitializingApplication)
//{
// this.State = ClientState.Disconnected;
//}
}
}
private bool CallAuthenticate()
{
if (this.AuthMode == AuthModeOption.Auth)
{
return this.loadBalancingPeer.OpAuthenticate(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, (this.EnableLobbyStatistics && this.Server == ServerConnection.MasterServer));
}
else
{
return this.loadBalancingPeer.OpAuthenticateOnce(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, this.EncryptionMode, this.ExpectedProtocol);
}
}
///
/// This method dispatches all available incoming commands and then sends this client's outgoing commands.
/// It uses DispatchIncomingCommands and SendOutgoingCommands to do that.
///
///
/// The Photon client libraries are designed to fit easily into a game or application. The application
/// is in control of the context (thread) in which incoming events and responses are executed and has
/// full control of the creation of UDP/TCP packages.
///
/// Sending packages and dispatching received messages are two separate tasks. Service combines them
/// into one method at the cost of control. It calls DispatchIncomingCommands and SendOutgoingCommands.
///
/// Call this method regularly (2..20 times a second).
///
/// This will Dispatch ANY received commands (unless a reliable command in-order is still missing) and
/// events AND will send queued outgoing commands. Fewer calls might be more effective if a device
/// cannot send many packets per second, as multiple operations might be combined into one package.
///
///
/// You could replace Service by:
///
/// while (DispatchIncomingCommands()); //Dispatch until everything is Dispatched...
/// SendOutgoingCommands(); //Send a UDP/TCP package with outgoing messages
///
///
///
public void Service()
{
if (this.loadBalancingPeer != null)
{
this.loadBalancingPeer.Service();
}
}
///
/// Private Disconnect variant that sets the state, too.
///
private void DisconnectToReconnect()
{
switch (this.Server)
{
case ServerConnection.NameServer:
this.State = ClientState.DisconnectingFromNameServer;
break;
case ServerConnection.MasterServer:
this.State = ClientState.DisconnectingFromMasterserver;
break;
case ServerConnection.GameServer:
this.State = ClientState.DisconnectingFromGameserver;
break;
}
this.loadBalancingPeer.Disconnect();
}
///
/// Privately used only.
/// Starts the "process" to connect to the game server (connect before a game is joined).
///
private bool ConnectToGameServer()
{
if (this.loadBalancingPeer.Connect(this.GameServerAddress, this.AppId, this.TokenForInit))
{
this.State = ClientState.ConnectingToGameserver;
return true;
}
// TODO: handle error "cant connect to GS"
return false;
}
///
/// While on the NameServer, this gets you the list of regional servers (short names and their IPs to ping them).
///
/// If the operation could be sent. If false, no operation was sent (e.g. while not connected to the NameServer).
public bool OpGetRegions()
{
if (this.Server != ServerConnection.NameServer)
{
return false;
}
bool sent = this.loadBalancingPeer.OpGetRegions(this.AppId);
if (sent)
{
this.AvailableRegions = null;
}
return sent;
}
///
/// Request the rooms and online status for a list of friends. All clients should set a unique UserId before connecting. The result is available in this.FriendList.
///
///
/// Used on Master Server to find the rooms played by a selected list of users.
/// The result will be stored in LoadBalancingClient.FriendList, which is null before the first server response.
///
/// Users identify themselves by setting a UserId in the LoadBalancingClient instance.
/// This will send the ID in OpAuthenticate during connect (to master and game servers).
/// Note: Changing a player's name doesn't make sense when using a friend list.
///
/// The list of usernames must be fetched from some other source (not provided by Photon).
///
///
/// Internal:
/// The server response includes 2 arrays of info (each index matching a friend from the request):
/// ParameterCode.FindFriendsResponseOnlineList = bool[] of online states
/// ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room)
///
/// Array of friend's names (make sure they are unique).
/// If the operation could be sent (requires connection).
public bool OpFindFriends(string[] friendsToFind)
{
if (this.loadBalancingPeer == null)
{
return false;
}
if (this.isFetchingFriendList || this.Server != ServerConnection.MasterServer)
{
return false; // fetching friends currently, so don't do it again (avoid changing the list while fetching friends)
}
this.isFetchingFriendList = true;
this.friendListRequested = friendsToFind;
return this.loadBalancingPeer.OpFindFriends(friendsToFind);
}
///
/// Joins the lobby on the Master Server, where you get a list of RoomInfos of currently open rooms.
/// This is an async request which triggers a OnOperationResponse() call.
///
/// The lobby join to. Use null for default lobby.
/// If the operation could be sent (has to be connected).
public bool OpJoinLobby(TypedLobby lobby)
{
if (lobby == null)
{
lobby = TypedLobby.Default;
}
bool sent = this.loadBalancingPeer.OpJoinLobby(lobby);
if (sent)
{
this.CurrentLobby = lobby;
}
return sent;
}
/// Opposite of joining a lobby. You don't have to explicitly leave a lobby to join another (client can be in one max, at any time).
/// If the operation could be sent (has to be connected).
public bool OpLeaveLobby()
{
return this.loadBalancingPeer.OpLeaveLobby();
}
/// Operation to join a random room if available. You can use room properties to filter accepted rooms.
///
/// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms.
/// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties.
/// You need to define which Custom Room Properties will be available for matchmaking when you create a room.
/// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby)
///
/// This operation fails if no rooms are fitting or available (all full, closed or not visible).
/// Override this class and implement OnOperationResponse(OperationResponse operationResponse).
///
/// OpJoinRandomRoom can only be called while the client is connected to a Master Server.
/// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method.
/// Alternatively, check the returned bool value.
///
/// While the server is looking for a game, the State will be Joining. It's set immediately when this method sent the Operation.
///
/// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically
/// to switch servers and join the room. When you're in the room, this client's State will become
/// ClientState.Joined (both, for joining or creating it).
/// Set a OnStateChangeAction method to check for states.
///
/// When joining a room, this client's Player Custom Properties will be sent to the room.
/// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
/// Note that the player properties will be cached locally and sent to any next room you would join, too.
///
/// More about matchmaking:
/// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby
///
/// You can define an array of expectedUsers, to block player slots in the room for these users.
/// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
///
/// Optional. A room will only be joined, if it matches these custom properties (with string keys).
/// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value.
/// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.
/// If the operation could be sent currently (requires connection to Master Server).
public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, string[] expectedUsers = null)
{
return OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, MatchmakingMode.FillRoom, TypedLobby.Default, null, expectedUsers);
}
/// Operation to join a random room if available. You can use room properties to filter accepted rooms.
///
/// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms.
/// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties.
/// You need to define which Custom Room Properties will be available for matchmaking when you create a room.
/// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby)
///
/// This operation fails if no rooms are fitting or available (all full, closed or not visible).
/// Override this class and implement OnOperationResponse(OperationResponse operationResponse).
///
/// OpJoinRandomRoom can only be called while the client is connected to a Master Server.
/// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method.
/// Alternatively, check the returned bool value.
///
/// While the server is looking for a game, the State will be Joining. It's set immediately when this method sent the Operation.
///
/// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically
/// to switch servers and join the room. When you're in the room, this client's State will become
/// ClientState.Joined (both, for joining or creating it).
/// Set a OnStateChangeAction method to check for states.
///
/// When joining a room, this client's Player Custom Properties will be sent to the room.
/// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
/// Note that the player properties will be cached locally and sent to any next room you would join, too.
///
/// More about matchmaking:
/// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby
///
/// Optional. A room will only be joined, if it matches these custom properties (with string keys).
/// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value.
/// Selects one of the available matchmaking algorithms. See MatchmakingMode enum for options.
/// If the operation could be sent currently (requires connection to Master Server).
public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, MatchmakingMode matchmakingMode)
{
return this.OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, matchmakingMode, TypedLobby.Default, null);
}
/// Operation to join a random room if available. You can use room properties to filter accepted rooms.
///
/// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms.
/// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties.
/// You need to define which Custom Room Properties will be available for matchmaking when you create a room.
/// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby)
///
/// This operation fails if no rooms are fitting or available (all full, closed or not visible).
/// Override this class and implement OnOperationResponse(OperationResponse operationResponse).
///
/// OpJoinRandomRoom can only be called while the client is connected to a Master Server.
/// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method.
/// Alternatively, check the returned bool value.
///
/// While the server is looking for a game, the State will be Joining.
/// It's set immediately when this method sent the Operation.
///
/// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically
/// to switch servers and join the room. When you're in the room, this client's State will become
/// ClientState.Joined (both, for joining or creating it).
/// Set a OnStateChangeAction method to check for states.
///
/// When joining a room, this client's Player Custom Properties will be sent to the room.
/// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
/// Note that the player properties will be cached locally and sent to any next room you would join, too.
///
/// The parameter lobby can be null (using the defaul lobby) or a typed lobby you make up.
/// Lobbies are created on the fly, as required by the clients. If you organize matchmaking with lobbies,
/// keep in mind that they also fragment your matchmaking. Using more lobbies will put less rooms in each.
///
/// The parameter sqlLobbyFilter can only be combined with the LobbyType.SqlLobby. In that case, it's used
/// to define a sql-like "WHERE" clause for filtering rooms. This is useful for skill-based matchmaking e.g..
///
/// More about matchmaking:
/// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby
///
/// You can define an array of expectedUsers, to block player slots in the room for these users.
/// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
///
/// Optional. A room will only be joined, if it matches these custom properties (with string keys).
/// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value.
/// Selects one of the available matchmaking algorithms. See MatchmakingMode enum for options.
/// The lobby in which to find a room. Use null for default lobby.
/// Can be used with LobbyType.SqlLobby only. This is a "where" clause of a sql statement. Use null for random game.
/// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.
/// If the operation could be sent currently (requires connection to Master Server).
public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, MatchmakingMode matchmakingMode, TypedLobby lobby, string sqlLobbyFilter, string[] expectedUsers = null)
{
if (lobby == null)
{
lobby = TypedLobby.Default;
}
this.State = ClientState.Joining;
this.lastJoinType = JoinType.JoinRandomRoom;
this.CurrentLobby = lobby;
this.enterRoomParamsCache = new EnterRoomParams();
this.enterRoomParamsCache.Lobby = lobby;
this.enterRoomParamsCache.ExpectedUsers = expectedUsers;
OpJoinRandomRoomParams opParams = new OpJoinRandomRoomParams();
opParams.ExpectedCustomRoomProperties = expectedCustomRoomProperties;
opParams.ExpectedMaxPlayers = expectedMaxPlayers;
opParams.MatchingType = matchmakingMode;
opParams.TypedLobby = lobby;
opParams.SqlLobbyFilter = sqlLobbyFilter;
opParams.ExpectedUsers = expectedUsers;
return this.loadBalancingPeer.OpJoinRandomRoom(opParams);
//return this.loadBalancingPeer.OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, playerPropsToSend, matchmakingMode, lobby, sqlLobbyFilter);
}
///
/// Joins a room by roomName. Useful when using room lists in lobbies or when you know the name otherwise.
///
///
/// This method is useful when you are using a lobby to list rooms and know their names.
/// A room's name has to be unique (per region and game version), so it does not matter which lobby it's in.
///
/// If the room is full, closed or not existing, this will fail. Override this class and implement
/// OnOperationResponse(OperationResponse operationResponse) to get the errors.
///
/// OpJoinRoom can only be called while the client is connected to a Master Server.
/// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method.
/// Alternatively, check the returned bool value.
///
/// While the server is joining the game, the State will be ClientState.Joining.
/// It's set immediately when this method sends the Operation.
///
/// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically
/// to switch servers and join the room. When you're in the room, this client's State will become
/// ClientState.Joined (both, for joining or creating it).
/// Set a OnStateChangeAction method to check for states.
///
/// When joining a room, this client's Player Custom Properties will be sent to the room.
/// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
/// Note that the player properties will be cached locally and sent to any next room you would join, too.
///
/// It's usually better to use OpJoinOrCreateRoom for invitations.
/// Then it does not matter if the room is already setup.
///
/// You can define an array of expectedUsers, to block player slots in the room for these users.
/// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
///
/// The name of the room to join. Must be existing already, open and non-full or can't be joined.
/// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.
/// If the operation could be sent currently (requires connection to Master Server).
public bool OpJoinRoom(string roomName, string[] expectedUsers = null)
{
this.State = ClientState.Joining;
this.lastJoinType = JoinType.JoinRoom;
bool onGameServer = this.Server == ServerConnection.GameServer;
EnterRoomParams opParams = new EnterRoomParams();
this.enterRoomParamsCache = opParams;
opParams.RoomName = roomName;
opParams.OnGameServer = onGameServer;
opParams.ExpectedUsers = expectedUsers;
return this.loadBalancingPeer.OpJoinRoom(opParams);
}
///
/// Rejoins a room by roomName (using the userID internally to return). Useful to return to a persisted room or after temporarily losing connection.
///
public bool OpReJoinRoom(string roomName)
{
this.State = ClientState.Joining;
this.lastJoinType = JoinType.JoinRoom;
bool onGameServer = this.Server == ServerConnection.GameServer;
EnterRoomParams opParams = new EnterRoomParams();
this.enterRoomParamsCache = opParams;
opParams.RoomName = roomName;
opParams.OnGameServer = onGameServer;
opParams.RejoinOnly = true;
return this.loadBalancingPeer.OpJoinRoom(opParams);
}
///
/// Joins a specific room by name. If the room does not exist (yet), it will be created implicitly.
///
///
/// Unlike OpJoinRoom, this operation does not fail if the room does not exist.
/// This can be useful when you send invitations to a room before actually creating it:
/// Any invited player (whoever is first) can call this and on demand, the room gets created implicitly.
///
/// This operation does not allow you to re-join a game. To return to a room, use OpJoinRoom with
/// the actorNumber which was assigned previously.
///
/// If you set room properties in RoomOptions, they get ignored when the room is existing already.
/// This avoids changing the room properties by late joining players. Only when the room gets created,
/// the RoomOptions are set in this case.
///
/// If the room is full or closed, this will fail. Override this class and implement
/// OnOperationResponse(OperationResponse operationResponse) to get the errors.
///
/// This method can only be called while the client is connected to a Master Server.
/// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before
/// calling this method. Alternatively, check the returned bool value.
///
/// While the server is joining the game, the State will be ClientState.Joining.
/// It's set immediately when this method sends the Operation.
///
/// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically
/// to switch servers and join the room. When you're in the room, this client's State will become
/// ClientState.Joined (both, for joining or creating it).
/// Set a OnStateChangeAction method to check for states.
///
/// When entering the room, this client's Player Custom Properties will be sent to the room.
/// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
/// Note that the player properties will be cached locally and sent to any next room you would join, too.
///
/// You can define an array of expectedUsers, to block player slots in the room for these users.
/// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
///
/// The name of the room to join (might be created implicitly).
/// Contains the parameters and properties of the new room. See RoomOptions class for a description of each.
/// Typed lobby to be used if the roomname is not in use (and room gets created). If != null, it will also set CurrentLobby.
/// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.
/// If the operation could be sent currently (requires connection to Master Server).
public bool OpJoinOrCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby, string[] expectedUsers = null)
{
this.State = ClientState.Joining;
this.lastJoinType = JoinType.JoinOrCreateRoom;
this.CurrentLobby = lobby;
bool onGameServer = this.Server == ServerConnection.GameServer;
EnterRoomParams opParams = new EnterRoomParams();
this.enterRoomParamsCache = opParams;
opParams.RoomName = roomName;
opParams.RoomOptions = roomOptions;
opParams.Lobby = lobby;
opParams.CreateIfNotExists = true;
opParams.OnGameServer = onGameServer;
opParams.ExpectedUsers = expectedUsers;
return this.loadBalancingPeer.OpJoinRoom(opParams);
}
///
/// Creates a new room on the server (or fails if the name is already in use).
///
///
/// If you don't want to create a unique room-name, pass null or "" as name and the server will assign a
/// roomName (a GUID as string). Room names are unique.
///
/// A room will be attached to the specified lobby. Use null as lobby to attach the
/// room to the lobby you are now in. If you are in no lobby, the default lobby is used.
///
/// Multiple lobbies can help separate players by map or skill or game type. Each room can only be found
/// in one lobby (no matter if defined by name and type or as default).
///
/// This method can only be called while the client is connected to a Master Server.
/// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method.
/// Alternatively, check the returned bool value.
///
/// Even when sent, the Operation will fail (on the server) if the roomName is in use.
/// Override this class and implement OnOperationResponse(OperationResponse operationResponse) to get the errors.
///
///
/// While the server is creating the game, the State will be ClientState.Joining.
/// The state Joining is used because the client is on the way to enter a room (no matter if joining or creating).
/// It's set immediately when this method sends the Operation.
///
/// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically
/// to switch servers and enter the room. When you're in the room, this client's State will become
/// ClientState.Joined (both, for joining or creating it).
/// Set a OnStateChangeAction method to check for states.
///
/// When entering the room, this client's Player Custom Properties will be sent to the room.
/// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room.
/// Note that the player properties will be cached locally and sent to any next room you would join, too.
///
/// You can define an array of expectedUsers, to block player slots in the room for these users.
/// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
///
/// The name to create a room with. Must be unique and not in use or can't be created. If null, the server will assign a GUID as name.
/// Contains the parameters and properties of the new room. See RoomOptions class for a description of each.
/// The lobby (name and type) in which to create the room. Null uses the current lobby or the default lobby (if not in a lobby).
/// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for.
/// If the operation could be sent currently (requires connection to Master Server).
public bool OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby, string[] expectedUsers = null)
{
this.State = ClientState.Joining;
this.lastJoinType = JoinType.CreateRoom;
this.CurrentLobby = lobby;
bool onGameServer = this.Server == ServerConnection.GameServer;
EnterRoomParams opParams = new EnterRoomParams();
this.enterRoomParamsCache = opParams;
opParams.RoomName = roomName;
opParams.RoomOptions = roomOptions;
opParams.Lobby = lobby;
opParams.OnGameServer = onGameServer;
opParams.ExpectedUsers = expectedUsers;
return this.loadBalancingPeer.OpCreateRoom(opParams);
//return this.loadBalancingPeer.OpCreateRoom(roomName, roomOptions, lobby, playerPropsToSend, onGameServer);
}
///
/// Leaves the CurrentRoom and returns to the Master server (back to the lobby).
/// OpLeaveRoom skips execution when the room is null or the server is not GameServer or the client is disconnecting from GS already.
/// OpLeaveRoom returns false in those cases and won't change the state, so check return of this method.
///
///
/// This method actually is not an operation per se. It sets a state and calls Disconnect().
/// This is is quicker than calling OpLeave and then disconnect (which also triggers a leave).
///
/// If the current room could be left (impossible while not in a room).
public bool OpLeaveRoom()
{
return OpLeaveRoom(false); //TURNBASED
}
///
/// Leaves the current room, optionally telling the server that the user is just becoming inactive.
///
///
/// If true, this player becomes inactive in the game and can return later (if PlayerTTL of the room is > 0).
///
/// OpLeaveRoom skips execution when the room is null or the server is not GameServer or the client is disconnecting from GS already.
/// OpLeaveRoom returns false in those cases and won't change the state, so check return of this method.
///
/// In some cases, this method will skip the OpLeave call and just call Disconnect(),
/// which not only leaves the room but also the server. Disconnect also triggers a leave and so that workflow is is quicker.
///
/// If the current room could be left (impossible while not in a room).
public bool OpLeaveRoom(bool becomeInactive)
{
if (this.CurrentRoom == null || this.Server != ServerConnection.GameServer || this.State == ClientState.DisconnectingFromGameserver)
{
return false;
}
if (becomeInactive)
{
this.State = ClientState.DisconnectingFromGameserver;
this.loadBalancingPeer.Disconnect();
}
else
{
this.State = ClientState.Leaving;
this.loadBalancingPeer.OpLeaveRoom(false); //TURNBASED users can leave a room forever or return later
}
return true;
}
/// Gets a list of games matching a SQL-like where clause.
///
/// Operation is only available for lobbies of type SqlLobby.
/// This is an async request which triggers a OnOperationResponse() call.
/// Returned game list is stored in RoomInfoList.
///
///
/// The lobby to query. Has to be of type SqlLobby.
/// The sql query statement.
/// If the operation could be sent (has to be connected).
public bool OpGetGameList(TypedLobby typedLobby, string sqlLobbyFilter)
{
return this.loadBalancingPeer.OpGetGameList(typedLobby, sqlLobbyFilter);
}
///
/// Updates and synchronizes a Player's Custom Properties. Optionally, expectedProperties can be provided as condition.
///
///
/// Custom Properties are a set of string keys and arbitrary values which is synchronized
/// for the players in a Room. They are available when the client enters the room, as
/// they are in the response of OpJoin and OpCreate.
///
/// Custom Properties either relate to the (current) Room or a Player (in that Room).
///
/// Both classes locally cache the current key/values and make them available as
/// property: CustomProperties. This is provided only to read them.
/// You must use the method SetCustomProperties to set/modify them.
///
/// Any client can set any Custom Properties anytime (when in a room).
/// It's up to the game logic to organize how they are best used.
///
/// You should call SetCustomProperties only with key/values that are new or changed. This reduces
/// traffic and performance.
///
/// Unless you define some expectedProperties, setting key/values is always permitted.
/// In this case, the property-setting client will not receive the new values from the server but
/// instead update its local cache in SetCustomProperties.
///
/// If you define expectedProperties, the server will skip updates if the server property-cache
/// does not contain all expectedProperties with the same values.
/// In this case, the property-setting client will get an update from the server and update it's
/// cached key/values at about the same time as everyone else.
///
/// The benefit of using expectedProperties can be only one client successfully sets a key from
/// one known value to another.
/// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
/// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
/// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
/// take the item will have it (and the others fail to set the ownership).
///
/// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
///
/// Defines which player the Custom Properties belong to. ActorID of a player.
/// Hashtable of Custom Properties that changes.
/// Provide some keys/values to use as condition for setting the new values. Client must be in room.
/// Defines if the set properties should be forwarded to a WebHook. Client must be in room.
public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
{
if (this.CurrentRoom == null)
{
// if you attempt to set this player's values without conditions, then fine:
if (expectedProperties == null && webFlags == null && this.LocalPlayer != null && this.LocalPlayer.ID == actorNr)
{
this.LocalPlayer.SetCustomProperties(propertiesToSet);
return true;
}
if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR)
{
this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. To use expectedProperties or webForward, you have to be in a room. State: " + this.State);
}
return false;
}
Hashtable customActorProperties = new Hashtable();
customActorProperties.MergeStringKeys(propertiesToSet);
return this.OpSetPropertiesOfActor(actorNr, customActorProperties, expectedProperties, webFlags);
}
/// Replaced by a newer version with WebFlags.
[Obsolete("Use the overload with WebFlags.")]
public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties, bool webForward)
{
return this.OpSetCustomPropertiesOfActor(actorNr, propertiesToSet, expectedProperties, (webForward ? new WebFlags(WebFlags.HttpForwardConst) : null));
}
/// Internally used to cache and set properties (including well known properties).
/// Requires being in a room (because this attempts to send an operation which will fail otherwise).
protected internal bool OpSetPropertiesOfActor(int actorNr, Hashtable actorProperties, Hashtable expectedProperties = null, WebFlags webFlags = null)
{
if (this.CurrentRoom == null)
{
if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR)
{
this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfActor() failed because this client is not in a room currently. State: " + this.State);
}
return false;
}
if (expectedProperties == null || expectedProperties.Count == 0)
{
Player target = this.CurrentRoom.GetPlayer(actorNr);
if (target != null)
{
target.InternalCacheProperties(actorProperties);
}
}
return this.loadBalancingPeer.OpSetPropertiesOfActor(actorNr, actorProperties, expectedProperties, webFlags);
}
///
/// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition.
///
///
/// Custom Properties are a set of string keys and arbitrary values which is synchronized
/// for the players in a Room. They are available when the client enters the room, as
/// they are in the response of OpJoin and OpCreate.
///
/// Custom Properties either relate to the (current) Room or a Player (in that Room).
///
/// Both classes locally cache the current key/values and make them available as
/// property: CustomProperties. This is provided only to read them.
/// You must use the method SetCustomProperties to set/modify them.
///
/// Any client can set any Custom Properties anytime (when in a room).
/// It's up to the game logic to organize how they are best used.
///
/// You should call SetCustomProperties only with key/values that are new or changed. This reduces
/// traffic and performance.
///
/// Unless you define some expectedProperties, setting key/values is always permitted.
/// In this case, the property-setting client will not receive the new values from the server but
/// instead update its local cache in SetCustomProperties.
///
/// If you define expectedProperties, the server will skip updates if the server property-cache
/// does not contain all expectedProperties with the same values.
/// In this case, the property-setting client will get an update from the server and update it's
/// cached key/values at about the same time as everyone else.
///
/// The benefit of using expectedProperties can be only one client successfully sets a key from
/// one known value to another.
/// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
/// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
/// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
/// take the item will have it (and the others fail to set the ownership).
///
/// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
///
/// Hashtable of Custom Properties that changes.
/// Provide some keys/values to use as condition for setting the new values.
/// Defines if the set properties should be forwarded to a WebHook.
public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
{
Hashtable customGameProps = new Hashtable();
customGameProps.MergeStringKeys(propertiesToSet);
return this.OpSetPropertiesOfRoom(customGameProps, expectedProperties, webFlags);
}
/// Replaced by a newer version with WebFlags.
[Obsolete("Use the overload with WebFlags.")]
public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties, bool webForward)
{
return this.OpSetCustomPropertiesOfRoom(propertiesToSet, expectedProperties, (webForward ? new WebFlags(WebFlags.HttpForwardConst) : null));
}
/// Internally used to cache and set properties (including well known properties).
/// Requires being in a room (because this attempts to send an operation which will fail otherwise).
protected internal bool OpSetPropertiesOfRoom(Hashtable gameProperties, Hashtable expectedProperties = null, WebFlags webFlags = null)
{
if (this.CurrentRoom == null)
{
if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR)
{
this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfRoom() failed because this client is not in a room currently. State: " + this.State);
}
return false;
}
if (expectedProperties == null || expectedProperties.Count == 0)
{
this.CurrentRoom.InternalCacheProperties(gameProperties);
}
return this.loadBalancingPeer.OpSetPropertiesOfRoom(gameProperties, expectedProperties, webFlags);
}
///
/// Send an event with custom code/type and any content to the other players in the same room.
///
/// This override explicitly uses another parameter order to not mix it up with the implementation for Hashtable only.
/// Identifies this type of event (and the content). Your game's event codes can start with 0.
/// Any serializable datatype (including Hashtable like the other OpRaiseEvent overloads).
/// If this event has to arrive reliably (potentially repeated if it's lost).
/// Contains (slightly) less often used options. If you pass null, the default options will be used.
/// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands.
public virtual bool OpRaiseEvent(byte eventCode, object customEventContent, bool sendReliable, RaiseEventOptions raiseEventOptions)
{
if (this.loadBalancingPeer == null)
{
return false;
}
return this.loadBalancingPeer.OpRaiseEvent(eventCode, customEventContent, sendReliable, raiseEventOptions);
}
///
/// Operation to handle this client's interest groups (for events in room).
///
///
/// Note the difference between passing null and byte[0]:
/// null won't add/remove any groups.
/// byte[0] will add/remove all (existing) groups.
/// First, removing groups is executed. This way, you could leave all groups and join only the ones provided.
///
/// Changes become active not immediately but when the server executes this operation (approximately RTT/2).
///
/// Groups to remove from interest. Null will not remove any. A byte[0] will remove all.
/// Groups to add to interest. Null will not add any. A byte[0] will add all current.
/// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands.
public virtual bool OpChangeGroups(byte[] groupsToRemove, byte[] groupsToAdd)
{
if (this.loadBalancingPeer == null)
{
return false;
}
return this.loadBalancingPeer.OpChangeGroups(groupsToRemove, groupsToAdd);
}
#endregion
#region Helpers
///
/// Privately used to read-out properties coming from the server in events and operation responses (which might be a bit tricky).
///
private void ReadoutProperties(Hashtable gameProperties, Hashtable actorProperties, int targetActorNr)
{
// read game properties and cache them locally
if (this.CurrentRoom != null && gameProperties != null)
{
this.CurrentRoom.InternalCacheProperties(gameProperties);
}
if (actorProperties != null && actorProperties.Count > 0)
{
if (targetActorNr > 0)
{
// we have a single entry in the actorProperties with one user's name
// targets MUST exist before you set properties
Player target = this.CurrentRoom.GetPlayer(targetActorNr);
if (target != null)
{
Hashtable props = this.ReadoutPropertiesForActorNr(actorProperties, targetActorNr);
target.InternalCacheProperties(props);
//SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerPropertiesChanged, target, props);
}
}
else
{
// in this case, we've got a key-value pair per actor (each
// value is a hashtable with the actor's properties then)
int actorNr;
Hashtable props;
string newName;
Player target;
foreach (object key in actorProperties.Keys)
{
actorNr = (int)key;
props = (Hashtable)actorProperties[key];
newName = (string)props[ActorProperties.PlayerName];
target = this.CurrentRoom.GetPlayer(actorNr);
if (target == null)
{
target = this.CreatePlayer(newName, actorNr, false, props);
this.CurrentRoom.StorePlayer(target);
}
target.InternalCacheProperties(props);
//SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerPropertiesChanged, target, props);
}
}
}
}
///
/// Privately used only to read properties for a distinct actor (which might be the hashtable OR a key-pair value IN the actorProperties).
///
private Hashtable ReadoutPropertiesForActorNr(Hashtable actorProperties, int actorNr)
{
if (actorProperties.ContainsKey(actorNr))
{
return (Hashtable)actorProperties[actorNr];
}
return actorProperties;
}
///
/// Internally used to set the LocalPlayer's ID (from -1 to the actual in-room ID).
///
/// New actor ID (a.k.a actorNr) assigned when joining a room.
protected internal void ChangeLocalID(int newID)
{
if (this.LocalPlayer == null)
{
this.DebugReturn(DebugLevel.WARNING, string.Format("Local actor is null or not in mActors! mLocalActor: {0} mActors==null: {1} newID: {2}", this.LocalPlayer, this.CurrentRoom.Players == null, newID));
}
if (this.CurrentRoom == null)
{
// change to new actor/player ID and make sure the player does not have a room reference left
this.LocalPlayer.ChangeLocalID(newID);
this.LocalPlayer.RoomReference = null;
}
else
{
// remove old actorId from actor list
this.CurrentRoom.RemovePlayer(this.LocalPlayer);
// change to new actor/player ID
this.LocalPlayer.ChangeLocalID(newID);
// update the room's list with the new reference
this.CurrentRoom.StorePlayer(this.LocalPlayer);
}
}
///
/// Internally used to clean up local instances of players and room.
///
private void CleanCachedValues()
{
this.ChangeLocalID(-1);
this.isFetchingFriendList = false;
// if this is called on the gameserver, we clean the room we were in. on the master, we keep the room to get into it
if (this.Server == ServerConnection.GameServer || this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated)
{
this.CurrentRoom = null; // players get cleaned up inside this, too, except LocalPlayer (which we keep)
}
// when we leave the master, we clean up the rooms list (which might be updated by the lobby when we join again)
if (this.Server == ServerConnection.MasterServer || this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated)
{
this.RoomInfoList.Clear();
}
}
///
/// Called internally, when a game was joined or created on the game server successfully.
///
///
/// This reads the response, finds out the local player's actorNumber (a.k.a. Player.ID) and applies properties of the room and players.
/// Errors for these operations are to be handled before this method is called.
///
/// Contains the server's response for an operation called by this peer.
private void GameEnteredOnGameServer(OperationResponse operationResponse)
{
this.CurrentRoom = this.CreateRoom(this.enterRoomParamsCache.RoomName, this.enterRoomParamsCache.RoomOptions);
this.CurrentRoom.LoadBalancingClient = this;
this.CurrentRoom.IsLocalClientInside = true;
// first change the local id, instead of first updating the actorList since actorList uses ID to update itself
// the local player's actor-properties are not returned in join-result. add this player to the list
int localActorNr = (int)operationResponse[ParameterCode.ActorNr];
this.ChangeLocalID(localActorNr);
if (operationResponse.Parameters.ContainsKey(ParameterCode.ActorList))
{
int[] actorsInRoom = (int[])operationResponse.Parameters[ParameterCode.ActorList];
this.UpdatedActorList(actorsInRoom);
}
Hashtable actorProperties = (Hashtable)operationResponse[ParameterCode.PlayerProperties];
Hashtable gameProperties = (Hashtable)operationResponse[ParameterCode.GameProperties];
this.ReadoutProperties(gameProperties, actorProperties, 0);
this.State = ClientState.Joined;
switch (operationResponse.OperationCode)
{
case OperationCode.CreateGame:
// TODO: add callback "game created"
break;
case OperationCode.JoinGame:
case OperationCode.JoinRandomGame:
// "game joined" should be called in another place (the join ev contains important info for the room).
break;
}
}
private void UpdatedActorList(int[] actorsInGame)
{
if (actorsInGame != null)
{
foreach (int userId in actorsInGame)
{
Player target = this.CurrentRoom.GetPlayer(userId);
if (target == null)
{
this.CurrentRoom.StorePlayer(this.CreatePlayer(string.Empty, userId, false, null));
}
}
}
}
///
/// Factory method to create a player instance - override to get your own player-type with custom features.
///
/// The name of the player to be created.
/// The player ID (a.k.a. actorNumber) of the player to be created.
/// Sets the distinction if the player to be created is your player or if its assigned to someone else.
/// The custom properties for this new player
/// The newly created player
protected internal virtual Player CreatePlayer(string actorName, int actorNumber, bool isLocal, Hashtable actorProperties)
{
Player newPlayer = new Player(actorName, actorNumber, isLocal, actorProperties);
return newPlayer;
}
/// Internal "factory" method to create a room-instance.
protected internal virtual Room CreateRoom(string roomName, RoomOptions opt)
{
Room r = new Room(roomName, opt);
return r;
}
private byte[] encryptionSecret;
#endregion
#region Implementation of IPhotonPeerListener
/// Debug output of low level api (and this client).
/// This method is not responsible to keep up the state of a LoadBalancingClient. Calling base.DebugReturn on overrides is optional.
public virtual void DebugReturn(DebugLevel level, string message)
{
#if !UNITY
Debug.WriteLine(message);
#else
if (level == DebugLevel.ERROR)
{
Debug.LogError(message);
}
else if (level == DebugLevel.WARNING)
{
Debug.LogWarning(message);
}
else if (level == DebugLevel.INFO)
{
Debug.Log(message);
}
else if (level == DebugLevel.ALL)
{
Debug.Log(message);
}
#endif
}
///
/// Uses the OperationResponses provided by the server to advance the internal state and call ops as needed.
///
///
/// When this method finishes, it will call your OnOpResponseAction (if any). This way, you can get any
/// operation response without overriding this class.
///
/// To implement a more complex game/app logic, you should implement your own class that inherits the
/// LoadBalancingClient. Override this method to use your own operation-responses easily.
///
/// This method is essential to update the internal state of a LoadBalancingClient, so overriding methods
/// must call base.OnOperationResponse().
///
/// Contains the server's response for an operation called by this peer.
public virtual void OnOperationResponse(OperationResponse operationResponse)
{
// if (operationResponse.ReturnCode != 0) this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull());
// use the "secret" or "token" whenever we get it. doesn't really matter if it's in AuthResponse.
if (operationResponse.Parameters.ContainsKey(ParameterCode.Secret))
{
if (this.AuthValues == null)
{
this.AuthValues = new AuthenticationValues();
//this.DebugReturn(DebugLevel.ERROR, "Server returned secret. Created AuthValues.");
}
this.AuthValues.Token = operationResponse[ParameterCode.Secret] as string;
}
switch (operationResponse.OperationCode)
{
case OperationCode.Authenticate:
case OperationCode.AuthenticateOnce:
{
if (operationResponse.ReturnCode != 0)
{
this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull() + " Server: " + this.Server + " Address: " + this.loadBalancingPeer.ServerAddress);
switch (operationResponse.ReturnCode)
{
case ErrorCode.InvalidAuthentication:
this.DisconnectedCause = DisconnectCause.InvalidAuthentication;
break;
case ErrorCode.CustomAuthenticationFailed:
this.DisconnectedCause = DisconnectCause.CustomAuthenticationFailed;
break;
case ErrorCode.InvalidRegion:
this.DisconnectedCause = DisconnectCause.InvalidRegion;
break;
case ErrorCode.MaxCcuReached:
this.DisconnectedCause = DisconnectCause.MaxCcuReached;
break;
case ErrorCode.OperationNotAllowedInCurrentState:
this.DisconnectedCause = DisconnectCause.OperationNotAllowedInCurrentState;
break;
}
this.State = ClientState.Disconnecting;
this.Disconnect();
break; // if auth didn't succeed, we disconnect (above) and exit this operation's handling
}
if (this.Server == ServerConnection.NameServer || this.Server == ServerConnection.MasterServer)
{
if (operationResponse.Parameters.ContainsKey(ParameterCode.UserId))
{
string incomingId = (string)operationResponse.Parameters[ParameterCode.UserId];
if (!string.IsNullOrEmpty(incomingId))
{
this.UserId = incomingId;
this.DebugReturn(DebugLevel.INFO, string.Format("Received your UserID from server. Updating local value to: {0}", this.UserId));
}
}
if (operationResponse.Parameters.ContainsKey(ParameterCode.NickName))
{
this.NickName = (string)operationResponse.Parameters[ParameterCode.NickName];
this.DebugReturn(DebugLevel.INFO, string.Format("Received your NickName from server. Updating local value to: {0}", this.NickName));
}
if (operationResponse.Parameters.ContainsKey(ParameterCode.EncryptionData))
{
this.SetupEncryption((Dictionary)operationResponse.Parameters[ParameterCode.EncryptionData]);
}
}
if (this.Server == ServerConnection.NameServer)
{
// on the NameServer, authenticate returns the MasterServer address for a region and we hop off to there
this.MasterServerAddress = operationResponse[ParameterCode.Address] as string;
if (this.AuthMode == AuthModeOption.AuthOnceWss)
{
this.DebugReturn(DebugLevel.INFO, string.Format("Due to AuthOnceWss, switching TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol));
this.loadBalancingPeer.TransportProtocol = this.ExpectedProtocol;
}
this.DisconnectToReconnect();
}
else if (this.Server == ServerConnection.MasterServer)
{
this.State = ClientState.ConnectedToMasterserver;
if (this.AuthMode != AuthModeOption.Auth)
{
this.loadBalancingPeer.OpSettings(this.EnableLobbyStatistics);
}
if (this.AutoJoinLobby)
{
this.loadBalancingPeer.OpJoinLobby(this.CurrentLobby);
}
}
else if (this.Server == ServerConnection.GameServer)
{
this.State = ClientState.Joining;
this.enterRoomParamsCache.PlayerProperties = this.LocalPlayer.AllProperties;
this.enterRoomParamsCache.OnGameServer = true;
if (this.lastJoinType == JoinType.JoinRoom || this.lastJoinType == JoinType.JoinRandomRoom || this.lastJoinType == JoinType.JoinOrCreateRoom)
{
this.loadBalancingPeer.OpJoinRoom(this.enterRoomParamsCache);
}
else if (this.lastJoinType == JoinType.CreateRoom)
{
this.loadBalancingPeer.OpCreateRoom(this.enterRoomParamsCache);
}
break;
}
break;
}
case OperationCode.GetRegions:
this.AvailableRegions = operationResponse[ParameterCode.Region] as string[];
this.AvailableRegionsServers = operationResponse[ParameterCode.Address] as string[];
break;
case OperationCode.JoinRandomGame: // this happens only on the master server. on gameserver this is a "regular" join
case OperationCode.CreateGame:
case OperationCode.JoinGame:
if (operationResponse.ReturnCode != 0)
{
//PhotonNetworkingMessage callback = PhotonNetworkingMessage.OnPhotonRandomJoinFailed;
//if (operationResponse.OperationCode == OperationCode.CreateGame) callback = PhotonNetworkingMessage.OnPhotonCreateRoomFailed;
//if (operationResponse.OperationCode == OperationCode.JoinGame) callback = PhotonNetworkingMessage.OnPhotonJoinRoomFailed;
//SendMonoMessage(callback, operationResponse.ReturnCode, operationResponse.DebugMessage);
if (this.Server == ServerConnection.GameServer)
{
this.DisconnectToReconnect();
}
else
{
this.State = (this.inLobby) ? ClientState.JoinedLobby : ClientState.ConnectedToMasterserver;
}
}
else
{
if (this.Server == ServerConnection.GameServer)
{
this.GameEnteredOnGameServer(operationResponse);
}
else
{
this.GameServerAddress = (string) operationResponse[ParameterCode.Address];
string roomName = operationResponse[ParameterCode.RoomName] as string;
if (!string.IsNullOrEmpty(roomName))
{
this.enterRoomParamsCache.RoomName = roomName;
}
this.DisconnectToReconnect();
}
}
break;
case OperationCode.GetGameList:
if (operationResponse.ReturnCode != 0)
{
this.DebugReturn(DebugLevel.ERROR, "GetGameList failed: " + operationResponse.ToStringFull());
break;
}
this.RoomInfoList = new Dictionary();
Hashtable games = (Hashtable)operationResponse[ParameterCode.GameList];
foreach (string gameName in games.Keys)
{
RoomInfo game = new RoomInfo(gameName, (Hashtable)games[gameName]);
this.RoomInfoList[gameName] = game;
}
// TODO: OnRoomListUpdate
break;
case OperationCode.JoinLobby:
this.State = ClientState.JoinedLobby;
this.inLobby = true;
// TODO: OnJoinedLobby
break;
case OperationCode.LeaveLobby:
this.State = ClientState.ConnectedToMasterserver;
this.inLobby = false;
break;
case OperationCode.Leave:
//this.CleanCachedValues(); // this is done in status change on "disconnect"
this.DisconnectToReconnect();
break;
case OperationCode.FindFriends:
if (operationResponse.ReturnCode != 0)
{
this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed: " + operationResponse.ToStringFull());
this.isFetchingFriendList = false;
break;
}
bool[] onlineList = operationResponse[ParameterCode.FindFriendsResponseOnlineList] as bool[];
string[] roomList = operationResponse[ParameterCode.FindFriendsResponseRoomIdList] as string[];
List friendList = new List(this.friendListRequested.Length);
for (int index = 0; index < this.friendListRequested.Length; index++)
{
FriendInfo friend = new FriendInfo();
friend.Name = this.friendListRequested[index];
friend.Room = roomList[index];
friend.IsOnline = onlineList[index];
friendList.Insert(index, friend);
}
this.FriendList = friendList;
this.friendListRequested = null;
this.isFetchingFriendList = false;
this.friendListTimestamp = Environment.TickCount;
if (this.friendListTimestamp == 0)
{
this.friendListTimestamp = 1; // makes sure the timestamp is not accidentally 0
}
break;
}
if (this.OnOpResponseAction != null) this.OnOpResponseAction(operationResponse);
}
///
/// Uses the connection's statusCodes to advance the internal state and call operations as needed.
///
/// This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnStatusChanged.
public virtual void OnStatusChanged(StatusCode statusCode)
{
switch (statusCode)
{
case StatusCode.Connect:
this.inLobby = false;
if (this.State == ClientState.ConnectingToNameServer)
{
if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL)
{
this.DebugReturn(DebugLevel.ALL, "Connected to nameserver.");
}
this.Server = ServerConnection.NameServer;
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when connecting to NameServer, invalidate the secret (only)
}
}
if (this.State == ClientState.ConnectingToGameserver)
{
if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL)
{
this.DebugReturn(DebugLevel.ALL, "Connected to gameserver.");
}
this.Server = ServerConnection.GameServer;
}
if (this.State == ClientState.ConnectingToMasterserver)
{
if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL)
{
this.DebugReturn(DebugLevel.ALL, "Connected to masterserver.");
}
this.Server = ServerConnection.MasterServer;
}
if (this.loadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure)
{
if (this.Server == ServerConnection.NameServer || this.AuthMode == AuthModeOption.Auth)
{
this.loadBalancingPeer.EstablishEncryption();
}
}
else
{
goto case StatusCode.EncryptionEstablished;
}
break;
case StatusCode.EncryptionEstablished:
// on nameserver, the "process" is stopped here, so the developer/game can either get regions or authenticate with a specific region
if (this.Server == ServerConnection.NameServer)
{
this.State = ClientState.ConnectedToNameServer;
// TODO: should we automatically get the regions?!
}
if (this.Server != ServerConnection.NameServer && (this.AuthMode == AuthModeOption.AuthOnce || this.AuthMode == AuthModeOption.AuthOnceWss))
{
// AuthMode "Once" means we only authenticate on the NameServer
break;
}
// on any other server we might now have to authenticate still, so the client can do anything at all
if (!this.didAuthenticate && (!this.IsUsingNameServer || this.CloudRegion != null))
{
// once encryption is availble, the client should send one (secure) authenticate. it includes the AppId (which identifies your app on the Photon Cloud)
this.didAuthenticate = this.CallAuthenticate();
if (this.didAuthenticate)
{
this.State = ClientState.Authenticating;
}
else
{
this.DebugReturn(DebugLevel.ERROR, "Error calling OpAuthenticate! Did not work. Check log output, AuthValues and if you're connected. State: " + this.State);
}
}
break;
case StatusCode.Disconnect:
// disconnect due to connection exception is handled below (don't connect to GS or master in that case)
this.CleanCachedValues();
this.didAuthenticate = false; // on connect, we know that we didn't
this.inLobby = false;
switch (this.State)
{
case ClientState.PeerCreated:
case ClientState.Disconnecting:
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
}
this.State = ClientState.Disconnected;
break;
case ClientState.DisconnectingFromGameserver:
case ClientState.DisconnectingFromNameServer:
this.Connect(); // this gets the client back to the Master Server
break;
case ClientState.DisconnectingFromMasterserver:
this.ConnectToGameServer(); // this connects the client with the Game Server (when joining/creating a room)
break;
default:
string stacktrace = "";
#if DEBUG && !NETFX_CORE
stacktrace = new System.Diagnostics.StackTrace(true).ToString();
#endif
this.DebugReturn(DebugLevel.WARNING, "Got a unexpected Disconnect in LoadBalancingClient State: " + this.State + ". Server: " + this.Server+ " Trace: " + stacktrace);
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
}
this.State = ClientState.Disconnected;
break;
}
break;
case StatusCode.DisconnectByServerUserLimit:
this.DebugReturn(DebugLevel.ERROR, "The Photon license's CCU Limit was reached. Server rejected this connection. Wait and re-try.");
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
}
this.DisconnectedCause = DisconnectCause.DisconnectByServerUserLimit;
this.State = ClientState.Disconnected;
break;
case StatusCode.ExceptionOnConnect:
case StatusCode.SecurityExceptionOnConnect:
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
}
this.DisconnectedCause = DisconnectCause.ExceptionOnConnect;
this.State = ClientState.Disconnected;
break;
case StatusCode.DisconnectByServer:
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
}
this.DisconnectedCause = DisconnectCause.DisconnectByServer;
this.State = ClientState.Disconnected;
break;
case StatusCode.DisconnectByServerLogic:
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
}
this.DisconnectedCause = DisconnectCause.DisconnectByServerLogic;
this.State = ClientState.Disconnected;
break;
case StatusCode.TimeoutDisconnect:
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
}
this.DisconnectedCause = DisconnectCause.TimeoutDisconnect;
this.State = ClientState.Disconnected;
break;
case StatusCode.Exception:
case StatusCode.ExceptionOnReceive:
if (this.AuthValues != null)
{
this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values)
}
this.DisconnectedCause = DisconnectCause.Exception;
this.State = ClientState.Disconnected;
break;
}
}
///
/// Uses the photonEvent's provided by the server to advance the internal state and call ops as needed.
///
/// This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnEvent.
public virtual void OnEvent(EventData photonEvent)
{
int actorNr = 0;
Player originatingPlayer = null;
if (photonEvent.Parameters.ContainsKey(ParameterCode.ActorNr))
{
actorNr = (int) photonEvent[ParameterCode.ActorNr];
if (this.CurrentRoom != null)
{
originatingPlayer = this.CurrentRoom.GetPlayer(actorNr);
}
}
switch (photonEvent.Code)
{
case EventCode.GameList:
case EventCode.GameListUpdate:
if (photonEvent.Code == EventCode.GameList)
{
this.RoomInfoList = new Dictionary();
}
Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList];
foreach (string gameName in games.Keys)
{
RoomInfo game = new RoomInfo(gameName, (Hashtable)games[gameName]);
if (game.removedFromList)
{
this.RoomInfoList.Remove(gameName);
}
else
{
this.RoomInfoList[gameName] = game;
}
}
//SendMonoMessage(PhotonNetworkingMessage.OnReceivedRoomListUpdate);
break;
case EventCode.Join:
Hashtable actorProperties = (Hashtable)photonEvent[ParameterCode.PlayerProperties];
if (originatingPlayer == null)
{
Player newPlayer = this.CreatePlayer(string.Empty, actorNr, false, actorProperties);
this.CurrentRoom.StorePlayer(newPlayer);
}
else
{
originatingPlayer.InternalCacheProperties(actorProperties);
originatingPlayer.IsInactive = false;
}
if (actorNr == this.LocalPlayer.ID)
{
// in this player's own join event, we get a complete list of players in the room, so check if we know each of the
int[] actorsInRoom = (int[])photonEvent[ParameterCode.ActorList];
this.UpdatedActorList(actorsInRoom);
}
break;
case EventCode.Leave:
bool isInactive = false;
if (photonEvent.Parameters.ContainsKey(ParameterCode.IsInactive))
{
isInactive = (bool)photonEvent.Parameters[ParameterCode.IsInactive];
}
if (isInactive)
{
originatingPlayer.IsInactive = true;
}
else
{
this.CurrentRoom.RemovePlayer(actorNr);
}
if (photonEvent.Parameters.ContainsKey(ParameterCode.MasterClientId))
{
int newMaster = (int)photonEvent[ParameterCode.MasterClientId];
if (newMaster != 0)
{
this.CurrentRoom.masterClientId = newMaster;
}
}
break;
case EventCode.PropertiesChanged:
// whenever properties are sent in-room, they can be broadcasted as event (which we handle here)
// we get PLAYERproperties if actorNr > 0 or ROOMproperties if actorNumber is not set or 0
int targetActorNr = 0;
if (photonEvent.Parameters.ContainsKey(ParameterCode.TargetActorNr))
{
targetActorNr = (int)photonEvent[ParameterCode.TargetActorNr];
}
Hashtable gameProperties = null;
Hashtable actorProps = null;
if (targetActorNr == 0)
{
gameProperties = (Hashtable)photonEvent[ParameterCode.Properties];
}
else
{
actorProps = (Hashtable)photonEvent[ParameterCode.Properties];
}
this.ReadoutProperties(gameProperties, actorProps, targetActorNr);
break;
case EventCode.AppStats:
// only the master server sends these in (1 minute) intervals
this.PlayersInRoomsCount = (int)photonEvent[ParameterCode.PeerCount];
this.RoomsCount = (int)photonEvent[ParameterCode.GameCount];
this.PlayersOnMasterCount = (int)photonEvent[ParameterCode.MasterPeerCount];
break;
case EventCode.LobbyStats:
string[] names = photonEvent[ParameterCode.LobbyName] as string[];
byte[] types = photonEvent[ParameterCode.LobbyType] as byte[];
int[] peers = photonEvent[ParameterCode.PeerCount] as int[];
int[] rooms = photonEvent[ParameterCode.GameCount] as int[];
this.lobbyStatistics.Clear();
for (int i = 0; i < names.Length; i++)
{
TypedLobbyInfo info = new TypedLobbyInfo();
info.Name = names[i];
info.Type = (LobbyType)types[i];
info.PlayerCount = peers[i];
info.RoomCount = rooms[i];
this.lobbyStatistics.Add(info);
}
//SendMonoMessage(PhotonNetworkingMessage.OnLobbyStatisticsUpdate);
break;
case EventCode.ErrorInfo:
if (this.OnEventAction != null)
{
this.OnEventAction(photonEvent);
}
break;
case EventCode.AuthEvent:
if (this.AuthValues == null)
{
this.AuthValues = new AuthenticationValues();
}
this.AuthValues.Token = photonEvent[ParameterCode.Secret] as string;
break;
}
if (this.OnEventAction != null) this.OnEventAction(photonEvent);
}
/// In Photon 4, "raw messages" will get their own callback method in the interface. Not used yet.
public virtual void OnMessage(object message)
{
this.DebugReturn(DebugLevel.ALL, string.Format("got OnMessage {0}", message));
}
#endregion
private void SetupEncryption(Dictionary encryptionData)
{
var mode = (EncryptionMode)(byte)encryptionData[EncryptionDataParameters.Mode];
switch (mode)
{
case EncryptionMode.PayloadEncryption:
byte[] encryptionSecret = (byte[])encryptionData[EncryptionDataParameters.Secret1];
this.loadBalancingPeer.InitPayloadEncryption(encryptionSecret);
break;
case EncryptionMode.DatagramEncryption:
{
byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1];
byte[] secret2 = (byte[])encryptionData[EncryptionDataParameters.Secret2];
this.loadBalancingPeer.InitDatagramEncryption(secret1, secret2);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
///
/// This operation makes Photon call your custom web-service by path/name with the given parameters (converted into Json).
///
///
/// A WebRPC calls a custom, http-based function on a server you provide. The uriPath is relative to a "base path"
/// which is configured server-side. The sent parameters get converted from C# types to Json. Vice versa, the response
/// of the web-service will be converted to C# types and sent back as normal operation response.
///
///
/// To use this feature, you have to setup your server:
///
/// For a Photon Cloud application,
/// visit the Dashboard and setup "WebHooks". The BaseUrl is used for WebRPCs as well.
///
///
/// The response by Photon will call OnOperationResponse() with Code: OperationCode.WebRpc.
/// To get this response, you can derive the LoadBalancingClient, or (much easier) you set a suitable
/// OnOpResponseAction to be called.
///
///
/// It's important to understand that the OperationResponse tells you if the WebRPC could be called or not
/// but the content of the response will contain the values the web-service sent (if any).
/// If the web-service could not execute the request, it might return another error and a message. This is
/// inside the OperationResponse.
///
/// The class WebRpcResponse is a helper-class that extracts the most valuable content from the WebRPC
/// response.
///
///
/// To get a WebRPC response, set a OnOpResponseAction:
///
/// this.OnOpResponseAction = this.OpResponseHandler;
///
/// It could look like this:
///
/// public void OpResponseHandler(OperationResponse operationResponse)
/// {
/// if (operationResponse.OperationCode == OperationCode.WebRpc)
/// {
/// if (operationResponse.ReturnCode != 0)
/// {
/// Console.WriteLine("WebRpc failed. Response: " + operationResponse.ToStringFull());
/// }
/// else
/// {
/// WebRpcResponse webResponse = new WebRpcResponse(operationResponse);
/// Console.WriteLine(webResponse.DebugMessage); // message from the webserver
///
/// // do something with the response...
/// }
/// }
/// }
///
/// The url path to call, relative to the baseUrl configured on Photon's server-side.
/// The parameters to send to the web-service method.
/// Defines if the authentication cookie gets sent to a WebHook (if setup).
public bool OpWebRpc(string uriPath, object parameters, bool sendAuthCookie = false)
{
Dictionary opParameters = new Dictionary();
opParameters.Add(ParameterCode.UriPath, uriPath);
opParameters.Add(ParameterCode.WebRpcParameters, parameters);
if (sendAuthCookie)
{
opParameters.Add(ParameterCode.EventForward, WebFlags.SendAuthCookieConst);
}
return this.loadBalancingPeer.OpCustom(OperationCode.WebRpc, opParameters, true);
}
}
}