浏览代码

Renamed AuthenticationManager to Auth to avoid conflct with a System API

Reframed the LobbyManager to maintain the life cycle of the remote Lobby.
-Is now responsible for maintaining the reference to the latest cache of the remote Lobby, 
-Will automatically set up the heartbeat for host,
-Also Works to prevent RateLimit Errors by creating cooldowns for each Lobby API Call
-Adjusted Roundtrip tests accordingly.

Moved Data synchronizastion to LobbySynchronizer
-Checks for local and remote changes and pushes/pulls player and lobby data accordingly.
/main/staging/2021_Upgrade/Async_Refactor
当前提交
6702d4b4
共有 12 个文件被更改,包括 569 次插入486 次删除
  1. 22
      Assets/Scripts/GameLobby/Game/GameManager.cs
  2. 3
      Assets/Scripts/GameLobby/Game/LocalLobby.cs
  3. 4
      Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs
  4. 641
      Assets/Scripts/GameLobby/Lobby/LobbyManager.cs
  5. 100
      Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs
  6. 2
      Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs
  7. 262
      Assets/Scripts/GameLobby/Tests/PlayMode/LobbyRoundtripTests.cs
  8. 1
      Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs
  9. 18
      Assets/Scripts/GameLobby/Tests/PlayMode/UtpTests.cs
  10. 2
      Assets/Scripts/GameLobby/UI/RateLimitVisibility.cs
  11. 0
      /Assets/Scripts/GameLobby/Auth/Auth.cs
  12. 0
      /Assets/Scripts/GameLobby/Auth/Auth.cs.meta

22
Assets/Scripts/GameLobby/Game/GameManager.cs


#pragma warning restore IDE0059
Application.wantsToQuit += OnWantToQuit;
LobbyManager = new LobbyManager();
m_LobbySynchronizer = new LobbySynchronizer(LobbyManager);
InitializeLobbies();
InitializeLocalValues();
StartVivoxLogin();
Locator.Get.Messenger.Subscribe(this);
BeginObservers();

await Auth.Authenticate(serviceProfileName);
}
void InitializeLobbies()
void InitializeLocalValues()
m_LocalLobby
.AddPlayer(m_LocalUser); // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens.
LobbyManager = new LobbyManager();
m_LobbySynchronizer = new LobbySynchronizer(LobbyManager);
m_LocalLobby.AddPlayer(m_LocalUser); // The local LobbyUser object will be hooked into UI
// before the LocalLobby is populated during lobby join,
// so the LocalLobby must know about it already when that happens.
}
/// <summary>

