浏览代码

Added Parrelsync and updated Service intializastion to use profiles based on what clone we are in.

/main/staging/2021_Upgrade/Async_Refactor
当前提交
30f2fd8b
共有 45 个文件被更改,包括 1832 次插入17 次删除
  1. 5
      Assets/Scripts/GameLobby/Auth/Identity.cs
  2. 13
      Assets/Scripts/GameLobby/Auth/SubIdentity_Authentication.cs
  3. 11
      Assets/Scripts/GameLobby/Game/GameManager.cs
  4. 8
      Assets/Scripts/GameLobby/LobbyRelaySample.asmdef
  5. 8
      Assets/Scripts/GameLobby/Tests/PlayMode/LobbyRoundtripTests.cs
  6. 4
      Assets/Scripts/GameLobby/Tests/PlayMode/RelayRoundTripTests.cs
  7. 3
      Assets/Scripts/GameLobby/Tests/PlayMode/Tests.Play.asmdef
  8. 11
      Assets/Scripts/GameLobby/Tests/PlayMode/UtpTests.cs
  9. 3
      Packages/ParrelSync.meta
  10. 167
      ProjectSettings/SceneTemplateSettings.json
  11. 8
      Packages/ParrelSync/Editor.meta
  12. 8
      Packages/ParrelSync/Editor/AssetModBlock.meta
  13. 22
      Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs
  14. 11
      Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta
  15. 34
      Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs
  16. 11
      Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta
  17. 664
      Packages/ParrelSync/Editor/ClonesManager.cs
  18. 11
      Packages/ParrelSync/Editor/ClonesManager.cs.meta
  19. 198
      Packages/ParrelSync/Editor/ClonesManagerWindow.cs
  20. 11
      Packages/ParrelSync/Editor/ClonesManagerWindow.cs.meta
  21. 13
      Packages/ParrelSync/Editor/ExternalLinks.cs
  22. 11
      Packages/ParrelSync/Editor/ExternalLinks.cs.meta
  23. 31
      Packages/ParrelSync/Editor/FileUtilities.cs
  24. 11
      Packages/ParrelSync/Editor/FileUtilities.cs.meta
  25. 8
      Packages/ParrelSync/Editor/NonCore.meta
  26. 78
      Packages/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs
  27. 11
      Packages/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta
  28. 26
      Packages/ParrelSync/Editor/NonCore/OtherMenuItem.cs
  29. 11
      Packages/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta
  30. 110
      Packages/ParrelSync/Editor/Preferences.cs
  31. 11
      Packages/ParrelSync/Editor/Preferences.cs.meta
  32. 112
      Packages/ParrelSync/Editor/Project.cs
  33. 11
      Packages/ParrelSync/Editor/Project.cs.meta
  34. 60
      Packages/ParrelSync/Editor/UpdateChecker.cs
  35. 11
      Packages/ParrelSync/Editor/UpdateChecker.cs.meta
  36. 73
      Packages/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs
  37. 11
      Packages/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta
  38. 10
      Packages/ParrelSync/package.json
  39. 7
      Packages/ParrelSync/package.json.meta
  40. 15
      Packages/ParrelSync/projectCloner.asmdef
  41. 7
      Packages/ParrelSync/projectCloner.asmdef.meta

5
Assets/Scripts/GameLobby/Auth/Identity.cs


/// <summary>
/// Our internal representation of the local player's credentials, wrapping the data required for interfacing with the identities of that player in the services.
/// (In use here, it just wraps Auth, but it can be used to combine multiple sets of credentials into one concept of a player.)
/// (This callback pattern also allows for Unit Testing, something UnityServices does support easily.)
public Identity(Action callbackOnAuthLogin)
public Identity(string profileName, Action callbackOnAuthLogin)
m_subIdentities.Add(IIdentityType.Auth, new SubIdentity_Authentication(callbackOnAuthLogin));
m_subIdentities.Add(IIdentityType.Auth, new SubIdentity_Authentication(profileName, callbackOnAuthLogin));
}
public SubIdentity GetSubIdentity(IIdentityType identityType)

13
Assets/Scripts/GameLobby/Auth/SubIdentity_Authentication.cs


using System;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Core;

/// <summary>
/// This will kick off a login.
/// </summary>
public SubIdentity_Authentication(Action onSigninComplete = null)
public SubIdentity_Authentication(string profileName = "default", Action onSigninComplete = null)
DoSignIn(onSigninComplete);
#pragma warning disable 4014
DoSignIn(profileName, onSigninComplete);
#pragma warning restore 4014
}
~SubIdentity_Authentication()
{

}
}
private async void DoSignIn(Action onSigninComplete)
private async Task DoSignIn(string profileName, Action onSigninComplete)
await UnityServices.InitializeAsync();
var serviceProfile = new InitializationOptions();
serviceProfile.SetProfile(profileName);
await UnityServices.InitializeAsync(serviceProfile);
AuthenticationService.Instance.SignedIn += OnSignInChange;
AuthenticationService.Instance.SignedOut += OnSignInChange;

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


using LobbyRelaySample.lobby;
using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using ParrelSync;
#endif
namespace LobbyRelaySample
{

#pragma warning disable IDE0059 // Unnecessary assignment of a value
var unused = Locator.Get;
#pragma warning restore IDE0059
Locator.Get.Provide(new Auth.Identity(OnAuthSignIn));
string serviceProfileName = "player";
#if UNITY_EDITOR
serviceProfileName = $"{serviceProfileName}_{ClonesManager.GetCurrentProject().name}";
#endif
Locator.Get.Provide(new Auth.Identity(serviceProfileName,OnAuthSignIn));
Application.wantsToQuit += OnWantToQuit;
}

8
Assets/Scripts/GameLobby/LobbyRelaySample.asmdef


"GUID:f2d49d9fa7e7eb3418e39723a7d3b92f",
"GUID:6087a74f6015aae4daed9a2577a7596c",
"GUID:4ebbcb26024b547159a86c39de1a8fa5",
"GUID:3bf5041814073ec4089849c425919d5a",
"GUID:3b8ed52f1b5c64994af4c4e0aa4b6c4b"
"GUID:3b8ed52f1b5c64994af4c4e0aa4b6c4b",
"GUID:894a6cc6ed5cd2645bb542978cbed6a9"
"precompiledReferences": [],
"precompiledReferences": [
""
],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],

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


[OneTimeSetUp]
public void Setup()
{
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication(() => { m_didSigninComplete = true; });
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication("testProfile", () => { m_didSigninComplete = true; });
m_mockUserData = new Dictionary<string, PlayerDataObject>();
m_mockUserData.Add("DisplayName", new PlayerDataObject(PlayerDataObject.VisibilityOptions.Member, "TestUser123"));
m_LocalUser = new LobbyUser(true);

LogAssert.ignoreFailingMessages = false;
Assert.Null(createLobby, "The returned object will be null, so expect to need to handle it.");
}
[UnityTest]
public IEnumerator LobbyPlayerDataUpdated()
{
yield return null;
}
}
}

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


using System;
using System.Collections;
using LobbyRelaySample;
using LobbyRelaySample.relay;
using NUnit.Framework;
using Test.Tools;
using Unity.Services.Relay;

[OneTimeSetUp]
public void Setup()
{
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication(() => { m_didSigninComplete = true; });
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication("testProfile", () => { m_didSigninComplete = true; });
}
[OneTimeTearDown]

3
Assets/Scripts/GameLobby/Tests/PlayMode/Tests.Play.asmdef


"LobbyRelaySample",
"Unity.Services.Lobbies",
"Unity.Services.Relay",
"Unity.Networking.Transport"
"Unity.Networking.Transport",
"Unity.Services.Core"
],
"includePlatforms": [],
"excludePlatforms": [],

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


using System;
using System.Collections;
using System.Threading.Tasks;
using Unity.Services.Core;
using Unity.Services.Relay.Models;
using UnityEngine;
using UnityEngine.TestTools;

public void Setup()
{
m_dummy = new GameObject();
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication(() => { m_didSigninComplete = true; });
m_auth = new LobbyRelaySample.Auth.SubIdentity_Authentication("TestProfile",() => { m_didSigninComplete = true; });
}
async Task InitServices()
{
await UnityServices.InitializeAsync();
m_didSigninComplete = true;
}
[OneTimeTearDown]

3
Packages/ParrelSync.meta


fileFormatVersion: 2
guid: 520a1688a3c84604b0e7836ecc15cee3
timeCreated: 1654698051

