您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
4196 行
199 KiB
4196 行
199 KiB
// -----------------------------------------------------------------------
|
|
// <copyright file="LoadBalancingClient.cs" company="Exit Games GmbH">
|
|
// Loadbalancing Framework for Photon - Copyright (C) 2018 Exit Games GmbH
|
|
// </copyright>
|
|
// <summary>
|
|
// Provides the operations and a state for games using the
|
|
// Photon LoadBalancing server.
|
|
// </summary>
|
|
// <author>developer@photonengine.com</author>
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#if UNITY_4_7 || UNITY_5 || UNITY_5_3_OR_NEWER
|
|
#define SUPPORTED_UNITY
|
|
#endif
|
|
|
|
|
|
namespace Photon.Realtime
|
|
{
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using ExitGames.Client.Photon;
|
|
|
|
#if SUPPORTED_UNITY
|
|
using UnityEngine;
|
|
using Debug = UnityEngine.Debug;
|
|
#endif
|
|
#if SUPPORTED_UNITY || NETFX_CORE
|
|
using Hashtable = ExitGames.Client.Photon.Hashtable;
|
|
using SupportClass = ExitGames.Client.Photon.SupportClass;
|
|
#endif
|
|
|
|
|
|
#region Enums
|
|
|
|
/// <summary>
|
|
/// State values for a client, which handles switching Photon server types, some operations, etc.
|
|
/// </summary>
|
|
/// \ingroup publicApi
|
|
public enum ClientState
|
|
{
|
|
/// <summary>Peer is created but not used yet.</summary>
|
|
PeerCreated,
|
|
|
|
/// <summary>Transition state while connecting to a server. On the Photon Cloud this sends the AppId and AuthenticationValues (UserID).</summary>
|
|
Authenticating,
|
|
|
|
/// <summary>Not Used.</summary>
|
|
Authenticated,
|
|
|
|
/// <summary>The client sent an OpJoinLobby and if this was done on the Master Server, it will result in. Depending on the lobby, it gets room listings.</summary>
|
|
JoiningLobby,
|
|
|
|
/// <summary>The client is in a lobby, connected to the MasterServer. Depending on the lobby, it gets room listings.</summary>
|
|
JoinedLobby,
|
|
|
|
/// <summary>Transition from MasterServer to GameServer.</summary>
|
|
DisconnectingFromMasterServer,
|
|
[Obsolete("Renamed to DisconnectingFromMasterServer")]
|
|
DisconnectingFromMasterserver = DisconnectingFromMasterServer,
|
|
|
|
/// <summary>Transition to GameServer (client authenticates and joins/creates a room).</summary>
|
|
ConnectingToGameServer,
|
|
[Obsolete("Renamed to ConnectingToGameServer")]
|
|
ConnectingToGameserver = ConnectingToGameServer,
|
|
|
|
/// <summary>Connected to GameServer (going to auth and join game).</summary>
|
|
ConnectedToGameServer,
|
|
[Obsolete("Renamed to ConnectedToGameServer")]
|
|
ConnectedToGameserver = ConnectedToGameServer,
|
|
|
|
/// <summary>Transition state while joining or creating a room on GameServer.</summary>
|
|
Joining,
|
|
|
|
/// <summary>The client entered a room. The CurrentRoom and Players are known and you can now raise events.</summary>
|
|
Joined,
|
|
|
|
/// <summary>Transition state when leaving a room.</summary>
|
|
Leaving,
|
|
|
|
/// <summary>Transition from GameServer to MasterServer (after leaving a room/game).</summary>
|
|
DisconnectingFromGameServer,
|
|
[Obsolete("Renamed to DisconnectingFromGameServer")]
|
|
DisconnectingFromGameserver = DisconnectingFromGameServer,
|
|
|
|
/// <summary>Connecting to MasterServer (includes sending authentication values).</summary>
|
|
ConnectingToMasterServer,
|
|
[Obsolete("Renamed to ConnectingToMasterServer.")]
|
|
ConnectingToMasterserver = ConnectingToMasterServer,
|
|
|
|
/// <summary>The client disconnects (from any server). This leads to state Disconnected.</summary>
|
|
Disconnecting,
|
|
|
|
/// <summary>The client is no longer connected (to any server). Connect to MasterServer to go on.</summary>
|
|
Disconnected,
|
|
|
|
/// <summary>Connected to MasterServer. You might use matchmaking or join a lobby now.</summary>
|
|
ConnectedToMasterServer,
|
|
[Obsolete("Renamed to ConnectedToMasterServer.")]
|
|
ConnectedToMasterserver = ConnectedToMasterServer,
|
|
[Obsolete("Renamed to ConnectedToMasterServer.")]
|
|
ConnectedToMaster = ConnectedToMasterServer,
|
|
|
|
/// <summary>Client connects to the NameServer. This process includes low level connecting and setting up encryption. When done, state becomes ConnectedToNameServer.</summary>
|
|
ConnectingToNameServer,
|
|
|
|
/// <summary>Client is connected to the NameServer and established encryption already. You should call OpGetRegions or ConnectToRegionMaster.</summary>
|
|
ConnectedToNameServer,
|
|
|
|
/// <summary>Clients disconnects (specifically) from the NameServer (usually to connect to the MasterServer).</summary>
|
|
DisconnectingFromNameServer,
|
|
|
|
/// <summary>Client was unable to connect to Name Server and will attempt to connect with an alternative network protocol (TCP).</summary>
|
|
ConnectWithFallbackProtocol
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Internal state, how this peer gets into a particular room (joining it or creating it).
|
|
/// </summary>
|
|
internal enum JoinType
|
|
{
|
|
/// <summary>This client creates a room, gets into it (no need to join) and can set room properties.</summary>
|
|
CreateRoom,
|
|
/// <summary>The room existed already and we join into it (not setting room properties).</summary>
|
|
JoinRoom,
|
|
/// <summary>Done on Master Server and (if successful) followed by a Join on Game Server.</summary>
|
|
JoinRandomRoom,
|
|
/// <summary>Done on Master Server and (if successful) followed by a Join or Create on Game Server.</summary>
|
|
JoinRandomOrCreateRoom,
|
|
/// <summary>Client is either joining or creating a room. On Master- and Game-Server.</summary>
|
|
JoinOrCreateRoom
|
|
}
|
|
|
|
/// <summary>Enumeration of causes for Disconnects (used in LoadBalancingClient.DisconnectedCause).</summary>
|
|
/// <remarks>Read the individual descriptions to find out what to do about this type of disconnect.</remarks>
|
|
public enum DisconnectCause
|
|
{
|
|
/// <summary>No error was tracked.</summary>
|
|
None,
|
|
/// <summary>OnStatusChanged: The server is not available or the address is wrong. Make sure the port is provided and the server is up.</summary>
|
|
ExceptionOnConnect,
|
|
/// <summary>OnStatusChanged: Some internal exception caused the socket code to fail. This may happen if you attempt to connect locally but the server is not available. In doubt: Contact Exit Games.</summary>
|
|
Exception,
|
|
|
|
/// <summary>OnStatusChanged: The server disconnected this client due to timing out (missing acknowledgement from the client).</summary>
|
|
ServerTimeout,
|
|
|
|
/// <summary>OnStatusChanged: This client detected that the server's responses are not received in due time.</summary>
|
|
ClientTimeout,
|
|
|
|
/// <summary>OnStatusChanged: The server disconnected this client from within the room's logic (the C# code).</summary>
|
|
DisconnectByServerLogic,
|
|
/// <summary>OnStatusChanged: The server disconnected this client for unknown reasons.</summary>
|
|
DisconnectByServerReasonUnknown,
|
|
|
|
/// <summary>OnOperationResponse: Authenticate in the Photon Cloud with invalid AppId. Update your subscription or contact Exit Games.</summary>
|
|
InvalidAuthentication,
|
|
/// <summary>OnOperationResponse: Authenticate in the Photon Cloud with invalid client values or custom authentication setup in Cloud Dashboard.</summary>
|
|
CustomAuthenticationFailed,
|
|
/// <summary>The authentication ticket should provide access to any Photon Cloud server without doing another authentication-service call. However, the ticket expired.</summary>
|
|
AuthenticationTicketExpired,
|
|
/// <summary>OnOperationResponse: Authenticate (temporarily) failed when using a Photon Cloud subscription without CCU Burst. Update your subscription.</summary>
|
|
MaxCcuReached,
|
|
|
|
/// <summary>OnOperationResponse: Authenticate when the app's Photon Cloud subscription is locked to some (other) region(s). Update your subscription or master server address.</summary>
|
|
InvalidRegion,
|
|
|
|
/// <summary>OnOperationResponse: Operation that's (currently) not available for this client (not authorized usually). Only tracked for op Authenticate.</summary>
|
|
OperationNotAllowedInCurrentState,
|
|
/// <summary>OnStatusChanged: The client disconnected from within the logic (the C# code).</summary>
|
|
DisconnectByClientLogic
|
|
}
|
|
|
|
/// <summary>Available server (types) for internally used field: server.</summary>
|
|
/// <remarks>Photon uses 3 different roles of servers: Name Server, Master Server and Game Server.</remarks>
|
|
public enum ServerConnection
|
|
{
|
|
/// <summary>This server is where matchmaking gets done and where clients can get lists of rooms in lobbies.</summary>
|
|
MasterServer,
|
|
/// <summary>This server handles a number of rooms to execute and relay the messages between players (in a room).</summary>
|
|
GameServer,
|
|
/// <summary>This server is used initially to get the address (IP) of a Master Server for a specific region. Not used for Photon OnPremise (self hosted).</summary>
|
|
NameServer
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines how the communication gets encrypted.
|
|
/// </summary>
|
|
public enum EncryptionMode
|
|
{
|
|
/// <summary>
|
|
/// This is the default encryption mode: Messages get encrypted only on demand (when you send operations with the "encrypt" parameter set to true).
|
|
/// </summary>
|
|
PayloadEncryption,
|
|
/// <summary>
|
|
/// With this encryption mode for UDP, the connection gets setup and all further datagrams get encrypted almost entirely. On-demand message encryption (like in PayloadEncryption) is unavailable.
|
|
/// </summary>
|
|
DatagramEncryption = 10,
|
|
/// <summary>
|
|
/// With this encryption mode for UDP, the connection gets setup with random sequence numbers and all further datagrams get encrypted almost entirely. On-demand message encryption (like in PayloadEncryption) is unavailable.
|
|
/// </summary>
|
|
DatagramEncryptionRandomSequence = 11,
|
|
/// <summary>
|
|
/// Same as above except that GCM mode is used to encrypt data
|
|
/// </summary>
|
|
DatagramEncryptionGCMRandomSequence = 12,
|
|
}
|
|
|
|
|
|
public static class EncryptionDataParameters
|
|
{
|
|
/// <summary>
|
|
/// Key for encryption mode
|
|
/// </summary>
|
|
public const byte Mode = 0;
|
|
/// <summary>
|
|
/// Key for first secret
|
|
/// </summary>
|
|
public const byte Secret1 = 1;
|
|
/// <summary>
|
|
/// Key for second secret
|
|
/// </summary>
|
|
public const byte Secret2 = 2;
|
|
}
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
public class LoadBalancingClient : IPhotonPeerListener
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public LoadBalancingPeer LoadBalancingPeer { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the binary protocol version used by this client
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Use this always instead of setting it via <see cref="LoadBalancingClient.LoadBalancingPeer"/>
|
|
/// (<see cref="PhotonPeer.SerializationProtocolType"/>) directly, especially when WSS protocol is used.
|
|
/// </remarks>
|
|
public SerializationProtocol SerializationProtocol
|
|
{
|
|
get
|
|
{
|
|
return this.LoadBalancingPeer.SerializationProtocolType;
|
|
}
|
|
set
|
|
{
|
|
this.LoadBalancingPeer.SerializationProtocolType = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>The version of your client. A new version also creates a new "virtual app" to separate players from older client versions.</summary>
|
|
public string AppVersion { get; set; }
|
|
|
|
/// <summary>The AppID as assigned from the Photon Cloud. If you host yourself, this is the "regular" Photon Server Application Name (most likely: "LoadBalancing").</summary>
|
|
public string AppId { get; set; }
|
|
|
|
|
|
/// <summary>User authentication values to be sent to the Photon server right after connecting.</summary>
|
|
/// <remarks>Set this property or pass AuthenticationValues by Connect(..., authValues).</remarks>
|
|
public AuthenticationValues AuthValues { get; set; }
|
|
|
|
/// <summary>Enables the new Authentication workflow.</summary>
|
|
public AuthModeOption AuthMode = AuthModeOption.Auth;
|
|
|
|
/// <summary>Defines how the communication gets encrypted.</summary>
|
|
public EncryptionMode EncryptionMode = EncryptionMode.PayloadEncryption;
|
|
|
|
/// <summary>Optionally contains a protocol which will be used on Master- and GameServer. </summary>
|
|
/// <remarks>
|
|
/// When using AuthMode = AuthModeOption.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.
|
|
///
|
|
/// This is nullable by design. In many cases, the protocol on the NameServer is not different from the other servers.
|
|
/// If set, the operation AuthOnce will contain this value and the OpAuth response on the NameServer will execute a protocol switch.
|
|
/// </remarks>
|
|
public ConnectionProtocol? ExpectedProtocol { get; private set; }
|
|
|
|
|
|
///<summary>Simplifies getting the token for connect/init requests, if this feature is enabled.</summary>
|
|
private string TokenForInit
|
|
{
|
|
get
|
|
{
|
|
if (this.AuthMode == AuthModeOption.Auth)
|
|
{
|
|
return null;
|
|
}
|
|
return (this.AuthValues != null) ? this.AuthValues.Token : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>Internally used cache for the server's token. Identifies a user/session and can be used to rejoin.</summary>
|
|
private string tokenCache;
|
|
|
|
|
|
/// <summary>True if this client uses a NameServer to get the Master Server address.</summary>
|
|
/// <remarks>This value is public, despite being an internal value, which should only be set by this client.</remarks>
|
|
public bool IsUsingNameServer { get; set; }
|
|
|
|
/// <summary>Name Server Host Name for Photon Cloud. Without port and without any prefix.</summary>
|
|
public string NameServerHost = "ns.exitgames.com";
|
|
|
|
/// <summary>Name Server for HTTP connections to the Photon Cloud. Includes prefix and port.</summary>
|
|
public string NameServerHttp = "http://ns.exitgames.com:80/photon/n";
|
|
|
|
/// <summary>Name Server Address for Photon Cloud (based on current protocol). You can use the default values and usually won't have to set this value.</summary>
|
|
public string NameServerAddress { get { return this.GetNameServerAddress(); } }
|
|
|
|
/// <summary>Name Server port per protocol (the UDP port is different than TCP, etc).</summary>
|
|
private static readonly Dictionary<ConnectionProtocol, int> ProtocolToNameServerPort = new Dictionary<ConnectionProtocol, int>() { { ConnectionProtocol.Udp, 5058 }, { ConnectionProtocol.Tcp, 4533 }, { ConnectionProtocol.WebSocket, 9093 }, { ConnectionProtocol.WebSocketSecure, 19093 } }; //, { ConnectionProtocol.RHttp, 6063 } };
|
|
|
|
/// <summary>Use the alternative ports for UDP connections in the Public Cloud (27000 to 27003).</summary>
|
|
/// <remarks>
|
|
/// This should be used when players have issues with connection stability.
|
|
/// Some players reported better connectivity for Steam games.
|
|
/// The effect might vary, which is why the alternative ports are not the new default.
|
|
///
|
|
/// The alternative (server) ports are 27000 up to 27003.
|
|
///
|
|
/// The values are appplied by replacing any incoming server-address string accordingly.
|
|
/// You only need to set this to true though.
|
|
///
|
|
/// This value does not affect TCP or WebSocket connections.
|
|
/// </remarks>
|
|
public bool UseAlternativeUdpPorts { get; set; }
|
|
|
|
/// <summary>Enables a fallback to another protocol in case a connect to the Name Server fails.</summary>
|
|
/// <remarks>
|
|
/// When connecting to the Name Server fails for a first time, the client will automatically select a different
|
|
/// network protocol and try connecting once more.
|
|
///
|
|
/// The fallback will use the default Name Server port as defined by ProtocolToNameServerPort.
|
|
///
|
|
/// The fallback for TCP is UDP. All other protocols fallback to TCP.
|
|
/// </remarks>
|
|
public bool EnableProtocolFallback { get; set; }
|
|
|
|
/// <summary>The currently used server address (if any). The type of server is define by Server property.</summary>
|
|
public string CurrentServerAddress { get { return this.LoadBalancingPeer.ServerAddress; } }
|
|
|
|
/// <summary>Your Master Server address. In PhotonCloud, call ConnectToRegionMaster() to find your Master Server.</summary>
|
|
/// <remarks>
|
|
/// 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).
|
|
/// </remarks>
|
|
public string MasterServerAddress { get; set; }
|
|
|
|
/// <summary>The game server's address for a particular room. In use temporarily, as assigned by master.</summary>
|
|
public string GameServerAddress { get; protected internal set; }
|
|
|
|
/// <summary>The server this client is currently connected or connecting to.</summary>
|
|
/// <remarks>
|
|
/// Each server (NameServer, MasterServer, GameServer) allow some operations and reject others.
|
|
/// </remarks>
|
|
public ServerConnection Server { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Defines a proxy URL for WebSocket connections. Can be the proxy or point to a .pac file.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This URL supports various definitions:
|
|
///
|
|
/// "user:pass@proxyaddress:port"<br/>
|
|
/// "proxyaddress:port"<br/>
|
|
/// "system:"<br/>
|
|
/// "pac:"<br/>
|
|
/// "pac:http://host/path/pacfile.pac"<br/>
|
|
///
|
|
/// Important: Don't define a protocol, except to point to a pac file. the proxy address should not begin with http:// or https://.
|
|
/// </remarks>
|
|
public string ProxyServerAddress;
|
|
|
|
/// <summary>Backing field for property.</summary>
|
|
private ClientState state = ClientState.PeerCreated;
|
|
|
|
/// <summary>Current state this client is in. Careful: several states are "transitions" that lead to other states.</summary>
|
|
public ClientState State
|
|
{
|
|
get
|
|
{
|
|
return this.state;
|
|
}
|
|
|
|
set
|
|
{
|
|
if (this.state == value)
|
|
{
|
|
return;
|
|
}
|
|
ClientState previousState = this.state;
|
|
this.state = value;
|
|
if (StateChanged != null) StateChanged(previousState, this.state);
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns if this client is currently connected or connecting to some type of server.</summary>
|
|
/// <remarks>This is even true while switching servers. Use IsConnectedAndReady to check only for those states that enable you to send Operations.</remarks>
|
|
public bool IsConnected { get { return this.LoadBalancingPeer != null && this.State != ClientState.PeerCreated && this.State != ClientState.Disconnected; } }
|
|
|
|
|
|
/// <summary>
|
|
/// A refined version of IsConnected which is true only if your connection is ready to send operations.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Not all operations can be called on all types of servers. If an operation is unavailable on the currently connected server,
|
|
/// this will result in a OperationResponse with ErrorCode != 0.
|
|
///
|
|
/// Examples: 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).
|
|
///
|
|
/// To check which server you are on, use: <see cref="Server"/>.
|
|
/// </remarks>
|
|
public bool IsConnectedAndReady
|
|
{
|
|
get
|
|
{
|
|
if (this.LoadBalancingPeer == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
switch (this.State)
|
|
{
|
|
case ClientState.PeerCreated:
|
|
case ClientState.Disconnected:
|
|
case ClientState.Disconnecting:
|
|
case ClientState.DisconnectingFromGameServer:
|
|
case ClientState.DisconnectingFromMasterServer:
|
|
case ClientState.DisconnectingFromNameServer:
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>Register a method to be called when this client's ClientState gets set.</summary>
|
|
/// <remarks>This can be useful to react to being connected, joined into a room, etc.</remarks>
|
|
public event Action<ClientState, ClientState> StateChanged;
|
|
|
|
/// <summary>Register a method to be called when an event got dispatched. Gets called after the LoadBalancingClient handled the internal events first.</summary>
|
|
/// <remarks>
|
|
/// This is an alternative to extending LoadBalancingClient to override OnEvent().
|
|
///
|
|
/// Note that OnEvent is calling EventReceived after it handled internal events first.
|
|
/// That means for example: Joining players will already be in the player list but leaving
|
|
/// players will already be removed from the room.
|
|
/// </remarks>
|
|
public event Action<EventData> EventReceived;
|
|
|
|
/// <summary>Register a method to be called when an operation response is received.</summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
public event Action<OperationResponse> OpResponseReceived;
|
|
|
|
|
|
/// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
|
|
/// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
|
|
public ConnectionCallbacksContainer ConnectionCallbackTargets;
|
|
|
|
/// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
|
|
/// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
|
|
public MatchMakingCallbacksContainer MatchMakingCallbackTargets;
|
|
|
|
/// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
|
|
/// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
|
|
internal InRoomCallbacksContainer InRoomCallbackTargets;
|
|
|
|
/// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
|
|
/// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
|
|
internal LobbyCallbacksContainer LobbyCallbackTargets;
|
|
|
|
/// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
|
|
/// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
|
|
internal WebRpcCallbacksContainer WebRpcCallbackTargets;
|
|
|
|
|
|
/// <summary>Wraps up the target objects for a group of callbacks, so they can be called conveniently.</summary>
|
|
/// <remarks>By using Add or Remove, objects can "subscribe" or "unsubscribe" for this group of callbacks.</remarks>
|
|
internal ErrorInfoCallbacksContainer ErrorInfoCallbackTargets;
|
|
|
|
/// <summary>Summarizes (aggregates) the different causes for disconnects of a client.</summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
public DisconnectCause DisconnectedCause { get; protected set; }
|
|
|
|
|
|
/// <summary>Internal value if the client is in a lobby.</summary>
|
|
/// <remarks>This is used to re-set this.State, when joining/creating a room fails.</remarks>
|
|
public bool InLobby
|
|
{
|
|
get { return this.State == ClientState.JoinedLobby; }
|
|
}
|
|
|
|
/// <summary>The lobby this client currently uses. Defined when joining a lobby or creating rooms</summary>
|
|
public TypedLobby CurrentLobby { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// If enabled, the client will get a list of available lobbies from the Master Server.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Set this value before the client connects to the Master Server. While connected to the Master
|
|
/// Server, a change has no effect.
|
|
///
|
|
/// Implement OptionalInfoCallbacks.OnLobbyStatisticsUpdate, to get the list of used lobbies.
|
|
///
|
|
/// The lobby statistics can be useful if your title dynamically uses lobbies, depending (e.g.)
|
|
/// on current player activity or such.
|
|
/// In this case, getting a list of available lobbies, their room-count and player-count can
|
|
/// be useful info.
|
|
///
|
|
/// ConnectUsingSettings sets this to the PhotonServerSettings value.
|
|
/// </remarks>
|
|
public bool EnableLobbyStatistics;
|
|
|
|
/// <summary>Internal lobby stats cache, used by LobbyStatistics.</summary>
|
|
private readonly List<TypedLobbyInfo> lobbyStatistics = new List<TypedLobbyInfo>();
|
|
|
|
|
|
/// <summary>The local player is never null but not valid unless the client is in a room, too. The ID will be -1 outside of rooms.</summary>
|
|
public Player LocalPlayer { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// The nickname of the player (synced with others). Same as client.LocalPlayer.NickName.
|
|
/// </summary>
|
|
public string NickName
|
|
{
|
|
get
|
|
{
|
|
return this.LocalPlayer.NickName;
|
|
}
|
|
|
|
set
|
|
{
|
|
if (this.LocalPlayer == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
this.LocalPlayer.NickName = value;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>An ID for this user. Sent in OpAuthenticate when you connect. If not set, the PlayerName is applied during connect.</summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>The current room this client is connected to (null if none available).</summary>
|
|
public Room CurrentRoom { get; set; }
|
|
|
|
|
|
/// <summary>Is true while being in a room (this.state == ClientState.Joined).</summary>
|
|
/// <remarks>
|
|
/// Aside from polling this value, game logic should implement IMatchmakingCallbacks in some class
|
|
/// and react when that gets called.<br/>
|
|
/// OpRaiseEvent, OpLeave and some other operations can only be used (successfully) when the client is in a room..
|
|
/// </remarks>
|
|
public bool InRoom
|
|
{
|
|
get
|
|
{
|
|
return this.state == ClientState.Joined && this.CurrentRoom != null;
|
|
}
|
|
}
|
|
|
|
/// <summary>Statistic value available on master server: Players on master (looking for games).</summary>
|
|
public int PlayersOnMasterCount { get; internal set; }
|
|
|
|
/// <summary>Statistic value available on master server: Players in rooms (playing).</summary>
|
|
public int PlayersInRoomsCount { get; internal set; }
|
|
|
|
/// <summary>Statistic value available on master server: Rooms currently created.</summary>
|
|
public int RoomsCount { get; internal set; }
|
|
|
|
|
|
/// <summary>Internally used to decide if a room must be created or joined on game server.</summary>
|
|
private JoinType lastJoinType;
|
|
|
|
/// <summary>Used when the client arrives on the GS, to join the room with the correct values.</summary>
|
|
private EnterRoomParams enterRoomParamsCache;
|
|
|
|
/// <summary>Used to cache a failed "enter room" operation on the Game Server, to return to the Master Server before calling a fail-callback.</summary>
|
|
private OperationResponse failedRoomEntryOperation;
|
|
|
|
|
|
/// <summary>Maximum of userIDs that can be sent in one friend list request.</summary>
|
|
private const int FriendRequestListMax = 512;
|
|
|
|
/// <summary>Contains the list of names of friends to look up their state on the server.</summary>
|
|
private string[] friendListRequested;
|
|
|
|
/// <summary>Internal flag to know if the client currently fetches a friend list.</summary>
|
|
public bool IsFetchingFriendList { get { return this.friendListRequested != null; } }
|
|
|
|
|
|
/// <summary>The cloud region this client connects to. Set by ConnectToRegionMaster(). Not set if you don't use a NameServer!</summary>
|
|
public string CloudRegion { get; private set; }
|
|
|
|
/// <summary>The cluster name provided by the Name Server.</summary>
|
|
/// <remarks>
|
|
/// The value is provided by the OpResponse for OpAuthenticate/OpAuthenticateOnce.
|
|
/// Default: null. This value only ever updates from the Name Server authenticate response.
|
|
/// </remarks>
|
|
public string CurrentCluster { get; private set; }
|
|
|
|
/// <summary>Contains the list if enabled regions this client may use. Null, unless the client got a response to OpGetRegions.</summary>
|
|
public RegionHandler RegionHandler;
|
|
|
|
/// <summary>Stores the best region summary of a previous session to speed up connecting.</summary>
|
|
private string bestRegionSummaryFromStorage;
|
|
|
|
/// <summary>Set when the best region pinging is done.</summary>
|
|
public string SummaryToCache;
|
|
|
|
/// <summary>Internal connection setting/flag. If the client should connect to the best region or not.</summary>
|
|
/// <remarks>
|
|
/// It's set in the Connect...() methods. Only ConnectUsingSettings() sets it to true.
|
|
/// If true, client will ping available regions and select the best.
|
|
/// A bestRegionSummaryFromStorage can be used to cut the ping time short.
|
|
/// </remarks>
|
|
private bool connectToBestRegion = true;
|
|
|
|
|
|
|
|
private class CallbackTargetChange
|
|
{
|
|
public readonly object Target;
|
|
/// <summary>Add if true, remove if false.</summary>
|
|
public readonly bool AddTarget;
|
|
|
|
public CallbackTargetChange(object target, bool addTarget)
|
|
{
|
|
this.Target = target;
|
|
this.AddTarget = addTarget;
|
|
}
|
|
}
|
|
|
|
private readonly Queue<CallbackTargetChange> callbackTargetChanges = new Queue<CallbackTargetChange>();
|
|
private readonly HashSet<object> callbackTargets = new HashSet<object>();
|
|
|
|
|
|
/// <summary>Creates a LoadBalancingClient with UDP protocol or the one specified.</summary>
|
|
/// <param name="protocol">Specifies the network protocol to use for connections.</param>
|
|
public LoadBalancingClient(ConnectionProtocol protocol = ConnectionProtocol.Udp)
|
|
{
|
|
this.ConnectionCallbackTargets = new ConnectionCallbacksContainer(this);
|
|
this.MatchMakingCallbackTargets = new MatchMakingCallbacksContainer(this);
|
|
this.InRoomCallbackTargets = new InRoomCallbacksContainer(this);
|
|
this.LobbyCallbackTargets = new LobbyCallbacksContainer(this);
|
|
this.WebRpcCallbackTargets = new WebRpcCallbacksContainer(this);
|
|
this.ErrorInfoCallbackTargets = new ErrorInfoCallbacksContainer(this);
|
|
|
|
this.LoadBalancingPeer = new LoadBalancingPeer(this, protocol);
|
|
this.SerializationProtocol = SerializationProtocol.GpBinaryV18;
|
|
this.LocalPlayer = this.CreatePlayer(string.Empty, -1, true, null); //TODO: Check if we can do this later
|
|
|
|
#if UNITY_WEBGL
|
|
if (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Tcp || this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Udp)
|
|
{
|
|
this.LoadBalancingPeer.Listener.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
|
|
this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
|
|
}
|
|
#endif
|
|
|
|
this.State = ClientState.PeerCreated;
|
|
}
|
|
|
|
|
|
/// <summary>Creates a LoadBalancingClient, setting various values needed before connecting.</summary>
|
|
/// <param name="masterAddress">The Master Server's address to connect to. Used in Connect.</param>
|
|
/// <param name="appId">The AppId of this title. Needed for the Photon Cloud. Find it in the Dashboard.</param>
|
|
/// <param name="gameVersion">A version for this client/build. In the Photon Cloud, players are separated by AppId, GameVersion and Region.</param>
|
|
/// <param name="protocol">Specifies the network protocol to use for connections.</param>
|
|
public LoadBalancingClient(string masterAddress, string appId, string gameVersion, ConnectionProtocol protocol = ConnectionProtocol.Udp) : this(protocol)
|
|
{
|
|
this.MasterServerAddress = masterAddress;
|
|
this.AppId = appId;
|
|
this.AppVersion = gameVersion;
|
|
}
|
|
|
|
public int NameServerPortOverride;
|
|
|
|
/// <summary>
|
|
/// Gets the NameServer Address (with prefix and port), based on the set protocol (this.LoadBalancingPeer.UsedProtocol).
|
|
/// </summary>
|
|
/// <returns>NameServer Address (with prefix and port).</returns>
|
|
private string GetNameServerAddress()
|
|
{
|
|
var protocolPort = 0;
|
|
ProtocolToNameServerPort.TryGetValue(this.LoadBalancingPeer.TransportProtocol, out protocolPort);
|
|
if (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Udp && this.UseAlternativeUdpPorts)
|
|
{
|
|
protocolPort = 27000;
|
|
}
|
|
|
|
if (this.NameServerPortOverride != 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.INFO, string.Format("Using NameServerPortOverride: {0}", this.NameServerPortOverride));
|
|
protocolPort = this.NameServerPortOverride;
|
|
}
|
|
|
|
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
|
|
|
|
|
|
// needed connect variants:
|
|
// connect to Name Server only (could include getregions) -> end after getregions
|
|
// connect to Region Master via Name Server (specific region/cluster) -> no getregions! authenticates and ends after on connected to master
|
|
// connect to Best Region via Name Server
|
|
// connect to Master Server (no Name Server, no appid)
|
|
|
|
public virtual bool ConnectUsingSettings(AppSettings appSettings)
|
|
{
|
|
if (appSettings == null)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "ConnectUsingSettings failed. The appSettings can't be null.'");
|
|
return false;
|
|
}
|
|
|
|
this.AppId = appSettings.AppIdRealtime;
|
|
this.AppVersion = appSettings.AppVersion;
|
|
|
|
this.IsUsingNameServer = appSettings.UseNameServer;
|
|
this.CloudRegion = appSettings.FixedRegion;
|
|
|
|
this.EnableLobbyStatistics = appSettings.EnableLobbyStatistics;
|
|
this.LoadBalancingPeer.DebugOut = appSettings.NetworkLogging;
|
|
|
|
this.AuthMode = appSettings.AuthMode;
|
|
this.LoadBalancingPeer.TransportProtocol = (this.AuthMode == AuthModeOption.AuthOnceWss) ? ConnectionProtocol.WebSocketSecure : appSettings.Protocol;
|
|
this.ExpectedProtocol = appSettings.Protocol;
|
|
this.EnableProtocolFallback = appSettings.EnableProtocolFallback;
|
|
|
|
this.connectToBestRegion = true;
|
|
this.bestRegionSummaryFromStorage = appSettings.BestRegionSummaryFromStorage;
|
|
this.DisconnectedCause = DisconnectCause.None;
|
|
|
|
|
|
this.CheckConnectSetupWebGl();
|
|
this.CheckConnectSetupXboxOne(); // may throw an exception if there are issues that can not be corrected
|
|
|
|
|
|
if (this.IsUsingNameServer)
|
|
{
|
|
this.Server = ServerConnection.NameServer;
|
|
if (!appSettings.IsDefaultNameServer)
|
|
{
|
|
this.NameServerHost = appSettings.Server;
|
|
}
|
|
|
|
this.ProxyServerAddress = appSettings.ProxyServer;
|
|
this.NameServerPortOverride = appSettings.Port;
|
|
if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
this.State = ClientState.ConnectingToNameServer;
|
|
}
|
|
else
|
|
{
|
|
this.Server = ServerConnection.MasterServer;
|
|
int portToUse = appSettings.IsDefaultPort ? 5055 : appSettings.Port; // TODO: setup new (default) port config
|
|
this.MasterServerAddress = string.Format("{0}:{1}", appSettings.Server, portToUse);
|
|
this.SerializationProtocol = SerializationProtocol.GpBinaryV16; // this is a workaround to use On Premises Servers, which don't support GpBinaryV18 yet.
|
|
if (!this.LoadBalancingPeer.Connect(this.MasterServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
this.State = ClientState.ConnectingToMasterServer;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
[Obsolete("Use ConnectToMasterServer() instead.")]
|
|
public bool Connect()
|
|
{
|
|
return this.ConnectToMasterServer();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the "process" to connect to a Master Server, using MasterServerAddress and AppId properties.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// To connect to the Photon Cloud, use ConnectUsingSettings() or ConnectToRegionMaster().
|
|
///
|
|
/// The process to connect includes several steps: the actual connecting, establishing encryption, authentification
|
|
/// (of app and optionally the user) and connecting to the MasterServer
|
|
///
|
|
/// 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)
|
|
/// </remarks>
|
|
public virtual bool ConnectToMasterServer()
|
|
{
|
|
// we check if try to connect to a self-hosted Photon Server
|
|
if (string.IsNullOrEmpty(this.AppId) || !this.IsUsingNameServer)
|
|
{
|
|
// this is a workaround to use with version v4.0.29.11263 or lower, which doesn't support GpBinaryV18 yet.
|
|
this.SerializationProtocol = SerializationProtocol.GpBinaryV16;
|
|
}
|
|
|
|
|
|
this.CheckConnectSetupWebGl();
|
|
this.CheckConnectSetupXboxOne(); // may throw an exception if there are issues that can not be corrected
|
|
|
|
|
|
this.connectToBestRegion = false;
|
|
this.DisconnectedCause = DisconnectCause.None;
|
|
if (this.LoadBalancingPeer.Connect(this.MasterServerAddress, this.ProxyServerAddress, this.AppId, this.TokenForInit))
|
|
{
|
|
this.State = ClientState.ConnectingToMasterServer;
|
|
this.Server = ServerConnection.MasterServer;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Connects to the NameServer for Photon Cloud, where a region and server list can be obtained.
|
|
/// </summary>
|
|
/// <see cref="OpGetRegions"/>
|
|
/// <returns>If the workflow was started or failed right away.</returns>
|
|
public bool ConnectToNameServer()
|
|
{
|
|
this.IsUsingNameServer = true;
|
|
this.CloudRegion = null;
|
|
|
|
|
|
this.CheckConnectSetupWebGl();
|
|
this.CheckConnectSetupXboxOne(); // may throw an exception if there are issues that can not be corrected
|
|
|
|
|
|
if (this.AuthMode == AuthModeOption.AuthOnceWss)
|
|
{
|
|
if (this.ExpectedProtocol == null)
|
|
{
|
|
this.ExpectedProtocol = this.LoadBalancingPeer.TransportProtocol;
|
|
}
|
|
this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
|
|
}
|
|
|
|
this.connectToBestRegion = false;
|
|
this.DisconnectedCause = DisconnectCause.None;
|
|
if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", this.TokenForInit))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
this.State = ClientState.ConnectingToNameServer;
|
|
this.Server = ServerConnection.NameServer;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Connects you to a specific region's Master Server, using the Name Server to find the IP.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the region is null or empty, no connection will be made.
|
|
/// If the region (code) provided is not available, the connection process will fail on the Name Server.
|
|
/// This method connects only to the region defined. No "Best Region" pinging will be done.
|
|
///
|
|
/// If the region string does not contain a "/", this means no specific cluster is requested.
|
|
/// To support "Sharding", the region gets a "/*" postfix in this case, to select a random cluster.
|
|
/// </remarks>
|
|
/// <returns>If the operation could be sent. If false, no operation was sent.</returns>
|
|
public bool ConnectToRegionMaster(string region)
|
|
{
|
|
if (string.IsNullOrEmpty(region))
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "ConnectToRegionMaster() failed. The region can not be null or empty.");
|
|
return false;
|
|
}
|
|
|
|
this.IsUsingNameServer = true;
|
|
|
|
if (this.State == ClientState.ConnectedToNameServer)
|
|
{
|
|
this.CloudRegion = region;
|
|
return this.CallAuthenticate();
|
|
}
|
|
|
|
this.LoadBalancingPeer.Disconnect();
|
|
|
|
if (!string.IsNullOrEmpty(region) && !region.Contains("/"))
|
|
{
|
|
region = region + "/*";
|
|
}
|
|
this.CloudRegion = region;
|
|
|
|
|
|
this.CheckConnectSetupWebGl();
|
|
this.CheckConnectSetupXboxOne(); // may throw an exception if there are issues that can not be corrected
|
|
|
|
|
|
if (this.AuthMode == AuthModeOption.AuthOnceWss)
|
|
{
|
|
if (this.ExpectedProtocol == null)
|
|
{
|
|
this.ExpectedProtocol = this.LoadBalancingPeer.TransportProtocol;
|
|
}
|
|
this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
|
|
}
|
|
|
|
this.connectToBestRegion = false;
|
|
this.DisconnectedCause = DisconnectCause.None;
|
|
if (!this.LoadBalancingPeer.Connect(this.NameServerAddress, this.ProxyServerAddress, "NameServer", null))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
this.State = ClientState.ConnectingToNameServer;
|
|
this.Server = ServerConnection.NameServer;
|
|
return true;
|
|
}
|
|
|
|
[Conditional("UNITY_WEBGL")]
|
|
private void CheckConnectSetupWebGl()
|
|
{
|
|
if (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocket && this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure)
|
|
{
|
|
this.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure.");
|
|
this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
|
|
}
|
|
|
|
this.EnableProtocolFallback = false; // no fallback on WebGL
|
|
}
|
|
|
|
[Conditional("UNITY_XBOXONE")]
|
|
private void CheckConnectSetupXboxOne()
|
|
{
|
|
this.AuthMode = AuthModeOption.Auth;
|
|
if (this.AuthValues == null)
|
|
{
|
|
UnityEngine.Debug.LogError("UNITY_XBOXONE builds must set AuthValues. Set this before calling any Connect method. Refer to the online docs for guidance.");
|
|
throw new Exception("UNITY_XBOXONE builds must set AuthValues.");
|
|
}
|
|
if (this.AuthValues.AuthPostData == null)
|
|
{
|
|
UnityEngine.Debug.LogError("UNITY_XBOXONE builds must use Photon's XBox Authentication and set the XSTS token by calling: PhotonNetwork.AuthValues.SetAuthPostData(xstsToken). Refer to the online docs for guidance.");
|
|
throw new Exception("UNITY_XBOXONE builds must use Photon's XBox Authentication.");
|
|
}
|
|
if (this.AuthValues.AuthType != CustomAuthenticationType.Xbox)
|
|
{
|
|
UnityEngine.Debug.LogWarning("UNITY_XBOXONE builds must use AuthValues.AuthType \"CustomAuthenticationType.Xbox\". PUN sets this value now. Refer to the online docs to avoid this warning.");
|
|
this.AuthValues.AuthType = CustomAuthenticationType.Xbox;
|
|
}
|
|
if (this.LoadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure)
|
|
{
|
|
UnityEngine.Debug.LogWarning("UNITY_XBOXONE builds must use WSS (Secure WebSockets) as Transport Protocol. Changing the protocol now.");
|
|
this.LoadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure;
|
|
}
|
|
|
|
this.EnableProtocolFallback = false; // no fallback on Xbox One
|
|
}
|
|
|
|
/// <summary>
|
|
/// Privately used only for reconnecting.
|
|
/// </summary>
|
|
private bool Connect(string serverAddress, string proxyServerAddress, ServerConnection serverType)
|
|
{
|
|
// TODO: Make sure app doesn't quit right now
|
|
|
|
if (this.State == ClientState.Disconnecting)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "Connect() failed. Can't connect while disconnecting (still). Current state: " + this.State);
|
|
return false;
|
|
}
|
|
|
|
|
|
// connect might fail, if the DNS name can't be resolved or if no network connection is available
|
|
this.DisconnectedCause = DisconnectCause.None;
|
|
bool connecting = this.LoadBalancingPeer.Connect(serverAddress, proxyServerAddress, this.AppId, this.TokenForInit);
|
|
|
|
if (connecting)
|
|
{
|
|
this.Server = serverType;
|
|
|
|
switch (serverType)
|
|
{
|
|
case ServerConnection.NameServer:
|
|
State = ClientState.ConnectingToNameServer;
|
|
break;
|
|
case ServerConnection.MasterServer:
|
|
State = ClientState.ConnectingToMasterServer;
|
|
break;
|
|
case ServerConnection.GameServer:
|
|
State = ClientState.ConnectingToGameServer;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return connecting;
|
|
}
|
|
|
|
|
|
/// <summary>Can be used to reconnect to the master server after a disconnect.</summary>
|
|
/// <remarks>Common use case: Press the Lock Button on a iOS device and you get disconnected immediately.</remarks>
|
|
public bool ReconnectToMaster()
|
|
{
|
|
if (this.AuthValues == null)
|
|
{
|
|
this.DebugReturn(DebugLevel.WARNING, "ReconnectToMaster() with AuthValues == null is not correct!");
|
|
this.AuthValues = new AuthenticationValues();
|
|
}
|
|
this.AuthValues.Token = this.tokenCache;
|
|
|
|
return this.Connect(this.MasterServerAddress, this.ProxyServerAddress, ServerConnection.MasterServer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can be used to return to a room quickly, by directly reconnecting to a game server to rejoin a room.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Rejoining room will not send any player properties. Instead client will receive up-to-date ones from server.
|
|
/// If you want to set new player properties, do it once rejoined.
|
|
/// </remarks>
|
|
/// <returns>False, if the conditions are not met. Then, this client does not attempt the ReconnectAndRejoin.</returns>
|
|
public bool ReconnectAndRejoin()
|
|
{
|
|
if (string.IsNullOrEmpty(this.GameServerAddress))
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "ReconnectAndRejoin() failed. It seems the client wasn't connected to a game server before (no address).");
|
|
return false;
|
|
}
|
|
if (this.enterRoomParamsCache == null)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "ReconnectAndRejoin() failed. It seems the client doesn't have any previous room to re-join.");
|
|
return false;
|
|
}
|
|
if (this.tokenCache == null)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "ReconnectAndRejoin() failed. It seems the client doesn't have any previous authentication token to re-connect.");
|
|
return false;
|
|
}
|
|
|
|
if (this.AuthValues == null)
|
|
{
|
|
this.AuthValues = new AuthenticationValues();
|
|
}
|
|
this.AuthValues.Token = this.tokenCache;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(this.GameServerAddress) && this.enterRoomParamsCache != null)
|
|
{
|
|
this.lastJoinType = JoinType.JoinRoom;
|
|
this.enterRoomParamsCache.RejoinOnly = true;
|
|
return this.Connect(this.GameServerAddress, this.ProxyServerAddress, ServerConnection.GameServer);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/// <summary>Disconnects this client from any server and sets this.State if the connection is successfuly closed.</summary>
|
|
public void Disconnect(DisconnectCause cause = DisconnectCause.DisconnectByClientLogic)
|
|
{
|
|
if (this.State != ClientState.Disconnected)
|
|
{
|
|
this.State = ClientState.Disconnecting;
|
|
this.DisconnectedCause = cause;
|
|
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;
|
|
//}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Private Disconnect variant that sets the state, too.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Useful to test loss of connection which will end in a client timeout. This modifies LoadBalancingPeer.NetworkSimulationSettings. Read remarks.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Use with care as this sets LoadBalancingPeer.IsSimulationEnabled.<br/>
|
|
/// Read LoadBalancingPeer.IsSimulationEnabled to check if this is on or off, if needed.<br/>
|
|
///
|
|
/// If simulateTimeout is true, LoadBalancingPeer.NetworkSimulationSettings.IncomingLossPercentage and
|
|
/// LoadBalancingPeer.NetworkSimulationSettings.OutgoingLossPercentage will be set to 100.<br/>
|
|
/// Obviously, this overrides any network simulation settings done before.<br/>
|
|
///
|
|
/// If you want fine-grained network simulation control, use the NetworkSimulationSettings.<br/>
|
|
///
|
|
/// The timeout will lead to a call to <see cref="IConnectionCallbacks.OnDisconnected"/>, as usual in a client timeout.
|
|
///
|
|
/// You could modify this method (or use NetworkSimulationSettings) to deliberately run into a server timeout by
|
|
/// just setting the OutgoingLossPercentage = 100 and the IncomingLossPercentage = 0.
|
|
/// </remarks>
|
|
/// <param name="simulateTimeout">If true, a connection loss is simulated. If false, the simulation ends.</param>
|
|
public void SimulateConnectionLoss(bool simulateTimeout)
|
|
{
|
|
this.DebugReturn(DebugLevel.WARNING, "SimulateConnectionLoss() set to: "+simulateTimeout);
|
|
|
|
if (simulateTimeout)
|
|
{
|
|
this.LoadBalancingPeer.NetworkSimulationSettings.IncomingLossPercentage = 100;
|
|
this.LoadBalancingPeer.NetworkSimulationSettings.OutgoingLossPercentage = 100;
|
|
}
|
|
|
|
this.LoadBalancingPeer.IsSimulationEnabled = simulateTimeout;
|
|
}
|
|
|
|
private bool CallAuthenticate()
|
|
{
|
|
if (this.AuthMode == AuthModeOption.Auth)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.Authenticate, this.Server, "Authenticate"))
|
|
{
|
|
return false;
|
|
}
|
|
return this.LoadBalancingPeer.OpAuthenticate(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, (this.EnableLobbyStatistics && this.Server == ServerConnection.MasterServer));
|
|
}
|
|
else
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.AuthenticateOnce, this.Server, "AuthenticateOnce"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ConnectionProtocol targetProtocolPastNameServer = this.ExpectedProtocol != null ? (ConnectionProtocol) this.ExpectedProtocol : this.LoadBalancingPeer.TransportProtocol;
|
|
return this.LoadBalancingPeer.OpAuthenticateOnce(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, this.EncryptionMode, targetProtocolPastNameServer);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// This method dispatches all available incoming commands and then sends this client's outgoing commands.
|
|
/// It uses DispatchIncomingCommands and SendOutgoingCommands to do that.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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 (10..50 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.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// You could replace Service by:
|
|
///
|
|
/// while (DispatchIncomingCommands()); //Dispatch until everything is Dispatched...
|
|
/// SendOutgoingCommands(); //Send a UDP/TCP package with outgoing messages
|
|
/// </example>
|
|
/// <seealso cref="PhotonPeer.DispatchIncomingCommands"/>
|
|
/// <seealso cref="PhotonPeer.SendOutgoingCommands"/>
|
|
public void Service()
|
|
{
|
|
if (this.LoadBalancingPeer != null)
|
|
{
|
|
this.LoadBalancingPeer.Service();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// While on the NameServer, this gets you the list of regional servers (short names and their IPs to ping them).
|
|
/// </summary>
|
|
/// <returns>If the operation could be sent. If false, no operation was sent (e.g. while not connected to the NameServer).</returns>
|
|
private bool OpGetRegions()
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.GetRegions, this.Server, "GetRegions"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool sent = this.LoadBalancingPeer.OpGetRegions(this.AppId);
|
|
return sent;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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:<br/>
|
|
/// The server response includes 2 arrays of info (each index matching a friend from the request):<br/>
|
|
/// ParameterCode.FindFriendsResponseOnlineList = bool[] of online states<br/>
|
|
/// ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room)<br/>
|
|
/// <br/>
|
|
/// The options may be used to define which state a room must match to be returned.
|
|
/// </remarks>
|
|
/// <param name="friendsToFind">Array of friend's names (make sure they are unique).</param>
|
|
/// <param name="options">Options that affect the result of the FindFriends operation.</param>
|
|
/// <returns>If the operation could be sent (requires connection).</returns>
|
|
public bool OpFindFriends(string[] friendsToFind, FindFriendsOptions options = null)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.FindFriends, this.Server, "FindFriends"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (this.IsFetchingFriendList)
|
|
{
|
|
this.DebugReturn(DebugLevel.WARNING, "OpFindFriends skipped: already fetching friends list.");
|
|
return false; // fetching friends currently, so don't do it again (avoid changing the list while fetching friends)
|
|
}
|
|
|
|
if (friendsToFind == null || friendsToFind.Length == 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpFindFriends skipped: friendsToFind array is null or empty.");
|
|
return false;
|
|
}
|
|
|
|
if (friendsToFind.Length > FriendRequestListMax)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, string.Format("OpFindFriends skipped: friendsToFind array exceeds allowed length of {0}.", FriendRequestListMax));
|
|
return false;
|
|
}
|
|
|
|
List<string> friendsList = new List<string>(friendsToFind.Length);
|
|
for (int i = 0; i < friendsToFind.Length; i++)
|
|
{
|
|
string friendUserId = friendsToFind[i];
|
|
if (string.IsNullOrEmpty(friendUserId))
|
|
{
|
|
this.DebugReturn(DebugLevel.WARNING,
|
|
string.Format(
|
|
"friendsToFind array contains a null or empty UserId, element at position {0} skipped.",
|
|
i));
|
|
}
|
|
else if (friendUserId.Equals(UserId))
|
|
{
|
|
this.DebugReturn(DebugLevel.WARNING,
|
|
string.Format(
|
|
"friendsToFind array contains local player's UserId \"{0}\", element at position {1} skipped.",
|
|
friendUserId,
|
|
i));
|
|
}
|
|
else if (friendsList.Contains(friendUserId))
|
|
{
|
|
this.DebugReturn(DebugLevel.WARNING,
|
|
string.Format(
|
|
"friendsToFind array contains duplicate UserId \"{0}\", element at position {1} skipped.",
|
|
friendUserId,
|
|
i));
|
|
}
|
|
else
|
|
{
|
|
friendsList.Add(friendUserId);
|
|
}
|
|
}
|
|
|
|
if (friendsList.Count == 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpFindFriends skipped: friends list to find is empty.");
|
|
return false;
|
|
}
|
|
|
|
string[] filteredArray = friendsList.ToArray();
|
|
bool sent = this.LoadBalancingPeer.OpFindFriends(filteredArray, options);
|
|
this.friendListRequested = sent ? filteredArray : null;
|
|
|
|
return sent;
|
|
}
|
|
|
|
/// <summary>If already connected to a Master Server, this joins the specified lobby. This request triggers an OnOperationResponse() call and the callback OnJoinedLobby().</summary>
|
|
/// <param name="lobby">The lobby to join. Use null for default lobby.</param>
|
|
/// <returns>If the operation could be sent. False, if the client is not IsConnectedAndReady or when it's not connected to a Master Server.</returns>
|
|
public bool OpJoinLobby(TypedLobby lobby)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.JoinLobby, this.Server, "JoinLobby"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (lobby == null)
|
|
{
|
|
lobby = TypedLobby.Default;
|
|
}
|
|
bool sent = this.LoadBalancingPeer.OpJoinLobby(lobby);
|
|
if (sent)
|
|
{
|
|
this.CurrentLobby = lobby;
|
|
this.State = ClientState.JoiningLobby;
|
|
}
|
|
|
|
return sent;
|
|
}
|
|
|
|
|
|
/// <summary>Opposite of joining a lobby. You don't have to explicitly leave a lobby to join another (client can be in one max, at any time).</summary>
|
|
/// <returns>If the operation could be sent (has to be connected).</returns>
|
|
public bool OpLeaveLobby()
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.LeaveLobby, this.Server, "LeaveLobby"))
|
|
{
|
|
return false;
|
|
}
|
|
return this.LoadBalancingPeer.OpLeaveLobby();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Joins a random room that matches the filter. Will callback: OnJoinedRoom or OnJoinRandomFailed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Used for random matchmaking. You can join any room or one with specific properties defined in opJoinRandomRoomParams.
|
|
///
|
|
/// 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).
|
|
/// It may also fail when actually joining the room which was found. Rooms may close, become full or empty anytime.
|
|
///
|
|
/// This method can only be called while the client is connected to a Master Server so you should
|
|
/// implement the callback OnConnectedToMaster.
|
|
/// Check the return value to make sure the operation will be called on the server.
|
|
/// Note: There will be no callbacks if this method returned false.
|
|
///
|
|
///
|
|
/// This client's State is set to ClientState.Joining immediately, when the operation could
|
|
/// be called. In the background, the client will switch servers and call various related operations.
|
|
///
|
|
/// When you're in the room, this client's State will become ClientState.Joined.
|
|
///
|
|
///
|
|
/// When entering 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 are not wiped when leaving a room.
|
|
///
|
|
/// More about matchmaking:
|
|
/// https://doc.photonengine.com/en-us/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.
|
|
/// </remarks>
|
|
/// <param name="opJoinRandomRoomParams">Optional definition of properties to filter rooms in random matchmaking.</param>
|
|
/// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
|
|
public bool OpJoinRandomRoom(OpJoinRandomRoomParams opJoinRandomRoomParams = null)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.JoinRandomGame, this.Server, "JoinRandomGame"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (opJoinRandomRoomParams == null)
|
|
{
|
|
opJoinRandomRoomParams = new OpJoinRandomRoomParams();
|
|
}
|
|
|
|
this.enterRoomParamsCache = new EnterRoomParams();
|
|
this.enterRoomParamsCache.Lobby = opJoinRandomRoomParams.TypedLobby;
|
|
this.enterRoomParamsCache.ExpectedUsers = opJoinRandomRoomParams.ExpectedUsers;
|
|
|
|
|
|
bool sending = this.LoadBalancingPeer.OpJoinRandomRoom(opJoinRandomRoomParams);
|
|
if (sending)
|
|
{
|
|
this.lastJoinType = JoinType.JoinRandomRoom;
|
|
this.State = ClientState.Joining;
|
|
}
|
|
return sending;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Attempts to join a room that matches the specified filter and creates a room if none found.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This operation is a combination of filter-based random matchmaking with the option to create a new room,
|
|
/// if no fitting room exists.
|
|
/// The benefit of that is that the room creation is done by the same operation and the room can be found
|
|
/// by the very next client, looking for similar rooms.
|
|
///
|
|
/// There are separate parameters for joining and creating a room.
|
|
///
|
|
/// This method can only be called while connected to a Master Server.
|
|
/// This client's State is set to ClientState.Joining immediately.
|
|
///
|
|
/// Either IMatchmakingCallbacks.OnJoinedRoom or IMatchmakingCallbacks.OnCreatedRoom get called.
|
|
///
|
|
/// More about matchmaking:
|
|
/// https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby
|
|
///
|
|
/// Check the return value to make sure the operation will be called on the server.
|
|
/// Note: There will be no callbacks if this method returned false.
|
|
/// </remarks>
|
|
/// <returns>If the operation will be sent (requires connection to Master Server).</returns>
|
|
public bool OpJoinRandomOrCreateRoom(OpJoinRandomRoomParams opJoinRandomRoomParams, EnterRoomParams createRoomParams)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.JoinRandomGame, this.Server, "OpJoinRandomOrCreateRoom"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (opJoinRandomRoomParams == null)
|
|
{
|
|
opJoinRandomRoomParams = new OpJoinRandomRoomParams();
|
|
}
|
|
if (createRoomParams == null)
|
|
{
|
|
createRoomParams = new EnterRoomParams();
|
|
}
|
|
|
|
createRoomParams.CreateIfNotExists = true;
|
|
this.enterRoomParamsCache = createRoomParams;
|
|
this.enterRoomParamsCache.Lobby = opJoinRandomRoomParams.TypedLobby;
|
|
this.enterRoomParamsCache.ExpectedUsers = opJoinRandomRoomParams.ExpectedUsers;
|
|
|
|
|
|
bool sending = this.LoadBalancingPeer.OpJoinRandomOrCreateRoom(opJoinRandomRoomParams, createRoomParams);
|
|
if (sending)
|
|
{
|
|
this.lastJoinType = JoinType.JoinRandomOrCreateRoom;
|
|
this.State = ClientState.Joining;
|
|
}
|
|
return sending;
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Creates a new room. Will callback: OnCreatedRoom and OnJoinedRoom or OnCreateRoomFailed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When successful, the client will enter the specified room and callback both OnCreatedRoom and OnJoinedRoom.
|
|
/// In all error cases, OnCreateRoomFailed gets called.
|
|
///
|
|
/// Creating a room will fail if the room name is already in use or when the RoomOptions clashing
|
|
/// with one another. Check the EnterRoomParams reference for the various room creation options.
|
|
///
|
|
///
|
|
/// This method can only be called while the client is connected to a Master Server so you should
|
|
/// implement the callback OnConnectedToMaster.
|
|
/// Check the return value to make sure the operation will be called on the server.
|
|
/// Note: There will be no callbacks if this method returned false.
|
|
///
|
|
///
|
|
/// When you're in the room, this client's State will become ClientState.Joined.
|
|
///
|
|
///
|
|
/// When entering 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 are not wiped when leaving a room.
|
|
///
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <param name="enterRoomParams">Definition of properties for the room to create.</param>
|
|
/// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
|
|
public bool OpCreateRoom(EnterRoomParams enterRoomParams)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.CreateGame, this.Server, "CreateGame"))
|
|
{
|
|
return false;
|
|
}
|
|
bool onGameServer = this.Server == ServerConnection.GameServer;
|
|
enterRoomParams.OnGameServer = onGameServer;
|
|
if (!onGameServer)
|
|
{
|
|
this.enterRoomParamsCache = enterRoomParams;
|
|
}
|
|
|
|
bool sending = this.LoadBalancingPeer.OpCreateRoom(enterRoomParams);
|
|
if (sending)
|
|
{
|
|
this.lastJoinType = JoinType.CreateRoom;
|
|
this.State = ClientState.Joining;
|
|
}
|
|
return sending;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Joins a specific room by name and creates it on demand. Will callback: OnJoinedRoom or OnJoinRoomFailed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Useful when players make up a room name to meet in:
|
|
/// All involved clients call the same method and whoever is first, also creates the room.
|
|
///
|
|
/// When successful, the client will enter the specified room.
|
|
/// The client which creates the room, will callback both OnCreatedRoom and OnJoinedRoom.
|
|
/// Clients that join an existing room will only callback OnJoinedRoom.
|
|
/// In all error cases, OnJoinRoomFailed gets called.
|
|
///
|
|
/// Joining a room will fail, if the room is full, closed or when the user
|
|
/// already is present in the room (checked by userId).
|
|
///
|
|
/// To return to a room, use OpRejoinRoom.
|
|
///
|
|
/// This method can only be called while the client is connected to a Master Server so you should
|
|
/// implement the callback OnConnectedToMaster.
|
|
/// Check the return value to make sure the operation will be called on the server.
|
|
/// Note: There will be no callbacks if this method returned false.
|
|
///
|
|
/// This client's State is set to ClientState.Joining immediately, when the operation could
|
|
/// be called. In the background, the client will switch servers and call various related operations.
|
|
///
|
|
/// When you're in the room, this client's State will become ClientState.Joined.
|
|
///
|
|
///
|
|
/// 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.
|
|
///
|
|
/// When entering 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 are not wiped when leaving a room.
|
|
///
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <param name="enterRoomParams">Definition of properties for the room to create or join.</param>
|
|
/// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
|
|
public bool OpJoinOrCreateRoom(EnterRoomParams enterRoomParams)
|
|
{
|
|
bool onGameServer = this.Server == ServerConnection.GameServer;
|
|
enterRoomParams.CreateIfNotExists = true;
|
|
enterRoomParams.OnGameServer = onGameServer;
|
|
if (!onGameServer)
|
|
{
|
|
this.enterRoomParamsCache = enterRoomParams;
|
|
}
|
|
|
|
bool sending = this.LoadBalancingPeer.OpJoinRoom(enterRoomParams);
|
|
if (sending)
|
|
{
|
|
this.lastJoinType = JoinType.JoinOrCreateRoom;
|
|
this.State = ClientState.Joining;
|
|
}
|
|
return sending;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Joins a room by name. Will callback: OnJoinedRoom or OnJoinRoomFailed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Useful when using lobbies or when players follow friends or invite each other.
|
|
///
|
|
/// When successful, the client will enter the specified room and callback via OnJoinedRoom.
|
|
/// In all error cases, OnJoinRoomFailed gets called.
|
|
///
|
|
/// Joining a room will fail if the room is full, closed, not existing or when the user
|
|
/// already is present in the room (checked by userId).
|
|
///
|
|
/// To return to a room, use OpRejoinRoom.
|
|
/// When players invite each other and it's unclear who's first to respond, use OpJoinOrCreateRoom instead.
|
|
///
|
|
/// This method can only be called while the client is connected to a Master Server so you should
|
|
/// implement the callback OnConnectedToMaster.
|
|
/// Check the return value to make sure the operation will be called on the server.
|
|
/// Note: There will be no callbacks if this method returned false.
|
|
///
|
|
/// A room's name has to be unique (per region, appid and gameversion).
|
|
/// When your title uses a global matchmaking or invitations (e.g. an external solution),
|
|
/// keep regions and the game versions in mind to join a room.
|
|
///
|
|
///
|
|
/// This client's State is set to ClientState.Joining immediately, when the operation could
|
|
/// be called. In the background, the client will switch servers and call various related operations.
|
|
///
|
|
/// When you're in the room, this client's State will become ClientState.Joined.
|
|
///
|
|
///
|
|
/// When entering 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 are not wiped when leaving a room.
|
|
///
|
|
/// You can define an array of expectedUsers, to reserve player slots in the room for friends or party members.
|
|
/// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages.
|
|
/// </remarks>
|
|
/// <param name="enterRoomParams">Definition of properties for the room to join.</param>
|
|
/// <returns>If the operation could be sent currently (requires connection to Master Server).</returns>
|
|
public bool OpJoinRoom(EnterRoomParams enterRoomParams)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.JoinGame, this.Server, "JoinRoom"))
|
|
{
|
|
return false;
|
|
}
|
|
bool onGameServer = this.Server == ServerConnection.GameServer;
|
|
enterRoomParams.OnGameServer = onGameServer;
|
|
if (!onGameServer)
|
|
{
|
|
this.enterRoomParamsCache = enterRoomParams;
|
|
}
|
|
|
|
bool sending = this.LoadBalancingPeer.OpJoinRoom(enterRoomParams);
|
|
if (sending)
|
|
{
|
|
this.lastJoinType = (enterRoomParams.CreateIfNotExists) ? JoinType.JoinOrCreateRoom : JoinType.JoinRoom;
|
|
this.State = ClientState.Joining;
|
|
}
|
|
return sending;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Rejoins a room by roomName (using the userID internally to return). Will callback: OnJoinedRoom or OnJoinRoomFailed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Used to return to a room, before this user was removed from the players list.
|
|
/// Internally, the userID will be checked by the server, to make sure this user is in the room (active or inactice).
|
|
///
|
|
/// In contrast to join, this operation never adds a players to a room. It will attempt to retake an existing
|
|
/// spot in the playerlist or fail. This makes sure the client doean't accidentally join a room when the
|
|
/// game logic meant to re-activate an existing actor in an existing room.
|
|
///
|
|
/// This method will fail on the server, when the room does not exist, can't be loaded (persistent rooms) or
|
|
/// when the userId is not in the player list of this room. This will lead to a callback OnJoinRoomFailed.
|
|
///
|
|
/// Rejoining room will not send any player properties. Instead client will receive up-to-date ones from server.
|
|
/// If you want to set new player properties, do it once rejoined.
|
|
/// </remarks>
|
|
public bool OpRejoinRoom(string roomName)
|
|
{
|
|
bool onGameServer = this.Server == ServerConnection.GameServer;
|
|
|
|
EnterRoomParams opParams = new EnterRoomParams();
|
|
this.enterRoomParamsCache = opParams;
|
|
opParams.RoomName = roomName;
|
|
opParams.OnGameServer = onGameServer;
|
|
opParams.RejoinOnly = true;
|
|
|
|
bool sending = this.LoadBalancingPeer.OpJoinRoom(opParams);
|
|
if (sending)
|
|
{
|
|
this.lastJoinType = JoinType.JoinRoom;
|
|
this.State = ClientState.Joining;
|
|
}
|
|
return sending;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Leaves the current room, optionally telling the server that the user is just becoming inactive. Will callback: OnLeftRoom.
|
|
/// </summary>
|
|
///
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <param name="becomeInactive">If true, this player becomes inactive in the game and can return later (if PlayerTTL of the room is != 0).</param>
|
|
/// <param name="sendAuthCookie">WebFlag: Securely transmit the encrypted object AuthCookie to the web service in PathLeave webhook when available</param>
|
|
/// <returns>If the current room could be left (impossible while not in a room).</returns>
|
|
public bool OpLeaveRoom(bool becomeInactive, bool sendAuthCookie = false)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.Leave, this.Server, "LeaveRoom"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
this.State = ClientState.Leaving;
|
|
return this.LoadBalancingPeer.OpLeaveRoom(becomeInactive, sendAuthCookie);
|
|
}
|
|
|
|
|
|
/// <summary>Gets a list of games matching a SQL-like where clause.</summary>
|
|
/// <remarks>
|
|
/// Operation is only available for lobbies of type SqlLobby.
|
|
/// This is an async request which triggers a OnOperationResponse() call.
|
|
/// </remarks>
|
|
/// <see cref="https://doc.photonengine.com/en-us/realtime/current/reference/matchmaking-and-lobby#sql_lobby_type"/>
|
|
/// <param name="typedLobby">The lobby to query. Has to be of type SqlLobby.</param>
|
|
/// <param name="sqlLobbyFilter">The sql query statement.</param>
|
|
/// <returns>If the operation could be sent (has to be connected).</returns>
|
|
public bool OpGetGameList(TypedLobby typedLobby, string sqlLobbyFilter)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.GetGameList, this.Server, "GetGameList"))
|
|
{
|
|
return false;
|
|
}
|
|
return this.LoadBalancingPeer.OpGetGameList(typedLobby, sqlLobbyFilter);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Updates and synchronizes a Player's Custom Properties. Optionally, expectedProperties can be provided as condition.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Custom Properties are a set of string keys and arbitrary values which is synchronized
|
|
/// for the players in a Room. They are available when the client enters the room, as
|
|
/// they are in the response of OpJoin and OpCreate.
|
|
///
|
|
/// Custom Properties either relate to the (current) Room or a Player (in that Room).
|
|
///
|
|
/// Both classes locally cache the current key/values and make them available as
|
|
/// property: CustomProperties. This is provided only to read them.
|
|
/// You must use the method SetCustomProperties to set/modify them.
|
|
///
|
|
/// Any client can set any Custom Properties anytime (when in a room).
|
|
/// It's up to the game logic to organize how they are best used.
|
|
///
|
|
/// You should call SetCustomProperties only with key/values that are new or changed. This reduces
|
|
/// traffic and performance.
|
|
///
|
|
/// Unless you define some expectedProperties, setting key/values is always permitted.
|
|
/// In this case, the property-setting client will not receive the new values from the server but
|
|
/// instead update its local cache in SetCustomProperties.
|
|
///
|
|
/// If you define expectedProperties, the server will skip updates if the server property-cache
|
|
/// does not contain all expectedProperties with the same values.
|
|
/// In this case, the property-setting client will get an update from the server and update it's
|
|
/// cached key/values at about the same time as everyone else.
|
|
///
|
|
/// The benefit of using expectedProperties can be only one client successfully sets a key from
|
|
/// one known value to another.
|
|
/// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
|
|
/// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
|
|
/// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
|
|
/// take the item will have it (and the others fail to set the ownership).
|
|
///
|
|
/// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
|
|
/// </remarks>
|
|
/// <param name="actorNr">Defines which player the Custom Properties belong to. ActorID of a player.</param>
|
|
/// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
|
|
/// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values. Client must be in room.</param>
|
|
/// <param name="webFlags">Defines if the set properties should be forwarded to a WebHook. Client must be in room.</param>
|
|
/// <returns>
|
|
/// False if propertiesToSet is null or empty or have zero string keys.
|
|
/// If not in a room, returns true if local player and expectedProperties and webFlags are null.
|
|
/// False if actorNr is lower than or equal to zero.
|
|
/// Otherwise, returns if the operation could be sent to the server.
|
|
/// </returns>
|
|
public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
|
|
{
|
|
if (propertiesToSet == null || propertiesToSet.Count == 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. propertiesToSet must not be null nor empty.");
|
|
return false;
|
|
}
|
|
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.ActorNumber == actorNr)
|
|
{
|
|
return this.LocalPlayer.SetCustomProperties(propertiesToSet);
|
|
}
|
|
|
|
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);
|
|
if (customActorProperties.Count == 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. Only string keys allowed for custom properties.");
|
|
return false;
|
|
}
|
|
return this.OpSetPropertiesOfActor(actorNr, customActorProperties, expectedProperties, webFlags);
|
|
}
|
|
|
|
|
|
/// <summary>Internally used to cache and set properties (including well known properties).</summary>
|
|
/// <remarks>Requires being in a room (because this attempts to send an operation which will fail otherwise).</remarks>
|
|
protected internal bool OpSetPropertiesOfActor(int actorNr, Hashtable actorProperties, Hashtable expectedProperties = null, WebFlags webFlags = null)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.SetProperties, this.Server, "SetProperties"))
|
|
{
|
|
return false;
|
|
}
|
|
if (actorProperties == null || actorProperties.Count == 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfActor() failed. actorProperties must not be null nor empty.");
|
|
return false;
|
|
}
|
|
bool res = this.LoadBalancingPeer.OpSetPropertiesOfActor(actorNr, actorProperties, expectedProperties, webFlags);
|
|
if (res && !this.CurrentRoom.BroadcastPropertiesChangeToAll && (expectedProperties == null || expectedProperties.Count == 0))
|
|
{
|
|
Player target = this.CurrentRoom.GetPlayer(actorNr);
|
|
if (target != null)
|
|
{
|
|
target.InternalCacheProperties(actorProperties);
|
|
this.InRoomCallbackTargets.OnPlayerPropertiesUpdate(target, actorProperties);
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Custom Properties are a set of string keys and arbitrary values which is synchronized
|
|
/// for the players in a Room. They are available when the client enters the room, as
|
|
/// they are in the response of OpJoin and OpCreate.
|
|
///
|
|
/// Custom Properties either relate to the (current) Room or a Player (in that Room).
|
|
///
|
|
/// Both classes locally cache the current key/values and make them available as
|
|
/// property: CustomProperties. This is provided only to read them.
|
|
/// You must use the method SetCustomProperties to set/modify them.
|
|
///
|
|
/// Any client can set any Custom Properties anytime (when in a room).
|
|
/// It's up to the game logic to organize how they are best used.
|
|
///
|
|
/// You should call SetCustomProperties only with key/values that are new or changed. This reduces
|
|
/// traffic and performance.
|
|
///
|
|
/// Unless you define some expectedProperties, setting key/values is always permitted.
|
|
/// In this case, the property-setting client will not receive the new values from the server but
|
|
/// instead update its local cache in SetCustomProperties.
|
|
///
|
|
/// If you define expectedProperties, the server will skip updates if the server property-cache
|
|
/// does not contain all expectedProperties with the same values.
|
|
/// In this case, the property-setting client will get an update from the server and update it's
|
|
/// cached key/values at about the same time as everyone else.
|
|
///
|
|
/// The benefit of using expectedProperties can be only one client successfully sets a key from
|
|
/// one known value to another.
|
|
/// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally.
|
|
/// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their
|
|
/// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to
|
|
/// take the item will have it (and the others fail to set the ownership).
|
|
///
|
|
/// Properties get saved with the game state for Turnbased games (which use IsPersistent = true).
|
|
/// </remarks>
|
|
/// <param name="propertiesToSet">Hashtable of Custom Properties that changes.</param>
|
|
/// <param name="expectedProperties">Provide some keys/values to use as condition for setting the new values.</param>
|
|
/// <param name="webFlags">Defines web flags for an optional PathProperties webhook.</param>
|
|
/// <returns>
|
|
/// False if propertiesToSet is null or empty or have zero string keys.
|
|
/// Otherwise, returns if the operation could be sent to the server.
|
|
/// </returns>
|
|
public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null)
|
|
{
|
|
if (propertiesToSet == null || propertiesToSet.Count == 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfRoom() failed. propertiesToSet must not be null nor empty.");
|
|
return false;
|
|
}
|
|
Hashtable customGameProps = new Hashtable();
|
|
customGameProps.MergeStringKeys(propertiesToSet);
|
|
if (customGameProps.Count == 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfRoom() failed. Only string keys are allowed for custom properties.");
|
|
return false;
|
|
}
|
|
return this.OpSetPropertiesOfRoom(customGameProps, expectedProperties, webFlags);
|
|
}
|
|
|
|
|
|
protected internal bool OpSetPropertyOfRoom(byte propCode, object value)
|
|
{
|
|
Hashtable properties = new Hashtable();
|
|
properties[propCode] = value;
|
|
return this.OpSetPropertiesOfRoom(properties);
|
|
}
|
|
|
|
/// <summary>Internally used to cache and set properties (including well known properties).</summary>
|
|
/// <remarks>Requires being in a room (because this attempts to send an operation which will fail otherwise).</remarks>
|
|
protected internal bool OpSetPropertiesOfRoom(Hashtable gameProperties, Hashtable expectedProperties = null, WebFlags webFlags = null)
|
|
{
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.SetProperties, this.Server, "SetProperties"))
|
|
{
|
|
return false;
|
|
}
|
|
if (gameProperties == null || gameProperties.Count == 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfRoom() failed. gameProperties must not be null nor empty.");
|
|
return false;
|
|
}
|
|
bool res = this.LoadBalancingPeer.OpSetPropertiesOfRoom(gameProperties, expectedProperties, webFlags);
|
|
if (res && !this.CurrentRoom.BroadcastPropertiesChangeToAll && (expectedProperties == null || expectedProperties.Count == 0))
|
|
{
|
|
this.CurrentRoom.InternalCacheProperties(gameProperties);
|
|
this.InRoomCallbackTargets.OnRoomPropertiesUpdate(gameProperties);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Send an event with custom code/type and any content to the other players in the same room.
|
|
/// </summary>
|
|
/// <param name="eventCode">Identifies this type of event (and the content). Your game's event codes can start with 0.</param>
|
|
/// <param name="customEventContent">Any serializable datatype (including Hashtable like the other OpRaiseEvent overloads).</param>
|
|
/// <param name="raiseEventOptions">Contains used send options. If you pass null, the default options will be used.</param>
|
|
/// <param name="sendOptions">Send options for reliable, encryption etc</param>
|
|
/// <returns>If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands.</returns>
|
|
public virtual bool OpRaiseEvent(byte eventCode, object customEventContent, RaiseEventOptions raiseEventOptions, SendOptions sendOptions)
|
|
{
|
|
if (this.LoadBalancingPeer == null)
|
|
{
|
|
return false;
|
|
}
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.RaiseEvent, this.Server, "RaiseEvent"))
|
|
{
|
|
return false;
|
|
}
|
|
return this.LoadBalancingPeer.OpRaiseEvent(eventCode, customEventContent, raiseEventOptions, sendOptions);
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Operation to handle this client's interest groups (for events in room).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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).
|
|
/// </remarks>
|
|
/// <param name="groupsToRemove">Groups to remove from interest. Null will not remove any. A byte[0] will remove all.</param>
|
|
/// <param name="groupsToAdd">Groups to add to interest. Null will not add any. A byte[0] will add all current.</param>
|
|
/// <returns>If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands.</returns>
|
|
public virtual bool OpChangeGroups(byte[] groupsToRemove, byte[] groupsToAdd)
|
|
{
|
|
if (this.LoadBalancingPeer == null)
|
|
{
|
|
return false;
|
|
}
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.ChangeGroups, this.Server, "ChangeGroups"))
|
|
{
|
|
return false;
|
|
}
|
|
return this.LoadBalancingPeer.OpChangeGroups(groupsToRemove, groupsToAdd);
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
/// <summary>
|
|
/// Privately used to read-out properties coming from the server in events and operation responses (which might be a bit tricky).
|
|
/// </summary>
|
|
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 (this.InRoom)
|
|
{
|
|
this.InRoomCallbackTargets.OnRoomPropertiesUpdate(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);
|
|
this.InRoomCallbackTargets.OnPlayerPropertiesUpdate(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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Privately used only to read properties for a distinct actor (which might be the hashtable OR a key-pair value IN the actorProperties).
|
|
/// </summary>
|
|
private Hashtable ReadoutPropertiesForActorNr(Hashtable actorProperties, int actorNr)
|
|
{
|
|
if (actorProperties.ContainsKey(actorNr))
|
|
{
|
|
return (Hashtable)actorProperties[actorNr];
|
|
}
|
|
|
|
return actorProperties;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internally used to set the LocalPlayer's ID (from -1 to the actual in-room ID).
|
|
/// </summary>
|
|
/// <param name="newID">New actor ID (a.k.a actorNr) assigned when joining a room.</param>
|
|
public 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);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Called internally, when a game was joined or created on the game server successfully.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <param name="operationResponse">Contains the server's response for an operation called by this peer.</param>
|
|
private void GameEnteredOnGameServer(OperationResponse operationResponse)
|
|
{
|
|
this.CurrentRoom = this.CreateRoom(this.enterRoomParamsCache.RoomName, this.enterRoomParamsCache.RoomOptions);
|
|
this.CurrentRoom.LoadBalancingClient = this;
|
|
|
|
// 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);
|
|
|
|
object temp;
|
|
if (operationResponse.Parameters.TryGetValue(ParameterCode.RoomOptionFlags, out temp))
|
|
{
|
|
this.CurrentRoom.SetRoomFlags((int)temp);
|
|
}
|
|
|
|
this.State = ClientState.Joined;
|
|
|
|
|
|
// the callbacks OnCreatedRoom and OnJoinedRoom are called in the event join. it contains important info about the room and players.
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Factory method to create a player instance - override to get your own player-type with custom features.
|
|
/// </summary>
|
|
/// <param name="actorName">The name of the player to be created. </param>
|
|
/// <param name="actorNumber">The player ID (a.k.a. actorNumber) of the player to be created.</param>
|
|
/// <param name="isLocal">Sets the distinction if the player to be created is your player or if its assigned to someone else.</param>
|
|
/// <param name="actorProperties">The custom properties for this new player</param>
|
|
/// <returns>The newly created player</returns>
|
|
protected internal virtual Player CreatePlayer(string actorName, int actorNumber, bool isLocal, Hashtable actorProperties)
|
|
{
|
|
Player newPlayer = new Player(actorName, actorNumber, isLocal, actorProperties);
|
|
return newPlayer;
|
|
}
|
|
|
|
/// <summary>Internal "factory" method to create a room-instance.</summary>
|
|
protected internal virtual Room CreateRoom(string roomName, RoomOptions opt)
|
|
{
|
|
Room r = new Room(roomName, opt);
|
|
return r;
|
|
}
|
|
|
|
private bool CheckIfOpAllowedOnServer(byte opCode, ServerConnection serverConnection)
|
|
{
|
|
switch (serverConnection)
|
|
{
|
|
case ServerConnection.MasterServer:
|
|
switch (opCode)
|
|
{
|
|
case OperationCode.CreateGame:
|
|
case OperationCode.Authenticate:
|
|
case OperationCode.AuthenticateOnce:
|
|
case OperationCode.FindFriends:
|
|
case OperationCode.GetGameList:
|
|
case OperationCode.GetLobbyStats:
|
|
case OperationCode.JoinGame:
|
|
case OperationCode.JoinLobby:
|
|
case OperationCode.LeaveLobby:
|
|
case OperationCode.WebRpc:
|
|
case OperationCode.ServerSettings:
|
|
case OperationCode.JoinRandomGame:
|
|
return true;
|
|
}
|
|
break;
|
|
case ServerConnection.GameServer:
|
|
switch (opCode)
|
|
{
|
|
case OperationCode.CreateGame:
|
|
case OperationCode.Authenticate:
|
|
case OperationCode.AuthenticateOnce:
|
|
case OperationCode.ChangeGroups:
|
|
case OperationCode.GetProperties:
|
|
case OperationCode.JoinGame:
|
|
case OperationCode.Leave:
|
|
case OperationCode.WebRpc:
|
|
case OperationCode.ServerSettings:
|
|
case OperationCode.SetProperties:
|
|
case OperationCode.RaiseEvent:
|
|
return true;
|
|
}
|
|
break;
|
|
case ServerConnection.NameServer:
|
|
switch (opCode)
|
|
{
|
|
case OperationCode.Authenticate:
|
|
case OperationCode.AuthenticateOnce:
|
|
case OperationCode.GetRegions:
|
|
case OperationCode.ServerSettings:
|
|
return true;
|
|
}
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException("serverConnection", serverConnection, null);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool CheckIfOpCanBeSent(byte opCode, ServerConnection serverConnection, string opName)
|
|
{
|
|
if (this.LoadBalancingPeer == null)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) can't be sent because peer is null", opName, opCode));
|
|
return false;
|
|
}
|
|
if (!this.CheckIfOpAllowedOnServer(opCode, serverConnection))
|
|
{
|
|
if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ERROR)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) not allowed on current server ({2})", opName, opCode, serverConnection));
|
|
}
|
|
return false;
|
|
}
|
|
if (!this.CheckIfClientIsReadyToCallOperation(opCode))
|
|
{
|
|
if (this.LoadBalancingPeer.DebugOut >= DebugLevel.ERROR)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) not called because client is not connected or not ready yet, client state: {2}", opName, opCode, Enum.GetName(typeof(ClientState), this.State)));
|
|
}
|
|
return false;
|
|
}
|
|
if (this.LoadBalancingPeer.PeerState != PeerStateValue.Connected)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, string.Format("Operation {0} ({1}) can't be sent because peer is not connected, peer state: {2}", opName, opCode, this.LoadBalancingPeer.PeerState));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool CheckIfClientIsReadyToCallOperation(byte opCode)
|
|
{
|
|
switch (opCode)
|
|
{
|
|
//case OperationCode.ServerSettings: // ??
|
|
//case OperationCode.WebRpc: // WebRPC works on MS and GS and I think it does not need the client to be ready
|
|
|
|
case OperationCode.Authenticate:
|
|
case OperationCode.AuthenticateOnce:
|
|
return this.IsConnectedAndReady ||
|
|
this.State == ClientState.ConnectingToNameServer || // this is required since we do not set state to ConnectedToNameServer before authentication
|
|
this.State == ClientState.ConnectingToMasterServer || // this is required since we do not set state to ConnectedToMasterServer before authentication
|
|
this.State == ClientState.ConnectingToGameServer; // this is required since we do not set state to ConnectedToGameServer before authentication
|
|
|
|
case OperationCode.ChangeGroups:
|
|
case OperationCode.GetProperties:
|
|
case OperationCode.SetProperties:
|
|
case OperationCode.RaiseEvent:
|
|
case OperationCode.Leave:
|
|
return this.InRoom;
|
|
|
|
case OperationCode.JoinGame:
|
|
case OperationCode.CreateGame:
|
|
return this.State == ClientState.ConnectedToMasterServer || this.InLobby || this.State == ClientState.ConnectedToGameServer; // CurrentRoom can be not null in case of quick rejoin
|
|
|
|
case OperationCode.LeaveLobby:
|
|
return this.InLobby;
|
|
|
|
case OperationCode.JoinRandomGame:
|
|
case OperationCode.FindFriends:
|
|
case OperationCode.GetGameList:
|
|
case OperationCode.GetLobbyStats: // do we need to be inside lobby to call this?
|
|
case OperationCode.JoinLobby: // You don't have to explicitly leave a lobby to join another (client can be in one max, at any time)
|
|
return this.State == ClientState.ConnectedToMasterServer || this.InLobby;
|
|
case OperationCode.GetRegions:
|
|
return this.State == ClientState.ConnectedToNameServer;
|
|
}
|
|
return this.IsConnected;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IPhotonPeerListener
|
|
|
|
/// <summary>Debug output of low level api (and this client).</summary>
|
|
/// <remarks>This method is not responsible to keep up the state of a LoadBalancingClient. Calling base.DebugReturn on overrides is optional.</remarks>
|
|
public virtual void DebugReturn(DebugLevel level, string message)
|
|
{
|
|
if (this.LoadBalancingPeer.DebugOut != DebugLevel.ALL && level > this.LoadBalancingPeer.DebugOut)
|
|
{
|
|
return;
|
|
}
|
|
#if !SUPPORTED_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
|
|
}
|
|
|
|
private void CallbackRoomEnterFailed(OperationResponse operationResponse)
|
|
{
|
|
if (operationResponse.ReturnCode != 0)
|
|
{
|
|
if (operationResponse.OperationCode == OperationCode.JoinGame)
|
|
{
|
|
this.MatchMakingCallbackTargets.OnJoinRoomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
|
|
}
|
|
else if (operationResponse.OperationCode == OperationCode.CreateGame)
|
|
{
|
|
this.MatchMakingCallbackTargets.OnCreateRoomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
|
|
}
|
|
else if (operationResponse.OperationCode == OperationCode.JoinRandomGame)
|
|
{
|
|
this.MatchMakingCallbackTargets.OnJoinRandomFailed(operationResponse.ReturnCode, operationResponse.DebugMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses the OperationResponses provided by the server to advance the internal state and call ops as needed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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().
|
|
/// </remarks>
|
|
/// <param name="operationResponse">Contains the server's response for an operation called by this peer.</param>
|
|
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;
|
|
this.tokenCache = this.AuthValues.Token;
|
|
}
|
|
|
|
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;
|
|
this.ConnectionCallbackTargets.OnCustomAuthenticationFailed(operationResponse.DebugMessage);
|
|
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;
|
|
case ErrorCode.AuthenticationTicketExpired:
|
|
this.DisconnectedCause = DisconnectCause.AuthenticationTicketExpired;
|
|
break;
|
|
}
|
|
|
|
this.Disconnect(this.DisconnectedCause);
|
|
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.LocalPlayer.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<byte, object>)operationResponse.Parameters[ParameterCode.EncryptionData]);
|
|
}
|
|
}
|
|
|
|
if (this.Server == ServerConnection.NameServer)
|
|
{
|
|
string receivedCluster = operationResponse[ParameterCode.Cluster] as string;
|
|
if (!string.IsNullOrEmpty(receivedCluster))
|
|
{
|
|
this.CurrentCluster = receivedCluster;
|
|
}
|
|
|
|
// 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.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Udp && this.UseAlternativeUdpPorts)
|
|
{
|
|
// TODO: Make this work with AuthOnceWss, which uses WSS on NameServer but "expects" to use UDP...
|
|
this.MasterServerAddress = this.MasterServerAddress.Replace("5058", "27000").Replace("5055", "27001").Replace("5056", "27002");
|
|
}
|
|
|
|
if (this.AuthMode == AuthModeOption.AuthOnceWss && this.ExpectedProtocol != null)
|
|
{
|
|
this.DebugReturn(DebugLevel.INFO, string.Format("AuthOnceWss mode. Auth response switches TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol));
|
|
this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol;
|
|
this.ExpectedProtocol = null;
|
|
}
|
|
this.DisconnectToReconnect();
|
|
}
|
|
else if (this.Server == ServerConnection.MasterServer)
|
|
{
|
|
this.State = ClientState.ConnectedToMasterServer;
|
|
if (this.failedRoomEntryOperation == null)
|
|
{
|
|
this.ConnectionCallbackTargets.OnConnectedToMaster();
|
|
}
|
|
else
|
|
{
|
|
this.CallbackRoomEnterFailed(this.failedRoomEntryOperation);
|
|
this.failedRoomEntryOperation = null;
|
|
}
|
|
|
|
if (this.AuthMode != AuthModeOption.Auth)
|
|
{
|
|
this.LoadBalancingPeer.OpSettings(this.EnableLobbyStatistics);
|
|
}
|
|
}
|
|
else if (this.Server == ServerConnection.GameServer)
|
|
{
|
|
this.State = ClientState.Joining;
|
|
|
|
if (this.enterRoomParamsCache.RejoinOnly)
|
|
{
|
|
this.enterRoomParamsCache.PlayerProperties = null;
|
|
}
|
|
else
|
|
{
|
|
Hashtable allProps = new Hashtable();
|
|
allProps.Merge(this.LocalPlayer.CustomProperties);
|
|
allProps[ActorProperties.PlayerName] = this.LocalPlayer.NickName;
|
|
|
|
this.enterRoomParamsCache.PlayerProperties = allProps;
|
|
}
|
|
|
|
this.enterRoomParamsCache.OnGameServer = true;
|
|
|
|
if (this.lastJoinType == JoinType.JoinRoom || this.lastJoinType == JoinType.JoinRandomRoom || this.lastJoinType == JoinType.JoinRandomOrCreateRoom || this.lastJoinType == JoinType.JoinOrCreateRoom)
|
|
{
|
|
this.LoadBalancingPeer.OpJoinRoom(this.enterRoomParamsCache);
|
|
}
|
|
else if (this.lastJoinType == JoinType.CreateRoom)
|
|
{
|
|
this.LoadBalancingPeer.OpCreateRoom(this.enterRoomParamsCache);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// optionally, OpAuth may return some data for the client to use. if it's available, call OnCustomAuthenticationResponse
|
|
Dictionary<string, object> data = (Dictionary<string, object>)operationResponse[ParameterCode.Data];
|
|
if (data != null)
|
|
{
|
|
this.ConnectionCallbackTargets.OnCustomAuthenticationResponse(data);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OperationCode.GetRegions:
|
|
// Debug.Log("GetRegions returned: " + operationResponse.ToStringFull());
|
|
|
|
if (operationResponse.ReturnCode == ErrorCode.InvalidAuthentication)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, string.Format("GetRegions failed. AppId is unknown on the (cloud) server. "+operationResponse.DebugMessage));
|
|
this.Disconnect(DisconnectCause.InvalidAuthentication);
|
|
break;
|
|
}
|
|
if (operationResponse.ReturnCode != ErrorCode.Ok)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "GetRegions failed. Can't provide regions list. ReturnCode: " + operationResponse.ReturnCode + ": " + operationResponse.DebugMessage);
|
|
this.Disconnect(DisconnectCause.InvalidAuthentication);
|
|
break;
|
|
}
|
|
if (this.RegionHandler == null)
|
|
{
|
|
this.RegionHandler = new RegionHandler();
|
|
}
|
|
|
|
if (this.RegionHandler.IsPinging)
|
|
{
|
|
this.DebugReturn(DebugLevel.WARNING, "Received an response for OpGetRegions while the RegionHandler is pinging regions already. Skipping this response in favor of completing the current region-pinging.");
|
|
return; // in this particular case, we suppress the duplicate GetRegion response. we don't want a callback for this, cause there is a warning already.
|
|
}
|
|
|
|
this.RegionHandler.SetRegions(operationResponse);
|
|
this.ConnectionCallbackTargets.OnRegionListReceived(this.RegionHandler);
|
|
|
|
if (this.connectToBestRegion)
|
|
{
|
|
// ping minimal regions (if one is known) and connect
|
|
this.RegionHandler.PingMinimumOfRegions(this.OnRegionPingCompleted, this.bestRegionSummaryFromStorage);
|
|
}
|
|
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)
|
|
{
|
|
if (this.Server == ServerConnection.GameServer)
|
|
{
|
|
this.failedRoomEntryOperation = operationResponse;
|
|
this.DisconnectToReconnect();
|
|
}
|
|
else
|
|
{
|
|
this.State = (this.InLobby) ? ClientState.JoinedLobby : ClientState.ConnectedToMasterServer;
|
|
this.CallbackRoomEnterFailed(operationResponse);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (this.Server == ServerConnection.GameServer)
|
|
{
|
|
this.GameEnteredOnGameServer(operationResponse);
|
|
}
|
|
else
|
|
{
|
|
this.GameServerAddress = (string)operationResponse[ParameterCode.Address];
|
|
if (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Udp && this.UseAlternativeUdpPorts)
|
|
{
|
|
this.GameServerAddress = this.GameServerAddress.Replace("5058", "27000").Replace("5055", "27001").Replace("5056", "27002");
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
List<RoomInfo> _RoomInfoList = new List<RoomInfo>();
|
|
|
|
Hashtable games = (Hashtable)operationResponse[ParameterCode.GameList];
|
|
foreach (string gameName in games.Keys)
|
|
{
|
|
_RoomInfoList.Add(new RoomInfo(gameName, (Hashtable)games[gameName]));
|
|
}
|
|
|
|
this.LobbyCallbackTargets.OnRoomListUpdate(_RoomInfoList);
|
|
break;
|
|
|
|
case OperationCode.JoinLobby:
|
|
this.State = ClientState.JoinedLobby;
|
|
this.LobbyCallbackTargets.OnJoinedLobby();
|
|
break;
|
|
|
|
case OperationCode.LeaveLobby:
|
|
this.State = ClientState.ConnectedToMasterServer;
|
|
// TODO: clean room list?!
|
|
this.LobbyCallbackTargets.OnLeftLobby();
|
|
break;
|
|
|
|
case OperationCode.Leave:
|
|
this.DisconnectToReconnect();
|
|
break;
|
|
|
|
case OperationCode.FindFriends:
|
|
if (operationResponse.ReturnCode != 0)
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed: " + operationResponse.ToStringFull());
|
|
this.friendListRequested = null;
|
|
break;
|
|
}
|
|
|
|
bool[] onlineList = operationResponse[ParameterCode.FindFriendsResponseOnlineList] as bool[];
|
|
string[] roomList = operationResponse[ParameterCode.FindFriendsResponseRoomIdList] as string[];
|
|
|
|
//if (onlineList == null || roomList == null || this.friendListRequested == null || onlineList.Length != this.friendListRequested.Length)
|
|
//{
|
|
// // TODO: Check if we should handle this case better / more extensively
|
|
// this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed. Some list is not set. OpResponse: " + operationResponse.ToStringFull());
|
|
// this.friendListRequested = null;
|
|
// this.isFetchingFriendList = false;
|
|
// break;
|
|
//}
|
|
|
|
List<FriendInfo> friendList = new List<FriendInfo>(this.friendListRequested.Length);
|
|
for (int index = 0; index < this.friendListRequested.Length; index++)
|
|
{
|
|
FriendInfo friend = new FriendInfo();
|
|
friend.UserId = this.friendListRequested[index];
|
|
friend.Room = roomList[index];
|
|
friend.IsOnline = onlineList[index];
|
|
friendList.Insert(index, friend);
|
|
}
|
|
|
|
this.friendListRequested = null;
|
|
|
|
this.MatchMakingCallbackTargets.OnFriendListUpdate(friendList);
|
|
break;
|
|
|
|
case OperationCode.WebRpc:
|
|
this.WebRpcCallbackTargets.OnWebRpcResponse(operationResponse);
|
|
break;
|
|
}
|
|
|
|
if (this.OpResponseReceived != null) this.OpResponseReceived(operationResponse);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses the connection's statusCodes to advance the internal state and call operations as needed.
|
|
/// </summary>
|
|
/// <remarks>This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnStatusChanged.</remarks>
|
|
public virtual void OnStatusChanged(StatusCode statusCode)
|
|
{
|
|
switch (statusCode)
|
|
{
|
|
case StatusCode.Connect:
|
|
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;
|
|
this.ConnectionCallbackTargets.OnConnected(); // if initial connect
|
|
}
|
|
|
|
|
|
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:
|
|
if (this.Server == ServerConnection.NameServer)
|
|
{
|
|
this.State = ClientState.ConnectedToNameServer;
|
|
|
|
// if there is no specific region to connect to, get available regions from the Name Server. the result triggers next actions in workflow
|
|
if (string.IsNullOrEmpty(this.CloudRegion))
|
|
{
|
|
this.OpGetRegions();
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// auth AuthOnce, no explicit authentication is needed on Master Server and Game Server. this is done via token, so: break
|
|
if (this.AuthMode == AuthModeOption.AuthOnce || this.AuthMode == AuthModeOption.AuthOnceWss)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// authenticate in all other cases (using the CloudRegion, if available)
|
|
bool authenticating = this.CallAuthenticate();
|
|
if (authenticating)
|
|
{
|
|
this.State = ClientState.Authenticating;
|
|
}
|
|
else
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "OpAuthenticate failed. Check log output and AuthValues. 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.friendListRequested = null;
|
|
|
|
bool wasInRoom = this.CurrentRoom != null;
|
|
this.CurrentRoom = null; // players get cleaned up inside this, too, except LocalPlayer (which we keep)
|
|
this.ChangeLocalID(-1); // depends on this.CurrentRoom, so it must be called after updating that
|
|
|
|
if (this.Server == ServerConnection.GameServer && wasInRoom)
|
|
{
|
|
this.MatchMakingCallbackTargets.OnLeftRoom();
|
|
}
|
|
|
|
if (this.ExpectedProtocol != null)
|
|
{
|
|
this.DebugReturn(DebugLevel.INFO, string.Format("AuthOnceWss mode. On disconnect switches TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol));
|
|
this.LoadBalancingPeer.TransportProtocol = (ConnectionProtocol)this.ExpectedProtocol;
|
|
this.ExpectedProtocol = null;
|
|
}
|
|
|
|
switch (this.State)
|
|
{
|
|
case ClientState.ConnectWithFallbackProtocol:
|
|
this.EnableProtocolFallback = false; // the client does a fallback only one time
|
|
this.LoadBalancingPeer.TransportProtocol = (this.LoadBalancingPeer.TransportProtocol == ConnectionProtocol.Tcp) ? ConnectionProtocol.Udp : ConnectionProtocol.Tcp;
|
|
this.NameServerPortOverride = 0;
|
|
this.ConnectToNameServer();
|
|
break;
|
|
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;
|
|
this.ConnectionCallbackTargets.OnDisconnected(this.DisconnectedCause);
|
|
break;
|
|
|
|
case ClientState.DisconnectingFromGameServer:
|
|
case ClientState.DisconnectingFromNameServer:
|
|
this.ConnectToMasterServer(); // this gets the client back to the Master Server
|
|
break;
|
|
|
|
case ClientState.DisconnectingFromMasterServer:
|
|
this.Connect(this.GameServerAddress, this.ProxyServerAddress, ServerConnection.GameServer); // this connects the client with the Game Server (when joining/creating a room)
|
|
break;
|
|
|
|
case ClientState.Disconnected:
|
|
// this client is already Disconnected, so no further action is needed.
|
|
// this.DebugReturn(DebugLevel.INFO, "LBC.OnStatusChanged(Disconnect) this.State: " + this.State + ". Server: " + this.Server);
|
|
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;
|
|
this.ConnectionCallbackTargets.OnDisconnected(this.DisconnectedCause);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case StatusCode.DisconnectByServerUserLimit:
|
|
this.DebugReturn(DebugLevel.ERROR, "This connection was rejected due to the apps CCU limit.");
|
|
this.DisconnectedCause = DisconnectCause.MaxCcuReached;
|
|
this.State = ClientState.Disconnecting;
|
|
break;
|
|
case StatusCode.ExceptionOnConnect:
|
|
case StatusCode.SecurityExceptionOnConnect:
|
|
case StatusCode.EncryptionFailedToEstablish:
|
|
this.DisconnectedCause = DisconnectCause.ExceptionOnConnect;
|
|
|
|
// if enabled, the client can attempt to connect with another networking-protocol to check if that connects
|
|
if (this.EnableProtocolFallback && this.State == ClientState.ConnectingToNameServer)
|
|
{
|
|
this.State = ClientState.ConnectWithFallbackProtocol;
|
|
}
|
|
else
|
|
{
|
|
this.State = ClientState.Disconnecting;
|
|
}
|
|
break;
|
|
case StatusCode.Exception:
|
|
case StatusCode.ExceptionOnReceive:
|
|
case StatusCode.SendError:
|
|
this.DisconnectedCause = DisconnectCause.Exception;
|
|
this.State = ClientState.Disconnecting;
|
|
break;
|
|
case StatusCode.DisconnectByServerTimeout:
|
|
this.DisconnectedCause = DisconnectCause.ServerTimeout;
|
|
this.State = ClientState.Disconnecting;
|
|
break;
|
|
case StatusCode.DisconnectByServerLogic:
|
|
this.DisconnectedCause = DisconnectCause.DisconnectByServerLogic;
|
|
this.State = ClientState.Disconnecting;
|
|
break;
|
|
case StatusCode.DisconnectByServerReasonUnknown:
|
|
this.DisconnectedCause = DisconnectCause.DisconnectByServerReasonUnknown;
|
|
this.State = ClientState.Disconnecting;
|
|
break;
|
|
case StatusCode.TimeoutDisconnect:
|
|
this.DisconnectedCause = DisconnectCause.ClientTimeout;
|
|
|
|
// if enabled, the client can attempt to connect with another networking-protocol to check if that connects
|
|
if (this.EnableProtocolFallback && this.State == ClientState.ConnectingToNameServer)
|
|
{
|
|
this.State = ClientState.ConnectWithFallbackProtocol;
|
|
}
|
|
else
|
|
{
|
|
this.State = ClientState.Disconnecting;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Uses the photonEvent's provided by the server to advance the internal state and call ops as needed.
|
|
/// </summary>
|
|
/// <remarks>This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnEvent.</remarks>
|
|
public virtual void OnEvent(EventData photonEvent)
|
|
{
|
|
int actorNr = photonEvent.Sender;
|
|
Player originatingPlayer = (this.CurrentRoom != null) ? this.CurrentRoom.GetPlayer(actorNr) : null;
|
|
|
|
switch (photonEvent.Code)
|
|
{
|
|
case EventCode.GameList:
|
|
case EventCode.GameListUpdate:
|
|
|
|
List<RoomInfo> _RoomInfoList = new List<RoomInfo>();
|
|
|
|
Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList];
|
|
foreach (string gameName in games.Keys)
|
|
{
|
|
_RoomInfoList.Add(new RoomInfo(gameName, (Hashtable)games[gameName]));
|
|
}
|
|
|
|
this.LobbyCallbackTargets.OnRoomListUpdate(_RoomInfoList);
|
|
|
|
break;
|
|
|
|
case EventCode.Join:
|
|
Hashtable actorProperties = (Hashtable)photonEvent[ParameterCode.PlayerProperties];
|
|
|
|
if (originatingPlayer == null)
|
|
{
|
|
originatingPlayer = this.CreatePlayer(string.Empty, actorNr, false, actorProperties);
|
|
this.CurrentRoom.StorePlayer(originatingPlayer);
|
|
}
|
|
else
|
|
{
|
|
originatingPlayer.InternalCacheProperties(actorProperties);
|
|
originatingPlayer.IsInactive = false;
|
|
originatingPlayer.HasRejoined = actorNr != this.LocalPlayer.ActorNumber; // event is for non-local player, who is known (by ActorNumber), so it's a returning player
|
|
}
|
|
|
|
if (actorNr == this.LocalPlayer.ActorNumber)
|
|
{
|
|
// 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);
|
|
|
|
// any operation that does a "rejoin" will set this value to true. this can indicate if the local player returns to a room.
|
|
originatingPlayer.HasRejoined = this.enterRoomParamsCache.RejoinOnly;
|
|
|
|
// joinWithCreateOnDemand can turn an OpJoin into creating the room. Then actorNumber is 1 and callback: OnCreatedRoom()
|
|
if (this.lastJoinType == JoinType.CreateRoom || (this.lastJoinType == JoinType.JoinOrCreateRoom && this.LocalPlayer.ActorNumber == 1))
|
|
{
|
|
this.MatchMakingCallbackTargets.OnCreatedRoom();
|
|
}
|
|
|
|
this.MatchMakingCallbackTargets.OnJoinedRoom();
|
|
}
|
|
else
|
|
{
|
|
this.InRoomCallbackTargets.OnPlayerEnteredRoom(originatingPlayer);
|
|
}
|
|
break;
|
|
|
|
case EventCode.Leave:
|
|
if (originatingPlayer != null)
|
|
{
|
|
bool isInactive = false;
|
|
if (photonEvent.Parameters.ContainsKey(ParameterCode.IsInactive))
|
|
{
|
|
isInactive = (bool)photonEvent.Parameters[ParameterCode.IsInactive];
|
|
}
|
|
|
|
if (isInactive)
|
|
{
|
|
originatingPlayer.IsInactive = true;
|
|
}
|
|
else
|
|
{
|
|
originatingPlayer.IsInactive = false;
|
|
this.CurrentRoom.RemovePlayer(actorNr);
|
|
}
|
|
}
|
|
|
|
if (photonEvent.Parameters.ContainsKey(ParameterCode.MasterClientId))
|
|
{
|
|
int newMaster = (int)photonEvent[ParameterCode.MasterClientId];
|
|
if (newMaster != 0)
|
|
{
|
|
this.CurrentRoom.masterClientId = newMaster;
|
|
this.InRoomCallbackTargets.OnMasterClientSwitched(this.CurrentRoom.GetPlayer(newMaster));
|
|
}
|
|
}
|
|
// finally, send notification that a player left
|
|
this.InRoomCallbackTargets.OnPlayerLeftRoom(originatingPlayer);
|
|
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[];
|
|
int[] peers = photonEvent[ParameterCode.PeerCount] as int[];
|
|
int[] rooms = photonEvent[ParameterCode.GameCount] as int[];
|
|
|
|
byte[] types;
|
|
ByteArraySlice slice = photonEvent[ParameterCode.LobbyType] as ByteArraySlice;
|
|
bool useByteArraySlice = slice != null;
|
|
|
|
if (useByteArraySlice)
|
|
{
|
|
types = slice.Buffer;
|
|
}
|
|
else
|
|
{
|
|
types = photonEvent[ParameterCode.LobbyType] as byte[];
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if (useByteArraySlice)
|
|
{
|
|
slice.Release();
|
|
}
|
|
|
|
this.LobbyCallbackTargets.OnLobbyStatisticsUpdate(this.lobbyStatistics);
|
|
break;
|
|
|
|
case EventCode.ErrorInfo:
|
|
this.ErrorInfoCallbackTargets.OnErrorInfo(new ErrorInfo(photonEvent));
|
|
break;
|
|
|
|
case EventCode.AuthEvent:
|
|
if (this.AuthValues == null)
|
|
{
|
|
this.AuthValues = new AuthenticationValues();
|
|
}
|
|
|
|
this.AuthValues.Token = photonEvent[ParameterCode.Secret] as string;
|
|
this.tokenCache = this.AuthValues.Token;
|
|
break;
|
|
|
|
}
|
|
|
|
this.UpdateCallbackTargets();
|
|
if (this.EventReceived != null) this.EventReceived(photonEvent);
|
|
}
|
|
|
|
/// <summary>In Photon 4, "raw messages" will get their own callback method in the interface. Not used yet.</summary>
|
|
public virtual void OnMessage(object message)
|
|
{
|
|
this.DebugReturn(DebugLevel.ALL, string.Format("got OnMessage {0}", message));
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
/// <summary>A callback of the RegionHandler, provided in OnRegionListReceived.</summary>
|
|
/// <param name="regionHandler">The regionHandler wraps up best region and other region relevant info.</param>
|
|
private void OnRegionPingCompleted(RegionHandler regionHandler)
|
|
{
|
|
//Debug.Log("OnRegionPingCompleted " + regionHandler.BestRegion);
|
|
//Debug.Log("RegionPingSummary: " + regionHandler.SummaryToCache);
|
|
this.SummaryToCache = regionHandler.SummaryToCache;
|
|
this.ConnectToRegionMaster(regionHandler.BestRegion.Code);
|
|
}
|
|
|
|
private void SetupEncryption(Dictionary<byte, object> 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:
|
|
case EncryptionMode.DatagramEncryptionRandomSequence:
|
|
{
|
|
byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1];
|
|
byte[] secret2 = (byte[])encryptionData[EncryptionDataParameters.Secret2];
|
|
this.LoadBalancingPeer.InitDatagramEncryption(secret1, secret2, mode == EncryptionMode.DatagramEncryptionRandomSequence);
|
|
}
|
|
break;
|
|
case EncryptionMode.DatagramEncryptionGCMRandomSequence:
|
|
{
|
|
byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1];
|
|
this.LoadBalancingPeer.InitDatagramEncryption(secret1, null, true, true);
|
|
}
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// This operation makes Photon call your custom web-service by path/name with the given parameters (converted into Json).
|
|
/// Use <see cref="IWebRpcCallback.OnWebRpcResponse"/> as a callback.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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, <a href="https://doc.photonengine.com/en-us/realtime/current/reference/webhooks">
|
|
/// visit the Dashboard </a> and setup "WebHooks". The BaseUrl is used for WebRPCs as well.
|
|
///
|
|
/// The class <see cref="WebRpcResponse"/> is a helper-class that extracts the most valuable content from the WebRPC
|
|
/// response.
|
|
/// </remarks>
|
|
/// <param name="uriPath">The url path to call, relative to the baseUrl configured on Photon's server-side.</param>
|
|
/// <param name="parameters">The parameters to send to the web-service method.</param>
|
|
/// <param name="sendAuthCookie">Defines if the authentication cookie gets sent to a WebHook (if setup).</param>
|
|
public bool OpWebRpc(string uriPath, object parameters, bool sendAuthCookie = false)
|
|
{
|
|
if (string.IsNullOrEmpty(uriPath))
|
|
{
|
|
this.DebugReturn(DebugLevel.ERROR, "WebRPC method name must not be null nor empty.");
|
|
return false;
|
|
}
|
|
if (!this.CheckIfOpCanBeSent(OperationCode.WebRpc, this.Server, "WebRpc"))
|
|
{
|
|
return false;
|
|
}
|
|
Dictionary<byte, object> opParameters = new Dictionary<byte, object>();
|
|
opParameters.Add(ParameterCode.UriPath, uriPath);
|
|
if (parameters != null)
|
|
{
|
|
opParameters.Add(ParameterCode.WebRpcParameters, parameters);
|
|
}
|
|
if (sendAuthCookie)
|
|
{
|
|
opParameters.Add(ParameterCode.EventForward, WebFlags.SendAuthCookieConst);
|
|
}
|
|
|
|
//return this.LoadBalancingPeer.OpCustom(OperationCode.WebRpc, opParameters, true);
|
|
return this.LoadBalancingPeer.SendOperation(OperationCode.WebRpc, opParameters, SendOptions.SendReliable);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Registers an object for callbacks for the implemented callback-interfaces.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Adding and removing callback targets is queued to not mess with callbacks in execution.
|
|
/// Internally, this means that the addition/removal is done before the LoadBalancingClient
|
|
/// calls the next callbacks. This detail should not affect a game's workflow.
|
|
///
|
|
/// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks,
|
|
/// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback.
|
|
///
|
|
/// See: <a href="https://doc.photonengine.com/en-us/realtime/current/reference/dotnet-callbacks"/>
|
|
/// </remarks>
|
|
/// <param name="target">The object that registers to get callbacks from this client.</param>
|
|
public void AddCallbackTarget(object target)
|
|
{
|
|
this.callbackTargetChanges.Enqueue(new CallbackTargetChange(target, true));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters an object from callbacks for the implemented callback-interfaces.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Adding and removing callback targets is queued to not mess with callbacks in execution.
|
|
/// Internally, this means that the addition/removal is done before the LoadBalancingClient
|
|
/// calls the next callbacks. This detail should not affect a game's workflow.
|
|
///
|
|
/// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks,
|
|
/// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback.
|
|
///
|
|
/// See: <a href="https://doc.photonengine.com/en-us/realtime/current/reference/dotnet-callbacks"></a>
|
|
/// </remarks>
|
|
/// <param name="target">The object that unregisters from getting callbacks.</param>
|
|
public void RemoveCallbackTarget(object target)
|
|
{
|
|
this.callbackTargetChanges.Enqueue(new CallbackTargetChange(target, false));
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Applies queued callback cahnges from a queue to the actual containers. Will cause exceptions if used while callbacks execute.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// There is no explicit check that this is not called during callbacks, however the implemented, private logic takes care of this.
|
|
/// </remarks>
|
|
protected internal void UpdateCallbackTargets()
|
|
{
|
|
while (this.callbackTargetChanges.Count > 0)
|
|
{
|
|
CallbackTargetChange change = this.callbackTargetChanges.Dequeue();
|
|
|
|
if (change.AddTarget)
|
|
{
|
|
if (this.callbackTargets.Contains(change.Target))
|
|
{
|
|
//Debug.Log("UpdateCallbackTargets skipped adding a target, as the object is already registered. Target: " + change.Target);
|
|
continue;
|
|
}
|
|
|
|
this.callbackTargets.Add(change.Target);
|
|
}
|
|
else
|
|
{
|
|
if (!this.callbackTargets.Contains(change.Target))
|
|
{
|
|
//Debug.Log("UpdateCallbackTargets skipped removing a target, as the object is not registered. Target: " + change.Target);
|
|
continue;
|
|
}
|
|
|
|
this.callbackTargets.Remove(change.Target);
|
|
}
|
|
|
|
this.UpdateCallbackTarget<IInRoomCallbacks>(change, this.InRoomCallbackTargets);
|
|
this.UpdateCallbackTarget<IConnectionCallbacks>(change, this.ConnectionCallbackTargets);
|
|
this.UpdateCallbackTarget<IMatchmakingCallbacks>(change, this.MatchMakingCallbackTargets);
|
|
this.UpdateCallbackTarget<ILobbyCallbacks>(change, this.LobbyCallbackTargets);
|
|
this.UpdateCallbackTarget<IWebRpcCallback>(change, this.WebRpcCallbackTargets);
|
|
this.UpdateCallbackTarget<IErrorInfoCallback>(change, this.ErrorInfoCallbackTargets);
|
|
|
|
IOnEventCallback onEventCallback = change.Target as IOnEventCallback;
|
|
if (onEventCallback != null)
|
|
{
|
|
if (change.AddTarget)
|
|
{
|
|
EventReceived += onEventCallback.OnEvent;
|
|
}
|
|
else
|
|
{
|
|
EventReceived -= onEventCallback.OnEvent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Helper method to cast and apply a target per (interface) type.</summary>
|
|
/// <typeparam name="T">Either of the interfaces for callbacks.</typeparam>
|
|
/// <param name="change">The queued change to apply (add or remove) some target.</param>
|
|
/// <param name="container">The container that calls callbacks on it's list of targets.</param>
|
|
private void UpdateCallbackTarget<T>(CallbackTargetChange change, List<T> container) where T : class
|
|
{
|
|
T target = change.Target as T;
|
|
if (target != null)
|
|
{
|
|
if (change.AddTarget)
|
|
{
|
|
container.Add(target);
|
|
}
|
|
else
|
|
{
|
|
container.Remove(target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Collection of "organizational" callbacks for the Realtime Api to cover: Connection and Regions.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Classes that implement this interface must be registered to get callbacks for various situations.
|
|
///
|
|
/// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
|
|
/// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
|
|
///
|
|
/// </remarks>
|
|
/// \ingroup callbacks
|
|
public interface IConnectionCallbacks
|
|
{
|
|
/// <summary>
|
|
/// Called to signal that the "low level connection" got established but before the client can call operation on the server.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// After the (low level transport) connection is established, the client will automatically send
|
|
/// the Authentication operation, which needs to get a response before the client can call other operations.
|
|
///
|
|
/// Your logic should wait for either: OnRegionListReceived or OnConnectedToMaster.
|
|
///
|
|
/// This callback is useful to detect if the server can be reached at all (technically).
|
|
/// Most often, it's enough to implement OnDisconnected(DisconnectCause cause) and check for the cause.
|
|
///
|
|
/// This is not called for transitions from the masterserver to game servers.
|
|
/// </remarks>
|
|
void OnConnected();
|
|
|
|
/// <summary>
|
|
/// Called when the client is connected to the Master Server and ready for matchmaking and other tasks.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The list of available rooms won't become available unless you join a lobby via LoadBalancingClient.OpJoinLobby.
|
|
/// You can join rooms and create them even without being in a lobby. The default lobby is used in that case.
|
|
/// </remarks>
|
|
void OnConnectedToMaster();
|
|
|
|
/// <summary>
|
|
/// Called after disconnecting from the Photon server. It could be a failure or an explicit disconnect call
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The reason for this disconnect is provided as DisconnectCause.
|
|
/// </remarks>
|
|
void OnDisconnected(DisconnectCause cause);
|
|
|
|
/// <summary>
|
|
/// Called when the Name Server provided a list of regions for your title.
|
|
/// </summary>
|
|
/// <remarks>Check the RegionHandler class description, to make use of the provided values.</remarks>
|
|
/// <param name="regionHandler">The currently used RegionHandler.</param>
|
|
void OnRegionListReceived(RegionHandler regionHandler);
|
|
|
|
|
|
/// <summary>
|
|
/// Called when your Custom Authentication service responds with additional data.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Custom Authentication services can include some custom data in their response.
|
|
/// When present, that data is made available in this callback as Dictionary.
|
|
/// While the keys of your data have to be strings, the values can be either string or a number (in Json).
|
|
/// You need to make extra sure, that the value type is the one you expect. Numbers become (currently) int64.
|
|
///
|
|
/// Example: void OnCustomAuthenticationResponse(Dictionary<string, object> data) { ... }
|
|
/// </remarks>
|
|
/// <see cref="https://doc.photonengine.com/en-us/realtime/current/reference/custom-authentication"/>
|
|
void OnCustomAuthenticationResponse(Dictionary<string, object> data);
|
|
|
|
/// <summary>
|
|
/// Called when the custom authentication failed. Followed by disconnect!
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Custom Authentication can fail due to user-input, bad tokens/secrets.
|
|
/// If authentication is successful, this method is not called. Implement OnJoinedLobby() or OnConnectedToMaster() (as usual).
|
|
///
|
|
/// During development of a game, it might also fail due to wrong configuration on the server side.
|
|
/// In those cases, logging the debugMessage is very important.
|
|
///
|
|
/// Unless you setup a custom authentication service for your app (in the [Dashboard](https://dashboard.photonengine.com)),
|
|
/// this won't be called!
|
|
/// </remarks>
|
|
/// <param name="debugMessage">Contains a debug message why authentication failed. This has to be fixed during development.</param>
|
|
void OnCustomAuthenticationFailed(string debugMessage);
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Collection of "organizational" callbacks for the Realtime Api to cover the Lobby.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Classes that implement this interface must be registered to get callbacks for various situations.
|
|
///
|
|
/// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
|
|
/// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
|
|
///
|
|
/// </remarks>
|
|
/// \ingroup callbacks
|
|
public interface ILobbyCallbacks
|
|
{
|
|
|
|
/// <summary>
|
|
/// Called on entering a lobby on the Master Server. The actual room-list updates will call OnRoomListUpdate.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// While in the lobby, the roomlist is automatically updated in fixed intervals (which you can't modify in the public cloud).
|
|
/// The room list gets available via OnRoomListUpdate.
|
|
/// </remarks>
|
|
void OnJoinedLobby();
|
|
|
|
/// <summary>
|
|
/// Called after leaving a lobby.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When you leave a lobby, [OpCreateRoom](@ref OpCreateRoom) and [OpJoinRandomRoom](@ref OpJoinRandomRoom)
|
|
/// automatically refer to the default lobby.
|
|
/// </remarks>
|
|
void OnLeftLobby();
|
|
|
|
/// <summary>
|
|
/// Called for any update of the room-listing while in a lobby (InLobby) on the Master Server.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Each item is a RoomInfo which might include custom properties (provided you defined those as lobby-listed when creating a room).
|
|
/// Not all types of lobbies provide a listing of rooms to the client. Some are silent and specialized for server-side matchmaking.
|
|
/// </remarks>
|
|
void OnRoomListUpdate(List<RoomInfo> roomList);
|
|
|
|
/// <summary>
|
|
/// Called when the Master Server sent an update for the Lobby Statistics.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This callback has two preconditions:
|
|
/// EnableLobbyStatistics must be set to true, before this client connects.
|
|
/// And the client has to be connected to the Master Server, which is providing the info about lobbies.
|
|
/// </remarks>
|
|
void OnLobbyStatisticsUpdate(List<TypedLobbyInfo> lobbyStatistics);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Collection of "organizational" callbacks for the Realtime Api to cover Matchmaking.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Classes that implement this interface must be registered to get callbacks for various situations.
|
|
///
|
|
/// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
|
|
/// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
|
|
///
|
|
/// </remarks>
|
|
/// \ingroup callbacks
|
|
public interface IMatchmakingCallbacks
|
|
{
|
|
|
|
/// <summary>
|
|
/// Called when the server sent the response to a FindFriends request.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// After calling OpFindFriends, the Master Server will cache the friend list and send updates to the friend
|
|
/// list. The friends includes the name, userId, online state and the room (if any) for each requested user/friend.
|
|
///
|
|
/// Use the friendList to update your UI and store it, if the UI should highlight changes.
|
|
/// </remarks>
|
|
void OnFriendListUpdate(List<FriendInfo> friendList);
|
|
|
|
/// <summary>
|
|
/// Called when this client created a room and entered it. OnJoinedRoom() will be called as well.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This callback is only called on the client which created a room (see OpCreateRoom).
|
|
///
|
|
/// As any client might close (or drop connection) anytime, there is a chance that the
|
|
/// creator of a room does not execute OnCreatedRoom.
|
|
///
|
|
/// If you need specific room properties or a "start signal", implement OnMasterClientSwitched()
|
|
/// and make each new MasterClient check the room's state.
|
|
/// </remarks>
|
|
void OnCreatedRoom();
|
|
|
|
/// <summary>
|
|
/// Called when the server couldn't create a room (OpCreateRoom failed).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Creating a room may fail for various reasons. Most often, the room already exists (roomname in use) or
|
|
/// the RoomOptions clash and it's impossible to create the room.
|
|
///
|
|
/// When creating a room fails on a Game Server:
|
|
/// The client will cache the failure internally and returns to the Master Server before it calls the fail-callback.
|
|
/// This way, the client is ready to find/create a room at the moment of the callback.
|
|
/// In this case, the client skips calling OnConnectedToMaster but returning to the Master Server will still call OnConnected.
|
|
/// Treat callbacks of OnConnected as pure information that the client could connect.
|
|
/// </remarks>
|
|
/// <param name="returnCode">Operation ReturnCode from the server.</param>
|
|
/// <param name="message">Debug message for the error.</param>
|
|
void OnCreateRoomFailed(short returnCode, string message);
|
|
|
|
/// <summary>
|
|
/// Called when the LoadBalancingClient entered a room, no matter if this client created it or simply joined.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When this is called, you can access the existing players in Room.Players, their custom properties and Room.CustomProperties.
|
|
///
|
|
/// In this callback, you could create player objects. For example in Unity, instantiate a prefab for the player.
|
|
///
|
|
/// If you want a match to be started "actively", enable the user to signal "ready" (using OpRaiseEvent or a Custom Property).
|
|
/// </remarks>
|
|
void OnJoinedRoom();
|
|
|
|
/// <summary>
|
|
/// Called when a previous OpJoinRoom call failed on the server.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Joining a room may fail for various reasons. Most often, the room is full or does not exist anymore
|
|
/// (due to someone else being faster or closing the room).
|
|
///
|
|
/// When joining a room fails on a Game Server:
|
|
/// The client will cache the failure internally and returns to the Master Server before it calls the fail-callback.
|
|
/// This way, the client is ready to find/create a room at the moment of the callback.
|
|
/// In this case, the client skips calling OnConnectedToMaster but returning to the Master Server will still call OnConnected.
|
|
/// Treat callbacks of OnConnected as pure information that the client could connect.
|
|
/// </remarks>
|
|
/// <param name="returnCode">Operation ReturnCode from the server.</param>
|
|
/// <param name="message">Debug message for the error.</param>
|
|
void OnJoinRoomFailed(short returnCode, string message);
|
|
|
|
/// <summary>
|
|
/// Called when a previous OpJoinRandom call failed on the server.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The most common causes are that a room is full or does not exist (due to someone else being faster or closing the room).
|
|
///
|
|
/// This operation is only ever sent to the Master Server. Once a room is found by the Master Server, the client will
|
|
/// head off to the designated Game Server and use the operation Join on the Game Server.
|
|
///
|
|
/// When using multiple lobbies (via OpJoinLobby or a TypedLobby parameter), another lobby might have more/fitting rooms.<br/>
|
|
/// </remarks>
|
|
/// <param name="returnCode">Operation ReturnCode from the server.</param>
|
|
/// <param name="message">Debug message for the error.</param>
|
|
void OnJoinRandomFailed(short returnCode, string message);
|
|
|
|
/// <summary>
|
|
/// Called when the local user/client left a room, so the game's logic can clean up it's internal state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When leaving a room, the LoadBalancingClient will disconnect the Game Server and connect to the Master Server.
|
|
/// This wraps up multiple internal actions.
|
|
///
|
|
/// Wait for the callback OnConnectedToMaster, before you use lobbies and join or create rooms.
|
|
/// </remarks>
|
|
void OnLeftRoom();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collection of "in room" callbacks for the Realtime Api to cover: Players entering or leaving, property updates and Master Client switching.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Classes that implement this interface must be registered to get callbacks for various situations.
|
|
///
|
|
/// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
|
|
/// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
|
|
///
|
|
/// </remarks>
|
|
/// \ingroup callbacks
|
|
public interface IInRoomCallbacks
|
|
{
|
|
/// <summary>
|
|
/// Called when a remote player entered the room. This Player is already added to the playerlist.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If your game starts with a certain number of players, this callback can be useful to check the
|
|
/// Room.playerCount and find out if you can start.
|
|
/// </remarks>
|
|
void OnPlayerEnteredRoom(Player newPlayer);
|
|
|
|
/// <summary>
|
|
/// Called when a remote player left the room or became inactive. Check otherPlayer.IsInactive.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If another player leaves the room or if the server detects a lost connection, this callback will
|
|
/// be used to notify your game logic.
|
|
///
|
|
/// Depending on the room's setup, players may become inactive, which means they may return and retake
|
|
/// their spot in the room. In such cases, the Player stays in the Room.Players dictionary.
|
|
///
|
|
/// If the player is not just inactive, it gets removed from the Room.Players dictionary, before
|
|
/// the callback is called.
|
|
/// </remarks>
|
|
void OnPlayerLeftRoom(Player otherPlayer);
|
|
|
|
|
|
/// <summary>
|
|
/// Called when a room's custom properties changed. The propertiesThatChanged contains all that was set via Room.SetCustomProperties.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Since v1.25 this method has one parameter: Hashtable propertiesThatChanged.<br/>
|
|
/// Changing properties must be done by Room.SetCustomProperties, which causes this callback locally, too.
|
|
/// </remarks>
|
|
/// <param name="propertiesThatChanged"></param>
|
|
void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged);
|
|
|
|
/// <summary>
|
|
/// Called when custom player-properties are changed. Player and the changed properties are passed as object[].
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Changing properties must be done by Player.SetCustomProperties, which causes this callback locally, too.
|
|
/// </remarks>
|
|
/// <param name="targetPlayer">Contains Player that changed.</param>
|
|
/// <param name="changedProps">Contains the properties that changed.</param>
|
|
void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps);
|
|
|
|
/// <summary>
|
|
/// Called after switching to a new MasterClient when the current one leaves.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is not called when this client enters a room.
|
|
/// The former MasterClient is still in the player list when this method get called.
|
|
/// </remarks>
|
|
void OnMasterClientSwitched(Player newMasterClient);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Event callback for the Realtime Api. Covers events from the server and those sent by clients via OpRaiseEvent.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Classes that implement this interface must be registered to get callbacks for various situations.
|
|
///
|
|
/// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
|
|
/// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
|
|
///
|
|
/// </remarks>
|
|
/// \ingroup callbacks
|
|
public interface IOnEventCallback
|
|
{
|
|
/// <summary>Called for any incoming events.</summary>
|
|
/// <remarks>
|
|
/// To receive events, implement IOnEventCallback in any class and register it via AddCallbackTarget
|
|
/// (either in LoadBalancingClient or PhotonNetwork).
|
|
///
|
|
/// With the EventData.Sender you can look up the Player who sent the event.
|
|
///
|
|
/// It is best practice to assign an eventCode for each different type of content and action, so the Code
|
|
/// will be essential to read the incoming events.
|
|
/// </remarks>
|
|
void OnEvent(EventData photonEvent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interface for "WebRpc" callbacks for the Realtime Api. Currently includes only responses for Web RPCs.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Classes that implement this interface must be registered to get callbacks for various situations.
|
|
///
|
|
/// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
|
|
/// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
|
|
///
|
|
/// </remarks>
|
|
/// \ingroup callbacks
|
|
public interface IWebRpcCallback
|
|
{
|
|
/// <summary>
|
|
/// Called when the response to a WebRPC is available. See <see cref="LoadBalancingClient.OpWebRpc"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Important: The response.ReturnCode is 0 if Photon was able to reach your web-service.<br/>
|
|
/// The content of the response is what your web-service sent. You can create a WebRpcResponse from it.<br/>
|
|
/// Example: WebRpcResponse webResponse = new WebRpcResponse(operationResponse);<br/>
|
|
///
|
|
/// Please note: Class OperationResponse is in a namespace which needs to be "used":<br/>
|
|
/// using ExitGames.Client.Photon; // includes OperationResponse (and other classes)
|
|
/// </remarks>
|
|
/// <example>
|
|
/// public void OnWebRpcResponse(OperationResponse response)
|
|
/// {
|
|
/// Debug.LogFormat("WebRPC operation response {0}", response.ToStringFull());
|
|
/// switch (response.ReturnCode)
|
|
/// {
|
|
/// case ErrorCode.Ok:
|
|
/// WebRpcResponse webRpcResponse = new WebRpcResponse(response);
|
|
/// Debug.LogFormat("Parsed WebRPC response {0}", response.ToStringFull());
|
|
/// if (string.IsNullOrEmpty(webRpcResponse.Name))
|
|
/// {
|
|
/// Debug.LogError("Unexpected: WebRPC response did not contain WebRPC method name");
|
|
/// }
|
|
/// if (webRpcResponse.ResultCode == 0) // success
|
|
/// {
|
|
/// switch (webRpcResponse.Name)
|
|
/// {
|
|
/// // todo: add your code here
|
|
/// case GetGameListWebRpcMethodName: // example
|
|
/// // ...
|
|
/// break;
|
|
/// }
|
|
/// }
|
|
/// else if (webRpcResponse.ResultCode == -1)
|
|
/// {
|
|
/// Debug.LogErrorFormat("Web server did not return ResultCode for WebRPC method=\"{0}\", Message={1}", webRpcResponse.Name, webRpcResponse.Message);
|
|
/// }
|
|
/// else
|
|
/// {
|
|
/// Debug.LogErrorFormat("Web server returned ResultCode={0} for WebRPC method=\"{1}\", Message={2}", webRpcResponse.ResultCode, webRpcResponse.Name, webRpcResponse.Message);
|
|
/// }
|
|
/// break;
|
|
/// case ErrorCode.ExternalHttpCallFailed: // web service unreachable
|
|
/// Debug.LogErrorFormat("WebRPC call failed as request could not be sent to the server. {0}", response.DebugMessage);
|
|
/// break;
|
|
/// case ErrorCode.HttpLimitReached: // too many WebRPCs in a short period of time
|
|
/// // the debug message should contain the limit exceeded
|
|
/// Debug.LogErrorFormat("WebRPCs rate limit exceeded: {0}", response.DebugMessage);
|
|
/// break;
|
|
/// case ErrorCode.InvalidOperation: // WebRPC not configured at all OR not configured properly OR trying to send on name server
|
|
/// if (PhotonNetwork.Server == ServerConnection.NameServer)
|
|
/// {
|
|
/// Debug.LogErrorFormat("WebRPC not supported on NameServer. {0}", response.DebugMessage);
|
|
/// }
|
|
/// else
|
|
/// {
|
|
/// Debug.LogErrorFormat("WebRPC not properly configured or not configured at all. {0}", response.DebugMessage);
|
|
/// }
|
|
/// break;
|
|
/// default:
|
|
/// // other unknown error, unexpected
|
|
/// Debug.LogErrorFormat("Unexpected error, {0} {1}", response.ReturnCode, response.DebugMessage);
|
|
/// break;
|
|
/// }
|
|
/// }
|
|
///
|
|
/// </example>
|
|
void OnWebRpcResponse(OperationResponse response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interface for <see cref="EventCode.ErrorInfo"/> event callback for the Realtime Api.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Classes that implement this interface must be registered to get callbacks for various situations.
|
|
///
|
|
/// To register for callbacks, call <see cref="LoadBalancingClient.AddCallbackTarget"/> and pass the class implementing this interface
|
|
/// To stop getting callbacks, call <see cref="LoadBalancingClient.RemoveCallbackTarget"/> and pass the class implementing this interface
|
|
///
|
|
/// </remarks>
|
|
/// \ingroup callbacks
|
|
public interface IErrorInfoCallback
|
|
{
|
|
/// <summary>
|
|
/// Called when the client receives an event from the server indicating that an error happened there.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// In most cases this could be either:
|
|
/// 1. an error from webhooks plugin (if HasErrorInfo is enabled), read more here:
|
|
/// https://doc.photonengine.com/en-us/realtime/current/gameplay/web-extensions/webhooks#options
|
|
/// 2. an error sent from a custom server plugin via PluginHost.BroadcastErrorInfoEvent, see example here:
|
|
/// https://doc.photonengine.com/en-us/server/current/plugins/manual#handling_http_response
|
|
/// 3. an error sent from the server, for example, when the limit of cached events has been exceeded in the room
|
|
/// (all clients will be disconnected and the room will be closed in this case)
|
|
/// read more here: https://doc.photonengine.com/en-us/realtime/current/gameplay/cached-events#special_considerations
|
|
///
|
|
/// If you implement <see cref="IOnEventCallback.OnEvent"/> or <see cref="LoadBalancingClient.EventReceived"/> you will also get this event.
|
|
/// </remarks>
|
|
/// <param name="errorInfo">Object containing information about the error</param>
|
|
void OnErrorInfo(ErrorInfo errorInfo);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Container type for callbacks defined by IConnectionCallbacks. See LoadBalancingCallbackTargets.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// While the interfaces of callbacks wrap up the methods that will be called,
|
|
/// the container classes implement a simple way to call a method on all registered objects.
|
|
/// </remarks>
|
|
public class ConnectionCallbacksContainer : List<IConnectionCallbacks>, IConnectionCallbacks
|
|
{
|
|
private readonly LoadBalancingClient client;
|
|
|
|
public ConnectionCallbacksContainer(LoadBalancingClient client)
|
|
{
|
|
this.client = client;
|
|
}
|
|
|
|
public void OnConnected()
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IConnectionCallbacks target in this)
|
|
{
|
|
target.OnConnected();
|
|
}
|
|
}
|
|
|
|
public void OnConnectedToMaster()
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IConnectionCallbacks target in this)
|
|
{
|
|
target.OnConnectedToMaster();
|
|
}
|
|
}
|
|
|
|
public void OnRegionListReceived(RegionHandler regionHandler)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IConnectionCallbacks target in this)
|
|
{
|
|
target.OnRegionListReceived(regionHandler);
|
|
}
|
|
}
|
|
|
|
public void OnDisconnected(DisconnectCause cause)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IConnectionCallbacks target in this)
|
|
{
|
|
target.OnDisconnected(cause);
|
|
}
|
|
}
|
|
|
|
public void OnCustomAuthenticationResponse(Dictionary<string, object> data)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IConnectionCallbacks target in this)
|
|
{
|
|
target.OnCustomAuthenticationResponse(data);
|
|
}
|
|
}
|
|
|
|
public void OnCustomAuthenticationFailed(string debugMessage)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IConnectionCallbacks target in this)
|
|
{
|
|
target.OnCustomAuthenticationFailed(debugMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Container type for callbacks defined by IMatchmakingCallbacks. See MatchMakingCallbackTargets.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// While the interfaces of callbacks wrap up the methods that will be called,
|
|
/// the container classes implement a simple way to call a method on all registered objects.
|
|
/// </remarks>
|
|
public class MatchMakingCallbacksContainer : List<IMatchmakingCallbacks>, IMatchmakingCallbacks
|
|
{
|
|
private readonly LoadBalancingClient client;
|
|
|
|
public MatchMakingCallbacksContainer(LoadBalancingClient client)
|
|
{
|
|
this.client = client;
|
|
}
|
|
|
|
public void OnCreatedRoom()
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IMatchmakingCallbacks target in this)
|
|
{
|
|
target.OnCreatedRoom();
|
|
}
|
|
}
|
|
|
|
public void OnJoinedRoom()
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IMatchmakingCallbacks target in this)
|
|
{
|
|
target.OnJoinedRoom();
|
|
}
|
|
}
|
|
|
|
public void OnCreateRoomFailed(short returnCode, string message)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IMatchmakingCallbacks target in this)
|
|
{
|
|
target.OnCreateRoomFailed(returnCode, message);
|
|
}
|
|
}
|
|
|
|
public void OnJoinRandomFailed(short returnCode, string message)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IMatchmakingCallbacks target in this)
|
|
{
|
|
target.OnJoinRandomFailed(returnCode, message);
|
|
}
|
|
}
|
|
|
|
public void OnJoinRoomFailed(short returnCode, string message)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IMatchmakingCallbacks target in this)
|
|
{
|
|
target.OnJoinRoomFailed(returnCode, message);
|
|
}
|
|
}
|
|
|
|
public void OnLeftRoom()
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IMatchmakingCallbacks target in this)
|
|
{
|
|
target.OnLeftRoom();
|
|
}
|
|
}
|
|
|
|
public void OnFriendListUpdate(List<FriendInfo> friendList)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IMatchmakingCallbacks target in this)
|
|
{
|
|
target.OnFriendListUpdate(friendList);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Container type for callbacks defined by IInRoomCallbacks. See InRoomCallbackTargets.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// While the interfaces of callbacks wrap up the methods that will be called,
|
|
/// the container classes implement a simple way to call a method on all registered objects.
|
|
/// </remarks>
|
|
internal class InRoomCallbacksContainer : List<IInRoomCallbacks>, IInRoomCallbacks
|
|
{
|
|
private readonly LoadBalancingClient client;
|
|
|
|
public InRoomCallbacksContainer(LoadBalancingClient client)
|
|
{
|
|
this.client = client;
|
|
}
|
|
|
|
public void OnPlayerEnteredRoom(Player newPlayer)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IInRoomCallbacks target in this)
|
|
{
|
|
target.OnPlayerEnteredRoom(newPlayer);
|
|
}
|
|
}
|
|
|
|
public void OnPlayerLeftRoom(Player otherPlayer)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IInRoomCallbacks target in this)
|
|
{
|
|
target.OnPlayerLeftRoom(otherPlayer);
|
|
}
|
|
}
|
|
|
|
public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IInRoomCallbacks target in this)
|
|
{
|
|
target.OnRoomPropertiesUpdate(propertiesThatChanged);
|
|
}
|
|
}
|
|
|
|
public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProp)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IInRoomCallbacks target in this)
|
|
{
|
|
target.OnPlayerPropertiesUpdate(targetPlayer, changedProp);
|
|
}
|
|
}
|
|
|
|
public void OnMasterClientSwitched(Player newMasterClient)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IInRoomCallbacks target in this)
|
|
{
|
|
target.OnMasterClientSwitched(newMasterClient);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Container type for callbacks defined by ILobbyCallbacks. See LobbyCallbackTargets.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// While the interfaces of callbacks wrap up the methods that will be called,
|
|
/// the container classes implement a simple way to call a method on all registered objects.
|
|
/// </remarks>
|
|
internal class LobbyCallbacksContainer : List<ILobbyCallbacks>, ILobbyCallbacks
|
|
{
|
|
private readonly LoadBalancingClient client;
|
|
|
|
public LobbyCallbacksContainer(LoadBalancingClient client)
|
|
{
|
|
this.client = client;
|
|
}
|
|
|
|
public void OnJoinedLobby()
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (ILobbyCallbacks target in this)
|
|
{
|
|
target.OnJoinedLobby();
|
|
}
|
|
}
|
|
|
|
public void OnLeftLobby()
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (ILobbyCallbacks target in this)
|
|
{
|
|
target.OnLeftLobby();
|
|
}
|
|
}
|
|
|
|
public void OnRoomListUpdate(List<RoomInfo> roomList)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (ILobbyCallbacks target in this)
|
|
{
|
|
target.OnRoomListUpdate(roomList);
|
|
}
|
|
}
|
|
|
|
public void OnLobbyStatisticsUpdate(List<TypedLobbyInfo> lobbyStatistics)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (ILobbyCallbacks target in this)
|
|
{
|
|
target.OnLobbyStatisticsUpdate(lobbyStatistics);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Container type for callbacks defined by IWebRpcCallback. See WebRpcCallbackTargets.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// While the interfaces of callbacks wrap up the methods that will be called,
|
|
/// the container classes implement a simple way to call a method on all registered objects.
|
|
/// </remarks>
|
|
internal class WebRpcCallbacksContainer : List<IWebRpcCallback>, IWebRpcCallback
|
|
{
|
|
private LoadBalancingClient client;
|
|
|
|
public WebRpcCallbacksContainer(LoadBalancingClient client)
|
|
{
|
|
this.client = client;
|
|
}
|
|
|
|
public void OnWebRpcResponse(OperationResponse response)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
|
|
foreach (IWebRpcCallback target in this)
|
|
{
|
|
target.OnWebRpcResponse(response);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Container type for callbacks defined by <see cref="IErrorInfoCallback"/>. See <see cref="LoadBalancingClient.ErrorInfoCallbackTargets"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// While the interfaces of callbacks wrap up the methods that will be called,
|
|
/// the container classes implement a simple way to call a method on all registered objects.
|
|
/// </remarks>
|
|
internal class ErrorInfoCallbacksContainer : List<IErrorInfoCallback>, IErrorInfoCallback
|
|
{
|
|
private LoadBalancingClient client;
|
|
|
|
public ErrorInfoCallbacksContainer(LoadBalancingClient client)
|
|
{
|
|
this.client = client;
|
|
}
|
|
|
|
public void OnErrorInfo(ErrorInfo errorInfo)
|
|
{
|
|
this.client.UpdateCallbackTargets();
|
|
foreach (IErrorInfoCallback target in this)
|
|
{
|
|
target.OnErrorInfo(errorInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Class wrapping the received <see cref="EventCode.ErrorInfo"/> event.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is passed inside <see cref="IErrorInfoCallback.OnErrorInfo"/> callback.
|
|
/// If you implement <see cref="IOnEventCallback.OnEvent"/> or <see cref="LoadBalancingClient.EventReceived"/> you will also get <see cref="EventCode.ErrorInfo"/> but not parsed.
|
|
///
|
|
/// In most cases this could be either:
|
|
/// 1. an error from webhooks plugin (if HasErrorInfo is enabled), read more here:
|
|
/// https://doc.photonengine.com/en-us/realtime/current/gameplay/web-extensions/webhooks#options
|
|
/// 2. an error sent from a custom server plugin via PluginHost.BroadcastErrorInfoEvent, see example here:
|
|
/// https://doc.photonengine.com/en-us/server/current/plugins/manual#handling_http_response
|
|
/// 3. an error sent from the server, for example, when the limit of cached events has been exceeded in the room
|
|
/// (all clients will be disconnected and the room will be closed in this case)
|
|
/// read more here: https://doc.photonengine.com/en-us/realtime/current/gameplay/cached-events#special_considerations
|
|
/// </remarks>
|
|
public class ErrorInfo
|
|
{
|
|
/// <summary>
|
|
/// String containing information about the error.
|
|
/// </summary>
|
|
public readonly string Info;
|
|
|
|
public ErrorInfo(EventData eventData)
|
|
{
|
|
this.Info = eventData[ParameterCode.Info] as string;
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return string.Format("ErrorInfo: {0}", this.Info);
|
|
}
|
|
}
|
|
}
|