();
99 | name = pathArray.Last();
100 |
101 | pathArray.RemoveAt(pathArray.Count() - 1);
102 | rootPath = string.Join(separator[0].ToString(), pathArray.ToArray());
103 |
104 | assetPath = projectPath + "/Assets";
105 | projectSettingsPath = projectPath + "/ProjectSettings";
106 | libraryPath = projectPath + "/Library";
107 | packagesPath = projectPath + "/Packages";
108 | autoBuildPath = projectPath + "/AutoBuild";
109 | localPackages = projectPath + "/LocalPackages";
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ParrelSync
2 | [](https://github.com/VeriorPies/ParrelSync/releases) [](https://github.com/VeriorPies/ParrelSync/wiki) [](https://github.com/VeriorPies/ParrelSync/blob/master/LICENSE.md) [](https://github.com/VeriorPies/ParrelSync/pulls) [](https://discord.gg/TmQk2qG)
3 |
4 | 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.
5 |
6 |
7 |
8 | 
9 |
10 | Test project changes on clients and server within seconds - both in editor
11 |
12 |
13 |
14 |
15 | ## Features
16 | 1. Test multiplayer gameplay without building the project
17 | 2. GUI tools for managing all project clones
18 | 3. Protected assets from being modified by other clone instances
19 | 4. Handy APIs to speed up testing workflows
20 | ## Installation
21 |
22 | 1. Backup your project folder or use a version control system such as [Git](https://git-scm.com/) or [SVN](https://subversion.apache.org/)
23 | 2. Download .unitypackage from the [latest release](https://github.com/VeriorPies/ParrelSync/releases) and import it to your project.
24 | 3. ParrelSync should appreared in the menu item bar after imported
25 | 
26 |
27 | Check out the [Installation-and-Update](https://github.com/VeriorPies/ParrelSync/wiki/Installation-and-Update) page for more details.
28 |
29 | ### UPM Package
30 | ParrelSync can also be installed via UPM package.
31 | After Unity 2019.3.4f1, Unity 2020.1a21, which support path query parameter of git package. You can install ParrelSync by adding the following to Package Manager.
32 |
33 | ```
34 | https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync
35 | ```
36 |
37 |
38 |  
39 |
40 | or by adding
41 |
42 | ```
43 | "com.veriorpies.parrelsync": "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync"
44 | ```
45 |
46 | to the `Packages/manifest.json` file
47 |
48 |
49 | ## Supported Platform
50 | Currently, ParrelSync supports Windows, macOS and Linux editors.
51 |
52 | ParrelSync has been tested with the following Unity version. However, it should also work with other versions as well.
53 | * *2022.3.56f1 LTS*
54 | * *2021.3.29f1 LTS*
55 | * *2020.3.1f1 LTS*
56 |
57 |
58 | ## APIs
59 | There's some useful APIs for speeding up the multiplayer testing workflow.
60 | Here's a basic example:
61 | ```
62 | if (ClonesManager.IsClone()) {
63 | // Automatically connect to local host if this is the clone editor
64 | }else{
65 | // Automatically start server if this is the original editor
66 | }
67 | ```
68 | Check out [the doc](https://github.com/VeriorPies/ParrelSync/wiki/List-of-APIs) to view the complete API list.
69 |
70 | ## How does it work?
71 | For each clone instance, ParrelSync will make a copy of the original project folder and reference the ```Asset```, ```Packages``` and ```ProjectSettings``` folder back to the original project with [symbolic link](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/mklink). Other folders such as ```Library```, ```Temp```, and ```obj``` will remain independent for each clone project.
72 |
73 | All clones are placed right next to the original project with suffix *```_clone_x```*, which will be something like this in the folder hierarchy.
74 | ```
75 | /ProjectName
76 | /ProjectName_clone_0
77 | /ProjectName_clone_1
78 | ...
79 | ```
80 | ## Discord Server
81 | We have a [Discord server](https://discord.gg/TmQk2qG).
82 |
83 | ## Need Help?
84 | Some common questions and troubleshooting can be found under the [Troubleshooting & FAQs](https://github.com/VeriorPies/ParrelSync/wiki/Troubleshooting-&-FAQs) page.
85 | You can also [create a question post](https://github.com/VeriorPies/ParrelSync/issues/new/choose), or ask on [Discord](https://discord.gg/TmQk2qG) if you prefer to have a real-time conversation.
86 |
87 | ## Support this project
88 | A star will be appreciated :)
89 |
90 | ## Credits
91 | This project is originated from hwaet's [UnityProjectCloner](https://github.com/hwaet/UnityProjectCloner)
92 |
--------------------------------------------------------------------------------
/ParrelSync/Editor/ParrelSyncProjectSettings.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using UnityEditor;
4 | using UnityEngine;
5 | using UnityEngine.UIElements;
6 |
7 | namespace ParrelSync
8 | {
9 | // With ScriptableObject derived classes, .cs and .asset filenames MUST be identical
10 | public class ParrelSyncProjectSettings : ScriptableObject
11 | {
12 | private const string ParrelSyncScriptableObjectsDirectory = "Assets/Plugins/ParrelSync/ScriptableObjects";
13 | private const string ParrelSyncSettingsPath = ParrelSyncScriptableObjectsDirectory + "/" +
14 | nameof(ParrelSyncProjectSettings) + ".asset";
15 |
16 | [SerializeField]
17 | [HideInInspector]
18 | private List m_OptionalSymbolicLinkFolders;
19 | public const string NameOfOptionalSymbolicLinkFolders = nameof(m_OptionalSymbolicLinkFolders);
20 |
21 | private static ParrelSyncProjectSettings GetOrCreateSettings()
22 | {
23 | ParrelSyncProjectSettings projectSettings;
24 | if (File.Exists(ParrelSyncSettingsPath))
25 | {
26 | projectSettings = AssetDatabase.LoadAssetAtPath(ParrelSyncSettingsPath);
27 |
28 | if (projectSettings == null)
29 | Debug.LogError("File Exists, but failed to load: " + ParrelSyncSettingsPath);
30 |
31 | return projectSettings;
32 | }
33 |
34 | projectSettings = CreateInstance();
35 | projectSettings.m_OptionalSymbolicLinkFolders = new List();
36 | if (!Directory.Exists(ParrelSyncScriptableObjectsDirectory))
37 | {
38 | Directory.CreateDirectory(ParrelSyncScriptableObjectsDirectory);
39 | }
40 | AssetDatabase.CreateAsset(projectSettings, ParrelSyncSettingsPath);
41 | AssetDatabase.SaveAssets();
42 | return projectSettings;
43 | }
44 |
45 | public static SerializedObject GetSerializedSettings()
46 | {
47 | return new SerializedObject(GetOrCreateSettings());
48 | }
49 | }
50 |
51 | public class ParrelSyncSettingsProvider : SettingsProvider
52 | {
53 | private const string MenuLocationInProjectSettings = "Project/ParrelSync";
54 |
55 | private SerializedObject _parrelSyncProjectSettings;
56 |
57 | private class Styles
58 | {
59 | public static readonly GUIContent SymlinkSectionHeading = new GUIContent("Optional Folders to Symbolically Link");
60 | }
61 |
62 | private ParrelSyncSettingsProvider(string path, SettingsScope scope = SettingsScope.User)
63 | : base(path, scope)
64 | {
65 | }
66 |
67 | public override void OnActivate(string searchContext, VisualElement rootElement)
68 | {
69 | // This function is called when the user clicks on the ParrelSyncSettings element in the Settings window.
70 | _parrelSyncProjectSettings = ParrelSyncProjectSettings.GetSerializedSettings();
71 | }
72 |
73 | public override void OnGUI(string searchContext)
74 | {
75 | var property = _parrelSyncProjectSettings.FindProperty(ParrelSyncProjectSettings.NameOfOptionalSymbolicLinkFolders);
76 | if (property is null || !property.isArray || property.arrayElementType != "string")
77 | return;
78 |
79 | var optionalFolderPaths = new List(property.arraySize);
80 | for (var i = 0; i < property.arraySize; ++i)
81 | {
82 | optionalFolderPaths.Add(property.GetArrayElementAtIndex(i).stringValue);
83 | }
84 | optionalFolderPaths.Add("");
85 |
86 | GUILayout.BeginVertical("GroupBox");
87 | GUILayout.Label(Styles.SymlinkSectionHeading);
88 | GUILayout.Space(5);
89 | var projectPath = ClonesManager.GetCurrentProjectPath();
90 | var optionalFolderPathsIsDirty = false;
91 | for (var i = 0; i < optionalFolderPaths.Count; ++i)
92 | {
93 | GUILayout.BeginHorizontal();
94 | EditorGUILayout.LabelField(optionalFolderPaths[i], EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
95 | if (GUILayout.Button("Select", GUILayout.Width(60)))
96 | {
97 | var result = EditorUtility.OpenFolderPanel("Select Folder to Symbolically Link...", "", "");
98 | if (result.Contains(projectPath))
99 | {
100 | optionalFolderPaths[i] = result.Replace(projectPath, "");
101 | optionalFolderPathsIsDirty = true;
102 | }
103 | else if (result != "")
104 | {
105 | Debug.LogWarning("Symbolic Link folder must be within the project directory");
106 | }
107 | }
108 | if (GUILayout.Button("Clear", GUILayout.Width(60)))
109 | {
110 | optionalFolderPaths[i] = "";
111 | optionalFolderPathsIsDirty = true;
112 | }
113 | GUILayout.EndHorizontal();
114 | }
115 | GUILayout.EndVertical();
116 |
117 | if (!optionalFolderPathsIsDirty)
118 | return;
119 |
120 | optionalFolderPaths.RemoveAll(str => str == "");
121 | property.arraySize = optionalFolderPaths.Count;
122 | for (var i = 0; i < property.arraySize; ++i)
123 | {
124 | property.GetArrayElementAtIndex(i).stringValue = optionalFolderPaths[i];
125 | }
126 | _parrelSyncProjectSettings.ApplyModifiedProperties();
127 | AssetDatabase.SaveAssets();
128 | }
129 |
130 | // Register the SettingsProvider
131 | [SettingsProvider]
132 | public static SettingsProvider CreateParrelSyncSettingsProvider()
133 | {
134 | return new ParrelSyncSettingsProvider(MenuLocationInProjectSettings, SettingsScope.Project)
135 | {
136 | keywords = GetSearchKeywordsFromGUIContentProperties()
137 | };
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/ParrelSync/Editor/Preferences.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using UnityEngine;
4 | using UnityEditor;
5 |
6 | namespace ParrelSync
7 | {
8 | ///
9 | /// To add value caching for functions
10 | ///
11 | public class BoolPreference
12 | {
13 | public string key { get; private set; }
14 | public bool defaultValue { get; private set; }
15 | public BoolPreference(string key, bool defaultValue)
16 | {
17 | this.key = key;
18 | this.defaultValue = defaultValue;
19 | }
20 |
21 | private bool? valueCache = null;
22 |
23 | public bool Value
24 | {
25 | get
26 | {
27 | if (valueCache == null)
28 | valueCache = EditorPrefs.GetBool(key, defaultValue);
29 |
30 | return (bool)valueCache;
31 | }
32 | set
33 | {
34 | if (valueCache == value)
35 | return;
36 |
37 | EditorPrefs.SetBool(key, value);
38 | valueCache = value;
39 | Debug.Log("Editor preference updated. key: " + key + ", value: " + value);
40 | }
41 | }
42 |
43 | public void ClearValue()
44 | {
45 | EditorPrefs.DeleteKey(key);
46 | valueCache = null;
47 | }
48 | }
49 |
50 |
51 | ///
52 | /// To add value caching for functions
53 | ///
54 | public class ListOfStringsPreference
55 | {
56 | private static string serializationToken = "|||";
57 | public string Key { get; private set; }
58 | public ListOfStringsPreference(string key)
59 | {
60 | Key = key;
61 | }
62 | public List GetStoredValue()
63 | {
64 | return this.Deserialize(EditorPrefs.GetString(Key));
65 | }
66 | public void SetStoredValue(List strings)
67 | {
68 | EditorPrefs.SetString(Key, this.Serialize(strings));
69 | }
70 | public void ClearStoredValue()
71 | {
72 | EditorPrefs.DeleteKey(Key);
73 | }
74 | public string Serialize(List data)
75 | {
76 | string result = string.Empty;
77 | foreach (var item in data)
78 | {
79 | if (item.Contains(serializationToken))
80 | {
81 | Debug.LogError("Unable to serialize this value ["+item+"], it contains the serialization token ["+serializationToken+"]");
82 | continue;
83 | }
84 |
85 | result += item + serializationToken;
86 | }
87 | return result;
88 | }
89 | public List Deserialize(string data)
90 | {
91 | return data.Split(serializationToken).ToList();
92 | }
93 | }
94 | public class Preferences : EditorWindow
95 | {
96 | [MenuItem("ParrelSync/Preferences", priority = 1)]
97 | private static void InitWindow()
98 | {
99 | Preferences window = (Preferences)EditorWindow.GetWindow(typeof(Preferences));
100 | window.titleContent = new GUIContent(ClonesManager.ProjectName + " Preferences");
101 | window.minSize = new Vector2(550, 300);
102 | window.Show();
103 | }
104 |
105 | ///
106 | /// Disable asset saving in clone editors?
107 | ///
108 | public static BoolPreference AssetModPref = new BoolPreference("ParrelSync_DisableClonesAssetSaving", true);
109 |
110 | ///
111 | /// In addition of checking the existence of UnityLockFile,
112 | /// also check is the is the UnityLockFile being opened.
113 | ///
114 | public static BoolPreference AlsoCheckUnityLockFileStaPref = new BoolPreference("ParrelSync_CheckUnityLockFileOpenStatus", true);
115 |
116 | ///
117 | /// A list of folders to create sybolic links for,
118 | /// useful for data that lives outside of the assets folder
119 | /// eg. Wwise project data
120 | ///
121 | public static ListOfStringsPreference OptionalSymbolicLinkFolders = new ListOfStringsPreference("ParrelSync_OptionalSymbolicLinkFolders");
122 |
123 | private void OnGUI()
124 | {
125 | if (ClonesManager.IsClone())
126 | {
127 | EditorGUILayout.HelpBox(
128 | "This is a clone project. Please use the original project editor to change preferences.",
129 | MessageType.Info);
130 | return;
131 | }
132 |
133 | GUILayout.BeginVertical("HelpBox");
134 | GUILayout.Label("Preferences");
135 | GUILayout.BeginVertical("GroupBox");
136 |
137 | AssetModPref.Value = EditorGUILayout.ToggleLeft(
138 | new GUIContent(
139 | "(recommended) Disable asset saving in clone editors- require re-open clone editors",
140 | "Disable asset saving in clone editors so all assets can only be modified from the original project editor"
141 | ),
142 | AssetModPref.Value);
143 |
144 | if (Application.platform == RuntimePlatform.WindowsEditor)
145 | {
146 | AlsoCheckUnityLockFileStaPref.Value = EditorGUILayout.ToggleLeft(
147 | new GUIContent(
148 | "Also check UnityLockFile lock status while checking clone projects running status",
149 | "Disable this can slightly increase Clones Manager window performance, but will lead to in-correct clone project running status" +
150 | "(the Clones Manager window show the clone project is still running even it's not) if the clone editor crashed"
151 | ),
152 | AlsoCheckUnityLockFileStaPref.Value);
153 | }
154 | GUILayout.EndVertical();
155 |
156 | GUILayout.BeginVertical("GroupBox");
157 | GUILayout.Label("Optional Folders to Symbolically Link");
158 | GUILayout.Space(5);
159 |
160 | // cache the current value
161 | List optionalFolderPaths = OptionalSymbolicLinkFolders.GetStoredValue();
162 | bool optionalFolderPathsAreDirty = false;
163 |
164 | // append a new row if full
165 | if (optionalFolderPaths.Last() != "")
166 | {
167 | optionalFolderPaths.Add("");
168 | }
169 |
170 | var projectPath = ClonesManager.GetCurrentProjectPath();
171 | for (int i = 0; i < optionalFolderPaths.Count; ++i)
172 | {
173 | GUILayout.BeginHorizontal();
174 | EditorGUILayout.LabelField(optionalFolderPaths[i], EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
175 | if (GUILayout.Button("Select Folder", GUILayout.Width(100)))
176 | {
177 | var result = EditorUtility.OpenFolderPanel("Select Folder to Symbolically Link...", "", "");
178 | if (result.Contains(projectPath))
179 | {
180 | optionalFolderPaths[i] = result.Replace(projectPath,"");
181 | optionalFolderPathsAreDirty = true;
182 | }
183 | else if( result != "")
184 | {
185 | Debug.LogWarning("Symbolic Link folder must be within the project directory");
186 | }
187 | }
188 | if (GUILayout.Button("Clear", GUILayout.Width(100)))
189 | {
190 | optionalFolderPaths[i] = "";
191 | optionalFolderPathsAreDirty = true;
192 | }
193 | GUILayout.EndHorizontal();
194 | }
195 |
196 | // only set the preference if the value is marked dirty
197 | if (optionalFolderPathsAreDirty)
198 | {
199 | optionalFolderPaths.RemoveAll(str=> str == "");
200 | OptionalSymbolicLinkFolders.SetStoredValue(optionalFolderPaths);
201 | }
202 |
203 | GUILayout.EndVertical();
204 |
205 | if (GUILayout.Button("Reset to default"))
206 | {
207 | AssetModPref.ClearValue();
208 | AlsoCheckUnityLockFileStaPref.ClearValue();
209 | OptionalSymbolicLinkFolders.ClearStoredValue();
210 | Debug.Log("Editor preferences cleared");
211 | }
212 | GUILayout.EndVertical();
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/ParrelSync/Editor/ClonesManagerWindow.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine;
2 | using UnityEditor;
3 | using System.IO;
4 |
5 | namespace ParrelSync
6 | {
7 | ///
8 | ///Clones manager Unity editor window
9 | ///
10 | public class ClonesManagerWindow : EditorWindow
11 | {
12 | ///
13 | /// Returns true if project clone exists.
14 | ///
15 | public bool isCloneCreated
16 | {
17 | get { return ClonesManager.GetCloneProjectsPath().Count >= 1; }
18 | }
19 |
20 | [MenuItem("ParrelSync/Clones Manager", priority = 0)]
21 | private static void InitWindow()
22 | {
23 | ClonesManagerWindow window = (ClonesManagerWindow)EditorWindow.GetWindow(typeof(ClonesManagerWindow));
24 | window.titleContent = new GUIContent("Clones Manager");
25 | window.Show();
26 | }
27 |
28 | ///
29 | /// For storing the scroll position of clones list
30 | ///
31 | Vector2 clonesScrollPos;
32 |
33 | private void OnGUI()
34 | {
35 | /// If it is a clone project...
36 | if (ClonesManager.IsClone())
37 | {
38 | //Find out the original project name and show the help box
39 | string originalProjectPath = ClonesManager.GetOriginalProjectPath();
40 | if (originalProjectPath == string.Empty)
41 | {
42 | /// If original project cannot be found, display warning message.
43 | EditorGUILayout.HelpBox(
44 | "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",
45 | MessageType.Warning);
46 | }
47 | else
48 | {
49 | /// If original project is present, display some usage info.
50 | EditorGUILayout.HelpBox(
51 | "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.",
52 | MessageType.Info);
53 | }
54 |
55 | //Clone project custom argument.
56 | GUILayout.BeginHorizontal();
57 | EditorGUILayout.LabelField("Arguments", GUILayout.Width(70));
58 | if (GUILayout.Button("?", GUILayout.Width(20)))
59 | {
60 | Application.OpenURL(ExternalLinks.CustomArgumentHelpLink);
61 | }
62 | GUILayout.EndHorizontal();
63 |
64 | string argumentFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.ArgumentFileName);
65 | //Need to be careful with file reading / writing since it will effect the deletion of
66 | // the clone project(The directory won't be fully deleted if there's still file inside being read or write).
67 | //The argument file will be deleted first at the beginning of the project deletion process
68 | //to prevent any further being read and write.
69 | //Will need to take some extra cautious if want to change the design of how file editing is handled.
70 | if (File.Exists(argumentFilePath))
71 | {
72 | string argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
73 | string argumentTextAreaInput = EditorGUILayout.TextArea(argument,
74 | GUILayout.Height(50),
75 | GUILayout.MaxWidth(300)
76 | );
77 | File.WriteAllText(argumentFilePath, argumentTextAreaInput, System.Text.Encoding.UTF8);
78 | }
79 | else
80 | {
81 | EditorGUILayout.LabelField("No argument file found.");
82 | }
83 | }
84 | else// If it is an original project...
85 | {
86 | if (isCloneCreated)
87 | {
88 | GUILayout.BeginVertical("HelpBox");
89 | GUILayout.Label("Clones of this Project");
90 |
91 | //List all clones
92 | clonesScrollPos =
93 | EditorGUILayout.BeginScrollView(clonesScrollPos);
94 | var cloneProjectsPath = ClonesManager.GetCloneProjectsPath();
95 | for (int i = 0; i < cloneProjectsPath.Count; i++)
96 | {
97 |
98 | GUILayout.BeginVertical("GroupBox");
99 | string cloneProjectPath = cloneProjectsPath[i];
100 |
101 | bool isOpenInAnotherInstance = ClonesManager.IsCloneProjectRunning(cloneProjectPath);
102 |
103 | if (isOpenInAnotherInstance == true)
104 | EditorGUILayout.LabelField("Clone " + i + " (Running)", EditorStyles.boldLabel);
105 | else
106 | EditorGUILayout.LabelField("Clone " + i);
107 |
108 |
109 | GUILayout.BeginHorizontal();
110 | EditorGUILayout.TextField("Clone project path", cloneProjectPath, EditorStyles.textField);
111 | if (GUILayout.Button("View Folder", GUILayout.Width(80)))
112 | {
113 | ClonesManager.OpenProjectInFileExplorer(cloneProjectPath);
114 | }
115 | GUILayout.EndHorizontal();
116 |
117 | GUILayout.BeginHorizontal();
118 | EditorGUILayout.LabelField("Arguments", GUILayout.Width(70));
119 | if (GUILayout.Button("?", GUILayout.Width(20)))
120 | {
121 | Application.OpenURL(ExternalLinks.CustomArgumentHelpLink);
122 | }
123 | GUILayout.EndHorizontal();
124 |
125 | string argumentFilePath = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
126 | //Need to be careful with file reading/writing since it will effect the deletion of
127 | //the clone project(The directory won't be fully deleted if there's still file inside being read or write).
128 | //The argument file will be deleted first at the beginning of the project deletion process
129 | //to prevent any further being read and write.
130 | //Will need to take some extra cautious if want to change the design of how file editing is handled.
131 | if (File.Exists(argumentFilePath))
132 | {
133 | string argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
134 | string argumentTextAreaInput = EditorGUILayout.TextArea(argument,
135 | GUILayout.Height(50),
136 | GUILayout.MaxWidth(300)
137 | );
138 | File.WriteAllText(argumentFilePath, argumentTextAreaInput, System.Text.Encoding.UTF8);
139 | }
140 | else
141 | {
142 | EditorGUILayout.LabelField("No argument file found.");
143 | }
144 |
145 | EditorGUILayout.Space();
146 | EditorGUILayout.Space();
147 | EditorGUILayout.Space();
148 |
149 |
150 | EditorGUI.BeginDisabledGroup(isOpenInAnotherInstance);
151 |
152 | if (GUILayout.Button("Open in New Editor"))
153 | {
154 | ClonesManager.OpenProject(cloneProjectPath);
155 | }
156 |
157 | GUILayout.BeginHorizontal();
158 | if (GUILayout.Button("Delete"))
159 | {
160 | bool delete = EditorUtility.DisplayDialog(
161 | "Delete the clone?",
162 | "Are you sure you want to delete the clone project '" + ClonesManager.GetCurrentProject().name + "_clone'?",
163 | "Delete",
164 | "Cancel");
165 | if (delete)
166 | {
167 | ClonesManager.DeleteClone(cloneProjectPath);
168 | }
169 | }
170 |
171 | GUILayout.EndHorizontal();
172 | EditorGUI.EndDisabledGroup();
173 | GUILayout.EndVertical();
174 |
175 | }
176 | EditorGUILayout.EndScrollView();
177 |
178 | if (GUILayout.Button("Add new clone"))
179 | {
180 | ClonesManager.CreateCloneFromCurrent();
181 | }
182 |
183 | GUILayout.EndVertical();
184 | GUILayout.FlexibleSpace();
185 | }
186 | else
187 | {
188 | /// If no clone created yet, we must create it.
189 | EditorGUILayout.HelpBox("No project clones found. Create a new one!", MessageType.Info);
190 | if (GUILayout.Button("Create new clone"))
191 | {
192 | ClonesManager.CreateCloneFromCurrent();
193 | }
194 | }
195 | }
196 | }
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/ParrelSync/Editor/ClonesManager.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Diagnostics;
3 | using UnityEngine;
4 | using UnityEditor;
5 | using System.Linq;
6 | using System.IO;
7 | using Debug = UnityEngine.Debug;
8 |
9 | namespace ParrelSync
10 | {
11 | ///
12 | /// Contains all required methods for creating a linked clone of the Unity project.
13 | ///
14 | public class ClonesManager
15 | {
16 | ///
17 | /// Name used for an identifying file created in the clone project directory.
18 | ///
19 | ///
20 | /// (!) Do not change this after the clone was created, because then connection will be lost.
21 | ///
22 | public const string CloneFileName = ".clone";
23 |
24 | ///
25 | /// Suffix added to the end of the project clone name when it is created.
26 | ///
27 | ///
28 | /// (!) Do not change this after the clone was created, because then connection will be lost.
29 | ///
30 | public const string CloneNameSuffix = "_clone";
31 |
32 | public const string ProjectName = "ParrelSync";
33 |
34 | ///
35 | /// The maximum number of clones
36 | ///
37 | public const int MaxCloneProjectCount = 10;
38 |
39 | ///
40 | /// Name of the file for storing clone's argument.
41 | ///
42 | public const string ArgumentFileName = ".parrelsyncarg";
43 |
44 | ///
45 | /// Default argument of the new clone
46 | ///
47 | public const string DefaultArgument = "client";
48 |
49 | #region Managing clones
50 |
51 | ///
52 | /// Creates clone from the project currently open in Unity Editor.
53 | ///
54 | ///
55 | public static Project CreateCloneFromCurrent()
56 | {
57 | if (IsClone())
58 | {
59 | Debug.LogError("This project is already a clone. Cannot clone it.");
60 | return null;
61 | }
62 |
63 | string currentProjectPath = ClonesManager.GetCurrentProjectPath();
64 | return ClonesManager.CreateCloneFromPath(currentProjectPath);
65 | }
66 |
67 | ///
68 | /// Creates clone of the project located at the given path.
69 | ///
70 | ///
71 | ///
72 | public static Project CreateCloneFromPath(string sourceProjectPath)
73 | {
74 | Project sourceProject = new Project(sourceProjectPath);
75 |
76 | string cloneProjectPath = null;
77 |
78 | //Find available clone suffix id
79 | for (int i = 0; i < MaxCloneProjectCount; i++)
80 | {
81 | string originalProjectPath = ClonesManager.GetCurrentProject().projectPath;
82 | string possibleCloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i;
83 |
84 | if (!Directory.Exists(possibleCloneProjectPath))
85 | {
86 | cloneProjectPath = possibleCloneProjectPath;
87 | break;
88 | }
89 | }
90 |
91 | if (string.IsNullOrEmpty(cloneProjectPath))
92 | {
93 | Debug.LogError("The number of cloned projects has reach its limit. Limit: " + MaxCloneProjectCount);
94 | return null;
95 | }
96 |
97 | Project cloneProject = new Project(cloneProjectPath);
98 |
99 | Debug.Log("Start cloning project, original project: " + sourceProject + ", clone project: " + cloneProject);
100 |
101 | ClonesManager.CreateProjectFolder(cloneProject);
102 |
103 | //Copy Folders
104 | Debug.Log("Library copy: " + cloneProject.libraryPath);
105 | ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, cloneProject.libraryPath,
106 | "Cloning Project Library '" + sourceProject.name + "'. ");
107 | Debug.Log("Packages copy: " + cloneProject.libraryPath);
108 | ClonesManager.CopyDirectoryWithProgressBar(sourceProject.packagesPath, cloneProject.packagesPath,
109 | "Cloning Project Packages '" + sourceProject.name + "'. ");
110 |
111 |
112 | //Link Folders
113 | ClonesManager.LinkFolders(sourceProject.assetPath, cloneProject.assetPath);
114 | ClonesManager.LinkFolders(sourceProject.projectSettingsPath, cloneProject.projectSettingsPath);
115 | ClonesManager.LinkFolders(sourceProject.autoBuildPath, cloneProject.autoBuildPath);
116 | ClonesManager.LinkFolders(sourceProject.localPackages, cloneProject.localPackages);
117 |
118 | //Optional Link Folders
119 | var optionalLinkPaths = Preferences.OptionalSymbolicLinkFolders.GetStoredValue();
120 | var projectSettings = ParrelSyncProjectSettings.GetSerializedSettings();
121 | var projectSettingsProperty = projectSettings.FindProperty("m_OptionalSymbolicLinkFolders");
122 | if (projectSettingsProperty is { isArray: true, arrayElementType: "string" })
123 | {
124 | for (var i = 0; i < projectSettingsProperty.arraySize; ++i)
125 | {
126 | optionalLinkPaths.Add(projectSettingsProperty.GetArrayElementAtIndex(i).stringValue);
127 | }
128 | }
129 | foreach (var path in optionalLinkPaths)
130 | {
131 | var sourceOptionalPath = sourceProjectPath + path;
132 | var cloneOptionalPath = cloneProjectPath + path;
133 | LinkFolders(sourceOptionalPath, cloneOptionalPath);
134 | }
135 |
136 | ClonesManager.RegisterClone(cloneProject);
137 |
138 | return cloneProject;
139 | }
140 |
141 | ///
142 | /// Registers a clone by placing an identifying ".clone" file in its root directory.
143 | ///
144 | ///
145 | private static void RegisterClone(Project cloneProject)
146 | {
147 | /// Add clone identifier file.
148 | string identifierFile = Path.Combine(cloneProject.projectPath, ClonesManager.CloneFileName);
149 | File.Create(identifierFile).Dispose();
150 |
151 | //Add argument file with default argument
152 | string argumentFilePath = Path.Combine(cloneProject.projectPath, ClonesManager.ArgumentFileName);
153 | File.WriteAllText(argumentFilePath, DefaultArgument, System.Text.Encoding.UTF8);
154 |
155 | /// Add collabignore.txt to stop the clone from messing with Unity Collaborate if it's enabled. Just in case.
156 | string collabignoreFile = Path.Combine(cloneProject.projectPath, "collabignore.txt");
157 | File.WriteAllText(collabignoreFile, "*"); /// Make it ignore ALL files in the clone.
158 | }
159 |
160 | ///
161 | /// Opens a project located at the given path (if one exists).
162 | ///
163 | ///
164 | public static void OpenProject(string projectPath)
165 | {
166 | if (!Directory.Exists(projectPath))
167 | {
168 | Debug.LogError("Cannot open the project - provided folder (" + projectPath + ") does not exist.");
169 | return;
170 | }
171 |
172 | if (projectPath == ClonesManager.GetCurrentProjectPath())
173 | {
174 | Debug.LogError("Cannot open the project - it is already open.");
175 | return;
176 | }
177 |
178 | //Validate (and update if needed) the "Packages" folder before opening clone project to ensure the clone project will have the
179 | //same "compiling environment" as the original project
180 | ValidateCopiedFoldersIntegrity.ValidateFolder(projectPath, GetOriginalProjectPath(), "Packages");
181 |
182 | string fileName = GetApplicationPath();
183 | string args = "-projectPath \"" + projectPath + "\"";
184 | Debug.Log("Opening project \"" + fileName + " " + args + "\"");
185 | ClonesManager.StartHiddenConsoleProcess(fileName, args);
186 | }
187 |
188 | private static string GetApplicationPath()
189 | {
190 | switch (Application.platform)
191 | {
192 | case RuntimePlatform.WindowsEditor:
193 | return EditorApplication.applicationPath;
194 | case RuntimePlatform.OSXEditor:
195 | return EditorApplication.applicationPath + "/Contents/MacOS/Unity";
196 | case RuntimePlatform.LinuxEditor:
197 | return EditorApplication.applicationPath;
198 | default:
199 | throw new System.NotImplementedException("Platform has not supported yet ;(");
200 | }
201 | }
202 |
203 | ///
204 | /// Is this project being opened by an Unity editor?
205 | ///
206 | ///
207 | ///
208 | public static bool IsCloneProjectRunning(string projectPath)
209 | {
210 |
211 | //Determine whether it is opened in another instance by checking the UnityLockFile
212 | string UnityLockFilePath = new string[] { projectPath, "Temp", "UnityLockfile" }
213 | .Aggregate(Path.Combine);
214 |
215 | switch (Application.platform)
216 | {
217 | case (RuntimePlatform.WindowsEditor):
218 | //Windows editor will lock "UnityLockfile" file when project is being opened.
219 | //Sometime, for instance: windows editor crash, the "UnityLockfile" will not be deleted even the project
220 | //isn't being opened, so a check to the "UnityLockfile" lock status may be necessary.
221 | if (Preferences.AlsoCheckUnityLockFileStaPref.Value)
222 | return File.Exists(UnityLockFilePath) && FileUtilities.IsFileLocked(UnityLockFilePath);
223 | else
224 | return File.Exists(UnityLockFilePath);
225 | case (RuntimePlatform.OSXEditor):
226 | //Mac editor won't lock "UnityLockfile" file when project is being opened
227 | return File.Exists(UnityLockFilePath);
228 | case (RuntimePlatform.LinuxEditor):
229 | return File.Exists(UnityLockFilePath);
230 | default:
231 | throw new System.NotImplementedException("IsCloneProjectRunning: Unsupport Platfrom: " + Application.platform);
232 | }
233 | }
234 |
235 | ///
236 | /// Deletes the clone of the currently open project, if such exists.
237 | ///
238 | public static void DeleteClone(string cloneProjectPath)
239 | {
240 | /// Clone won't be able to delete itself.
241 | if (ClonesManager.IsClone()) return;
242 |
243 | ///Extra precautions.
244 | if (cloneProjectPath == string.Empty) return;
245 | if (cloneProjectPath == ClonesManager.GetOriginalProjectPath()) return;
246 |
247 | //Check what OS is
248 | string identifierFile;
249 | string args;
250 | switch (Application.platform)
251 | {
252 | case (RuntimePlatform.WindowsEditor):
253 | Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
254 |
255 | //The argument file will be deleted first at the beginning of the project deletion process
256 | //to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
257 | //If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
258 | identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
259 | File.Delete(identifierFile);
260 |
261 | args = "/c " + @"rmdir /s/q " + string.Format("\"{0}\"", cloneProjectPath);
262 | StartHiddenConsoleProcess("cmd.exe", args);
263 |
264 | break;
265 | case (RuntimePlatform.OSXEditor):
266 | Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
267 |
268 | //The argument file will be deleted first at the beginning of the project deletion process
269 | //to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
270 | //If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
271 | identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
272 | File.Delete(identifierFile);
273 |
274 | FileUtil.DeleteFileOrDirectory(cloneProjectPath);
275 |
276 | break;
277 | case (RuntimePlatform.LinuxEditor):
278 | Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
279 | identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName);
280 | File.Delete(identifierFile);
281 |
282 | FileUtil.DeleteFileOrDirectory(cloneProjectPath);
283 |
284 | break;
285 | default:
286 | Debug.LogWarning("Not in a known editor. Where are you!?");
287 | break;
288 | }
289 | }
290 |
291 | #endregion
292 |
293 | #region Creating project folders
294 |
295 | ///
296 | /// Creates an empty folder using data in the given Project object
297 | ///
298 | ///
299 | public static void CreateProjectFolder(Project project)
300 | {
301 | string path = project.projectPath;
302 | Debug.Log("Creating new empty folder at: " + path);
303 | Directory.CreateDirectory(path);
304 | }
305 |
306 | ///
307 | /// 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.
308 | ///
309 | ///
310 | ///
311 | [System.Obsolete]
312 | public static void CopyLibraryFolder(Project sourceProject, Project destinationProject)
313 | {
314 | if (Directory.Exists(destinationProject.libraryPath))
315 | {
316 | Debug.LogWarning("Library copy: destination path already exists! ");
317 | return;
318 | }
319 |
320 | Debug.Log("Library copy: " + destinationProject.libraryPath);
321 | ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, destinationProject.libraryPath,
322 | "Cloning project '" + sourceProject.name + "'. ");
323 | }
324 |
325 | #endregion
326 |
327 | #region Creating symlinks
328 |
329 | ///
330 | /// Creates a symlink between destinationPath and sourcePath (Mac version).
331 | ///
332 | ///
333 | ///
334 | private static void CreateLinkMac(string sourcePath, string destinationPath)
335 | {
336 | sourcePath = sourcePath.Replace(" ", "\\ ");
337 | destinationPath = destinationPath.Replace(" ", "\\ ");
338 | var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath);
339 |
340 | Debug.Log("Mac hard link " + command);
341 |
342 | ClonesManager.ExecuteBashCommand(command);
343 | }
344 |
345 | ///
346 | /// Creates a symlink between destinationPath and sourcePath (Linux version).
347 | ///
348 | ///
349 | ///
350 | private static void CreateLinkLinux(string sourcePath, string destinationPath)
351 | {
352 | sourcePath = sourcePath.Replace(" ", "\\ ");
353 | destinationPath = destinationPath.Replace(" ", "\\ ");
354 | var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath);
355 |
356 | Debug.Log("Linux Symlink " + command);
357 |
358 | ClonesManager.ExecuteBashCommand(command);
359 | }
360 |
361 | ///
362 | /// Creates a symlink between destinationPath and sourcePath (Windows version).
363 | ///
364 | ///
365 | ///
366 | private static void CreateLinkWin(string sourcePath, string destinationPath)
367 | {
368 | string cmd = "/C mklink /J " + string.Format("\"{0}\" \"{1}\"", destinationPath, sourcePath);
369 | Debug.Log("Windows junction: " + cmd);
370 | ClonesManager.StartHiddenConsoleProcess("cmd.exe", cmd);
371 | }
372 |
373 | //TODO(?) avoid terminal calls and use proper api stuff. See below for windows!
374 | ////https://docs.microsoft.com/en-us/windows/desktop/api/ioapiset/nf-ioapiset-deviceiocontrol
375 | //[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
376 | //private static extern bool DeviceIoControl(System.IntPtr hDevice, uint dwIoControlCode,
377 | // System.IntPtr InBuffer, int nInBufferSize,
378 | // System.IntPtr OutBuffer, int nOutBufferSize,
379 | // out int pBytesReturned, System.IntPtr lpOverlapped);
380 |
381 | ///
382 | /// Create a link / junction from the original project to it's clone.
383 | ///
384 | ///
385 | ///
386 | public static void LinkFolders(string sourcePath, string destinationPath)
387 | {
388 | if ((Directory.Exists(destinationPath) == false) && (Directory.Exists(sourcePath) == true))
389 | {
390 | switch (Application.platform)
391 | {
392 | case (RuntimePlatform.WindowsEditor):
393 | CreateLinkWin(sourcePath, destinationPath);
394 | break;
395 | case (RuntimePlatform.OSXEditor):
396 | CreateLinkMac(sourcePath, destinationPath);
397 | break;
398 | case (RuntimePlatform.LinuxEditor):
399 | CreateLinkLinux(sourcePath, destinationPath);
400 | break;
401 | default:
402 | Debug.LogWarning("Not in a known editor. Application.platform: " + Application.platform);
403 | break;
404 | }
405 | }
406 | else
407 | {
408 | Debug.LogWarning("Skipping Asset link, it already exists: " + destinationPath);
409 | }
410 | }
411 |
412 | #endregion
413 |
414 | #region Utility methods
415 |
416 | private static bool? isCloneFileExistCache = null;
417 |
418 | ///
419 | /// Returns true if the project currently open in Unity Editor is a clone.
420 | ///
421 | ///
422 | public static bool IsClone()
423 | {
424 | if (isCloneFileExistCache == null)
425 | {
426 | /// The project is a clone if its root directory contains an empty file named ".clone".
427 | string cloneFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.CloneFileName);
428 | isCloneFileExistCache = File.Exists(cloneFilePath);
429 | }
430 |
431 | return (bool)isCloneFileExistCache;
432 | }
433 |
434 | ///
435 | /// Get the path to the current unityEditor project folder's info
436 | ///
437 | ///
438 | public static string GetCurrentProjectPath()
439 | {
440 | return Application.dataPath.Replace("/Assets", "");
441 | }
442 |
443 | ///
444 | /// Return a project object that describes all the paths we need to clone it.
445 | ///
446 | ///
447 | public static Project GetCurrentProject()
448 | {
449 | string pathString = ClonesManager.GetCurrentProjectPath();
450 | return new Project(pathString);
451 | }
452 |
453 | ///
454 | /// Get the argument of this clone project.
455 | /// If this is the original project, will return an empty string.
456 | ///
457 | ///
458 | public static string GetArgument()
459 | {
460 | string argument = "";
461 | if (IsClone())
462 | {
463 | string argumentFilePath = Path.Combine(GetCurrentProjectPath(), ClonesManager.ArgumentFileName);
464 | if (File.Exists(argumentFilePath))
465 | {
466 | argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8);
467 | }
468 | }
469 |
470 | return argument;
471 | }
472 |
473 | ///
474 | /// Returns the path to the original project.
475 | /// If currently open project is the original, returns its own path.
476 | /// If the original project folder cannot be found, retuns an empty string.
477 | ///
478 | ///
479 | public static string GetOriginalProjectPath()
480 | {
481 | if (IsClone())
482 | {
483 | /// If this is a clone...
484 | /// Original project path can be deduced by removing the suffix from the clone's path.
485 | string cloneProjectPath = ClonesManager.GetCurrentProject().projectPath;
486 |
487 | int index = cloneProjectPath.LastIndexOf(ClonesManager.CloneNameSuffix);
488 | if (index > 0)
489 | {
490 | string originalProjectPath = cloneProjectPath.Substring(0, index);
491 | if (Directory.Exists(originalProjectPath)) return originalProjectPath;
492 | }
493 |
494 | return string.Empty;
495 | }
496 | else
497 | {
498 | /// If this is the original, we return its own path.
499 | return ClonesManager.GetCurrentProjectPath();
500 | }
501 | }
502 |
503 | ///
504 | /// Returns all clone projects path.
505 | ///
506 | ///
507 | public static List GetCloneProjectsPath()
508 | {
509 | List projectsPath = new List();
510 | for (int i = 0; i < MaxCloneProjectCount; i++)
511 | {
512 | string originalProjectPath = ClonesManager.GetCurrentProject().projectPath;
513 | string cloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i;
514 |
515 | if (Directory.Exists(cloneProjectPath))
516 | projectsPath.Add(cloneProjectPath);
517 | }
518 |
519 | return projectsPath;
520 | }
521 |
522 | ///
523 | /// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
524 | ///
525 | /// Directory to be copied.
526 | /// Destination directory (created automatically if needed).
527 | /// Optional string added to the beginning of the progress bar window header.
528 | public static void CopyDirectoryWithProgressBar(string sourcePath, string destinationPath,
529 | string progressBarPrefix = "")
530 | {
531 | var source = new DirectoryInfo(sourcePath);
532 | var destination = new DirectoryInfo(destinationPath);
533 |
534 | long totalBytes = 0;
535 | long copiedBytes = 0;
536 |
537 | ClonesManager.CopyDirectoryWithProgressBarRecursive(source, destination, ref totalBytes, ref copiedBytes,
538 | progressBarPrefix);
539 | EditorUtility.ClearProgressBar();
540 | }
541 |
542 | ///
543 | /// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
544 | /// Same as the previous method, but uses recursion to copy all nested folders as well.
545 | ///
546 | /// Directory to be copied.
547 | /// Destination directory (created automatically if needed).
548 | /// Total bytes to be copied. Calculated automatically, initialize at 0.
549 | /// To track already copied bytes. Calculated automatically, initialize at 0.
550 | /// Optional string added to the beginning of the progress bar window header.
551 | private static void CopyDirectoryWithProgressBarRecursive(DirectoryInfo source, DirectoryInfo destination,
552 | ref long totalBytes, ref long copiedBytes, string progressBarPrefix = "")
553 | {
554 | /// Directory cannot be copied into itself.
555 | if (source.FullName.ToLower() == destination.FullName.ToLower())
556 | {
557 | Debug.LogError("Cannot copy directory into itself.");
558 | return;
559 | }
560 |
561 | /// Calculate total bytes, if required.
562 | if (totalBytes == 0)
563 | {
564 | totalBytes = ClonesManager.GetDirectorySize(source, true, progressBarPrefix);
565 | }
566 |
567 | /// Create destination directory, if required.
568 | if (!Directory.Exists(destination.FullName))
569 | {
570 | Directory.CreateDirectory(destination.FullName);
571 | }
572 |
573 | /// Copy all files from the source.
574 | foreach (FileInfo file in source.GetFiles())
575 | {
576 | // Ensure file exists before continuing.
577 | if (!file.Exists)
578 | {
579 | continue;
580 | }
581 |
582 | try
583 | {
584 | file.CopyTo(Path.Combine(destination.ToString(), file.Name), true);
585 | }
586 | catch (IOException)
587 | {
588 | /// Some files may throw IOException if they are currently open in Unity editor.
589 | /// Just ignore them in such case.
590 | }
591 |
592 | /// Account the copied file size.
593 | copiedBytes += file.Length;
594 |
595 | /// Display the progress bar.
596 | float progress = (float)copiedBytes / (float)totalBytes;
597 | bool cancelCopy = EditorUtility.DisplayCancelableProgressBar(
598 | progressBarPrefix + "Copying '" + source.FullName + "' to '" + destination.FullName + "'...",
599 | "(" + (progress * 100f).ToString("F2") + "%) Copying file '" + file.Name + "'...",
600 | progress);
601 | if (cancelCopy) return;
602 | }
603 |
604 | /// Copy all nested directories from the source.
605 | foreach (DirectoryInfo sourceNestedDir in source.GetDirectories())
606 | {
607 | DirectoryInfo nextDestingationNestedDir = destination.CreateSubdirectory(sourceNestedDir.Name);
608 | ClonesManager.CopyDirectoryWithProgressBarRecursive(sourceNestedDir, nextDestingationNestedDir,
609 | ref totalBytes, ref copiedBytes, progressBarPrefix);
610 | }
611 | }
612 |
613 | ///
614 | /// Calculates the size of the given directory. Displays a progress bar.
615 | ///
616 | /// Directory, which size has to be calculated.
617 | /// If true, size will include all nested directories.
618 | /// Optional string added to the beginning of the progress bar window header.
619 | /// Size of the directory in bytes.
620 | private static long GetDirectorySize(DirectoryInfo directory, bool includeNested = false,
621 | string progressBarPrefix = "")
622 | {
623 | EditorUtility.DisplayProgressBar(progressBarPrefix + "Calculating size of directories...",
624 | "Scanning '" + directory.FullName + "'...", 0f);
625 |
626 | /// Calculate size of all files in directory.
627 | long filesSize = directory.GetFiles().Sum((FileInfo file) => file.Exists ? file.Length : 0);
628 |
629 | /// Calculate size of all nested directories.
630 | long directoriesSize = 0;
631 | if (includeNested)
632 | {
633 | IEnumerable nestedDirectories = directory.GetDirectories();
634 | foreach (DirectoryInfo nestedDir in nestedDirectories)
635 | {
636 | directoriesSize += ClonesManager.GetDirectorySize(nestedDir, true, progressBarPrefix);
637 | }
638 | }
639 |
640 | return filesSize + directoriesSize;
641 | }
642 |
643 | ///
644 | /// Starts process in the system console, taking the given fileName and args.
645 | ///
646 | ///
647 | ///
648 | private static void StartHiddenConsoleProcess(string fileName, string args)
649 | {
650 | System.Diagnostics.Process.Start(fileName, args);
651 | }
652 |
653 | ///
654 | /// Thanks to https://github.com/karl-/unity-symlink-utility/blob/master/SymlinkUtility.cs
655 | ///
656 | ///
657 | private static void ExecuteBashCommand(string command)
658 | {
659 | command = command.Replace("\"", "\"\"");
660 |
661 | var proc = new Process()
662 | {
663 | StartInfo = new ProcessStartInfo
664 | {
665 | FileName = "/bin/bash",
666 | Arguments = "-c \"" + command + "\"",
667 | UseShellExecute = false,
668 | RedirectStandardOutput = true,
669 | RedirectStandardError = true,
670 | CreateNoWindow = true
671 | }
672 | };
673 |
674 | using (proc)
675 | {
676 | proc.Start();
677 | proc.WaitForExit();
678 |
679 | if (!proc.StandardError.EndOfStream)
680 | {
681 | UnityEngine.Debug.LogError(proc.StandardError.ReadToEnd());
682 | }
683 | }
684 | }
685 |
686 | public static void OpenProjectInFileExplorer(string path)
687 | {
688 | System.Diagnostics.Process.Start(@path);
689 | }
690 | #endregion
691 | }
692 | }
693 |
--------------------------------------------------------------------------------