diff options
Diffstat (limited to 'Packages/com.vrchat.core.vpm-resolver/Editor/PackageMaker/PackageMakerWindow.cs')
| -rw-r--r-- | Packages/com.vrchat.core.vpm-resolver/Editor/PackageMaker/PackageMakerWindow.cs | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/PackageMaker/PackageMakerWindow.cs b/Packages/com.vrchat.core.vpm-resolver/Editor/PackageMaker/PackageMakerWindow.cs new file mode 100644 index 0000000..4edcbc3 --- /dev/null +++ b/Packages/com.vrchat.core.vpm-resolver/Editor/PackageMaker/PackageMakerWindow.cs @@ -0,0 +1,488 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using VRC.PackageManagement.Core.Types.Packages; + +namespace VRC.PackageManagement.PackageMaker +{ + public class PackageMakerWindow : EditorWindow + { + // VisualElements + private VisualElement _rootView; + private TextField _targetAssetFolderField; + private TextField _packageIDField; + private Button _actionButton; + private EnumField _targetVRCPackageField; + private TextField _authorNameField; + private TextField _authorEmailField; + private TextField _authorUrlField; + private static string _projectDir; + private PackageMakerWindowData _windowData; + + private void LoadDataFromSave() + { + if (!string.IsNullOrWhiteSpace(_windowData.targetAssetFolder)) + { + _targetAssetFolderField.SetValueWithoutNotify(_windowData.targetAssetFolder); + } + _packageIDField.SetValueWithoutNotify(_windowData.packageID); + _targetVRCPackageField.SetValueWithoutNotify(_windowData.relatedPackage); + _authorEmailField.SetValueWithoutNotify(_windowData.authorEmail); + _authorNameField.SetValueWithoutNotify(_windowData.authorName); + _authorUrlField.SetValueWithoutNotify(_windowData.authorUrl); + + RefreshActionButtonState(); + } + + private void OnEnable() + { + _projectDir = Directory.GetParent(Application.dataPath).FullName; + Refresh(); + } + + [MenuItem("VRChat SDK/Utilities/Package Maker")] + public static void ShowWindow() + { + PackageMakerWindow wnd = GetWindow<PackageMakerWindow>(); + wnd.titleContent = new GUIContent("Package Maker"); + } + + [MenuItem("Assets/Export VPM as UnityPackage")] + private static void ExportAsUnityPackage () + { + + var foldersToExport = new List<string>(); + StringBuilder exportFilename = new StringBuilder("exported"); + foreach (string guid in Selection.assetGUIDs) + { + string selectedFolder = AssetDatabase.GUIDToAssetPath(guid); + var manifestPath = Path.Combine(selectedFolder, VRCPackageManifest.Filename); + var manifest = VRCPackageManifest.GetManifestAtPath(manifestPath); + if (manifest == null) + { + Debug.LogWarning($"Could not read valid Package Manifest at {manifestPath}. You need to create this first to export a VPM Package."); + continue; + } + exportFilename.Append($"-{manifest.Id}-{manifest.Version}"); + foldersToExport.Add(selectedFolder); + } + + exportFilename.Append(".unitypackage"); + var exportDir = Path.Combine(Directory.GetCurrentDirectory(), "Exports"); + Directory.CreateDirectory(exportDir); + AssetDatabase.ExportPackage + ( + foldersToExport.ToArray(), + Path.Combine(exportDir, exportFilename.ToString()), + ExportPackageOptions.Recurse | ExportPackageOptions.Interactive + ); + } + + private void Refresh() + { + if (_windowData == null) + { + _windowData = PackageMakerWindowData.GetOrCreate(); + } + + if (_rootView == null) return; + + if (_windowData != null) + { + LoadDataFromSave(); + } + } + + private void RefreshActionButtonState() + { + _actionButton.SetEnabled( + StringIsValidAssetFolder(_windowData.targetAssetFolder) && + !string.IsNullOrWhiteSpace(_windowData.packageID) && + _authorNameField.value != null && + IsValidEmail(_authorEmailField.value) + ); + } + + /// <summary> + /// Unity calls the CreateGUI method automatically when the window needs to display + /// </summary> + private void CreateGUI() + { + if (_windowData == null) + { + _windowData = PackageMakerWindowData.GetOrCreate(); + } + + ScrollView scrollView = new(); + rootVisualElement.Add(scrollView); + + _rootView = scrollView; + _rootView.name = "root-view"; + _rootView.styleSheets.Add((StyleSheet) Resources.Load("PackageMakerWindowStyle")); + + // Create Target Asset folder and register for drag and drop events + _rootView.Add(CreateTargetFolderElement()); + _rootView.Add(CreatePackageIDElement()); + _rootView.Add(CreateAuthorElement()); + _rootView.Add(CreateTargetVRCPackageElement()); + _rootView.Add(CreateActionButton()); + + Refresh(); + } + + public enum VRCPackageEnum + { + None = 0, + Worlds = 1, + Avatars = 2, + Base = 3 + } + + private VisualElement CreateTargetVRCPackageElement() + { + _targetVRCPackageField = new EnumField("Related VRChat Package", VRCPackageEnum.None); + _targetVRCPackageField.RegisterValueChangedCallback(OnTargetVRCPackageChanged); + var box = new Box(); + box.Add(_targetVRCPackageField); + return box; + } + + private void OnTargetVRCPackageChanged(ChangeEvent<Enum> evt) + { + _windowData.relatedPackage = (VRCPackageEnum)evt.newValue; + _windowData.Save(); + } + + private VisualElement CreateActionButton() + { + _actionButton = new Button(OnActionButtonPressed) + { + text = "Convert Assets to Package", + name = "action-button" + }; + return _actionButton; + } + + private void OnActionButtonPressed() + { + bool result = EditorUtility.DisplayDialog("One-Way Conversion", + $"This process will move the assets from {_windowData.targetAssetFolder} into a new Package with the id {_windowData.packageID} and give it references to {_windowData.relatedPackage}.", + "Ok", "Wait, not yet."); + if (result) + { + string newPackageFolderPath = Path.Combine(_projectDir, "Packages", _windowData.packageID); + Directory.CreateDirectory(newPackageFolderPath); + var fullTargetAssetFolder = Path.Combine(_projectDir, _windowData.targetAssetFolder); + DoMigration(fullTargetAssetFolder, newPackageFolderPath); + ForceRefresh(); + } + } + + public static void ForceRefresh () + { + MethodInfo method = typeof( UnityEditor.PackageManager.Client ).GetMethod( "Resolve", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly ); + if( method != null ) + method.Invoke( null, null ); + + AssetDatabase.Refresh(); + } + + private VisualElement CreatePackageIDElement() + { + var box = new Box() + { + name = "package-name-box" + }; + + _packageIDField = new TextField("Package ID", 255, false, false, '*'); + _packageIDField.RegisterValueChangedCallback(OnPackageIDChanged); + box.Add(_packageIDField); + + box.Add(new Label("Lowercase letters, numbers and dots only.") + { + name="description", + tooltip = "Standard practice is reverse domain notation like com.vrchat.packagename. Needs to be unique across VRChat, so if you don't own a domain you can try your username.", + }); + + return box; + } + + private VisualElement CreateAuthorElement() + { + // Construct author fields + _authorNameField = new TextField("Author Name"); + _authorEmailField = new TextField("Author Email"); + _authorUrlField = new TextField("Author URL (optional)"); + + // Save name to window data and toggle the Action Button if its status changed + _authorNameField.RegisterValueChangedCallback((evt) => + { + _windowData.authorName = evt.newValue; + Debug.Log($"Window author name is {evt.newValue}"); + RefreshActionButtonState(); + }); + + // Save email to window data if valid and toggle the Action Button if its status changed + _authorEmailField.RegisterValueChangedCallback((evt) => + { + // Only save email if it appears valid + if (IsValidEmail(evt.newValue)) + { + _windowData.authorEmail = evt.newValue; + } + RefreshActionButtonState(); + }); + + // Save url to window data, doesn't affect action button state + _authorUrlField.RegisterValueChangedCallback((evt) => + { + _windowData.authorUrl = evt.newValue; + }); + + // Add new fields to layout + var box = new Box(); + box.Add(_authorNameField); + box.Add(_authorEmailField); + box.Add(_authorUrlField); + return box; + } + + private bool IsValidEmail(string evtNewValue) + { + try + { + var addr = new System.Net.Mail.MailAddress(evtNewValue); + return addr.Address == evtNewValue; + } + catch + { + return false; + } + } + + private Regex packageIdRegex = new Regex("[^a-z0-9.]"); + private void OnPackageIDChanged(ChangeEvent<string> evt) + { + if (evt.newValue != null) + { + string newId = packageIdRegex.Replace(evt.newValue, "-"); + _packageIDField.SetValueWithoutNotify(newId); + _windowData.packageID = newId; + _windowData.Save(); + } + RefreshActionButtonState(); + } + + private VisualElement CreateTargetFolderElement() + { + var targetFolderBox = new Box() + { + name = "editor-target-box" + }; + + _targetAssetFolderField = new TextField("Target Folder"); + _targetAssetFolderField.RegisterCallback<DragEnterEvent>(OnTargetAssetFolderDragEnter, TrickleDown.TrickleDown); + _targetAssetFolderField.RegisterCallback<DragLeaveEvent>(OnTargetAssetFolderDragLeave, TrickleDown.TrickleDown); + _targetAssetFolderField.RegisterCallback<DragUpdatedEvent>(OnTargetAssetFolderDragUpdated, TrickleDown.TrickleDown); + _targetAssetFolderField.RegisterCallback<DragPerformEvent>(OnTargetAssetFolderDragPerform, TrickleDown.TrickleDown); + _targetAssetFolderField.RegisterCallback<DragExitedEvent>(OnTargetAssetFolderDragExited, TrickleDown.TrickleDown); + _targetAssetFolderField.RegisterValueChangedCallback(OnTargetAssetFolderValueChanged); + targetFolderBox.Add(_targetAssetFolderField); + + targetFolderBox.Add(new Label("Drag and Drop an Assets Folder to Convert Above"){name="description"}); + return targetFolderBox; + } + + #region TargetAssetFolder Field Events + + private bool StringIsValidAssetFolder(string targetFolder) + { + return !string.IsNullOrWhiteSpace(targetFolder) && AssetDatabase.IsValidFolder(targetFolder); + } + + private void OnTargetAssetFolderValueChanged(ChangeEvent<string> evt) + { + string targetFolder = evt.newValue; + + if (StringIsValidAssetFolder(targetFolder)) + { + _windowData.targetAssetFolder = evt.newValue; + _windowData.Save(); + RefreshActionButtonState(); + } + else + { + _targetAssetFolderField.SetValueWithoutNotify(evt.previousValue); + } + } + + private void OnTargetAssetFolderDragExited(DragExitedEvent evt) + { + DragAndDrop.visualMode = DragAndDropVisualMode.None; + } + + private void OnTargetAssetFolderDragPerform(DragPerformEvent evt) + { + var targetFolder = DragAndDrop.paths[0]; + if (!string.IsNullOrWhiteSpace(targetFolder) && AssetDatabase.IsValidFolder(targetFolder)) + { + _targetAssetFolderField.value = targetFolder; + } + else + { + Debug.LogError($"Could not accept {targetFolder}. Needs to be a folder within the project"); + } + } + + private void OnTargetAssetFolderDragUpdated(DragUpdatedEvent evt) + { + if (DragAndDrop.paths.Length == 1) + { + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + DragAndDrop.AcceptDrag(); + } + else + { + DragAndDrop.visualMode = DragAndDropVisualMode.Rejected; + } + } + + private void OnTargetAssetFolderDragLeave(DragLeaveEvent evt) + { + DragAndDrop.visualMode = DragAndDropVisualMode.None; + } + + private void OnTargetAssetFolderDragEnter(DragEnterEvent evt) + { + if (DragAndDrop.paths.Length == 1) + { + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + DragAndDrop.AcceptDrag(); + } + } + + #endregion + + #region Migration Logic + + private void DoMigration(string corePath, string targetDir) + { + + EditorUtility.DisplayProgressBar("Migrating Package", "Creating Starter Package", 0.1f); + + // Convert PackageType enum to VRC Package ID string + string packageType = null; + switch (_windowData.relatedPackage) + { + case VRCPackageEnum.Avatars: + packageType = "com.vrchat.avatars"; + break; + case VRCPackageEnum.Base: + packageType = "com.vrchat.base"; + break; + case VRCPackageEnum.Worlds: + packageType = "com.vrchat.worlds"; + break; + } + + string parentDir = new DirectoryInfo(targetDir)?.Parent.FullName; + var packageDir = Core.Utilities.CreateStarterPackage(_windowData.packageID, parentDir, packageType); + + // Modify manifest to add author + // Todo: add support for passing author into CreateStarterPackage + var manifest = + VRCPackageManifest.GetManifestAtPath(Path.Combine(packageDir, VRCPackageManifest.Filename)) as + VRCPackageManifest; + manifest.author = new Author() + { + email = _windowData.authorEmail, + name = _windowData.authorName, + url = _windowData.authorUrl + }; + manifest.Save(); + + var allFiles = GetAllFiles(corePath).ToList(); + MoveFilesToPackageDir(allFiles, corePath, targetDir); + + // Clear target asset folder since it should no longer exist + _windowData.targetAssetFolder = ""; + } + + private static IEnumerable<string> GetAllFiles(string path) + { + var excludedPaths = new List<string>() + { + "Editor.meta" + }; + return Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories) + .Where( + s => excludedPaths.All(entry => !s.Contains(entry)) + ); + } + + public static void MoveFilesToPackageDir(List<string> files, string pathBase, string targetDir) + { + EditorUtility.DisplayProgressBar("Migrating Package", "Moving Package Files", 0f); + float totalFiles = files.Count; + + for (int i = 0; i < files.Count; i++) + { + try + { + EditorUtility.DisplayProgressBar("Migrating Package", "Moving Package Files", i / totalFiles); + var file = files[i]; + string simplifiedPath = file.Replace($"{pathBase}\\", ""); + + string dest = null; + if (simplifiedPath.Contains("Editor\\")) + { + // Remove extra 'Editor' subfolders + dest = simplifiedPath.Replace("Editor\\", ""); + dest = Path.Combine(targetDir, "Editor", dest); + } + else + { + // Make complete path to Runtime folder + dest = Path.Combine(targetDir, "Runtime", simplifiedPath); + } + + string targetEnclosingDir = Path.GetDirectoryName(dest); + Directory.CreateDirectory(targetEnclosingDir); + var sourceFile = Path.Combine(pathBase, simplifiedPath); + File.Move(sourceFile, dest); + } + catch (Exception e) + { + Debug.LogError($"Error moving {files[i]}: {e.Message}"); + continue; + } + } + + Directory.Delete(pathBase, true); // cleans up leftover folders since only files are moved + EditorUtility.ClearProgressBar(); + } + + // Important while we're doing copy-and-rename in order to rename paths with "Assets" without renaming paths with "Sample Assets" + public static string ReplaceFirst(string text, string search, string replace) + { + int pos = text.IndexOf(search); + if (pos < 0) + { + return text; + } + + return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + } + + #endregion + } + +}
\ No newline at end of file |
