Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active March 12, 2024 00:44
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save yasirkula/dfc43134fbfefb820d0adbc5d7c25fb3 to your computer and use it in GitHub Desktop.
Save yasirkula/dfc43134fbfefb820d0adbc5d7c25fb3 to your computer and use it in GitHub Desktop.
Extract a .unitypackage to any directory (even outside the project folder) from within Unity
//#define OPEN_ASSET_STORE_CACHE_AS_INITIAL_PATH
#define STOP_EXTRACTION_WHEN_WINDOW_CLOSED
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;
using UnityEditor;
using UnityEngine;
public class UnitypackageExtractor : EditorWindow
{
private string packagePath, outputPath;
private bool isWorking, isDecompressing, needsRepaint;
private int currentProgress, totalProgress;
private bool assemblyReloadLockedHint;
private Thread runningThread;
private bool abortThread;
[MenuItem( "Window/Unitypackage Extractor" )]
private static void Init()
{
UnitypackageExtractor window = GetWindow<UnitypackageExtractor>();
window.titleContent = new GUIContent( "Extractor" );
window.minSize = new Vector2( 300f, 100f );
window.Show();
}
#if STOP_EXTRACTION_WHEN_WINDOW_CLOSED
private void OnDestroy()
{
abortThread = true;
}
#endif
private void Update()
{
if( needsRepaint )
{
needsRepaint = false;
Repaint();
}
}
private void OnGUI()
{
packagePath = PathField( ".unitypackage Path: ", packagePath, false );
outputPath = PathField( "Output Path: ", outputPath, true );
if( !isWorking )
{
if( GUILayout.Button( "Extract!" ) )
{
if( string.IsNullOrEmpty( packagePath ) || !File.Exists( packagePath ) )
{
Debug.LogError( ".unitypackage doesn't exist at: " + packagePath );
return;
}
if( string.IsNullOrEmpty( outputPath ) )
{
Debug.LogError( "Output Path can't be blank" );
return;
}
if( !Directory.Exists( outputPath ) )
Directory.CreateDirectory( outputPath );
else if( Directory.GetFileSystemEntries( outputPath ).Length > 0 )
{
Debug.LogError( "Output Path must be an empty folder" );
return;
}
abortThread = false;
isDecompressing = true;
currentProgress = 0;
totalProgress = 0;
runningThread = new Thread( Execute );
runningThread.Start();
assemblyReloadLockedHint = false;
isWorking = true;
EditorApplication.LockReloadAssemblies();
EditorApplication.update -= UnlockAssembliesAfterExtraction;
EditorApplication.update += UnlockAssembliesAfterExtraction;
}
}
else if( GUILayout.Button( "Stop" ) )
abortThread = true;
Rect progressbarRect = EditorGUILayout.GetControlRect( false, EditorGUIUtility.singleLineHeight );
if( isWorking )
{
if( isDecompressing )
EditorGUI.ProgressBar( progressbarRect, 0f, "Decompressing Archive" );
else if( totalProgress > 0 )
EditorGUI.ProgressBar( progressbarRect, (float) currentProgress / totalProgress, string.Concat( "Extracting Assets: ", currentProgress, "/", totalProgress ) );
}
}
private void UnlockAssembliesAfterExtraction()
{
if( !isWorking )
{
EditorApplication.update -= UnlockAssembliesAfterExtraction;
EditorApplication.UnlockReloadAssemblies();
}
else
{
if( EditorApplication.isPlayingOrWillChangePlaymode )
{
EditorApplication.isPlaying = false;
Debug.LogWarning( "Can't enter Play mode while extracting a Unitypackage, <b>Stop</b> the operation first!" );
}
if( !assemblyReloadLockedHint && EditorApplication.isCompiling )
{
assemblyReloadLockedHint = true;
Debug.LogWarning( "Can't reload assemblies while extracting a Unitypackage, <b>Stop</b> the operation first!" );
}
}
}
private string PathField( string label, string path, bool isDirectory )
{
GUILayout.BeginHorizontal();
path = EditorGUILayout.TextField( label, path );
if( GUILayout.Button( "o", GUILayout.Width( 25f ) ) )
{
string selectedPath;
if( isDirectory )
selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", "", "" );
else
{
#if OPEN_ASSET_STORE_CACHE_AS_INITIAL_PATH
string initialPath = Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData );
if( !string.IsNullOrEmpty( initialPath ) )
initialPath = Path.Combine( initialPath, "Unity/Asset Store-5.x" );
if( !Directory.Exists( initialPath ) )
initialPath = "";
#else
string initialPath = "";
#endif
selectedPath = EditorUtility.OpenFilePanel( "Choose .unitypackage", initialPath, "unitypackage" );
}
if( !string.IsNullOrEmpty( selectedPath ) )
path = selectedPath;
GUIUtility.keyboardControl = 0; // Remove focus from active text field
}
GUILayout.EndHorizontal();
return path;
}
private void Execute()
{
try
{
string packagePath = this.packagePath;
string outputPath = this.outputPath;
using( FileStream fs = new FileStream( packagePath, FileMode.Open, FileAccess.Read ) )
ExtractTarGz( fs, outputPath );
foreach( string directory in Directory.GetDirectories( outputPath ) )
{
if( abortThread )
break;
string pathnameFile = Path.Combine( directory, "pathname" );
if( !File.Exists( pathnameFile ) )
continue;
string path = File.ReadAllText( pathnameFile );
int newLineIndex = path.IndexOf( '\n' );
if( newLineIndex > 0 )
path = path.Substring( 0, newLineIndex );
path = Path.Combine( outputPath, path.Trim() );
Directory.CreateDirectory( Path.GetDirectoryName( path ) );
string assetFile = Path.Combine( directory, "asset" );
if( File.Exists( assetFile ) )
File.Move( assetFile, path );
else // This is a directory
Directory.CreateDirectory( path );
string assetMetaFile = Path.Combine( directory, "asset.meta" );
if( File.Exists( assetMetaFile ) )
File.Move( assetMetaFile, path + ".meta" );
Directory.Delete( directory, true );
currentProgress++;
needsRepaint = true;
}
if( !abortThread )
Debug.Log( "<b>Finished extracting:</b> " + packagePath );
else
Debug.Log( "<b>Stopped extracting:</b> " + packagePath );
}
catch( Exception e )
{
Debug.LogException( e );
}
isWorking = false;
}
// Credit: https://gist.github.com/davetransom/553aeb3c4388c3eb448c0afe564cd2e3
private void ExtractTarGz( Stream stream, string outputDir )
{
// A GZipStream is not seekable, so copy it to a MemoryStream first
using( GZipStream gzip = new GZipStream( stream, CompressionMode.Decompress ) )
{
const int chunk = 4096;
using( MemoryStream ms = new MemoryStream() )
{
int read;
byte[] buffer = new byte[chunk];
do
{
if( abortThread )
return;
read = gzip.Read( buffer, 0, chunk );
ms.Write( buffer, 0, read );
}
while( read == chunk );
// Count number of files in the archive first
ms.Seek( 0, SeekOrigin.Begin );
ExtractTar( ms, outputDir, true );
// Extract the files afterwards
isDecompressing = false;
ms.Seek( 0, SeekOrigin.Begin );
ExtractTar( ms, outputDir, false );
}
}
}
// Credit: https://gist.github.com/davetransom/553aeb3c4388c3eb448c0afe564cd2e3
private void ExtractTar( Stream stream, string outputDir, bool isCountingFiles )
{
byte[] buffer = new byte[100];
while( true )
{
if( abortThread )
return;
stream.Read( buffer, 0, 100 );
string name = Encoding.ASCII.GetString( buffer ).Trim( '\0', ' ' );
if( name != null )
name = name.Trim();
if( string.IsNullOrEmpty( name ) )
break;
stream.Seek( 24, SeekOrigin.Current );
stream.Read( buffer, 0, 12 );
long size;
string hex = Encoding.ASCII.GetString( buffer, 0, 12 ).Trim( '\0', ' ' );
try
{
size = Convert.ToInt64( hex, 8 );
}
catch( Exception ex )
{
throw new Exception( "Could not parse hex: " + hex, ex );
}
stream.Seek( 376L, SeekOrigin.Current );
string output = Path.Combine( outputDir, name );
if( size > 0 ) // Ignores directory entries
{
if( isCountingFiles )
{
stream.Seek( size, SeekOrigin.Current );
totalProgress++;
}
else
{
Directory.CreateDirectory( Path.GetDirectoryName( output ) );
using( FileStream fs = File.Open( output, FileMode.OpenOrCreate, FileAccess.Write ) )
{
byte[] blob = new byte[size];
stream.Read( blob, 0, blob.Length );
fs.Write( blob, 0, blob.Length );
}
currentProgress++;
needsRepaint = true;
}
}
else if( isCountingFiles )
totalProgress++; // Each directory will be processed after being extracted
long offset = 512 - ( stream.Position % 512 );
if( offset == 512 )
offset = 0;
stream.Seek( offset, SeekOrigin.Current );
}
}
}
@yasirkula
Copy link
Author

yasirkula commented Mar 17, 2020

How To

Simply create a folder called Editor inside your Project window and add this script inside it. Then, open Window-Unitypackage Extractor, configure the parameters and hit "Extract!". The operation will be handled by a separate thread, so it won't block Unity.

If you don't want to extract all assets inside the .unitypackage, hit "Select Assets to Extract..." first.

UnitypackageExtractor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment