2023-09-17 06:37:55 +00:00
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 ,
UdonSharp = 4 ,
}
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.clientsim" ; // we want ClientSim too, need to specify that for now
break ;
case VRCPackageEnum . UdonSharp :
packageType = "com.vrchat.udonsharp" ;
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
}
2023-09-10 04:16:23 +00:00
}