void OnJoinedLobby()
{
m_LobbySynchronizer.BeginTracking(m_LocalLobby, m_LocalUser);
m_LobbySynchronizer.StartSynch(m_LocalLobby, m_LocalUser);
SetUserLobbyState();
// The host has the opportunity to reject incoming players, but to do so the player needs to connect to Relay without having game logic available.

{
m_LocalUser.ResetState();
#pragma warning disable 4014
LobbyManager.LeaveLobbyAsync(m_LocalLobby.LobbyID);
LobbyManager.LeaveLobbyAsync();
m_LobbySynchronizer.EndTracking();
m_LobbySynchronizer.EndSynch();
m_VivoxSetup.LeaveLobbyChannel();
if (m_RelaySetup != null)

if (!string.IsNullOrEmpty(m_LocalLobby?.LobbyID))
{
#pragma warning disable 4014
LobbyManager.LeaveLobbyAsync(m_LocalLobby?.LobbyID);
LobbyManager.LeaveLobbyAsync();
#pragma warning restore 4014
m_LocalLobby = null;
}

3
Assets/Scripts/GameLobby/Game/LocalLobby.cs


using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Serialization;
namespace LobbyRelaySample
{

{
Dictionary<string, LobbyUser> m_LobbyUsers = new Dictionary<string, LobbyUser>();
public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers;
public bool canPullUpdate;
public bool changedByLobbySynch;
#region LocalLobbyData
public struct LobbyData

4
Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs


return data;
data.Add("DisplayName", user.DisplayName); // The lobby doesn't need to know any data beyond the name and state; Relay will handle the rest.
data.Add("UserStatus", ((int)user.UserStatus).ToString());
data.Add("Emote", user.Emote.ToString());
data.Add("Emote", ((int)user.Emote).ToString());
return data;
}

Private = remoteLobby.IsPrivate,
LobbyName = remoteLobby.Name,
MaxPlayerCount = remoteLobby.MaxPlayers,
State_LastEdit = remoteLobby.LastUpdated.Ticks,
State_LastEdit = remoteLobby.Data?.ContainsKey("State_LastEdit") == true ? long.Parse(remoteLobby.Data["State_LastEdit"].Value) : localLobbyToUpdate.Data.State_LastEdit,
Color_LastEdit = remoteLobby.Data?.ContainsKey("Color_LastEdit") == true ? long.Parse(remoteLobby.Data["Color_LastEdit"].Value) : localLobbyToUpdate.Data.Color_LastEdit,
RelayNGOCode_LastEdit = remoteLobby.Data?.ContainsKey("RelayNGOCode_LastEdit") == true ? long.Parse(remoteLobby.Data["RelayNGOCode_LastEdit"].Value) : localLobbyToUpdate.Data.RelayNGOCode_LastEdit
};

641
Assets/Scripts/GameLobby/Lobby/LobbyManager.cs


namespace LobbyRelaySample
{
/// <summary>
/// An abstraction layer between the direct calls into the Lobby API and the outcomes you actually want. E.g. you can request to get a readable list of
/// current lobbies and not need to make the query call directly.
/// </summary>
public class LobbyManager: IDisposable
{
/// <summary>
/// An abstraction layer between the direct calls into the Lobby API and the outcomes you actually want. E.g. you can request to get a readable list of
/// current lobbies and not need to make the query call directly.
/// </summary>
///
/// Manages one Lobby at a time, Only entry points to a lobby with ID is via JoinAsync, CreateAsync, and QuickJoinAsync
public class LobbyManager : IDisposable
{
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the game will be actively in just one lobby at a time, though they could be in more on the service side.)
public Lobby CurrentLobby => m_CurrentLobby;
Lobby m_CurrentLobby;
const int
k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
#region Rate Limiting
public enum RequestType
{
Query = 0,
Join,
QuickJoin,
Host
}
public bool InLobby()
{
if (m_CurrentLobby == null)
{
Debug.LogError("LobbyManager not currently in a lobby. Did you CreateLobbyAsync or JoinLobbyAsync?");
return false;
}
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
// (This assumes that the game will be actively in just one lobby at a time, though they could be in more on the service side.)
return true;
}
public Lobby CurrentLobby => m_currentLobby;
const int k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
Lobby m_currentLobby;
public RateLimiter GetRateLimit(RequestType type)
{
if (type == RequestType.Join)
return m_JoinCooldown;
else if (type == RequestType.QuickJoin)
return m_QuickJoinCooldown;
else if (type == RequestType.Host)
return m_CreateCooldown;
return m_QueryCooldown;
}
// Rate Limits are posted here: https://docs.unity.com/lobby/rate-limits.html
#region Lobby API calls are rate limited, and some other operations might want an alert when the rate limits have passed.
RateLimiter m_QueryCooldown = new RateLimiter(1f);
RateLimiter m_CreateCooldown = new RateLimiter(3f);
RateLimiter m_JoinCooldown = new RateLimiter(3f);
RateLimiter m_QuickJoinCooldown = new RateLimiter(10f);
RateLimiter m_GetLobbyCooldown = new RateLimiter(1f);
RateLimiter m_DeleteLobbyCooldown = new RateLimiter(.2f);
RateLimiter m_UpdateLobbyCooldown = new RateLimiter(.3f);
RateLimiter m_UpdatePlayerCooldown = new RateLimiter(.3f);
RateLimiter m_LeaveLobbyOrRemovePlayer = new RateLimiter(.3f);
RateLimiter m_HeartBeatCooldown = new RateLimiter(6f);
// Note that some APIs limit to 1 call per N seconds, while others limit to M calls per N seconds. We'll treat all APIs as though they limited to 1 call per N seconds.
// Also, this is seralized, so don't reorder the values unless you know what that will affect.
public enum RequestType
{
Query = 0,
Join,
QuickJoin,
Host
}
#endregion
public RateLimiter GetRateLimit(RequestType type)
{
if (type == RequestType.Join)
return m_JoinCooldown;
else if (type == RequestType.QuickJoin)
return m_QuickJoinCooldown;
else if (type == RequestType.Host)
return m_CreateCooldown;
return m_QueryCooldown;
}
Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LobbyUser user)
{
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>();
RateLimiter m_QueryCooldown = new RateLimiter(1f); // Used for both the lobby list UI and the in-lobby updating. In the latter case, updates can be cached.
RateLimiter m_CreateCooldown = new RateLimiter(3f);
RateLimiter m_JoinCooldown = new RateLimiter(3f);
RateLimiter m_QuickJoinCooldown = new RateLimiter(10f);
RateLimiter m_GetLobbyCooldown = new RateLimiter(1f);
RateLimiter m_DeleteLobbyCooldown = new RateLimiter(.2f);
RateLimiter m_UpdateLobbyCooldown = new RateLimiter(.2f);
RateLimiter m_UpdatePlayerCooldown = new RateLimiter(.2f);
RateLimiter m_LeaveLobbyOrRemovePlayer = new RateLimiter(.2f);
RateLimiter m_HeartBeatCooldown = new RateLimiter(6f);
var displayNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.DisplayName);
data.Add("DisplayName", displayNameObject);
return data;
}
public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LobbyUser localUser)
{
if (m_CreateCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit.");
return null;
}
#endregion
await m_CreateCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Creating");
static Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LobbyUser user)
{
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>();
try
{
string uasId = AuthenticationService.Instance.PlayerId;
var displayNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.DisplayName);
var emoteNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.Emote.ToString());
CreateLobbyOptions createOptions = new CreateLobbyOptions
{
IsPrivate = isPrivate,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
m_CurrentLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
#pragma warning disable 4014
HeartBeatLoop();
#pragma warning restore 4014
return m_CurrentLobby;
}
catch (Exception ex)
{
Debug.LogError($"Lobby Create failed:\n{ex}");
return null;
}
}
data.Add("DisplayName", displayNameObject);
data.Add("Emote", emoteNameObject);
return data;
}
public async Task<Lobby> JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser)
{
if (m_JoinCooldown.IsInCooldown ||
(lobbyId == null && lobbyCode == null))
{
return null;
}
await m_JoinCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Joining");
/// <summary>
/// Attempt to create a new lobby and then join it.
/// </summary>
public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LobbyUser localUser)
{
if (m_CreateCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit.");
return null;
}
string uasId = AuthenticationService.Instance.PlayerId;
var playerData = CreateInitialPlayerData(localUser);
if (!string.IsNullOrEmpty(lobbyId))
{
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions
{Player = new Player(id: uasId, data: playerData)};
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
}
else
{
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions
{Player = new Player(id: uasId, data: playerData)};
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
}
try
{
string uasId = AuthenticationService.Instance.PlayerId;
return m_CurrentLobby;
}
CreateLobbyOptions createOptions = new CreateLobbyOptions
{
IsPrivate = isPrivate,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
var lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
public async Task<Lobby> QuickJoinLobbyAsync(LobbyUser localUser, LobbyColor limitToColor = LobbyColor.None)
{
//We dont want to queue a quickjoin
if (m_QuickJoinCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Quick Join Lobby hit the rate limit.");
return null;
}
return lobby;
}
catch (Exception ex)
{
Debug.LogError($"Lobby Create failed:\n{ex}");
return null;
}
}
await m_QuickJoinCooldown.WaitUntilCooldown();
Debug.Log("Lobby - QuickJoining.");
var filters = LobbyColorToFilters(limitToColor);
string uasId = AuthenticationService.Instance.PlayerId;
public async Task<Lobby> GetLobbyAsync(string lobbyId)
{
await m_GetLobbyCooldown.WaitUntilCooldown();
var joinRequest = new QuickJoinLobbyOptions
{
Filter = filters,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
return await LobbyService.Instance.GetLobbyAsync(lobbyId);
}
return m_CurrentLobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest);
}
/// <summary>
/// Attempt to join an existing lobby. Either ID xor code can be null.
/// </summary>
public async Task<Lobby> JoinLobbyAsync(string lobbyId, string lobbyCode, LobbyUser localUser)
{
//Dont want to queue the join action in this case.
if (m_JoinCooldown.IsInCooldown ||
(lobbyId == null && lobbyCode == null))
{
return null;
}
public async Task<QueryResponse> RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None)
{
await m_QueryCooldown.WaitUntilCooldown();
await m_JoinCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Retrieving List.");
string uasId = AuthenticationService.Instance.PlayerId;
Lobby joinedLobby = null;
var playerData = CreateInitialPlayerData(localUser);
if (!string.IsNullOrEmpty(lobbyId))
{
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions { Player = new Player(id: uasId, data: playerData) };
joinedLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
}
else
{
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions { Player = new Player(id: uasId, data: playerData) };
joinedLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions);
}
var filters = LobbyColorToFilters(limitToColor);
return joinedLobby;
}
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
}
/// <summary>
/// Attempt to join the first lobby among the available lobbies that match the filtered limitToColor.
/// </summary>
public async Task<Lobby> QuickJoinLobbyAsync(LobbyUser localUser, LobbyColor limitToColor = LobbyColor.None)
{
//We dont want to queue a quickjoin
if (m_QuickJoinCooldown.IsInCooldown)
{
UnityEngine.Debug.LogWarning("Quick Join Lobby hit the rate limit.");
return null;
}
List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor)
{
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int) LobbyColor.Orange).ToString(),
QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Green)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int) LobbyColor.Green).ToString(),
QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Blue)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int) LobbyColor.Blue).ToString(),
QueryFilter.OpOptions.EQ));
return filters;
}
await m_QuickJoinCooldown.WaitUntilCooldown();
var filters = LobbyColorToFilters(limitToColor);
string uasId = AuthenticationService.Instance.PlayerId;
public async Task<Lobby> GetLobbyAsync(string lobbyId = null)
{
if (!InLobby())
return null;
await m_GetLobbyCooldown.WaitUntilCooldown();
lobbyId ??= m_CurrentLobby.Id;
return m_CurrentLobby = await LobbyService.Instance.GetLobbyAsync(lobbyId);
}
var joinRequest = new QuickJoinLobbyOptions
{
Filter = filters,
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser))
};
public async Task LeaveLobbyAsync()
{
await m_LeaveLobbyOrRemovePlayer.WaitUntilCooldown();
if (!InLobby())
return;
string playerId = AuthenticationService.Instance.PlayerId;
Debug.Log($"{playerId} leaving Lobby {m_CurrentLobby.Id}");
var lobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest);
return lobby;
}
await LobbyService.Instance.RemovePlayerAsync(m_CurrentLobby.Id, playerId);
m_CurrentLobby = null;
}
public async Task<Lobby> UpdatePlayerDataAsync(Dictionary<string, string> data)
{
if (!InLobby())
return null;
await m_UpdatePlayerCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Updating Player Data");
string playerId = AuthenticationService.Instance.PlayerId;
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>();
foreach (var dataNew in data)
{
PlayerDataObject dataObj = new PlayerDataObject(visibility: PlayerDataObject.VisibilityOptions.Member,
value: dataNew.Value);
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
}
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = dataCurr,
AllocationId = null,
ConnectionInfo = null
};
return m_CurrentLobby =
await LobbyService.Instance.UpdatePlayerAsync(m_CurrentLobby.Id, playerId, updateOptions);
}
/// <summary>
/// Used for getting the list of all active lobbies, without needing full info for each.
/// </summary>
/// <param name="onListRetrieved">If called with null, retrieval was unsuccessful. Else, this will be given a list of contents to display, as pairs of a lobby code and a display string for that lobby.</param>
public async Task<QueryResponse> RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None)
{
await m_QueryCooldown.WaitUntilCooldown();
var filters = LobbyColorToFilters(limitToColor);
public async Task<Lobby> UpdatePlayerRelayInfoAsync(string lobbyID, string allocationId, string connectionInfo)
{
if (!InLobby())
return null;
await m_UpdatePlayerCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Relay Info (Player)");
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions
{
Count = k_maxLobbiesToShow,
Filters = filters
};
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
}
string playerId = AuthenticationService.Instance.PlayerId;
List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor)
{
List<QueryFilter> filters = new List<QueryFilter>();
if (limitToColor == LobbyColor.Orange)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Orange).ToString(), QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Green)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Green).ToString(), QueryFilter.OpOptions.EQ));
else if (limitToColor == LobbyColor.Blue)
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Blue).ToString(), QueryFilter.OpOptions.EQ));
return filters;
}
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = new Dictionary<string, PlayerDataObject>(),
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
return m_CurrentLobby = await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions);
}
/// <summary>
/// Attempt to leave a lobby, and then delete it if no players remain.
/// </summary>
/// <param name="onComplete">Called once the request completes, regardless of success or failure.</param>
public async Task LeaveLobbyAsync(string lobbyId)
{
await m_LeaveLobbyOrRemovePlayer.WaitUntilCooldown();
public async Task<Lobby> UpdateLobbyDataAsync(Dictionary<string, string> data)
{
if (!InLobby())
return null;
await m_UpdateLobbyCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Updating Lobby Data");
string playerId = AuthenticationService.Instance.PlayerId;
await LobbyService.Instance.RemovePlayerAsync(lobbyId, playerId);
}
Dictionary<string, DataObject> dataCurr = m_CurrentLobby.Data ?? new Dictionary<string, DataObject>();
/// <param name="data">Key-value pairs, which will overwrite any existing data for these keys. Presumed to be available to all lobby members but not publicly.</param>
public async Task UpdatePlayerDataAsync(string lobbyID, Dictionary<string, string> data)
{
await m_UpdatePlayerCooldown.WaitUntilCooldown();
string playerId = AuthenticationService.Instance.PlayerId;
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>();
foreach (var dataNew in data)
{
PlayerDataObject dataObj = new PlayerDataObject(visibility: PlayerDataObject.VisibilityOptions.Member, value: dataNew.Value);
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
}
var shouldLock = false;
foreach (var dataNew in data)
{
// Special case: We want to be able to filter on our color data, so we need to supply an arbitrary index to retrieve later. Uses N# for numerics, instead of S# for strings.
DataObject.IndexOptions index = dataNew.Key == "Color" ? DataObject.IndexOptions.N1 : 0;
DataObject
dataObj = new DataObject(DataObject.VisibilityOptions.Public, dataNew.Value,
index); // Public so that when we request the list of lobbies, we can get info about them for filtering.
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = dataCurr,
AllocationId = null,
ConnectionInfo = null
};
await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions);
}
//Special Use: Get the state of the Local lobby so we can lock it from appearing in queries if it's not in the "Lobby" State
if (dataNew.Key == "State")
{
Enum.TryParse(dataNew.Value, out LobbyState lobbyState);
shouldLock = lobbyState != LobbyState.Lobby;
}
}
/// <summary>
/// Lobby can be provided info about Relay (or any other remote allocation) so it can add automatic disconnect handling.
/// </summary>
public async Task UpdatePlayerRelayInfoAsync(string lobbyID, string allocationId, string connectionInfo)
{
await m_UpdatePlayerCooldown.WaitUntilCooldown();
string playerId = AuthenticationService.Instance.PlayerId;
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions {Data = dataCurr, IsLocked = shouldLock};
return m_CurrentLobby = await LobbyService.Instance.UpdateLobbyAsync(m_CurrentLobby.Id, updateOptions);
}
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions
{
Data = new Dictionary<string, PlayerDataObject>(),
AllocationId = allocationId,
ConnectionInfo = connectionInfo
};
await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions);
}
/// <param name="data">Key-value pairs, which will overwrite any existing data for these keys. Presumed to be available to all lobby members but not publicly.</param>
public async Task<Lobby> UpdateLobbyDataAsync(Lobby remoteLobby, Dictionary<string, string> data)
{
await m_UpdateLobbyCooldown.WaitUntilCooldown();
public async Task DeleteLobbyAsync()
{
if (!InLobby())
return;
await m_DeleteLobbyCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Deleting Lobby");
Dictionary<string, DataObject> dataCurr = remoteLobby.Data ?? new Dictionary<string, DataObject>();
await LobbyService.Instance.DeleteLobbyAsync(m_CurrentLobby.Id);
}
var shouldLock = false;
foreach (var dataNew in data)
{
// Special case: We want to be able to filter on our color data, so we need to supply an arbitrary index to retrieve later. Uses N# for numerics, instead of S# for strings.
DataObject.IndexOptions index = dataNew.Key == "Color" ? DataObject.IndexOptions.N1 : 0;
DataObject dataObj = new DataObject(DataObject.VisibilityOptions.Public, dataNew.Value, index); // Public so that when we request the list of lobbies, we can get info about them for filtering.
if (dataCurr.ContainsKey(dataNew.Key))
dataCurr[dataNew.Key] = dataObj;
else
dataCurr.Add(dataNew.Key, dataObj);
//Special Use: Get the state of the Local lobby so we can lock it from appearing in queries if it's not in the "Lobby" State
if (dataNew.Key == "State")
{
Enum.TryParse(dataNew.Value, out LobbyState lobbyState);
shouldLock = lobbyState != LobbyState.Lobby;
}
}
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = dataCurr, IsLocked = shouldLock };
var result = await LobbyService.Instance.UpdateLobbyAsync(remoteLobby.Id, updateOptions);
return result;
}
public void Dispose()
{
m_CurrentLobby = null;
}
public async Task SendHeartbeatPingAsync(string remoteLobbyId)
{
if (m_HeartBeatCooldown.IsInCooldown)
return;
await m_HeartBeatCooldown.WaitUntilCooldown();
await LobbyService.Instance.SendHeartbeatPingAsync(remoteLobbyId);
}
#region HeartBeat
public void Dispose()
{
throw new NotImplementedException();
}
//Since the LobbyManager maintains the "connection" to the lobby, we will continue to heartbeat until host leaves.
async Task SendHeartbeatPingAsync()
{
if (!InLobby())
return;
if (m_HeartBeatCooldown.IsInCooldown)
return;
await m_HeartBeatCooldown.WaitUntilCooldown();
Debug.Log("Lobby - Heartbeat");
public class RateLimiter
{
public Action<bool> onCooldownChange;
public readonly float m_CooldownSeconds;
public readonly int m_CoolDownMS;
Queue<Task> m_TaskQueue = new Queue<Task>();
Task m_DequeuedTask;
await LobbyService.Instance.SendHeartbeatPingAsync(m_CurrentLobby.Id);
}
private bool m_IsInCooldown = false;
public bool IsInCooldown
{
get => m_IsInCooldown;
private set
{
if (m_IsInCooldown != value)
{
m_IsInCooldown = value;
onCooldownChange?.Invoke(m_IsInCooldown);
}
}
}
async Task HeartBeatLoop()
{
while (m_CurrentLobby != null)
{
await SendHeartbeatPingAsync();
await Task.Delay(8000);
}
}
public RateLimiter(float cooldownSeconds)
{
m_CooldownSeconds = cooldownSeconds;
m_CoolDownMS = Mathf.FloorToInt(m_CooldownSeconds * 1000);
}
#endregion
}
//Manages the Cooldown for each service call.
//Adds a buffer to account for ping times.
public class RateLimiter
{
public Action<bool> onCooldownChange;
public readonly float cooldownSeconds;
public readonly int coolDownMS;
public readonly int pingBufferMS;
public async Task WaitUntilCooldown() //TODO YAGNI Handle Multiple commands? Return bool if already waiting?
{
//No Queue!
if (CanCall())
return;
//(If you're still getting rate limit errors, try increasing the pingBuffer)
public RateLimiter(float cooldownSeconds, int pingBuffer = 100)
{
this.cooldownSeconds = cooldownSeconds;
pingBufferMS = pingBuffer;
coolDownMS =
Mathf.CeilToInt(this.cooldownSeconds * 1000) +
pingBufferMS;
}
while (m_IsInCooldown)
{
await Task.Delay(50);
}
}
bool CanCall()
{
if (!IsInCooldown)
{
public async Task WaitUntilCooldown()
{
//No Queue!
if (!m_IsInCooldown)
{
CoolDownAsync();
CooldownAsync();
return true;
}
else return false;
return;
}
}
while (m_IsInCooldown)
{
await Task.Delay(10);
}
}
async Task CoolDownAsync()
{
if (m_IsInCooldown)
return;
IsInCooldown = true;
await Task.Delay(m_CoolDownMS);
if (m_TaskQueue.Count > 0)
{
m_DequeuedTask = m_TaskQueue.Dequeue();
await CoolDownAsync();
}
IsInCooldown = false;
}
}
async Task CooldownAsync()
{
IsInCooldown = true;
await Task.Delay(coolDownMS);
IsInCooldown = false;
}
}
bool m_IsInCooldown = false;
public bool IsInCooldown
{
get => m_IsInCooldown;
private set
{
if (m_IsInCooldown != value)
{
m_IsInCooldown = value;
onCooldownChange?.Invoke(m_IsInCooldown);
}
}
}
}
}

