Editor/Core/GeneratedAssetFiles.cs

Editor utility that manages generated asset files during import. It reserves output names, computes content hashes for generated temp files, reuses existing matching files by name/numbering, moves or deletes temp files into the destination, and resolves in-text references to the final imported paths.

File AccessExternal Download
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;

namespace ImportUnityPackage;

/// <summary>Names derived assets beside their sources, reusing matching content without an on-disk index.</summary>
internal sealed class GeneratedAssetFiles
{
	readonly string scratch;
	readonly string destination;
	readonly HashSet<string> reserved;
	readonly List<(string Token, string Preferred, string File)> pending = new();
	readonly Dictionary<string, string> resolved = new();
	readonly HashSet<string> assigned = new( StringComparer.OrdinalIgnoreCase );
	readonly Dictionary<string, Dictionary<int, string>> existing = new( StringComparer.OrdinalIgnoreCase );

	public GeneratedAssetFiles( ImportPlan plan, string stage, string destination )
	{
		this.destination = destination;
		scratch = Path.Combine( Path.GetDirectoryName( stage ), "generated" );
		reserved = plan.Archive.Assets.Select( a => a.Path ).ToHashSet( StringComparer.OrdinalIgnoreCase );
		foreach ( var item in plan.Assets )
		{
			if ( item.Vmat ) reserved.Add( Path.ChangeExtension( item.Asset.Path, ".vmat" ) );
			if ( item.Tmat ) reserved.Add( Path.ChangeExtension( item.Asset.Path, ".tmat" ) );
			if ( item.Vmdl ) reserved.Add( UnityImport.ModelOutput( item.Asset.Path ) );
			if ( item.Sound ) reserved.Add( Path.ChangeExtension( item.Asset.Path, ".sound" ) );
			if ( item.Vtex ) reserved.Add( item.Asset.Path + ".vtex" );
			if ( item.Prefab ) reserved.Add( UnityImport.PlacementOutput( item.Asset.Path, ".prefab" ) );
			if ( item.Scene ) reserved.Add( UnityImport.PlacementOutput( item.Asset.Path, ".scene" ) );
			if ( item.Terrain ) { reserved.Add( UnityImport.PlacementOutput( item.Asset.Path, ".terrain" ) ); reserved.Add( UnityImport.PlacementOutput( item.Asset.Path, ".terrain" ) + "_d" ); }
		}
	}

	public (string Reference, string File) Add( string source, string purpose, string extension )
	{
		Directory.CreateDirectory( scratch );
		var token = $"iup:generated:{pending.Count}";
		var file = Path.Combine( scratch, pending.Count + ".iup" );
		var preferred = Path.ChangeExtension( source, null ) + "_iup_" + purpose + extension;
		pending.Add( (token, preferred, file) );
		return (token, file);
	}

	public void Complete( Func<string, string> output, CancellationToken cancel )
	{
		foreach ( var item in pending )
		{
			cancel.ThrowIfCancellationRequested();
			var hash = ImportMergePlan.HashFile( item.File, cancel ) ?? throw new IOException( "Generated asset is missing: " + item.Preferred );
			var candidates = Existing( item.Preferred, cancel );
			string Name( int number ) => number == 0 ? item.Preferred : Path.ChangeExtension( item.Preferred, null ) + "_" + number + Path.GetExtension( item.Preferred );
			// Look for matching numbered variants even when an earlier suffix has been deleted.
			var match = candidates.Where( p => p.Value == hash ).OrderBy( p => p.Key ).Select( p => Name( p.Key ) )
				.FirstOrDefault( path => !reserved.Contains( path ) );
			var chosen = match;
			if ( chosen == null )
			{
				var number = 0;
				while ( true )
				{
					cancel.ThrowIfCancellationRequested();
					chosen = Name( number );
					if ( !reserved.Contains( chosen ) && !candidates.ContainsKey( number ) && !Directory.Exists( Path.Combine( destination, chosen ) ) ) break;
					number++;
				}
				candidates[number] = hash;
			}
			resolved[item.Token] = "Imported/" + chosen;
			if ( assigned.Add( chosen ) ) File.Move( item.File, output( chosen ) );
			else File.Delete( item.File );
		}
	}

	Dictionary<int, string> Existing( string preferred, CancellationToken cancel )
	{
		if ( existing.TryGetValue( preferred, out var result ) ) return result;
		result = new();
		var directory = Path.GetDirectoryName( Path.Combine( destination, preferred ) );
		UnityImport.CheckDirectory( directory );
		if ( Directory.Exists( directory ) )
		{
			var pattern = new Regex( "^" + Regex.Escape( Path.GetFileNameWithoutExtension( preferred ) ) + @"(?:_([1-9][0-9]*))?" + Regex.Escape( Path.GetExtension( preferred ) ) + "$", RegexOptions.IgnoreCase );
			foreach ( var file in Directory.EnumerateFiles( directory ) )
			{
				cancel.ThrowIfCancellationRequested();
				var number = Number( file, pattern );
				if ( number < 0 ) continue;
				UnityImport.CheckDirectory( file );
				result[number] = ImportMergePlan.HashFile( file, cancel );
			}
		}
		existing[preferred] = result;
		return result;
	}

	static int Number( string path, Regex pattern )
	{
		var match = pattern.Match( Path.GetFileName( path ) );
		if ( !match.Success ) return -1;
		return !match.Groups[1].Success ? 0 : int.TryParse( match.Groups[1].Value, out var number ) ? number : -1;
	}

	public string ResolveDocument( string text ) => Regex.Replace( text, "\"iup:generated:[0-9]+\"", m => UnityMaterial.Quote( resolved[m.Value[1..^1]] ) );
}