diff --git a/Assets/Plugins/ParrelSync.meta b/Assets/Plugins/ParrelSync.meta
new file mode 100644
index 0000000..82fb126
--- /dev/null
+++ b/Assets/Plugins/ParrelSync.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 711bbd1e36ca42a4bad871eb6a3de1bc
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor.meta b/Assets/Plugins/ParrelSync/Editor.meta
new file mode 100644
index 0000000..56fd131
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a31ea7d0315594440839cdb0db6bc411
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/AssetModBlock.meta b/Assets/Plugins/ParrelSync/Editor/AssetModBlock.meta
new file mode 100644
index 0000000..3bb4f70
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/AssetModBlock.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8b14e706b1e7cb044b23837e8a70cad9
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/AssetModBlock/EditorQuit.cs b/Assets/Plugins/ParrelSync/Editor/AssetModBlock/EditorQuit.cs
new file mode 100644
index 0000000..dc181d1
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/AssetModBlock/EditorQuit.cs
@@ -0,0 +1,22 @@
+using UnityEditor;
+namespace ParrelSync
+{
+ [InitializeOnLoad]
+ public class EditorQuit
+ {
+ ///
+ /// Is editor being closed
+ ///
+ static public bool IsQuiting { get; private set; }
+ static void Quit()
+ {
+ IsQuiting = true;
+ }
+
+ static EditorQuit()
+ {
+ IsQuiting = false;
+ EditorApplication.quitting += Quit;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta b/Assets/Plugins/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta
new file mode 100644
index 0000000..2296dac
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bf2888ff90706904abc2d851c3e59e00
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs b/Assets/Plugins/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs
new file mode 100644
index 0000000..6587482
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs
@@ -0,0 +1,34 @@
+using UnityEditor;
+using UnityEngine;
+namespace ParrelSync
+{
+ ///
+ /// For preventing assets being modified from the clone instance.
+ ///
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta b/Assets/Plugins/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta
new file mode 100644
index 0000000..7158175
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 755e570bd21b39440a923056e60f1450
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/ClonesManager.cs b/Assets/Plugins/ParrelSync/Editor/ClonesManager.cs
new file mode 100644
index 0000000..df47ca0
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ClonesManager.cs
@@ -0,0 +1,692 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using UnityEngine;
+using UnityEditor;
+using System.Linq;
+using System.IO;
+using Debug = UnityEngine.Debug;
+
+namespace ParrelSync
+{
+ ///
+ /// Contains all required methods for creating a linked clone of the Unity project.
+ ///
+ public class ClonesManager
+ {
+ ///
+ /// Name used for an identifying file created in the clone project directory.
+ ///
+ ///
+ /// (!) Do not change this after the clone was created, because then connection will be lost.
+ ///
+ public const string CloneFileName = ".clone";
+
+ ///
+ /// Suffix added to the end of the project clone name when it is created.
+ ///
+ ///
+ /// (!) Do not change this after the clone was created, because then connection will be lost.
+ ///
+ public const string CloneNameSuffix = "_clone";
+
+ public const string ProjectName = "ParrelSync";
+
+ ///
+ /// The maximum number of clones
+ ///
+ public const int MaxCloneProjectCount = 10;
+
+ ///
+ /// Name of the file for storing clone's argument.
+ ///
+ public const string ArgumentFileName = ".parrelsyncarg";
+
+ ///
+ /// Default argument of the new clone
+ ///
+ public const string DefaultArgument = "client";
+
+ #region Managing clones
+
+ ///
+ /// Creates clone from the project currently open in Unity Editor.
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Creates clone of the project located at the given path.
+ ///
+ ///
+ ///
+ 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);
+
+ //Optional Link Folders
+ var optionalLinkPaths = Preferences.OptionalSymbolicLinkFolders.GetStoredValue();
+ var projectSettings = ParrelSyncProjectSettings.GetSerializedSettings();
+ var projectSettingsProperty = projectSettings.FindProperty("m_OptionalSymbolicLinkFolders");
+ if (projectSettingsProperty is { isArray: true, arrayElementType: "string" })
+ {
+ for (var i = 0; i < projectSettingsProperty.arraySize; ++i)
+ {
+ optionalLinkPaths.Add(projectSettingsProperty.GetArrayElementAtIndex(i).stringValue);
+ }
+ }
+ foreach (var path in optionalLinkPaths)
+ {
+ var sourceOptionalPath = sourceProjectPath + path;
+ var cloneOptionalPath = cloneProjectPath + path;
+ LinkFolders(sourceOptionalPath, cloneOptionalPath);
+ }
+
+ ClonesManager.RegisterClone(cloneProject);
+
+ return cloneProject;
+ }
+
+ ///
+ /// Registers a clone by placing an identifying ".clone" file in its root directory.
+ ///
+ ///
+ 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.
+ }
+
+ ///
+ /// Opens a project located at the given path (if one exists).
+ ///
+ ///
+ 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;
+ }
+
+ //Validate (and update if needed) the "Packages" folder before opening clone project to ensure the clone project will have the
+ //same "compiling environment" as the original project
+ ValidateCopiedFoldersIntegrity.ValidateFolder(projectPath, GetOriginalProjectPath(), "Packages");
+
+ 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 ;(");
+ }
+ }
+
+ ///
+ /// Is this project being opened by an Unity editor?
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Deletes the clone of the currently open project, if such exists.
+ ///
+ 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
+
+ ///
+ /// Creates an empty folder using data in the given Project object
+ ///
+ ///
+ public static void CreateProjectFolder(Project project)
+ {
+ string path = project.projectPath;
+ Debug.Log("Creating new empty folder at: " + path);
+ Directory.CreateDirectory(path);
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ [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
+
+ ///
+ /// Creates a symlink between destinationPath and sourcePath (Mac version).
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Creates a symlink between destinationPath and sourcePath (Linux version).
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Creates a symlink between destinationPath and sourcePath (Windows version).
+ ///
+ ///
+ ///
+ 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);
+
+ ///
+ /// Create a link / junction from the original project to it's clone.
+ ///
+ ///
+ ///
+ 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;
+
+ ///
+ /// Returns true if the project currently open in Unity Editor is a clone.
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// Get the path to the current unityEditor project folder's info
+ ///
+ ///
+ public static string GetCurrentProjectPath()
+ {
+ return Application.dataPath.Replace("/Assets", "");
+ }
+
+ ///
+ /// Return a project object that describes all the paths we need to clone it.
+ ///
+ ///
+ public static Project GetCurrentProject()
+ {
+ string pathString = ClonesManager.GetCurrentProjectPath();
+ return new Project(pathString);
+ }
+
+ ///
+ /// Get the argument of this clone project.
+ /// If this is the original project, will return an empty string.
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Returns all clone projects path.
+ ///
+ ///
+ public static List GetCloneProjectsPath()
+ {
+ List projectsPath = new List();
+ 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;
+ }
+
+ ///
+ /// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
+ ///
+ /// Directory to be copied.
+ /// Destination directory (created automatically if needed).
+ /// Optional string added to the beginning of the progress bar window header.
+ 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();
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Directory to be copied.
+ /// Destination directory (created automatically if needed).
+ /// Total bytes to be copied. Calculated automatically, initialize at 0.
+ /// To track already copied bytes. Calculated automatically, initialize at 0.
+ /// Optional string added to the beginning of the progress bar window header.
+ 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())
+ {
+ // Ensure file exists before continuing.
+ if (!file.Exists)
+ {
+ continue;
+ }
+
+ 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);
+ }
+ }
+
+ ///
+ /// Calculates the size of the given directory. Displays a progress bar.
+ ///
+ /// Directory, which size has to be calculated.
+ /// If true, size will include all nested directories.
+ /// Optional string added to the beginning of the progress bar window header.
+ /// Size of the directory in bytes.
+ 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.Exists ? file.Length : 0);
+
+ /// Calculate size of all nested directories.
+ long directoriesSize = 0;
+ if (includeNested)
+ {
+ IEnumerable nestedDirectories = directory.GetDirectories();
+ foreach (DirectoryInfo nestedDir in nestedDirectories)
+ {
+ directoriesSize += ClonesManager.GetDirectorySize(nestedDir, true, progressBarPrefix);
+ }
+ }
+
+ return filesSize + directoriesSize;
+ }
+
+ ///
+ /// Starts process in the system console, taking the given fileName and args.
+ ///
+ ///
+ ///
+ private static void StartHiddenConsoleProcess(string fileName, string args)
+ {
+ System.Diagnostics.Process.Start(fileName, args);
+ }
+
+ ///
+ /// Thanks to https://github.com/karl-/unity-symlink-utility/blob/master/SymlinkUtility.cs
+ ///
+ ///
+ 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
+ }
+}
diff --git a/Assets/Plugins/ParrelSync/Editor/ClonesManager.cs.meta b/Assets/Plugins/ParrelSync/Editor/ClonesManager.cs.meta
new file mode 100644
index 0000000..5800cf8
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ClonesManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6148e48ed6b61d748b187d06d3687b83
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/ClonesManagerWindow.cs b/Assets/Plugins/ParrelSync/Editor/ClonesManagerWindow.cs
new file mode 100644
index 0000000..ad9619e
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ClonesManagerWindow.cs
@@ -0,0 +1,198 @@
+using UnityEngine;
+using UnityEditor;
+using System.IO;
+
+namespace ParrelSync
+{
+ ///
+ ///Clones manager Unity editor window
+ ///
+ public class ClonesManagerWindow : EditorWindow
+ {
+ ///
+ /// Returns true if project clone exists.
+ ///
+ 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();
+ }
+
+ ///
+ /// For storing the scroll position of clones list
+ ///
+ 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();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Assets/Plugins/ParrelSync/Editor/ClonesManagerWindow.cs.meta b/Assets/Plugins/ParrelSync/Editor/ClonesManagerWindow.cs.meta
new file mode 100644
index 0000000..ac75a04
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ClonesManagerWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a041d83486c20b84bbf5077ddfbbca37
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/ExternalLinks.cs b/Assets/Plugins/ParrelSync/Editor/ExternalLinks.cs
new file mode 100644
index 0000000..84809bc
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ExternalLinks.cs
@@ -0,0 +1,13 @@
+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";
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/ExternalLinks.cs.meta b/Assets/Plugins/ParrelSync/Editor/ExternalLinks.cs.meta
new file mode 100644
index 0000000..c238b5c
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ExternalLinks.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 65daf17fbe5101b41977305639f30c65
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/FileUtilities.cs b/Assets/Plugins/ParrelSync/Editor/FileUtilities.cs
new file mode 100644
index 0000000..999ee02
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/FileUtilities.cs
@@ -0,0 +1,31 @@
+using System.IO;
+using UnityEngine;
+
+namespace ParrelSync
+{
+ public class FileUtilities
+ {
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/FileUtilities.cs.meta b/Assets/Plugins/ParrelSync/Editor/FileUtilities.cs.meta
new file mode 100644
index 0000000..2733944
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/FileUtilities.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 11fdc6f78f8c965499a870ca06dca6bc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/NonCore.meta b/Assets/Plugins/ParrelSync/Editor/NonCore.meta
new file mode 100644
index 0000000..5b4e192
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/NonCore.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 74a7aa389726f964ab34c52e208c2a43
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs b/Assets/Plugins/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs
new file mode 100644
index 0000000..2bb988a
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs
@@ -0,0 +1,78 @@
+namespace ParrelSync.NonCore
+{
+ using UnityEditor;
+ using UnityEngine;
+
+ ///
+ /// 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.
+ ///
+ [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;
+ }
+ }
+
+ /////
+ ///// For debug purpose
+ /////
+ //[MenuItem("ParrelSync/(Debug)Delete AskFeedbackDialog keys")]
+ //private static void DebugDeleteAllKeys()
+ //{
+ // EditorPrefs.DeleteKey(InitializeOnLoadCountKey);
+ // EditorPrefs.DeleteKey(StopShowingKey);
+ // Debug.Log("AskFeedbackDialog keys deleted");
+ //}
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta b/Assets/Plugins/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta
new file mode 100644
index 0000000..20a2a0b
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 894412a5b602e6c4ba2cf2d01f4f92b5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/NonCore/OtherMenuItem.cs b/Assets/Plugins/ParrelSync/Editor/NonCore/OtherMenuItem.cs
new file mode 100644
index 0000000..0f42af9
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/NonCore/OtherMenuItem.cs
@@ -0,0 +1,26 @@
+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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta b/Assets/Plugins/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta
new file mode 100644
index 0000000..563d7a2
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7191fa4bfa12ae749b27f73ed292eaf1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/ParrelSyncProjectSettings.cs b/Assets/Plugins/ParrelSync/Editor/ParrelSyncProjectSettings.cs
new file mode 100644
index 0000000..576d41c
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ParrelSyncProjectSettings.cs
@@ -0,0 +1,140 @@
+using System.Collections.Generic;
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace ParrelSync
+{
+ // With ScriptableObject derived classes, .cs and .asset filenames MUST be identical
+ public class ParrelSyncProjectSettings : ScriptableObject
+ {
+ private const string ParrelSyncScriptableObjectsDirectory = "Assets/Plugins/ParrelSync/ScriptableObjects";
+ private const string ParrelSyncSettingsPath = ParrelSyncScriptableObjectsDirectory + "/" +
+ nameof(ParrelSyncProjectSettings) + ".asset";
+
+ [SerializeField]
+ [HideInInspector]
+ private List m_OptionalSymbolicLinkFolders;
+ public const string NameOfOptionalSymbolicLinkFolders = nameof(m_OptionalSymbolicLinkFolders);
+
+ private static ParrelSyncProjectSettings GetOrCreateSettings()
+ {
+ ParrelSyncProjectSettings projectSettings;
+ if (File.Exists(ParrelSyncSettingsPath))
+ {
+ projectSettings = AssetDatabase.LoadAssetAtPath(ParrelSyncSettingsPath);
+
+ if (projectSettings == null)
+ Debug.LogError("File Exists, but failed to load: " + ParrelSyncSettingsPath);
+
+ return projectSettings;
+ }
+
+ projectSettings = CreateInstance();
+ projectSettings.m_OptionalSymbolicLinkFolders = new List();
+ if (!Directory.Exists(ParrelSyncScriptableObjectsDirectory))
+ {
+ Directory.CreateDirectory(ParrelSyncScriptableObjectsDirectory);
+ }
+ AssetDatabase.CreateAsset(projectSettings, ParrelSyncSettingsPath);
+ AssetDatabase.SaveAssets();
+ return projectSettings;
+ }
+
+ public static SerializedObject GetSerializedSettings()
+ {
+ return new SerializedObject(GetOrCreateSettings());
+ }
+ }
+
+ public class ParrelSyncSettingsProvider : SettingsProvider
+ {
+ private const string MenuLocationInProjectSettings = "Project/ParrelSync";
+
+ private SerializedObject _parrelSyncProjectSettings;
+
+ private class Styles
+ {
+ public static readonly GUIContent SymlinkSectionHeading = new GUIContent("Optional Folders to Symbolically Link");
+ }
+
+ private ParrelSyncSettingsProvider(string path, SettingsScope scope = SettingsScope.User)
+ : base(path, scope)
+ {
+ }
+
+ public override void OnActivate(string searchContext, VisualElement rootElement)
+ {
+ // This function is called when the user clicks on the ParrelSyncSettings element in the Settings window.
+ _parrelSyncProjectSettings = ParrelSyncProjectSettings.GetSerializedSettings();
+ }
+
+ public override void OnGUI(string searchContext)
+ {
+ var property = _parrelSyncProjectSettings.FindProperty(ParrelSyncProjectSettings.NameOfOptionalSymbolicLinkFolders);
+ if (property is null || !property.isArray || property.arrayElementType != "string")
+ return;
+
+ var optionalFolderPaths = new List(property.arraySize);
+ for (var i = 0; i < property.arraySize; ++i)
+ {
+ optionalFolderPaths.Add(property.GetArrayElementAtIndex(i).stringValue);
+ }
+ optionalFolderPaths.Add("");
+
+ GUILayout.BeginVertical("GroupBox");
+ GUILayout.Label(Styles.SymlinkSectionHeading);
+ GUILayout.Space(5);
+ var projectPath = ClonesManager.GetCurrentProjectPath();
+ var optionalFolderPathsIsDirty = false;
+ for (var i = 0; i < optionalFolderPaths.Count; ++i)
+ {
+ GUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField(optionalFolderPaths[i], EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
+ if (GUILayout.Button("Select", GUILayout.Width(60)))
+ {
+ var result = EditorUtility.OpenFolderPanel("Select Folder to Symbolically Link...", "", "");
+ if (result.Contains(projectPath))
+ {
+ optionalFolderPaths[i] = result.Replace(projectPath, "");
+ optionalFolderPathsIsDirty = true;
+ }
+ else if (result != "")
+ {
+ Debug.LogWarning("Symbolic Link folder must be within the project directory");
+ }
+ }
+ if (GUILayout.Button("Clear", GUILayout.Width(60)))
+ {
+ optionalFolderPaths[i] = "";
+ optionalFolderPathsIsDirty = true;
+ }
+ GUILayout.EndHorizontal();
+ }
+ GUILayout.EndVertical();
+
+ if (!optionalFolderPathsIsDirty)
+ return;
+
+ optionalFolderPaths.RemoveAll(str => str == "");
+ property.arraySize = optionalFolderPaths.Count;
+ for (var i = 0; i < property.arraySize; ++i)
+ {
+ property.GetArrayElementAtIndex(i).stringValue = optionalFolderPaths[i];
+ }
+ _parrelSyncProjectSettings.ApplyModifiedProperties();
+ AssetDatabase.SaveAssets();
+ }
+
+ // Register the SettingsProvider
+ [SettingsProvider]
+ public static SettingsProvider CreateParrelSyncSettingsProvider()
+ {
+ return new ParrelSyncSettingsProvider(MenuLocationInProjectSettings, SettingsScope.Project)
+ {
+ keywords = GetSearchKeywordsFromGUIContentProperties()
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/ParrelSyncProjectSettings.cs.meta b/Assets/Plugins/ParrelSync/Editor/ParrelSyncProjectSettings.cs.meta
new file mode 100644
index 0000000..95506e4
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ParrelSyncProjectSettings.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c0011418c9d75434988a06b6df93b283
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/Preferences.cs b/Assets/Plugins/ParrelSync/Editor/Preferences.cs
new file mode 100644
index 0000000..095a470
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/Preferences.cs
@@ -0,0 +1,215 @@
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using UnityEditor;
+
+namespace ParrelSync
+{
+ ///
+ /// To add value caching for functions
+ ///
+ 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);
+ }
+ }
+
+ public void ClearValue()
+ {
+ EditorPrefs.DeleteKey(key);
+ valueCache = null;
+ }
+ }
+
+
+ ///
+ /// To add value caching for functions
+ ///
+ public class ListOfStringsPreference
+ {
+ private static string serializationToken = "|||";
+ public string Key { get; private set; }
+ public ListOfStringsPreference(string key)
+ {
+ Key = key;
+ }
+ public List GetStoredValue()
+ {
+ return this.Deserialize(EditorPrefs.GetString(Key));
+ }
+ public void SetStoredValue(List strings)
+ {
+ EditorPrefs.SetString(Key, this.Serialize(strings));
+ }
+ public void ClearStoredValue()
+ {
+ EditorPrefs.DeleteKey(Key);
+ }
+ public string Serialize(List data)
+ {
+ string result = string.Empty;
+ foreach (var item in data)
+ {
+ if (item.Contains(serializationToken))
+ {
+ Debug.LogError("Unable to serialize this value ["+item+"], it contains the serialization token ["+serializationToken+"]");
+ continue;
+ }
+
+ result += item + serializationToken;
+ }
+ return result;
+ }
+ public List Deserialize(string data)
+ {
+ return data.Split(serializationToken).ToList();
+ }
+ }
+ public class Preferences : EditorWindow
+ {
+ [MenuItem("ParrelSync/Preferences", priority = 1)]
+ private static void InitWindow()
+ {
+ Preferences window = (Preferences)EditorWindow.GetWindow(typeof(Preferences));
+ window.titleContent = new GUIContent(ClonesManager.ProjectName + " Preferences");
+ window.minSize = new Vector2(550, 300);
+ window.Show();
+ }
+
+ ///
+ /// Disable asset saving in clone editors?
+ ///
+ public static BoolPreference AssetModPref = new BoolPreference("ParrelSync_DisableClonesAssetSaving", true);
+
+ ///
+ /// In addition of checking the existence of UnityLockFile,
+ /// also check is the is the UnityLockFile being opened.
+ ///
+ public static BoolPreference AlsoCheckUnityLockFileStaPref = new BoolPreference("ParrelSync_CheckUnityLockFileOpenStatus", true);
+
+ ///
+ /// A list of folders to create sybolic links for,
+ /// useful for data that lives outside of the assets folder
+ /// eg. Wwise project data
+ ///
+ public static ListOfStringsPreference OptionalSymbolicLinkFolders = new ListOfStringsPreference("ParrelSync_OptionalSymbolicLinkFolders");
+
+ private void OnGUI()
+ {
+ if (ClonesManager.IsClone())
+ {
+ EditorGUILayout.HelpBox(
+ "This is a clone project. Please use the original project editor to change preferences.",
+ MessageType.Info);
+ return;
+ }
+
+ GUILayout.BeginVertical("HelpBox");
+ GUILayout.Label("Preferences");
+ GUILayout.BeginVertical("GroupBox");
+
+ AssetModPref.Value = EditorGUILayout.ToggleLeft(
+ new GUIContent(
+ "(recommended) Disable asset saving in clone editors- require re-open clone editors",
+ "Disable asset saving in clone editors so all assets can only be modified from the original project editor"
+ ),
+ AssetModPref.Value);
+
+ if (Application.platform == RuntimePlatform.WindowsEditor)
+ {
+ AlsoCheckUnityLockFileStaPref.Value = EditorGUILayout.ToggleLeft(
+ new GUIContent(
+ "Also check UnityLockFile lock status while checking clone projects running status",
+ "Disable this can slightly increase Clones Manager window performance, but will lead to in-correct clone project running status" +
+ "(the Clones Manager window show the clone project is still running even it's not) if the clone editor crashed"
+ ),
+ AlsoCheckUnityLockFileStaPref.Value);
+ }
+ GUILayout.EndVertical();
+
+ GUILayout.BeginVertical("GroupBox");
+ GUILayout.Label("Optional Folders to Symbolically Link");
+ GUILayout.Space(5);
+
+ // cache the current value
+ List optionalFolderPaths = OptionalSymbolicLinkFolders.GetStoredValue();
+ bool optionalFolderPathsAreDirty = false;
+
+ // append a new row if full
+ if (optionalFolderPaths.Last() != "")
+ {
+ optionalFolderPaths.Add("");
+ }
+
+ var projectPath = ClonesManager.GetCurrentProjectPath();
+ for (int i = 0; i < optionalFolderPaths.Count; ++i)
+ {
+ GUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField(optionalFolderPaths[i], EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
+ if (GUILayout.Button("Select Folder", GUILayout.Width(100)))
+ {
+ var result = EditorUtility.OpenFolderPanel("Select Folder to Symbolically Link...", "", "");
+ if (result.Contains(projectPath))
+ {
+ optionalFolderPaths[i] = result.Replace(projectPath,"");
+ optionalFolderPathsAreDirty = true;
+ }
+ else if( result != "")
+ {
+ Debug.LogWarning("Symbolic Link folder must be within the project directory");
+ }
+ }
+ if (GUILayout.Button("Clear", GUILayout.Width(100)))
+ {
+ optionalFolderPaths[i] = "";
+ optionalFolderPathsAreDirty = true;
+ }
+ GUILayout.EndHorizontal();
+ }
+
+ // only set the preference if the value is marked dirty
+ if (optionalFolderPathsAreDirty)
+ {
+ optionalFolderPaths.RemoveAll(str=> str == "");
+ OptionalSymbolicLinkFolders.SetStoredValue(optionalFolderPaths);
+ }
+
+ GUILayout.EndVertical();
+
+ if (GUILayout.Button("Reset to default"))
+ {
+ AssetModPref.ClearValue();
+ AlsoCheckUnityLockFileStaPref.ClearValue();
+ OptionalSymbolicLinkFolders.ClearStoredValue();
+ Debug.Log("Editor preferences cleared");
+ }
+ GUILayout.EndVertical();
+ }
+ }
+}
diff --git a/Assets/Plugins/ParrelSync/Editor/Preferences.cs.meta b/Assets/Plugins/ParrelSync/Editor/Preferences.cs.meta
new file mode 100644
index 0000000..0166f9a
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/Preferences.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 24641be1c0410a745b529e61b508679f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/Project.cs b/Assets/Plugins/ParrelSync/Editor/Project.cs
new file mode 100644
index 0000000..7e7c387
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/Project.cs
@@ -0,0 +1,112 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ParrelSync
+{
+ public class Project : System.ICloneable
+ {
+ public string name;
+ public string projectPath;
+ string rootPath;
+ public string assetPath;
+ public string projectSettingsPath;
+ public string libraryPath;
+ public string packagesPath;
+ public string autoBuildPath;
+ public string localPackages;
+
+ char[] separator = new char[1] { '/' };
+
+
+ ///
+ /// Default constructor
+ ///
+ public Project()
+ {
+
+ }
+
+
+ ///
+ /// Initialize the project object by parsing its full path returned by Unity into a bunch of individual folder names and paths.
+ ///
+ ///
+ public Project(string path)
+ {
+ ParsePath(path);
+ }
+
+
+ ///
+ /// Create a new object with the same settings
+ ///
+ ///
+ public object Clone()
+ {
+ Project newProject = new Project();
+ newProject.rootPath = rootPath;
+ newProject.projectPath = projectPath;
+ newProject.assetPath = assetPath;
+ newProject.projectSettingsPath = projectSettingsPath;
+ newProject.libraryPath = libraryPath;
+ newProject.name = name;
+ newProject.separator = separator;
+ newProject.packagesPath = packagesPath;
+ newProject.autoBuildPath = autoBuildPath;
+ newProject.localPackages = localPackages;
+
+
+ return newProject;
+ }
+
+
+ ///
+ /// Update the project object by renaming and reparsing it. Pass in the new name of a project, and it'll update the other member variables to match.
+ ///
+ ///
+ public void updateNewName(string newName)
+ {
+ name = newName;
+ ParsePath(rootPath + "/" + name + "/Assets");
+ }
+
+
+ ///
+ /// Debug override so we can quickly print out the project info.
+ ///
+ ///
+ public override string ToString()
+ {
+ string printString = name + "\n" +
+ rootPath + "\n" +
+ projectPath + "\n" +
+ assetPath + "\n" +
+ projectSettingsPath + "\n" +
+ packagesPath + "\n" +
+ autoBuildPath + "\n" +
+ localPackages + "\n" +
+ libraryPath;
+ return (printString);
+ }
+
+ private void ParsePath(string path)
+ {
+ //Unity's Application functions return the Assets path in the Editor.
+ projectPath = path;
+
+ //pop off the last part of the path for the project name, keep the rest for the root path
+ List pathArray = projectPath.Split(separator).ToList();
+ name = pathArray.Last();
+
+ pathArray.RemoveAt(pathArray.Count() - 1);
+ rootPath = string.Join(separator[0].ToString(), pathArray.ToArray());
+
+ assetPath = projectPath + "/Assets";
+ projectSettingsPath = projectPath + "/ProjectSettings";
+ libraryPath = projectPath + "/Library";
+ packagesPath = projectPath + "/Packages";
+ autoBuildPath = projectPath + "/AutoBuild";
+ localPackages = projectPath + "/LocalPackages";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/Project.cs.meta b/Assets/Plugins/ParrelSync/Editor/Project.cs.meta
new file mode 100644
index 0000000..84d9855
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/Project.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ec8d3a1577179ef44815739178cf75b4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/UpdateChecker.cs b/Assets/Plugins/ParrelSync/Editor/UpdateChecker.cs
new file mode 100644
index 0000000..a93895d
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/UpdateChecker.cs
@@ -0,0 +1,60 @@
+using System;
+using UnityEditor;
+using UnityEngine;
+namespace ParrelSync.Update
+{
+ ///
+ /// A simple update checker
+ ///
+ public class UpdateChecker
+ {
+ //const string LocalVersionFilePath = "Assets/ParrelSync/VERSION.txt";
+ public const string LocalVersion = "1.5.2";
+ [MenuItem("ParrelSync/Check for update", priority = 20)]
+ static void CheckForUpdate()
+ {
+ using (System.Net.WebClient client = new System.Net.WebClient())
+ {
+ try
+ {
+ //This won't work with UPM packages
+ //string localVersionText = AssetDatabase.LoadAssetAtPath(LocalVersionFilePath).text;
+
+ string localVersionText = LocalVersion;
+ Debug.Log("Local version text : " + LocalVersion);
+
+ string latesteVersionText = client.DownloadString(ExternalLinks.RemoteVersionURL);
+ Debug.Log("latest version text got: " + latesteVersionText);
+ string messageBody = "Current Version: " + localVersionText +"\n"
+ +"Latest Version: " + latesteVersionText + "\n";
+ var latestVersion = new Version(latesteVersionText);
+ var localVersion = new Version(localVersionText);
+
+ if (latestVersion > localVersion)
+ {
+ Debug.Log("There's a newer version");
+ messageBody += "There's a newer version available";
+ if(EditorUtility.DisplayDialog("Check for update.", messageBody, "Get latest release", "Close"))
+ {
+ Application.OpenURL(ExternalLinks.Releases);
+ }
+ }
+ else
+ {
+ Debug.Log("Current version is up-to-date.");
+ messageBody += "Current version is up-to-date.";
+ EditorUtility.DisplayDialog("Check for update.", messageBody,"OK");
+ }
+
+ }
+ catch (Exception exp)
+ {
+ Debug.LogError("Error with checking update. Exception: " + exp);
+ EditorUtility.DisplayDialog("Update Error","Error with checking update. \nSee console for more details.",
+ "OK"
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/Assets/Plugins/ParrelSync/Editor/UpdateChecker.cs.meta b/Assets/Plugins/ParrelSync/Editor/UpdateChecker.cs.meta
new file mode 100644
index 0000000..8dcd733
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/UpdateChecker.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d3453b3f1a20ea148b5028f8556a7be5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs b/Assets/Plugins/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs
new file mode 100644
index 0000000..1ee73bc
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs
@@ -0,0 +1,73 @@
+namespace ParrelSync
+{
+ using UnityEditor;
+ using UnityEngine;
+ using System;
+ using System.Text;
+ using System.Security.Cryptography;
+ using System.IO;
+ using System.Linq;
+
+ [InitializeOnLoad]
+ public class ValidateCopiedFoldersIntegrity
+ {
+ const string SessionStateKey = "ValidateCopiedFoldersIntegrity_Init";
+ ///
+ /// Called once on editor startup.
+ /// Validate copied folders integrity in clone project
+ ///
+ static ValidateCopiedFoldersIntegrity()
+ {
+ if (!SessionState.GetBool(SessionStateKey, false))
+ {
+ SessionState.SetBool(SessionStateKey, true);
+ if (!ClonesManager.IsClone()) { return; }
+
+ ValidateFolder(ClonesManager.GetCurrentProjectPath(), ClonesManager.GetOriginalProjectPath(), "Packages");
+ }
+ }
+
+ public static void ValidateFolder(string targetRoot, string originalRoot, string folderName)
+ {
+ var targetFolderPath = Path.Combine(targetRoot, folderName);
+ var targetFolderHash = CreateMd5ForFolder(targetFolderPath);
+
+ var originalFolderPath = Path.Combine(originalRoot, folderName);
+ var originalFolderHash = CreateMd5ForFolder(originalFolderPath);
+
+ if (targetFolderHash != originalFolderHash)
+ {
+ Debug.Log("ParrelSync: Detected changes in '" + folderName + "' directory. Updating cloned project...");
+ FileUtil.ReplaceDirectory(originalFolderPath, targetFolderPath);
+ }
+ }
+
+ static string CreateMd5ForFolder(string path)
+ {
+ // assuming you want to include nested folders
+ var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)
+ .OrderBy(p => p).ToList();
+
+ MD5 md5 = MD5.Create();
+
+ for (int i = 0; i < files.Count; i++)
+ {
+ string file = files[i];
+
+ // hash path
+ string relativePath = file.Substring(path.Length + 1);
+ byte[] pathBytes = Encoding.UTF8.GetBytes(relativePath.ToLower());
+ md5.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0);
+
+ // hash contents
+ byte[] contentBytes = File.ReadAllBytes(file);
+ if (i == files.Count - 1)
+ md5.TransformFinalBlock(contentBytes, 0, contentBytes.Length);
+ else
+ md5.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0);
+ }
+
+ return BitConverter.ToString(md5.Hash).Replace("-", "").ToLower();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta b/Assets/Plugins/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta
new file mode 100644
index 0000000..daab522
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d8fb344b9abf5274abd744833474b087
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/package.json b/Assets/Plugins/ParrelSync/package.json
new file mode 100644
index 0000000..08bb747
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "com.veriorpies.parrelsync",
+ "displayName": "ParrelSync",
+ "version": "1.5.2",
+ "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": {}
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/package.json.meta b/Assets/Plugins/ParrelSync/package.json.meta
new file mode 100644
index 0000000..4ced740
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/package.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a2a889c264e34b47a7349cbcb2cbedd7
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Plugins/ParrelSync/projectCloner.asmdef b/Assets/Plugins/ParrelSync/projectCloner.asmdef
new file mode 100644
index 0000000..1e56b06
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/projectCloner.asmdef
@@ -0,0 +1,15 @@
+{
+ "name": "ParrelSync",
+ "references": [],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Assets/Plugins/ParrelSync/projectCloner.asmdef.meta b/Assets/Plugins/ParrelSync/projectCloner.asmdef.meta
new file mode 100644
index 0000000..3aa8857
--- /dev/null
+++ b/Assets/Plugins/ParrelSync/projectCloner.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 894a6cc6ed5cd2645bb542978cbed6a9
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Multiplayer/NetworkedGameSetup.cs b/Assets/Scripts/Multiplayer/NetworkedGameSetup.cs
index 0bad16a..b538805 100644
--- a/Assets/Scripts/Multiplayer/NetworkedGameSetup.cs
+++ b/Assets/Scripts/Multiplayer/NetworkedGameSetup.cs
@@ -63,7 +63,7 @@ public class NetworkedGameSetup : NetworkBehaviour
{
RopeSimulator ropeSim = GetComponentInChildren();
- //// Assuming 2 players
+ // Assuming 2 players
ropeSim.BuildRope(players[0].GetComponent(), players[1].GetComponent());
}