100
Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs


LocalLobby m_LocalLobby;
LobbyUser m_LocalUser;
LobbyManager m_LobbyManager;
bool m_ShouldPushData = false;
bool m_LocalChanges = false;
const int
k_approvalMaxMS = 10000; // Used for determining if a user should timeout if they are unable to connect.
const int k_approvalMaxMS = 10000; // Used for determining if a user should timeout if they are unable to connect.
const int k_UpdateIntervalMS = 100;
const int k_UpdateIntervalMS = 1000;
public LobbySynchronizer(LobbyManager lobbyManager)
{

public void BeginTracking(LocalLobby localLobby, LobbyUser localUser)
public void StartSynch(LocalLobby localLobby, LobbyUser localUser)
m_ShouldPushData = true;
m_LocalChanges = true;
Locator.Get.Messenger.Subscribe(this);
#pragma warning disable 4014
UpdateLoopAsync();

public void EndTracking()
public void EndSynch()
m_ShouldPushData = false;
m_LocalChanges = false;
Locator.Get.Messenger.Unsubscribe(this);
if (m_LocalLobby != null)

/// </summary>
async Task UpdateLoopAsync()
{
Lobby latestLobby = null;
if (!m_LocalUser.IsApproved && m_lifetime > k_approvalMaxMS)
if (UserTimedOut())
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Connection attempt timed out!");
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
LeaveLobbyBecauseTimeout();
break;
if (m_ShouldPushData)
await PushDataToLobby();
if (m_LocalChanges)
latestLobby = await PushDataToLobby();
UpdateLocalLobby();
latestLobby = await m_LobbyManager.GetLobbyAsync();
if (IfRemoteLobbyChanged(latestLobby))
LobbyConverters.RemoteToLocal(latestLobby, m_LocalLobby);
if (!LobbyHasHost())
{
LeaveLobbyBecauseNoHost();
break;
}
async Task PushDataToLobby()
bool IfRemoteLobbyChanged(Lobby remoteLobby)
m_ShouldPushData = false;
return remoteLobby.LastUpdated.ToFileTime() > m_LocalLobby.Data.State_LastEdit;
}
bool UserTimedOut()
{
return m_LocalUser.IsApproved && m_lifetime > k_approvalMaxMS;
}
async Task<Lobby> PushDataToLobby()
{
m_LocalChanges = false;
m_LocalLobby.changedByLobbySynch = true;
m_LobbyManager.UpdateLobbyDataAsync(m_LobbyManager.CurrentLobby,
await m_LobbyManager.UpdateLobbyDataAsync(
m_LobbyManager.UpdatePlayerDataAsync(m_LobbyManager.CurrentLobby.Id,
return await m_LobbyManager.UpdatePlayerDataAsync(
void UpdateLocalLobby()
bool LobbyHasHost()
m_LocalLobby.canPullUpdate = true;
//synching our local lobby
LobbyConverters.RemoteToLocal(m_LobbyManager.CurrentLobby, m_LocalLobby);
return;
return true;
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Host left the lobby! Disconnecting...");
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
return false;
return true;
}
void LeaveLobbyBecauseTimeout()
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Connection attempt timed out!");
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
void LeaveLobbyBecauseNoHost()
{
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup,
"Host left the lobby! Disconnecting...");
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null);
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu);
}
}

) // When the player leaves, their LocalLobby is cleared out.
{
EndTracking();
EndSynch();
if (localLobby.canPullUpdate)
//Catch for infinite update looping from the synchronizer.
if (localLobby.changedByLobbySynch)
localLobby.canPullUpdate = false;
localLobby.changedByLobbySynch = false;
m_ShouldPushData = true;
m_LocalChanges = true;
EndTracking();
EndSynch();
}
}
}

