当前提交
80021bb5
共有 39 个文件被更改,包括 1859 次插入 和 56 次删除
-
78Assets/Scenes/mainScene.unity
-
1Assets/Scripts/GameLobby/LobbyRelaySample.asmdef
-
53Assets/Scripts/GameLobby/NGO/SetupInGame.cs
-
167ProjectSettings/SceneTemplateSettings.json
-
8Packages/ParrelSync/Editor.meta
-
8Packages/ParrelSync/Editor/AssetModBlock.meta
-
22Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs
-
11Packages/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta
-
34Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs
-
11Packages/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta
-
664Packages/ParrelSync/Editor/ClonesManager.cs
-
11Packages/ParrelSync/Editor/ClonesManager.cs.meta
-
198Packages/ParrelSync/Editor/ClonesManagerWindow.cs
-
11Packages/ParrelSync/Editor/ClonesManagerWindow.cs.meta
-
13Packages/ParrelSync/Editor/ExternalLinks.cs
-
11Packages/ParrelSync/Editor/ExternalLinks.cs.meta
-
31Packages/ParrelSync/Editor/FileUtilities.cs
-
11Packages/ParrelSync/Editor/FileUtilities.cs.meta
-
8Packages/ParrelSync/Editor/NonCore.meta
-
78Packages/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs
-
11Packages/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta
-
26Packages/ParrelSync/Editor/NonCore/OtherMenuItem.cs
-
11Packages/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta
-
110Packages/ParrelSync/Editor/Preferences.cs
-
11Packages/ParrelSync/Editor/Preferences.cs.meta
-
112Packages/ParrelSync/Editor/Project.cs
-
11Packages/ParrelSync/Editor/Project.cs.meta
-
60Packages/ParrelSync/Editor/UpdateChecker.cs
-
11Packages/ParrelSync/Editor/UpdateChecker.cs.meta
-
73Packages/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs
-
11Packages/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta
-
15Packages/ParrelSync/ParrelSync.asmdef
-
7Packages/ParrelSync/ParrelSync.asmdef.meta
-
10Packages/ParrelSync/package.json
-
7Packages/ParrelSync/package.json.meta
|
|||
{ |
|||
"templatePinStates": [], |
|||
"dependencyTypeInfos": [ |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.AnimationClip", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.Animations.AnimatorController", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.AnimatorOverrideController", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.Audio.AudioMixerController", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.ComputeShader", |
|||
"ignore": true, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Cubemap", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.GameObject", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.LightingDataAsset", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": false |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.LightingSettings", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Material", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.MonoScript", |
|||
"ignore": true, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.PhysicMaterial", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.PhysicsMaterial2D", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Rendering.PostProcessing.PostProcessResources", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Rendering.VolumeProfile", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEditor.SceneAsset", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": false |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Shader", |
|||
"ignore": true, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.ShaderVariantCollection", |
|||
"ignore": true, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Texture", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Texture2D", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
}, |
|||
{ |
|||
"userAdded": false, |
|||
"type": "UnityEngine.Timeline.TimelineAsset", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 0, |
|||
"supportsModification": true |
|||
} |
|||
], |
|||
"defaultDependencyTypeInfo": { |
|||
"userAdded": false, |
|||
"type": "<default_scene_template_dependencies>", |
|||
"ignore": false, |
|||
"defaultInstantiationMode": 1, |
|||
"supportsModification": true |
|||
}, |
|||
"newSceneOverride": 0 |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a31ea7d0315594440839cdb0db6bc411 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 8b14e706b1e7cb044b23837e8a70cad9 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using UnityEditor; |
|||
namespace ParrelSync |
|||
{ |
|||
[InitializeOnLoad] |
|||
public class EditorQuit |
|||
{ |
|||
/// <summary>
|
|||
/// Is editor being closed
|
|||
/// </summary>
|
|||
static public bool IsQuiting { get; private set; } |
|||
static void Quit() |
|||
{ |
|||
IsQuiting = true; |
|||
} |
|||
|
|||
static EditorQuit() |
|||
{ |
|||
IsQuiting = false; |
|||
EditorApplication.quitting += Quit; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: bf2888ff90706904abc2d851c3e59e00 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using UnityEditor; |
|||
using UnityEngine; |
|||
namespace ParrelSync |
|||
{ |
|||
/// <summary>
|
|||
/// For preventing assets being modified from the clone instance.
|
|||
/// </summary>
|
|||
public class ParrelSyncAssetModificationProcessor : UnityEditor.AssetModificationProcessor |
|||
{ |
|||
public static string[] OnWillSaveAssets(string[] paths) |
|||
{ |
|||
if (ClonesManager.IsClone() && Preferences.AssetModPref.Value) |
|||
{ |
|||
if (paths != null && paths.Length > 0 && !EditorQuit.IsQuiting) |
|||
{ |
|||
EditorUtility.DisplayDialog( |
|||
ClonesManager.ProjectName + ": Asset modifications saving detected and blocked", |
|||
"Asset modifications saving are blocked in the clone instance. \n\n" + |
|||
"This is a clone of the original project. \n" + |
|||
"Making changes to asset files via the clone editor is not recommended. \n" + |
|||
"Please use the original editor window if you want to make changes to the project files.", |
|||
"ok" |
|||
); |
|||
foreach (var path in paths) |
|||
{ |
|||
Debug.Log("Attempting to save " + path + " are blocked."); |
|||
} |
|||
} |
|||
return new string[0] { }; |
|||
} |
|||
return paths; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 755e570bd21b39440a923056e60f1450 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using UnityEngine; |
|||
using UnityEditor; |
|||
using System.Linq; |
|||
using System.IO; |
|||
using Debug = UnityEngine.Debug; |
|||
|
|||
namespace ParrelSync |
|||
{ |
|||
/// <summary>
|
|||
/// Contains all required methods for creating a linked clone of the Unity project.
|
|||
/// </summary>
|
|||
public class ClonesManager |
|||
{ |
|||
/// <summary>
|
|||
/// Name used for an identifying file created in the clone project directory.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// (!) Do not change this after the clone was created, because then connection will be lost.
|
|||
/// </remarks>
|
|||
public const string CloneFileName = ".clone"; |
|||
|
|||
/// <summary>
|
|||
/// Suffix added to the end of the project clone name when it is created.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// (!) Do not change this after the clone was created, because then connection will be lost.
|
|||
/// </remarks>
|
|||
public const string CloneNameSuffix = "_clone"; |
|||
|
|||
public const string ProjectName = "ParrelSync"; |
|||
|
|||
/// <summary>
|
|||
/// The maximum number of clones
|
|||
/// </summary>
|
|||
public const int MaxCloneProjectCount = 10; |
|||
|
|||
/// <summary>
|
|||
/// Name of the file for storing clone's argument.
|
|||
/// </summary>
|
|||
public const string ArgumentFileName = ".parrelsyncarg"; |
|||
|
|||
/// <summary>
|
|||
/// Default argument of the new clone
|
|||
/// </summary>
|
|||
public const string DefaultArgument = "client"; |
|||
|
|||
#region Managing clones
|
|||
|
|||
/// <summary>
|
|||
/// Creates clone from the project currently open in Unity Editor.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static Project CreateCloneFromCurrent() |
|||
{ |
|||
if (IsClone()) |
|||
{ |
|||
Debug.LogError("This project is already a clone. Cannot clone it."); |
|||
return null; |
|||
} |
|||
|
|||
string currentProjectPath = ClonesManager.GetCurrentProjectPath(); |
|||
return ClonesManager.CreateCloneFromPath(currentProjectPath); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates clone of the project located at the given path.
|
|||
/// </summary>
|
|||
/// <param name="sourceProjectPath"></param>
|
|||
/// <returns></returns>
|
|||
public static Project CreateCloneFromPath(string sourceProjectPath) |
|||
{ |
|||
Project sourceProject = new Project(sourceProjectPath); |
|||
|
|||
string cloneProjectPath = null; |
|||
|
|||
//Find available clone suffix id
|
|||
for (int i = 0; i < MaxCloneProjectCount; i++) |
|||
{ |
|||
string originalProjectPath = ClonesManager.GetCurrentProject().projectPath; |
|||
string possibleCloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i; |
|||
|
|||
if (!Directory.Exists(possibleCloneProjectPath)) |
|||
{ |
|||
cloneProjectPath = possibleCloneProjectPath; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(cloneProjectPath)) |
|||
{ |
|||
Debug.LogError("The number of cloned projects has reach its limit. Limit: " + MaxCloneProjectCount); |
|||
return null; |
|||
} |
|||
|
|||
Project cloneProject = new Project(cloneProjectPath); |
|||
|
|||
Debug.Log("Start cloning project, original project: " + sourceProject + ", clone project: " + cloneProject); |
|||
|
|||
ClonesManager.CreateProjectFolder(cloneProject); |
|||
|
|||
//Copy Folders
|
|||
Debug.Log("Library copy: " + cloneProject.libraryPath); |
|||
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, cloneProject.libraryPath, |
|||
"Cloning Project Library '" + sourceProject.name + "'. "); |
|||
Debug.Log("Packages copy: " + cloneProject.libraryPath); |
|||
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.packagesPath, cloneProject.packagesPath, |
|||
"Cloning Project Packages '" + sourceProject.name + "'. "); |
|||
|
|||
|
|||
//Link Folders
|
|||
ClonesManager.LinkFolders(sourceProject.assetPath, cloneProject.assetPath); |
|||
ClonesManager.LinkFolders(sourceProject.projectSettingsPath, cloneProject.projectSettingsPath); |
|||
ClonesManager.LinkFolders(sourceProject.autoBuildPath, cloneProject.autoBuildPath); |
|||
ClonesManager.LinkFolders(sourceProject.localPackages, cloneProject.localPackages); |
|||
|
|||
ClonesManager.RegisterClone(cloneProject); |
|||
|
|||
return cloneProject; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Registers a clone by placing an identifying ".clone" file in its root directory.
|
|||
/// </summary>
|
|||
/// <param name="cloneProject"></param>
|
|||
private static void RegisterClone(Project cloneProject) |
|||
{ |
|||
/// Add clone identifier file.
|
|||
string identifierFile = Path.Combine(cloneProject.projectPath, ClonesManager.CloneFileName); |
|||
File.Create(identifierFile).Dispose(); |
|||
|
|||
//Add argument file with default argument
|
|||
string argumentFilePath = Path.Combine(cloneProject.projectPath, ClonesManager.ArgumentFileName); |
|||
File.WriteAllText(argumentFilePath, DefaultArgument, System.Text.Encoding.UTF8); |
|||
|
|||
/// Add collabignore.txt to stop the clone from messing with Unity Collaborate if it's enabled. Just in case.
|
|||
string collabignoreFile = Path.Combine(cloneProject.projectPath, "collabignore.txt"); |
|||
File.WriteAllText(collabignoreFile, "*"); /// Make it ignore ALL files in the clone.
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Opens a project located at the given path (if one exists).
|
|||
/// </summary>
|
|||
/// <param name="projectPath"></param>
|
|||
public static void OpenProject(string projectPath) |
|||
{ |
|||
if (!Directory.Exists(projectPath)) |
|||
{ |
|||
Debug.LogError("Cannot open the project - provided folder (" + projectPath + ") does not exist."); |
|||
return; |
|||
} |
|||
|
|||
if (projectPath == ClonesManager.GetCurrentProjectPath()) |
|||
{ |
|||
Debug.LogError("Cannot open the project - it is already open."); |
|||
return; |
|||
} |
|||
|
|||
string fileName = GetApplicationPath(); |
|||
string args = "-projectPath \"" + projectPath + "\""; |
|||
Debug.Log("Opening project \"" + fileName + " " + args + "\""); |
|||
ClonesManager.StartHiddenConsoleProcess(fileName, args); |
|||
} |
|||
|
|||
private static string GetApplicationPath() |
|||
{ |
|||
switch (Application.platform) |
|||
{ |
|||
case RuntimePlatform.WindowsEditor: |
|||
return EditorApplication.applicationPath; |
|||
case RuntimePlatform.OSXEditor: |
|||
return EditorApplication.applicationPath + "/Contents/MacOS/Unity"; |
|||
case RuntimePlatform.LinuxEditor: |
|||
return EditorApplication.applicationPath; |
|||
default: |
|||
throw new System.NotImplementedException("Platform has not supported yet ;("); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Is this project being opened by an Unity editor?
|
|||
/// </summary>
|
|||
/// <param name="projectPath"></param>
|
|||
/// <returns></returns>
|
|||
public static bool IsCloneProjectRunning(string projectPath) |
|||
{ |
|||
|
|||
//Determine whether it is opened in another instance by checking the UnityLockFile
|
|||
string UnityLockFilePath = new string[] { projectPath, "Temp", "UnityLockfile" } |
|||
.Aggregate(Path.Combine); |
|||
|
|||
switch (Application.platform) |
|||
{ |
|||
case (RuntimePlatform.WindowsEditor): |
|||
//Windows editor will lock "UnityLockfile" file when project is being opened.
|
|||
//Sometime, for instance: windows editor crash, the "UnityLockfile" will not be deleted even the project
|
|||
//isn't being opened, so a check to the "UnityLockfile" lock status may be necessary.
|
|||
if (Preferences.AlsoCheckUnityLockFileStaPref.Value) |
|||
return File.Exists(UnityLockFilePath) && FileUtilities.IsFileLocked(UnityLockFilePath); |
|||
else |
|||
return File.Exists(UnityLockFilePath); |
|||
case (RuntimePlatform.OSXEditor): |
|||
//Mac editor won't lock "UnityLockfile" file when project is being opened
|
|||
return File.Exists(UnityLockFilePath); |
|||
case (RuntimePlatform.LinuxEditor): |
|||
return File.Exists(UnityLockFilePath); |
|||
default: |
|||
throw new System.NotImplementedException("IsCloneProjectRunning: Unsupport Platfrom: " + Application.platform); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Deletes the clone of the currently open project, if such exists.
|
|||
/// </summary>
|
|||
public static void DeleteClone(string cloneProjectPath) |
|||
{ |
|||
/// Clone won't be able to delete itself.
|
|||
if (ClonesManager.IsClone()) return; |
|||
|
|||
///Extra precautions.
|
|||
if (cloneProjectPath == string.Empty) return; |
|||
if (cloneProjectPath == ClonesManager.GetOriginalProjectPath()) return; |
|||
|
|||
//Check what OS is
|
|||
string identifierFile; |
|||
string args; |
|||
switch (Application.platform) |
|||
{ |
|||
case (RuntimePlatform.WindowsEditor): |
|||
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); |
|||
|
|||
//The argument file will be deleted first at the beginning of the project deletion process
|
|||
//to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
|
|||
//If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
|
|||
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); |
|||
File.Delete(identifierFile); |
|||
|
|||
args = "/c " + @"rmdir /s/q " + string.Format("\"{0}\"", cloneProjectPath); |
|||
StartHiddenConsoleProcess("cmd.exe", args); |
|||
|
|||
break; |
|||
case (RuntimePlatform.OSXEditor): |
|||
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); |
|||
|
|||
//The argument file will be deleted first at the beginning of the project deletion process
|
|||
//to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.)
|
|||
//If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed.
|
|||
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); |
|||
File.Delete(identifierFile); |
|||
|
|||
FileUtil.DeleteFileOrDirectory(cloneProjectPath); |
|||
|
|||
break; |
|||
case (RuntimePlatform.LinuxEditor): |
|||
Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); |
|||
identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); |
|||
File.Delete(identifierFile); |
|||
|
|||
FileUtil.DeleteFileOrDirectory(cloneProjectPath); |
|||
|
|||
break; |
|||
default: |
|||
Debug.LogWarning("Not in a known editor. Where are you!?"); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
#region Creating project folders
|
|||
|
|||
/// <summary>
|
|||
/// Creates an empty folder using data in the given Project object
|
|||
/// </summary>
|
|||
/// <param name="project"></param>
|
|||
public static void CreateProjectFolder(Project project) |
|||
{ |
|||
string path = project.projectPath; |
|||
Debug.Log("Creating new empty folder at: " + path); |
|||
Directory.CreateDirectory(path); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copies the full contents of the unity library. We want to do this to avoid the lengthy re-serialization of the whole project when it opens up the clone.
|
|||
/// </summary>
|
|||
/// <param name="sourceProject"></param>
|
|||
/// <param name="destinationProject"></param>
|
|||
[System.Obsolete] |
|||
public static void CopyLibraryFolder(Project sourceProject, Project destinationProject) |
|||
{ |
|||
if (Directory.Exists(destinationProject.libraryPath)) |
|||
{ |
|||
Debug.LogWarning("Library copy: destination path already exists! "); |
|||
return; |
|||
} |
|||
|
|||
Debug.Log("Library copy: " + destinationProject.libraryPath); |
|||
ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, destinationProject.libraryPath, |
|||
"Cloning project '" + sourceProject.name + "'. "); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
#region Creating symlinks
|
|||
|
|||
/// <summary>
|
|||
/// Creates a symlink between destinationPath and sourcePath (Mac version).
|
|||
/// </summary>
|
|||
/// <param name="sourcePath"></param>
|
|||
/// <param name="destinationPath"></param>
|
|||
private static void CreateLinkMac(string sourcePath, string destinationPath) |
|||
{ |
|||
sourcePath = sourcePath.Replace(" ", "\\ "); |
|||
destinationPath = destinationPath.Replace(" ", "\\ "); |
|||
var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath); |
|||
|
|||
Debug.Log("Mac hard link " + command); |
|||
|
|||
ClonesManager.ExecuteBashCommand(command); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a symlink between destinationPath and sourcePath (Linux version).
|
|||
/// </summary>
|
|||
/// <param name="sourcePath"></param>
|
|||
/// <param name="destinationPath"></param>
|
|||
private static void CreateLinkLinux(string sourcePath, string destinationPath) |
|||
{ |
|||
sourcePath = sourcePath.Replace(" ", "\\ "); |
|||
destinationPath = destinationPath.Replace(" ", "\\ "); |
|||
var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath); |
|||
|
|||
Debug.Log("Linux Symlink " + command); |
|||
|
|||
ClonesManager.ExecuteBashCommand(command); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a symlink between destinationPath and sourcePath (Windows version).
|
|||
/// </summary>
|
|||
/// <param name="sourcePath"></param>
|
|||
/// <param name="destinationPath"></param>
|
|||
private static void CreateLinkWin(string sourcePath, string destinationPath) |
|||
{ |
|||
string cmd = "/C mklink /J " + string.Format("\"{0}\" \"{1}\"", destinationPath, sourcePath); |
|||
Debug.Log("Windows junction: " + cmd); |
|||
ClonesManager.StartHiddenConsoleProcess("cmd.exe", cmd); |
|||
} |
|||
|
|||
//TODO(?) avoid terminal calls and use proper api stuff. See below for windows!
|
|||
////https://docs.microsoft.com/en-us/windows/desktop/api/ioapiset/nf-ioapiset-deviceiocontrol
|
|||
//[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|||
//private static extern bool DeviceIoControl(System.IntPtr hDevice, uint dwIoControlCode,
|
|||
// System.IntPtr InBuffer, int nInBufferSize,
|
|||
// System.IntPtr OutBuffer, int nOutBufferSize,
|
|||
// out int pBytesReturned, System.IntPtr lpOverlapped);
|
|||
|
|||
/// <summary>
|
|||
/// Create a link / junction from the original project to it's clone.
|
|||
/// </summary>
|
|||
/// <param name="sourcePath"></param>
|
|||
/// <param name="destinationPath"></param>
|
|||
public static void LinkFolders(string sourcePath, string destinationPath) |
|||
{ |
|||
if ((Directory.Exists(destinationPath) == false) && (Directory.Exists(sourcePath) == true)) |
|||
{ |
|||
switch (Application.platform) |
|||
{ |
|||
case (RuntimePlatform.WindowsEditor): |
|||
CreateLinkWin(sourcePath, destinationPath); |
|||
break; |
|||
case (RuntimePlatform.OSXEditor): |
|||
CreateLinkMac(sourcePath, destinationPath); |
|||
break; |
|||
case (RuntimePlatform.LinuxEditor): |
|||
CreateLinkLinux(sourcePath, destinationPath); |
|||
break; |
|||
default: |
|||
Debug.LogWarning("Not in a known editor. Application.platform: " + Application.platform); |
|||
break; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Debug.LogWarning("Skipping Asset link, it already exists: " + destinationPath); |
|||
} |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
#region Utility methods
|
|||
|
|||
private static bool? isCloneFileExistCache = null; |
|||
|
|||
/// <summary>
|
|||
/// Returns true if the project currently open in Unity Editor is a clone.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static bool IsClone() |
|||
{ |
|||
if (isCloneFileExistCache == null) |
|||
{ |
|||
/// The project is a clone if its root directory contains an empty file named ".clone".
|
|||
string cloneFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.CloneFileName); |
|||
isCloneFileExistCache = File.Exists(cloneFilePath); |
|||
} |
|||
|
|||
return (bool)isCloneFileExistCache; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the path to the current unityEditor project folder's info
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static string GetCurrentProjectPath() |
|||
{ |
|||
return Application.dataPath.Replace("/Assets", ""); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Return a project object that describes all the paths we need to clone it.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static Project GetCurrentProject() |
|||
{ |
|||
string pathString = ClonesManager.GetCurrentProjectPath(); |
|||
return new Project(pathString); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the argument of this clone project.
|
|||
/// If this is the original project, will return an empty string.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static string GetArgument() |
|||
{ |
|||
string argument = ""; |
|||
if (IsClone()) |
|||
{ |
|||
string argumentFilePath = Path.Combine(GetCurrentProjectPath(), ClonesManager.ArgumentFileName); |
|||
if (File.Exists(argumentFilePath)) |
|||
{ |
|||
argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8); |
|||
} |
|||
} |
|||
|
|||
return argument; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the path to the original project.
|
|||
/// If currently open project is the original, returns its own path.
|
|||
/// If the original project folder cannot be found, retuns an empty string.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static string GetOriginalProjectPath() |
|||
{ |
|||
if (IsClone()) |
|||
{ |
|||
/// If this is a clone...
|
|||
/// Original project path can be deduced by removing the suffix from the clone's path.
|
|||
string cloneProjectPath = ClonesManager.GetCurrentProject().projectPath; |
|||
|
|||
int index = cloneProjectPath.LastIndexOf(ClonesManager.CloneNameSuffix); |
|||
if (index > 0) |
|||
{ |
|||
string originalProjectPath = cloneProjectPath.Substring(0, index); |
|||
if (Directory.Exists(originalProjectPath)) return originalProjectPath; |
|||
} |
|||
|
|||
return string.Empty; |
|||
} |
|||
else |
|||
{ |
|||
/// If this is the original, we return its own path.
|
|||
return ClonesManager.GetCurrentProjectPath(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns all clone projects path.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public static List<string> GetCloneProjectsPath() |
|||
{ |
|||
List<string> projectsPath = new List<string>(); |
|||
for (int i = 0; i < MaxCloneProjectCount; i++) |
|||
{ |
|||
string originalProjectPath = ClonesManager.GetCurrentProject().projectPath; |
|||
string cloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i; |
|||
|
|||
if (Directory.Exists(cloneProjectPath)) |
|||
projectsPath.Add(cloneProjectPath); |
|||
} |
|||
|
|||
return projectsPath; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
|
|||
/// </summary>
|
|||
/// <param name="source">Directory to be copied.</param>
|
|||
/// <param name="destination">Destination directory (created automatically if needed).</param>
|
|||
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
|
|||
public static void CopyDirectoryWithProgressBar(string sourcePath, string destinationPath, |
|||
string progressBarPrefix = "") |
|||
{ |
|||
var source = new DirectoryInfo(sourcePath); |
|||
var destination = new DirectoryInfo(destinationPath); |
|||
|
|||
long totalBytes = 0; |
|||
long copiedBytes = 0; |
|||
|
|||
ClonesManager.CopyDirectoryWithProgressBarRecursive(source, destination, ref totalBytes, ref copiedBytes, |
|||
progressBarPrefix); |
|||
EditorUtility.ClearProgressBar(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
|
|||
/// Same as the previous method, but uses recursion to copy all nested folders as well.
|
|||
/// </summary>
|
|||
/// <param name="source">Directory to be copied.</param>
|
|||
/// <param name="destination">Destination directory (created automatically if needed).</param>
|
|||
/// <param name="totalBytes">Total bytes to be copied. Calculated automatically, initialize at 0.</param>
|
|||
/// <param name="copiedBytes">To track already copied bytes. Calculated automatically, initialize at 0.</param>
|
|||
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
|
|||
private static void CopyDirectoryWithProgressBarRecursive(DirectoryInfo source, DirectoryInfo destination, |
|||
ref long totalBytes, ref long copiedBytes, string progressBarPrefix = "") |
|||
{ |
|||
/// Directory cannot be copied into itself.
|
|||
if (source.FullName.ToLower() == destination.FullName.ToLower()) |
|||
{ |
|||
Debug.LogError("Cannot copy directory into itself."); |
|||
return; |
|||
} |
|||
|
|||
/// Calculate total bytes, if required.
|
|||
if (totalBytes == 0) |
|||
{ |
|||
totalBytes = ClonesManager.GetDirectorySize(source, true, progressBarPrefix); |
|||
} |
|||
|
|||
/// Create destination directory, if required.
|
|||
if (!Directory.Exists(destination.FullName)) |
|||
{ |
|||
Directory.CreateDirectory(destination.FullName); |
|||
} |
|||
|
|||
/// Copy all files from the source.
|
|||
foreach (FileInfo file in source.GetFiles()) |
|||
{ |
|||
try |
|||
{ |
|||
file.CopyTo(Path.Combine(destination.ToString(), file.Name), true); |
|||
} |
|||
catch (IOException) |
|||
{ |
|||
/// Some files may throw IOException if they are currently open in Unity editor.
|
|||
/// Just ignore them in such case.
|
|||
} |
|||
|
|||
/// Account the copied file size.
|
|||
copiedBytes += file.Length; |
|||
|
|||
/// Display the progress bar.
|
|||
float progress = (float)copiedBytes / (float)totalBytes; |
|||
bool cancelCopy = EditorUtility.DisplayCancelableProgressBar( |
|||
progressBarPrefix + "Copying '" + source.FullName + "' to '" + destination.FullName + "'...", |
|||
"(" + (progress * 100f).ToString("F2") + "%) Copying file '" + file.Name + "'...", |
|||
progress); |
|||
if (cancelCopy) return; |
|||
} |
|||
|
|||
/// Copy all nested directories from the source.
|
|||
foreach (DirectoryInfo sourceNestedDir in source.GetDirectories()) |
|||
{ |
|||
DirectoryInfo nextDestingationNestedDir = destination.CreateSubdirectory(sourceNestedDir.Name); |
|||
ClonesManager.CopyDirectoryWithProgressBarRecursive(sourceNestedDir, nextDestingationNestedDir, |
|||
ref totalBytes, ref copiedBytes, progressBarPrefix); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates the size of the given directory. Displays a progress bar.
|
|||
/// </summary>
|
|||
/// <param name="directory">Directory, which size has to be calculated.</param>
|
|||
/// <param name="includeNested">If true, size will include all nested directories.</param>
|
|||
/// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
|
|||
/// <returns>Size of the directory in bytes.</returns>
|
|||
private static long GetDirectorySize(DirectoryInfo directory, bool includeNested = false, |
|||
string progressBarPrefix = "") |
|||
{ |
|||
EditorUtility.DisplayProgressBar(progressBarPrefix + "Calculating size of directories...", |
|||
"Scanning '" + directory.FullName + "'...", 0f); |
|||
|
|||
/// Calculate size of all files in directory.
|
|||
long filesSize = directory.GetFiles().Sum((FileInfo file) => file.Length); |
|||
|
|||
/// Calculate size of all nested directories.
|
|||
long directoriesSize = 0; |
|||
if (includeNested) |
|||
{ |
|||
IEnumerable<DirectoryInfo> nestedDirectories = directory.GetDirectories(); |
|||
foreach (DirectoryInfo nestedDir in nestedDirectories) |
|||
{ |
|||
directoriesSize += ClonesManager.GetDirectorySize(nestedDir, true, progressBarPrefix); |
|||
} |
|||
} |
|||
|
|||
return filesSize + directoriesSize; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Starts process in the system console, taking the given fileName and args.
|
|||
/// </summary>
|
|||
/// <param name="fileName"></param>
|
|||
/// <param name="args"></param>
|
|||
private static void StartHiddenConsoleProcess(string fileName, string args) |
|||
{ |
|||
System.Diagnostics.Process.Start(fileName, args); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Thanks to https://github.com/karl-/unity-symlink-utility/blob/master/SymlinkUtility.cs
|
|||
/// </summary>
|
|||
/// <param name="command"></param>
|
|||
private static void ExecuteBashCommand(string command) |
|||
{ |
|||
command = command.Replace("\"", "\"\""); |
|||
|
|||
var proc = new Process() |
|||
{ |
|||
StartInfo = new ProcessStartInfo |
|||
{ |
|||
FileName = "/bin/bash", |
|||
Arguments = "-c \"" + command + "\"", |
|||
UseShellExecute = false, |
|||
RedirectStandardOutput = true, |
|||
RedirectStandardError = true, |
|||
CreateNoWindow = true |
|||
} |
|||
}; |
|||
|
|||
using (proc) |
|||
{ |
|||
proc.Start(); |
|||
proc.WaitForExit(); |
|||
|
|||
if (!proc.StandardError.EndOfStream) |
|||
{ |
|||
UnityEngine.Debug.LogError(proc.StandardError.ReadToEnd()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static void OpenProjectInFileExplorer(string path) |
|||
{ |
|||
System.Diagnostics.Process.Start(@path); |
|||
} |
|||
#endregion
|
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 6148e48ed6b61d748b187d06d3687b83 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a041d83486c20b84bbf5077ddfbbca37 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
namespace ParrelSync |
|||
{ |
|||
public class ExternalLinks |
|||
{ |
|||
public const string RemoteVersionURL = "https://raw.githubusercontent.com/VeriorPies/ParrelSync/master/VERSION.txt"; |
|||
public const string Releases = "https://github.com/VeriorPies/ParrelSync/releases"; |
|||
public const string CustomArgumentHelpLink = "https://github.com/VeriorPies/ParrelSync/wiki/Argument"; |
|||
|
|||
public const string GitHubHome = "https://github.com/VeriorPies/ParrelSync/"; |
|||
public const string GitHubIssue = "https://github.com/VeriorPies/ParrelSync/issues"; |
|||
public const string FAQ = "https://github.com/VeriorPies/ParrelSync/wiki/Troubleshooting-&-FAQs"; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 65daf17fbe5101b41977305639f30c65 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System.IO; |
|||
using UnityEngine; |
|||
|
|||
namespace ParrelSync |
|||
{ |
|||
public class FileUtilities : MonoBehaviour |
|||
{ |
|||
public static bool IsFileLocked(string path) |
|||
{ |
|||
FileInfo file = new FileInfo(path); |
|||
try |
|||
{ |
|||
using (FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None)) |
|||
{ |
|||
stream.Close(); |
|||
} |
|||
} |
|||
catch (IOException) |
|||
{ |
|||
//the file is unavailable because it is:
|
|||
//still being written to
|
|||
//or being processed by another thread
|
|||
//or does not exist (has already been processed)
|
|||
return true; |
|||
} |
|||
|
|||
//file is not locked
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 11fdc6f78f8c965499a870ca06dca6bc |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 74a7aa389726f964ab34c52e208c2a43 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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");
|
|||
//}
|
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 894412a5b602e6c4ba2cf2d01f4f92b5 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 7191fa4bfa12ae749b27f73ed292eaf1 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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; |
|||
} |
|||
} |
|||
|
|||
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.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); |
|||
|
|||
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(); |
|||
if (GUILayout.Button("Reset to default")) |
|||
{ |
|||
AssetModPref.ClearValue(); |
|||
AlsoCheckUnityLockFileStaPref.ClearValue(); |
|||
Debug.Log("Editor preferences cleared"); |
|||
} |
|||
GUILayout.EndVertical(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 24641be1c0410a745b529e61b508679f |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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"; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: ec8d3a1577179ef44815739178cf75b4 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
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.0"; |
|||
[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" |
|||
); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: d3453b3f1a20ea148b5028f8556a7be5 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
namespace ParrelSync.NonCore |
|||
{ |
|||
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("Packages"); |
|||
} |
|||
} |
|||
|
|||
static void ValidateFolder(string folderName) |
|||
{ |
|||
var currentProjectPath = Path.Combine(ClonesManager.GetCurrentProjectPath(), folderName); |
|||
var currentFolderHash = CreateMd5ForFolder(currentProjectPath); |
|||
|
|||
var originalProjectPath = Path.Combine(ClonesManager.GetOriginalProjectPath(), folderName); |
|||
var originalFolderHash = CreateMd5ForFolder(originalProjectPath); |
|||
|
|||
if (currentFolderHash != originalFolderHash) |
|||
{ |
|||
Debug.Log("ParrelSync: Detected '" + folderName + "' folder changes in the original project. Updating..."); |
|||
FileUtil.ReplaceDirectory(originalProjectPath, currentProjectPath); |
|||
} |
|||
} |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: d8fb344b9abf5274abd744833474b087 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
{ |
|||
"name": "ParrelSync", |
|||
"references": [], |
|||
"includePlatforms": [ |
|||
"Editor" |
|||
], |
|||
"excludePlatforms": [], |
|||
"allowUnsafeCode": false, |
|||
"overrideReferences": false, |
|||
"precompiledReferences": [], |
|||
"autoReferenced": true, |
|||
"defineConstraints": [], |
|||
"versionDefines": [], |
|||
"noEngineReferences": false |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 894a6cc6ed5cd2645bb542978cbed6a9 |
|||
AssemblyDefinitionImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
{ |
|||
"name": "com.veriorpies.parrelsync", |
|||
"displayName": "ParrelSync", |
|||
"version": "1.5.0", |
|||
"unity": "2018.4", |
|||
"description": "ParrelSync is a Unity editor extension that allows users to test multiplayer gameplay without building the project by having another Unity editor window opened and mirror the changes from the original project.", |
|||
"license": "MIT", |
|||
"keywords": [ "Networking", "Utils", "Editor", "Extensions" ], |
|||
"dependencies": {} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a2a889c264e34b47a7349cbcb2cbedd7 |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
撰写
预览
正在加载...
取消
保存
Reference in new issue