├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── need-help.md │ └── other.md ├── .gitignore ├── Examples.meta ├── Examples ├── CustomArgumentExample.cs └── CustomArgumentExample.cs.meta ├── Images.meta ├── Images ├── .gitignore ├── AfterImported.png ├── ScreenShot 1.png ├── ScreenShot 1.png.meta ├── ScreenShot 2.png ├── ScreenShot 2.png.meta ├── Showcase 1.gif ├── Showcase 1.gif.meta ├── UPM_1.png ├── UPM_2.png ├── doc.meta └── doc │ ├── Argument.png │ ├── Argument.png.meta │ ├── UpdateButton.png │ └── UpdateButton.png.meta ├── LICENSE.md ├── LICENSE.md.meta ├── ParrelSync.meta ├── ParrelSync ├── Editor.meta ├── Editor │ ├── AssetModBlock.meta │ ├── AssetModBlock │ │ ├── EditorQuit.cs │ │ ├── EditorQuit.cs.meta │ │ ├── ParrelSyncAssetModificationProcessor.cs │ │ └── ParrelSyncAssetModificationProcessor.cs.meta │ ├── ClonesManager.cs │ ├── ClonesManager.cs.meta │ ├── ClonesManagerWindow.cs │ ├── ClonesManagerWindow.cs.meta │ ├── ExternalLinks.cs │ ├── ExternalLinks.cs.meta │ ├── FileUtilities.cs │ ├── FileUtilities.cs.meta │ ├── NonCore.meta │ ├── NonCore │ │ ├── AskFeedbackDialog.cs │ │ ├── AskFeedbackDialog.cs.meta │ │ ├── OtherMenuItem.cs │ │ └── OtherMenuItem.cs.meta │ ├── ParrelSyncProjectSettings.cs │ ├── ParrelSyncProjectSettings.cs.meta │ ├── Preferences.cs │ ├── Preferences.cs.meta │ ├── Project.cs │ ├── Project.cs.meta │ ├── UpdateChecker.cs │ ├── UpdateChecker.cs.meta │ ├── ValidateCopiedFoldersIntegrity.cs │ └── ValidateCopiedFoldersIntegrity.cs.meta ├── package.json ├── package.json.meta ├── projectCloner.asmdef └── projectCloner.asmdef.meta ├── README.md ├── README.md.meta ├── VERSION.txt └── VERSION.txt.meta /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots/Video** 24 | If applicable, add screenshots or video link( Youtube/Vimeo) to help explain the problem. 25 | 26 | **Enviroment (please complete the following information):** 27 | - Unity Editor Version: (For Example: Windows 2019.3.0f6) 28 | - ParrelSync Version: (For Example: 1.4.3) 29 | - OS Version: (For Example:Windows 10 21H1, macOS Big Sur, Ubuntu 20.04 LTS) 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for this project 4 | title: "[Feature Request]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the feature you'd like** 11 | A clear and concise description of what feature you would like to have. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/need-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Need Help 3 | about: Ask a question if you need help 4 | title: "[Question]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the question** 11 | 12 | **Screenshots/Video** 13 | If applicable, add screenshots or video link( Youtube/Vimeo) to help explain the question. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: other 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Uu]ser[Ss]ettings/ 12 | 13 | # MemoryCaptures can get excessive in size. 14 | # They also could contain extremely sensitive data 15 | /[Mm]emoryCaptures/ 16 | 17 | # Asset meta data should only be ignored when the corresponding asset is also ignored 18 | !/[Aa]ssets/**/*.meta 19 | 20 | # Uncomment this line if you wish to ignore the asset store tools plugin 21 | # /[Aa]ssets/AssetStoreTools* 22 | 23 | # Autogenerated Jetbrains Rider plugin 24 | /[Aa]ssets/Plugins/Editor/JetBrains* 25 | 26 | # Visual Studio cache directory 27 | .vs/ 28 | 29 | # JebBrains Idea directory 30 | .idea/ 31 | 32 | # Gradle cache directory 33 | .gradle/ 34 | 35 | # Autogenerated VS/MD/Consulo solution and project files 36 | ExportedObj/ 37 | .consulo/ 38 | *.csproj 39 | *.unityproj 40 | *.sln 41 | *.suo 42 | *.tmp 43 | *.user 44 | *.userprefs 45 | *.pidb 46 | *.booproj 47 | *.svd 48 | *.pdb 49 | *.mdb 50 | *.opendb 51 | *.VC.db 52 | 53 | # Unity3D generated meta files 54 | *.pidb.meta 55 | *.pdb.meta 56 | *.mdb.meta 57 | 58 | # Unity3D generated file on crash reports 59 | sysinfo.txt 60 | 61 | # Builds 62 | *.apk 63 | *.aab 64 | *.unitypackage 65 | 66 | # Crashlytics generated file 67 | crashlytics-build.properties 68 | 69 | # Packed Addressables 70 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 71 | 72 | # Temporary auto-generated Android Assets 73 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 74 | /[Aa]ssets/[Ss]treamingAssets/aa/* -------------------------------------------------------------------------------- /Examples.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 654be33bd44a76a4c8c180d1da6ad066 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Examples/CustomArgumentExample.cs: -------------------------------------------------------------------------------- 1 | // This should be editor only 2 | #if UNITY_EDITOR 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | 7 | namespace ParrelSync.Example 8 | { 9 | public class CustomArgumentExample : MonoBehaviour 10 | { 11 | // Start is called before the first frame update 12 | void Start() 13 | { 14 | // Is this editor instance running a clone project? 15 | if (ClonesManager.IsClone()) 16 | { 17 | Debug.Log("This is a clone project."); 18 | 19 | //Argument can be set from the clones manager window. 20 | string customArgument = ClonesManager.GetArgument(); 21 | Debug.Log("The custom argument of this clone project is: " + customArgument); 22 | // Do what ever you need with the argument string. 23 | } 24 | else 25 | { 26 | Debug.Log("This is the original project."); 27 | } 28 | } 29 | } 30 | } 31 | #endif -------------------------------------------------------------------------------- /Examples/CustomArgumentExample.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 346d302ecc25a9a41b48b857ce51d873 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Images.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 48414d84ef850864a8230f75a126e924 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Images/.gitignore: -------------------------------------------------------------------------------- 1 | *.meta -------------------------------------------------------------------------------- /Images/AfterImported.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeriorPies/ParrelSync/610157ad762084380380148ba8ce14e266a6da97/Images/AfterImported.png -------------------------------------------------------------------------------- /Images/ScreenShot 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeriorPies/ParrelSync/610157ad762084380380148ba8ce14e266a6da97/Images/ScreenShot 1.png -------------------------------------------------------------------------------- /Images/ScreenShot 1.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8e7ab7b941833114c83d5235b0d3e503 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 10 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 3 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 2048 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | forceMaximumCompressionQuality_BC6H_BC7: 0 73 | spriteSheet: 74 | serializedVersion: 2 75 | sprites: [] 76 | outline: [] 77 | physicsShape: [] 78 | bones: [] 79 | spriteID: 80 | internalID: 0 81 | vertices: [] 82 | indices: 83 | edges: [] 84 | weights: [] 85 | secondaryTextures: [] 86 | spritePackingTag: 87 | pSDRemoveMatte: 0 88 | pSDShowRemoveMatteOption: 0 89 | userData: 90 | assetBundleName: 91 | assetBundleVariant: 92 | -------------------------------------------------------------------------------- /Images/ScreenShot 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeriorPies/ParrelSync/610157ad762084380380148ba8ce14e266a6da97/Images/ScreenShot 2.png -------------------------------------------------------------------------------- /Images/ScreenShot 2.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e7b08aac7381fd442a4530d395b9a5df 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 10 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 3 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 2048 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | forceMaximumCompressionQuality_BC6H_BC7: 0 73 | spriteSheet: 74 | serializedVersion: 2 75 | sprites: [] 76 | outline: [] 77 | physicsShape: [] 78 | bones: [] 79 | spriteID: 80 | internalID: 0 81 | vertices: [] 82 | indices: 83 | edges: [] 84 | weights: [] 85 | secondaryTextures: [] 86 | spritePackingTag: 87 | pSDRemoveMatte: 0 88 | pSDShowRemoveMatteOption: 0 89 | userData: 90 | assetBundleName: 91 | assetBundleVariant: 92 | -------------------------------------------------------------------------------- /Images/Showcase 1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeriorPies/ParrelSync/610157ad762084380380148ba8ce14e266a6da97/Images/Showcase 1.gif -------------------------------------------------------------------------------- /Images/Showcase 1.gif.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4a58501c181cc2949a20c7c0751ace7f 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 10 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 3 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 2048 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | forceMaximumCompressionQuality_BC6H_BC7: 0 73 | spriteSheet: 74 | serializedVersion: 2 75 | sprites: [] 76 | outline: [] 77 | physicsShape: [] 78 | bones: [] 79 | spriteID: 80 | internalID: 0 81 | vertices: [] 82 | indices: 83 | edges: [] 84 | weights: [] 85 | secondaryTextures: [] 86 | spritePackingTag: 87 | pSDRemoveMatte: 0 88 | pSDShowRemoveMatteOption: 0 89 | userData: 90 | assetBundleName: 91 | assetBundleVariant: 92 | -------------------------------------------------------------------------------- /Images/UPM_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeriorPies/ParrelSync/610157ad762084380380148ba8ce14e266a6da97/Images/UPM_1.png -------------------------------------------------------------------------------- /Images/UPM_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeriorPies/ParrelSync/610157ad762084380380148ba8ce14e266a6da97/Images/UPM_2.png -------------------------------------------------------------------------------- /Images/doc.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bf44c3cd2ba9b004d8a6040d11f422c3 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Images/doc/Argument.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeriorPies/ParrelSync/610157ad762084380380148ba8ce14e266a6da97/Images/doc/Argument.png -------------------------------------------------------------------------------- /Images/doc/Argument.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 47741ec536e56b848b8bcd2332d141ea 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 10 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 3 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 2048 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | forceMaximumCompressionQuality_BC6H_BC7: 0 73 | spriteSheet: 74 | serializedVersion: 2 75 | sprites: [] 76 | outline: [] 77 | physicsShape: [] 78 | bones: [] 79 | spriteID: 80 | internalID: 0 81 | vertices: [] 82 | indices: 83 | edges: [] 84 | weights: [] 85 | secondaryTextures: [] 86 | spritePackingTag: 87 | pSDRemoveMatte: 0 88 | pSDShowRemoveMatteOption: 0 89 | userData: 90 | assetBundleName: 91 | assetBundleVariant: 92 | -------------------------------------------------------------------------------- /Images/doc/UpdateButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VeriorPies/ParrelSync/610157ad762084380380148ba8ce14e266a6da97/Images/doc/UpdateButton.png -------------------------------------------------------------------------------- /Images/doc/UpdateButton.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0f26555e30991e44d9039cb9d8e0cd3e 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 10 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 3 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 2048 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | forceMaximumCompressionQuality_BC6H_BC7: 0 73 | spriteSheet: 74 | serializedVersion: 2 75 | sprites: [] 76 | outline: [] 77 | physicsShape: [] 78 | bones: [] 79 | spriteID: 80 | internalID: 0 81 | vertices: [] 82 | indices: 83 | edges: [] 84 | weights: [] 85 | secondaryTextures: [] 86 | spritePackingTag: 87 | pSDRemoveMatte: 0 88 | pSDShowRemoveMatteOption: 0 89 | userData: 90 | assetBundleName: 91 | assetBundleVariant: 92 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Greg M 4 | Copyright (c) 2020 Ian and Contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f4b327eab8d866e4087e166da8cafc09 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /ParrelSync.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8f5fec620d3bc9546a41a5b67cb9f8b6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ParrelSync/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a31ea7d0315594440839cdb0db6bc411 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ParrelSync/Editor/AssetModBlock.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8b14e706b1e7cb044b23837e8a70cad9 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ParrelSync/Editor/AssetModBlock/EditorQuit.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | namespace ParrelSync 3 | { 4 | [InitializeOnLoad] 5 | public class EditorQuit 6 | { 7 | /// 8 | /// Is editor being closed 9 | /// 10 | static public bool IsQuiting { get; private set; } 11 | static void Quit() 12 | { 13 | IsQuiting = true; 14 | } 15 | 16 | static EditorQuit() 17 | { 18 | IsQuiting = false; 19 | EditorApplication.quitting += Quit; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bf2888ff90706904abc2d851c3e59e00 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | namespace ParrelSync 4 | { 5 | /// 6 | /// For preventing assets being modified from the clone instance. 7 | /// 8 | public class ParrelSyncAssetModificationProcessor : UnityEditor.AssetModificationProcessor 9 | { 10 | public static string[] OnWillSaveAssets(string[] paths) 11 | { 12 | if (ClonesManager.IsClone() && Preferences.AssetModPref.Value) 13 | { 14 | if (paths != null && paths.Length > 0 && !EditorQuit.IsQuiting) 15 | { 16 | EditorUtility.DisplayDialog( 17 | ClonesManager.ProjectName + ": Asset modifications saving detected and blocked", 18 | "Asset modifications saving are blocked in the clone instance. \n\n" + 19 | "This is a clone of the original project. \n" + 20 | "Making changes to asset files via the clone editor is not recommended. \n" + 21 | "Please use the original editor window if you want to make changes to the project files.", 22 | "ok" 23 | ); 24 | foreach (var path in paths) 25 | { 26 | Debug.Log("Attempting to save " + path + " are blocked."); 27 | } 28 | } 29 | return new string[0] { }; 30 | } 31 | return paths; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 755e570bd21b39440a923056e60f1450 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ParrelSync/Editor/ClonesManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6148e48ed6b61d748b187d06d3687b83 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /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/ClonesManagerWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a041d83486c20b84bbf5077ddfbbca37 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/Editor/ExternalLinks.cs: -------------------------------------------------------------------------------- 1 | namespace ParrelSync 2 | { 3 | public class ExternalLinks 4 | { 5 | public const string RemoteVersionURL = "https://raw.githubusercontent.com/VeriorPies/ParrelSync/master/VERSION.txt"; 6 | public const string Releases = "https://github.com/VeriorPies/ParrelSync/releases"; 7 | public const string CustomArgumentHelpLink = "https://github.com/VeriorPies/ParrelSync/wiki/Argument"; 8 | 9 | public const string GitHubHome = "https://github.com/VeriorPies/ParrelSync/"; 10 | public const string GitHubIssue = "https://github.com/VeriorPies/ParrelSync/issues"; 11 | public const string FAQ = "https://github.com/VeriorPies/ParrelSync/wiki/Troubleshooting-&-FAQs"; 12 | } 13 | } -------------------------------------------------------------------------------- /ParrelSync/Editor/ExternalLinks.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 65daf17fbe5101b41977305639f30c65 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/Editor/FileUtilities.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using UnityEngine; 3 | 4 | namespace ParrelSync 5 | { 6 | public class FileUtilities 7 | { 8 | public static bool IsFileLocked(string path) 9 | { 10 | FileInfo file = new FileInfo(path); 11 | try 12 | { 13 | using (FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None)) 14 | { 15 | stream.Close(); 16 | } 17 | } 18 | catch (IOException) 19 | { 20 | //the file is unavailable because it is: 21 | //still being written to 22 | //or being processed by another thread 23 | //or does not exist (has already been processed) 24 | return true; 25 | } 26 | 27 | //file is not locked 28 | return false; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ParrelSync/Editor/FileUtilities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 11fdc6f78f8c965499a870ca06dca6bc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/Editor/NonCore.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 74a7aa389726f964ab34c52e208c2a43 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ParrelSync/Editor/NonCore/AskFeedbackDialog.cs: -------------------------------------------------------------------------------- 1 | namespace ParrelSync.NonCore 2 | { 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | /// 7 | /// A simple script to display feedback/star dialog after certain time of project being opened/re-compiled. 8 | /// Will only pop-up once unless "Remind me next time" are chosen. 9 | /// Removing this file from project wont effect any other functions. 10 | /// 11 | [InitializeOnLoad] 12 | public class AskFeedbackDialog 13 | { 14 | const string InitializeOnLoadCountKey = "ParrelSync_InitOnLoadCount", StopShowingKey = "ParrelSync_StopShowFeedBack"; 15 | static AskFeedbackDialog() 16 | { 17 | if (EditorPrefs.HasKey(StopShowingKey)) { return; } 18 | 19 | int InitializeOnLoadCount = EditorPrefs.GetInt(InitializeOnLoadCountKey, 0); 20 | if (InitializeOnLoadCount > 20) 21 | { 22 | ShowDialog(); 23 | } 24 | else 25 | { 26 | EditorPrefs.SetInt(InitializeOnLoadCountKey, InitializeOnLoadCount + 1); 27 | } 28 | } 29 | 30 | //[MenuItem("ParrelSync/(Debug)Show AskFeedbackDialog ")] 31 | private static void ShowDialog() 32 | { 33 | int option = EditorUtility.DisplayDialogComplex("Do you like " + ParrelSync.ClonesManager.ProjectName + "?", 34 | "Do you like " + ParrelSync.ClonesManager.ProjectName + "?\n" + 35 | "If so, please don't hesitate to star it on GitHub and contribute to the project!", 36 | "Star on GitHub", 37 | "Close", 38 | "Remind me next time" 39 | ); 40 | 41 | switch (option) 42 | { 43 | // First parameter. 44 | case 0: 45 | Debug.Log("AskFeedbackDialog: Star on GitHub selected"); 46 | EditorPrefs.SetBool(StopShowingKey, true); 47 | EditorPrefs.DeleteKey(InitializeOnLoadCountKey); 48 | Application.OpenURL(ExternalLinks.GitHubHome); 49 | break; 50 | // Second parameter. 51 | case 1: 52 | Debug.Log("AskFeedbackDialog: Close and never show again."); 53 | EditorPrefs.SetBool(StopShowingKey, true); 54 | EditorPrefs.DeleteKey(InitializeOnLoadCountKey); 55 | break; 56 | // Third parameter. 57 | case 2: 58 | Debug.Log("AskFeedbackDialog: Remind me next time"); 59 | EditorPrefs.SetInt(InitializeOnLoadCountKey, 0); 60 | break; 61 | default: 62 | //Debug.Log("Close windows."); 63 | break; 64 | } 65 | } 66 | 67 | ///// 68 | ///// For debug purpose 69 | ///// 70 | //[MenuItem("ParrelSync/(Debug)Delete AskFeedbackDialog keys")] 71 | //private static void DebugDeleteAllKeys() 72 | //{ 73 | // EditorPrefs.DeleteKey(InitializeOnLoadCountKey); 74 | // EditorPrefs.DeleteKey(StopShowingKey); 75 | // Debug.Log("AskFeedbackDialog keys deleted"); 76 | //} 77 | } 78 | } -------------------------------------------------------------------------------- /ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 894412a5b602e6c4ba2cf2d01f4f92b5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/Editor/NonCore/OtherMenuItem.cs: -------------------------------------------------------------------------------- 1 | namespace ParrelSync.NonCore 2 | { 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | public class OtherMenuItem 7 | { 8 | [MenuItem("ParrelSync/GitHub/View this project on GitHub", priority = 10)] 9 | private static void OpenGitHub() 10 | { 11 | Application.OpenURL(ExternalLinks.GitHubHome); 12 | } 13 | 14 | [MenuItem("ParrelSync/GitHub/View FAQ", priority = 11)] 15 | private static void OpenFAQ() 16 | { 17 | Application.OpenURL(ExternalLinks.FAQ); 18 | } 19 | 20 | [MenuItem("ParrelSync/GitHub/View Issues", priority = 12)] 21 | private static void OpenGitHubIssues() 22 | { 23 | Application.OpenURL(ExternalLinks.GitHubIssue); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7191fa4bfa12ae749b27f73ed292eaf1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /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/ParrelSyncProjectSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c0011418c9d75434988a06b6df93b283 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /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/Preferences.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 24641be1c0410a745b529e61b508679f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/Editor/Project.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace ParrelSync 5 | { 6 | public class Project : System.ICloneable 7 | { 8 | public string name; 9 | public string projectPath; 10 | string rootPath; 11 | public string assetPath; 12 | public string projectSettingsPath; 13 | public string libraryPath; 14 | public string packagesPath; 15 | public string autoBuildPath; 16 | public string localPackages; 17 | 18 | char[] separator = new char[1] { '/' }; 19 | 20 | 21 | /// 22 | /// Default constructor 23 | /// 24 | public Project() 25 | { 26 | 27 | } 28 | 29 | 30 | /// 31 | /// Initialize the project object by parsing its full path returned by Unity into a bunch of individual folder names and paths. 32 | /// 33 | /// 34 | public Project(string path) 35 | { 36 | ParsePath(path); 37 | } 38 | 39 | 40 | /// 41 | /// Create a new object with the same settings 42 | /// 43 | /// 44 | public object Clone() 45 | { 46 | Project newProject = new Project(); 47 | newProject.rootPath = rootPath; 48 | newProject.projectPath = projectPath; 49 | newProject.assetPath = assetPath; 50 | newProject.projectSettingsPath = projectSettingsPath; 51 | newProject.libraryPath = libraryPath; 52 | newProject.name = name; 53 | newProject.separator = separator; 54 | newProject.packagesPath = packagesPath; 55 | newProject.autoBuildPath = autoBuildPath; 56 | newProject.localPackages = localPackages; 57 | 58 | 59 | return newProject; 60 | } 61 | 62 | 63 | /// 64 | /// 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. 65 | /// 66 | /// 67 | public void updateNewName(string newName) 68 | { 69 | name = newName; 70 | ParsePath(rootPath + "/" + name + "/Assets"); 71 | } 72 | 73 | 74 | /// 75 | /// Debug override so we can quickly print out the project info. 76 | /// 77 | /// 78 | public override string ToString() 79 | { 80 | string printString = name + "\n" + 81 | rootPath + "\n" + 82 | projectPath + "\n" + 83 | assetPath + "\n" + 84 | projectSettingsPath + "\n" + 85 | packagesPath + "\n" + 86 | autoBuildPath + "\n" + 87 | localPackages + "\n" + 88 | libraryPath; 89 | return (printString); 90 | } 91 | 92 | private void ParsePath(string path) 93 | { 94 | //Unity's Application functions return the Assets path in the Editor. 95 | projectPath = path; 96 | 97 | //pop off the last part of the path for the project name, keep the rest for the root path 98 | List pathArray = projectPath.Split(separator).ToList(); 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 | } -------------------------------------------------------------------------------- /ParrelSync/Editor/Project.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ec8d3a1577179ef44815739178cf75b4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/Editor/UpdateChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEngine; 4 | namespace ParrelSync.Update 5 | { 6 | /// 7 | /// A simple update checker 8 | /// 9 | public class UpdateChecker 10 | { 11 | //const string LocalVersionFilePath = "Assets/ParrelSync/VERSION.txt"; 12 | public const string LocalVersion = "1.5.2"; 13 | [MenuItem("ParrelSync/Check for update", priority = 20)] 14 | static void CheckForUpdate() 15 | { 16 | using (System.Net.WebClient client = new System.Net.WebClient()) 17 | { 18 | try 19 | { 20 | //This won't work with UPM packages 21 | //string localVersionText = AssetDatabase.LoadAssetAtPath(LocalVersionFilePath).text; 22 | 23 | string localVersionText = LocalVersion; 24 | Debug.Log("Local version text : " + LocalVersion); 25 | 26 | string latesteVersionText = client.DownloadString(ExternalLinks.RemoteVersionURL); 27 | Debug.Log("latest version text got: " + latesteVersionText); 28 | string messageBody = "Current Version: " + localVersionText +"\n" 29 | +"Latest Version: " + latesteVersionText + "\n"; 30 | var latestVersion = new Version(latesteVersionText); 31 | var localVersion = new Version(localVersionText); 32 | 33 | if (latestVersion > localVersion) 34 | { 35 | Debug.Log("There's a newer version"); 36 | messageBody += "There's a newer version available"; 37 | if(EditorUtility.DisplayDialog("Check for update.", messageBody, "Get latest release", "Close")) 38 | { 39 | Application.OpenURL(ExternalLinks.Releases); 40 | } 41 | } 42 | else 43 | { 44 | Debug.Log("Current version is up-to-date."); 45 | messageBody += "Current version is up-to-date."; 46 | EditorUtility.DisplayDialog("Check for update.", messageBody,"OK"); 47 | } 48 | 49 | } 50 | catch (Exception exp) 51 | { 52 | Debug.LogError("Error with checking update. Exception: " + exp); 53 | EditorUtility.DisplayDialog("Update Error","Error with checking update. \nSee console for more details.", 54 | "OK" 55 | ); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ParrelSync/Editor/UpdateChecker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d3453b3f1a20ea148b5028f8556a7be5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs: -------------------------------------------------------------------------------- 1 | namespace ParrelSync 2 | { 3 | using UnityEditor; 4 | using UnityEngine; 5 | using System; 6 | using System.Text; 7 | using System.Security.Cryptography; 8 | using System.IO; 9 | using System.Linq; 10 | 11 | [InitializeOnLoad] 12 | public class ValidateCopiedFoldersIntegrity 13 | { 14 | const string SessionStateKey = "ValidateCopiedFoldersIntegrity_Init"; 15 | /// 16 | /// Called once on editor startup. 17 | /// Validate copied folders integrity in clone project 18 | /// 19 | static ValidateCopiedFoldersIntegrity() 20 | { 21 | if (!SessionState.GetBool(SessionStateKey, false)) 22 | { 23 | SessionState.SetBool(SessionStateKey, true); 24 | if (!ClonesManager.IsClone()) { return; } 25 | 26 | ValidateFolder(ClonesManager.GetCurrentProjectPath(), ClonesManager.GetOriginalProjectPath(), "Packages"); 27 | } 28 | } 29 | 30 | public static void ValidateFolder(string targetRoot, string originalRoot, string folderName) 31 | { 32 | var targetFolderPath = Path.Combine(targetRoot, folderName); 33 | var targetFolderHash = CreateMd5ForFolder(targetFolderPath); 34 | 35 | var originalFolderPath = Path.Combine(originalRoot, folderName); 36 | var originalFolderHash = CreateMd5ForFolder(originalFolderPath); 37 | 38 | if (targetFolderHash != originalFolderHash) 39 | { 40 | Debug.Log("ParrelSync: Detected changes in '" + folderName + "' directory. Updating cloned project..."); 41 | FileUtil.ReplaceDirectory(originalFolderPath, targetFolderPath); 42 | } 43 | } 44 | 45 | static string CreateMd5ForFolder(string path) 46 | { 47 | // assuming you want to include nested folders 48 | var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories) 49 | .OrderBy(p => p).ToList(); 50 | 51 | MD5 md5 = MD5.Create(); 52 | 53 | for (int i = 0; i < files.Count; i++) 54 | { 55 | string file = files[i]; 56 | 57 | // hash path 58 | string relativePath = file.Substring(path.Length + 1); 59 | byte[] pathBytes = Encoding.UTF8.GetBytes(relativePath.ToLower()); 60 | md5.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0); 61 | 62 | // hash contents 63 | byte[] contentBytes = File.ReadAllBytes(file); 64 | if (i == files.Count - 1) 65 | md5.TransformFinalBlock(contentBytes, 0, contentBytes.Length); 66 | else 67 | md5.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0); 68 | } 69 | 70 | return BitConverter.ToString(md5.Hash).Replace("-", "").ToLower(); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d8fb344b9abf5274abd744833474b087 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ParrelSync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.veriorpies.parrelsync", 3 | "displayName": "ParrelSync", 4 | "version": "1.5.2", 5 | "unity": "2018.4", 6 | "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.", 7 | "license": "MIT", 8 | "keywords": [ "Networking", "Utils", "Editor", "Extensions" ], 9 | "dependencies": {} 10 | } -------------------------------------------------------------------------------- /ParrelSync/package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a2a889c264e34b47a7349cbcb2cbedd7 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /ParrelSync/projectCloner.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ParrelSync", 3 | "references": [], 4 | "includePlatforms": [ 5 | "Editor" 6 | ], 7 | "excludePlatforms": [], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [], 11 | "autoReferenced": true, 12 | "defineConstraints": [], 13 | "versionDefines": [], 14 | "noEngineReferences": false 15 | } -------------------------------------------------------------------------------- /ParrelSync/projectCloner.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 894a6cc6ed5cd2645bb542978cbed6a9 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ParrelSync 2 | [![Release](https://img.shields.io/github/v/release/VeriorPies/ParrelSync)](https://github.com/VeriorPies/ParrelSync/releases) [![Documentation](https://img.shields.io/badge/documentation-brightgreen.svg)](https://github.com/VeriorPies/ParrelSync/wiki) [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/VeriorPies/ParrelSync/blob/master/LICENSE.md) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-blue.svg)](https://github.com/VeriorPies/ParrelSync/pulls) [![Chats](https://img.shields.io/discord/710688100996743200)](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 | ![ShortGif](https://raw.githubusercontent.com/VeriorPies/ParrelSync/master/Images/Showcase%201.gif) 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 | ![UpdateButtonInMenu](https://github.com/VeriorPies/ParrelSync/raw/master/Images/AfterImported.png) 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 | ![UPM_Image](https://github.com/VeriorPies/ParrelSync/raw/master/Images/UPM_1.png?raw=true) ![UPM_Image2](https://github.com/VeriorPies/ParrelSync/raw/master/Images/UPM_2.png?raw=true) 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 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fa3f2fa6aced9b54b970c8996957df3b 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 1.5.2 -------------------------------------------------------------------------------- /VERSION.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9833b240625d4e14995c296b87e34b96 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------