167
ProjectSettings/SceneTemplateSettings.json


{
"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
}

8
Packages/ParrelSync/Editor.meta


fileFormatVersion: 2
guid: a31ea7d0315594440839cdb0db6bc411
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Packages/ParrelSync/Editor/AssetModBlock.meta


fileFormatVersion: 2
guid: 8b14e706b1e7cb044b23837e8a70cad9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

22
Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs


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;
}
}
}

11
Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta


fileFormatVersion: 2
guid: bf2888ff90706904abc2d851c3e59e00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

34
Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs


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;
}
}
}

11
Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta


fileFormatVersion: 2
guid: 755e570bd21b39440a923056e60f1450
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

664
Packages/ParrelSync/Editor/ClonesManager.cs


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
}
}

11
Packages/ParrelSync/Editor/ClonesManager.cs.meta


fileFormatVersion: 2
guid: 6148e48ed6b61d748b187d06d3687b83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

198
Packages/ParrelSync/Editor/ClonesManagerWindow.cs


using UnityEngine;
using UnityEditor;
using System.IO;
namespace ParrelSync
{
/// <summary>
///Clones manager Unity editor window
/// </summary>
public class ClonesManagerWindow : EditorWindow
{
/// <summary>
/// Returns true if project clone exists.
/// </summary>
public bool isCloneCreated
{
get { return ClonesManager.GetCloneProjectsPath().Count >= 1; }
}
[MenuItem("ParrelSync/Clones Manager", priority = 0)]
private static void InitWindow()
{
ClonesManagerWindow window = (ClonesManagerWindow)EditorWindow.GetWindow(typeof(ClonesManagerWindow));
window.titleContent = new GUIContent("Clones Manager");
window.Show();
}
/// <summary>
/// For storing the scroll position of clones list
/// </summary>
Vector2 clonesScrollPos;
private void OnGUI()
{
/// If it is a clone project...
if (ClonesManager.IsClone())
{
//Find out the original project name and show the help box
string originalProjectPath = ClonesManager.GetOriginalProjectPath();
if (originalProjectPath == string.Empty)
{
/// If original project cannot be found, display warning message.
EditorGUILayout.HelpBox(
"This project is a clone, but the link to the original seems lost.\nYou have to manually open the original and create a new clone instead of this one.\n",
MessageType.Warning);
}
else
{
/// If original project is present, display some usage info.
EditorGUILayout.HelpBox(
"This project is a clone of the project '" + Path.GetFileName(originalProjectPath) + "'.\nIf you want to make changes the project files or manage clones, please open the original project through Unity Hub.",
MessageType.Info);
}
//Clone project custom argument.
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Arguments", GUILayout.Width(70));
if (GUILayout.Button("?", GUILayout.Width(20)))
{
Application.OpenURL(ExternalLinks.CustomArgumentHelpLink);
}
GUILayout.EndHorizontal();
string argumentFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.ArgumentFileName);
//Need to be careful with file reading / writing since it will effect the deletion of
// the clone project(The directory won't be fully deleted if there's still file inside being read or write).
//The argument file will be deleted first at the beginning of the project deletion process
//to prevent any further being read and write.
//Will need to take some extra cautious if want to change the design of how file editing is handled.
if (File.Exists(argumentFilePath))
{
string argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
string argumentTextAreaInput = EditorGUILayout.TextArea(argument,
GUILayout.Height(50),
GUILayout.MaxWidth(300)
);
File.WriteAllText(argumentFilePath, argumentTextAreaInput, System.Text.Encoding.UTF8);
}
else
{
EditorGUILayout.LabelField("No argument file found.");
}
}
else// If it is an original project...
{
if (isCloneCreated)
{
GUILayout.BeginVertical("HelpBox");
GUILayout.Label("Clones of this Project");
//List all clones
clonesScrollPos =
EditorGUILayout.BeginScrollView(clonesScrollPos);
var cloneProjectsPath = ClonesManager.GetCloneProjectsPath();
for (int i = 0; i < cloneProjectsPath.Count; i++)
{
GUILayout.BeginVertical("GroupBox");
string cloneProjectPath = cloneProjectsPath[i];
bool isOpenInAnotherInstance = ClonesManager.IsCloneProjectRunning(cloneProjectPath);
if (isOpenInAnotherInstance == true)
EditorGUILayout.LabelField("Clone " + i + " (Running)", EditorStyles.boldLabel);
else
EditorGUILayout.LabelField("Clone " + i);
GUILayout.BeginHorizontal();
EditorGUILayout.TextField("Clone project path", cloneProjectPath, EditorStyles.textField);
if (GUILayout.Button("View Folder", GUILayout.Width(80)))
{
ClonesManager.OpenProjectInFileExplorer(cloneProjectPath);
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Arguments", GUILayout.Width(70));
if (GUILayout.Button("?", GUILayout.Width(20)))
{
Application.OpenURL(ExternalLinks.CustomArgumentHelpLink);
}
GUILayout.EndHorizontal();
string argumentFilePath = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
//Need to be careful with file reading/writing since it will effect the deletion of
//the clone project(The directory won't be fully deleted if there's still file inside being read or write).
//The argument file will be deleted first at the beginning of the project deletion process
//to prevent any further being read and write.
//Will need to take some extra cautious if want to change the design of how file editing is handled.
if (File.Exists(argumentFilePath))
{
string argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
string argumentTextAreaInput = EditorGUILayout.TextArea(argument,
GUILayout.Height(50),
GUILayout.MaxWidth(300)
);
File.WriteAllText(argumentFilePath, argumentTextAreaInput, System.Text.Encoding.UTF8);
}
else
{
EditorGUILayout.LabelField("No argument file found.");
}
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUI.BeginDisabledGroup(isOpenInAnotherInstance);
if (GUILayout.Button("Open in New Editor"))
{
ClonesManager.OpenProject(cloneProjectPath);
}
GUILayout.BeginHorizontal();
if (GUILayout.Button("Delete"))
{
bool delete = EditorUtility.DisplayDialog(
"Delete the clone?",
"Are you sure you want to delete the clone project '" + ClonesManager.GetCurrentProject().name + "_clone'?",
"Delete",
"Cancel");
if (delete)
{
ClonesManager.DeleteClone(cloneProjectPath);
}
}
GUILayout.EndHorizontal();
EditorGUI.EndDisabledGroup();
GUILayout.EndVertical();
}
EditorGUILayout.EndScrollView();
if (GUILayout.Button("Add new clone"))
{
ClonesManager.CreateCloneFromCurrent();
}
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
}
else
{
/// If no clone created yet, we must create it.
EditorGUILayout.HelpBox("No project clones found. Create a new one!", MessageType.Info);
if (GUILayout.Button("Create new clone"))
{
ClonesManager.CreateCloneFromCurrent();
}
}
}
}
}
}

