Imported ParallelSync
This commit is contained in:
parent
b1cda4f602
commit
91b3c89657
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 711bbd1e36ca42a4bad871eb6a3de1bc
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a31ea7d0315594440839cdb0db6bc411
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8b14e706b1e7cb044b23837e8a70cad9
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,22 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bf2888ff90706904abc2d851c3e59e00
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,34 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 755e570bd21b39440a923056e60f1450
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <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);
|
||||||
|
|
||||||
|
//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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//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 ;(");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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())
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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.Exists ? file.Length : 0);
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6148e48ed6b61d748b187d06d3687b83
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,198 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a041d83486c20b84bbf5077ddfbbca37
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 65daf17fbe5101b41977305639f30c65
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 11fdc6f78f8c965499a870ca06dca6bc
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 74a7aa389726f964ab34c52e208c2a43
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,78 @@
|
||||||
|
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");
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 894412a5b602e6c4ba2cf2d01f4f92b5
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7191fa4bfa12ae749b27f73ed292eaf1
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -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<string> m_OptionalSymbolicLinkFolders;
|
||||||
|
public const string NameOfOptionalSymbolicLinkFolders = nameof(m_OptionalSymbolicLinkFolders);
|
||||||
|
|
||||||
|
private static ParrelSyncProjectSettings GetOrCreateSettings()
|
||||||
|
{
|
||||||
|
ParrelSyncProjectSettings projectSettings;
|
||||||
|
if (File.Exists(ParrelSyncSettingsPath))
|
||||||
|
{
|
||||||
|
projectSettings = AssetDatabase.LoadAssetAtPath<ParrelSyncProjectSettings>(ParrelSyncSettingsPath);
|
||||||
|
|
||||||
|
if (projectSettings == null)
|
||||||
|
Debug.LogError("File Exists, but failed to load: " + ParrelSyncSettingsPath);
|
||||||
|
|
||||||
|
return projectSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
projectSettings = CreateInstance<ParrelSyncProjectSettings>();
|
||||||
|
projectSettings.m_OptionalSymbolicLinkFolders = new List<string>();
|
||||||
|
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<string>(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<Styles>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c0011418c9d75434988a06b6df93b283
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,215 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearValue()
|
||||||
|
{
|
||||||
|
EditorPrefs.DeleteKey(key);
|
||||||
|
valueCache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// To add value caching for <see cref="EditorPrefs"/> functions
|
||||||
|
/// </summary>
|
||||||
|
public class ListOfStringsPreference
|
||||||
|
{
|
||||||
|
private static string serializationToken = "|||";
|
||||||
|
public string Key { get; private set; }
|
||||||
|
public ListOfStringsPreference(string key)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
}
|
||||||
|
public List<string> GetStoredValue()
|
||||||
|
{
|
||||||
|
return this.Deserialize(EditorPrefs.GetString(Key));
|
||||||
|
}
|
||||||
|
public void SetStoredValue(List<string> strings)
|
||||||
|
{
|
||||||
|
EditorPrefs.SetString(Key, this.Serialize(strings));
|
||||||
|
}
|
||||||
|
public void ClearStoredValue()
|
||||||
|
{
|
||||||
|
EditorPrefs.DeleteKey(Key);
|
||||||
|
}
|
||||||
|
public string Serialize(List<string> 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<string> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable asset saving in clone editors?
|
||||||
|
/// </summary>
|
||||||
|
public static BoolPreference AssetModPref = new BoolPreference("ParrelSync_DisableClonesAssetSaving", true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In addition of checking the existence of UnityLockFile,
|
||||||
|
/// also check is the is the UnityLockFile being opened.
|
||||||
|
/// </summary>
|
||||||
|
public static BoolPreference AlsoCheckUnityLockFileStaPref = new BoolPreference("ParrelSync_CheckUnityLockFileOpenStatus", true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A list of folders to create sybolic links for,
|
||||||
|
/// useful for data that lives outside of the assets folder
|
||||||
|
/// eg. Wwise project data
|
||||||
|
/// </summary>
|
||||||
|
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<string> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 24641be1c0410a745b529e61b508679f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -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] { '/' };
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default constructor
|
||||||
|
/// </summary>
|
||||||
|
public Project()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the project object by parsing its full path returned by Unity into a bunch of individual folder names and paths.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
public Project(string path)
|
||||||
|
{
|
||||||
|
ParsePath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new object with the same settings
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
public void updateNewName(string newName)
|
||||||
|
{
|
||||||
|
name = newName;
|
||||||
|
ParsePath(rootPath + "/" + name + "/Assets");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Debug override so we can quickly print out the project info.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
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<string> pathArray = projectPath.Split(separator).ToList<string>();
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ec8d3a1577179ef44815739178cf75b4
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
namespace ParrelSync.Update
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A simple update checker
|
||||||
|
/// </summary>
|
||||||
|
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<TextAsset>(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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d3453b3f1a20ea148b5028f8556a7be5
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -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";
|
||||||
|
/// <summary>
|
||||||
|
/// Called once on editor startup.
|
||||||
|
/// Validate copied folders integrity in clone project
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d8fb344b9abf5274abd744833474b087
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -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": {}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a2a889c264e34b47a7349cbcb2cbedd7
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "ParrelSync",
|
||||||
|
"references": [],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
|
],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 894a6cc6ed5cd2645bb542978cbed6a9
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -63,7 +63,7 @@ public class NetworkedGameSetup : NetworkBehaviour
|
||||||
{
|
{
|
||||||
RopeSimulator ropeSim = GetComponentInChildren<RopeSimulator>();
|
RopeSimulator ropeSim = GetComponentInChildren<RopeSimulator>();
|
||||||
|
|
||||||
//// Assuming 2 players
|
// Assuming 2 players
|
||||||
ropeSim.BuildRope(players[0].GetComponent<RopeJoint>(), players[1].GetComponent<RopeJoint>());
|
ropeSim.BuildRope(players[0].GetComponent<RopeJoint>(), players[1].GetComponent<RopeJoint>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue