当前提交
356c8836
共有 1004 个文件被更改,包括 4320 次插入 和 2104 次删除
-
6Assets/Prefabs/UI/LobbyButtonUI.prefab
-
3Assets/Prefabs/UI/LobbyCodeCanvas.prefab
-
140Assets/Scripts/Auth/NameGenerator.cs
-
6Assets/Scripts/Entities/GameStateManager.cs
-
542Assets/Scripts/Entities/LocalLobby.cs
-
8Assets/Scripts/Entities/LocalLobbyObserver.cs
-
200Assets/Scripts/Infrastructure/Locator.cs
-
126Assets/Scripts/Infrastructure/LogHandler.cs
-
176Assets/Scripts/Infrastructure/Messenger.cs
-
276Assets/Scripts/Infrastructure/UpdateSlow.cs
-
218Assets/Scripts/Lobby/LobbyAPIInterface.cs
-
439Assets/Scripts/Lobby/LobbyAsyncRequests.cs
-
218Assets/Scripts/Lobby/LobbyContentHeartbeat.cs
-
46Assets/Scripts/Lobby/LobbyListHeartbeat.cs
-
126Assets/Scripts/Lobby/ReadyCheck.cs
-
178Assets/Scripts/Lobby/ToLocalLobby.cs
-
4Assets/Scripts/LobbyRelaySample.asmdef
-
260Assets/Scripts/Tests/PlayMode/LobbyReadyCheckTests.cs
-
326Assets/Scripts/Tests/PlayMode/LobbyRoundtripTests.cs
-
2Assets/Scripts/Tests/PlayMode/Tests.Play.asmdef
-
40Assets/Scripts/UI/CountdownUI.cs
-
66Assets/Scripts/UI/DisplayCodeUI.cs
-
30Assets/Scripts/UI/EndGameButtonUI.cs
-
30Assets/Scripts/UI/ExitButtonUI.cs
-
120Assets/Scripts/UI/InLobbyUserList.cs
-
138Assets/Scripts/UI/InLobbyUserUI.cs
-
42Assets/Scripts/UI/JoinCreateLobbyUI.cs
-
38Assets/Scripts/UI/LobbyNameUI.cs
-
46Assets/Scripts/UI/ReadyCheckUI.cs
-
38Assets/Scripts/UI/RelayAddressUI.cs
-
4Assets/Scripts/UI/ShowWhenLobbyStateUI.cs
-
106Assets/Scripts/UI/SpinnerUI.cs
-
30Assets/Scripts/UI/StartLobbyButtonUI.cs
-
22Packages/manifest.json
-
62Packages/packages-lock.json
-
24ProjectSettings/PackageManagerSettings.asset
-
6ProjectSettings/ProjectSettings.asset
-
2ProjectSettings/UnityConnectSettings.asset
-
42README.md
-
242Packages/com.unity.services.authentication/.README - External.md
-
53Packages/com.unity.services.authentication/CHANGELOG.md
-
7Packages/com.unity.services.authentication/CHANGELOG.md.meta
-
5Packages/com.unity.services.authentication/Documentation~/com.unity.services.authentication.md
-
8Packages/com.unity.services.authentication/Editor.meta
-
5Packages/com.unity.services.authentication/Editor/AssemblyInfo.cs
-
3Packages/com.unity.services.authentication/Editor/AssemblyInfo.cs.meta
-
298Packages/com.unity.services.authentication/Editor/AuthenticationAdminClient.cs
-
3Packages/com.unity.services.authentication/Editor/AuthenticationAdminClient.cs.meta
-
148Packages/com.unity.services.authentication/Editor/AuthenticationAdminNetworkClient.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationAdminNetworkClient.cs.meta
-
17Packages/com.unity.services.authentication/Editor/AuthenticationIdentifier.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationIdentifier.cs.meta
-
47Packages/com.unity.services.authentication/Editor/AuthenticationService.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationService.cs.meta
-
292Packages/com.unity.services.authentication/Editor/AuthenticationSettingsElement.cs
-
3Packages/com.unity.services.authentication/Editor/AuthenticationSettingsElement.cs.meta
-
47Packages/com.unity.services.authentication/Editor/AuthenticationSettingsHelper.cs
-
3Packages/com.unity.services.authentication/Editor/AuthenticationSettingsHelper.cs.meta
-
58Packages/com.unity.services.authentication/Editor/AuthenticationSettingsProvider.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationSettingsProvider.cs.meta
-
20Packages/com.unity.services.authentication/Editor/AuthenticationTopMenu.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationTopMenu.cs.meta
-
79Packages/com.unity.services.authentication/Editor/IAuthenticationAdminClient.cs
-
3Packages/com.unity.services.authentication/Editor/IAuthenticationAdminClient.cs.meta
-
399Packages/com.unity.services.authentication/Editor/IdProviderElement.cs
-
3Packages/com.unity.services.authentication/Editor/IdProviderElement.cs.meta
-
8Packages/com.unity.services.authentication/Editor/Models.meta
-
49Packages/com.unity.services.authentication/Editor/Models/CreateIdProviderRequest.cs
-
11Packages/com.unity.services.authentication/Editor/Models/CreateIdProviderRequest.cs.meta
-
20Packages/com.unity.services.authentication/Editor/Models/DeleteIdProviderRequest.cs
-
11Packages/com.unity.services.authentication/Editor/Models/DeleteIdProviderRequest.cs.meta
-
19Packages/com.unity.services.authentication/Editor/Models/GetIdDomainResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/GetIdDomainResponse.cs.meta
-
56Packages/com.unity.services.authentication/Editor/Models/IdProviderResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/IdProviderResponse.cs.meta
-
19Packages/com.unity.services.authentication/Editor/Models/ListIdProviderResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/ListIdProviderResponse.cs.meta
-
29Packages/com.unity.services.authentication/Editor/Models/TokenExchangeErrorResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/TokenExchangeErrorResponse.cs.meta
-
16Packages/com.unity.services.authentication/Editor/Models/TokenExchangeRequest.cs
-
11Packages/com.unity.services.authentication/Editor/Models/TokenExchangeRequest.cs.meta
-
16Packages/com.unity.services.authentication/Editor/Models/TokenExchangeResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/TokenExchangeResponse.cs.meta
-
44Packages/com.unity.services.authentication/Editor/Models/UpdateIdProviderRequest.cs
-
11Packages/com.unity.services.authentication/Editor/Models/UpdateIdProviderRequest.cs.meta
-
3Packages/com.unity.services.authentication/Editor/USS.meta
-
41Packages/com.unity.services.authentication/Editor/USS/AuthenticationStyleSheet.uss
-
3Packages/com.unity.services.authentication/Editor/USS/AuthenticationStyleSheet.uss.meta
-
3Packages/com.unity.services.authentication/Editor/UXML.meta
-
15Packages/com.unity.services.authentication/Editor/UXML/AuthenticationProjectSettings.uxml
|
|||
using System; |
|||
using System.Text; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Just for fun, give a cute default player name if no name is provided.
|
|||
/// </summary>
|
|||
public static class NameGenerator |
|||
{ |
|||
public static string GetName(string userId) |
|||
{ |
|||
int seed = userId.GetHashCode(); |
|||
seed *= Math.Sign(seed); |
|||
StringBuilder nameOutput = new StringBuilder(); |
|||
#region Word part
|
|||
int word = seed % 22; |
|||
if (word == 0) // Note that some more data-driven approach would be better.
|
|||
nameOutput.Append("Ant"); |
|||
else if (word == 1) |
|||
nameOutput.Append("Bear"); |
|||
else if (word == 2) |
|||
nameOutput.Append("Cow"); |
|||
else if (word == 3) |
|||
nameOutput.Append("Dog"); |
|||
else if (word == 4) |
|||
nameOutput.Append("Eel"); |
|||
else if (word == 5) |
|||
nameOutput.Append("Frog"); |
|||
else if (word == 6) |
|||
nameOutput.Append("Gopher"); |
|||
else if (word == 7) |
|||
nameOutput.Append("Heron"); |
|||
else if (word == 8) |
|||
nameOutput.Append("Ibex"); |
|||
else if (word == 9) |
|||
nameOutput.Append("Jerboa"); |
|||
else if (word == 10) |
|||
nameOutput.Append("Koala"); |
|||
else if (word == 11) |
|||
nameOutput.Append("Llama"); |
|||
else if (word == 12) |
|||
nameOutput.Append("Moth"); |
|||
else if (word == 13) |
|||
nameOutput.Append("Newt"); |
|||
else if (word == 14) |
|||
nameOutput.Append("Owl"); |
|||
else if (word == 15) |
|||
nameOutput.Append("Puffin"); |
|||
else if (word == 16) |
|||
nameOutput.Append("Raven"); |
|||
else if (word == 17) |
|||
nameOutput.Append("Snake"); |
|||
else if (word == 18) |
|||
nameOutput.Append("Trout"); |
|||
else if (word == 19) |
|||
nameOutput.Append("Vulture"); |
|||
else if (word == 20) |
|||
nameOutput.Append("Wolf"); |
|||
else |
|||
nameOutput.Append("Zebra"); |
|||
#endregion
|
|||
|
|||
int number = seed % 1000; |
|||
nameOutput.Append(number.ToString("000")); |
|||
|
|||
return nameOutput.ToString(); |
|||
} |
|||
} |
|||
} |
|||
using System; |
|||
using System.Text; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Just for fun, give a cute default player name if no name is provided.
|
|||
/// </summary>
|
|||
public static class NameGenerator |
|||
{ |
|||
public static string GetName(string userId) |
|||
{ |
|||
int seed = userId.GetHashCode(); |
|||
seed *= Math.Sign(seed); |
|||
StringBuilder nameOutput = new StringBuilder(); |
|||
#region Word part
|
|||
int word = seed % 22; |
|||
if (word == 0) // Note that some more data-driven approach would be better.
|
|||
nameOutput.Append("Ant"); |
|||
else if (word == 1) |
|||
nameOutput.Append("Bear"); |
|||
else if (word == 2) |
|||
nameOutput.Append("Cow"); |
|||
else if (word == 3) |
|||
nameOutput.Append("Dog"); |
|||
else if (word == 4) |
|||
nameOutput.Append("Eel"); |
|||
else if (word == 5) |
|||
nameOutput.Append("Frog"); |
|||
else if (word == 6) |
|||
nameOutput.Append("Gopher"); |
|||
else if (word == 7) |
|||
nameOutput.Append("Heron"); |
|||
else if (word == 8) |
|||
nameOutput.Append("Ibex"); |
|||
else if (word == 9) |
|||
nameOutput.Append("Jerboa"); |
|||
else if (word == 10) |
|||
nameOutput.Append("Koala"); |
|||
else if (word == 11) |
|||
nameOutput.Append("Llama"); |
|||
else if (word == 12) |
|||
nameOutput.Append("Moth"); |
|||
else if (word == 13) |
|||
nameOutput.Append("Newt"); |
|||
else if (word == 14) |
|||
nameOutput.Append("Owl"); |
|||
else if (word == 15) |
|||
nameOutput.Append("Puffin"); |
|||
else if (word == 16) |
|||
nameOutput.Append("Raven"); |
|||
else if (word == 17) |
|||
nameOutput.Append("Snake"); |
|||
else if (word == 18) |
|||
nameOutput.Append("Trout"); |
|||
else if (word == 19) |
|||
nameOutput.Append("Vulture"); |
|||
else if (word == 20) |
|||
nameOutput.Append("Wolf"); |
|||
else |
|||
nameOutput.Append("Zebra"); |
|||
#endregion
|
|||
|
|||
int number = seed % 1000; |
|||
nameOutput.Append(number.ToString("000")); |
|||
|
|||
return nameOutput.ToString(); |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
[Flags] |
|||
public enum LobbyState |
|||
{ |
|||
Lobby = 1, |
|||
CountDown = 2, |
|||
InGame = 4 |
|||
} |
|||
|
|||
public struct LobbyInfo |
|||
{ |
|||
public string LobbyID { get; set; } |
|||
public string LobbyCode { get; set; } |
|||
public string RelayCode { get; set; } |
|||
public string LobbyName { get; set; } |
|||
public bool Private { get; set; } |
|||
public int MaxPlayerCount { get; set; } |
|||
public LobbyState State { get; set; } |
|||
public long? AllPlayersReadyTime { get; set; } |
|||
|
|||
public LobbyInfo(LobbyInfo existing) |
|||
{ |
|||
LobbyID = existing.LobbyID; |
|||
LobbyCode = existing.LobbyCode; |
|||
RelayCode = existing.RelayCode; |
|||
LobbyName = existing.LobbyName; |
|||
Private = existing.Private; |
|||
MaxPlayerCount = existing.MaxPlayerCount; |
|||
State = existing.State; |
|||
AllPlayersReadyTime = existing.AllPlayersReadyTime; |
|||
} |
|||
|
|||
public LobbyInfo(string lobbyCode) |
|||
{ |
|||
LobbyID = null; |
|||
LobbyCode = lobbyCode; |
|||
RelayCode = null; |
|||
LobbyName = null; |
|||
Private = false; |
|||
MaxPlayerCount = -1; |
|||
State = LobbyState.Lobby; |
|||
AllPlayersReadyTime = null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A local wrapper around a lobby's remote data, with additional functionality for providing that data to UI elements and tracking local player objects.
|
|||
/// </summary>
|
|||
[System.Serializable] |
|||
public class LocalLobby : Observed<LocalLobby> |
|||
{ |
|||
Dictionary<string, LobbyUser> m_LobbyUsers = new Dictionary<string, LobbyUser>(); |
|||
public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers; |
|||
|
|||
#region LocalLobbyData
|
|||
private LobbyInfo m_data; |
|||
public LobbyInfo Data |
|||
{ |
|||
get { return new LobbyInfo(m_data); } |
|||
} |
|||
|
|||
float m_CountDownTime; |
|||
|
|||
public float CountDownTime |
|||
{ |
|||
get { return m_CountDownTime; } |
|||
set |
|||
{ |
|||
m_CountDownTime = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
DateTime m_TargetEndTime; |
|||
|
|||
public DateTime TargetEndTime |
|||
{ |
|||
get => m_TargetEndTime; |
|||
set |
|||
{ |
|||
m_TargetEndTime = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
ServerAddress m_relayServer; |
|||
|
|||
public ServerAddress RelayServer |
|||
{ |
|||
get => m_relayServer; |
|||
set |
|||
{ |
|||
m_relayServer = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
public void AddPlayer(LobbyUser user) |
|||
{ |
|||
if (m_LobbyUsers.ContainsKey(user.ID)) |
|||
{ |
|||
Debug.LogError($"Cant add player {user.DisplayName}({user.ID}) to lobby: {LobbyID} twice"); |
|||
return; |
|||
} |
|||
|
|||
DoAddPlayer(user); |
|||
OnChanged(this); |
|||
} |
|||
|
|||
private void DoAddPlayer(LobbyUser user) |
|||
{ |
|||
m_LobbyUsers.Add(user.ID, user); |
|||
user.onChanged += OnChangedUser; |
|||
} |
|||
|
|||
public void RemovePlayer(LobbyUser user) |
|||
{ |
|||
DoRemoveUser(user); |
|||
OnChanged(this); |
|||
} |
|||
|
|||
private void DoRemoveUser(LobbyUser user) |
|||
{ |
|||
if (!m_LobbyUsers.ContainsKey(user.ID)) |
|||
{ |
|||
Debug.LogWarning($"Player {user.DisplayName}({user.ID}) does not exist in lobby: {LobbyID}"); |
|||
return; |
|||
} |
|||
|
|||
m_LobbyUsers.Remove(user.ID); |
|||
user.onChanged -= OnChangedUser; |
|||
} |
|||
|
|||
private void OnChangedUser(LobbyUser user) |
|||
{ |
|||
OnChanged(this); |
|||
} |
|||
|
|||
public string LobbyID |
|||
{ |
|||
get => m_data.LobbyID; |
|||
set |
|||
{ |
|||
m_data.LobbyID = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public string LobbyCode |
|||
{ |
|||
get => m_data.LobbyCode; |
|||
set |
|||
{ |
|||
m_data.LobbyCode = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public string RelayCode |
|||
{ |
|||
get => m_data.RelayCode; |
|||
set |
|||
{ |
|||
m_data.RelayCode = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public string LobbyName |
|||
{ |
|||
get => m_data.LobbyName; |
|||
set |
|||
{ |
|||
m_data.LobbyName = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public LobbyState State |
|||
{ |
|||
get => m_data.State; |
|||
set |
|||
{ |
|||
m_data.State = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public bool Private |
|||
{ |
|||
get => m_data.Private; |
|||
set |
|||
{ |
|||
m_data.Private = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public int PlayerCount => m_LobbyUsers.Count; |
|||
|
|||
public int MaxPlayerCount |
|||
{ |
|||
get => m_data.MaxPlayerCount; |
|||
set |
|||
{ |
|||
m_data.MaxPlayerCount = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public long? AllPlayersReadyTime => m_data.AllPlayersReadyTime; |
|||
|
|||
/// <summary>
|
|||
/// Checks if we have n players that have the Status.
|
|||
/// -1 Count means you need all Lobbyusers
|
|||
/// </summary>
|
|||
/// <returns>True if enough players are of the input status.</returns>
|
|||
public bool PlayersOfState(UserStatus status, int playersCount = -1) |
|||
{ |
|||
var statePlayers = m_LobbyUsers.Values.Count(user => user.UserStatus == status); |
|||
|
|||
if (playersCount < 0) |
|||
return statePlayers == m_LobbyUsers.Count; |
|||
return statePlayers == playersCount; |
|||
} |
|||
|
|||
public void CopyObserved(LobbyInfo info, Dictionary<string, LobbyUser> oldUsers) |
|||
{ |
|||
m_data = info; |
|||
if (oldUsers == null) |
|||
m_LobbyUsers = new Dictionary<string, LobbyUser>(); |
|||
else |
|||
{ |
|||
List<LobbyUser> toRemove = new List<LobbyUser>(); |
|||
foreach (var user in m_LobbyUsers) |
|||
{ |
|||
if (oldUsers.ContainsKey(user.Key)) |
|||
user.Value.CopyObserved(oldUsers[user.Key]); |
|||
else |
|||
toRemove.Add(user.Value); |
|||
} |
|||
|
|||
foreach (var remove in toRemove) |
|||
{ |
|||
DoRemoveUser(remove); |
|||
} |
|||
|
|||
foreach (var oldUser in oldUsers) |
|||
{ |
|||
if (!m_LobbyUsers.ContainsKey(oldUser.Key)) |
|||
DoAddPlayer(oldUser.Value); |
|||
} |
|||
} |
|||
|
|||
OnChanged(this); |
|||
} |
|||
|
|||
public override void CopyObserved(LocalLobby oldObserved) |
|||
{ |
|||
CopyObserved(oldObserved.Data, oldObserved.m_LobbyUsers); |
|||
} |
|||
} |
|||
} |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
[Flags] |
|||
public enum LobbyState |
|||
{ |
|||
Lobby = 1, |
|||
CountDown = 2, |
|||
InGame = 4 |
|||
} |
|||
|
|||
public struct LobbyInfo |
|||
{ |
|||
public string LobbyID { get; set; } |
|||
public string LobbyCode { get; set; } |
|||
public string RelayCode { get; set; } |
|||
public string LobbyName { get; set; } |
|||
public bool Private { get; set; } |
|||
public int MaxPlayerCount { get; set; } |
|||
public LobbyState State { get; set; } |
|||
public long? AllPlayersReadyTime { get; set; } |
|||
|
|||
public LobbyInfo(LobbyInfo existing) |
|||
{ |
|||
LobbyID = existing.LobbyID; |
|||
LobbyCode = existing.LobbyCode; |
|||
RelayCode = existing.RelayCode; |
|||
LobbyName = existing.LobbyName; |
|||
Private = existing.Private; |
|||
MaxPlayerCount = existing.MaxPlayerCount; |
|||
State = existing.State; |
|||
AllPlayersReadyTime = existing.AllPlayersReadyTime; |
|||
} |
|||
|
|||
public LobbyInfo(string lobbyCode) |
|||
{ |
|||
LobbyID = null; |
|||
LobbyCode = lobbyCode; |
|||
RelayCode = null; |
|||
LobbyName = null; |
|||
Private = false; |
|||
MaxPlayerCount = -1; |
|||
State = LobbyState.Lobby; |
|||
AllPlayersReadyTime = null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A local wrapper around a lobby's remote data, with additional functionality for providing that data to UI elements and tracking local player objects.
|
|||
/// </summary>
|
|||
[System.Serializable] |
|||
public class LocalLobby : Observed<LocalLobby> |
|||
{ |
|||
Dictionary<string, LobbyUser> m_LobbyUsers = new Dictionary<string, LobbyUser>(); |
|||
public Dictionary<string, LobbyUser> LobbyUsers => m_LobbyUsers; |
|||
|
|||
#region LocalLobbyData
|
|||
private LobbyInfo m_data; |
|||
public LobbyInfo Data |
|||
{ |
|||
get { return new LobbyInfo(m_data); } |
|||
} |
|||
|
|||
float m_CountDownTime; |
|||
|
|||
public float CountDownTime |
|||
{ |
|||
get { return m_CountDownTime; } |
|||
set |
|||
{ |
|||
m_CountDownTime = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
DateTime m_TargetEndTime; |
|||
|
|||
public DateTime TargetEndTime |
|||
{ |
|||
get => m_TargetEndTime; |
|||
set |
|||
{ |
|||
m_TargetEndTime = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
ServerAddress m_relayServer; |
|||
|
|||
public ServerAddress RelayServer |
|||
{ |
|||
get => m_relayServer; |
|||
set |
|||
{ |
|||
m_relayServer = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
public void AddPlayer(LobbyUser user) |
|||
{ |
|||
if (m_LobbyUsers.ContainsKey(user.ID)) |
|||
{ |
|||
Debug.LogError($"Cant add player {user.DisplayName}({user.ID}) to lobby: {LobbyID} twice"); |
|||
return; |
|||
} |
|||
|
|||
DoAddPlayer(user); |
|||
OnChanged(this); |
|||
} |
|||
|
|||
private void DoAddPlayer(LobbyUser user) |
|||
{ |
|||
m_LobbyUsers.Add(user.ID, user); |
|||
user.onChanged += OnChangedUser; |
|||
} |
|||
|
|||
public void RemovePlayer(LobbyUser user) |
|||
{ |
|||
DoRemoveUser(user); |
|||
OnChanged(this); |
|||
} |
|||
|
|||
private void DoRemoveUser(LobbyUser user) |
|||
{ |
|||
if (!m_LobbyUsers.ContainsKey(user.ID)) |
|||
{ |
|||
Debug.LogWarning($"Player {user.DisplayName}({user.ID}) does not exist in lobby: {LobbyID}"); |
|||
return; |
|||
} |
|||
|
|||
m_LobbyUsers.Remove(user.ID); |
|||
user.onChanged -= OnChangedUser; |
|||
} |
|||
|
|||
private void OnChangedUser(LobbyUser user) |
|||
{ |
|||
OnChanged(this); |
|||
} |
|||
|
|||
public string LobbyID |
|||
{ |
|||
get => m_data.LobbyID; |
|||
set |
|||
{ |
|||
m_data.LobbyID = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public string LobbyCode |
|||
{ |
|||
get => m_data.LobbyCode; |
|||
set |
|||
{ |
|||
m_data.LobbyCode = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public string RelayCode |
|||
{ |
|||
get => m_data.RelayCode; |
|||
set |
|||
{ |
|||
m_data.RelayCode = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public string LobbyName |
|||
{ |
|||
get => m_data.LobbyName; |
|||
set |
|||
{ |
|||
m_data.LobbyName = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public LobbyState State |
|||
{ |
|||
get => m_data.State; |
|||
set |
|||
{ |
|||
m_data.State = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public bool Private |
|||
{ |
|||
get => m_data.Private; |
|||
set |
|||
{ |
|||
m_data.Private = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public int PlayerCount => m_LobbyUsers.Count; |
|||
|
|||
public int MaxPlayerCount |
|||
{ |
|||
get => m_data.MaxPlayerCount; |
|||
set |
|||
{ |
|||
m_data.MaxPlayerCount = value; |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
|
|||
public long? AllPlayersReadyTime => m_data.AllPlayersReadyTime; |
|||
|
|||
/// <summary>
|
|||
/// Checks if we have n players that have the Status.
|
|||
/// -1 Count means you need all Lobbyusers
|
|||
/// </summary>
|
|||
/// <returns>True if enough players are of the input status.</returns>
|
|||
public bool PlayersOfState(UserStatus status, int playersCount = -1) |
|||
{ |
|||
var statePlayers = m_LobbyUsers.Values.Count(user => user.UserStatus == status); |
|||
|
|||
if (playersCount < 0) |
|||
return statePlayers == m_LobbyUsers.Count; |
|||
return statePlayers == playersCount; |
|||
} |
|||
|
|||
public void CopyObserved(LobbyInfo info, Dictionary<string, LobbyUser> oldUsers) |
|||
{ |
|||
m_data = info; |
|||
if (oldUsers == null) |
|||
m_LobbyUsers = new Dictionary<string, LobbyUser>(); |
|||
else |
|||
{ |
|||
List<LobbyUser> toRemove = new List<LobbyUser>(); |
|||
foreach (var user in m_LobbyUsers) |
|||
{ |
|||
if (oldUsers.ContainsKey(user.Key)) |
|||
user.Value.CopyObserved(oldUsers[user.Key]); |
|||
else |
|||
toRemove.Add(user.Value); |
|||
} |
|||
|
|||
foreach (var remove in toRemove) |
|||
{ |
|||
DoRemoveUser(remove); |
|||
} |
|||
|
|||
foreach (var oldUser in oldUsers) |
|||
{ |
|||
if (!m_LobbyUsers.ContainsKey(oldUser.Key)) |
|||
DoAddPlayer(oldUser.Value); |
|||
} |
|||
} |
|||
|
|||
OnChanged(this); |
|||
} |
|||
|
|||
public override void CopyObserved(LocalLobby oldObserved) |
|||
{ |
|||
CopyObserved(oldObserved.Data, oldObserved.m_LobbyUsers); |
|||
} |
|||
} |
|||
} |
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public class LocalLobbyObserver : ObserverBehaviour<LocalLobby> { } |
|||
} |
|||
namespace LobbyRelaySample |
|||
{ |
|||
public class LocalLobbyObserver : ObserverBehaviour<LocalLobby> { } |
|||
} |
|
|||
using LobbyRelaySample.Auth; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Allows Located services to transfer data to their replacements if needed.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The base interface type you want to Provide.</typeparam>
|
|||
public interface IProvidable<T> |
|||
{ |
|||
void OnReProvided(T previousProvider); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Base Locator behavior, without static access.
|
|||
/// </summary>
|
|||
public class LocatorBase |
|||
{ |
|||
private Dictionary<Type, object> m_provided = new Dictionary<Type, object>(); |
|||
|
|||
/// <summary>
|
|||
/// On construction, we can prepare default implementations of any services we expect to be required. This way, if for some reason the actual implementations
|
|||
/// are never Provided (e.g. for tests), nothing will break.
|
|||
/// </summary>
|
|||
public LocatorBase() |
|||
{ |
|||
Provide(new Messenger()); |
|||
Provide(new UpdateSlowNoop()); |
|||
Provide(new IdentityNoop()); |
|||
|
|||
FinishConstruction(); |
|||
} |
|||
|
|||
protected virtual void FinishConstruction() { } |
|||
|
|||
/// <summary>
|
|||
/// Call this to indicate that something is available for global access.
|
|||
/// </summary>
|
|||
private void ProvideAny<T>(T instance) where T : IProvidable<T> |
|||
{ |
|||
Type type = typeof(T); |
|||
if (m_provided.ContainsKey(type)) |
|||
{ |
|||
var previousProvision = (T)m_provided[type]; |
|||
instance.OnReProvided(previousProvision); |
|||
m_provided.Remove(type); |
|||
} |
|||
|
|||
m_provided.Add(type, instance); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// If a T has previously been Provided, this will retrieve it. Else, null is returned.
|
|||
/// </summary>
|
|||
private T Locate<T>() where T : class |
|||
{ |
|||
Type type = typeof(T); |
|||
if (!m_provided.ContainsKey(type)) |
|||
return null; |
|||
return m_provided[type] as T; |
|||
} |
|||
|
|||
// To limit global access to only components that should have it, and to reduce programmer error, we'll declare explicit flavors of Provide and getters for them.
|
|||
public IMessenger Messenger => Locate<IMessenger>(); |
|||
public void Provide(IMessenger messenger) { ProvideAny(messenger); } |
|||
|
|||
public IUpdateSlow UpdateSlow => Locate<IUpdateSlow>(); |
|||
public void Provide(IUpdateSlow updateSlow) { ProvideAny(updateSlow); } |
|||
|
|||
public IIdentity Identity => Locate<IIdentity>(); |
|||
public void Provide(IIdentity identity) { ProvideAny(identity); } |
|||
|
|||
// As you add more Provided types, be sure their default implementations are included in the constructor.
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Anything which provides itself to a Locator can then be globally accessed. This should be a single access point for things that *want* to be singleton (that is,
|
|||
/// when they want to be available for use by arbitrary, unknown clients) but might not always be available or might need alternate flavors for tests, logging, etc.
|
|||
/// </summary>
|
|||
public class Locator : LocatorBase |
|||
{ |
|||
private static Locator s_instance; |
|||
|
|||
public static Locator Get |
|||
{ |
|||
get |
|||
{ |
|||
if (s_instance == null) |
|||
s_instance = new Locator(); |
|||
return s_instance; |
|||
} |
|||
} |
|||
|
|||
protected override void FinishConstruction() |
|||
{ |
|||
s_instance = this; |
|||
} |
|||
} |
|||
using LobbyRelaySample.Auth; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Allows Located services to transfer data to their replacements if needed.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The base interface type you want to Provide.</typeparam>
|
|||
public interface IProvidable<T> |
|||
{ |
|||
void OnReProvided(T previousProvider); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Base Locator behavior, without static access.
|
|||
/// </summary>
|
|||
public class LocatorBase |
|||
{ |
|||
private Dictionary<Type, object> m_provided = new Dictionary<Type, object>(); |
|||
|
|||
/// <summary>
|
|||
/// On construction, we can prepare default implementations of any services we expect to be required. This way, if for some reason the actual implementations
|
|||
/// are never Provided (e.g. for tests), nothing will break.
|
|||
/// </summary>
|
|||
public LocatorBase() |
|||
{ |
|||
Provide(new Messenger()); |
|||
Provide(new UpdateSlowNoop()); |
|||
Provide(new IdentityNoop()); |
|||
|
|||
FinishConstruction(); |
|||
} |
|||
|
|||
protected virtual void FinishConstruction() { } |
|||
|
|||
/// <summary>
|
|||
/// Call this to indicate that something is available for global access.
|
|||
/// </summary>
|
|||
private void ProvideAny<T>(T instance) where T : IProvidable<T> |
|||
{ |
|||
Type type = typeof(T); |
|||
if (m_provided.ContainsKey(type)) |
|||
{ |
|||
var previousProvision = (T)m_provided[type]; |
|||
instance.OnReProvided(previousProvision); |
|||
m_provided.Remove(type); |
|||
} |
|||
|
|||
m_provided.Add(type, instance); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// If a T has previously been Provided, this will retrieve it. Else, null is returned.
|
|||
/// </summary>
|
|||
private T Locate<T>() where T : class |
|||
{ |
|||
Type type = typeof(T); |
|||
if (!m_provided.ContainsKey(type)) |
|||
return null; |
|||
return m_provided[type] as T; |
|||
} |
|||
|
|||
// To limit global access to only components that should have it, and to reduce programmer error, we'll declare explicit flavors of Provide and getters for them.
|
|||
public IMessenger Messenger => Locate<IMessenger>(); |
|||
public void Provide(IMessenger messenger) { ProvideAny(messenger); } |
|||
|
|||
public IUpdateSlow UpdateSlow => Locate<IUpdateSlow>(); |
|||
public void Provide(IUpdateSlow updateSlow) { ProvideAny(updateSlow); } |
|||
|
|||
public IIdentity Identity => Locate<IIdentity>(); |
|||
public void Provide(IIdentity identity) { ProvideAny(identity); } |
|||
|
|||
// As you add more Provided types, be sure their default implementations are included in the constructor.
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Anything which provides itself to a Locator can then be globally accessed. This should be a single access point for things that *want* to be singleton (that is,
|
|||
/// when they want to be available for use by arbitrary, unknown clients) but might not always be available or might need alternate flavors for tests, logging, etc.
|
|||
/// </summary>
|
|||
public class Locator : LocatorBase |
|||
{ |
|||
private static Locator s_instance; |
|||
|
|||
public static Locator Get |
|||
{ |
|||
get |
|||
{ |
|||
if (s_instance == null) |
|||
s_instance = new Locator(); |
|||
return s_instance; |
|||
} |
|||
} |
|||
|
|||
protected override void FinishConstruction() |
|||
{ |
|||
s_instance = this; |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using UnityEngine; |
|||
using Object = UnityEngine.Object; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public enum LogMode |
|||
{ |
|||
Critical, // Errors only.
|
|||
Warnings, // Errors and Warnings
|
|||
Verbose // Everything
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Overrides the Default Unity Logging with our own
|
|||
/// </summary>
|
|||
public class LogHandler : ILogHandler |
|||
{ |
|||
public LogMode mode = LogMode.Critical; |
|||
|
|||
static LogHandler s_instance; |
|||
ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler; //Store the unity default logger to print to console.
|
|||
|
|||
public static LogHandler Get() |
|||
{ |
|||
if (s_instance != null) return s_instance; |
|||
s_instance = new LogHandler(); |
|||
Debug.unityLogger.logHandler = s_instance; |
|||
return s_instance; |
|||
} |
|||
|
|||
public void LogFormat(LogType logType, Object context, string format, params object[] args) |
|||
{ |
|||
if (logType == LogType.Exception) // Exceptions are captured by LogException?
|
|||
return; |
|||
|
|||
if (logType == LogType.Error || logType == LogType.Assert) |
|||
{ |
|||
m_DefaultLogHandler.LogFormat(logType, context, format, args); |
|||
return; |
|||
} |
|||
|
|||
if (mode == LogMode.Critical) |
|||
return; |
|||
|
|||
if (logType == LogType.Warning) |
|||
{ |
|||
m_DefaultLogHandler.LogFormat(logType, context, format, args); |
|||
return; |
|||
} |
|||
|
|||
if (mode != LogMode.Verbose) |
|||
return; |
|||
|
|||
m_DefaultLogHandler.LogFormat(logType, context, format, args); |
|||
} |
|||
|
|||
public void LogException(Exception exception, Object context) |
|||
{ |
|||
m_DefaultLogHandler.LogException(exception, context); |
|||
} |
|||
} |
|||
} |
|||
using System; |
|||
using UnityEngine; |
|||
using Object = UnityEngine.Object; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public enum LogMode |
|||
{ |
|||
Critical, // Errors only.
|
|||
Warnings, // Errors and Warnings
|
|||
Verbose // Everything
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Overrides the default Unity logging with our own, so that verbose logs (both from the services and from any of our Debug.Log* calls) don't clutter the Console.
|
|||
/// </summary>
|
|||
public class LogHandler : ILogHandler |
|||
{ |
|||
public LogMode mode = LogMode.Critical; |
|||
|
|||
static LogHandler s_instance; |
|||
ILogHandler m_DefaultLogHandler = Debug.unityLogger.logHandler; //Store the unity default logger to print to console.
|
|||
|
|||
public static LogHandler Get() |
|||
{ |
|||
if (s_instance != null) return s_instance; |
|||
s_instance = new LogHandler(); |
|||
Debug.unityLogger.logHandler = s_instance; |
|||
return s_instance; |
|||
} |
|||
|
|||
public void LogFormat(LogType logType, Object context, string format, params object[] args) |
|||
{ |
|||
if (logType == LogType.Exception) // Exceptions are captured by LogException?
|
|||
return; |
|||
|
|||
if (logType == LogType.Error || logType == LogType.Assert) |
|||
{ |
|||
m_DefaultLogHandler.LogFormat(logType, context, format, args); |
|||
return; |
|||
} |
|||
|
|||
if (mode == LogMode.Critical) |
|||
return; |
|||
|
|||
if (logType == LogType.Warning) |
|||
{ |
|||
m_DefaultLogHandler.LogFormat(logType, context, format, args); |
|||
return; |
|||
} |
|||
|
|||
if (mode != LogMode.Verbose) |
|||
return; |
|||
|
|||
m_DefaultLogHandler.LogFormat(logType, context, format, args); |
|||
} |
|||
|
|||
public void LogException(Exception exception, Object context) |
|||
{ |
|||
m_DefaultLogHandler.LogException(exception, context); |
|||
} |
|||
} |
|||
} |
|
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
using Stopwatch = System.Diagnostics.Stopwatch; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Ensure that message contents are obvious but not dependent on spelling strings correctly.
|
|||
/// </summary>
|
|||
public enum MessageType |
|||
{ |
|||
// These are assigned arbitrary explicit values so that if a MessageType is serialized and more enum values are later inserted/removed, the serialized values need not be reassigned.
|
|||
// (If you want to remove a message, make sure it isn't serialized somewhere first.)
|
|||
None = 0, |
|||
RenameRequest = 1, |
|||
JoinLobbyRequest = 2, |
|||
CreateLobbyRequest = 3, |
|||
QueryLobbies = 4, |
|||
PlayerJoinedLobby = 5, |
|||
PlayerLeftLobby = 6, |
|||
ChangeGameState = 7, |
|||
ChangeLobbyUserState = 8, |
|||
HostInitReadyCheck = 9, |
|||
LocalUserReadyCheckResponse = 10, |
|||
UserSetEmote = 11, |
|||
ToLobby = 12, |
|||
Client_EndReadyCountdownAt = 13, |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Something that wants to subscribe to messages from arbitrary, unknown senders.
|
|||
/// </summary>
|
|||
public interface IReceiveMessages |
|||
{ |
|||
void OnReceiveMessage(MessageType type, object msg); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Something to which IReceiveMessages can send/subscribe for arbitrary messages.
|
|||
/// </summary>
|
|||
public interface IMessenger : IReceiveMessages, IProvidable<IMessenger> |
|||
{ |
|||
void Subscribe(IReceiveMessages receiver); |
|||
void Unsubscribe(IReceiveMessages receiver); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Core mechanism for routing messages to arbitrary listeners.
|
|||
/// </summary>
|
|||
public class Messenger : IMessenger |
|||
{ |
|||
private List<IReceiveMessages> m_receivers = new List<IReceiveMessages>(); |
|||
private const float k_durationToleranceMs = 10; |
|||
|
|||
/// <summary>
|
|||
/// Assume that you won't receive messages in a specific order.
|
|||
/// </summary>
|
|||
public virtual void Subscribe(IReceiveMessages receiver) |
|||
{ |
|||
if (!m_receivers.Contains(receiver)) |
|||
m_receivers.Add(receiver); |
|||
} |
|||
|
|||
public virtual void Unsubscribe(IReceiveMessages receiver) |
|||
{ |
|||
m_receivers.Remove(receiver); |
|||
} |
|||
|
|||
public virtual void OnReceiveMessage(MessageType type, object msg) |
|||
{ |
|||
Stopwatch stopwatch = new Stopwatch(); |
|||
for (int r = 0; r < m_receivers.Count; r++) |
|||
{ |
|||
stopwatch.Restart(); |
|||
m_receivers[r].OnReceiveMessage(type, msg); |
|||
stopwatch.Stop(); |
|||
if (stopwatch.ElapsedMilliseconds > k_durationToleranceMs) |
|||
Debug.LogWarning($"Message recipient \"{m_receivers[r]}\" took too long to process message \"{msg}\" of type {type}"); |
|||
} |
|||
} |
|||
|
|||
public void OnReProvided(IMessenger previousProvider) |
|||
{ |
|||
if (previousProvider is Messenger) |
|||
m_receivers.AddRange((previousProvider as Messenger).m_receivers); |
|||
} |
|||
} |
|||
} |
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
using Stopwatch = System.Diagnostics.Stopwatch; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Ensure that message contents are obvious but not dependent on spelling strings correctly.
|
|||
/// </summary>
|
|||
public enum MessageType |
|||
{ |
|||
// These are assigned arbitrary explicit values so that if a MessageType is serialized and more enum values are later inserted/removed, the serialized values need not be reassigned.
|
|||
// (If you want to remove a message, make sure it isn't serialized somewhere first.)
|
|||
None = 0, |
|||
RenameRequest = 1, |
|||
JoinLobbyRequest = 2, |
|||
CreateLobbyRequest = 3, |
|||
QueryLobbies = 4, |
|||
PlayerJoinedLobby = 5, |
|||
PlayerLeftLobby = 6, |
|||
ChangeGameState = 7, |
|||
ChangeLobbyUserState = 8, |
|||
HostInitReadyCheck = 9, |
|||
LocalUserReadyCheckResponse = 10, |
|||
UserSetEmote = 11, |
|||
ToLobby = 12, |
|||
Client_EndReadyCountdownAt = 13, |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Something that wants to subscribe to messages from arbitrary, unknown senders.
|
|||
/// </summary>
|
|||
public interface IReceiveMessages |
|||
{ |
|||
void OnReceiveMessage(MessageType type, object msg); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Something to which IReceiveMessages can send/subscribe for arbitrary messages.
|
|||
/// </summary>
|
|||
public interface IMessenger : IReceiveMessages, IProvidable<IMessenger> |
|||
{ |
|||
void Subscribe(IReceiveMessages receiver); |
|||
void Unsubscribe(IReceiveMessages receiver); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Core mechanism for routing messages to arbitrary listeners.
|
|||
/// </summary>
|
|||
public class Messenger : IMessenger |
|||
{ |
|||
private List<IReceiveMessages> m_receivers = new List<IReceiveMessages>(); |
|||
private const float k_durationToleranceMs = 10; |
|||
|
|||
/// <summary>
|
|||
/// Assume that you won't receive messages in a specific order.
|
|||
/// </summary>
|
|||
public virtual void Subscribe(IReceiveMessages receiver) |
|||
{ |
|||
if (!m_receivers.Contains(receiver)) |
|||
m_receivers.Add(receiver); |
|||
} |
|||
|
|||
public virtual void Unsubscribe(IReceiveMessages receiver) |
|||
{ |
|||
m_receivers.Remove(receiver); |
|||
} |
|||
|
|||
public virtual void OnReceiveMessage(MessageType type, object msg) |
|||
{ |
|||
Stopwatch stopwatch = new Stopwatch(); |
|||
for (int r = 0; r < m_receivers.Count; r++) |
|||
{ |
|||
stopwatch.Restart(); |
|||
m_receivers[r].OnReceiveMessage(type, msg); |
|||
stopwatch.Stop(); |
|||
if (stopwatch.ElapsedMilliseconds > k_durationToleranceMs) |
|||
Debug.LogWarning($"Message recipient \"{m_receivers[r]}\" took too long to process message \"{msg}\" of type {type}"); |
|||
} |
|||
} |
|||
|
|||
public void OnReProvided(IMessenger previousProvider) |
|||
{ |
|||
if (previousProvider is Messenger) |
|||
m_receivers.AddRange((previousProvider as Messenger).m_receivers); |
|||
} |
|||
} |
|||
} |
|
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
using Stopwatch = System.Diagnostics.Stopwatch; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public delegate void UpdateMethod(float dt); |
|||
|
|||
public interface IUpdateSlow : IProvidable<IUpdateSlow> |
|||
{ |
|||
void OnUpdate(float dt); |
|||
void Subscribe(UpdateMethod onUpdate); |
|||
void Unsubscribe(UpdateMethod onUpdate); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A default implementation.
|
|||
/// </summary>
|
|||
public class UpdateSlowNoop : IUpdateSlow |
|||
{ |
|||
public void OnUpdate(float dt) { } |
|||
public void Subscribe(UpdateMethod onUpdate) { } |
|||
public void Unsubscribe(UpdateMethod onUpdate) { } |
|||
public void OnReProvided(IUpdateSlow prev) { } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Some objects might need to be on a slower update loop than the usual MonoBehaviour Update, e.g. to refresh data from services.
|
|||
/// Some might also not want to be coupled to a Unity object at all but still need an update loop.
|
|||
/// </summary>
|
|||
public class UpdateSlow : MonoBehaviour, IUpdateSlow |
|||
{ |
|||
[SerializeField] |
|||
[Tooltip("Update interval. Note that lobby Get requests must occur at least 1 second apart, so this period should likely be greater than that.")] |
|||
private float m_updatePeriod = 1.5f; |
|||
[SerializeField] |
|||
[Tooltip("If a subscriber to slow update takes longer than this to execute, it can be automatically unsubscribed.")] |
|||
private float m_durationToleranceMs = 10; |
|||
[SerializeField] |
|||
[Tooltip("We ordinarily automatically remove a subscriber that takes too long. Otherwise, we'll simply log.")] |
|||
private bool m_doNotRemoveIfTooLong = false; |
|||
private List<UpdateMethod> m_subscribers = new List<UpdateMethod>(); |
|||
private float m_updateTimer = 0; |
|||
private int m_nextActiveSubIndex = 0; // For staggering subscribers, to prevent spikes of lots of things triggering at once.
|
|||
|
|||
public void Awake() |
|||
{ |
|||
Locator.Get.Provide(this); |
|||
} |
|||
public void OnDestroy() |
|||
{ |
|||
// We should clean up references in case they would prevent garbage collection.
|
|||
m_subscribers.Clear(); |
|||
} |
|||
|
|||
/// <summary>Don't assume that onUpdate will be called in any particular order compared to other subscribers.</summary>
|
|||
public void Subscribe(UpdateMethod onUpdate) |
|||
{ |
|||
if (!m_subscribers.Contains(onUpdate)) |
|||
m_subscribers.Add(onUpdate); |
|||
} |
|||
/// <summary>Safe to call even if onUpdate was not previously Subscribed.</summary>
|
|||
public void Unsubscribe(UpdateMethod onUpdate) |
|||
{ |
|||
int index = m_subscribers.IndexOf(onUpdate); |
|||
if (index >= 0) |
|||
{ |
|||
m_subscribers.Remove(onUpdate); |
|||
if (index < m_nextActiveSubIndex) |
|||
m_nextActiveSubIndex--; |
|||
} |
|||
} |
|||
|
|||
private void Update() |
|||
{ |
|||
if (m_subscribers.Count == 0) |
|||
return; |
|||
m_updateTimer += Time.deltaTime; |
|||
float effectivePeriod = m_updatePeriod / m_subscribers.Count; |
|||
while (m_updateTimer > effectivePeriod) |
|||
{ |
|||
m_updateTimer -= effectivePeriod; |
|||
OnUpdate(effectivePeriod); |
|||
} |
|||
} |
|||
|
|||
public void OnUpdate(float dt) |
|||
{ |
|||
Stopwatch stopwatch = new Stopwatch(); |
|||
m_nextActiveSubIndex = System.Math.Max(0, System.Math.Min(m_subscribers.Count - 1, m_nextActiveSubIndex)); // Just a backup.
|
|||
UpdateMethod onUpdate = m_subscribers[m_nextActiveSubIndex]; |
|||
if (onUpdate == null || onUpdate.Target == null) // In case something forgets to Unsubscribe when it dies.
|
|||
{ Remove(m_nextActiveSubIndex, $"Did not Unsubscribe from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}"); |
|||
return; |
|||
} |
|||
if (onUpdate.Method.ToString().Contains("<")) // Detect an anonymous or lambda or local method that cannot be Unsubscribed, by checking for a character that can't exist in a declared method name.
|
|||
{ Remove(m_nextActiveSubIndex, $"Removed anonymous from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}"); |
|||
return; |
|||
} |
|||
|
|||
stopwatch.Restart(); |
|||
onUpdate?.Invoke(dt); |
|||
stopwatch.Stop(); |
|||
if (stopwatch.ElapsedMilliseconds > m_durationToleranceMs) |
|||
{ |
|||
if (!m_doNotRemoveIfTooLong) |
|||
Remove(m_nextActiveSubIndex, $"UpdateSlow subscriber took too long, removing: {onUpdate.Target} : {onUpdate.Method}"); |
|||
else |
|||
{ |
|||
Debug.LogWarning($"UpdateSlow subscriber took too long: {onUpdate.Target} : {onUpdate.Method}"); |
|||
Increment(); |
|||
} |
|||
} |
|||
else |
|||
Increment(); |
|||
|
|||
void Remove(int index, string msg) |
|||
{ |
|||
m_subscribers.RemoveAt(index); |
|||
m_nextActiveSubIndex--; |
|||
Debug.LogError(msg); |
|||
Increment(); |
|||
} |
|||
void Increment() |
|||
{ |
|||
m_nextActiveSubIndex++; |
|||
if (m_nextActiveSubIndex >= m_subscribers.Count) |
|||
m_nextActiveSubIndex = 0; |
|||
} |
|||
} |
|||
|
|||
public void OnReProvided(IUpdateSlow prevUpdateSlow) |
|||
{ |
|||
if (prevUpdateSlow is UpdateSlow) |
|||
m_subscribers.AddRange((prevUpdateSlow as UpdateSlow).m_subscribers); |
|||
} |
|||
} |
|||
} |
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
using Stopwatch = System.Diagnostics.Stopwatch; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public delegate void UpdateMethod(float dt); |
|||
|
|||
public interface IUpdateSlow : IProvidable<IUpdateSlow> |
|||
{ |
|||
void OnUpdate(float dt); |
|||
void Subscribe(UpdateMethod onUpdate); |
|||
void Unsubscribe(UpdateMethod onUpdate); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A default implementation.
|
|||
/// </summary>
|
|||
public class UpdateSlowNoop : IUpdateSlow |
|||
{ |
|||
public void OnUpdate(float dt) { } |
|||
public void Subscribe(UpdateMethod onUpdate) { } |
|||
public void Unsubscribe(UpdateMethod onUpdate) { } |
|||
public void OnReProvided(IUpdateSlow prev) { } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Some objects might need to be on a slower update loop than the usual MonoBehaviour Update, e.g. to refresh data from services.
|
|||
/// Some might also not want to be coupled to a Unity object at all but still need an update loop.
|
|||
/// </summary>
|
|||
public class UpdateSlow : MonoBehaviour, IUpdateSlow |
|||
{ |
|||
[SerializeField] |
|||
[Tooltip("Update interval. Note that lobby Get requests must occur at least 1 second apart, so this period should likely be greater than that.")] |
|||
private float m_updatePeriod = 1.5f; |
|||
[SerializeField] |
|||
[Tooltip("If a subscriber to slow update takes longer than this to execute, it can be automatically unsubscribed.")] |
|||
private float m_durationToleranceMs = 10; |
|||
[SerializeField] |
|||
[Tooltip("We ordinarily automatically remove a subscriber that takes too long. Otherwise, we'll simply log.")] |
|||
private bool m_doNotRemoveIfTooLong = false; |
|||
private List<UpdateMethod> m_subscribers = new List<UpdateMethod>(); |
|||
private float m_updateTimer = 0; |
|||
private int m_nextActiveSubIndex = 0; // For staggering subscribers, to prevent spikes of lots of things triggering at once.
|
|||
|
|||
public void Awake() |
|||
{ |
|||
Locator.Get.Provide(this); |
|||
} |
|||
public void OnDestroy() |
|||
{ |
|||
// We should clean up references in case they would prevent garbage collection.
|
|||
m_subscribers.Clear(); |
|||
} |
|||
|
|||
/// <summary>Don't assume that onUpdate will be called in any particular order compared to other subscribers.</summary>
|
|||
public void Subscribe(UpdateMethod onUpdate) |
|||
{ |
|||
if (!m_subscribers.Contains(onUpdate)) |
|||
m_subscribers.Add(onUpdate); |
|||
} |
|||
/// <summary>Safe to call even if onUpdate was not previously Subscribed.</summary>
|
|||
public void Unsubscribe(UpdateMethod onUpdate) |
|||
{ |
|||
int index = m_subscribers.IndexOf(onUpdate); |
|||
if (index >= 0) |
|||
{ |
|||
m_subscribers.Remove(onUpdate); |
|||
if (index < m_nextActiveSubIndex) |
|||
m_nextActiveSubIndex--; |
|||
} |
|||
} |
|||
|
|||
private void Update() |
|||
{ |
|||
if (m_subscribers.Count == 0) |
|||
return; |
|||
m_updateTimer += Time.deltaTime; |
|||
float effectivePeriod = m_updatePeriod / m_subscribers.Count; |
|||
while (m_updateTimer > effectivePeriod) |
|||
{ |
|||
m_updateTimer -= effectivePeriod; |
|||
OnUpdate(effectivePeriod); |
|||
} |
|||
} |
|||
|
|||
public void OnUpdate(float dt) |
|||
{ |
|||
Stopwatch stopwatch = new Stopwatch(); |
|||
m_nextActiveSubIndex = System.Math.Max(0, System.Math.Min(m_subscribers.Count - 1, m_nextActiveSubIndex)); // Just a backup.
|
|||
UpdateMethod onUpdate = m_subscribers[m_nextActiveSubIndex]; |
|||
if (onUpdate == null || onUpdate.Target == null) // In case something forgets to Unsubscribe when it dies.
|
|||
{ Remove(m_nextActiveSubIndex, $"Did not Unsubscribe from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}"); |
|||
return; |
|||
} |
|||
if (onUpdate.Method.ToString().Contains("<")) // Detect an anonymous or lambda or local method that cannot be Unsubscribed, by checking for a character that can't exist in a declared method name.
|
|||
{ Remove(m_nextActiveSubIndex, $"Removed anonymous from UpdateSlow: {onUpdate.Target} : {onUpdate.Method}"); |
|||
return; |
|||
} |
|||
|
|||
stopwatch.Restart(); |
|||
onUpdate?.Invoke(dt); |
|||
stopwatch.Stop(); |
|||
if (stopwatch.ElapsedMilliseconds > m_durationToleranceMs) |
|||
{ |
|||
if (!m_doNotRemoveIfTooLong) |
|||
Remove(m_nextActiveSubIndex, $"UpdateSlow subscriber took too long, removing: {onUpdate.Target} : {onUpdate.Method}"); |
|||
else |
|||
{ |
|||
Debug.LogWarning($"UpdateSlow subscriber took too long: {onUpdate.Target} : {onUpdate.Method}"); |
|||
Increment(); |
|||
} |
|||
} |
|||
else |
|||
Increment(); |
|||
|
|||
void Remove(int index, string msg) |
|||
{ |
|||
m_subscribers.RemoveAt(index); |
|||
m_nextActiveSubIndex--; |
|||
Debug.LogError(msg); |
|||
Increment(); |
|||
} |
|||
void Increment() |
|||
{ |
|||
m_nextActiveSubIndex++; |
|||
if (m_nextActiveSubIndex >= m_subscribers.Count) |
|||
m_nextActiveSubIndex = 0; |
|||
} |
|||
} |
|||
|
|||
public void OnReProvided(IUpdateSlow prevUpdateSlow) |
|||
{ |
|||
if (prevUpdateSlow is UpdateSlow) |
|||
m_subscribers.AddRange((prevUpdateSlow as UpdateSlow).m_subscribers); |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Unity.Services.Rooms; |
|||
using Unity.Services.Rooms.Models; |
|||
using Unity.Services.Rooms.Rooms; |
|||
|
|||
namespace LobbyRelaySample.Lobby |
|||
{ |
|||
/// <summary>
|
|||
/// Does all the interactions with the Lobby API.
|
|||
/// </summary>
|
|||
public static class LobbyAPIInterface |
|||
{ |
|||
private class InProgressRequest<T> |
|||
{ |
|||
public InProgressRequest(Task<T> task, Action<T> onComplete) |
|||
{ |
|||
DoRequest(task, onComplete); |
|||
} |
|||
|
|||
private async void DoRequest(Task<T> task, Action<T> onComplete) |
|||
{ |
|||
T result = default; |
|||
string currentTrace = System.Environment.StackTrace; |
|||
try { |
|||
result = await task; |
|||
} catch (Exception e) { |
|||
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e); |
|||
throw eFull; |
|||
} finally { |
|||
onComplete?.Invoke(result); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private const int k_maxLobbiesToShow = 64; |
|||
|
|||
public static void CreateLobbyAsync(string requesterUASId, string lobbyName, int maxPlayers, bool isPrivate, Action<Response<Room>> onComplete) |
|||
{ |
|||
CreateRoomRequest createRequest = new CreateRoomRequest(new CreateRequest( |
|||
name: lobbyName, |
|||
player: new Unity.Services.Rooms.Models.Player(requesterUASId), |
|||
maxPlayers: maxPlayers, |
|||
isPrivate: isPrivate |
|||
)); |
|||
var task = RoomsService.RoomsApiClient.CreateRoomAsync(createRequest); |
|||
new InProgressRequest<Response<Room>>(task, onComplete); |
|||
} |
|||
|
|||
public static void DeleteLobbyAsync(string lobbyId, Action<Response> onComplete) |
|||
{ |
|||
DeleteRoomRequest deleteRequest = new DeleteRoomRequest(lobbyId); |
|||
var task = RoomsService.RoomsApiClient.DeleteRoomAsync(deleteRequest); |
|||
new InProgressRequest<Response>(task, onComplete); |
|||
} |
|||
|
|||
public static void JoinLobbyAsync(string requesterUASId, string lobbyId, string lobbyCode, Action<Response<Room>> onComplete) |
|||
{ |
|||
JoinRoomRequest joinRequest = new JoinRoomRequest(new JoinRequest( |
|||
player: new Unity.Services.Rooms.Models.Player(requesterUASId), |
|||
id: lobbyId, |
|||
roomCode: lobbyCode |
|||
)); |
|||
var task = RoomsService.RoomsApiClient.JoinRoomAsync(joinRequest); |
|||
new InProgressRequest<Response<Room>>(task, onComplete); |
|||
} |
|||
|
|||
public static void LeaveLobbyAsync(string requesterUASId, string lobbyId, Action<Response> onComplete) |
|||
{ |
|||
RemovePlayerRequest leaveRequest = new RemovePlayerRequest(lobbyId, requesterUASId); |
|||
var task = RoomsService.RoomsApiClient.RemovePlayerAsync(leaveRequest); |
|||
new InProgressRequest<Response>(task, onComplete); |
|||
} |
|||
|
|||
public static void QueryAllLobbiesAsync(Action<Response<QueryResponse>> onComplete) |
|||
{ |
|||
QueryRoomsRequest queryRequest = new QueryRoomsRequest(new QueryRequest(count: k_maxLobbiesToShow)); |
|||
var task = RoomsService.RoomsApiClient.QueryRoomsAsync(queryRequest); |
|||
new InProgressRequest<Response<QueryResponse>>(task, onComplete); |
|||
} |
|||
|
|||
public static void GetLobbyAsync(string lobbyId, Action<Response<Room>> onComplete) |
|||
{ |
|||
GetRoomRequest getRequest = new GetRoomRequest(lobbyId); |
|||
var task = RoomsService.RoomsApiClient.GetRoomAsync(getRequest); |
|||
new InProgressRequest<Response<Room>>(task, onComplete); |
|||
} |
|||
|
|||
public static void UpdateLobbyAsync(string lobbyId, Dictionary<string, DataObject> data, Action<Response<Room>> onComplete) |
|||
{ |
|||
UpdateRoomRequest updateRequest = new UpdateRoomRequest(lobbyId, new UpdateRequest( |
|||
data: data |
|||
)); |
|||
var task = RoomsService.RoomsApiClient.UpdateRoomAsync(updateRequest); |
|||
new InProgressRequest<Response<Room>>(task, onComplete); |
|||
} |
|||
|
|||
public static void UpdatePlayerAsync(string lobbyId, string playerId, Dictionary<string, PlayerDataObject> data, Action<Response<Room>> onComplete) |
|||
{ |
|||
UpdatePlayerRequest updateRequest = new UpdatePlayerRequest(lobbyId, playerId, new PlayerUpdateRequest( |
|||
data: data |
|||
)); |
|||
var task = RoomsService.RoomsApiClient.UpdatePlayerAsync(updateRequest); |
|||
new InProgressRequest<Response<Room>>(task, onComplete); |
|||
} |
|||
} |
|||
} |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Unity.Services.Lobbies; |
|||
using Unity.Services.Lobbies.Models; |
|||
|
|||
namespace LobbyRelaySample.lobby |
|||
{ |
|||
/// <summary>
|
|||
/// Does all the interactions with the Lobby API.
|
|||
/// </summary>
|
|||
public static class LobbyAPIInterface |
|||
{ |
|||
private class InProgressRequest<T> |
|||
{ |
|||
public InProgressRequest(Task<T> task, Action<T> onComplete) |
|||
{ |
|||
DoRequest(task, onComplete); |
|||
} |
|||
|
|||
private async void DoRequest(Task<T> task, Action<T> onComplete) |
|||
{ |
|||
T result = default; |
|||
string currentTrace = System.Environment.StackTrace; |
|||
try { |
|||
result = await task; |
|||
} catch (Exception e) { |
|||
Exception eFull = new Exception($"Call stack before async call:\n{currentTrace}\n", e); |
|||
throw eFull; |
|||
} finally { |
|||
onComplete?.Invoke(result); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private const int k_maxLobbiesToShow = 64; |
|||
|
|||
public static void CreateLobbyAsync(string requesterUASId, string lobbyName, int maxPlayers, bool isPrivate, Action<Response<Lobby>> onComplete) |
|||
{ |
|||
CreateLobbyRequest createRequest = new CreateLobbyRequest(new CreateRequest( |
|||
name: lobbyName, |
|||
player: new Player(requesterUASId), |
|||
maxPlayers: maxPlayers, |
|||
isPrivate: isPrivate |
|||
)); |
|||
var task = LobbyService.LobbyApiClient.CreateLobbyAsync(createRequest); |
|||
new InProgressRequest<Response<Lobby>>(task, onComplete); |
|||
} |
|||
|
|||
public static void DeleteLobbyAsync(string lobbyId, Action<Response> onComplete) |
|||
{ |
|||
DeleteLobbyRequest deleteRequest = new DeleteLobbyRequest(lobbyId); |
|||
var task = LobbyService.LobbyApiClient.DeleteLobbyAsync(deleteRequest); |
|||
new InProgressRequest<Response>(task, onComplete); |
|||
} |
|||
|
|||
public static void JoinLobbyAsync_ByCode(string requesterUASId, string lobbyCode, Action<Response<Lobby>> onComplete) |
|||
{ |
|||
JoinLobbyByCodeRequest joinRequest = new JoinLobbyByCodeRequest(new JoinByCodeRequest(lobbyCode, new Player(requesterUASId))); |
|||
var task = LobbyService.LobbyApiClient.JoinLobbyByCodeAsync(joinRequest); |
|||
new InProgressRequest<Response<Lobby>>(task, onComplete); |
|||
} |
|||
|
|||
public static void JoinLobbyAsync_ById(string requesterUASId, string lobbyId, Action<Response<Lobby>> onComplete) |
|||
{ |
|||
JoinLobbyByIdRequest joinRequest = new JoinLobbyByIdRequest(lobbyId, new Player(requesterUASId)); |
|||
var task = LobbyService.LobbyApiClient.JoinLobbyByIdAsync(joinRequest); |
|||
new InProgressRequest<Response<Lobby>>(task, onComplete); |
|||
} |
|||
|
|||
public static void LeaveLobbyAsync(string requesterUASId, string lobbyId, Action<Response> onComplete) |
|||
{ |
|||
RemovePlayerRequest leaveRequest = new RemovePlayerRequest(lobbyId, requesterUASId); |
|||
var task = LobbyService.LobbyApiClient.RemovePlayerAsync(leaveRequest); |
|||
new InProgressRequest<Response>(task, onComplete); |
|||
} |
|||
|
|||
public static void QueryAllLobbiesAsync(Action<Response<QueryResponse>> onComplete) |
|||
{ |
|||
QueryLobbiesRequest queryRequest = new QueryLobbiesRequest(new QueryRequest(count: k_maxLobbiesToShow)); |
|||
var task = LobbyService.LobbyApiClient.QueryLobbiesAsync(queryRequest); |
|||
new InProgressRequest<Response<QueryResponse>>(task, onComplete); |
|||
} |
|||
|
|||
public static void GetLobbyAsync(string lobbyId, Action<Response<Lobby>> onComplete) |
|||
{ |
|||
GetLobbyRequest getRequest = new GetLobbyRequest(lobbyId); |
|||
var task = LobbyService.LobbyApiClient.GetLobbyAsync(getRequest); |
|||
new InProgressRequest<Response<Lobby>>(task, onComplete); |
|||
} |
|||
|
|||
public static void UpdateLobbyAsync(string lobbyId, Dictionary<string, DataObject> data, Action<Response<Lobby>> onComplete) |
|||
{ |
|||
UpdateLobbyRequest updateRequest = new UpdateLobbyRequest(lobbyId, new UpdateRequest( |
|||
data: data |
|||
)); |
|||
var task = LobbyService.LobbyApiClient.UpdateLobbyAsync(updateRequest); |
|||
new InProgressRequest<Response<Lobby>>(task, onComplete); |
|||
} |
|||
|
|||
public static void UpdatePlayerAsync(string lobbyId, string playerId, Dictionary<string, PlayerDataObject> data, Action<Response<Lobby>> onComplete) |
|||
{ |
|||
UpdatePlayerRequest updateRequest = new UpdatePlayerRequest(lobbyId, playerId, new PlayerUpdateRequest( |
|||
data: data |
|||
)); |
|||
var task = LobbyService.LobbyApiClient.UpdatePlayerAsync(updateRequest); |
|||
new InProgressRequest<Response<Lobby>>(task, onComplete); |
|||
} |
|||
} |
|||
} |
|
|||
using LobbyRelaySample.Lobby; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Authentication; |
|||
using Unity.Services.Rooms; |
|||
using Unity.Services.Rooms.Models; |
|||
|
|||
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 LobbyAsyncRequests |
|||
{ |
|||
// Just doing a singleton since static access is all that's really necessary but we also need to be able to subscribe to the slow update loop.
|
|||
private static LobbyAsyncRequests s_instance; |
|||
|
|||
public static LobbyAsyncRequests Instance |
|||
{ |
|||
get |
|||
{ |
|||
if (s_instance == null) |
|||
s_instance = new LobbyAsyncRequests(); |
|||
return s_instance; |
|||
} |
|||
} |
|||
|
|||
public LobbyAsyncRequests() |
|||
{ |
|||
Locator.Get.UpdateSlow.Subscribe(UpdateLobby); // Shouldn't need to unsubscribe since this instance won't be replaced.
|
|||
} |
|||
|
|||
private static bool IsSuccessful(Response response) |
|||
{ |
|||
return response != null && response.Status >= 200 && response.Status < 300; // Uses HTTP status codes, so 2xx is a success.
|
|||
} |
|||
|
|||
#region We want to cache the lobby object so we don't query for it every time we need to do a different lobby operation or view current data.
|
|||
// (This assumes that the player will be actively in just one lobby at a time, though they could passively be in more.)
|
|||
private Queue<Action> m_pendingOperations = new Queue<Action>(); |
|||
private string m_currentLobbyId = null; |
|||
private Room m_lastKnownLobby; |
|||
private bool m_isMidRetrieve = false; |
|||
public Room CurrentLobby => m_lastKnownLobby; |
|||
|
|||
public void BeginTracking(string lobbyId) |
|||
{ |
|||
m_currentLobbyId = lobbyId; |
|||
} |
|||
|
|||
public void EndTracking() |
|||
{ |
|||
m_currentLobbyId = null; |
|||
} |
|||
|
|||
private void UpdateLobby(float unused) |
|||
{ |
|||
if (!string.IsNullOrEmpty(m_currentLobbyId)) |
|||
RetrieveLobbyAsync(m_currentLobbyId, OnComplete); |
|||
|
|||
void OnComplete(Room lobby) |
|||
{ |
|||
if (lobby != null) |
|||
m_lastKnownLobby = lobby; |
|||
m_isMidRetrieve = false; |
|||
HandlePendingOperations(); |
|||
} |
|||
} |
|||
|
|||
private void HandlePendingOperations() |
|||
{ |
|||
while (m_pendingOperations.Count > 0) |
|||
m_pendingOperations.Dequeue()?.Invoke(); // Note: If this ends up enqueuing a bunch of operations, we might need to batch them and/or ensure they don't all execute at once.
|
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
/// <summary>
|
|||
/// Attempt to create a new lobby and then join it.
|
|||
/// </summary>
|
|||
public void CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, Action<Room> onSuccess, Action onFailure) |
|||
{ |
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
LobbyAPIInterface.CreateLobbyAsync(uasId, lobbyName, maxPlayers, isPrivate, OnLobbyCreated); |
|||
|
|||
void OnLobbyCreated(Response<Room> response) |
|||
{ |
|||
if (!IsSuccessful(response)) |
|||
onFailure?.Invoke(); |
|||
else |
|||
{ |
|||
var pendingLobby = response.Result; |
|||
onSuccess?.Invoke(pendingLobby); // The Create request automatically joins the lobby, so we need not take further action.
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>Attempt to join an existing lobby. Either ID xor code can be null.</summary>
|
|||
public void JoinLobbyAsync(string lobbyId, string lobbyCode, Action<Room> onSuccess, Action onFailure) |
|||
{ |
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
LobbyAPIInterface.JoinLobbyAsync(uasId, lobbyId, lobbyCode, OnLobbyJoined); |
|||
|
|||
void OnLobbyJoined(Response<Room> response) |
|||
{ |
|||
if (!IsSuccessful(response)) |
|||
onFailure?.Invoke(); |
|||
else |
|||
onSuccess?.Invoke(response?.Result); |
|||
} |
|||
} |
|||
|
|||
/// <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 void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<Response<QueryResponse>> onError = null) |
|||
{ |
|||
LobbyAPIInterface.QueryAllLobbiesAsync(OnLobbyListRetrieved); |
|||
|
|||
void OnLobbyListRetrieved(Response<QueryResponse> response) |
|||
{ |
|||
if (IsSuccessful(response)) |
|||
onListRetrieved?.Invoke(response?.Result); |
|||
else |
|||
onError?.Invoke(response); |
|||
} |
|||
} |
|||
/// <param name="onComplete">If no lobby is retrieved, this is given null.</param>
|
|||
private void RetrieveLobbyAsync(string lobbyId, Action<Room> onComplete) |
|||
{ |
|||
if (m_isMidRetrieve) |
|||
return; // Not calling onComplete since there's just the one point at which this is called.
|
|||
m_isMidRetrieve = true; |
|||
LobbyAPIInterface.GetLobbyAsync(lobbyId, OnGet); |
|||
|
|||
void OnGet(Response<Room> response) |
|||
{ |
|||
m_isMidRetrieve = false; |
|||
onComplete?.Invoke(response?.Result); |
|||
} |
|||
} |
|||
|
|||
/// <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 void LeaveLobbyAsync(string lobbyId, Action onComplete) |
|||
{ |
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
LobbyAPIInterface.LeaveLobbyAsync(uasId, lobbyId, OnLeftLobby); |
|||
|
|||
void OnLeftLobby(Response response) |
|||
{ |
|||
onComplete?.Invoke(); |
|||
|
|||
// Lobbies will automatically delete the lobby if unoccupied, so we don't need to take further action.
|
|||
|
|||
// TEMP. As of 6/31/21, the lobbies service doesn't automatically delete emptied lobbies, though that functionality is expected in the near-term.
|
|||
// Until then, we'll do a delete request whenever we leave, and if it's invalid, we'll just get a 403 back.
|
|||
LobbyAPIInterface.DeleteLobbyAsync(lobbyId, null); |
|||
} |
|||
} |
|||
|
|||
/// <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 void UpdatePlayerDataAsync(Dictionary<string, string> data, Action onComplete) |
|||
{ |
|||
if (!ShouldUpdateData(() => { UpdatePlayerDataAsync(data, onComplete); }, onComplete)) |
|||
return; |
|||
|
|||
Room lobby = m_lastKnownLobby; |
|||
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); |
|||
} |
|||
|
|||
LobbyAPIInterface.UpdatePlayerAsync(lobby.Id, Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id"), dataCurr, (r) => { onComplete?.Invoke(); }); |
|||
} |
|||
|
|||
/// <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 void UpdateLobbyDataAsync(Dictionary<string, string> data, Action onComplete) |
|||
{ |
|||
if (!ShouldUpdateData(() => { UpdateLobbyDataAsync(data, onComplete); }, onComplete)) |
|||
return; |
|||
|
|||
Room lobby = m_lastKnownLobby; |
|||
Dictionary<string, DataObject> dataCurr = lobby.Data ?? new Dictionary<string, DataObject>(); |
|||
foreach (var dataNew in data) |
|||
{ |
|||
DataObject dataObj = new DataObject(visibility: DataObject.VisibilityOptions.Public, value: dataNew.Value); // 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); |
|||
} |
|||
|
|||
LobbyAPIInterface.UpdateLobbyAsync(lobby.Id, dataCurr, (r) => { onComplete?.Invoke(); }); |
|||
} |
|||
|
|||
private bool ShouldUpdateData(Action caller, Action onComplete) |
|||
{ |
|||
if (m_isMidRetrieve) |
|||
{ m_pendingOperations.Enqueue(caller); |
|||
return false; |
|||
} |
|||
Room lobby = m_lastKnownLobby; |
|||
if (lobby == null) |
|||
{ onComplete?.Invoke(); |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
using LobbyRelaySample.lobby; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Authentication; |
|||
using Unity.Services.Lobbies; |
|||
using Unity.Services.Lobbies.Models; |
|||
|
|||
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 LobbyAsyncRequests |
|||
{ |
|||
// Just doing a singleton since static access is all that's really necessary but we also need to be able to subscribe to the slow update loop.
|
|||
private static LobbyAsyncRequests s_instance; |
|||
|
|||
public static LobbyAsyncRequests Instance |
|||
{ |
|||
get |
|||
{ |
|||
if (s_instance == null) |
|||
s_instance = new LobbyAsyncRequests(); |
|||
return s_instance; |
|||
} |
|||
} |
|||
|
|||
public LobbyAsyncRequests() |
|||
{ |
|||
Locator.Get.UpdateSlow.Subscribe(UpdateLobby); // Shouldn't need to unsubscribe since this instance won't be replaced.
|
|||
} |
|||
|
|||
private static bool IsSuccessful(Response response) |
|||
{ |
|||
return response != null && response.Status >= 200 && response.Status < 300; // Uses HTTP status codes, so 2xx is a success.
|
|||
} |
|||
|
|||
#region We want to cache the lobby object so we don't query for it every time we need to do a different lobby operation or view current data.
|
|||
// (This assumes that the player will be actively in just one lobby at a time, though they could passively be in more.)
|
|||
private Queue<Action> m_pendingOperations = new Queue<Action>(); |
|||
private string m_currentLobbyId = null; |
|||
private Lobby m_lastKnownLobby; |
|||
private bool m_isMidRetrieve = false; |
|||
public Lobby CurrentLobby => m_lastKnownLobby; |
|||
|
|||
public void BeginTracking(string lobbyId) |
|||
{ |
|||
m_currentLobbyId = lobbyId; |
|||
} |
|||
|
|||
public void EndTracking() |
|||
{ |
|||
m_currentLobbyId = null; |
|||
} |
|||
|
|||
private void UpdateLobby(float unused) |
|||
{ |
|||
if (!string.IsNullOrEmpty(m_currentLobbyId)) |
|||
RetrieveLobbyAsync(m_currentLobbyId, OnComplete); |
|||
|
|||
void OnComplete(Lobby lobby) |
|||
{ |
|||
if (lobby != null) |
|||
m_lastKnownLobby = lobby; |
|||
m_isMidRetrieve = false; |
|||
HandlePendingOperations(); |
|||
} |
|||
} |
|||
|
|||
private void HandlePendingOperations() |
|||
{ |
|||
while (m_pendingOperations.Count > 0) |
|||
m_pendingOperations.Dequeue()?.Invoke(); // Note: If this ends up enqueuing a bunch of operations, we might need to batch them and/or ensure they don't all execute at once.
|
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
/// <summary>
|
|||
/// Attempt to create a new lobby and then join it.
|
|||
/// </summary>
|
|||
public void CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, Action<Lobby> onSuccess, Action onFailure) |
|||
{ |
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
LobbyAPIInterface.CreateLobbyAsync(uasId, lobbyName, maxPlayers, isPrivate, OnLobbyCreated); |
|||
|
|||
void OnLobbyCreated(Response<Lobby> response) |
|||
{ |
|||
if (!IsSuccessful(response)) |
|||
onFailure?.Invoke(); |
|||
else |
|||
{ |
|||
var pendingLobby = response.Result; |
|||
onSuccess?.Invoke(pendingLobby); // The Create request automatically joins the lobby, so we need not take further action.
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>Attempt to join an existing lobby. Either ID xor code can be null.</summary>
|
|||
public void JoinLobbyAsync(string lobbyId, string lobbyCode, Action<Lobby> onSuccess, Action onFailure) |
|||
{ |
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
if (!string.IsNullOrEmpty(lobbyId)) |
|||
LobbyAPIInterface.JoinLobbyAsync_ById(uasId, lobbyId, OnLobbyJoined); |
|||
else |
|||
LobbyAPIInterface.JoinLobbyAsync_ByCode(uasId, lobbyCode, OnLobbyJoined); |
|||
|
|||
void OnLobbyJoined(Response<Lobby> response) |
|||
{ |
|||
if (!IsSuccessful(response)) |
|||
onFailure?.Invoke(); |
|||
else |
|||
onSuccess?.Invoke(response?.Result); |
|||
} |
|||
} |
|||
|
|||
/// <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 void RetrieveLobbyListAsync(Action<QueryResponse> onListRetrieved, Action<Response<QueryResponse>> onError = null) |
|||
{ |
|||
LobbyAPIInterface.QueryAllLobbiesAsync(OnLobbyListRetrieved); |
|||
|
|||
void OnLobbyListRetrieved(Response<QueryResponse> response) |
|||
{ |
|||
if (IsSuccessful(response)) |
|||
onListRetrieved?.Invoke(response?.Result); |
|||
else |
|||
onError?.Invoke(response); |
|||
} |
|||
} |
|||
/// <param name="onComplete">If no lobby is retrieved, this is given null.</param>
|
|||
private void RetrieveLobbyAsync(string lobbyId, Action<Lobby> onComplete) |
|||
{ |
|||
if (m_isMidRetrieve) |
|||
return; // Not calling onComplete since there's just the one point at which this is called.
|
|||
m_isMidRetrieve = true; |
|||
LobbyAPIInterface.GetLobbyAsync(lobbyId, OnGet); |
|||
|
|||
void OnGet(Response<Lobby> response) |
|||
{ |
|||
m_isMidRetrieve = false; |
|||
onComplete?.Invoke(response?.Result); |
|||
} |
|||
} |
|||
|
|||
/// <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 void LeaveLobbyAsync(string lobbyId, Action onComplete) |
|||
{ |
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
LobbyAPIInterface.LeaveLobbyAsync(uasId, lobbyId, OnLeftLobby); |
|||
|
|||
void OnLeftLobby(Response response) |
|||
{ |
|||
onComplete?.Invoke(); |
|||
|
|||
// Lobbies will automatically delete the lobby if unoccupied, so we don't need to take further action.
|
|||
|
|||
// TEMP. As of 6/31/21, the lobbies service doesn't automatically delete emptied lobbies, though that functionality is expected in the near-term.
|
|||
// Until then, we'll do a delete request whenever we leave, and if it's invalid, we'll just get a 403 back.
|
|||
LobbyAPIInterface.DeleteLobbyAsync(lobbyId, null); |
|||
} |
|||
} |
|||
|
|||
/// <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 void UpdatePlayerDataAsync(Dictionary<string, string> data, Action onComplete) |
|||
{ |
|||
if (!ShouldUpdateData(() => { UpdatePlayerDataAsync(data, onComplete); }, onComplete)) |
|||
return; |
|||
|
|||
Lobby lobby = m_lastKnownLobby; |
|||
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); |
|||
} |
|||
|
|||
LobbyAPIInterface.UpdatePlayerAsync(lobby.Id, Locator.Get.Identity.GetSubIdentity(Auth.IIdentityType.Auth).GetContent("id"), dataCurr, (r) => { onComplete?.Invoke(); }); |
|||
} |
|||
|
|||
/// <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 void UpdateLobbyDataAsync(Dictionary<string, string> data, Action onComplete) |
|||
{ |
|||
if (!ShouldUpdateData(() => { UpdateLobbyDataAsync(data, onComplete); }, onComplete)) |
|||
return; |
|||
|
|||
Lobby lobby = m_lastKnownLobby; |
|||
Dictionary<string, DataObject> dataCurr = lobby.Data ?? new Dictionary<string, DataObject>(); |
|||
foreach (var dataNew in data) |
|||
{ |
|||
DataObject dataObj = new DataObject(visibility: DataObject.VisibilityOptions.Public, value: dataNew.Value); // 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); |
|||
} |
|||
|
|||
LobbyAPIInterface.UpdateLobbyAsync(lobby.Id, dataCurr, (r) => { onComplete?.Invoke(); }); |
|||
} |
|||
|
|||
private bool ShouldUpdateData(Action caller, Action onComplete) |
|||
{ |
|||
if (m_isMidRetrieve) |
|||
{ m_pendingOperations.Enqueue(caller); |
|||
return false; |
|||
} |
|||
Lobby lobby = m_lastKnownLobby; |
|||
if (lobby == null) |
|||
{ onComplete?.Invoke(); |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using LobbyRemote = Unity.Services.Rooms.Models.Room; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Keep updated on changes to a joined lobby.
|
|||
/// </summary>
|
|||
public class LobbyContentHeartbeat |
|||
{ |
|||
private LocalLobby m_localLobby; |
|||
private LobbyUser m_localUser; |
|||
private bool m_isAwaitingQuery = false; |
|||
private bool m_shouldPushData = false; |
|||
|
|||
public void BeginTracking(LocalLobby lobby, LobbyUser localUser) |
|||
{ |
|||
m_localLobby = lobby; |
|||
m_localUser = localUser; |
|||
Locator.Get.UpdateSlow.Subscribe(OnUpdate); |
|||
m_localLobby.onChanged += OnLocalLobbyChanged; |
|||
m_shouldPushData = true; // Ensure the initial presence of a new player is pushed to the lobby; otherwise, when a non-host joins, the LocalLobby never receives their data until they push something new.
|
|||
} |
|||
|
|||
public void EndTracking() |
|||
{ |
|||
m_shouldPushData = false; |
|||
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate); |
|||
if (m_localLobby != null) |
|||
m_localLobby.onChanged -= OnLocalLobbyChanged; |
|||
m_localLobby = null; |
|||
m_localUser = null; |
|||
} |
|||
|
|||
private void OnLocalLobbyChanged(LocalLobby changed) |
|||
{ |
|||
if (string.IsNullOrEmpty(changed.LobbyID)) // When the player leaves, their LocalLobby is cleared out but maintained.
|
|||
EndTracking(); |
|||
m_shouldPushData = true; |
|||
} |
|||
|
|||
public void OnUpdate(float dt) |
|||
{ |
|||
if (m_isAwaitingQuery || m_localLobby == null) |
|||
return; |
|||
|
|||
m_isAwaitingQuery = true; // Note that because we make async calls, if one of them fails and doesn't call our callback, this will never be reset to false.
|
|||
if (m_shouldPushData) |
|||
PushDataToLobby(); |
|||
else |
|||
OnRetrieve(); |
|||
|
|||
void PushDataToLobby() |
|||
{ |
|||
if (m_localUser == null) |
|||
{ |
|||
m_isAwaitingQuery = false; |
|||
return; // Don't revert m_shouldPushData yet, so that we can retry.
|
|||
} |
|||
m_shouldPushData = false; |
|||
|
|||
if (m_localUser.IsHost) |
|||
DoLobbyDataPush(); |
|||
else |
|||
DoPlayerDataPush(); |
|||
} |
|||
|
|||
void DoLobbyDataPush() |
|||
{ |
|||
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(Lobby.ToLocalLobby.RetrieveLobbyData(m_localLobby), () => { DoPlayerDataPush(); }); |
|||
} |
|||
|
|||
void DoPlayerDataPush() |
|||
{ |
|||
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(Lobby.ToLocalLobby.RetrieveUserData(m_localUser), () => { m_isAwaitingQuery = false; }); |
|||
} |
|||
|
|||
void OnRetrieve() |
|||
{ |
|||
m_isAwaitingQuery = false; |
|||
LobbyRemote lobby = LobbyAsyncRequests.Instance.CurrentLobby; |
|||
if (lobby == null) return; |
|||
bool prevShouldPush = m_shouldPushData; |
|||
var prevState = m_localLobby.State; |
|||
Lobby.ToLocalLobby.Convert(lobby, m_localLobby, m_localUser); |
|||
m_shouldPushData = prevShouldPush; |
|||
CheckForAllPlayersReady(); |
|||
|
|||
if (prevState != LobbyState.Lobby && m_localLobby.State == LobbyState.Lobby) |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ToLobby, null); |
|||
} |
|||
|
|||
|
|||
void CheckForAllPlayersReady() |
|||
{ |
|||
bool areAllPlayersReady = m_localLobby.AllPlayersReadyTime != null; |
|||
if (areAllPlayersReady) |
|||
{ |
|||
long targetTimeTicks = m_localLobby.AllPlayersReadyTime.Value; |
|||
DateTime targetTime = new DateTime(targetTimeTicks); |
|||
if (targetTime.Subtract(DateTime.Now).Seconds < 0) |
|||
return; |
|||
|
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.Client_EndReadyCountdownAt, targetTime); // Note that this could be called multiple times.
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
using System; |
|||
using LobbyRemote = Unity.Services.Lobbies.Models.Lobby; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Keep updated on changes to a joined lobby.
|
|||
/// </summary>
|
|||
public class LobbyContentHeartbeat |
|||
{ |
|||
private LocalLobby m_localLobby; |
|||
private LobbyUser m_localUser; |
|||
private bool m_isAwaitingQuery = false; |
|||
private bool m_shouldPushData = false; |
|||
|
|||
public void BeginTracking(LocalLobby lobby, LobbyUser localUser) |
|||
{ |
|||
m_localLobby = lobby; |
|||
m_localUser = localUser; |
|||
Locator.Get.UpdateSlow.Subscribe(OnUpdate); |
|||
m_localLobby.onChanged += OnLocalLobbyChanged; |
|||
m_shouldPushData = true; // Ensure the initial presence of a new player is pushed to the lobby; otherwise, when a non-host joins, the LocalLobby never receives their data until they push something new.
|
|||
} |
|||
|
|||
public void EndTracking() |
|||
{ |
|||
m_shouldPushData = false; |
|||
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate); |
|||
if (m_localLobby != null) |
|||
m_localLobby.onChanged -= OnLocalLobbyChanged; |
|||
m_localLobby = null; |
|||
m_localUser = null; |
|||
} |
|||
|
|||
private void OnLocalLobbyChanged(LocalLobby changed) |
|||
{ |
|||
if (string.IsNullOrEmpty(changed.LobbyID)) // When the player leaves, their LocalLobby is cleared out but maintained.
|
|||
EndTracking(); |
|||
m_shouldPushData = true; |
|||
} |
|||
|
|||
public void OnUpdate(float dt) |
|||
{ |
|||
if (m_isAwaitingQuery || m_localLobby == null) |
|||
return; |
|||
|
|||
m_isAwaitingQuery = true; // Note that because we make async calls, if one of them fails and doesn't call our callback, this will never be reset to false.
|
|||
if (m_shouldPushData) |
|||
PushDataToLobby(); |
|||
else |
|||
OnRetrieve(); |
|||
|
|||
void PushDataToLobby() |
|||
{ |
|||
if (m_localUser == null) |
|||
{ |
|||
m_isAwaitingQuery = false; |
|||
return; // Don't revert m_shouldPushData yet, so that we can retry.
|
|||
} |
|||
m_shouldPushData = false; |
|||
|
|||
if (m_localUser.IsHost) |
|||
DoLobbyDataPush(); |
|||
else |
|||
DoPlayerDataPush(); |
|||
} |
|||
|
|||
void DoLobbyDataPush() |
|||
{ |
|||
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(lobby.ToLocalLobby.RetrieveLobbyData(m_localLobby), () => { DoPlayerDataPush(); }); |
|||
} |
|||
|
|||
void DoPlayerDataPush() |
|||
{ |
|||
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(lobby.ToLocalLobby.RetrieveUserData(m_localUser), () => { m_isAwaitingQuery = false; }); |
|||
} |
|||
|
|||
void OnRetrieve() |
|||
{ |
|||
m_isAwaitingQuery = false; |
|||
LobbyRemote lobbyRemote = LobbyAsyncRequests.Instance.CurrentLobby; |
|||
if (lobbyRemote == null) return; |
|||
bool prevShouldPush = m_shouldPushData; |
|||
var prevState = m_localLobby.State; |
|||
lobby.ToLocalLobby.Convert(lobbyRemote, m_localLobby, m_localUser); |
|||
m_shouldPushData = prevShouldPush; |
|||
CheckForAllPlayersReady(); |
|||
|
|||
if (prevState != LobbyState.Lobby && m_localLobby.State == LobbyState.Lobby) |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ToLobby, null); |
|||
} |
|||
|
|||
|
|||
void CheckForAllPlayersReady() |
|||
{ |
|||
bool areAllPlayersReady = m_localLobby.AllPlayersReadyTime != null; |
|||
if (areAllPlayersReady) |
|||
{ |
|||
long targetTimeTicks = m_localLobby.AllPlayersReadyTime.Value; |
|||
DateTime targetTime = new DateTime(targetTimeTicks); |
|||
if (targetTime.Subtract(DateTime.Now).Seconds < 0) |
|||
return; |
|||
|
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.Client_EndReadyCountdownAt, targetTime); // Note that this could be called multiple times.
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Keeps the lobby list updated automatically.
|
|||
/// </summary>
|
|||
public class LobbyListHeartbeat : MonoBehaviour |
|||
{ |
|||
public void SetActive(bool isActive) |
|||
{ |
|||
if (isActive) |
|||
Locator.Get.UpdateSlow.Subscribe(OnUpdate); |
|||
else |
|||
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate); |
|||
} |
|||
|
|||
private void OnUpdate(float dt) |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null); |
|||
} |
|||
} |
|||
} |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Keeps the lobby list updated automatically.
|
|||
/// </summary>
|
|||
public class LobbyListHeartbeat : MonoBehaviour |
|||
{ |
|||
public void SetActive(bool isActive) |
|||
{ |
|||
if (isActive) |
|||
Locator.Get.UpdateSlow.Subscribe(OnUpdate); |
|||
else |
|||
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate); |
|||
} |
|||
|
|||
private void OnUpdate(float dt) |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null); |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// On the host, this will watch for all players to ready, and once they have, it will prepare for a synchronized countdown.
|
|||
/// </summary>
|
|||
public class ReadyCheck : IDisposable |
|||
{ |
|||
float m_ReadyTime = 5; |
|||
|
|||
public ReadyCheck(float readyTime = 5) |
|||
{ |
|||
m_ReadyTime = readyTime; |
|||
} |
|||
|
|||
public void BeginCheckingForReady() |
|||
{ |
|||
Locator.Get.UpdateSlow.Subscribe(OnUpdate); |
|||
} |
|||
|
|||
public void EndCheckingForReady() |
|||
{ |
|||
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks the lobby to see if we have all Readied up. If so, send out a message with the target time at which to end a countdown.
|
|||
/// </summary>
|
|||
void OnUpdate(float dt) |
|||
{ |
|||
var lobby = LobbyAsyncRequests.Instance.CurrentLobby; |
|||
if (lobby == null || lobby.Players.Count == 0) |
|||
return; |
|||
|
|||
int readyCount = lobby.Players.Count((p) => |
|||
{ |
|||
if (p.Data?.ContainsKey("UserStatus") != true) // Needs to be "!= true" to handle null properly.
|
|||
return false; |
|||
UserStatus status; |
|||
if (Enum.TryParse(p.Data["UserStatus"].Value, out status)) |
|||
return status == UserStatus.Ready; |
|||
return false; |
|||
}); |
|||
|
|||
if (readyCount == lobby.Players.Count) |
|||
{ |
|||
Dictionary<string, string> data = new Dictionary<string, string>(); |
|||
DateTime targetTime = DateTime.Now.AddSeconds(m_ReadyTime); |
|||
data.Add("AllPlayersReady", targetTime.Ticks.ToString()); |
|||
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(data, null); |
|||
EndCheckingForReady(); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
EndCheckingForReady(); |
|||
} |
|||
} |
|||
} |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// On the host, this will watch for all players to ready, and once they have, it will prepare for a synchronized countdown.
|
|||
/// </summary>
|
|||
public class ReadyCheck : IDisposable |
|||
{ |
|||
float m_ReadyTime = 5; |
|||
|
|||
public ReadyCheck(float readyTime = 5) |
|||
{ |
|||
m_ReadyTime = readyTime; |
|||
} |
|||
|
|||
public void BeginCheckingForReady() |
|||
{ |
|||
Locator.Get.UpdateSlow.Subscribe(OnUpdate); |
|||
} |
|||
|
|||
public void EndCheckingForReady() |
|||
{ |
|||
Locator.Get.UpdateSlow.Unsubscribe(OnUpdate); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks the lobby to see if we have all Readied up. If so, send out a message with the target time at which to end a countdown.
|
|||
/// </summary>
|
|||
void OnUpdate(float dt) |
|||
{ |
|||
var lobby = LobbyAsyncRequests.Instance.CurrentLobby; |
|||
if (lobby == null || lobby.Players.Count == 0) |
|||
return; |
|||
|
|||
int readyCount = lobby.Players.Count((p) => |
|||
{ |
|||
if (p.Data?.ContainsKey("UserStatus") != true) // Needs to be "!= true" to handle null properly.
|
|||
return false; |
|||
UserStatus status; |
|||
if (Enum.TryParse(p.Data["UserStatus"].Value, out status)) |
|||
return status == UserStatus.Ready; |
|||
return false; |
|||
}); |
|||
|
|||
if (readyCount == lobby.Players.Count) |
|||
{ |
|||
Dictionary<string, string> data = new Dictionary<string, string>(); |
|||
DateTime targetTime = DateTime.Now.AddSeconds(m_ReadyTime); |
|||
data.Add("AllPlayersReady", targetTime.Ticks.ToString()); |
|||
LobbyAsyncRequests.Instance.UpdateLobbyDataAsync(data, null); |
|||
EndCheckingForReady(); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
EndCheckingForReady(); |
|||
} |
|||
} |
|||
} |
|
|||
using System.Collections.Generic; |
|||
using Unity.Services.Rooms.Models; |
|||
|
|||
namespace LobbyRelaySample.Lobby |
|||
{ |
|||
/// <summary>
|
|||
/// Convert the lobby resulting from a request into a LocalLobby for use in the game logic.
|
|||
/// </summary>
|
|||
public static class ToLocalLobby |
|||
{ |
|||
/// <summary>
|
|||
/// Create a new LocalLobby from the content of a retrieved lobby. Its data can be copied into an existing LocalLobby for use.
|
|||
/// </summary>
|
|||
public static void Convert(Room lobby, LocalLobby outputToHere, LobbyUser existingLocalUser = null) |
|||
{ |
|||
LobbyInfo info = new LobbyInfo |
|||
{ LobbyID = lobby.Id, |
|||
LobbyCode = lobby.RoomCode, |
|||
Private = lobby.IsPrivate, |
|||
LobbyName = lobby.Name, |
|||
MaxPlayerCount = lobby.MaxPlayers, |
|||
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : null, |
|||
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState) int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby, |
|||
AllPlayersReadyTime = lobby.Data?.ContainsKey("AllPlayersReady") == true ? long.Parse(lobby.Data["AllPlayersReady"].Value) : (long?)null |
|||
}; |
|||
Dictionary<string, LobbyUser> lobbyUsers = new Dictionary<string, LobbyUser>(); |
|||
foreach (var player in lobby.Players) |
|||
{ |
|||
if (existingLocalUser != null && player.Id.Equals(existingLocalUser.ID)) |
|||
{ |
|||
existingLocalUser.IsHost = lobby.HostId.Equals(player.Id); |
|||
existingLocalUser.DisplayName = player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : existingLocalUser.DisplayName; |
|||
existingLocalUser.Emote = player.Data?.ContainsKey("Emote") == true ? player.Data["Emote"].Value : existingLocalUser.Emote; |
|||
lobbyUsers.Add(existingLocalUser.ID, existingLocalUser); |
|||
} |
|||
else |
|||
{ |
|||
LobbyUser user = new LobbyUser( |
|||
displayName: player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : "NewPlayer", |
|||
isHost: lobby.HostId.Equals(player.Id), |
|||
id: player.Id, |
|||
emote: player.Data?.ContainsKey("Emote") == true ? player.Data["Emote"].Value : null, |
|||
userStatus: player.Data?.ContainsKey("UserStatus") == true ? player.Data["UserStatus"].Value : UserStatus.Lobby.ToString() |
|||
); |
|||
lobbyUsers.Add(user.ID, user); |
|||
} |
|||
} |
|||
|
|||
outputToHere.CopyObserved(info, lobbyUsers); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create a list of new LocalLobby from the content of a retrieved lobby.
|
|||
/// </summary>
|
|||
public static List<LocalLobby> Convert(QueryResponse response) |
|||
{ |
|||
List<LocalLobby> retLst = new List<LocalLobby>(); |
|||
foreach (var lobby in response.Results) |
|||
retLst.Add(Convert(lobby)); |
|||
return retLst; |
|||
} |
|||
private static LocalLobby Convert(Room lobby) |
|||
{ |
|||
LocalLobby data = new LocalLobby(); |
|||
Convert(lobby, data, null); |
|||
return data; |
|||
} |
|||
|
|||
public static Dictionary<string, string> RetrieveLobbyData(LocalLobby lobby) |
|||
{ |
|||
Dictionary<string, string> data = new Dictionary<string, string>(); |
|||
data.Add("RelayCode", lobby.RelayCode); |
|||
data.Add("State", ((int)lobby.State).ToString()); |
|||
// We only want the ArePlayersReadyTime to be set when we actually are ready for it, and it's null otherwise. So, don't set that here.
|
|||
return data; |
|||
} |
|||
|
|||
public static Dictionary<string, string> RetrieveUserData(LobbyUser user) |
|||
{ |
|||
Dictionary<string, string> data = new Dictionary<string, string>(); |
|||
if (user == null || string.IsNullOrEmpty(user.ID)) |
|||
return data; |
|||
data.Add("DisplayName", user.DisplayName); |
|||
data.Add("Emote", user.Emote); // Emote could be null, which is fine.
|
|||
data.Add("UserStatus", user.UserStatus.ToString()); |
|||
return data; |
|||
} |
|||
} |
|||
} |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Lobbies.Models; |
|||
|
|||
namespace LobbyRelaySample.lobby |
|||
{ |
|||
/// <summary>
|
|||
/// Convert the lobby resulting from a request into a LocalLobby for use in the game logic.
|
|||
/// </summary>
|
|||
public static class ToLocalLobby |
|||
{ |
|||
/// <summary>
|
|||
/// Create a new LocalLobby from the content of a retrieved lobby. Its data can be copied into an existing LocalLobby for use.
|
|||
/// </summary>
|
|||
public static void Convert(Lobby lobby, LocalLobby outputToHere, LobbyUser existingLocalUser = null) |
|||
{ |
|||
LobbyInfo info = new LobbyInfo |
|||
{ LobbyID = lobby.Id, |
|||
LobbyCode = lobby.LobbyCode, |
|||
Private = lobby.IsPrivate, |
|||
LobbyName = lobby.Name, |
|||
MaxPlayerCount = lobby.MaxPlayers, |
|||
RelayCode = lobby.Data?.ContainsKey("RelayCode") == true ? lobby.Data["RelayCode"].Value : null, |
|||
State = lobby.Data?.ContainsKey("State") == true ? (LobbyState) int.Parse(lobby.Data["State"].Value) : LobbyState.Lobby, |
|||
AllPlayersReadyTime = lobby.Data?.ContainsKey("AllPlayersReady") == true ? long.Parse(lobby.Data["AllPlayersReady"].Value) : (long?)null |
|||
}; |
|||
Dictionary<string, LobbyUser> lobbyUsers = new Dictionary<string, LobbyUser>(); |
|||
foreach (var player in lobby.Players) |
|||
{ |
|||
if (existingLocalUser != null && player.Id.Equals(existingLocalUser.ID)) |
|||
{ |
|||
existingLocalUser.IsHost = lobby.HostId.Equals(player.Id); |
|||
existingLocalUser.DisplayName = player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : existingLocalUser.DisplayName; |
|||
existingLocalUser.Emote = player.Data?.ContainsKey("Emote") == true ? player.Data["Emote"].Value : existingLocalUser.Emote; |
|||
lobbyUsers.Add(existingLocalUser.ID, existingLocalUser); |
|||
} |
|||
else |
|||
{ |
|||
LobbyUser user = new LobbyUser( |
|||
displayName: player.Data?.ContainsKey("DisplayName") == true ? player.Data["DisplayName"].Value : "NewPlayer", |
|||
isHost: lobby.HostId.Equals(player.Id), |
|||
id: player.Id, |
|||
emote: player.Data?.ContainsKey("Emote") == true ? player.Data["Emote"].Value : null, |
|||
userStatus: player.Data?.ContainsKey("UserStatus") == true ? player.Data["UserStatus"].Value : UserStatus.Lobby.ToString() |
|||
); |
|||
lobbyUsers.Add(user.ID, user); |
|||
} |
|||
} |
|||
|
|||
outputToHere.CopyObserved(info, lobbyUsers); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create a list of new LocalLobby from the content of a retrieved lobby.
|
|||
/// </summary>
|
|||
public static List<LocalLobby> Convert(QueryResponse response) |
|||
{ |
|||
List<LocalLobby> retLst = new List<LocalLobby>(); |
|||
foreach (var lobby in response.Results) |
|||
retLst.Add(Convert(lobby)); |
|||
return retLst; |
|||
} |
|||
private static LocalLobby Convert(Lobby lobby) |
|||
{ |
|||
LocalLobby data = new LocalLobby(); |
|||
Convert(lobby, data, null); |
|||
return data; |
|||
} |
|||
|
|||
public static Dictionary<string, string> RetrieveLobbyData(LocalLobby lobby) |
|||
{ |
|||
Dictionary<string, string> data = new Dictionary<string, string>(); |
|||
data.Add("RelayCode", lobby.RelayCode); |
|||
data.Add("State", ((int)lobby.State).ToString()); |
|||
// We only want the ArePlayersReadyTime to be set when we actually are ready for it, and it's null otherwise. So, don't set that here.
|
|||
return data; |
|||
} |
|||
|
|||
public static Dictionary<string, string> RetrieveUserData(LobbyUser user) |
|||
{ |
|||
Dictionary<string, string> data = new Dictionary<string, string>(); |
|||
if (user == null || string.IsNullOrEmpty(user.ID)) |
|||
return data; |
|||
data.Add("DisplayName", user.DisplayName); |
|||
data.Add("Emote", user.Emote); // Emote could be null, which is fine.
|
|||
data.Add("UserStatus", user.UserStatus.ToString()); |
|||
return data; |
|||
} |
|||
} |
|||
} |
|
|||
using LobbyRelaySample; |
|||
using NUnit.Framework; |
|||
using System.Collections; |
|||
using Unity.Services.Rooms; |
|||
using Unity.Services.Rooms.Models; |
|||
using UnityEngine; |
|||
using UnityEngine.TestTools; |
|||
using LobbyAPIInterface = LobbyRelaySample.Lobby.LobbyAPIInterface; |
|||
|
|||
namespace Test |
|||
{ |
|||
public class LobbyReadyCheckTests |
|||
{ |
|||
private string m_workingLobbyId; |
|||
private LobbyRelaySample.Auth.Identity m_auth; |
|||
private bool m_didSigninComplete = false; |
|||
private GameObject m_updateSlowObj; |
|||
|
|||
[OneTimeSetUp] |
|||
public void Setup() |
|||
{ |
|||
m_auth = new LobbyRelaySample.Auth.Identity(() => { m_didSigninComplete = true; }); |
|||
Locator.Get.Provide(m_auth); |
|||
m_updateSlowObj = new GameObject("UpdateSlowTest"); |
|||
m_updateSlowObj.AddComponent<UpdateSlow>(); |
|||
} |
|||
|
|||
[UnityTearDown] |
|||
public IEnumerator PerTestTeardown() |
|||
{ |
|||
if (m_workingLobbyId != null) |
|||
{ LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, null); |
|||
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.
|
|||
} |
|||
|
|||
[OneTimeTearDown] |
|||
public void Teardown() |
|||
{ |
|||
Locator.Get.Provide(new LobbyRelaySample.Auth.IdentityNoop()); |
|||
m_auth.Dispose(); |
|||
LogAssert.ignoreFailingMessages = false; |
|||
LobbyAsyncRequests.Instance.EndTracking(); |
|||
GameObject.Destroy(m_updateSlowObj); |
|||
} |
|||
|
|||
private IEnumerator WaitForSignin() |
|||
{ |
|||
// 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."); |
|||
} |
|||
|
|||
private IEnumerator CreateLobby(string lobbyName, string userId) |
|||
{ |
|||
Response<Room> createResponse = null; |
|||
float timeout = 5; |
|||
LobbyAPIInterface.CreateLobbyAsync(userId, lobbyName, 4, false, (r) => { createResponse = r; }); |
|||
while (createResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (lobby creation)."); |
|||
m_workingLobbyId = createResponse.Result.Id; |
|||
} |
|||
|
|||
private IEnumerator PushPlayerData(LobbyUser player) |
|||
{ |
|||
bool hasPushedPlayerData = false; |
|||
float timeout = 5; |
|||
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(LobbyRelaySample.Lobby.ToLocalLobby.RetrieveUserData(player), () => { hasPushedPlayerData = true; }); // LobbyContentHeartbeat normally does this.
|
|||
while (!hasPushedPlayerData && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (push player data)."); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// After creating a lobby and a player, signal that the player is Ready. This should lead to a countdown time being set for all players.
|
|||
/// </summary>
|
|||
[UnityTest] |
|||
public IEnumerator SetCountdownTimeSinglePlayer() |
|||
{ |
|||
LogAssert.ignoreFailingMessages = true; // Not sure why, but when auth logs in, it sometimes generates an error: "A Native Collection has not been disposed[...]." We don't want this to cause test failures, since in practice it *seems* to not negatively impact behavior.
|
|||
ReadyCheck readyCheck = new ReadyCheck(5); // This ready time is used for the countdown target end, not for any of the timing of actually detecting readies.
|
|||
yield return WaitForSignin(); |
|||
|
|||
string userId = m_auth.GetSubIdentity(LobbyRelaySample.Auth.IIdentityType.Auth).GetContent("id"); |
|||
yield return CreateLobby("TestReadyLobby1", userId); |
|||
|
|||
LobbyAsyncRequests.Instance.BeginTracking(m_workingLobbyId); |
|||
yield return new WaitForSeconds(2); // Allow the initial lobby retrieval.
|
|||
|
|||
LobbyUser user = new LobbyUser(); |
|||
user.ID = userId; |
|||
user.UserStatus = UserStatus.Ready; |
|||
yield return PushPlayerData(user); |
|||
|
|||
readyCheck.BeginCheckingForReady(); |
|||
float timeout = 5; // Long enough for two slow updates
|
|||
yield return new WaitForSeconds(timeout); |
|||
|
|||
readyCheck.Dispose(); |
|||
LobbyAsyncRequests.Instance.EndTracking(); |
|||
|
|||
yield return new WaitForSeconds(2); // Buffer to prevent a 429 on the upcoming Get, since there's a Get request on the slow upate loop when that's active.
|
|||
Response<Room> getResponse = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.GetLobbyAsync(m_workingLobbyId, (r) => { getResponse = r; }); |
|||
while (getResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (get lobby)."); |
|||
Assert.NotNull(getResponse.Result, "Retrieved lobby successfully."); |
|||
Assert.NotNull(getResponse.Result.Data, "Lobby should have data."); |
|||
|
|||
Assert.True(getResponse.Result.Data.ContainsKey("AllPlayersReady"), "Check for AllPlayersReady key."); |
|||
string readyString = getResponse.Result.Data["AllPlayersReady"]?.Value; |
|||
Assert.NotNull(readyString, "Check for non-null AllPlayersReady."); |
|||
Assert.True(long.TryParse(readyString, out long ticks), "Check for ticks value in AllPlayersReady."); // This will be based on the current time, so we won't check for a specific value.
|
|||
} |
|||
|
|||
// Can't test with multiple players on one machine, since anonymous UAS credentials can't be manually supplied.
|
|||
} |
|||
} |
|||
using LobbyRelaySample; |
|||
using NUnit.Framework; |
|||
using System.Collections; |
|||
using Unity.Services.Lobbies; |
|||
using Unity.Services.Lobbies.Models; |
|||
using UnityEngine; |
|||
using UnityEngine.TestTools; |
|||
using LobbyAPIInterface = LobbyRelaySample.lobby.LobbyAPIInterface; |
|||
|
|||
namespace Test |
|||
{ |
|||
public class LobbyReadyCheckTests |
|||
{ |
|||
private string m_workingLobbyId; |
|||
private LobbyRelaySample.Auth.Identity m_auth; |
|||
private bool m_didSigninComplete = false; |
|||
private GameObject m_updateSlowObj; |
|||
|
|||
[OneTimeSetUp] |
|||
public void Setup() |
|||
{ |
|||
m_auth = new LobbyRelaySample.Auth.Identity(() => { m_didSigninComplete = true; }); |
|||
Locator.Get.Provide(m_auth); |
|||
m_updateSlowObj = new GameObject("UpdateSlowTest"); |
|||
m_updateSlowObj.AddComponent<UpdateSlow>(); |
|||
} |
|||
|
|||
[UnityTearDown] |
|||
public IEnumerator PerTestTeardown() |
|||
{ |
|||
if (m_workingLobbyId != null) |
|||
{ LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, null); |
|||
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.
|
|||
} |
|||
|
|||
[OneTimeTearDown] |
|||
public void Teardown() |
|||
{ |
|||
Locator.Get.Provide(new LobbyRelaySample.Auth.IdentityNoop()); |
|||
m_auth.Dispose(); |
|||
LogAssert.ignoreFailingMessages = false; |
|||
LobbyAsyncRequests.Instance.EndTracking(); |
|||
GameObject.Destroy(m_updateSlowObj); |
|||
} |
|||
|
|||
private IEnumerator WaitForSignin() |
|||
{ |
|||
// 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."); |
|||
} |
|||
|
|||
private IEnumerator CreateLobby(string lobbyName, string userId) |
|||
{ |
|||
Response<Lobby> createResponse = null; |
|||
float timeout = 5; |
|||
LobbyAPIInterface.CreateLobbyAsync(userId, lobbyName, 4, false, (r) => { createResponse = r; }); |
|||
while (createResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (lobby creation)."); |
|||
m_workingLobbyId = createResponse.Result.Id; |
|||
} |
|||
|
|||
private IEnumerator PushPlayerData(LobbyUser player) |
|||
{ |
|||
bool hasPushedPlayerData = false; |
|||
float timeout = 5; |
|||
LobbyAsyncRequests.Instance.UpdatePlayerDataAsync(LobbyRelaySample.lobby.ToLocalLobby.RetrieveUserData(player), () => { hasPushedPlayerData = true; }); // LobbyContentHeartbeat normally does this.
|
|||
while (!hasPushedPlayerData && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (push player data)."); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// After creating a lobby and a player, signal that the player is Ready. This should lead to a countdown time being set for all players.
|
|||
/// </summary>
|
|||
[UnityTest] |
|||
public IEnumerator SetCountdownTimeSinglePlayer() |
|||
{ |
|||
LogAssert.ignoreFailingMessages = true; // Not sure why, but when auth logs in, it sometimes generates an error: "A Native Collection has not been disposed[...]." We don't want this to cause test failures, since in practice it *seems* to not negatively impact behavior.
|
|||
ReadyCheck readyCheck = new ReadyCheck(5); // This ready time is used for the countdown target end, not for any of the timing of actually detecting readies.
|
|||
yield return WaitForSignin(); |
|||
|
|||
string userId = m_auth.GetSubIdentity(LobbyRelaySample.Auth.IIdentityType.Auth).GetContent("id"); |
|||
yield return CreateLobby("TestReadyLobby1", userId); |
|||
|
|||
LobbyAsyncRequests.Instance.BeginTracking(m_workingLobbyId); |
|||
yield return new WaitForSeconds(2); // Allow the initial lobby retrieval.
|
|||
|
|||
LobbyUser user = new LobbyUser(); |
|||
user.ID = userId; |
|||
user.UserStatus = UserStatus.Ready; |
|||
yield return PushPlayerData(user); |
|||
|
|||
readyCheck.BeginCheckingForReady(); |
|||
float timeout = 5; // Long enough for two slow updates
|
|||
yield return new WaitForSeconds(timeout); |
|||
|
|||
readyCheck.Dispose(); |
|||
LobbyAsyncRequests.Instance.EndTracking(); |
|||
|
|||
yield return new WaitForSeconds(2); // Buffer to prevent a 429 on the upcoming Get, since there's a Get request on the slow upate loop when that's active.
|
|||
Response<Lobby> getResponse = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.GetLobbyAsync(m_workingLobbyId, (r) => { getResponse = r; }); |
|||
while (getResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (get lobby)."); |
|||
Assert.NotNull(getResponse.Result, "Retrieved lobby successfully."); |
|||
Assert.NotNull(getResponse.Result.Data, "Lobby should have data."); |
|||
|
|||
Assert.True(getResponse.Result.Data.ContainsKey("AllPlayersReady"), "Check for AllPlayersReady key."); |
|||
string readyString = getResponse.Result.Data["AllPlayersReady"]?.Value; |
|||
Assert.NotNull(readyString, "Check for non-null AllPlayersReady."); |
|||
Assert.True(long.TryParse(readyString, out long ticks), "Check for ticks value in AllPlayersReady."); // This will be based on the current time, so we won't check for a specific value.
|
|||
} |
|||
|
|||
// Can't test with multiple players on one machine, since anonymous UAS credentials can't be manually supplied.
|
|||
} |
|||
} |
|
|||
using NUnit.Framework; |
|||
using System.Collections; |
|||
using System.Linq; |
|||
using Unity.Services.Rooms; |
|||
using Unity.Services.Rooms.Models; |
|||
using UnityEngine; |
|||
using UnityEngine.TestTools; |
|||
using LobbyAPIInterface = LobbyRelaySample.Lobby.LobbyAPIInterface; |
|||
|
|||
namespace Test |
|||
{ |
|||
/// <summary>
|
|||
/// Hits the Authentication and Lobbies services in order to ensure lobbies can be created and deleted.
|
|||
/// The actual code accessing lobbies should go through LobbyAsyncRequests.
|
|||
/// </summary>
|
|||
public class LobbyRoundtripTests |
|||
{ |
|||
private string m_workingLobbyId; |
|||
private LobbyRelaySample.Auth.SubIdentity_Authentication m_auth; |
|||
private bool m_didSigninComplete = false; |
|||
|
|||
[OneTimeSetUp] |
|||
public void Setup() |
|||
{ |
|||
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication(() => { m_didSigninComplete = true; }); |
|||
} |
|||
|
|||
[UnityTearDown] |
|||
public IEnumerator PerTestTeardown() |
|||
{ |
|||
if (m_workingLobbyId != null) |
|||
{ LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, null); |
|||
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.
|
|||
} |
|||
|
|||
[OneTimeTearDown] |
|||
public void Teardown() |
|||
{ |
|||
m_auth?.Dispose(); |
|||
LogAssert.ignoreFailingMessages = false; |
|||
} |
|||
|
|||
[UnityTest] |
|||
public IEnumerator DoRoundtrip() |
|||
{ |
|||
LogAssert.ignoreFailingMessages = true; // Not sure why, but when auth logs in, it sometimes generates an error: "A Native Collection has not been disposed[...]." We don't want this to cause test failures, since in practice it *seems* to not negatively impact behavior.
|
|||
|
|||
// 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.
|
|||
Response<QueryResponse> queryResponse = null; |
|||
float timeout = 5; |
|||
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponse = qr; }); |
|||
while (queryResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (query #0)"); |
|||
Assert.IsTrue(queryResponse.Status >= 200 && queryResponse.Status < 300, "QueryAllLobbiesAsync should return a success code. (#0)"); |
|||
int numLobbiesIni = queryResponse.Result.Results?.Count ?? 0; |
|||
|
|||
// Create a test lobby.
|
|||
Response<Room> createResponse = null; |
|||
timeout = 5; |
|||
string lobbyName = "TestLobby-JustATest-123"; |
|||
LobbyAPIInterface.CreateLobbyAsync(m_auth.GetContent("id"), lobbyName, 100, false, (r) => { createResponse = r; }); |
|||
while (createResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (create)"); |
|||
Assert.IsTrue(createResponse.Status >= 200 && createResponse.Status < 300, "CreateLobbyAsync should return a success code."); |
|||
m_workingLobbyId = createResponse.Result.Id; |
|||
Assert.AreEqual(lobbyName, createResponse.Result.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.
|
|||
queryResponse = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponse = qr; }); |
|||
while (queryResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (query #1)"); |
|||
Assert.IsTrue(queryResponse.Status >= 200 && queryResponse.Status < 300, "QueryAllLobbiesAsync should return a success code. (#1)"); |
|||
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Result.Results.Count, "Queried lobbies list should contain the test lobby."); |
|||
Assert.IsTrue(queryResponse.Result.Results.Where(r => r.Name == lobbyName).Count() == 1, "Checking queried lobby for name."); |
|||
Assert.IsTrue(queryResponse.Result.Results.Where(r => r.Id == m_workingLobbyId).Count() == 1, "Checking queried lobby for ID."); |
|||
|
|||
// Query for solely the test lobby via GetLobby.
|
|||
Response<Room> getResponse = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.GetLobbyAsync(createResponse.Result.Id, (r) => { getResponse = r; }); |
|||
while (getResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (get)"); |
|||
Assert.IsTrue(getResponse.Status >= 200 && getResponse.Status < 300, "GetLobbyAsync should return a success code."); |
|||
Assert.AreEqual(lobbyName, getResponse.Result.Name, "Checking the lobby we got for name."); |
|||
Assert.AreEqual(m_workingLobbyId, getResponse.Result.Id, "Checking the lobby we got for ID."); |
|||
|
|||
// Delete the test lobby.
|
|||
Response deleteResponse = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, (r) => { deleteResponse = r; }); |
|||
while (deleteResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (delete)"); |
|||
Assert.IsTrue(deleteResponse.Status >= 200 && deleteResponse.Status < 300, "DeleteLobbyAsync should return a success code."); |
|||
m_workingLobbyId = null; |
|||
|
|||
// Query to ensure the lobby is gone.
|
|||
yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request.
|
|||
Response<QueryResponse> queryResponseTwo = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponseTwo = qr; }); |
|||
while (queryResponseTwo == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (query #2)"); |
|||
Assert.IsTrue(queryResponseTwo.Status >= 200 && queryResponseTwo.Status < 300, "QueryAllLobbiesAsync should return a success code. (#2)"); |
|||
Assert.AreEqual(numLobbiesIni, queryResponseTwo.Result.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); |
|||
LogAssert.ignoreFailingMessages = false; |
|||
} |
|||
|
|||
[UnityTest] |
|||
public IEnumerator OnCompletesOnFailure() |
|||
{ |
|||
LogAssert.ignoreFailingMessages = true; |
|||
if (!m_didSigninComplete) |
|||
yield return new WaitForSeconds(3); |
|||
if (!m_didSigninComplete) |
|||
Assert.Fail("Did not sign in."); |
|||
|
|||
bool? didComplete = null; |
|||
LobbyAPIInterface.CreateLobbyAsync("ThisStringIsInvalidHere", "lobby name", 123, false, (r) => { didComplete = (r == null); }); |
|||
float timeout = 5; |
|||
while (didComplete == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check"); |
|||
Assert.NotNull(didComplete, "Should have called onComplete, even if the async request failed."); |
|||
Assert.True(didComplete, "The returned object will be null, so expect to need to handle it."); |
|||
} |
|||
} |
|||
} |
|||
using NUnit.Framework; |
|||
using System.Collections; |
|||
using System.Linq; |
|||
using Unity.Services.Lobbies; |
|||
using Unity.Services.Lobbies.Models; |
|||
using UnityEngine; |
|||
using UnityEngine.TestTools; |
|||
using LobbyAPIInterface = LobbyRelaySample.lobby.LobbyAPIInterface; |
|||
|
|||
namespace Test |
|||
{ |
|||
/// <summary>
|
|||
/// Hits the Authentication and Lobbies services in order to ensure lobbies can be created and deleted.
|
|||
/// The actual code accessing lobbies should go through LobbyAsyncRequests.
|
|||
/// </summary>
|
|||
public class LobbyRoundtripTests |
|||
{ |
|||
private string m_workingLobbyId; |
|||
private LobbyRelaySample.Auth.SubIdentity_Authentication m_auth; |
|||
private bool m_didSigninComplete = false; |
|||
|
|||
[OneTimeSetUp] |
|||
public void Setup() |
|||
{ |
|||
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication(() => { m_didSigninComplete = true; }); |
|||
} |
|||
|
|||
[UnityTearDown] |
|||
public IEnumerator PerTestTeardown() |
|||
{ |
|||
if (m_workingLobbyId != null) |
|||
{ LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, null); |
|||
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.
|
|||
} |
|||
|
|||
[OneTimeTearDown] |
|||
public void Teardown() |
|||
{ |
|||
m_auth?.Dispose(); |
|||
LogAssert.ignoreFailingMessages = false; |
|||
} |
|||
|
|||
[UnityTest] |
|||
public IEnumerator DoRoundtrip() |
|||
{ |
|||
LogAssert.ignoreFailingMessages = true; // Not sure why, but when auth logs in, it sometimes generates an error: "A Native Collection has not been disposed[...]." We don't want this to cause test failures, since in practice it *seems* to not negatively impact behavior.
|
|||
|
|||
// 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.
|
|||
Response<QueryResponse> queryResponse = null; |
|||
float timeout = 5; |
|||
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponse = qr; }); |
|||
while (queryResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (query #0)"); |
|||
Assert.IsTrue(queryResponse.Status >= 200 && queryResponse.Status < 300, "QueryAllLobbiesAsync should return a success code. (#0)"); |
|||
int numLobbiesIni = queryResponse.Result.Results?.Count ?? 0; |
|||
|
|||
// Create a test lobby.
|
|||
Response<Lobby> createResponse = null; |
|||
timeout = 5; |
|||
string lobbyName = "TestLobby-JustATest-123"; |
|||
LobbyAPIInterface.CreateLobbyAsync(m_auth.GetContent("id"), lobbyName, 100, false, (r) => { createResponse = r; }); |
|||
while (createResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (create)"); |
|||
Assert.IsTrue(createResponse.Status >= 200 && createResponse.Status < 300, "CreateLobbyAsync should return a success code."); |
|||
m_workingLobbyId = createResponse.Result.Id; |
|||
Assert.AreEqual(lobbyName, createResponse.Result.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.
|
|||
queryResponse = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponse = qr; }); |
|||
while (queryResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (query #1)"); |
|||
Assert.IsTrue(queryResponse.Status >= 200 && queryResponse.Status < 300, "QueryAllLobbiesAsync should return a success code. (#1)"); |
|||
Assert.AreEqual(1 + numLobbiesIni, queryResponse.Result.Results.Count, "Queried lobbies list should contain the test lobby."); |
|||
Assert.IsTrue(queryResponse.Result.Results.Where(r => r.Name == lobbyName).Count() == 1, "Checking queried lobby for name."); |
|||
Assert.IsTrue(queryResponse.Result.Results.Where(r => r.Id == m_workingLobbyId).Count() == 1, "Checking queried lobby for ID."); |
|||
|
|||
// Query for solely the test lobby via GetLobby.
|
|||
Response<Lobby> getResponse = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.GetLobbyAsync(createResponse.Result.Id, (r) => { getResponse = r; }); |
|||
while (getResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (get)"); |
|||
Assert.IsTrue(getResponse.Status >= 200 && getResponse.Status < 300, "GetLobbyAsync should return a success code."); |
|||
Assert.AreEqual(lobbyName, getResponse.Result.Name, "Checking the lobby we got for name."); |
|||
Assert.AreEqual(m_workingLobbyId, getResponse.Result.Id, "Checking the lobby we got for ID."); |
|||
|
|||
// Delete the test lobby.
|
|||
Response deleteResponse = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.DeleteLobbyAsync(m_workingLobbyId, (r) => { deleteResponse = r; }); |
|||
while (deleteResponse == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (delete)"); |
|||
Assert.IsTrue(deleteResponse.Status >= 200 && deleteResponse.Status < 300, "DeleteLobbyAsync should return a success code."); |
|||
m_workingLobbyId = null; |
|||
|
|||
// Query to ensure the lobby is gone.
|
|||
yield return new WaitForSeconds(1); // To prevent a possible 429 with the upcoming Query request.
|
|||
Response<QueryResponse> queryResponseTwo = null; |
|||
timeout = 5; |
|||
LobbyAPIInterface.QueryAllLobbiesAsync((qr) => { queryResponseTwo = qr; }); |
|||
while (queryResponseTwo == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check (query #2)"); |
|||
Assert.IsTrue(queryResponseTwo.Status >= 200 && queryResponseTwo.Status < 300, "QueryAllLobbiesAsync should return a success code. (#2)"); |
|||
Assert.AreEqual(numLobbiesIni, queryResponseTwo.Result.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); |
|||
LogAssert.ignoreFailingMessages = false; |
|||
} |
|||
|
|||
[UnityTest] |
|||
public IEnumerator OnCompletesOnFailure() |
|||
{ |
|||
LogAssert.ignoreFailingMessages = true; |
|||
if (!m_didSigninComplete) |
|||
yield return new WaitForSeconds(3); |
|||
if (!m_didSigninComplete) |
|||
Assert.Fail("Did not sign in."); |
|||
|
|||
bool? didComplete = null; |
|||
LobbyAPIInterface.CreateLobbyAsync("ThisStringIsInvalidHere", "lobby name", 123, false, (r) => { didComplete = (r == null); }); |
|||
float timeout = 5; |
|||
while (didComplete == null && timeout > 0) |
|||
{ yield return new WaitForSeconds(0.25f); |
|||
timeout -= 0.25f; |
|||
} |
|||
Assert.Greater(timeout, 0, "Timeout check"); |
|||
Assert.NotNull(didComplete, "Should have called onComplete, even if the async request failed."); |
|||
Assert.True(didComplete, "The returned object will be null, so expect to need to handle it."); |
|||
} |
|||
} |
|||
} |
|
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// After all players ready up for the game, this will show the countdown that occurs.
|
|||
/// </summary>
|
|||
public class CountdownUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
TMP_Text m_CountDownText; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
if (observed.CountDownTime <= 0) |
|||
return; |
|||
m_CountDownText.SetText($"Starting in: {observed.CountDownTime}"); |
|||
} |
|||
} |
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// After all players ready up for the game, this will show the countdown that occurs.
|
|||
/// </summary>
|
|||
public class CountdownUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
TMP_Text m_CountDownText; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
if (observed.CountDownTime <= 0) |
|||
return; |
|||
m_CountDownText.SetText($"Starting in: {observed.CountDownTime}"); |
|||
} |
|||
} |
|||
} |
|
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Watches a lobby or relay code for updates, displaying the current code to lobby members.
|
|||
/// </summary>
|
|||
public class DisplayCodeUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
public enum CodeType { Lobby = 0, Relay = 1 } |
|||
|
|||
[SerializeField] |
|||
TMP_InputField m_outputText; |
|||
[SerializeField] |
|||
CodeType m_codeType; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
string code = m_codeType == CodeType.Lobby ? observed.LobbyCode : observed.RelayCode; |
|||
|
|||
if (!string.IsNullOrEmpty(code)) |
|||
{ |
|||
m_outputText.text = code; |
|||
Show(); |
|||
} |
|||
else |
|||
{ |
|||
Hide(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Watches a lobby or relay code for updates, displaying the current code to lobby members.
|
|||
/// </summary>
|
|||
public class DisplayCodeUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
public enum CodeType { Lobby = 0, Relay = 1 } |
|||
|
|||
[SerializeField] |
|||
TMP_InputField m_outputText; |
|||
[SerializeField] |
|||
CodeType m_codeType; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
string code = m_codeType == CodeType.Lobby ? observed.LobbyCode : observed.RelayCode; |
|||
|
|||
if (!string.IsNullOrEmpty(code)) |
|||
{ |
|||
m_outputText.text = code; |
|||
Show(); |
|||
} |
|||
else |
|||
{ |
|||
Hide(); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// After connecting to Relay, the host can use this to end the game, returning to the regular lobby state.
|
|||
/// </summary>
|
|||
public class EndGameButtonUI : MonoBehaviour |
|||
{ |
|||
public void EndGame() |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ToLobby, null); |
|||
} |
|||
} |
|||
} |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// After connecting to Relay, the host can use this to end the game, returning to the regular lobby state.
|
|||
/// </summary>
|
|||
public class EndGameButtonUI : MonoBehaviour |
|||
{ |
|||
public void EndGame() |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ToLobby, null); |
|||
} |
|||
} |
|||
} |
|
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// When the main menu's Exit button is selected, send a quit signal.
|
|||
/// </summary>
|
|||
public class ExitButtonUI : MonoBehaviour |
|||
{ |
|||
public void OnExitButton() |
|||
{ |
|||
Application.Quit(); |
|||
} |
|||
} |
|||
} |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// When the main menu's Exit button is selected, send a quit signal.
|
|||
/// </summary>
|
|||
public class ExitButtonUI : MonoBehaviour |
|||
{ |
|||
public void OnExitButton() |
|||
{ |
|||
Application.Quit(); |
|||
} |
|||
} |
|||
} |
|
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Contains the InLobbyUserUI instances while showing the UI for a lobby.
|
|||
/// </summary>
|
|||
[RequireComponent(typeof(LocalLobbyObserver))] |
|||
public class InLobbyUserList : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
List<InLobbyUserUI> m_UserUIObjects = new List<InLobbyUserUI>(); |
|||
List<string> m_CurrentUsers = new List<string>(); // Just for keeping track more easily of which users are already displayed.
|
|||
|
|||
/// <summary>
|
|||
/// When the observed data updates, we need to detect changes to the list of players.
|
|||
/// </summary>
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
for (int id = m_CurrentUsers.Count - 1; id >= 0; id--) // We might remove users if they aren't in the new data, so iterate backwards.
|
|||
{ |
|||
string userId = m_CurrentUsers[id]; |
|||
if (!observed.LobbyUsers.ContainsKey(userId)) |
|||
{ |
|||
foreach (var ui in m_UserUIObjects) |
|||
{ |
|||
if (ui.UserId == userId) |
|||
{ |
|||
ui.OnUserLeft(); |
|||
OnUserLeft(userId); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
foreach (var lobbyUserKvp in observed.LobbyUsers) // If there are new players, we need to hook them into the UI.
|
|||
{ |
|||
if (m_CurrentUsers.Contains(lobbyUserKvp.Key)) |
|||
continue; |
|||
m_CurrentUsers.Add(lobbyUserKvp.Key); |
|||
|
|||
foreach (var pcu in m_UserUIObjects) |
|||
{ |
|||
if (pcu.IsAssigned) |
|||
continue; |
|||
pcu.SetUser(lobbyUserKvp.Value); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
void OnUserLeft(string userID) |
|||
{ |
|||
if (!m_CurrentUsers.Contains(userID)) |
|||
return; |
|||
m_CurrentUsers.Remove(userID); |
|||
} |
|||
} |
|||
} |
|||
using System.Collections.Generic; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Contains the InLobbyUserUI instances while showing the UI for a lobby.
|
|||
/// </summary>
|
|||
[RequireComponent(typeof(LocalLobbyObserver))] |
|||
public class InLobbyUserList : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
List<InLobbyUserUI> m_UserUIObjects = new List<InLobbyUserUI>(); |
|||
List<string> m_CurrentUsers = new List<string>(); // Just for keeping track more easily of which users are already displayed.
|
|||
|
|||
/// <summary>
|
|||
/// When the observed data updates, we need to detect changes to the list of players.
|
|||
/// </summary>
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
for (int id = m_CurrentUsers.Count - 1; id >= 0; id--) // We might remove users if they aren't in the new data, so iterate backwards.
|
|||
{ |
|||
string userId = m_CurrentUsers[id]; |
|||
if (!observed.LobbyUsers.ContainsKey(userId)) |
|||
{ |
|||
foreach (var ui in m_UserUIObjects) |
|||
{ |
|||
if (ui.UserId == userId) |
|||
{ |
|||
ui.OnUserLeft(); |
|||
OnUserLeft(userId); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
foreach (var lobbyUserKvp in observed.LobbyUsers) // If there are new players, we need to hook them into the UI.
|
|||
{ |
|||
if (m_CurrentUsers.Contains(lobbyUserKvp.Key)) |
|||
continue; |
|||
m_CurrentUsers.Add(lobbyUserKvp.Key); |
|||
|
|||
foreach (var pcu in m_UserUIObjects) |
|||
{ |
|||
if (pcu.IsAssigned) |
|||
continue; |
|||
pcu.SetUser(lobbyUserKvp.Value); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
void OnUserLeft(string userID) |
|||
{ |
|||
if (!m_CurrentUsers.Contains(userID)) |
|||
return; |
|||
m_CurrentUsers.Remove(userID); |
|||
} |
|||
} |
|||
} |
|
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// When inside a lobby, this will show information about a player, whether local or remote.
|
|||
/// </summary>
|
|||
[RequireComponent(typeof(LobbyUserObserver))] |
|||
public class InLobbyUserUI : ObserverPanel<LobbyUser> |
|||
{ |
|||
[SerializeField] |
|||
TMP_Text m_DisplayNameText; |
|||
|
|||
[SerializeField] |
|||
TMP_Text m_StatusText; |
|||
|
|||
[SerializeField] |
|||
TMP_Text m_EmoteText; |
|||
|
|||
public bool IsAssigned |
|||
{ |
|||
get { return UserId != null; } |
|||
} |
|||
|
|||
public string UserId { get; private set; } |
|||
private LobbyUserObserver m_observer; |
|||
|
|||
public void SetUser(LobbyUser myLobbyUser) |
|||
{ |
|||
Show(); |
|||
if (m_observer == null) |
|||
m_observer = GetComponent<LobbyUserObserver>(); |
|||
m_observer.BeginObserving(myLobbyUser); |
|||
UserId = myLobbyUser.ID; |
|||
} |
|||
|
|||
public void OnUserLeft() |
|||
{ |
|||
UserId = null; |
|||
Hide(); |
|||
m_observer.EndObserving(); |
|||
} |
|||
|
|||
public override void ObservedUpdated(LobbyUser observed) |
|||
{ |
|||
m_DisplayNameText.SetText(observed.DisplayName); |
|||
m_StatusText.SetText(SetStatusFancy(observed.UserStatus)); |
|||
m_EmoteText.SetText(observed.Emote); |
|||
} |
|||
|
|||
string SetStatusFancy(UserStatus status) |
|||
{ |
|||
switch (status) |
|||
{ |
|||
case UserStatus.Lobby: |
|||
return "<color=#56B4E9>Lobby.</color>"; // Light Blue
|
|||
case UserStatus.Ready: |
|||
return "<color=#009E73>Ready!</color>"; // Light Mint
|
|||
case UserStatus.Connecting: |
|||
return "<color=#F0E442>Connecting.</color>"; // Bright Yellow
|
|||
case UserStatus.Connected: |
|||
return "<color=#005500>Connected.</color>"; //Orange
|
|||
default: |
|||
return "<color=#56B4E9>In Lobby.</color>"; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// When inside a lobby, this will show information about a player, whether local or remote.
|
|||
/// </summary>
|
|||
[RequireComponent(typeof(LobbyUserObserver))] |
|||
public class InLobbyUserUI : ObserverPanel<LobbyUser> |
|||
{ |
|||
[SerializeField] |
|||
TMP_Text m_DisplayNameText; |
|||
|
|||
[SerializeField] |
|||
TMP_Text m_StatusText; |
|||
|
|||
[SerializeField] |
|||
TMP_Text m_EmoteText; |
|||
|
|||
public bool IsAssigned |
|||
{ |
|||
get { return UserId != null; } |
|||
} |
|||
|
|||
public string UserId { get; private set; } |
|||
private LobbyUserObserver m_observer; |
|||
|
|||
public void SetUser(LobbyUser myLobbyUser) |
|||
{ |
|||
Show(); |
|||
if (m_observer == null) |
|||
m_observer = GetComponent<LobbyUserObserver>(); |
|||
m_observer.BeginObserving(myLobbyUser); |
|||
UserId = myLobbyUser.ID; |
|||
} |
|||
|
|||
public void OnUserLeft() |
|||
{ |
|||
UserId = null; |
|||
Hide(); |
|||
m_observer.EndObserving(); |
|||
} |
|||
|
|||
public override void ObservedUpdated(LobbyUser observed) |
|||
{ |
|||
m_DisplayNameText.SetText(observed.DisplayName); |
|||
m_StatusText.SetText(SetStatusFancy(observed.UserStatus)); |
|||
m_EmoteText.SetText(observed.Emote); |
|||
} |
|||
|
|||
string SetStatusFancy(UserStatus status) |
|||
{ |
|||
switch (status) |
|||
{ |
|||
case UserStatus.Lobby: |
|||
return "<color=#56B4E9>Lobby.</color>"; // Light Blue
|
|||
case UserStatus.Ready: |
|||
return "<color=#009E73>Ready!</color>"; // Light Mint
|
|||
case UserStatus.Connecting: |
|||
return "<color=#F0E442>Connecting.</color>"; // Bright Yellow
|
|||
case UserStatus.Connected: |
|||
return "<color=#005500>Connected.</color>"; //Orange
|
|||
default: |
|||
return "<color=#56B4E9>In Lobby.</color>"; |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// The panel that holds the lobby joining and creation panels.
|
|||
/// </summary>
|
|||
public class JoinCreateLobbyUI : ObserverPanel<LocalGameState> |
|||
{ |
|||
public override void ObservedUpdated(LocalGameState observed) |
|||
{ |
|||
if (observed.State == GameState.JoinMenu) |
|||
{ |
|||
Show(); |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null); |
|||
} |
|||
else |
|||
{ |
|||
Hide(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// The panel that holds the lobby joining and creation panels.
|
|||
/// </summary>
|
|||
public class JoinCreateLobbyUI : ObserverPanel<LocalGameState> |
|||
{ |
|||
public override void ObservedUpdated(LocalGameState observed) |
|||
{ |
|||
if (observed.State == GameState.JoinMenu) |
|||
{ |
|||
Show(); |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.QueryLobbies, null); |
|||
} |
|||
else |
|||
{ |
|||
Hide(); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Displays the name of the lobby.
|
|||
/// </summary>
|
|||
public class LobbyNameUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
TMP_Text m_lobbyNameText; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
m_lobbyNameText.SetText(observed.LobbyName); |
|||
} |
|||
} |
|||
} |
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Displays the name of the lobby.
|
|||
/// </summary>
|
|||
public class LobbyNameUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
TMP_Text m_lobbyNameText; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
m_lobbyNameText.SetText(observed.LobbyName); |
|||
} |
|||
} |
|||
} |
|
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Button callbacks for the "Ready"/"Not Ready" buttons used to indicate the local player is ready/not ready.
|
|||
/// </summary>
|
|||
public class ReadyCheckUI : MonoBehaviour |
|||
{ |
|||
public void OnReadyButton() |
|||
{ |
|||
ChangeState(UserStatus.Ready); |
|||
} |
|||
public void OnCancelButton() |
|||
{ |
|||
ChangeState(UserStatus.Lobby); |
|||
} |
|||
private void ChangeState(UserStatus status) |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeLobbyUserState, status); |
|||
} |
|||
} |
|||
} |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Button callbacks for the "Ready"/"Not Ready" buttons used to indicate the local player is ready/not ready.
|
|||
/// </summary>
|
|||
public class ReadyCheckUI : MonoBehaviour |
|||
{ |
|||
public void OnReadyButton() |
|||
{ |
|||
ChangeState(UserStatus.Ready); |
|||
} |
|||
public void OnCancelButton() |
|||
{ |
|||
ChangeState(UserStatus.Lobby); |
|||
} |
|||
private void ChangeState(UserStatus status) |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeLobbyUserState, status); |
|||
} |
|||
} |
|||
} |
|
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Displays the IP when connected to Relay.
|
|||
/// </summary>
|
|||
public class RelayAddressUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
TMP_Text m_IPAddressText; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
m_IPAddressText.SetText(observed.RelayServer?.ToString()); |
|||
} |
|||
} |
|||
} |
|||
using TMPro; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Displays the IP when connected to Relay.
|
|||
/// </summary>
|
|||
public class RelayAddressUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
TMP_Text m_IPAddressText; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
m_IPAddressText.SetText(observed.RelayServer?.ToString()); |
|||
} |
|||
} |
|||
} |
|
|||
using System.Text; |
|||
using TMPro; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Controls a simple throbber that is displayed when the lobby list is being refreshed.
|
|||
/// </summary>
|
|||
public class SpinnerUI : ObserverPanel<LobbyServiceData> |
|||
{ |
|||
public TMP_Text errorText; |
|||
public UIPanelBase spinnerImage; |
|||
public UIPanelBase noServerText; |
|||
public UIPanelBase errorTextVisibility; |
|||
|
|||
public override void ObservedUpdated(LobbyServiceData observed) |
|||
{ |
|||
if (observed.State == LobbyServiceState.Fetching) |
|||
{ |
|||
Show(); |
|||
spinnerImage.Show(); |
|||
noServerText.Hide(); |
|||
errorTextVisibility.Hide(); |
|||
} |
|||
else if (observed.State == LobbyServiceState.Error) |
|||
{ |
|||
spinnerImage.Hide(); |
|||
errorTextVisibility.Show(); |
|||
var errorString = new StringBuilder(); |
|||
errorString.Append("Error"); |
|||
var codeString = ": " + observed.lastErrorCode; |
|||
if (observed.lastErrorCode < 1) |
|||
codeString = "."; |
|||
|
|||
errorString.Append(codeString); |
|||
errorText.SetText(errorString.ToString()); |
|||
} |
|||
else if (observed.State == LobbyServiceState.Fetched) |
|||
{ |
|||
if (observed.CurrentLobbies.Count < 1) |
|||
{ |
|||
noServerText.Show(); |
|||
} |
|||
else |
|||
{ |
|||
noServerText.Hide(); |
|||
} |
|||
|
|||
spinnerImage.Hide(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
using System.Text; |
|||
using TMPro; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// <summary>
|
|||
/// Controls a simple throbber that is displayed when the lobby list is being refreshed.
|
|||
/// </summary>
|
|||
public class SpinnerUI : ObserverPanel<LobbyServiceData> |
|||
{ |
|||
public TMP_Text errorText; |
|||
public UIPanelBase spinnerImage; |
|||
public UIPanelBase noServerText; |
|||
public UIPanelBase errorTextVisibility; |
|||
|
|||
public override void ObservedUpdated(LobbyServiceData observed) |
|||
{ |
|||
if (observed.State == LobbyServiceState.Fetching) |
|||
{ |
|||
Show(); |
|||
spinnerImage.Show(); |
|||
noServerText.Hide(); |
|||
errorTextVisibility.Hide(); |
|||
} |
|||
else if (observed.State == LobbyServiceState.Error) |
|||
{ |
|||
spinnerImage.Hide(); |
|||
errorTextVisibility.Show(); |
|||
var errorString = new StringBuilder(); |
|||
errorString.Append("Error"); |
|||
var codeString = ": " + observed.lastErrorCode; |
|||
if (observed.lastErrorCode < 1) |
|||
codeString = "."; |
|||
|
|||
errorString.Append(codeString); |
|||
errorText.SetText(errorString.ToString()); |
|||
} |
|||
else if (observed.State == LobbyServiceState.Fetched) |
|||
{ |
|||
if (observed.CurrentLobbies.Count < 1) |
|||
{ |
|||
noServerText.Show(); |
|||
} |
|||
else |
|||
{ |
|||
noServerText.Hide(); |
|||
} |
|||
|
|||
spinnerImage.Hide(); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Main menu start button.
|
|||
/// </summary>
|
|||
public class StartLobbyButtonUI : MonoBehaviour |
|||
{ |
|||
public void ToJoinMenu() |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeGameState, GameState.JoinMenu); |
|||
} |
|||
} |
|||
} |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Main menu start button.
|
|||
/// </summary>
|
|||
public class StartLobbyButtonUI : MonoBehaviour |
|||
{ |
|||
public void ToJoinMenu() |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeGameState, GameState.JoinMenu); |
|||
} |
|||
} |
|||
} |
|
|||
# UPM Package Starter Kit |
|||
|
|||
The purpose of this starter kit is to provide the data structure and development guidelines for new packages meant for the **Unity Package Manager (UPM)**. |
|||
|
|||
## Are you ready to become a package? |
|||
The Package Manager is a work-in-progress for Unity and, in that sense, there are a few criteria that must be met for your package to be considered on the package list at this time: |
|||
- **Your code accesses public Unity C# APIs only.** If you have a native code component, it will need to ship with an official editor release. Internal API access might eventually be possible for Unity made packages, but not at this time. |
|||
- **Your code doesn't require security, obfuscation, or conditional access control.** Anyone should be able to download your package and access the source code. |
|||
|
|||
|
|||
## Package structure |
|||
|
|||
```none |
|||
<root> |
|||
├── package.json |
|||
├── README.md |
|||
├── CHANGELOG.md |
|||
├── LICENSE.md |
|||
├── Third Party Notices.md |
|||
├── QAReport.md |
|||
├── Editor |
|||
│ ├── Unity.[YourPackageName].Editor.asmdef |
|||
│ └── EditorExample.cs |
|||
├── Runtime |
|||
│ ├── Unity.[YourPackageName].asmdef |
|||
│ └── RuntimeExample.cs |
|||
├── Tests |
|||
│ ├── .tests.json |
|||
│ ├── Editor |
|||
│ │ ├── Unity.[YourPackageName].Editor.Tests.asmdef |
|||
│ │ └── EditorExampleTest.cs |
|||
│ └── Runtime |
|||
│ ├── Unity.[YourPackageName].Tests.asmdef |
|||
│ └── RuntimeExampleTest.cs |
|||
├── Samples |
|||
│ └── Example |
|||
│ ├── .sample.json |
|||
│ └── SampleExample.cs |
|||
└── Documentation~ |
|||
├── your-package-name.md |
|||
└── Images |
|||
``` |
|||
|
|||
## Develop your package |
|||
Package development works best within the Unity Editor. Here's how to set that up: |
|||
|
|||
1. Start **Unity**, create a local empty project. |
|||
|
|||
1. In a console (or terminal) application, go to the newly created project folder, then copy the contents of this starter kit into the packages directory. |
|||
__Note:__ Your directory name must be the name of your package (Example: `"com.unity.terrain-builder"`) |
|||
|
|||
1. ##### Fill in your package information |
|||
|
|||
Update the following required fields in file **package.json**: |
|||
- `"name"`: Package name, it should follow this naming convention: `"com.[YourCompanyName].[sub-group].[your-package-name]"` |
|||
(Example: `"com.unity.2d.animation"`, where `sub-group` should match the sub-group you selected in Gitlab) |
|||
- `"displayName"`: Package user friendly display name. (Example: `"Terrain Builder SDK"`). <br>__Note:__ Use a display name that will help users understand what your package is intended for. |
|||
- `"version"`: Package version `"X.Y.Z"`, your project **must** adhere to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). |
|||
Follow this guideline: |
|||
- To introduce a new feature or bug fix, increment the minor version (X.**Y**.Z) |
|||
- To introduce a breaking API change, increment the major version (**X**.Y.Z) |
|||
- The patch version (X.Y.**Z**), is reserved for sustainable engineering use only. |
|||
- `"unity"`: Unity Version your package is compatible with. (Example: `"2018.1"`) |
|||
- `"description"`: This description appears in the Package Manager window when the user selects this package from the list. For best results, use this text to summarize what the package does and how it can benefit the user.<br>__Note:__ Special formatting characters are supported, including line breaks (`\n`) and unicode characters such as bullets (`\u25AA`).<br> |
|||
|
|||
Update the following recommended fields in file **package.json**: |
|||
- `"dependencies"`: List of packages this package depends on. All dependencies will also be downloaded and loaded in a project with your package. Here's an example: |
|||
``` |
|||
dependencies: { |
|||
"com.unity.ads": "1.0.0" |
|||
"com.unity.analytics": "2.0.0" |
|||
} |
|||
``` |
|||
- `"keywords"`: List of words that will be indexed by the package manager search engine to facilitate discovery. |
|||
|
|||
Update the following field in file **Tests/.tests.json**: |
|||
- `"createSeparatePackage"`: If this is set to true, the CI will create a separate package for these tests. If you leave it set to false, the tests will remain part of the published package. If you set it to true, the tests in your package will automatically be moved to a separate package, and metadata will be added at publish time to link the packages together. This allows you to have a large number of tests, or assets, etc. that you don't want to include in your main package, while making it easy to test your package with those tests & fixtures. |
|||
|
|||
1. You should now see your package in the Project Window, along with all other available packages for your project. |
|||
|
|||
1. ##### Rename and update assembly definition files. |
|||
|
|||
Assembly definition files are used to generate C# assemblies during compilation. Package code must include asmdef files to ensure package code isolation. You can read up on assembly definition files [here](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html). |
|||
|
|||
If your package contains Editor code, rename and modify [Editor/Unity.YourPackageName.Editor.asmdef](Editor/Unity.YourPackageName.Editor.asmdef). Otherwise, delete the Editor directory. |
|||
* Name **must** match your package name, suffixed by `.Editor` (i.e `Unity.[YourPackageName].Editor`) |
|||
* Assembly **must** reference `Unity.[YourPackageName]` (if you have any Runtime) |
|||
* Platforms **must** include `"Editor"` |
|||
|
|||
If your package contains code that needs to be included in Unity runtime builds, rename and modify [Runtime/Unity.YourPackageName.asmdef](Runtime/Unity.YourPackageName.asmdef). Otherwise, delete the Runtime directory. |
|||
* Name **must** match your package name (i.e `Unity.[YourPackageName]`) |
|||
|
|||
If your package has Editor code, you **must** include Editor Tests in your package. In that case, rename and modify [Tests/Editor/Unity.YourPackageName.Editor.Tests.asmdef](Tests/Editor/Unity.YourPackageName.Editor.Tests.asmdef). |
|||
* Name **must** match your package name, suffixed by `.Editor.Tests` (i.e `Unity.[YourPackageName].Editor.Tests`) |
|||
* Assembly **must** reference `Unity.[YourPackageName].Editor` and `Unity.[YourPackageName]` (if you have any Runtime) |
|||
* Platforms **must** include `"Editor"` |
|||
* Optional Unity references **must** include `"TestAssemblies"` to allow your Editor Tests to show up in the Test Runner/run on Katana when your package is listed in project manifest `testables` |
|||
|
|||
If your package has Runtime code, you **must** include Playmode Tests in your package. In that case, rename and modify [Tests/Runtime/Unity.YourPackageName.Tests.asmdef](Tests/Runtime/Unity.YourPackageName.Tests.asmdef). |
|||
* Name **must** match your package name, suffixed by `.Tests` (i.e `Unity.[YourPackageName].Tests`) |
|||
* Assembly **must** reference `Unity.[YourPackageName]` |
|||
* Optional Unity references **must** include `"TestAssemblies"` to allow your Playmode Tests to show up in the Test Runner/run on Katana when your package is listed in project manifest `testables` |
|||
|
|||
> |
|||
> The reason for choosing such name schema is to ensure that the name of the assembly built based on *assembly definition file* (_a.k.a .asmdef_) will follow the .Net [Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/index) |
|||
|
|||
1. ##### Document your package. |
|||
|
|||
Rename and update **your-package-name.md** documentation file. Use this documentation template file to create preliminary, high-level documentation. This document is meant to introduce users to the features and sample files included in your package. Your package documentation files will be used to generate online and local docs, available from the package manager UI. |
|||
|
|||
**Document your public APIs** |
|||
* All public APIs need to be documented with XmlDoc. If you don't need an API to be accessed by clients, mark it as internal instead. |
|||
* API documentation is generated from [XmlDoc tags](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/xml-documentation-comments) included with all public APIs found in the package. See [Editor/EditorExample.cs](Editor/EditorExample.cs) for an example. |
|||
|
|||
**Documentation flow** |
|||
* Documentation needs to be ready when a publish request is sent to Release Management, as they will ask the documentation team to review it. |
|||
* The package will remain in `preview` mode until the final documentation is completed. Users will have access to the developer-generated documentation only in preview packages. |
|||
* When the documentation is completed, the documentation team will update the package git repo with the updates and they will publish it on the web. |
|||
* The package's development team will then need to submit a new package version with updated docs. |
|||
* The starting page in the user manual that links to package documentation is [Here](https://docs.unity3d.com/Manual/PackagesList.html). |
|||
* The `Documentation~` folder is suffixed with `~` so that its content does not get loaded in the editor, which is the recommended behavior. If this is problematic, you can still name it `Documentation` and all tools will still work correctly. `.Documentation` is also supported. |
|||
|
|||
**Test your documentation locally** |
|||
As you are developing your documentation, you can see what your documentation will look like by using the DocTools extension (optional). |
|||
Once the DocTools package is installed, it will add a `Generate Documentation` button in the Package Manager UI's details of your installed packages. To install the extension, follow these steps: |
|||
|
|||
1. Make sure you have `Package Manager UI v1.9.6` or above. |
|||
1. Your project manifest will need to point to a staging registry for this, which you can do by adding this line to it: `"registry": "https://staging-packages.unity.com"` |
|||
1. Install `Package Manager DocTools v1.0.0-preview.6` or above from the `Package Manager UI` (in the `All Packages` section). |
|||
1. After installation, you will see a `Generate Documentation` button which will generate the documentation locally, and open a web browser to a locally served version of your documentation so you can preview it. |
|||
1. (optional) If your package documentation contains multiple `.md` files for the user manual, see [this page](https://docs.unity3d.com/Packages/com.unity.package-manager-doctools@1.0/manual/index.html#table-of-content) to add a table of content to your documentation. |
|||
|
|||
The DocTools extension is still in preview, if you come across arguable results, please discuss them on #docs-packman. |
|||
|
|||
1. ##### Add samples to your package (code & assets). |
|||
If your package contains a sample, rename the `Samples/Example` folder, and update the `.sample.json` file in it. |
|||
|
|||
In the case where your package contains multiple samples, you can make a copy of the `Samples/Example` folder for each sample, and update the `.sample.json` file accordingly. |
|||
|
|||
Similar to `.tests.json` file, there is a `"createSeparatePackage"` field in `.sample.json`.If set to true, the CI will create an separate package for the sample.. |
|||
|
|||
Delete the `Samples` folder altogether if your package does not need samples. |
|||
|
|||
As of Unity release 2019.1, the /Samples directory of a package will be recognized by the package manager. Samples will not be imported to Unity when the package is added to a project, but will instead be offered to users of the package as an optional import, which can be added to their "/Assets" directory through a UI option. |
|||
|
|||
1. ##### Validate your package. |
|||
|
|||
Before you publish your package, you need to make sure that it passes all the necessary validation checks by using the Package Validation Suite extension (optional). |
|||
Once the Validation Suite package is installed, it will add a `Validate` button in the Package Manager UI's details of your installed packages. To install the extension, follow these steps: |
|||
|
|||
1. Make sure you have `Package Manager UI v1.9.6` or above. |
|||
1. Your project manifest will need to point to a staging registry for this, which you can do by adding this line to it: `"registry": "https://staging-packages.unity.com"` |
|||
1. Install `Package Validation Suite v0.3.0-preview.13` or above from the `Package Manager UI` in the `All Packages` section. If you can't find it there, try turning on `Show preview packages` in the `Advanced` menu. |
|||
1. After installation, you will see a `Validate` button show up in the Package Manager UI, which, when pressed, will run a series of tests and expose a `See Results` button for additional explanation. |
|||
1. If it succeeds, you will see a green bar with a `Success` message. |
|||
1. If it fails, you will see a red bar with a `Failed` message. |
|||
|
|||
The validation suite is still in preview, if you come across arguable results, please discuss them on #release-management. |
|||
|
|||
1. ##### Add tests to your package. |
|||
|
|||
All packages must contain tests. Tests are essential for Unity to ensure that the package works as expected in different scenarios. |
|||
|
|||
**Editor tests** |
|||
* Write all your Editor Tests in `Tests/Editor` |
|||
|
|||
**Playmode Tests** |
|||
* Write all your Playmode Tests in `Tests/Runtime`. |
|||
|
|||
1. ##### Update **CHANGELOG.md**. |
|||
|
|||
Every new feature or bug fix should have a trace in this file. For more details on the chosen changelog format, see [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). |
|||
|
|||
## Create a Pre-Release Package |
|||
Pre-Release Packages are a great way of getting your features in front of Unity Developers in order to get early feedback on functionality and UI designs. Pre-Release packages need to go through the publishing to production flow, as would any other package, but with diminished requirements. Here are the supported Pre-Release tags (to be used in package.json,`version` field), along with the requirements for each one: |
|||
|
|||
**Preview** - ex: `"version" : "1.2.0-preview"` |
|||
* Expected Package structure respected |
|||
* Package loads in Unity Editor without errors |
|||
* License file present - With third party notices file if necessary |
|||
* Test coverage is good - Optional but preferred |
|||
* Public APIs documented, minimal feature docs exists- Optional but preferred |
|||
|
|||
## Make sure your package meets all legal requirements |
|||
|
|||
##### Update **Third Party Notices.md** & **License.md** |
|||
|
|||
1. If your package has third-party elements and its licenses are approved, then all the licenses must be added to the `Third Party Notices.md` file. Simply duplicate the `Component Name/License Type/Provide License Details` section if you have more then one licenes. |
|||
|
|||
a. Concerning `[Provide License Details]` in the `Third Party Notices.md`, a URL can work as long as it actually points to the reproduced license and the copyright information _(if applicable)_. |
|||
|
|||
1. If your package does not have third party elements, you can remove the `Third Party Notices.md` file from your package. |
|||
|
|||
## Preparing your package for Staging |
|||
|
|||
Before publishing your package to production, you must send your package on the Package Manager's **staging** repository. The staging repository is monitored by QA and release management, and is where package validation will take place before it is accepted in production. |
|||
## *** IMPORTANT: The staging repository is publicly accessible, do not publish any packages with sensitive material you aren't ready to share with the public *** |
|||
|
|||
|
|||
1. Publishing your changes to the package manager's **staging** repository happens from Gitlab. To do so, simply setup your project's Continuous integration, which will be triggered by "Tags" on your branches. |
|||
* Join the **#devs-packman** channel on Slack, and request a staging **USERNAME** and **API_KEY**. |
|||
* In Gitlab, under the **Settings-> CI/CD -> Secret Variables** section, setup the following 2 project variables: |
|||
* API_KEY = [your API KEY] |
|||
* USER_NAME = [your USER NAME@unity] |
|||
* You're almost done! To publish a version of your package, make sure all your changes are checked into Gitlab, then create a new tag to reflect the version you are publishing (ex. "v1.2.2"), **the tag will trigger a publish to Staging**. You can view progress you the publish request by switch over to the "CI / CD" part of your project. |
|||
|
|||
1. Do it yourself CI |
|||
|
|||
If you are using your own CI, it is still recommended that you use the `build.sh` wrapper script that comes with the starter kit, as it handle the installation of the actual CI build scripts for you. |
|||
|
|||
Instead of calling `npm pack` and `npm publish` in the package root folder in your CI, use |
|||
``` |
|||
./build.sh package-ci pack --git-head $CI_COMMIT_SHA --git-url $CI_REPOSITORY_URL |
|||
``` |
|||
and |
|||
``` |
|||
./build.sh package-ci publish --git-head $CI_COMMIT_SHA --git-url $CI_REPOSITORY_URL |
|||
``` |
|||
respectively. |
|||
|
|||
1. Test your package locally |
|||
|
|||
Now that your package is published on the package manager's **staging** repository, you can test your package in the editor by creating a new project, and editing the project's `manifest.json` file to point to your staging package, as such: |
|||
``` |
|||
dependencies: { |
|||
"com.[YourCompanyName].[sub-group].[your-package-name]": "0.1.0" |
|||
}, |
|||
"registry": "https://staging-packages.unity.com" |
|||
``` |
|||
|
|||
## Get your package published to Production |
|||
|
|||
Packages are promoted to the **production** repository from **staging**, described above. |
|||
|
|||
Once you feel comfortable that your package is ready for prime time, and passes validation (Validation Suite), reach out to Unity so your package can be passed along to Release Management, for evaluation. |
|||
|
|||
**Release management will validate your package content, and check that the editor/playmode tests are passed before promoting the package to production. You will receive a confirmation email once the package is in production.** |
|||
|
|||
**You're almost done!** |
|||
At this point, your package is available on the cloud, but not discoverable through the editor: |
|||
|
|||
1. Contact the Package Manager team to ask them to add your package to the list of discoverable package for the Unity Editor. All you need to provide is the package name (com.[YourCompanyName].[sub-group].[your-package-name]) |
|
|||
# Changelog |
|||
All notable changes to this package will be documented in this file. |
|||
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) |
|||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). |
|||
|
|||
## [Unreleased] |
|||
|
|||
## [0.4.0-preview] - 2021-06-16 |
|||
### Changed |
|||
- Remove `SetOAuthClient()` as the authentication flow is simplified. |
|||
- Updated the initialization code to initialize with `UnityServices.Initialize()` |
|||
|
|||
## [0.4.0-preview] - 2021-06-07 |
|||
### Added |
|||
- Added Project Settings UI to configure ID providers. |
|||
- Added `SignInWithSteam`, `LinkWithSteam` functions. |
|||
- Changed the public interface of the Authentication service from a static instance and static methods to a singleton instance hidden behind an interface. |
|||
|
|||
### Changed |
|||
- Change the public signature of `Authentication` to return a Task, as opposed to a IAsyncOperation |
|||
- Change the public API names of `Authentication` to `Async` |
|||
|
|||
## [0.3.1-preview] - 2021-04-23 |
|||
### Changed |
|||
- Change the `SignInFailed` event to take `AuthenticationException` instead of a string as parameter. It can provide more information for debugging purposes. |
|||
- Fixed the `com.unity.services.core` package dependency version. |
|||
|
|||
## [0.3.0-preview] - 2021-04-21 |
|||
### Added |
|||
- Added `SignInWithApple`, `LinkWithApple`, `SignInWithGoogle`, `LinkWithGoogle`, `SignInWithFacebook`, `LinkWithFacebook` functions. |
|||
- Added `SignInWithSessionToken` |
|||
- Added error codes used by the social scenarios to `AuthenticationError`. |
|||
|
|||
## [0.2.3-preview] - 2021-03-23 |
|||
### Changed |
|||
- Rename the package from `com.unity.services.identity` to `com.unity.services.authentication`. Renamed the internal types/methods, too. |
|||
|
|||
## [0.2.2-preview] - 2021-03-15 |
|||
### Added |
|||
- Core package integration |
|||
|
|||
## [0.2.1-preview] - 2021-03-05 |
|||
|
|||
- Fixed dependency on Utilities package |
|||
|
|||
## [0.2.0-preview] - 2021-03-05 |
|||
|
|||
- Removed requirement for OAuth client ID to be specified (automatically uses project default OAuth client) |
|||
|
|||
## [0.1.0-preview] - 2021-01-18 |
|||
|
|||
### This is the first release of *com.unity.services.identity*. |
|
|||
fileFormatVersion: 2 |
|||
guid: 06e5cc5b0f38f824b835fc371283773e |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
# About Authentication SDK |
|||
|
|||
This is the Authentication SDK, a way to manage player accounts through the Unity User Authentication Service (UAS). |
|||
|
|||
This package is currently in pre-release state and is not guaranteed to be fully functional. |
|
|||
fileFormatVersion: 2 |
|||
guid: 9ab04733f4c8a4d7181b07dfa234aa46 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
[assembly: InternalsVisibleTo("Unity.Services.Authentication.EditorTests")] |
|||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // For Moq
|
|
|||
fileFormatVersion: 2 |
|||
guid: 80f9842e95c141e59bcc9d49c93cc2e1 |
|||
timeCreated: 1620833560 |
|
|||
using System; |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using Newtonsoft.Json; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Authentication.Models; |
|||
using Unity.Services.Authentication.Utilities; |
|||
using Unity.Services.Core; |
|||
using UnityEditor; |
|||
using UnityEngine; |
|||
using ILogger = Unity.Services.Authentication.Utilities.ILogger; |
|||
using Logger = Unity.Services.Authentication.Utilities.Logger; |
|||
|
|||
[assembly: InternalsVisibleTo("Unity.Services.Authentication.Editor.Tests")] |
|||
[assembly: InternalsVisibleTo("Unity.Services.Authentication.EditorTests")] |
|||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // For Moq
|
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
static class AuthenticationAdminClientManager |
|||
{ |
|||
internal static IAuthenticationAdminClient Instance { get; set; } = AuthenticationAdminClient(); |
|||
|
|||
static IAuthenticationAdminClient AuthenticationAdminClient() |
|||
{ |
|||
var logger = new Logger("[Authentication]"); |
|||
IDateTimeWrapper dateTime = new DateTimeWrapper(); |
|||
INetworkingUtilities networkUtilities = new NetworkingUtilities(null, logger); |
|||
string orgId = GetOrganizationId(); |
|||
var networkClient = new AuthenticationAdminNetworkClient("https://services.unity.com", |
|||
orgId, |
|||
CloudProjectSettings.projectId, |
|||
networkUtilities, |
|||
logger); |
|||
|
|||
return new AuthenticationAdminClient(logger, networkClient); |
|||
} |
|||
|
|||
// GetOrganizationId will gets the organization id associated with this Unity project.
|
|||
static string GetOrganizationId() |
|||
{ |
|||
// This is a temporary workaround to get the Genesis organization foreign key for non-DevX enhanced Unity versions.
|
|||
// When the eventual changes are backported into previous versions of Unity, this will no longer be necessary.
|
|||
Assembly assembly = Assembly.GetAssembly(typeof(EditorWindow)); |
|||
var unityConnectInstance = assembly.CreateInstance("UnityEditor.Connect.UnityConnect", false, BindingFlags.NonPublic | BindingFlags.Instance, null, null, null, null); Type t = unityConnectInstance.GetType(); |
|||
var projectInfo = t.GetProperty("projectInfo").GetValue(unityConnectInstance, null); |
|||
|
|||
Type projectInfoType = projectInfo.GetType(); |
|||
return projectInfoType.GetProperty("organizationForeignKey").GetValue(projectInfo, null) as string; |
|||
} |
|||
} |
|||
|
|||
class AuthenticationAdminClient : IAuthenticationAdminClient |
|||
{ |
|||
string m_IdDomain; |
|||
IAuthenticationAdminNetworkClient m_AuthenticationAdminNetworkClient; |
|||
ILogger m_Logger; |
|||
|
|||
string m_orgForeignKey; |
|||
string m_servicesGatewayToken; |
|||
string m_genesisToken; |
|||
|
|||
internal enum ServiceCalled |
|||
{ |
|||
TokenExchange, |
|||
AuthenticationAdmin |
|||
} |
|||
|
|||
public AuthenticationAdminClient(ILogger logger, IAuthenticationAdminNetworkClient networkClient, string genesisToken = "") |
|||
{ |
|||
m_Logger = logger; |
|||
m_AuthenticationAdminNetworkClient = networkClient; |
|||
m_genesisToken = genesisToken; |
|||
} |
|||
|
|||
public IAsyncOperation<string> GetIDDomain() |
|||
{ |
|||
var asyncOp = new AsyncOperation<string>(); |
|||
Action<string> getIdDomainFunc = token => |
|||
{ |
|||
var getDefaultIdDomainRequest = m_AuthenticationAdminNetworkClient.GetDefaultIdDomain(token); |
|||
getDefaultIdDomainRequest.Completed += request => HandleGetIdDomainAPICall(asyncOp, request); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => getIdDomainFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> CreateIdProvider(string iddomain, CreateIdProviderRequest body) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> createIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.CreateIdProvider(body, iddomain, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => createIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<ListIdProviderResponse> ListIdProviders(string iddomain) |
|||
{ |
|||
var asyncOp = new AsyncOperation<ListIdProviderResponse>(); |
|||
Action<string> listIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.ListIdProvider(iddomain, token); |
|||
request.Completed += req => HandleListIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => listIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> UpdateIdProvider(string iddomain, string type, UpdateIdProviderRequest body) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> enableIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.UpdateIdProvider(body, iddomain, type, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => enableIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> EnableIdProvider(string iddomain, string type) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> enableIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.EnableIdProvider(iddomain, type, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => enableIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> DisableIdProvider(string iddomain, string type) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> disableIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.DisableIdProvider(iddomain, type, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => disableIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> DeleteIdProvider(string iddomain, string type) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> deleteIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.DeleteIdProvider(iddomain, type, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => deleteIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IdProviderResponse CloneIdProvider(IdProviderResponse x) |
|||
{ |
|||
return x.Clone(); |
|||
} |
|||
|
|||
internal IAsyncOperation<string> ExchangeToken(string token) |
|||
{ |
|||
var asyncOp = new AsyncOperation<string>(); |
|||
var request = m_AuthenticationAdminNetworkClient.TokenExchange(token); |
|||
request.Completed += req => HandleTokenExchange(asyncOp, req); |
|||
return asyncOp; |
|||
} |
|||
|
|||
void HandleGetIdDomainAPICall(AsyncOperation<string> asyncOp, IWebRequest<GetIdDomainResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.AuthenticationAdmin)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
m_IdDomain = request?.ResponseBody?.Id; |
|||
asyncOp.Succeed(request?.ResponseBody?.Id); |
|||
} |
|||
|
|||
void HandleTokenExchange(AsyncOperation<string> asyncOp, IWebRequest<TokenExchangeResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.TokenExchange)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var token = request?.ResponseBody?.Token; |
|||
m_servicesGatewayToken = token; |
|||
asyncOp.Succeed(token); |
|||
} |
|||
|
|||
void HandleIdProviderResponseApiCall(AsyncOperation<IdProviderResponse> asyncOp, IWebRequest<IdProviderResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.AuthenticationAdmin)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
asyncOp.Succeed(request?.ResponseBody); |
|||
} |
|||
|
|||
void HandleListIdProviderResponseApiCall(AsyncOperation<ListIdProviderResponse> asyncOp, IWebRequest<ListIdProviderResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.AuthenticationAdmin)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
asyncOp.Succeed(request?.ResponseBody); |
|||
} |
|||
|
|||
void HandleEmptyResponseApiCall(AsyncOperation<IdProviderResponse> asyncOp, IWebRequest<IdProviderResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.AuthenticationAdmin)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
asyncOp.Succeed(request?.ResponseBody); |
|||
} |
|||
|
|||
internal bool HandleError<Q, T>(AsyncOperation<Q> asyncOp, IWebRequest<T> request, ServiceCalled sc) |
|||
{ |
|||
if (!request.RequestFailed) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (request.NetworkError) |
|||
{ |
|||
asyncOp.Fail(new AuthenticationException(AuthenticationError.NetworkError)); |
|||
return true; |
|||
} |
|||
m_Logger?.Error("Error message: " + request.ErrorMessage); |
|||
|
|||
try |
|||
{ |
|||
switch (sc) |
|||
{ |
|||
case ServiceCalled.TokenExchange: |
|||
var tokenExchangeErrorResponse = JsonConvert.DeserializeObject<TokenExchangeErrorResponse>(request.ErrorMessage); |
|||
asyncOp.Fail(new AuthenticationException(tokenExchangeErrorResponse.Name, tokenExchangeErrorResponse.Message)); |
|||
break; |
|||
case ServiceCalled.AuthenticationAdmin: |
|||
var authenticationAdminErrorResponse = JsonConvert.DeserializeObject<AuthenticationErrorResponse>(request.ErrorMessage); |
|||
asyncOp.Fail(new AuthenticationException(authenticationAdminErrorResponse.Title, authenticationAdminErrorResponse.Detail)); |
|||
break; |
|||
default: |
|||
asyncOp.Fail(new AuthenticationException(AuthenticationError.UnknownError, "Unknown error")); |
|||
break; |
|||
} |
|||
} |
|||
catch (JsonException ex) |
|||
{ |
|||
asyncOp.Fail(new AuthenticationException(AuthenticationError.UnknownError, "Failed to deserialize server response: " + request.ErrorMessage)); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
asyncOp.Fail(new AuthenticationException(AuthenticationError.UnknownError, "Unknown error deserializing server response: " + request.ErrorMessage)); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
void getGenesisToken() |
|||
{ |
|||
if (m_genesisToken == "") |
|||
{ |
|||
m_genesisToken = CloudProjectSettings.accessToken; |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: ca4a5cace103447794de02de329b8eeb |
|||
timeCreated: 1620234241 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Authentication.Utilities; |
|||
using UnityEngine; |
|||
using ILogger = Unity.Services.Authentication.Utilities.ILogger; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
interface IAuthenticationAdminNetworkClient |
|||
{ |
|||
IWebRequest<TokenExchangeResponse> TokenExchange(string token); |
|||
IWebRequest<GetIdDomainResponse> GetDefaultIdDomain(string token); |
|||
IWebRequest<IdProviderResponse> CreateIdProvider(CreateIdProviderRequest body, string idDomain, string token); |
|||
IWebRequest<ListIdProviderResponse> ListIdProvider(string idDomain, string token); |
|||
IWebRequest<IdProviderResponse> UpdateIdProvider(UpdateIdProviderRequest body, string idDomain, string type, string token); |
|||
IWebRequest<IdProviderResponse> EnableIdProvider(string idDomain, string type, string token); |
|||
IWebRequest<IdProviderResponse> DisableIdProvider(string idDomain, string type, string token); |
|||
IWebRequest<IdProviderResponse> DeleteIdProvider(string idDomain, string type, string token); |
|||
} |
|||
|
|||
class AuthenticationAdminNetworkClient : IAuthenticationAdminNetworkClient |
|||
{ |
|||
const string k_ServicesGatewayStem = "/api/player-identity/v1/organizations/"; |
|||
const string k_GetDefaultIdDomainStem = "/iddomains/default"; |
|||
const string k_TokenExchangeStem = "/api/auth/v1/genesis-token-exchange/unity"; |
|||
|
|||
readonly string m_ServicesGatewayHost; |
|||
|
|||
readonly string m_GetDefaultIdDomainUrl; |
|||
readonly string m_TokenExchangeUrl; |
|||
|
|||
readonly string m_OrganizationId; |
|||
readonly string m_ProjectId; |
|||
|
|||
readonly INetworkingUtilities m_NetworkClient; |
|||
|
|||
readonly Dictionary<string, string> m_CommonPlayerIdentityHeaders; |
|||
|
|||
internal AuthenticationAdminNetworkClient(string servicesGatewayHost, |
|||
string organizationId, |
|||
string projectId, |
|||
INetworkingUtilities networkClient, |
|||
ILogger logger) |
|||
{ |
|||
m_ServicesGatewayHost = servicesGatewayHost; |
|||
m_OrganizationId = organizationId; |
|||
m_ProjectId = projectId; |
|||
|
|||
m_GetDefaultIdDomainUrl = servicesGatewayHost + k_ServicesGatewayStem + organizationId + k_GetDefaultIdDomainStem; |
|||
m_TokenExchangeUrl = servicesGatewayHost + k_TokenExchangeStem; |
|||
m_NetworkClient = networkClient; |
|||
|
|||
m_CommonPlayerIdentityHeaders = new Dictionary<string, string> |
|||
{ |
|||
["ProjectId"] = projectId, |
|||
// The Error-Version header enables RFC7807HttpError error responses
|
|||
["Error-Version"] = "v1" |
|||
}; |
|||
} |
|||
|
|||
public IWebRequest<GetIdDomainResponse> GetDefaultIdDomain(string token) |
|||
{ |
|||
return m_NetworkClient.Get<GetIdDomainResponse>(m_GetDefaultIdDomainUrl, addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
public IWebRequest<TokenExchangeResponse> TokenExchange(string token) |
|||
{ |
|||
var body = new TokenExchangeRequest(); |
|||
body.Token = token; |
|||
return m_NetworkClient.PostJson<TokenExchangeResponse>(m_TokenExchangeUrl, body); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> CreateIdProvider(CreateIdProviderRequest body, string idDomain, string token) |
|||
{ |
|||
return m_NetworkClient.PostJson<IdProviderResponse>(CreateIdProviderUrl(idDomain), body, addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
public IWebRequest<ListIdProviderResponse> ListIdProvider(string idDomain, string token) |
|||
{ |
|||
return m_NetworkClient.Get<ListIdProviderResponse>(ListIdProviderUrl(idDomain), addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> UpdateIdProvider(UpdateIdProviderRequest body, string idDomain, string type, string token) |
|||
{ |
|||
return m_NetworkClient.Put<IdProviderResponse>(UpdateIdProviderUrl(idDomain, type), body, addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> EnableIdProvider(string idDomain, string type, string token) |
|||
{ |
|||
return m_NetworkClient.Post<IdProviderResponse>(EnableIdProviderUrl(idDomain, type), addJsonHeader(addTokenHeader(m_CommonPlayerIdentityHeaders, token))); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> DisableIdProvider(string idDomain, string type, string token) |
|||
{ |
|||
return m_NetworkClient.Post<IdProviderResponse>(DisableIdProviderUrl(idDomain, type), addJsonHeader(addTokenHeader(m_CommonPlayerIdentityHeaders, token))); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> DeleteIdProvider(string idDomain, string type, string token) |
|||
{ |
|||
return m_NetworkClient.Delete<IdProviderResponse>(DeleteIdProviderUrl(idDomain, type), addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
Dictionary<string, string> addTokenHeader(Dictionary<string, string> d, string token) |
|||
{ |
|||
var headers = new Dictionary<string, string>(d); |
|||
headers.Add("Authorization", "Bearer " + token); |
|||
return headers; |
|||
} |
|||
|
|||
Dictionary<string, string> addJsonHeader(Dictionary<string, string> d) |
|||
{ |
|||
var headers = new Dictionary<string, string>(d); |
|||
headers.Add("Content-Type", "application/json"); |
|||
return headers; |
|||
} |
|||
|
|||
string CreateIdProviderUrl(string idDomain) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps"; |
|||
} |
|||
|
|||
string ListIdProviderUrl(string idDomain) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps"; |
|||
} |
|||
|
|||
string UpdateIdProviderUrl(string idDomain, string type) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps/" + type; |
|||
} |
|||
|
|||
string DeleteIdProviderUrl(string idDomain, string type) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps/" + type; |
|||
} |
|||
|
|||
string EnableIdProviderUrl(string idDomain, string type) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps/" + type + "/enable"; |
|||
} |
|||
|
|||
string DisableIdProviderUrl(string idDomain, string type) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps/" + type + "/disable"; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 45791e692f25c4a61adf3f50dc1397ac |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Unity.Services.Core.Editor; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
/// <summary>
|
|||
/// Implementation of the <see cref="IEditorGameServiceIdentifier"/> for the Authentication package
|
|||
/// </summary>
|
|||
/// <remarks>This identifier MUST be public struct.</remarks>
|
|||
public struct AuthenticationIdentifier : IEditorGameServiceIdentifier |
|||
{ |
|||
/// <summary>
|
|||
/// Key for the Authentication package
|
|||
/// </summary>
|
|||
public string GetKey() => "Authentication"; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 6ac6e0d63f116404caeed7d030ba58cb |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Unity.Services.Core.Editor; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
class AuthenticationService : IEditorGameService |
|||
{ |
|||
/// <summary>
|
|||
/// Name of the service
|
|||
/// Used for error handling and service fetching
|
|||
/// </summary>
|
|||
public string Name => "Authentication Service"; |
|||
|
|||
/// <summary>
|
|||
/// Identifier for the service
|
|||
/// Used when registering and fetching the service
|
|||
/// </summary>
|
|||
public IEditorGameServiceIdentifier Identifier { get; } = new AuthenticationIdentifier(); |
|||
|
|||
/// <summary>
|
|||
/// Flag used to determine whether COPPA Compliance should be adhered to
|
|||
/// for this service
|
|||
/// </summary>
|
|||
public bool RequiresCoppaCompliance => false; |
|||
|
|||
/// <summary>
|
|||
/// Flag used to determine whether this service has a dashboard
|
|||
/// </summary>
|
|||
public bool HasDashboard => false; |
|||
|
|||
/// <summary>
|
|||
/// Getter for the formatted dashboard url
|
|||
/// If <see cref="HasDashboard"/> is false, this field only need return null or empty string
|
|||
/// </summary>
|
|||
/// <returns>The formatted URL</returns>
|
|||
public string GetFormattedDashboardUrl() |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The enabler which allows the service to toggle on/off
|
|||
/// Can be set to null, in which case there would be no toggle
|
|||
/// </summary>
|
|||
public IEditorGameServiceEnabler Enabler { get; } = null; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 8907e4a6f69d84349b9fdf60013bdd52 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Core; |
|||
using UnityEditor; |
|||
using UnityEditor.UIElements; |
|||
using UnityEngine; |
|||
using UnityEngine.UIElements; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
class AuthenticationSettingsElement : VisualElement |
|||
{ |
|||
const string k_Uxml = "Packages/com.unity.services.authentication/Editor/UXML/AuthenticationProjectSettings.uxml"; |
|||
const string k_Uss = "Packages/com.unity.services.authentication/Editor/USS/AuthenticationStyleSheet.uss"; |
|||
|
|||
IAuthenticationAdminClient m_AdminClient; |
|||
|
|||
string m_ProjectId; |
|||
string m_IdDomainId; |
|||
|
|||
// Whether skip the confirmation window for tests/automation.
|
|||
bool m_SkipConfirmation; |
|||
|
|||
TextElement m_WaitingTextElement; |
|||
TextElement m_ErrorTextElement; |
|||
VisualElement m_AddIdProviderContainer; |
|||
List<string> m_AddIdProviderTypeChoices; |
|||
PopupField<string> m_AddIdProviderType; |
|||
Button m_RefreshButton; |
|||
Button m_AddButton; |
|||
VisualElement m_IdProviderListContainer; |
|||
|
|||
/// <summary>
|
|||
/// The text to show when the settings is waitng for an async operation to finish.
|
|||
/// </summary>
|
|||
public TextElement WaitingTextElement => m_WaitingTextElement; |
|||
|
|||
/// <summary>
|
|||
/// The text to show when there is an error.
|
|||
/// </summary>
|
|||
public TextElement ErrorTextElement => m_ErrorTextElement; |
|||
|
|||
/// <summary>
|
|||
/// The add ID provider choices in the dropdown list.
|
|||
/// </summary>
|
|||
public IEnumerable<string> AddIdProviderTypeChoices => m_AddIdProviderTypeChoices; |
|||
|
|||
/// <summary>
|
|||
/// The add ID provider dropdown list.
|
|||
/// </summary>
|
|||
public PopupField<string> AddIdProviderType => m_AddIdProviderType; |
|||
|
|||
/// <summary>
|
|||
/// The button to refresh the ID provider list.
|
|||
/// </summary>
|
|||
public Button RefreshButton => m_RefreshButton; |
|||
|
|||
/// <summary>
|
|||
/// The button to add a new ID provider.
|
|||
/// </summary>
|
|||
public Button AddButton => m_AddButton; |
|||
|
|||
/// <summary>
|
|||
/// The container to add ID providers.
|
|||
/// </summary>
|
|||
public VisualElement IdProviderListContainer => m_IdProviderListContainer; |
|||
|
|||
/// <summary>
|
|||
/// Event triggered when the <cref="AuthenticationSettingsElement"/> starts or finishes waiting for an async operation.
|
|||
/// The first parameter of the callback is the sender.
|
|||
/// The second parameter is true if it starts waiting, and false if it finishes waiting.
|
|||
/// </summary>
|
|||
public event Action<AuthenticationSettingsElement, bool> Waiting; |
|||
|
|||
public AuthenticationSettingsElement(IAuthenticationAdminClient adminClient, string projectId, bool skipConfirmation = false) |
|||
{ |
|||
m_AdminClient = adminClient; |
|||
m_ProjectId = projectId; |
|||
m_SkipConfirmation = skipConfirmation; |
|||
|
|||
var containerAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_Uxml); |
|||
if (containerAsset != null) |
|||
{ |
|||
var containerUI = containerAsset.CloneTree().contentContainer; |
|||
|
|||
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(k_Uss); |
|||
if (styleSheet != null) |
|||
{ |
|||
containerUI.styleSheets.Add(styleSheet); |
|||
} |
|||
else |
|||
{ |
|||
throw new Exception("Asset not found: " + k_Uss); |
|||
} |
|||
|
|||
m_WaitingTextElement = containerUI.Q<TextElement>(className: "auth-progress"); |
|||
m_ErrorTextElement = containerUI.Q<TextElement>(className: "auth-error"); |
|||
|
|||
m_RefreshButton = containerUI.Q<Button>("id-provider-refresh"); |
|||
m_RefreshButton.clicked += RefreshIdProviders; |
|||
|
|||
m_AddButton = containerUI.Q<Button>("id-provider-add"); |
|||
m_AddButton.SetEnabled(false); |
|||
m_AddButton.clicked += AddIdProvider; |
|||
|
|||
m_IdProviderListContainer = containerUI.Q<VisualElement>(className: "auth-id-provider-list"); |
|||
|
|||
m_AddIdProviderContainer = containerUI.Q<VisualElement>("id-provider-type"); |
|||
|
|||
Add(containerUI); |
|||
} |
|||
else |
|||
{ |
|||
throw new Exception("Asset not found: " + k_Uxml); |
|||
} |
|||
} |
|||
|
|||
public void RefreshIdProviders() |
|||
{ |
|||
ShowWaiting(); |
|||
if (m_IdDomainId == null) |
|||
{ |
|||
GetIdDomain(); |
|||
} |
|||
else |
|||
{ |
|||
ListIdProviders(); |
|||
} |
|||
} |
|||
|
|||
void GetIdDomain() |
|||
{ |
|||
var asyncOp = m_AdminClient.GetIDDomain(); |
|||
asyncOp.Completed += OnGetIdDomainCompleted; |
|||
} |
|||
|
|||
void OnGetIdDomainCompleted(IAsyncOperation<string> asyncOp) |
|||
{ |
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
OnError(asyncOp.Exception); |
|||
return; |
|||
} |
|||
|
|||
m_IdDomainId = asyncOp.Result; |
|||
ListIdProviders(); |
|||
} |
|||
|
|||
void ListIdProviders() |
|||
{ |
|||
var asyncOp = m_AdminClient.ListIdProviders(m_IdDomainId); |
|||
asyncOp.Completed += OnListIdProvidersCompleted; |
|||
} |
|||
|
|||
void OnListIdProvidersCompleted(IAsyncOperation<ListIdProviderResponse> asyncOp) |
|||
{ |
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
OnError(asyncOp.Exception); |
|||
return; |
|||
} |
|||
m_IdProviderListContainer.Clear(); |
|||
|
|||
if (asyncOp.Result?.Results != null) |
|||
{ |
|||
foreach (var provider in asyncOp.Result.Results) |
|||
{ |
|||
CreateIdProviderElement(provider); |
|||
} |
|||
} |
|||
|
|||
UpdateAddIdproviderList(); |
|||
HideWaiting(); |
|||
} |
|||
|
|||
void UpdateAddIdproviderList() |
|||
{ |
|||
var unusedIdProviders = new List<string>(IdProviderType.All); |
|||
|
|||
foreach (var child in m_IdProviderListContainer.Children()) |
|||
{ |
|||
if (!(child is IdProviderElement)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var idProviderElement = (IdProviderElement)child; |
|||
unusedIdProviders.Remove(idProviderElement.SavedValue.Type); |
|||
} |
|||
unusedIdProviders.Sort(); |
|||
|
|||
m_AddIdProviderContainer.Clear(); |
|||
m_AddIdProviderTypeChoices = unusedIdProviders; |
|||
if (unusedIdProviders.Count == 0) |
|||
{ |
|||
m_AddButton.SetEnabled(false); |
|||
} |
|||
else |
|||
{ |
|||
if (unusedIdProviders.Count > 0) |
|||
{ |
|||
m_AddIdProviderType = new PopupField<string>(null, unusedIdProviders, 0); |
|||
m_AddIdProviderContainer.Add(m_AddIdProviderType); |
|||
} |
|||
m_AddButton.SetEnabled(true); |
|||
} |
|||
} |
|||
|
|||
void AddIdProvider() |
|||
{ |
|||
var idProvider = new IdProviderResponse |
|||
{ |
|||
New = true, |
|||
Type = m_AddIdProviderType.value |
|||
}; |
|||
|
|||
CreateIdProviderElement(idProvider); |
|||
} |
|||
|
|||
void OnError(Exception error) |
|||
{ |
|||
error = AuthenticationSettingsHelper.ExtractException(error); |
|||
|
|||
m_ErrorTextElement.style.display = DisplayStyle.Flex; |
|||
m_ErrorTextElement.text = AuthenticationSettingsHelper.ExceptionToString(error); |
|||
Debug.LogError(error); |
|||
HideWaiting(); |
|||
} |
|||
|
|||
void CreateIdProviderElement(IdProviderResponse idProvider) |
|||
{ |
|||
var options = IdProviderOptions.GetOptions(idProvider.Type); |
|||
if (options == null) |
|||
{ |
|||
// the SDK doesn't support the ID provider type yet. Skip.
|
|||
return; |
|||
} |
|||
|
|||
var idProviderElement = new IdProviderElement(m_IdDomainId, m_AdminClient, idProvider, options, m_SkipConfirmation); |
|||
m_IdProviderListContainer.Add(idProviderElement); |
|||
idProviderElement.Waiting += OnIdProviderWaiting; |
|||
idProviderElement.Deleted += OnIdProviderDeleted; |
|||
idProviderElement.Error += OnIdProviderError; |
|||
|
|||
m_IdProviderListContainer.Add(idProviderElement); |
|||
UpdateAddIdproviderList(); |
|||
} |
|||
|
|||
void OnIdProviderWaiting(IdProviderElement sender, bool waiting) |
|||
{ |
|||
if (waiting) |
|||
{ |
|||
ShowWaiting(); |
|||
} |
|||
else |
|||
{ |
|||
HideWaiting(); |
|||
} |
|||
} |
|||
|
|||
void OnIdProviderDeleted(IdProviderElement sender) |
|||
{ |
|||
m_IdProviderListContainer.Remove(sender); |
|||
UpdateAddIdproviderList(); |
|||
} |
|||
|
|||
void OnIdProviderError(IdProviderElement sender, Exception error) |
|||
{ |
|||
OnError(error); |
|||
} |
|||
|
|||
void ShowWaiting() |
|||
{ |
|||
// clear previous error when a new async action is triggered.
|
|||
m_ErrorTextElement.style.display = DisplayStyle.None; |
|||
m_ErrorTextElement.text = string.Empty; |
|||
|
|||
m_WaitingTextElement.style.display = DisplayStyle.Flex; |
|||
SetEnabled(false); |
|||
|
|||
Waiting?.Invoke(this, true); |
|||
} |
|||
|
|||
void HideWaiting() |
|||
{ |
|||
m_WaitingTextElement.style.display = DisplayStyle.None; |
|||
SetEnabled(true); |
|||
Waiting?.Invoke(this, false); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 1f130c428a4b4feb9d46ecee3206cc34 |
|||
timeCreated: 1620752364 |
|
|||
using System; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
static class AuthenticationSettingsHelper |
|||
{ |
|||
internal static Exception ExtractException(Exception exception) |
|||
{ |
|||
var aggregatedException = exception as AggregateException; |
|||
if (aggregatedException == null) |
|||
{ |
|||
return exception; |
|||
} |
|||
|
|||
if (aggregatedException.InnerExceptions.Count > 1) |
|||
{ |
|||
// There are multiple exceptions aggregated, don't try to extract exception.
|
|||
return exception; |
|||
} |
|||
|
|||
// It returns the first exception.
|
|||
return aggregatedException.InnerException; |
|||
} |
|||
|
|||
internal static string ExceptionToString(Exception exception) |
|||
{ |
|||
var errorMessage = "[ERROR] "; |
|||
var currentError = exception; |
|||
var firstError = true; |
|||
while (currentError != null) |
|||
{ |
|||
if (!firstError) |
|||
{ |
|||
errorMessage += "\n---> "; |
|||
} |
|||
else |
|||
{ |
|||
firstError = false; |
|||
} |
|||
errorMessage += currentError.Message; |
|||
currentError = currentError.InnerException; |
|||
} |
|||
|
|||
return errorMessage; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 7caccdcd0f744a8d911a745661de6c5c |
|||
timeCreated: 1621631992 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Core.Editor; |
|||
using UnityEditor; |
|||
using UnityEngine; |
|||
using UnityEngine.UIElements; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
class AuthenticationSettingsProvider : EditorGameServiceSettingsProvider |
|||
{ |
|||
const string k_Title = "Authentication"; |
|||
|
|||
AuthenticationSettingsProvider(SettingsScope scopes, IEnumerable<string> keywords = null) |
|||
: base(GenerateProjectSettingsPath(k_Title), scopes, keywords) {} |
|||
|
|||
/// <summary>
|
|||
/// Accessor for the operate service
|
|||
/// Used to toggle and get dashboard access
|
|||
/// </summary>
|
|||
protected override IEditorGameService EditorGameService => EditorGameServiceRegistry.Instance.GetEditorGameService<AuthenticationIdentifier>(); |
|||
|
|||
/// <summary>
|
|||
/// Title shown in the header for the project settings
|
|||
/// </summary>
|
|||
protected override string Title => k_Title; |
|||
|
|||
/// <summary>
|
|||
/// Description show in the header for the project settings
|
|||
/// </summary>
|
|||
protected override string Description => "This package provides a system for working with the Unity User Authentication Service (UAS), including log-in, player ID and access token retrieval, and session persistence."; |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override VisualElement GenerateServiceDetailUI() |
|||
{ |
|||
var settingsElement = new AuthenticationSettingsElement(AuthenticationAdminClientManager.Instance, CloudProjectSettings.projectId); |
|||
settingsElement.RefreshIdProviders(); |
|||
|
|||
return settingsElement; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override VisualElement GenerateUnsupportedDetailUI() |
|||
{ |
|||
return GenerateServiceDetailUI(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Method which adds your settings provider to ProjectSettings
|
|||
/// </summary>
|
|||
/// <returns>A <see cref="AuthenticationSettingsProvider"/>.</returns>
|
|||
[SettingsProvider] |
|||
public static SettingsProvider CreateSettingsProvider() |
|||
{ |
|||
return new AuthenticationSettingsProvider(SettingsScope.Project); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 78b5efd28d743406b8682bcb71c787a3 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
#if ENABLE_EDITOR_GAME_SERVICES
|
|||
using System; |
|||
using UnityEditor; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
static class AuthenticationTopMenu |
|||
{ |
|||
const int k_ConfigureMenuPriority = 100; |
|||
const int k_ToolsMenuPriority = k_ConfigureMenuPriority + 11; |
|||
const string k_ServiceMenuRoot = "Services/Authentication/"; |
|||
|
|||
[MenuItem(k_ServiceMenuRoot + "Configure", priority = k_ConfigureMenuPriority)] |
|||
static void ShowProjectSettings() |
|||
{ |
|||
SettingsService.OpenProjectSettings("Project/Services/Authentication"); |
|||
} |
|||
} |
|||
} |
|||
#endif
|
|
|||
fileFormatVersion: 2 |
|||
guid: f35b6d8fe19bd49b386370e507468195 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Core; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
static class IdProviderType |
|||
{ |
|||
public const string Apple = "apple.com"; |
|||
public const string Facebook = "facebook.com"; |
|||
public const string Steam = "steampowered.com"; |
|||
public const string Google = "google.com"; |
|||
|
|||
public static readonly string[] All = |
|||
{ |
|||
Apple, |
|||
Facebook, |
|||
Google, |
|||
Steam |
|||
}; |
|||
} |
|||
|
|||
interface IAuthenticationAdminClient |
|||
{ |
|||
/// <summary>
|
|||
/// Get the ID domain associated with the project.
|
|||
/// </summary>
|
|||
/// <param name="projectId">The Unity project ID.</param>
|
|||
/// <returns>Async operation with the id domain ID as the result.</returns>
|
|||
IAsyncOperation<string> GetIDDomain(); |
|||
|
|||
/// <summary>
|
|||
/// Lists all ID providers created for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <returns>The list of ID Providers configured in the ID domain.</returns>
|
|||
IAsyncOperation<ListIdProviderResponse> ListIdProviders(string iddomain); |
|||
|
|||
/// <summary>
|
|||
/// Create a new ID provider for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="request">The ID provider to create.</param>
|
|||
/// <returns>The ID Provider created.</returns>
|
|||
IAsyncOperation<IdProviderResponse> CreateIdProvider(string iddomain, CreateIdProviderRequest request); |
|||
|
|||
/// <summary>
|
|||
/// Update an ID provider for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="request">The ID provider to create.</param>
|
|||
/// <returns>The ID Provider updated.</returns>
|
|||
IAsyncOperation<IdProviderResponse> UpdateIdProvider(string iddomain, string type, UpdateIdProviderRequest request); |
|||
|
|||
/// <summary>
|
|||
/// Enable an ID provider for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="type">The type of the ID provider.</param>
|
|||
/// <returns>The ID Provider updated.</returns>
|
|||
IAsyncOperation<IdProviderResponse> EnableIdProvider(string iddomain, string type); |
|||
|
|||
/// <summary>
|
|||
/// Disable an ID provider for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="type">The type of the ID provider.</param>
|
|||
/// <returns>The ID Provider updated.</returns>
|
|||
IAsyncOperation<IdProviderResponse> DisableIdProvider(string iddomain, string type); |
|||
|
|||
/// <summary>
|
|||
/// Delete a specific ID provider from the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="type">The type of the ID provider.</param>
|
|||
/// <returns>The async operation to check whether the task is done.</returns>
|
|||
IAsyncOperation<IdProviderResponse> DeleteIdProvider(string iddomain, string type); |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a6e05710dedc4ca8904dd440ac8d523f |
|||
timeCreated: 1620232781 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Core; |
|||
using UnityEditor; |
|||
using UnityEngine; |
|||
using UnityEngine.UIElements; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
class IdProviderOptions |
|||
{ |
|||
public const string IdProviderApple = "apple.com"; |
|||
public const string IdProviderGoogle = "google.com"; |
|||
public const string IdProviderFacebook = "facebook.com"; |
|||
public const string IdProviderSteam = "steampowered.com"; |
|||
|
|||
public string IdProviderType { get; set; } |
|||
public string DisplayName { get; set; } |
|||
public string ClientIdDisplayName { get; set; } = "Client ID"; |
|||
|
|||
public string ClientSecretDisplayName { get; set; } = "Client Secret"; |
|||
public bool NeedClientSecret {get; set; } |
|||
|
|||
static readonly Dictionary<string, IdProviderOptions> s_IdProviderOptions = new Dictionary<string, IdProviderOptions> |
|||
{ |
|||
[IdProviderApple] = new IdProviderOptions |
|||
{ |
|||
IdProviderType = IdProviderApple, |
|||
DisplayName = "Sign-in with Apple", |
|||
ClientIdDisplayName = "App ID", |
|||
NeedClientSecret = false |
|||
}, |
|||
[IdProviderGoogle] = new IdProviderOptions |
|||
{ |
|||
IdProviderType = IdProviderGoogle, |
|||
DisplayName = "Google", |
|||
ClientIdDisplayName = "Client ID", |
|||
NeedClientSecret = false |
|||
}, |
|||
[IdProviderFacebook] = new IdProviderOptions |
|||
{ |
|||
IdProviderType = IdProviderFacebook, |
|||
DisplayName = "Facebook", |
|||
ClientIdDisplayName = "App ID", |
|||
ClientSecretDisplayName = "App Secret", |
|||
NeedClientSecret = true |
|||
}, |
|||
[IdProviderSteam] = new IdProviderOptions |
|||
{ |
|||
IdProviderType = IdProviderSteam, |
|||
DisplayName = "Steam", |
|||
ClientIdDisplayName = "App ID", |
|||
ClientSecretDisplayName = "Key", |
|||
NeedClientSecret = true |
|||
} |
|||
}; |
|||
|
|||
public static IdProviderOptions GetOptions(string idProviderType) |
|||
{ |
|||
if (!s_IdProviderOptions.ContainsKey(idProviderType)) |
|||
{ |
|||
return null; |
|||
} |
|||
return s_IdProviderOptions[idProviderType]; |
|||
} |
|||
} |
|||
|
|||
class IdProviderElement : VisualElement |
|||
{ |
|||
const string k_ElementUxml = "Packages/com.unity.services.authentication/Editor/UXML/IdProviderElement.uxml"; |
|||
|
|||
string m_IdDomainId; |
|||
IAuthenticationAdminClient m_AdminClient; |
|||
IdProviderOptions m_Options; |
|||
|
|||
Foldout m_Container; |
|||
Toggle m_Enabled; |
|||
TextField m_ClientId; |
|||
TextField m_ClientSecret; |
|||
Button m_SaveButton; |
|||
Button m_CancelButton; |
|||
Button m_DeleteButton; |
|||
|
|||
// Whether skip the confirmation window for tests/automation.
|
|||
bool m_SkipConfirmation; |
|||
|
|||
/// <summary>
|
|||
/// The foldout container to show or hide the ID provider details.
|
|||
/// </summary>
|
|||
public Foldout Container => m_Container; |
|||
|
|||
/// <summary>
|
|||
/// The toggle to control whether the ID provider is enabled.
|
|||
/// </summary>
|
|||
public Toggle EnabledToggle => m_Enabled; |
|||
|
|||
/// <summary>
|
|||
/// The text field to fill the client ID.
|
|||
/// </summary>
|
|||
public TextField ClientIdField => m_ClientId; |
|||
|
|||
/// <summary>
|
|||
/// The text field to fill the client secret.
|
|||
/// </summary>
|
|||
public TextField ClientSecretField => m_ClientSecret; |
|||
|
|||
/// <summary>
|
|||
/// The button to save the changes.
|
|||
/// </summary>
|
|||
public Button SaveButton => m_SaveButton; |
|||
|
|||
/// <summary>
|
|||
/// The button to cancel changes.
|
|||
/// </summary>
|
|||
public Button CancelButton => m_CancelButton; |
|||
|
|||
|
|||
/// <summary>
|
|||
/// The button to delete the current ID provider.
|
|||
/// </summary>
|
|||
public Button DeleteButton => m_DeleteButton; |
|||
|
|||
/// <summary>
|
|||
/// Event triggered when the <cref="IdProviderElement"/> starts or finishes waiting for an async operation.
|
|||
/// The first parameter of the callback is the sender.
|
|||
/// The second parameter is true if it starts waiting, and false if it finishes waiting.
|
|||
/// </summary>
|
|||
public event Action<IdProviderElement, bool> Waiting; |
|||
|
|||
/// <summary>
|
|||
/// Event triggered when the current <cref="IdProviderElement"/> needs to be deleted by the container.
|
|||
/// The parameter of the callback is the sender.
|
|||
/// </summary>
|
|||
public event Action<IdProviderElement> Deleted; |
|||
|
|||
/// <summary>
|
|||
/// Event triggered when the current <cref="IdProviderElement"/> catches an error.
|
|||
/// The first parameter of the callback is the sender.
|
|||
/// The second parameter is the exception caught by the element.
|
|||
/// </summary>
|
|||
public event Action<IdProviderElement, Exception> Error; |
|||
|
|||
/// <summary>
|
|||
/// The value saved on the server side.
|
|||
/// </summary>
|
|||
public IdProviderResponse SavedValue { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The value of that is about to be saved to the server.
|
|||
/// </summary>
|
|||
public IdProviderResponse CurrentValue { get; set; } |
|||
|
|||
public bool Changed => |
|||
SavedValue?.Type != CurrentValue?.Type || |
|||
SavedValue?.Disabled != CurrentValue?.Disabled || |
|||
(SavedValue?.ClientId ?? "") != (CurrentValue?.ClientId ?? "") || |
|||
(SavedValue?.ClientSecret ?? "") != (CurrentValue?.ClientSecret ?? ""); |
|||
|
|||
public bool IsValid => |
|||
!string.IsNullOrEmpty(CurrentValue.ClientId) && |
|||
(!m_Options.NeedClientSecret || !string.IsNullOrEmpty(CurrentValue.ClientSecret)); |
|||
|
|||
public IdProviderElement(string idDomain, IAuthenticationAdminClient adminClient, IdProviderResponse savedValue, IdProviderOptions options, bool skipConfirmation = false) |
|||
{ |
|||
m_IdDomainId = idDomain; |
|||
m_AdminClient = adminClient; |
|||
m_Options = options; |
|||
m_SkipConfirmation = skipConfirmation; |
|||
|
|||
SavedValue = savedValue; |
|||
CurrentValue = SavedValue.Clone(); |
|||
|
|||
var containerAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_ElementUxml); |
|||
if (containerAsset != null) |
|||
{ |
|||
var containerUI = containerAsset.CloneTree().contentContainer; |
|||
|
|||
m_Container = containerUI.Q<Foldout>(className: "auth-id-provider-details"); |
|||
m_Container.text = savedValue.Type; |
|||
|
|||
// If the ID Provider element is new, default to unfold.
|
|||
m_Container.value = SavedValue.New; |
|||
|
|||
m_Enabled = containerUI.Q<Toggle>("id-provider-enabled"); |
|||
m_Enabled.RegisterCallback<ChangeEvent<bool>>(OnEnabledChanged); |
|||
|
|||
m_ClientId = containerUI.Q<TextField>("id-provider-client-id"); |
|||
m_ClientId.label = options.ClientIdDisplayName; |
|||
m_ClientId.RegisterCallback<ChangeEvent<string>>(OnClientIdChanged); |
|||
|
|||
m_ClientSecret = containerUI.Q<TextField>("id-provider-client-secret"); |
|||
if (options.NeedClientSecret) |
|||
{ |
|||
m_ClientSecret.label = options.ClientSecretDisplayName; |
|||
m_ClientSecret.RegisterCallback<ChangeEvent<string>>(OnClientSecretChanged); |
|||
} |
|||
else |
|||
{ |
|||
m_ClientSecret.style.display = DisplayStyle.None; |
|||
} |
|||
|
|||
m_SaveButton = containerUI.Q<Button>("id-provider-save"); |
|||
m_SaveButton.SetEnabled(false); |
|||
m_SaveButton.clicked += OnSaveButtonClicked; |
|||
|
|||
m_CancelButton = containerUI.Q<Button>("id-provider-cancel"); |
|||
m_CancelButton.SetEnabled(false); |
|||
m_CancelButton.clicked += OnCancelButtonClicked; |
|||
|
|||
m_DeleteButton = containerUI.Q<Button>("id-provider-delete"); |
|||
m_DeleteButton.clicked += OnDeleteButtonClicked; |
|||
m_DeleteButton.SetEnabled(!savedValue.New); |
|||
|
|||
ResetCurrentValue(); |
|||
Add(containerUI); |
|||
} |
|||
else |
|||
{ |
|||
throw new Exception("Asset not found: " + k_ElementUxml); |
|||
} |
|||
} |
|||
|
|||
void RefreshButtons() |
|||
{ |
|||
bool hasChanges = Changed; |
|||
m_SaveButton.SetEnabled(hasChanges && IsValid); |
|||
m_CancelButton.SetEnabled(hasChanges || SavedValue.New); |
|||
if (SavedValue.New) |
|||
{ |
|||
m_DeleteButton.SetEnabled(false); |
|||
m_DeleteButton.style.display = DisplayStyle.None; |
|||
} |
|||
else |
|||
{ |
|||
m_DeleteButton.SetEnabled(true); |
|||
m_DeleteButton.style.display = DisplayStyle.Flex; |
|||
} |
|||
} |
|||
|
|||
void OnEnabledChanged(ChangeEvent<bool> e) |
|||
{ |
|||
CurrentValue.Disabled = !e.newValue; |
|||
RefreshButtons(); |
|||
} |
|||
|
|||
void OnClientIdChanged(ChangeEvent<string> e) |
|||
{ |
|||
CurrentValue.ClientId = e.newValue; |
|||
RefreshButtons(); |
|||
} |
|||
|
|||
void OnClientSecretChanged(ChangeEvent<string> e) |
|||
{ |
|||
CurrentValue.ClientSecret = e.newValue; |
|||
RefreshButtons(); |
|||
} |
|||
|
|||
void OnSaveButtonClicked() |
|||
{ |
|||
int option = DisplayDialogComplex("Save your changes", "Do you want to save the ID provider changes?", "Save", "Cancel", ""); |
|||
switch (option) |
|||
{ |
|||
case 0: |
|||
Waiting?.Invoke(this, true); |
|||
|
|||
if (SavedValue.New) |
|||
{ |
|||
var asyncOp = m_AdminClient.CreateIdProvider(m_IdDomainId, new CreateIdProviderRequest(CurrentValue)); |
|||
asyncOp.Completed += OnSaveCompleted; |
|||
} |
|||
else |
|||
{ |
|||
var body = new UpdateIdProviderRequest(CurrentValue); |
|||
var asyncOp = m_AdminClient.UpdateIdProvider(m_IdDomainId, CurrentValue.Type, body); |
|||
asyncOp.Completed += OnSaveCompleted; |
|||
} |
|||
break; |
|||
|
|||
case 1: |
|||
break; |
|||
|
|||
default: |
|||
Debug.LogError("Unrecognized option."); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void OnSaveCompleted(IAsyncOperation<IdProviderResponse> asyncOp) |
|||
{ |
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
Error?.Invoke(this, AuthenticationSettingsHelper.ExtractException(asyncOp.Exception)); |
|||
Waiting?.Invoke(this, false); |
|||
return; |
|||
} |
|||
|
|||
SavedValue = asyncOp.Result; |
|||
|
|||
// Check enable/disable status
|
|||
if (SavedValue.Disabled != CurrentValue.Disabled) |
|||
{ |
|||
SavedValue.ClientSecret = CurrentValue.ClientSecret; |
|||
asyncOp = CurrentValue.Disabled ? m_AdminClient.DisableIdProvider(m_IdDomainId, CurrentValue.Type) : m_AdminClient.EnableIdProvider(m_IdDomainId, CurrentValue.Type); |
|||
asyncOp.Completed += OnEnableDisableCompleted; |
|||
return; |
|||
} |
|||
|
|||
// Enable/disable is not changed
|
|||
ResetCurrentValue(); |
|||
RefreshButtons(); |
|||
Waiting?.Invoke(this, false); |
|||
} |
|||
|
|||
void OnEnableDisableCompleted(IAsyncOperation<IdProviderResponse> asyncOp) |
|||
{ |
|||
// Handle enable/disable exception
|
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
Error?.Invoke(this, AuthenticationSettingsHelper.ExtractException(asyncOp.Exception)); |
|||
Waiting?.Invoke(this, false); |
|||
return; |
|||
} |
|||
|
|||
// Only reset current value when no exception
|
|||
SavedValue = asyncOp.Result; |
|||
ResetCurrentValue(); |
|||
RefreshButtons(); |
|||
Waiting?.Invoke(this, false); |
|||
} |
|||
|
|||
void OnCancelButtonClicked() |
|||
{ |
|||
if (SavedValue.New) |
|||
{ |
|||
// It's a new ID provider and it hasn't been saved to the server yet.
|
|||
// Simply trigger delete event to notify parent to remove the element from the list.
|
|||
Deleted?.Invoke(this); |
|||
return; |
|||
} |
|||
ResetCurrentValue(); |
|||
} |
|||
|
|||
void ResetCurrentValue() |
|||
{ |
|||
CurrentValue = SavedValue.Clone(); |
|||
m_Enabled.value = !CurrentValue.Disabled; |
|||
m_ClientId.value = CurrentValue.ClientId ?? ""; |
|||
m_ClientSecret.value = CurrentValue.ClientSecret ?? ""; |
|||
|
|||
RefreshButtons(); |
|||
} |
|||
|
|||
void OnDeleteButtonClicked() |
|||
{ |
|||
int option = DisplayDialogComplex("Delete Request", "Do you want to delete the ID Provider?", "Delete", "Cancel", ""); |
|||
switch (option) |
|||
{ |
|||
// Delete
|
|||
case 0: |
|||
Waiting?.Invoke(this, true); |
|||
var asyncOp = m_AdminClient.DeleteIdProvider(m_IdDomainId, CurrentValue.Type); |
|||
asyncOp.Completed += OnDeleteCompleted; |
|||
break; |
|||
|
|||
// Cancel
|
|||
case 1: |
|||
break; |
|||
|
|||
default: |
|||
Debug.LogError("Unrecognized option."); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void OnDeleteCompleted(IAsyncOperation<IdProviderResponse> asyncOp) |
|||
{ |
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
Error?.Invoke(this, AuthenticationSettingsHelper.ExtractException(asyncOp.Exception)); |
|||
Waiting?.Invoke(this, false); |
|||
return; |
|||
} |
|||
|
|||
// Simply trigger delete event to notify parent to remove the element from the list.
|
|||
Deleted?.Invoke(this); |
|||
ResetCurrentValue(); |
|||
Waiting?.Invoke(this, false); |
|||
} |
|||
|
|||
int DisplayDialogComplex(string title, string message, string ok, string cancel, string alt) |
|||
{ |
|||
if (Application.isBatchMode || m_SkipConfirmation) |
|||
return 0; |
|||
|
|||
return EditorUtility.DisplayDialogComplex(title, message, ok, cancel, alt); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 47435640c6bd4c5085515305c09812b2 |
|||
timeCreated: 1620320241 |
|
|||
fileFormatVersion: 2 |
|||
guid: 4a4020826b7ea4ccaacf75ad3f61c1f3 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class CreateIdProviderRequest |
|||
{ |
|||
[Preserve] |
|||
public CreateIdProviderRequest() {} |
|||
|
|||
[Preserve] |
|||
public CreateIdProviderRequest(IdProviderResponse body) |
|||
{ |
|||
ClientId = body.ClientId; |
|||
ClientSecret = body.ClientSecret; |
|||
Type = body.Type; |
|||
Disabled = body.Disabled; |
|||
} |
|||
|
|||
[JsonProperty("clientId")] |
|||
public string ClientId; |
|||
|
|||
[JsonProperty("clientSecret")] |
|||
public string ClientSecret; |
|||
|
|||
[JsonProperty("type")] |
|||
public string Type; |
|||
|
|||
[JsonProperty("disabled")] |
|||
public bool Disabled; |
|||
|
|||
public override bool Equals(Object obj) |
|||
{ |
|||
// Check for null and compare run-time types.
|
|||
if ((obj == null) || !GetType().Equals(obj.GetType())) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
CreateIdProviderRequest c = (CreateIdProviderRequest)obj; |
|||
return (ClientId == c.ClientId) && |
|||
(ClientSecret == c.ClientSecret) && |
|||
(Disabled == c.Disabled) && |
|||
(Type == c.Type); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: be1b1eaa739554966a83f3094a060c10 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class DeleteIdProviderRequest |
|||
{ |
|||
[Preserve] |
|||
public DeleteIdProviderRequest() {} |
|||
|
|||
[JsonProperty("IdDomain")] |
|||
public string IdDomain; |
|||
|
|||
// string type
|
|||
[JsonProperty("type")] |
|||
public string Type; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 8123abbc4107b4f96b214a0be8153b82 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class GetIdDomainResponse |
|||
{ |
|||
[Preserve] |
|||
public GetIdDomainResponse() {} |
|||
|
|||
[JsonProperty("id")] |
|||
public string Id; |
|||
|
|||
[JsonProperty("name")] |
|||
public string Name; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 9014e1c2ef9b94c00bb9aacb76012679 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class IdProviderResponse |
|||
{ |
|||
[Preserve] |
|||
public IdProviderResponse() {} |
|||
|
|||
[JsonIgnore] |
|||
public bool New; |
|||
|
|||
[JsonProperty("clientId")] |
|||
public string ClientId; |
|||
|
|||
[JsonProperty("clientSecret")] |
|||
public string ClientSecret; |
|||
|
|||
[JsonProperty("type")] |
|||
public string Type; |
|||
|
|||
[JsonProperty("disabled")] |
|||
public bool Disabled; |
|||
|
|||
public IdProviderResponse Clone() |
|||
{ |
|||
return new IdProviderResponse |
|||
{ |
|||
New = New, |
|||
Type = Type, |
|||
ClientId = ClientId, |
|||
ClientSecret = ClientSecret, |
|||
Disabled = Disabled |
|||
}; |
|||
} |
|||
|
|||
public override bool Equals(Object obj) |
|||
{ |
|||
// Check for null and compare run-time types.
|
|||
if ((obj == null) || !GetType().Equals(obj.GetType())) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
IdProviderResponse c = (IdProviderResponse)obj; |
|||
return (New == c.New) && |
|||
(ClientId == c.ClientId) && |
|||
(ClientSecret == c.ClientSecret) && |
|||
(Disabled == c.Disabled) && |
|||
(Type == c.Type); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 7eacdd408d38b48c899541aa578c5269 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class ListIdProviderResponse |
|||
{ |
|||
[Preserve] |
|||
public ListIdProviderResponse() {} |
|||
|
|||
[JsonProperty("total")] |
|||
public int Total; |
|||
|
|||
[JsonProperty("results")] |
|||
public IdProviderResponse[] Results; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 2daa2da5cf49d49f39454c147fce718c |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
/// <summary>
|
|||
/// The model for error response from authentication server.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// There is another field "details" in the error response. It provides additional details
|
|||
/// to the error. It's ignored in this deserialized class since it's not needed by the client SDK.
|
|||
/// </remarks>
|
|||
[Serializable] |
|||
class TokenExchangeErrorResponse |
|||
{ |
|||
[Preserve] |
|||
public TokenExchangeErrorResponse() {} |
|||
|
|||
[JsonProperty("name")] |
|||
public string Name; |
|||
|
|||
[JsonProperty("message")] |
|||
public string Message; |
|||
|
|||
[JsonProperty("status")] |
|||
public int Status; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: c6f0eee34b47a4cb8b94023a4898c02d |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class TokenExchangeRequest |
|||
{ |
|||
[Preserve] |
|||
public TokenExchangeRequest() {} |
|||
|
|||
[JsonProperty("token")] |
|||
public string Token; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: e4bf54815194b46d4a6a172313afe524 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class TokenExchangeResponse |
|||
{ |
|||
[Preserve] |
|||
public TokenExchangeResponse() {} |
|||
|
|||
[JsonProperty("token")] |
|||
public string Token; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: faea4b70d595b40938db3363e4ca22fa |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class UpdateIdProviderRequest |
|||
{ |
|||
[Preserve] |
|||
public UpdateIdProviderRequest() {} |
|||
|
|||
[Preserve] |
|||
public UpdateIdProviderRequest(IdProviderResponse body) |
|||
{ |
|||
ClientId = body.ClientId; |
|||
ClientSecret = body.ClientSecret; |
|||
Type = body.Type; |
|||
} |
|||
|
|||
[JsonProperty("clientId")] |
|||
public string ClientId; |
|||
|
|||
[JsonProperty("clientSecret")] |
|||
public string ClientSecret; |
|||
|
|||
[JsonProperty("type")] |
|||
public string Type; |
|||
|
|||
public override bool Equals(Object obj) |
|||
{ |
|||
// Check for null and compare run-time types.
|
|||
if ((obj == null) || !GetType().Equals(obj.GetType())) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
UpdateIdProviderRequest c = (UpdateIdProviderRequest)obj; |
|||
return (ClientId == c.ClientId) && |
|||
(ClientSecret == c.ClientSecret) && |
|||
(Type == c.Type); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: bdcbfea2e2c424771b46cfa662b4a798 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 82d2f74434ae476295725ecf6f372b2e |
|||
timeCreated: 1620232007 |
|
|||
.auth-progress { |
|||
-unity-text-align: middle-center; |
|||
font-size: 18px; |
|||
margin-top: 20px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.auth-error { |
|||
margin-top: 10px; |
|||
margin-bottom: 10px; |
|||
color: red; |
|||
display: none; |
|||
} |
|||
|
|||
.auth-id-providers-text { |
|||
font-size: 18px; |
|||
-unity-font-style: bold; |
|||
} |
|||
|
|||
.auth-id-provider-container { |
|||
flex-direction: column; |
|||
margin: 10px; |
|||
} |
|||
|
|||
.auth-id-provider-details { |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
.auth-id-providers-title { |
|||
flex-direction: row; |
|||
} |
|||
|
|||
.auth-button-container { |
|||
flex-grow: 1; |
|||
flex-direction: row; |
|||
justify-content: flex-end; |
|||
} |
|||
|
|||
.auth-danger-button { |
|||
color: red; |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 72a4ecb8b8884295aa937c771cf80c39 |
|||
timeCreated: 1620232067 |
|
|||
fileFormatVersion: 2 |
|||
guid: de6fee8b1a0b4e6daa6017f3473d3ae4 |
|||
timeCreated: 1620232017 |
|
|||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns="UnityEngine.UIElements" editor-extension-mode="False"> |
|||
<ui:VisualElement class="auth-id-provider-container"> |
|||
<ui:TextElement text="Please wait..." class="auth-progress"/> |
|||
<ui:TextElement text="" class="auth-error"/> |
|||
<ui:VisualElement class="auth-id-providers-title"> |
|||
<ui:TextElement text="ID Providers" class="auth-id-providers-text"/> |
|||
<ui:VisualElement class="auth-button-container"> |
|||
<ui:VisualElement name="id-provider-type" /> |
|||
<ui:Button name="id-provider-add" text="Add" display-tooltip-when-elided="true" /> |
|||
<ui:Button name="id-provider-refresh" text="Refresh" display-tooltip-when-elided="true" /> |
|||
</ui:VisualElement> |
|||
</ui:VisualElement> |
|||
<ui:VisualElement class="auth-id-provider-list" /> |
|||
</ui:VisualElement> |
|||
</ui:UXML> |
部分文件因为文件数量过多而无法显示
撰写
预览
正在加载...
取消
保存
Reference in new issue