11
Packages/ParrelSync/Editor/ClonesManagerWindow.cs.meta


fileFormatVersion: 2
guid: a041d83486c20b84bbf5077ddfbbca37
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

13
Packages/ParrelSync/Editor/ExternalLinks.cs


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";
}
}

11
Packages/ParrelSync/Editor/ExternalLinks.cs.meta


fileFormatVersion: 2
guid: 65daf17fbe5101b41977305639f30c65
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

31
Packages/ParrelSync/Editor/FileUtilities.cs


using System.IO;
using UnityEngine;
namespace ParrelSync
{
public class FileUtilities : MonoBehaviour
{
public static bool IsFileLocked(string path)
{
FileInfo file = new FileInfo(path);
try
{
using (FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None))
{
stream.Close();
}
}
catch (IOException)
{
//the file is unavailable because it is:
//still being written to
//or being processed by another thread
//or does not exist (has already been processed)
return true;
}
//file is not locked
return false;
}
}
}

11
Packages/ParrelSync/Editor/FileUtilities.cs.meta


fileFormatVersion: 2
guid: 11fdc6f78f8c965499a870ca06dca6bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
Packages/ParrelSync/Editor/NonCore.meta


fileFormatVersion: 2
guid: 74a7aa389726f964ab34c52e208c2a43
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

78
Packages/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs


