Editor/ImportAssetPreparation.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Editor;
using Sandbox;
namespace ImportUnityPackage;
public record ImportPreparationResult( string[] Warnings, bool Cancelled, int Rebuilt )
{
public string[] Errors { get; init; } = Array.Empty<string>();
}
/// <summary>Runs on the editor context after the imported folder has been committed.</summary>
public static class ImportAssetPreparation
{
static Task repairTask;
/// <summary>Repair already compiled resources in an existing import without reimporting source files.</summary>
[ConCmd( "unity_import_repair_resources" )]
public static void Repair( string directory )
{
if ( repairTask is { IsCompleted: false } ) return;
repairTask = RepairAsync( directory );
}
static async Task RepairAsync( string directory )
{
try
{
var assets = Project.Current.GetAssetsPath();
var root = Path.GetFullPath( Path.Combine( assets, "imported" ) ) + Path.DirectorySeparatorChar;
var target = Path.GetFullPath( Path.IsPathRooted( directory ) ? directory : Path.Combine( assets, directory ) );
if ( !(target.Equals( root.TrimEnd( Path.DirectorySeparatorChar ), StringComparison.OrdinalIgnoreCase ) || target.StartsWith( root, StringComparison.OrdinalIgnoreCase )) || !Directory.Exists( target ) )
throw new ArgumentException( "Choose an existing package folder under this project's Assets/imported." );
var stale = Directory.EnumerateFiles( target, "*", SearchOption.AllDirectories )
.Where( f => ResourceOrder( f ) > 0 )
.Where( f => AssetSystem.FindByPath( f ) is { IsCompiled: true } asset && !asset.IsCompiledAndUpToDate ).ToArray();
Log.Info( $"UNITY_RESOURCE_REPAIR: preparing {stale.Length} out-of-date resources in {target}" );
var result = await Run( stale, null, CancellationToken.None );
foreach ( var warning in result.Warnings ) Log.Warning( warning );
foreach ( var error in result.Errors ) Log.Error( error );
Log.Info( $"UNITY_RESOURCE_REPAIR complete: {stale.Length} checked, {result.Rebuilt} full rebuilds, {result.Warnings.Length} warnings, {result.Errors.Length} errors." );
}
catch ( Exception ex ) { Log.Error( $"UNITY_RESOURCE_REPAIR failed: {ex}" ); }
}
public static async Task<ImportPreparationResult> RunImport( ImportResult result, IProgress<ImportProgress> progress, CancellationToken cancel )
{
var files = result.Files.ToHashSet( StringComparer.OrdinalIgnoreCase );
var warnings = new List<string>();
foreach ( var changed in result.ChangedFiles )
{
if ( cancel.IsCancellationRequested ) break;
try
{
var asset = AssetSystem.RegisterFile( changed );
if ( asset == null ) continue;
foreach ( var dependant in asset.GetDependants( true ) )
if ( ResourceOrder( dependant.Path ) > 0 && dependant.HasSourceFile ) files.Add( dependant.GetSourceFile( true ) );
}
catch ( Exception ex ) { warnings.Add( $"{changed}: could not discover affected resources: {ex.Message}" ); }
}
var prepared = await Run( files, progress, cancel );
return prepared with { Warnings = warnings.Concat( prepared.Warnings ).ToArray() };
}
public static async Task<ImportPreparationResult> Run( IEnumerable<string> files, IProgress<ImportProgress> progress, CancellationToken cancel )
{
var warnings = new List<string>();
var errors = new List<string>();
var resources = new List<Asset>();
var rebuilt = 0;
try
{
// Register all sources before compiling textures, then materials, then models.
foreach ( var file in files.Distinct( StringComparer.OrdinalIgnoreCase ).OrderBy( ResourceOrder ) )
{
cancel.ThrowIfCancellationRequested();
if ( Path.GetExtension( file ).ToLowerInvariant() is ".mat" or ".json" or ".mtl" ) continue;
try
{
var asset = AssetSystem.RegisterFile( file );
if ( ResourceOrder( file ) > 0 )
{
if ( asset == null ) throw new InvalidOperationException( "Asset registration returned no resource" );
resources.Add( asset );
}
}
catch ( Exception ex ) { errors.Add( $"{file}: registration failed: {ex.Message}" ); }
}
for ( var i = 0; i < resources.Count; i++ )
{
cancel.ThrowIfCancellationRequested();
var asset = resources[i];
progress?.Report( new( (double)i / resources.Count, $"Preparing resource {i + 1}/{resources.Count}: {asset.Name}" ) );
try
{
if ( !asset.IsCompiledAndUpToDate ) await asset.CompileIfNeededAsync();
cancel.ThrowIfCancellationRequested();
// Incremental compilation can leave generated children absent from the asset registry,
// even when their _c files exist. One full rebuild restores those dependencies.
if ( !asset.IsCompileFailed && !asset.IsCompiledAndUpToDate )
{
asset.Compile( true );
rebuilt++;
}
if ( asset.IsCompileFailed || !asset.IsCompiledAndUpToDate )
errors.Add( $"{asset.Path}: compilation or dependency preparation failed; see the editor console." );
}
catch ( OperationCanceledException ) { throw; }
catch ( Exception ex ) { errors.Add( $"{asset.Path}: resource preparation failed: {ex.Message}" ); }
await Task.Delay( 1, cancel );
}
}
catch ( OperationCanceledException ) when ( cancel.IsCancellationRequested )
{
return new( warnings.ToArray(), true, rebuilt ) { Errors = errors.ToArray() };
}
progress?.Report( new( 1, "Import complete" ) );
return new( warnings.ToArray(), false, rebuilt ) { Errors = errors.ToArray() };
}
static int ResourceOrder( string file ) => Path.GetExtension( file ).ToLowerInvariant() switch
{
".vtex" => 1,
".vmat" or ".tmat" => 2,
".vmdl" => 3,
_ => 0
};
}