ArabDesert/Packages/com.vrchat.core.vpm-resolver/Editor/PackageMaker/PackageMakerWindow.cs

485 lines
18 KiB
C#

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();
}
_rootView = rootVisualElement;
_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
}
}