2
Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs


}
}
private async Task CheckForComplete()
private void CheckForComplete()
{
if (m_joinState == (JoinState.Joined | JoinState.Bound) && this != null
) // this will equal null (i.e. this component has been destroyed) if the host left the lobby during the Relay connection sequence.

262
Assets/Scripts/GameLobby/Tests/PlayMode/LobbyRoundtripTests.cs


using NUnit.Framework;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Unity.Services.Core;
using Unity.Services.Authentication;
using Debug = UnityEngine.Debug;
/// <summary>
/// Accesses the Authentication and Lobby services in order to ensure lobbies can be created and deleted.
/// LobbyAsyncRequests wraps the Lobby API, so go through that in practice. This simply ensures the connection to the Lobby service is functional.
///
/// If the tests pass, you can assume you are connecting to the Lobby service itself properly.
/// </summary>
public class LobbyRoundtripTests
{
string m_workingLobbyId;
bool m_didSigninComplete = false;
string playerID;
Dictionary<string, PlayerDataObject> m_mockUserData; // This is handled in the LobbyAsyncRequest calls normally, but we need to supply this for the direct Lobby API calls.
LobbyUser m_LocalUser;
LobbyManager m_LobbyManager;
[OneTimeSetUp]
public IEnumerator Setup()
{
/// <summary>
/// Accesses the Authentication and Lobby services in order to ensure lobbies can be created and deleted.
/// LobbyAsyncRequests wraps the Lobby API, so go through that in practice. This simply ensures the connection to the Lobby service is functional.
///
/// If the tests pass, you can assume you are connecting to the Lobby service itself properly.
/// </summary>
public class LobbyRoundtripTests
{
string playerID;
m_mockUserData = new Dictionary<string, PlayerDataObject>();
m_mockUserData.Add("DisplayName", new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, "TestUser123"));
m_LocalUser = new LobbyUser(true);
return TestAuthSetup();
}
Dictionary<string, PlayerDataObject>
m_mockUserData; // This is handled in the LobbyAsyncRequest calls normally, but we need to supply this for the direct Lobby API calls.
IEnumerator TestAuthSetup()
{
yield return AsyncTestHelper.Await(async ()=> await UnityServices.InitializeAsync());
yield return AsyncTestHelper.Await(async () => await AuthenticationService.Instance.SignInAnonymouslyAsync());
m_didSigninComplete = true;
}
LobbyUser m_LocalUser;
LobbyManager m_LobbyManager;
[UnityTearDown]
public IEnumerator PerTestTeardown()
{
if (m_workingLobbyId != null)
{ yield return AsyncTestHelper.Await(async ()=> await m_LobbyManager.LeaveLobbyAsync(m_workingLobbyId));
m_workingLobbyId = null;
}
yield return new WaitForSeconds(0.5f); // We need a yield anyway, so wait long enough to probably delete the lobby. There currently (6/22/2021) aren't other tests that would have issues if this took longer.
}
[OneTimeSetUp]
public void Setup()
{
m_mockUserData = new Dictionary<string, PlayerDataObject>();
m_mockUserData.Add("DisplayName",
new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, "TestUser123"));
m_LocalUser = new LobbyUser(true);
m_LobbyManager = new LobbyManager();
#pragma warning disable 4014
TestAuthSetup();
#pragma warning restore 4014
}
async Task TestAuthSetup()
{
await Auth.Authenticate("test");
}
/// <summary>
/// Make sure the entire roundtrip for Lobby works: Once signed in, create a lobby, query to make sure it exists, then delete it.
/// </summary>
[UnityTest]
public IEnumerator DoRoundtrip()
{
#region Setup
[UnityTearDown]
public IEnumerator PerTestTeardown()
{
if (m_LobbyManager.CurrentLobby != null)
{
yield return AsyncTestHelper.Await(async () => await m_LobbyManager.LeaveLobbyAsync());
}
}
// Wait a reasonable amount of time for sign-in to complete.
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
// Since we're signed in through the same pathway as the actual game, the list of lobbies will include any that have been made in the game itself, so we should account for those.
// If you want to get around this, consider having a secondary project using the same assets with its own credentials.
yield return
new WaitForSeconds(
1); // To prevent a possible 429 with the upcoming Query request, in case a previous test had one; Query requests can only occur at a rate of 1 per second.
QueryResponse queryResponse = null;
Debug.Log("Getting Lobby List 1");
/// <summary>
/// Make sure the entire roundtrip for Lobby works: Once signed in, create a lobby, query to make sure it exists, then delete it.
/// </summary>
[UnityTest]
public IEnumerator DoRoundtrip()
{
#region Setup
yield return AsyncTestHelper.Await(async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
yield return AsyncTestHelper.Await(async () => await Auth.Authenticating());
// Since we're signed in through the same pathway as the actual game, the list of lobbies will include any that have been made in the game itself, so we should account for those.
// If you want to get around this, consider having a secondary project using the same assets with its own credentials.
yield return
new WaitForSeconds(
1); // To prevent a possible 429 with the upcoming Query request, in case a previous test had one; Query requests can only occur at a rate of 1 per second.
QueryResponse queryResponse = null;
Debug.Log("Getting Lobby List 1");
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#0)");
int numLobbiesIni = queryResponse.Results?.Count ?? 0;
yield return AsyncTestHelper.Await(
async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
#endregion
// Create a test lobby.
Lobby createResponse = null;
string lobbyName = "TestLobby-JustATest-123";
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#0)");
int numLobbiesIni = queryResponse.Results?.Count ?? 0;
yield return AsyncTestHelper.Await(async () =>
createResponse = await m_LobbyManager.CreateLobbyAsync(
lobbyName,
100,
false,
m_LocalUser));
#endregion
Assert.IsNotNull(createResponse, "CreateLobbyAsync should return a non-null result.");
m_workingLobbyId = createResponse.Id;
Assert.AreEqual(lobbyName, createResponse.Name, "Created lobby should match the provided name.");
// Query for the test lobby via QueryAllLobbies.
yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request.
Debug.Log("Getting Lobby List 2");
// Create a test lobby.
Lobby createLobby = null;
string lobbyName = "TestLobby-JustATest-123";
yield return AsyncTestHelper.Await(async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
yield return AsyncTestHelper.Await(async () =>
createLobby = await m_LobbyManager.CreateLobbyAsync(
lobbyName,
100,
false,
m_LocalUser));
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#1)");
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Results.Count, "Queried lobbies list should contain the test lobby.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Name == lobbyName).Count() == 1, "Checking queried lobby for name.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Id == m_workingLobbyId).Count() == 1, "Checking queried lobby for ID.");
Assert.IsNotNull(createLobby, "CreateLobbyAsync should return a non-null result.");
Assert.AreEqual(lobbyName, createLobby.Name, "Created lobby should match the provided name.");
var createLobbyId = createLobby.Id;
// Query for the test lobby via QueryAllLobbies.
Debug.Log("Getting Lobby List 2");
yield return AsyncTestHelper.Await(
async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
Debug.Log("Getting current Lobby");
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#1)");
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Results.Count,
"Queried lobbies list should contain the test lobby.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Name == lobbyName).Count() == 1,
"Checking queried lobby for name.");
Assert.IsTrue(queryResponse.Results.Where(r => r.Id == createLobbyId).Count() == 1,
"Checking queried lobby for ID.");
Lobby currentLobby = null;
Assert.IsNotNull(currentLobby, "GetLobbyAsync should return a non-null result.");
Assert.AreEqual(lobbyName, currentLobby.Name, "Checking the lobby we got for name.");
Assert.AreEqual(m_workingLobbyId, currentLobby.Id, "Checking the lobby we got for ID.");
Debug.Log("Getting current Lobby");
Lobby currentLobby = m_LobbyManager.CurrentLobby;
Assert.IsNotNull(currentLobby, "GetLobbyAsync should return a non-null result.");
Assert.AreEqual(lobbyName, currentLobby.Name, "Checking the lobby we got for name.");
Assert.AreEqual(createLobbyId, currentLobby.Id, "Checking the lobby we got for ID.");
Debug.Log("Deleting current Lobby");
// Delete the test lobby.
yield return AsyncTestHelper.Await(async () => await m_LobbyManager.LeaveLobbyAsync());
Debug.Log("Deleting current Lobby");
// Delete the test lobby.
yield return AsyncTestHelper.Await(async ()=> await m_LobbyManager.LeaveLobbyAsync(m_workingLobbyId));
createLobbyId = null;
Debug.Log("Getting Lobby List 3");
yield return AsyncTestHelper.Await(
async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
m_workingLobbyId = null;
Debug.Log("Getting Lobby List 3");
yield return AsyncTestHelper.Await(async () => queryResponse = await m_LobbyManager.RetrieveLobbyListAsync());
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#2)");
Assert.AreEqual(numLobbiesIni, queryResponse.Results.Count, "Queried lobbies list should be empty.");
Assert.IsNotNull(queryResponse, "QueryAllLobbiesAsync should return a non-null result. (#2)");
Assert.AreEqual(numLobbiesIni, queryResponse.Results.Count, "Queried lobbies list should be empty.");
}
// Some error messages might be asynchronous, so to reduce spillover into other tests, just wait here for a bit before proceeding.
yield return new WaitForSeconds(3);
}
/// <summary>
/// If the Lobby create call fails, we return null
/// </summary>
[UnityTest]
public IEnumerator CreateFailsWithNull()
{
yield return AsyncTestHelper.Await(async () => await Auth.Authenticating());
/// <summary>
/// If the Lobby create call fails, we return null
/// </summary>
[UnityTest]
public IEnumerator CreateFailsWithNull()
{
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
LogAssert.ignoreFailingMessages = true; // Multiple errors will appears for the exception.
Lobby createLobby = null;
yield return AsyncTestHelper.Await(async () =>
createLobby = await m_LobbyManager.CreateLobbyAsync(
"lobby name",
123,
false,
m_LocalUser));
LogAssert.ignoreFailingMessages = true; // Multiple errors will appears for the exception.
Lobby createLobby = null;
yield return AsyncTestHelper.Await(async () =>
createLobby = await m_LobbyManager.CreateLobbyAsync(
"lobby name",
123,
false,
m_LocalUser));
LogAssert.ignoreFailingMessages = false;
Assert.Null(createLobby, "The returned object will be null, so expect to need to handle it.");
yield return new WaitForSeconds(3); //Since CreateLobby cannot be queued, we need to give this a buffer before moving on to other tests.
LogAssert.ignoreFailingMessages = false;
}
Assert.Null(createLobby, "The returned object will be null, so expect to need to handle it.");
}
[UnityTest]
public IEnumerator CooldownTest()
{
var rateLimiter = new RateLimiter(3);
Stopwatch timer = new Stopwatch();
timer.Start();
//pass Through the first request, which triggers the cooldown.
yield return AsyncTestHelper.Await(async () => await rateLimiter.WaitUntilCooldown());
//Should wait for one second total
yield return AsyncTestHelper.Await(async () => await rateLimiter.WaitUntilCooldown());
timer.Stop();
var elapsedMS = timer.ElapsedMilliseconds;
Debug.Log($"Cooldown took {elapsedMS}/{rateLimiter.coolDownMS} milliseconds.");
var difference = Mathf.Abs(elapsedMS - rateLimiter.coolDownMS);
Assert.IsTrue(difference<50&&difference>=0);
[UnityTest]
public IEnumerator LobbyPlayerDataUpdated()
{
yield return null;
}
}
}
}
}

