当前提交
2ca155b9
共有 143 个文件被更改,包括 3719 次插入 和 2077 次删除
-
61Assets/Prefabs/GameManager.prefab
-
31Assets/Prefabs/UI/BackButtonBG.prefab
-
70Assets/Prefabs/UI/GameCanvas.prefab
-
80Assets/Prefabs/UI/JoinContent.prefab
-
65Assets/Prefabs/UI/JoinCreateCanvas.prefab
-
109Assets/Prefabs/UI/LobbyCanvas.prefab
-
34Assets/Prefabs/UI/LobbyCodeCanvas.prefab
-
48Assets/Prefabs/UI/LobbyUserList.prefab
-
41Assets/Prefabs/UI/MainMenuCanvas.prefab
-
34Assets/Prefabs/UI/RelayCodeCanvas.prefab
-
35Assets/Prefabs/UI/SpinnerUI.prefab
-
84Assets/Prefabs/UI/UserInteractionPanel.prefab
-
384Assets/Scenes/mainScene.unity
-
2Assets/Scripts/GameLobby/Auth/Auth.cs.meta
-
49Assets/Scripts/GameLobby/Game/Countdown.cs
-
462Assets/Scripts/GameLobby/Game/GameManager.cs
-
4Assets/Scripts/GameLobby/Game/LobbyUserObserver.cs
-
353Assets/Scripts/GameLobby/Game/LocalLobby.cs
-
22Assets/Scripts/GameLobby/Game/ServerAddress.cs
-
7Assets/Scripts/GameLobby/Infrastructure/Locator.cs
-
2Assets/Scripts/GameLobby/Infrastructure/Messenger.cs
-
4Assets/Scripts/GameLobby/Infrastructure/Observed.cs
-
2Assets/Scripts/GameLobby/Infrastructure/ObserverBehaviour.cs
-
14Assets/Scripts/GameLobby/Infrastructure/UpdateSlow.cs
-
148Assets/Scripts/GameLobby/Lobby/LobbyConverters.cs
-
8Assets/Scripts/GameLobby/LobbyRelaySample.asmdef
-
6Assets/Scripts/GameLobby/NGO/InGameRunner.cs
-
4Assets/Scripts/GameLobby/NGO/IntroOutroRunner.cs
-
14Assets/Scripts/GameLobby/NGO/NetworkedDataStore.cs
-
4Assets/Scripts/GameLobby/NGO/PlayerCursor.cs
-
8Assets/Scripts/GameLobby/NGO/ResultsUserUI.cs
-
10Assets/Scripts/GameLobby/NGO/Scorer.cs
-
111Assets/Scripts/GameLobby/NGO/SetupInGame.cs
-
10Assets/Scripts/GameLobby/NGO/SymbolContainer.cs
-
4Assets/Scripts/GameLobby/NGO/SymbolKillVolume.cs
-
4Assets/Scripts/GameLobby/NGO/SymbolObject.cs
-
2Assets/Scripts/GameLobby/Relay/AsyncRequestRelay.cs
-
2Assets/Scripts/GameLobby/Relay/RelayPendingApproval.cs
-
50Assets/Scripts/GameLobby/Relay/RelayUtpClient.cs
-
81Assets/Scripts/GameLobby/Relay/RelayUtpHost.cs
-
468Assets/Scripts/GameLobby/Relay/RelayUtpSetup.cs
-
8Assets/Scripts/GameLobby/Tests/Editor/MessengerTests.cs
-
4Assets/Scripts/GameLobby/Tests/Editor/ObserverTests.cs
-
148Assets/Scripts/GameLobby/Tests/PlayMode/LobbyRoundtripTests.cs
-
127Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs
-
4Assets/Scripts/GameLobby/Tests/PlayMode/Tests.Play.asmdef
-
24Assets/Scripts/GameLobby/Tests/PlayMode/UtpTests.cs
-
11Assets/Scripts/GameLobby/UI/CountdownUI.cs
-
12Assets/Scripts/GameLobby/UI/CreateMenuUI.cs
-
30Assets/Scripts/GameLobby/UI/DisplayCodeUI.cs
-
2Assets/Scripts/GameLobby/UI/EmoteButtonUI.cs
-
24Assets/Scripts/GameLobby/UI/GameStateVisibilityUI.cs
-
30Assets/Scripts/GameLobby/UI/InLobbyUserList.cs
-
65Assets/Scripts/GameLobby/UI/InLobbyUserUI.cs
-
22Assets/Scripts/GameLobby/UI/JoinCreateLobbyUI.cs
-
40Assets/Scripts/GameLobby/UI/JoinMenuUI.cs
-
26Assets/Scripts/GameLobby/UI/LobbyButtonUI.cs
-
7Assets/Scripts/GameLobby/UI/LobbyNameUI.cs
-
12Assets/Scripts/GameLobby/UI/LobbyUserVolumeUI.cs
-
56Assets/Scripts/GameLobby/UI/RateLimitVisibility.cs
-
2Assets/Scripts/GameLobby/UI/ReadyCheckUI.cs
-
16Assets/Scripts/GameLobby/UI/RecolorForLobbyType.cs
-
14Assets/Scripts/GameLobby/UI/RelayAddressUI.cs
-
53Assets/Scripts/GameLobby/UI/ShowWhenLobbyStateUI.cs
-
38Assets/Scripts/GameLobby/UI/SpinnerUI.cs
-
12Assets/Scripts/GameLobby/UI/UIPanelBase.cs
-
15Assets/Scripts/GameLobby/UI/UserNameUI.cs
-
42Assets/Scripts/GameLobby/UI/UserStateVisibilityUI.cs
-
3Assets/Scripts/GameLobby/Vivox/VivoxSetup.cs
-
37Packages/manifest.json
-
91Packages/packages-lock.json
-
15ProjectSettings/PackageManagerSettings.asset
-
2ProjectSettings/RiderScriptEditorPersistedState.asset
-
2Assets/Scripts/GameLobby/Infrastructure/Actionvalue.cs.meta
-
118Assets/Scripts/GameLobby/Auth/Auth.cs
-
44Assets/Scripts/GameLobby/Game/LocalLobbyList.cs
-
55Assets/Scripts/GameLobby/Game/LocalPlayer.cs
-
39Assets/Scripts/GameLobby/Infrastructure/Actionvalue.cs
-
409Assets/Scripts/GameLobby/Lobby/LobbyManager.cs
-
193Assets/Scripts/GameLobby/Lobby/LobbySynchronizer.cs
-
3Packages/ParrelSync.meta
-
167ProjectSettings/SceneTemplateSettings.json
-
8Packages/ParrelSync/Editor.meta
-
10Packages/ParrelSync/package.json
-
7Packages/ParrelSync/package.json.meta
-
15Packages/ParrelSync/projectCloner.asmdef
-
7Packages/ParrelSync/projectCloner.asmdef.meta
-
8Packages/ParrelSync/Editor/AssetModBlock.meta
-
22Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs
-
11Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta
-
34Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs
-
11Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta
-
664Packages/ParrelSync/Editor/ClonesManager.cs
-
11Packages/ParrelSync/Editor/ClonesManager.cs.meta
-
11Packages/ParrelSync/Editor/ClonesManagerWindow.cs.meta
-
13Packages/ParrelSync/Editor/ExternalLinks.cs
-
11Packages/ParrelSync/Editor/ExternalLinks.cs.meta
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Holds a LobbyUser value and notifies all subscribers when it has been changed.
|
|||
/// Holds a LocalPlayer value and notifies all subscribers when it has been changed.
|
|||
public class LobbyUserObserver : ObserverBehaviour<LobbyUser> { } |
|||
public class LobbyUserObserver : ObserverBehaviour<LocalPlayer> { } |
|||
} |
|
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
using System; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample.UI |
|||
{ |
|||
/// </summary>
|
|||
public class ShowWhenLobbyStateUI : ObserverPanel<LocalLobby> |
|||
{ |
|||
[SerializeField] |
|||
private LobbyState m_ShowThisWhen; |
|||
|
|||
public override void ObservedUpdated(LocalLobby observed) |
|||
{ |
|||
if (m_ShowThisWhen.HasFlag(observed.State)) |
|||
Show(); |
|||
else |
|||
Hide(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// </summary>
|
|||
public class ShowWhenLobbyStateUI : UIPanelBase |
|||
{ |
|||
[SerializeField] |
|||
LobbyState m_ShowThisWhen; |
|||
|
|||
public void LobbyChanged(LobbyState lobbyState) |
|||
{ |
|||
if (m_ShowThisWhen.HasFlag(lobbyState)) |
|||
Show(); |
|||
else |
|||
Hide(); |
|||
} |
|||
|
|||
public override void Start() |
|||
{ |
|||
base.Start(); |
|||
Manager.LocalLobby.LocalLobbyState.onChanged += LobbyChanged; |
|||
} |
|||
|
|||
public void OnDestroy() |
|||
{ |
|||
if (GameManager.Instance == null) |
|||
return; |
|||
Manager.LocalLobby.LocalLobbyState.onChanged -= LobbyChanged; |
|||
} |
|||
} |
|||
} |
|
|||
using System.Threading.Tasks; |
|||
using Unity.Services.Authentication; |
|||
using Unity.Services.Core; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public enum AuthState |
|||
{ |
|||
Initialized, |
|||
Authenticating, |
|||
Authenticated, |
|||
Error, |
|||
TimedOut |
|||
} |
|||
|
|||
public static class Auth |
|||
{ |
|||
public static AuthState AuthenticationState { get; private set; } = AuthState.Initialized; |
|||
|
|||
public static async Task<AuthState> Authenticate(string profile,int tries = 5) |
|||
{ |
|||
//If we are already authenticated, just return Auth
|
|||
if (AuthenticationState == AuthState.Authenticated) |
|||
{ |
|||
return AuthenticationState; |
|||
} |
|||
|
|||
if (AuthenticationState == AuthState.Authenticating) |
|||
{ |
|||
Debug.LogWarning("Cant Authenticate if we are authenticating or authenticated"); |
|||
await Authenticating(); |
|||
return AuthenticationState; |
|||
} |
|||
|
|||
var profileOptions = new InitializationOptions(); |
|||
profileOptions.SetProfile(profile); |
|||
await UnityServices.InitializeAsync(profileOptions); |
|||
await SignInAnonymouslyAsync(tries); |
|||
Debug.Log($"Auth attempts Finished : {AuthenticationState.ToString()}"); |
|||
|
|||
return AuthenticationState; |
|||
} |
|||
|
|||
//Awaitable task that will pass the clientID once authentication is done.
|
|||
public static string ID() |
|||
{ |
|||
return AuthenticationService.Instance.PlayerId; |
|||
} |
|||
|
|||
//Awaitable task that will pass once authentication is done.
|
|||
public static async Task<AuthState> Authenticating() |
|||
{ |
|||
while (AuthenticationState == AuthState.Authenticating || AuthenticationState == AuthState.Initialized) |
|||
{ |
|||
await Task.Delay(200); |
|||
} |
|||
|
|||
return AuthenticationState; |
|||
} |
|||
|
|||
public static bool DoneAuthenticating() |
|||
{ |
|||
return AuthenticationState != AuthState.Authenticating && |
|||
AuthenticationState != AuthState.Initialized; |
|||
} |
|||
|
|||
static async Task SignInAnonymouslyAsync(int maxRetries) |
|||
{ |
|||
AuthenticationState = AuthState.Authenticating; |
|||
var tries = 0; |
|||
while (AuthenticationState == AuthState.Authenticating && tries < maxRetries) |
|||
{ |
|||
try |
|||
{ |
|||
|
|||
//To ensure staging login vs non staging
|
|||
await AuthenticationService.Instance.SignInAnonymouslyAsync(); |
|||
|
|||
if (AuthenticationService.Instance.IsSignedIn && AuthenticationService.Instance.IsAuthorized) |
|||
{ |
|||
AuthenticationState = AuthState.Authenticated; |
|||
break; |
|||
} |
|||
} |
|||
catch (AuthenticationException ex) |
|||
{ |
|||
// Compare error code to AuthenticationErrorCodes
|
|||
// Notify the player with the proper error message
|
|||
Debug.LogError(ex); |
|||
AuthenticationState = AuthState.Error; |
|||
} |
|||
catch (RequestFailedException exception) |
|||
{ |
|||
// Compare error code to CommonErrorCodes
|
|||
// Notify the player with the proper error message
|
|||
Debug.LogError(exception); |
|||
AuthenticationState = AuthState.Error; |
|||
} |
|||
|
|||
tries++; |
|||
await Task.Delay(1000); |
|||
} |
|||
|
|||
if (AuthenticationState != AuthState.Authenticated) |
|||
{ |
|||
Debug.LogWarning($"Player was not signed in successfully after {tries} attempts"); |
|||
AuthenticationState = AuthState.TimedOut; |
|||
} |
|||
} |
|||
|
|||
public static void SignOut() |
|||
{ |
|||
AuthenticationService.Instance.SignOut(false); |
|||
AuthenticationState = AuthState.Initialized; |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Used when displaying the lobby list, to indicate when we are awaiting an updated lobby query.
|
|||
/// </summary>
|
|||
public enum LobbyQueryState |
|||
{ |
|||
Empty, |
|||
Fetching, |
|||
Error, |
|||
Fetched |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Holds data related to the Lobby service itself - The latest retrieved lobby list, the state of retrieval.
|
|||
/// </summary>
|
|||
[System.Serializable] |
|||
public class LocalLobbyList |
|||
{ |
|||
LobbyQueryState m_CurrentState = LobbyQueryState.Empty; |
|||
|
|||
public CallbackValue<LobbyQueryState> QueryState = new CallbackValue<LobbyQueryState>(); |
|||
|
|||
public Action<Dictionary<string, LocalLobby>> onLobbyListChange; |
|||
Dictionary<string, LocalLobby> m_currentLobbies = new Dictionary<string, LocalLobby>(); |
|||
|
|||
/// <summary>
|
|||
/// Maps from a lobby's ID to the local representation of it. This allows us to remember which remote lobbies are which LocalLobbies.
|
|||
/// Will only trigger if the dictionary is set wholesale. Changes in the size or contents will not trigger OnChanged.
|
|||
/// </summary>
|
|||
public Dictionary<string, LocalLobby> CurrentLobbies |
|||
{ |
|||
get { return m_currentLobbies; } |
|||
set |
|||
{ |
|||
m_currentLobbies = value; |
|||
onLobbyListChange?.Invoke(m_currentLobbies); |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Current state of the user in the lobby.
|
|||
/// This is a Flags enum to allow for the Inspector to select multiples for various UI features.
|
|||
/// </summary>
|
|||
[Flags] |
|||
public enum UserStatus |
|||
{ |
|||
None = 0, |
|||
Connecting = 1, // User has joined a lobby but has not yet connected to Relay.
|
|||
Lobby = 2, // User is in a lobby and connected to Relay.
|
|||
Ready = 4, // User has selected the ready button, to ready for the "game" to start.
|
|||
InGame = 8, // User is part of a "game" that has started.
|
|||
Menu = 16 // User is not in a lobby, in one of the main menus.
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Data for a local player instance. This will update data and is observed to know when to push local player changes to the entire lobby.
|
|||
/// </summary>
|
|||
[Serializable] |
|||
public class LocalPlayer : Observed<LocalPlayer> |
|||
{ |
|||
public CallbackValue<bool> IsHost = new CallbackValue<bool>(false); |
|||
public CallbackValue<string> DisplayName = new CallbackValue<string>(""); |
|||
public CallbackValue<EmoteType> Emote = new CallbackValue<EmoteType>(EmoteType.None); |
|||
public CallbackValue<UserStatus> UserStatus = new CallbackValue<UserStatus>((UserStatus)0); |
|||
public CallbackValue<string> ID = new CallbackValue<string>(""); |
|||
|
|||
public LocalPlayer(string id, bool isHost, string displayName, |
|||
EmoteType emote = default, UserStatus status = default) |
|||
{ |
|||
IsHost.Value = isHost; |
|||
DisplayName.Value = displayName; |
|||
Emote.Value = emote; |
|||
UserStatus.Value = status; |
|||
ID.Value = id; |
|||
} |
|||
|
|||
public void ResetState() |
|||
{ |
|||
IsHost.Value = false; |
|||
Emote.Value = EmoteType.None; |
|||
UserStatus.Value = LobbyRelaySample.UserStatus.Menu; |
|||
} |
|||
|
|||
|
|||
public override void CopyObserved(LocalPlayer observed) |
|||
{ |
|||
OnChanged(this); |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
public class CallbackValue<T> |
|||
{ |
|||
public Action<T> onChanged; |
|||
|
|||
|
|||
public CallbackValue() |
|||
{ |
|||
|
|||
} |
|||
public CallbackValue(T cachedValue) |
|||
{ |
|||
m_CachedValue = cachedValue; |
|||
} |
|||
|
|||
public T Value |
|||
{ |
|||
get => m_CachedValue; |
|||
set |
|||
{ |
|||
if (m_CachedValue.Equals(value)) |
|||
return; |
|||
m_CachedValue = value; |
|||
onChanged?.Invoke(m_CachedValue); |
|||
} |
|||
} |
|||
|
|||
public void SetNoCallback(T value) |
|||
{ |
|||
m_CachedValue = value; |
|||
} |
|||
|
|||
T m_CachedValue = default; |
|||
} |
|||
} |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Unity.Services.Authentication; |
|||
using Unity.Services.Lobbies; |
|||
using Unity.Services.Lobbies.Models; |
|||
using UnityEngine; |
|||
|
|||
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>
|
|||
///
|
|||
/// Manages one Lobby at a time, Only entry points to a lobby with ID is via JoinAsync, CreateAsync, and QuickJoinAsync
|
|||
public class LobbyManager : IDisposable |
|||
{ |
|||
//Once connected to a lobby, cache the local lobby object so we don't query for it for every lobby operation.
|
|||
// (This assumes that the game will be actively in just one lobby at a time, though they could be in more on the service side.)
|
|||
|
|||
public Lobby CurrentLobby => m_CurrentLobby; |
|||
Lobby m_CurrentLobby; |
|||
|
|||
const int k_maxLobbiesToShow = 16; // If more are necessary, consider retrieving paginated results or using filters.
|
|||
|
|||
Task m_HeartBeatTask; |
|||
#region Rate Limiting
|
|||
public enum RequestType |
|||
{ |
|||
Query = 0, |
|||
Join, |
|||
QuickJoin, |
|||
Host |
|||
} |
|||
|
|||
public bool InLobby() |
|||
{ |
|||
if (m_CurrentLobby == null) |
|||
{ |
|||
Debug.LogError("LobbyManager not currently in a lobby. Did you CreateLobbyAsync or JoinLobbyAsync?"); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public RateLimiter GetRateLimit(RequestType type) |
|||
{ |
|||
if (type == RequestType.Join) |
|||
return m_JoinCooldown; |
|||
else if (type == RequestType.QuickJoin) |
|||
return m_QuickJoinCooldown; |
|||
else if (type == RequestType.Host) |
|||
return m_CreateCooldown; |
|||
return m_QueryCooldown; |
|||
} |
|||
|
|||
// Rate Limits are posted here: https://docs.unity.com/lobby/rate-limits.html
|
|||
|
|||
RateLimiter m_QueryCooldown = new RateLimiter(1f); |
|||
RateLimiter m_CreateCooldown = new RateLimiter(3f); |
|||
RateLimiter m_JoinCooldown = new RateLimiter(3f); |
|||
RateLimiter m_QuickJoinCooldown = new RateLimiter(10f); |
|||
RateLimiter m_GetLobbyCooldown = new RateLimiter(1f); |
|||
RateLimiter m_DeleteLobbyCooldown = new RateLimiter(.2f); |
|||
RateLimiter m_UpdateLobbyCooldown = new RateLimiter(.3f); |
|||
RateLimiter m_UpdatePlayerCooldown = new RateLimiter(.3f); |
|||
RateLimiter m_LeaveLobbyOrRemovePlayer = new RateLimiter(.3f); |
|||
RateLimiter m_HeartBeatCooldown = new RateLimiter(6f); |
|||
|
|||
#endregion
|
|||
|
|||
Dictionary<string, PlayerDataObject> CreateInitialPlayerData(LocalPlayer user) |
|||
{ |
|||
Dictionary<string, PlayerDataObject> data = new Dictionary<string, PlayerDataObject>(); |
|||
|
|||
var displayNameObject = new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, user.DisplayName.Value); |
|||
data.Add("DisplayName", displayNameObject); |
|||
return data; |
|||
} |
|||
|
|||
public async Task<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers, bool isPrivate, LocalPlayer localUser) |
|||
{ |
|||
if (m_CreateCooldown.IsInCooldown) |
|||
{ |
|||
UnityEngine.Debug.LogWarning("Create Lobby hit the rate limit."); |
|||
return null; |
|||
} |
|||
|
|||
await m_CreateCooldown.WaitUntilCooldown(); |
|||
|
|||
Debug.Log("Lobby - Creating"); |
|||
|
|||
try |
|||
{ |
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
|
|||
CreateLobbyOptions createOptions = new CreateLobbyOptions |
|||
{ |
|||
IsPrivate = isPrivate, |
|||
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser)) |
|||
}; |
|||
m_CurrentLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions); |
|||
|
|||
StartHeartBeat(); |
|||
|
|||
return m_CurrentLobby; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Debug.LogError($"Lobby Create failed:\n{ex}"); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public async Task<Lobby> JoinLobbyAsync(string lobbyId, string lobbyCode, LocalPlayer localUser) |
|||
{ |
|||
if (m_JoinCooldown.IsInCooldown || |
|||
(lobbyId == null && lobbyCode == null)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
await m_JoinCooldown.WaitUntilCooldown(); |
|||
Debug.Log($"{localUser.DisplayName}({localUser.ID}) Joining Lobby- {lobbyId} with {lobbyCode}"); |
|||
|
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
var playerData = CreateInitialPlayerData(localUser); |
|||
if (!string.IsNullOrEmpty(lobbyId)) |
|||
{ |
|||
JoinLobbyByIdOptions joinOptions = new JoinLobbyByIdOptions |
|||
{ Player = new Player(id: uasId, data: playerData) }; |
|||
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions); |
|||
} |
|||
else |
|||
{ |
|||
JoinLobbyByCodeOptions joinOptions = new JoinLobbyByCodeOptions |
|||
{ Player = new Player(id: uasId, data: playerData) }; |
|||
m_CurrentLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, joinOptions); |
|||
} |
|||
|
|||
return m_CurrentLobby; |
|||
} |
|||
|
|||
public async Task<Lobby> QuickJoinLobbyAsync(LocalPlayer localUser, LobbyColor limitToColor = LobbyColor.None) |
|||
{ |
|||
//We dont want to queue a quickjoin
|
|||
if (m_QuickJoinCooldown.IsInCooldown) |
|||
{ |
|||
UnityEngine.Debug.LogWarning("Quick Join Lobby hit the rate limit."); |
|||
return null; |
|||
} |
|||
|
|||
await m_QuickJoinCooldown.WaitUntilCooldown(); |
|||
Debug.Log("Lobby - Quick Joining."); |
|||
var filters = LobbyColorToFilters(limitToColor); |
|||
string uasId = AuthenticationService.Instance.PlayerId; |
|||
|
|||
var joinRequest = new QuickJoinLobbyOptions |
|||
{ |
|||
Filter = filters, |
|||
Player = new Player(id: uasId, data: CreateInitialPlayerData(localUser)) |
|||
}; |
|||
|
|||
return m_CurrentLobby = await LobbyService.Instance.QuickJoinLobbyAsync(joinRequest); |
|||
} |
|||
|
|||
public async Task<QueryResponse> RetrieveLobbyListAsync(LobbyColor limitToColor = LobbyColor.None) |
|||
{ |
|||
await m_QueryCooldown.WaitUntilCooldown(); |
|||
|
|||
Debug.Log("Lobby - Retrieving List."); |
|||
|
|||
var filters = LobbyColorToFilters(limitToColor); |
|||
|
|||
QueryLobbiesOptions queryOptions = new QueryLobbiesOptions |
|||
{ |
|||
Count = k_maxLobbiesToShow, |
|||
Filters = filters |
|||
}; |
|||
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions); |
|||
} |
|||
|
|||
List<QueryFilter> LobbyColorToFilters(LobbyColor limitToColor) |
|||
{ |
|||
List<QueryFilter> filters = new List<QueryFilter>(); |
|||
if (limitToColor == LobbyColor.Orange) |
|||
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Orange).ToString(), |
|||
QueryFilter.OpOptions.EQ)); |
|||
else if (limitToColor == LobbyColor.Green) |
|||
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Green).ToString(), |
|||
QueryFilter.OpOptions.EQ)); |
|||
else if (limitToColor == LobbyColor.Blue) |
|||
filters.Add(new QueryFilter(QueryFilter.FieldOptions.N1, ((int)LobbyColor.Blue).ToString(), |
|||
QueryFilter.OpOptions.EQ)); |
|||
return filters; |
|||
} |
|||
|
|||
public async Task<Lobby> GetLobbyAsync(string lobbyId = null) |
|||
{ |
|||
if (!InLobby()) |
|||
return null; |
|||
await m_GetLobbyCooldown.WaitUntilCooldown(); |
|||
lobbyId ??= m_CurrentLobby.Id; |
|||
return m_CurrentLobby = await LobbyService.Instance.GetLobbyAsync(lobbyId); |
|||
} |
|||
|
|||
public async Task LeaveLobbyAsync() |
|||
{ |
|||
await m_LeaveLobbyOrRemovePlayer.WaitUntilCooldown(); |
|||
if (!InLobby()) |
|||
return; |
|||
string playerId = AuthenticationService.Instance.PlayerId; |
|||
Debug.Log($"{playerId} leaving Lobby {m_CurrentLobby.Id}"); |
|||
|
|||
await LobbyService.Instance.RemovePlayerAsync(m_CurrentLobby.Id, playerId); |
|||
m_CurrentLobby = null; |
|||
} |
|||
|
|||
public async Task<Lobby> UpdatePlayerDataAsync(Dictionary<string, string> data) |
|||
{ |
|||
if (!InLobby()) |
|||
return null; |
|||
await m_UpdatePlayerCooldown.WaitUntilCooldown(); |
|||
Debug.Log("Lobby - Updating Player Data"); |
|||
|
|||
string playerId = AuthenticationService.Instance.PlayerId; |
|||
Dictionary<string, PlayerDataObject> dataCurr = new Dictionary<string, PlayerDataObject>(); |
|||
foreach (var dataNew in data) |
|||
{ |
|||
PlayerDataObject dataObj = new PlayerDataObject(visibility: PlayerDataObject.VisibilityOptions.Member, |
|||
value: dataNew.Value); |
|||
if (dataCurr.ContainsKey(dataNew.Key)) |
|||
dataCurr[dataNew.Key] = dataObj; |
|||
else |
|||
dataCurr.Add(dataNew.Key, dataObj); |
|||
} |
|||
|
|||
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions |
|||
{ |
|||
Data = dataCurr, |
|||
AllocationId = null, |
|||
ConnectionInfo = null |
|||
}; |
|||
return m_CurrentLobby = |
|||
await LobbyService.Instance.UpdatePlayerAsync(m_CurrentLobby.Id, playerId, updateOptions); |
|||
} |
|||
|
|||
public async Task<Lobby> UpdatePlayerRelayInfoAsync(string lobbyID, string allocationId, string connectionInfo) |
|||
{ |
|||
if (!InLobby()) |
|||
return null; |
|||
await m_UpdatePlayerCooldown.WaitUntilCooldown(); |
|||
Debug.Log("Lobby - Relay Info (Player)"); |
|||
|
|||
string playerId = AuthenticationService.Instance.PlayerId; |
|||
|
|||
UpdatePlayerOptions updateOptions = new UpdatePlayerOptions |
|||
{ |
|||
Data = new Dictionary<string, PlayerDataObject>(), |
|||
AllocationId = allocationId, |
|||
ConnectionInfo = connectionInfo |
|||
}; |
|||
return m_CurrentLobby = await LobbyService.Instance.UpdatePlayerAsync(lobbyID, playerId, updateOptions); |
|||
} |
|||
|
|||
public async Task<Lobby> UpdateLobbyDataAsync(Dictionary<string, string> data) |
|||
{ |
|||
if (!InLobby()) |
|||
return null; |
|||
await m_UpdateLobbyCooldown.WaitUntilCooldown(); |
|||
Debug.Log("Lobby - Updating Lobby Data"); |
|||
|
|||
Dictionary<string, DataObject> dataCurr = m_CurrentLobby.Data ?? new Dictionary<string, DataObject>(); |
|||
|
|||
var shouldLock = false; |
|||
foreach (var dataNew in data) |
|||
{ |
|||
// Special case: We want to be able to filter on our color data, so we need to supply an arbitrary index to retrieve later. Uses N# for numerics, instead of S# for strings.
|
|||
DataObject.IndexOptions index = dataNew.Key == "LocalLobbyColor" ? DataObject.IndexOptions.N1 : 0; |
|||
DataObject |
|||
dataObj = new DataObject(DataObject.VisibilityOptions.Public, dataNew.Value, |
|||
index); // Public so that when we request the list of lobbies, we can get info about them for filtering.
|
|||
if (dataCurr.ContainsKey(dataNew.Key)) |
|||
dataCurr[dataNew.Key] = dataObj; |
|||
else |
|||
dataCurr.Add(dataNew.Key, dataObj); |
|||
|
|||
//Special Use: Get the state of the Local lobby so we can lock it from appearing in queries if it's not in the "Lobby" LocalLobbyState
|
|||
if (dataNew.Key == "LocalLobbyState") |
|||
{ |
|||
Enum.TryParse(dataNew.Value, out LobbyState lobbyState); |
|||
shouldLock = lobbyState != LobbyState.Lobby; |
|||
} |
|||
} |
|||
|
|||
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = dataCurr, IsLocked = shouldLock }; |
|||
return m_CurrentLobby = await LobbyService.Instance.UpdateLobbyAsync(m_CurrentLobby.Id, updateOptions); |
|||
} |
|||
|
|||
public async Task DeleteLobbyAsync() |
|||
{ |
|||
if (!InLobby()) |
|||
return; |
|||
await m_DeleteLobbyCooldown.WaitUntilCooldown(); |
|||
Debug.Log("Lobby - Deleting Lobby"); |
|||
|
|||
await LobbyService.Instance.DeleteLobbyAsync(m_CurrentLobby.Id); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
m_CurrentLobby = null; |
|||
m_HeartBeatTask.Dispose(); |
|||
} |
|||
|
|||
#region HeartBeat
|
|||
|
|||
//Since the LobbyManager maintains the "connection" to the lobby, we will continue to heartbeat until host leaves.
|
|||
async Task SendHeartbeatPingAsync() |
|||
{ |
|||
if (!InLobby()) |
|||
return; |
|||
if (m_HeartBeatCooldown.IsInCooldown) |
|||
return; |
|||
await m_HeartBeatCooldown.WaitUntilCooldown(); |
|||
Debug.Log("Lobby - Heartbeat"); |
|||
|
|||
await LobbyService.Instance.SendHeartbeatPingAsync(m_CurrentLobby.Id); |
|||
} |
|||
|
|||
void StartHeartBeat() |
|||
{ |
|||
#pragma warning disable 4014
|
|||
m_HeartBeatTask = HeartBeatLoop(); |
|||
#pragma warning restore 4014
|
|||
} |
|||
async Task HeartBeatLoop() |
|||
{ |
|||
while (m_CurrentLobby != null) |
|||
{ |
|||
await SendHeartbeatPingAsync(); |
|||
await Task.Delay(8000); |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
} |
|||
|
|||
//Manages the Cooldown for each service call.
|
|||
//Adds a buffer to account for ping times.
|
|||
public class RateLimiter |
|||
{ |
|||
public Action<bool> onCooldownChange; |
|||
public readonly float cooldownSeconds; |
|||
public readonly int coolDownMS; |
|||
public readonly int pingBufferMS; |
|||
|
|||
//(If you're still getting rate limit errors, try increasing the pingBuffer)
|
|||
public RateLimiter(float cooldownSeconds, int pingBuffer = 100) |
|||
{ |
|||
this.cooldownSeconds = cooldownSeconds; |
|||
pingBufferMS = pingBuffer; |
|||
coolDownMS = |
|||
Mathf.CeilToInt(this.cooldownSeconds * 1000) + |
|||
pingBufferMS; |
|||
} |
|||
|
|||
public async Task WaitUntilCooldown() |
|||
{ |
|||
//No Queue!
|
|||
if (!m_IsInCooldown) |
|||
{ |
|||
#pragma warning disable 4014
|
|||
CooldownAsync(); |
|||
#pragma warning restore 4014
|
|||
return; |
|||
} |
|||
|
|||
while (m_IsInCooldown) |
|||
{ |
|||
await Task.Delay(10); |
|||
} |
|||
} |
|||
|
|||
async Task CooldownAsync() |
|||
{ |
|||
IsInCooldown = true; |
|||
await Task.Delay(coolDownMS); |
|||
IsInCooldown = false; |
|||
} |
|||
|
|||
bool m_IsInCooldown = false; |
|||
|
|||
public bool IsInCooldown |
|||
{ |
|||
get => m_IsInCooldown; |
|||
private set |
|||
{ |
|||
if (m_IsInCooldown != value) |
|||
{ |
|||
m_IsInCooldown = value; |
|||
onCooldownChange?.Invoke(m_IsInCooldown); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using LobbyRelaySample.lobby; |
|||
using Unity.Services.Lobbies.Models; |
|||
using UnityEngine; |
|||
|
|||
namespace LobbyRelaySample |
|||
{ |
|||
/// <summary>
|
|||
/// Keep updated on changes to a joined lobby, at a speed compliant with Lobby's rate limiting.
|
|||
/// </summary>
|
|||
public class LobbySynchronizer : IReceiveMessages, IDisposable |
|||
{ |
|||
LocalLobby m_LocalLobby; |
|||
LocalPlayer m_LocalUser; |
|||
LobbyManager m_LobbyManager; |
|||
bool m_LocalChanges = false; |
|||
|
|||
const int |
|||
k_approvalMaxMS = 10000; // Used for determining if a user should timeout if they are unable to connect.
|
|||
|
|||
int m_lifetime = 0; |
|||
const int k_UpdateIntervalMS = 1000; |
|||
|
|||
public LobbySynchronizer(LobbyManager lobbyManager) |
|||
{ |
|||
m_LobbyManager = lobbyManager; |
|||
} |
|||
|
|||
public void StartSynch(LocalLobby localLobby, LocalPlayer localUser) |
|||
{ |
|||
m_LocalUser = localUser; |
|||
m_LocalLobby = localLobby; |
|||
m_LocalLobby.LobbyID.onChanged += OnLobbyIdChanged; |
|||
m_LocalChanges = true; |
|||
Locator.Get.Messenger.Subscribe(this); |
|||
#pragma warning disable 4014
|
|||
UpdateLoopAsync(); |
|||
#pragma warning restore 4014
|
|||
m_lifetime = 0; |
|||
} |
|||
|
|||
public void EndSynch() |
|||
{ |
|||
m_LocalChanges = false; |
|||
|
|||
Locator.Get.Messenger.Unsubscribe(this); |
|||
if (m_LocalLobby != null) |
|||
m_LocalLobby.LobbyID.onChanged -= OnLobbyIdChanged; |
|||
|
|||
m_LocalLobby = null; |
|||
} |
|||
|
|||
//TODO Stop players from joining lobby while game is underway.
|
|||
public void OnReceiveMessage(MessageType type, object msg) |
|||
{ |
|||
// if (type == MessageType.ClientUserSeekingDisapproval)
|
|||
// {
|
|||
// bool shouldDisapprove =
|
|||
// m_LocalLobby.LocalLobbyState !=
|
|||
// LocalLobbyState.Lobby; // By not refreshing, it's possible to have a lobby in the lobby list UI after its countdown starts and then try joining.
|
|||
// if (shouldDisapprove)
|
|||
// (msg as Action<relay.Approval>)?.Invoke(relay.Approval.GameAlreadyStarted);
|
|||
// }
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// If there have been any data changes since the last update, push them to Lobby. Regardless, pull for the most recent data.
|
|||
/// (Unless we're already awaiting a query, in which case continue waiting.)
|
|||
/// </summary>
|
|||
async Task UpdateLoopAsync() |
|||
{ |
|||
Lobby latestLobby = null; |
|||
|
|||
while (m_LocalLobby != null) |
|||
{ |
|||
latestLobby = await GetLatestRemoteLobby(); |
|||
|
|||
if (IfRemoteLobbyChanged(latestLobby)) |
|||
{ |
|||
//Pulling remote changes, and applying them to the local lobby usually flags it as changed,
|
|||
//Causing another pull, the RemoteToLocal converter ensures this does not happen by flagging the lobby.
|
|||
LobbyConverters.RemoteToLocal(latestLobby, m_LocalLobby, false); |
|||
} |
|||
|
|||
if (!LobbyHasHost()) |
|||
{ |
|||
LeaveLobbyBecauseNoHost(); |
|||
break; |
|||
} |
|||
|
|||
var areAllusersReady = AreAllUsersReady(); |
|||
if (areAllusersReady && m_LocalLobby.LocalLobbyState.Value == LobbyState.Lobby) |
|||
{ |
|||
GameManager.Instance.BeginCountdown(); |
|||
} |
|||
else if (!areAllusersReady && m_LocalLobby.LocalLobbyState.Value == LobbyState.CountDown) |
|||
{ |
|||
GameManager.Instance.CancelCountDown(); |
|||
} |
|||
|
|||
m_lifetime += k_UpdateIntervalMS; |
|||
await Task.Delay(k_UpdateIntervalMS); |
|||
} |
|||
} |
|||
|
|||
async Task<Lobby> GetLatestRemoteLobby() |
|||
{ |
|||
Lobby latestLobby = null; |
|||
if (m_LocalLobby.IsLobbyChanged()) |
|||
{ |
|||
latestLobby = await PushDataToLobby(); |
|||
} |
|||
else |
|||
{ |
|||
latestLobby = await m_LobbyManager.GetLobbyAsync(); |
|||
} |
|||
|
|||
return latestLobby; |
|||
} |
|||
|
|||
bool IfRemoteLobbyChanged(Lobby remoteLobby) |
|||
{ |
|||
var remoteLobbyTime = remoteLobby.LastUpdated.ToFileTimeUtc(); |
|||
var localLobbyTime = m_LocalLobby.LastUpdated.Value; |
|||
var isLocalOutOfDate = remoteLobbyTime > localLobbyTime; |
|||
return isLocalOutOfDate; |
|||
} |
|||
|
|||
async Task<Lobby> PushDataToLobby() |
|||
{ |
|||
m_LocalChanges = false; |
|||
|
|||
if (m_LocalUser.IsHost.Value) |
|||
await m_LobbyManager.UpdateLobbyDataAsync( |
|||
LobbyConverters.LocalToRemoteData(m_LocalLobby)); |
|||
|
|||
return await m_LobbyManager.UpdatePlayerDataAsync( |
|||
LobbyConverters.LocalToRemoteUserData(m_LocalUser)); |
|||
} |
|||
|
|||
bool AreAllUsersReady() |
|||
{ |
|||
foreach (var lobbyUser in m_LocalLobby.LocalPlayers.Values) |
|||
{ |
|||
if (lobbyUser.UserStatus.Value != UserStatus.Ready) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
bool LobbyHasHost() |
|||
{ |
|||
if (!m_LocalUser.IsHost.Value) |
|||
{ |
|||
foreach (var lobbyUser in m_LocalLobby.LocalPlayers) |
|||
{ |
|||
if (lobbyUser.Value.IsHost.Value) |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
void LeaveLobbyBecauseNoHost() |
|||
{ |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.DisplayErrorPopup, |
|||
"Host left the lobby! Disconnecting..."); |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.EndGame, null); |
|||
Locator.Get.Messenger.OnReceiveMessage(MessageType.ChangeMenuState, GameState.JoinMenu); |
|||
} |
|||
|
|||
public void OnLobbyIdChanged(string lobbyID) |
|||
{ |
|||
if (string.IsNullOrEmpty(lobbyID) |
|||
) // When the player leaves, their LocalLobby is cleared out.
|
|||
{ |
|||
EndSynch(); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
EndSynch(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 520a1688a3c84604b0e7836ecc15cee3 |
|||
timeCreated: 1654698051 |
|
|||
{ |
|||
"templatePinStates": [], |
|||
"dependencyTypeInfos": [ |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.AnimationClip", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.Animations.AnimatorController", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.AnimatorOverrideController", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.Audio.AudioMixerController", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.ComputeShader", |
|||
"ignore": true, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Cubemap", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.GameObject", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.LightingDataAsset", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": false |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.LightingSettings", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Material", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.MonoScript", |
|||
"ignore": true, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.PhysicMaterial", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.PhysicsMaterial2D", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Rendering.PostProcessing.PostProcessResources", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Rendering.VolumeProfile", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.SceneAsset", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": false |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Shader", |
|||
"ignore": true, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.ShaderVariantCollection", |
|||
"ignore": true, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Texture", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Texture2D", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Timeline.TimelineAsset", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
} |
|||
], |
|||
"defaultDependencyTypeInfo": { |
|||
"userAdded": false, |
|||
"type": "<default_scene_template_dependencies>", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
"newSceneOverride": 0 |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a31ea7d0315594440839cdb0db6bc411 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
{ |
|||
"name": "com.veriorpies.parrelsync", |
|||
"displayName": "ParrelSync", |
|||
"version": "1.5.0", |
|||
"unity": "2018.4", |
|||
"description": "ParrelSync is a Unity editor extension that allows users to test multiplayer gameplay without building the project by having another Unity editor window opened and mirror the changes from the original project.", |
|||
"license": "MIT", |
|||
"keywords": [ "Networking", "Utils", "Editor", "Extensions" ], |
|||
"dependencies": {} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a2a889c264e34b47a7349cbcb2cbedd7 |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
{ |
|||
"name": "ParrelSync", |
|||
"references": [], |
|||
"includePlatforms": [ |
|||
"Editor" |
|||
], |
|||
"excludePlatforms": [], |
|||
"allowUnsafeCode": false, |
|||
"overrideReferences": false, |
|||
"precompiledReferences": [], |
|||
"autoReferenced": true, |
|||
"defineConstraints": [], |
|||
"versionDefines": [], |
|||
"noEngineReferences": false |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 894a6cc6ed5cd2645bb542978cbed6a9 |
|||
AssemblyDefinitionImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 8b14e706b1e7cb044b23837e8a70cad9 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using UnityEditor; |
|||
namespace ParrelSync |
|||
{ |
|||
[InitializeOnLoad] |
|||
public class EditorQuit |
|||
{ |
|||
/// <summary>
|
|||
/// Is editor being closed
|
|||
/// </summary>
|
|||
static public bool IsQuiting { get; private set; } |
|||
static void Quit() |
|||
{ |
|||
IsQuiting = true; |
|||
} |
|||
|
|||
static EditorQuit() |
|||
{ |
|||
IsQuiting = false; |
|||
EditorApplication.quitting += Quit; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: bf2888ff90706904abc2d851c3e59e00 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using UnityEditor; |
|||
using UnityEngine; |
|||
namespace ParrelSync |
|||
{ |
|||
/// <summary>
|
|||
/// For preventing assets being modified from the clone instance.
|
|||
/// </summary>
|
|||
public class ParrelSyncAssetModificationProcessor : UnityEditor.AssetModificationProcessor |
|||
{ |
|||
public static string[] OnWillSaveAssets(string[] paths) |
|||
{ |
|||
if (ClonesManager.IsClone() && Preferences.AssetModPref.Value) |
|||
{ |
|||
if (paths != null && paths.Length > 0 && !EditorQuit.IsQuiting) |
|||
{ |
|||
EditorUtility.DisplayDialog( |
|||
ClonesManager.ProjectName + ": Asset modifications saving detected and blocked", |
|||
"Asset modifications saving are blocked in the clone instance. \n\n" + |
|||
"This is a clone of the original project. \n" + |
|||
"Making changes to asset files via the clone editor is not recommended. \n" + |
|||
"Please use the original editor window if you want to make changes to the project files.", |
|||
"ok" |
|||
); |
|||
foreach (var path in paths) |
|||
{ |
|||
Debug.Log("Attempting to save " + path + " are blocked."); |
|||
} |
|||
} |
|||
return new string[0] { }; |
|||
} |
|||
return paths; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 755e570bd21b39440a923056e60f1450 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using UnityEngine; |
|||
using UnityEditor; |
|||
using System.Linq; |
|||
using System.IO; |
|||
using Debug = UnityEngine.Debug; |
|||
|
|||
namespace ParrelSync |
|||
{ |
|||
/// <summary>
|
|||
/// Contains all required methods for creating a linked clone of the Unity project.
|
|||
/// </summary>
|
|||
public class ClonesManager |
|||
{ |
|||
/// <summary>
|
|||
/// Name used for an identifying file created in the clone project directory.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// (!) Do not change this after the clone was created, because then connection will be lost.
|
|||
/// </remarks>
|
|||
public const string CloneFileName = ".clone"; |
|||
|
|||
/// <summary>
|
|||
/// Suffix added to the end of the project clone name when it is created.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// (!) Do not change this after the clone was created, because then connection will be lost.
|
|||
/// </remarks>
|
|||
public const string CloneNameSuffix = "_clone"; |
|||
|
|||
public const string ProjectName = "ParrelSync"; |
|||
|
|||
/// <summary>
|
|||
/// The maximum number of clones
|
|||
/// </summary>
|
|||
public const int MaxCloneProjectCount = 10; |
|||
|
|||
/// <summary>
|
|||
/// Name of the file for storing clone's argument.
|
|||
/// </summary>
|
|||
public const string ArgumentFileName = ".parrelsyncarg"; |
|||
|
|||
/// <summary>
|
|||
/// Default argument of the new clone
|
|||
/// </summary>
|
|||
public const string DefaultArgument = "client"; |
|||
|
|||
#region Managing clones
|
|||
|
|||
/// <summary>
|
|||
/// Creates clone from the project currently open in Unity Editor.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static Project CreateCloneFromCurrent() |
|||
{ |
|||
if (IsClone()) |
|||
{ |
|||
Debug.LogError("This project is already a clone. Cannot clone it."); |
|||
return null; |
|||
} |
|||
|
|||
string currentProjectPath = ClonesManager.GetCurrentProjectPath(); |
|||
return ClonesManager.CreateCloneFromPath(currentProjectPath); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates clone of the project located at the given path.
|
|||
/// </summary>
|
|||
/// <param name="sourceProjectPath"></param>
|
|||
/// <returns></returns>
|
|||
public static Project CreateCloneFromPath(string sourceProjectPath) |
|||
{ |
|||
Project sourceProject = new Project(sourceProjectPath); |
|||
|
|||
string cloneProjectPath = null; |
|||
|
|||
//Find available clone suffix id
|
|||
for (int i = 0; i < MaxCloneProjectCount; i++) |
|||
{ |
|||
string originalProjectPath = ClonesManager.GetCurrentProject().projectPath; |
|||
string possibleCloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i; |
|||
|
|||
if (!Directory.Exists(possibleCloneProjectPath)) |
|||
{ |
|||
cloneProjectPath = possibleCloneProjectPath; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(cloneProjectPath)) |
|||
{ |
|||
Debug.LogError("The number of cloned projects has reach its limit. Limit: " + MaxCloneProjectCount); |
|||
return null; |
|||
} |
|||
|
|||
Project cloneProject = new Project(cloneProjectPath); |
|||
|
|||
Debug.Log("Start cloning project, original project: " + sourceProject + ", clone project: " + cloneProject); |
|||
|
|||
ClonesManager.CreateProjectFolder(cloneProject); |
|||
|
|||
//Copy Folders
|
|||
Debug.Log("Library copy: " + cloneProject.libraryPath); |
|||
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, cloneProject.libraryPath, |
|||
"Cloning Project Library '" + sourceProject.name + "'. "); |
|||
Debug.Log("Packages copy: " + cloneProject.libraryPath); |
|||
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.packagesPath, cloneProject.packagesPath, |
|||
"Cloning Project Packages '" + sourceProject.name + "'. "); |
|||
|
|||
|
|||
//Link Folders
|
|||
ClonesManager.LinkFolders(sourceProject.assetPath, cloneProject.assetPath); |
|||
ClonesManager.LinkFolders(sourceProject.projectSettingsPath, cloneProject.projectSettingsPath); |
|||
ClonesManager.LinkFolders(sourceProject.autoBuildPath, cloneProject.autoBuildPath); |
|||
ClonesManager.LinkFolders(sourceProject.localPackages, cloneProject.localPackages); |
|||
|
|||
ClonesManager.RegisterClone(cloneProject); |
|||
|
|||
return cloneProject; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Registers a clone by placing an identifying ".clone" file in its root directory.
|
|||
/// </summary>
|
|||
/// <param name="cloneProject"></param>
|
|||
private static void RegisterClone(Project cloneProject) |
|||
{ |
|||
/// Add clone identifier file.
|
|||
string identifierFile = Path.Combine(cloneProject.projectPath, ClonesManager.CloneFileName); |
|||
File.Create(identifierFile).Dispose(); |
|||
|
|||
//Add argument file with default argument
|
|||
string argumentFilePath = Path.Combine(cloneProject.projectPath, ClonesManager.ArgumentFileName); |
|||
File.WriteAllText(argumentFilePath, DefaultArgument, System.Text.Encoding.UTF8); |
|||
|
|||
/// Add collabignore.txt to stop the clone from messing with Unity Collaborate if it's enabled. Just in case.
|
|||
string collabignoreFile = Path.Combine(cloneProject.projectPath, "collabignore.txt"); |
|||
File.WriteAllText(collabignoreFile, "*"); /// Make it ignore ALL files in the clone.
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Opens a project located at the given path (if one exists).
|
|||
/// </summary>
|
|||
/// <param name="projectPath"></param>
|
|||
public static void OpenProject(string projectPath) |
|||
{ |
|||
if (!Directory.Exists(projectPath)) |
|||
{ |
|||
Debug.LogError("Cannot open the project - provided folder (" + projectPath + ") does not exist."); |
|||
return; |
|||
} |
|||
|
|||
if (projectPath == ClonesManager.GetCurrentProjectPath()) |
|||
{ |
|||
Debug.LogError("Cannot open the project - it is already open."); |
|||
return; |
|||
} |
|||
|
|||
string fileName = GetApplicationPath(); |
|||
string args = "-projectPath \"" + projectPath + "\""; |
|||
Debug.Log("Opening project \"" + fileName + " " + args + "\""); |
|||
ClonesManager.StartHiddenConsoleProcess(fileName, args); |
|||
} |
|||
|
|||
private static string GetApplicationPath() |
|||
{ |
|||
switch (Application.platform) |
|||
{ |
|||
case RuntimePlatform.WindowsEditor: |
|||
return EditorApplication.applicationPath; |
|||
case RuntimePlatform.OSXEditor: |
|||
return EditorApplication.applicationPath + "/Contents/MacOS/Unity"; |
|||
case RuntimePlatform.LinuxEditor: |
|||
return EditorApplication.applicationPath; |
|||
default: |
|||
throw new System.NotImplementedException("Platform has not supported yet ;("); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Is this project being opened by an Unity editor?
|
|||
/// </summary>
|
|||
/// <param name="projectPath"></param>
|
|||
/// <returns></returns>
|
|||
public static bool IsCloneProjectRunning(string projectPath) |
|||
{ |
|||
|
|||
//Determine whether it is opened in another instance by checking the UnityLockFile
|
|||
string UnityLockFilePath = new string[] { projectPath, "Temp", "UnityLockfile" } |
|||
.Aggregate(Path.Combine); |
|||
|
|||
switch (Application.platform) |
|||
{ |
|||
case (RuntimePlatform.WindowsEditor): |
|||
//Windows editor will lock "UnityLockfile" file when project is being opened.
|
|||
//Sometime, for instance: windows editor crash, the "UnityLockfile" will not be deleted even the project
|
|||
//isn't being opened, so a check to the "UnityLockfile" lock status may be necessary.
|
|||
if (Preferences.AlsoCheckUnityLockFileStaPref.Value) |
|||
return File.Exists(UnityLockFilePath) && FileUtilities.IsFileLocked(UnityLockFilePath); |
|||
else |
|||
return File.Exists(UnityLockFilePath); |
|||
case (RuntimePlatform.OSXEditor): |
|||
//Mac editor won't lock "UnityLockfile" file when project is being opened
|
|||
return File.Exists(UnityLockFilePath); |
|||
case (RuntimePlatform.LinuxEditor): |
|||
return File.Exists(UnityLockFilePath); |
|||
default: |
|||
throw new System.NotImplementedException("IsCloneProjectRunning: Unsupport Platfrom: " + Application.platform); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Deletes the clone of the currently open project, if such exists.
|
|||
/// </summary>
|
|||
public static void DeleteClone(string cloneProjectPath) |
|||
{ |
|||
/// Clone won't be able to delete itself.
|
|||
if (ClonesManager.IsClone()) return; |
|||
|
|||
///Extra precautions.
|
|||
if (cloneProjectPath == string.Empty) return; |
|||
if (cloneProjectPath == ClonesManager.GetOriginalProjectPath()) return; |
|||
|
|||
//Check what OS is
|
|||
string identifierFile; |
|||
string args; |
|||
switch (Application.platform) |
|||
{ |
|||
case (RuntimePlatform.WindowsEditor): |
|||
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); |
|||
|
|||
//The argument file will be deleted first at the beginning of the project deletion process
|
|||
//to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
|
|||
//If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
|
|||
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); |
|||
File.Delete(identifierFile); |
|||
|
|||
args = "/c " + @"rmdir /s/q " + string.Format("\"{0}\"", cloneProjectPath); |
|||
StartHiddenConsoleProcess("cmd.exe", args); |
|||
|
|||
break; |
|||
case (RuntimePlatform.OSXEditor): |
|||
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); |
|||
|
|||
//The argument file will be deleted first at the beginning of the project deletion process
|
|||
//to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
|
|||
//If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
|
|||
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); |
|||
File.Delete(identifierFile); |
|||
|
|||
FileUtil.DeleteFileOrDirectory(cloneProjectPath); |
|||
|
|||
break; |
|||
case (RuntimePlatform.LinuxEditor): |
|||
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); |
|||
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); |
|||
File.Delete(identifierFile); |
|||
|
|||
FileUtil.DeleteFileOrDirectory(cloneProjectPath); |
|||
|
|||
break; |
|||
default: |
|||
Debug.LogWarning("Not in a known editor. Where are you!?"); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
#region Creating project folders
|
|||
|
|||
/// <summary>
|
|||
/// Creates an empty folder using data in the given Project object
|
|||
/// </summary>
|
|||
/// <param name="project"></param>
|
|||
public static void CreateProjectFolder(Project project) |
|||
{ |
|||
string path = project.projectPath; |
|||
Debug.Log("Creating new empty folder at: " + path); |
|||
Directory.CreateDirectory(path); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copies the full contents of the unity library. We want to do this to avoid the lengthy re-serialization of the whole project when it opens up the clone.
|
|||
/// </summary>
|
|||
/// <param name="sourceProject"></param>
|
|||
/// <param name="destinationProject"></param>
|
|||
[System.Obsolete] |
|||
public static void CopyLibraryFolder(Project sourceProject, Project destinationProject) |
|||
{ |
|||
if (Directory.Exists(destinationProject.libraryPath)) |
|||
{ |
|||
Debug.LogWarning("Library copy: destination path already exists! "); |
|||
return; |
|||
} |
|||
|
|||
Debug.Log("Library copy: " + destinationProject.libraryPath); |
|||
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, destinationProject.libraryPath, |
|||
"Cloning project '" + sourceProject.name + "'. "); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
#region Creating symlinks
|
|||
|
|||
/// <summary>
|
|||
/// Creates a symlink between destinationPath and sourcePath (Mac version).
|
|||
/// </summary>
|
|||
/// <param name="sourcePath"></param>
|
|||
/// <param name="destinationPath"></param>
|
|||
private static void CreateLinkMac(string sourcePath, string destinationPath) |
|||
{ |
|||
sourcePath = sourcePath.Replace(" ", "\\ "); |
|||
destinationPath = destinationPath.Replace(" ", "\\ "); |
|||
var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath); |
|||
|
|||
Debug.Log("Mac hard link " + command); |
|||
|
|||
ClonesManager.ExecuteBashCommand(command); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a symlink between destinationPath and sourcePath (Linux version).
|
|||
/// </summary>
|
|||
/// <param name="sourcePath"></param>
|
|||
/// <param name="destinationPath"></param>
|
|||
private static void CreateLinkLinux(string sourcePath, string destinationPath) |
|||
{ |
|||
sourcePath = sourcePath.Replace(" ", "\\ "); |
|||
destinationPath = destinationPath.Replace(" ", "\\ "); |
|||
var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath); |
|||
|
|||
Debug.Log("Linux Symlink " + command); |
|||
|
|||
ClonesManager.ExecuteBashCommand(command); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a symlink between destinationPath and sourcePath (Windows version).
|
|||
/// </summary>
|
|||
/// <param name="sourcePath"></param>
|
|||
/// <param name="destinationPath"></param>
|
|||
private static void CreateLinkWin(string sourcePath, string destinationPath) |
|||
{ |
|||
string cmd = "/C mklink /J " + string.Format("\"{0}\" \"{1}\"", destinationPath, sourcePath); |
|||
Debug.Log("Windows junction: " + cmd); |
|||
ClonesManager.StartHiddenConsoleProcess("cmd.exe", cmd); |
|||
} |
|||
|
|||
//TODO(?) avoid terminal calls and use proper api stuff. See below for windows!
|
|||
////https://docs.microsoft.com/en-us/windows/desktop/api/ioapiset/nf-ioapiset-deviceiocontrol
|
|||
//[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|||
//private static extern bool DeviceIoControl(System.IntPtr hDevice, uint dwIoControlCode,
|
|||
// System.IntPtr InBuffer, int nInBufferSize,
|
|||
// System.IntPtr OutBuffer, int nOutBufferSize,
|
|||
// out int pBytesReturned, System.IntPtr lpOverlapped);
|
|||
|
|||
/// <summary>
|
|||
/// Create a link / junction from the original project to it's clone.
|
|||
/// </summary>
|
|||
/// <param name="sourcePath"></param>
|
|||
/// <param name="destinationPath"></param>
|
|||
public static void LinkFolders(string sourcePath, string destinationPath) |
|||
{ |
|||
if ((Directory.Exists(destinationPath) == false) && (Directory.Exists(sourcePath) == true)) |
|||
{ |
|||
switch (Application.platform) |
|||
{ |
|||
case (RuntimePlatform.WindowsEditor): |
|||
CreateLinkWin(sourcePath, destinationPath); |
|||
break; |
|||
case (RuntimePlatform.OSXEditor): |
|||
CreateLinkMac(sourcePath, destinationPath); |
|||
break; |
|||
case (RuntimePlatform.LinuxEditor): |
|||
CreateLinkLinux(sourcePath, destinationPath); |
|||
break; |
|||
default: |
|||
Debug.LogWarning("Not in a known editor. Application.platform: " + Application.platform); |
|||
break; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Debug.LogWarning("Skipping Asset link, it already exists: " + destinationPath); |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
#region Utility methods
|
|||
|
|||
private static bool? isCloneFileExistCache = null; |
|||
|
|||
/// <summary>
|
|||
/// Returns true if the project currently open in Unity Editor is a clone.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static bool IsClone() |
|||
{ |
|||
if (isCloneFileExistCache == null) |
|||
{ |
|||
/// The project is a clone if its root directory contains an empty file named ".clone".
|
|||
string cloneFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.CloneFileName); |
|||
isCloneFileExistCache = File.Exists(cloneFilePath); |
|||
} |
|||
|
|||
return (bool)isCloneFileExistCache; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the path to the current unityEditor project folder's info
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static string GetCurrentProjectPath() |
|||
{ |
|||
return Application.dataPath.Replace("/Assets", ""); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Return a project object that describes all the paths we need to clone it.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static Project GetCurrentProject() |
|||
{ |
|||
string pathString = ClonesManager.GetCurrentProjectPath(); |
|||
return new Project(pathString); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the argument of this clone project.
|
|||
/// If this is the original project, will return an empty string.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static string GetArgument() |
|||
{ |
|||
string argument = ""; |
|||
if (IsClone()) |
|||
{ |
|||
string argumentFilePath = Path.Combine(GetCurrentProjectPath(), ClonesManager.ArgumentFileName); |
|||
if (File.Exists(argumentFilePath)) |
|||
{ |
|||
argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8); |
|||
} |
|||
} |
|||
|
|||
return argument; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the path to the original project.
|
|||
/// If currently open project is the original, returns its own path.
|
|||
/// If the original project folder cannot be found, retuns an empty string.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static string GetOriginalProjectPath() |
|||
{ |
|||
if (IsClone()) |
|||
{ |
|||
/// If this is a clone...
|
|||
/// Original project path can be deduced by removing the suffix from the clone's path.
|
|||
string cloneProjectPath = ClonesManager.GetCurrentProject().projectPath; |
|||
|
|||
int index = cloneProjectPath.LastIndexOf(ClonesManager.CloneNameSuffix); |
|||
if (index > 0) |
|||
{ |
|||
string originalProjectPath = cloneProjectPath.Substring(0, index); |
|||
if (Directory.Exists(originalProjectPath)) return originalProjectPath; |
|||
} |
|||
|
|||
return string.Empty; |
|||
} |
|||
else |
|||
{ |
|||
/// If this is the original, we return its own path.
|
|||
return ClonesManager.GetCurrentProjectPath(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns all clone projects path.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static List<string> GetCloneProjectsPath() |
|||
{ |
|||
List<string> projectsPath = new List<string>(); |
|||
for (int i = 0; i < MaxCloneProjectCount; i++) |
|||
{ |
|||
string originalProjectPath = ClonesManager.GetCurrentProject().projectPath; |
|||
string cloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i; |
|||
|
|||
if (Directory.Exists(cloneProjectPath)) |
|||
projectsPath.Add(cloneProjectPath); |
|||
} |
|||
|
|||
return projectsPath; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
|
|||
/// </summary>
|
|||
/// <param name="source">Directory to be copied.</param>
|
|||
/// <param name="destination">Destination directory (created automatically if needed).</param>
|
|||
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
|
|||
public static void CopyDirectoryWithProgressBar(string sourcePath, string destinationPath, |
|||
string progressBarPrefix = "") |
|||
{ |
|||
var source = new DirectoryInfo(sourcePath); |
|||
var destination = new DirectoryInfo(destinationPath); |
|||
|
|||
long totalBytes = 0; |
|||
long copiedBytes = 0; |
|||
|
|||
ClonesManager.CopyDirectoryWithProgressBarRecursive(source, destination, ref totalBytes, ref copiedBytes, |
|||
progressBarPrefix); |
|||
EditorUtility.ClearProgressBar(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
|
|||
/// Same as the previous method, but uses recursion to copy all nested folders as well.
|
|||
/// </summary>
|
|||
/// <param name="source">Directory to be copied.</param>
|
|||
/// <param name="destination">Destination directory (created automatically if needed).</param>
|
|||
/// <param name="totalBytes">Total bytes to be copied. Calculated automatically, initialize at 0.</param>
|
|||
/// <param name="copiedBytes">To track already copied bytes. Calculated automatically, initialize at 0.</param>
|
|||
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
|
|||
private static void CopyDirectoryWithProgressBarRecursive(DirectoryInfo source, DirectoryInfo destination, |
|||
ref long totalBytes, ref long copiedBytes, string progressBarPrefix = "") |
|||
{ |
|||
/// Directory cannot be copied into itself.
|
|||
if (source.FullName.ToLower() == destination.FullName.ToLower()) |
|||
{ |
|||
Debug.LogError("Cannot copy directory into itself."); |
|||
return; |
|||
} |
|||
|
|||
/// Calculate total bytes, if required.
|
|||
if (totalBytes == 0) |
|||
{ |
|||
totalBytes = ClonesManager.GetDirectorySize(source, true, progressBarPrefix); |
|||
} |
|||
|
|||
/// Create destination directory, if required.
|
|||
if (!Directory.Exists(destination.FullName)) |
|||
{ |
|||
Directory.CreateDirectory(destination.FullName); |
|||
} |
|||
|
|||
/// Copy all files from the source.
|
|||
foreach (FileInfo file in source.GetFiles()) |
|||
{ |
|||
try |
|||
{ |
|||
file.CopyTo(Path.Combine(destination.ToString(), file.Name), true); |
|||
} |
|||
catch (IOException) |
|||
{ |
|||
/// Some files may throw IOException if they are currently open in Unity editor.
|
|||
/// Just ignore them in such case.
|
|||
} |
|||
|
|||
/// Account the copied file size.
|
|||
copiedBytes += file.Length; |
|||
|
|||
/// Display the progress bar.
|
|||
float progress = (float)copiedBytes / (float)totalBytes; |
|||
bool cancelCopy = EditorUtility.DisplayCancelableProgressBar( |
|||
progressBarPrefix + "Copying '" + source.FullName + "' to '" + destination.FullName + "'...", |
|||
"(" + (progress * 100f).ToString("F2") + "%) Copying file '" + file.Name + "'...", |
|||
progress); |
|||
if (cancelCopy) return; |
|||
} |
|||
|
|||
/// Copy all nested directories from the source.
|
|||
foreach (DirectoryInfo sourceNestedDir in source.GetDirectories()) |
|||
{ |
|||
DirectoryInfo nextDestingationNestedDir = destination.CreateSubdirectory(sourceNestedDir.Name); |
|||
ClonesManager.CopyDirectoryWithProgressBarRecursive(sourceNestedDir, nextDestingationNestedDir, |
|||
ref totalBytes, ref copiedBytes, progressBarPrefix); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates the size of the given directory. Displays a progress bar.
|
|||
/// </summary>
|
|||
/// <param name="directory">Directory, which size has to be calculated.</param>
|
|||
/// <param name="includeNested">If true, size will include all nested directories.</param>
|
|||
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
|
|||
/// <returns>Size of the directory in bytes.</returns>
|
|||
private static long GetDirectorySize(DirectoryInfo directory, bool includeNested = false, |
|||
string progressBarPrefix = "") |
|||
{ |
|||
EditorUtility.DisplayProgressBar(progressBarPrefix + "Calculating size of directories...", |
|||
"Scanning '" + directory.FullName + "'...", 0f); |
|||
|
|||
/// Calculate size of all files in directory.
|
|||
long filesSize = directory.GetFiles().Sum((FileInfo file) => file.Length); |
|||
|
|||
/// Calculate size of all nested directories.
|
|||
long directoriesSize = 0; |
|||
if (includeNested) |
|||
{ |
|||
IEnumerable<DirectoryInfo> nestedDirectories = directory.GetDirectories(); |
|||
foreach (DirectoryInfo nestedDir in nestedDirectories) |
|||
{ |
|||
directoriesSize += ClonesManager.GetDirectorySize(nestedDir, true, progressBarPrefix); |
|||
} |
|||
} |
|||
|
|||
return filesSize + directoriesSize; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Starts process in the system console, taking the given fileName and args.
|
|||
/// </summary>
|
|||
/// <param name="fileName"></param>
|
|||
/// <param name="args"></param>
|
|||
private static void StartHiddenConsoleProcess(string fileName, string args) |
|||
{ |
|||
System.Diagnostics.Process.Start(fileName, args); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Thanks to https://github.com/karl-/unity-symlink-utility/blob/master/SymlinkUtility.cs
|
|||
/// </summary>
|
|||
/// <param name="command"></param>
|
|||
private static void ExecuteBashCommand(string command) |
|||
{ |
|||
command = command.Replace("\"", "\"\""); |
|||
|
|||
var proc = new Process() |
|||
{ |
|||
StartInfo = new ProcessStartInfo |
|||
{ |
|||
FileName = "/bin/bash", |
|||
Arguments = "-c \"" + command + "\"", |
|||
UseShellExecute = false, |
|||
RedirectStandardOutput = true, |
|||
RedirectStandardError = true, |
|||
CreateNoWindow = true |
|||
} |
|||
}; |
|||
|
|||
using (proc) |
|||
{ |
|||
proc.Start(); |
|||
proc.WaitForExit(); |
|||
|
|||
if (!proc.StandardError.EndOfStream) |
|||
{ |
|||
UnityEngine.Debug.LogError(proc.StandardError.ReadToEnd()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static void OpenProjectInFileExplorer(string path) |
|||
{ |
|||
System.Diagnostics.Process.Start(@path); |
|||
} |
|||
#endregion
|
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 6148e48ed6b61d748b187d06d3687b83 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: a041d83486c20b84bbf5077ddfbbca37 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
namespace ParrelSync |
|||
{ |
|||
public class ExternalLinks |
|||
{ |
|||
public const string RemoteVersionURL = "https://raw.githubusercontent.com/VeriorPies/ParrelSync/master/VERSION.txt"; |
|||
public const string Releases = "https://github.com/VeriorPies/ParrelSync/releases"; |
|||
public const string CustomArgumentHelpLink = "https://github.com/VeriorPies/ParrelSync/wiki/Argument"; |
|||
|
|||
public const string GitHubHome = "https://github.com/VeriorPies/ParrelSync/"; |
|||
public const string GitHubIssue = "https://github.com/VeriorPies/ParrelSync/issues"; |
|||
public const string FAQ = "https://github.com/VeriorPies/ParrelSync/wiki/Troubleshooting-&-FAQs"; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 65daf17fbe5101b41977305639f30c65 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
部分文件因为文件数量过多而无法显示
撰写
预览
正在加载...
取消
保存
Reference in new issue