namespace ParrelSync.NonCore
{
using UnityEditor;
using UnityEngine;
/// <summary>
/// A simple script to display feedback/star dialog after certain time of project being opened/re-compiled.
/// Will only pop-up once unless "Remind me next time" are chosen.
/// Removing this file from project wont effect any other functions.
/// </summary>
[InitializeOnLoad]
public class AskFeedbackDialog
{
const string InitializeOnLoadCountKey = "ParrelSync_InitOnLoadCount", StopShowingKey = "ParrelSync_StopShowFeedBack";
static AskFeedbackDialog()
{
if (EditorPrefs.HasKey(StopShowingKey)) { return; }
int InitializeOnLoadCount = EditorPrefs.GetInt(InitializeOnLoadCountKey, 0);
if (InitializeOnLoadCount > 20)
{
ShowDialog();
}
else
{
EditorPrefs.SetInt(InitializeOnLoadCountKey, InitializeOnLoadCount + 1);
}
}
//[MenuItem("ParrelSync/(Debug)Show AskFeedbackDialog ")]
private static void ShowDialog()
{
int option = EditorUtility.DisplayDialogComplex("Do you like " + ParrelSync.ClonesManager.ProjectName + "?",
"Do you like " + ParrelSync.ClonesManager.ProjectName + "?\n" +
"If so, please don't hesitate to star it on GitHub and contribute to the project!",
"Star on GitHub",
"Close",
"Remind me next time"
);
switch (option)
{
// First parameter.
case 0:
Debug.Log("AskFeedbackDialog: Star on GitHub selected");
EditorPrefs.SetBool(StopShowingKey, true);
EditorPrefs.DeleteKey(InitializeOnLoadCountKey);
Application.OpenURL(ExternalLinks.GitHubHome);
break;
// Second parameter.
case 1:
Debug.Log("AskFeedbackDialog: Close and never show again.");
EditorPrefs.SetBool(StopShowingKey, true);
EditorPrefs.DeleteKey(InitializeOnLoadCountKey);
break;
// Third parameter.
case 2:
Debug.Log("AskFeedbackDialog: Remind me next time");
EditorPrefs.SetInt(InitializeOnLoadCountKey, 0);
break;
default:
//Debug.Log("Close windows.");
break;
}
}
///// <summary>
///// For debug purpose
///// </summary>
//[MenuItem("ParrelSync/(Debug)Delete AskFeedbackDialog keys")]
//private static void DebugDeleteAllKeys()
//{
// EditorPrefs.DeleteKey(InitializeOnLoadCountKey);
// EditorPrefs.DeleteKey(StopShowingKey);
// Debug.Log("AskFeedbackDialog keys deleted");
//}
}
}

11
Packages/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta


fileFormatVersion: 2
guid: 894412a5b602e6c4ba2cf2d01f4f92b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

26
Packages/ParrelSync/Editor/NonCore/OtherMenuItem.cs


namespace ParrelSync.NonCore
{
using UnityEditor;
using UnityEngine;
public class OtherMenuItem
{
[MenuItem("ParrelSync/GitHub/View this project on GitHub", priority = 10)]
private static void OpenGitHub()
{
Application.OpenURL(ExternalLinks.GitHubHome);
}
[MenuItem("ParrelSync/GitHub/View FAQ", priority = 11)]
private static void OpenFAQ()
{
Application.OpenURL(ExternalLinks.FAQ);
}
[MenuItem("ParrelSync/GitHub/View Issues", priority = 12)]
private static void OpenGitHubIssues()
{
Application.OpenURL(ExternalLinks.GitHubIssue);
}
}
}

11
Packages/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta


fileFormatVersion: 2
guid: 7191fa4bfa12ae749b27f73ed292eaf1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

110
Packages/ParrelSync/Editor/Preferences.cs


using UnityEngine;
using UnityEditor;
namespace ParrelSync
{
/// <summary>
/// To add value caching for <see cref="EditorPrefs"/> functions
/// </summary>
public class BoolPreference
{
public string key { get; private set; }
public bool defaultValue { get; private set; }
public BoolPreference(string key, bool defaultValue)
{
this.key = key;
this.defaultValue = defaultValue;
}
private bool? valueCache = null;
public bool Value
{
get
{
if (valueCache == null)
valueCache = EditorPrefs.GetBool(key, defaultValue);
return (bool)valueCache;
}
set
{
if (valueCache == value)
return;
EditorPrefs.SetBool(key, value);
valueCache = value;
Debug.Log("Editor preference updated. key: " + key + ", value: " + value);