1
Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs


/// </summary>
public class RelayRoundTripTests
{
bool m_DidSigninComplete = false;
[OneTimeSetUp]
public void Setup()

18
Assets/Scripts/GameLobby/Tests/PlayMode/UtpTests.cs


using LobbyRelaySample;
using LobbyRelaySample.relay;
using NUnit.Framework;
using Test.Tools;
using Unity.Networking.Transport;
using Unity.Services.Core;
using Unity.Services.Relay.Models;

public void Setup()
{
m_dummy = new GameObject();
Auth.Authenticate("testProfile");
#pragma warning disable 4014
TestAuthSetup();
#pragma warning restore 4014
async Task InitServices()
async Task TestAuthSetup()
await UnityServices.InitializeAsync();
m_didSigninComplete = true;
await Auth.Authenticate("test");
}
[OneTimeTearDown]

{
#if ENABLE_MANAGED_UNITYTLS
if (!m_didSigninComplete)
yield return new WaitForSeconds(3);
if (!m_didSigninComplete)
Assert.Fail("Did not sign in.");
yield return new WaitForSeconds(1); // To prevent a possible 429 after a previous test.
yield return AsyncTestHelper.Await(async () => await Auth.Authenticating());
RelayUtpTest relaySetup = m_dummy.AddComponent<RelayUtpTest>();
relaySetup.OnGetEndpoint = OnGetEndpoint;

2
Assets/Scripts/GameLobby/UI/RateLimitVisibility.cs


void OnDestroy()
{
if (GameManager.Instance == null || GameManager.Instance.LobbyManager == null)
return;
GameManager.Instance.LobbyManager.GetRateLimit(m_requestType).onCooldownChange -= UpdateVisibility;
}

/Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs → /Assets/Scripts/GameLobby/Auth/Auth.cs

/Assets/Scripts/GameLobby/Auth/AuthenticationManager.cs.meta → /Assets/Scripts/GameLobby/Auth/Auth.cs.meta

正在加载...
取消
保存