{"TotalCount":22,"Files":[{"Ident":"grubs.importunitypackage","Path":"Editor/Core/ImportStorage.cs","FileName":"ImportStorage.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\r\nusing System.IO;\r\nusing System.Threading;\r\n\r\nnamespace ImportUnityPackage;\r\n\r\n/// \u003Csummary\u003EBounded retries for Windows sharing/access errors during directory finalization.\u003C/summary\u003E\r\ninternal static class ImportStorage\r\n{\r\n\tinternal static bool IsTemporaryAccessError( Exception exception ) =\u003E\r\n\t\texception is IOException or UnauthorizedAccessException \u0026\u0026 (exception.HResult \u0026 0xffff) is 5 or 32 or 33;\r\n\r\n\tinternal static void Retry( Action operation, CancellationToken cancel, Action\u003Cint\u003E retrying = null )\r\n\t{\r\n\t\tfor ( var attempt = 0; ; attempt\u002B\u002B )\r\n\t\t{\r\n\t\t\tcancel.ThrowIfCancellationRequested();\r\n\t\t\ttry { operation(); return; }\r\n\t\t\tcatch ( Exception ex ) when ( IsTemporaryAccessError( ex ) \u0026\u0026 attempt \u003C 6 )\r\n\t\t\t{\r\n\t\t\t\tretrying?.Invoke( attempt \u002B 1 );\r\n\t\t\t\t// Maximum wait is 4.05 seconds. A cancellation interrupts the wait immediately.\r\n\t\t\t\tif ( cancel.WaitHandle.WaitOne( Math.Min( 150 \u003C\u003C attempt, 1000 ) ) ) cancel.ThrowIfCancellationRequested();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/GeneratedAssetFiles.cs","FileName":"GeneratedAssetFiles.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.RegularExpressions;\nusing System.Threading;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003ENames derived assets beside their sources, reusing matching content without an on-disk index.\u003C/summary\u003E\ninternal sealed class GeneratedAssetFiles\n{\n\treadonly string scratch;\n\treadonly string destination;\n\treadonly HashSet\u003Cstring\u003E reserved;\n\treadonly List\u003C(string Token, string Preferred, string File)\u003E pending = new();\n\treadonly Dictionary\u003Cstring, string\u003E resolved = new();\n\treadonly HashSet\u003Cstring\u003E assigned = new( StringComparer.OrdinalIgnoreCase );\n\treadonly Dictionary\u003Cstring, Dictionary\u003Cint, string\u003E\u003E existing = new( StringComparer.OrdinalIgnoreCase );\n\n\tpublic GeneratedAssetFiles( ImportPlan plan, string stage, string destination )\n\t{\n\t\tthis.destination = destination;\n\t\tscratch = Path.Combine( Path.GetDirectoryName( stage ), \u0022generated\u0022 );\n\t\treserved = plan.Archive.Assets.Select( a =\u003E a.Path ).ToHashSet( StringComparer.OrdinalIgnoreCase );\n\t\tforeach ( var item in plan.Assets )\n\t\t{\n\t\t\tif ( item.Vmat ) reserved.Add( Path.ChangeExtension( item.Asset.Path, \u0022.vmat\u0022 ) );\n\t\t\tif ( item.Tmat ) reserved.Add( Path.ChangeExtension( item.Asset.Path, \u0022.tmat\u0022 ) );\n\t\t\tif ( item.Vmdl ) reserved.Add( Path.ChangeExtension( item.Asset.Path, \u0022.vmdl\u0022 ) );\n\t\t\tif ( UnityImport.NeedsTextureResource( item.Asset.Path ) ) reserved.Add( item.Asset.Path \u002B \u0022.vtex\u0022 );\n\t\t}\n\t}\n\n\tpublic (string Reference, string File) Add( string source, string purpose, string extension )\n\t{\n\t\tDirectory.CreateDirectory( scratch );\n\t\tvar token = $\u0022iup:generated:{pending.Count}\u0022;\n\t\tvar file = Path.Combine( scratch, pending.Count \u002B \u0022.iup\u0022 );\n\t\tvar preferred = Path.ChangeExtension( source, null ) \u002B \u0022_iup_\u0022 \u002B purpose \u002B extension;\n\t\tpending.Add( (token, preferred, file) );\n\t\treturn (token, file);\n\t}\n\n\tpublic void Complete( Func\u003Cstring, string\u003E output, CancellationToken cancel )\n\t{\n\t\tforeach ( var item in pending )\n\t\t{\n\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\tvar hash = ImportMergePlan.HashFile( item.File, cancel ) ?? throw new IOException( \u0022Generated asset is missing: \u0022 \u002B item.Preferred );\n\t\t\tvar candidates = Existing( item.Preferred, cancel );\n\t\t\tstring Name( int number ) =\u003E number == 0 ? item.Preferred : Path.ChangeExtension( item.Preferred, null ) \u002B \u0022_\u0022 \u002B number \u002B Path.GetExtension( item.Preferred );\n\t\t\t// Look for matching numbered variants even when an earlier suffix has been deleted.\n\t\t\tvar match = candidates.Where( p =\u003E p.Value == hash ).OrderBy( p =\u003E p.Key ).Select( p =\u003E Name( p.Key ) )\n\t\t\t\t.FirstOrDefault( path =\u003E !reserved.Contains( path ) );\n\t\t\tvar chosen = match;\n\t\t\tif ( chosen == null )\n\t\t\t{\n\t\t\t\tvar number = 0;\n\t\t\t\twhile ( true )\n\t\t\t\t{\n\t\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\t\tchosen = Name( number );\n\t\t\t\t\tif ( !reserved.Contains( chosen ) \u0026\u0026 !candidates.ContainsKey( number ) \u0026\u0026 !Directory.Exists( Path.Combine( destination, chosen ) ) ) break;\n\t\t\t\t\tnumber\u002B\u002B;\n\t\t\t\t}\n\t\t\t\tcandidates[number] = hash;\n\t\t\t}\n\t\t\tresolved[item.Token] = \u0022Imported/\u0022 \u002B chosen;\n\t\t\tif ( assigned.Add( chosen ) ) File.Move( item.File, output( chosen ) );\n\t\t\telse File.Delete( item.File );\n\t\t}\n\t}\n\n\tDictionary\u003Cint, string\u003E Existing( string preferred, CancellationToken cancel )\n\t{\n\t\tif ( existing.TryGetValue( preferred, out var result ) ) return result;\n\t\tresult = new();\n\t\tvar directory = Path.GetDirectoryName( Path.Combine( destination, preferred ) );\n\t\tUnityImport.CheckDirectory( directory );\n\t\tif ( Directory.Exists( directory ) )\n\t\t{\n\t\t\tvar pattern = new Regex( \u0022^\u0022 \u002B Regex.Escape( Path.GetFileNameWithoutExtension( preferred ) ) \u002B @\u0022(?:_([1-9][0-9]*))?\u0022 \u002B Regex.Escape( Path.GetExtension( preferred ) ) \u002B \u0022$\u0022, RegexOptions.IgnoreCase );\n\t\t\tforeach ( var file in Directory.EnumerateFiles( directory ) )\n\t\t\t{\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\tvar number = Number( file, pattern );\n\t\t\t\tif ( number \u003C 0 ) continue;\n\t\t\t\tUnityImport.CheckDirectory( file );\n\t\t\t\tresult[number] = ImportMergePlan.HashFile( file, cancel );\n\t\t\t}\n\t\t}\n\t\texisting[preferred] = result;\n\t\treturn result;\n\t}\n\n\tstatic int Number( string path, Regex pattern )\n\t{\n\t\tvar match = pattern.Match( Path.GetFileName( path ) );\n\t\tif ( !match.Success ) return -1;\n\t\treturn !match.Groups[1].Success ? 0 : int.TryParse( match.Groups[1].Value, out var number ) ? number : -1;\n\t}\n\n\tpublic string ResolveDocument( string text ) =\u003E Regex.Replace( text, \u0022\\\u0022iup:generated:[0-9]\u002B\\\u0022\u0022, m =\u003E UnityMaterial.Quote( resolved[m.Value[1..^1]] ) );\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityImport.cs","FileName":"UnityImport.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Threading;\n\nnamespace ImportUnityPackage;\n\npublic record ImportOptions( bool Materials, bool Models, bool Vmat, bool Tmat, bool Vmdl )\n{\n\tpublic bool Automatic { get; init; }\n\tpublic static ImportOptions Auto( bool additionalTerrainMaterials = false ) =\u003E new( true, true, true, additionalTerrainMaterials, true ) { Automatic = true };\n}\npublic record ImportIssue( string Asset, string Code, string Message )\n{\n\t// Compatibility fallbacks are warnings; failing to produce a selected resource is an error.\n\tpublic string Severity =\u003E Code == \u0022conversion-skipped\u0022 ? \u0022Error\u0022 : \u0022Warning\u0022;\n}\npublic record ImportResult( string Directory, int AssetCount, int ConvertedCount, string[] Files, string[] Warnings )\n{\n\tpublic ImportIssue[] Unresolved { get; init; } = Array.Empty\u003CImportIssue\u003E();\n\tpublic string ReportJson { get; init; }\n\tpublic void ExportReport( string path ) =\u003E File.WriteAllText( path, ReportJson );\n\tpublic string[] ChangedFiles { get; init; } = Array.Empty\u003Cstring\u003E();\n\tpublic string[] Information { get; init; } = Array.Empty\u003Cstring\u003E();\n}\npublic record TextureChannelRequest( string Source, string Destination, int Channel, double Scale, bool Invert );\npublic delegate void ExtractTextureChannels( IReadOnlyList\u003CTextureChannelRequest\u003E requests, CancellationToken cancel );\npublic delegate byte[] ExtractTextureChannel( string source, int channel, double scale, bool invert, CancellationToken cancel );\n\npublic static class UnityImport\n{\n\tpublic static bool NeedsTextureResource( string path ) =\u003E Path.GetExtension( path ).ToLowerInvariant() is \u0022.exr\u0022 or \u0022.psd\u0022 or \u0022.tif\u0022 or \u0022.tiff\u0022;\n\tpublic static bool IsEnabled( UnityAsset asset, ImportOptions options ) =\u003E asset.Kind switch\n\t{\n\t\tUnityAssetKind.Material or UnityAssetKind.Texture =\u003E options.Materials,\n\t\tUnityAssetKind.Model or UnityAssetKind.ModelSupport =\u003E options.Models,\n\t\t_ =\u003E false\n\t};\n\n\tpublic static HashSet\u003CUnityAsset\u003E Selection( UnityArchive archive, ImportOptions options ) =\u003E\n\t\tImportCatalog.Read( archive ).CreatePlan( options ).Assets.Select( a =\u003E a.Asset ).ToHashSet();\n\n\tinternal static string ReadText( string path )\n\t{\n\t\tif ( new FileInfo( path ).Length \u003E 16 * 1024 * 1024 ) throw new InvalidDataException( \u0022Material or metadata exceeds the 16 MiB text limit.\u0022 );\n\t\treturn File.ReadAllText( path );\n\t}\n\n\tpublic static ImportResult Run( UnityArchive archive, string assetsDirectory, ImportOptions options,\n\t\tIProgress\u003CImportProgress\u003E progress, CancellationToken cancel, ExtractTextureChannel extractChannel = null, ExtractTextureChannels extractChannels = null )\n\t\t=\u003E Run( ImportCatalog.Read( archive, cancel ).CreatePlan( options ), assetsDirectory, progress, cancel, extractChannel, extractChannels );\n\n\tpublic static ImportResult Run( ImportPlan plan, string assetsDirectory,\n\t\tIProgress\u003CImportProgress\u003E progress, CancellationToken cancel, ExtractTextureChannel extractChannel = null, ExtractTextureChannels extractChannels = null )\n\t{\n\t\tusing var merge = ImportMergePlan.Create( plan, assetsDirectory, cancel );\n\t\tmerge.Prepare( progress, cancel, extractChannel, extractChannels );\n\t\tvar result = merge.Commit( progress, cancel );\n\t\tmerge.Dispose();\n\t\treturn result with { Warnings = result.Warnings.Concat( merge.CleanupWarnings ).Distinct().ToArray() };\n\t}\n\n\tinternal static ImportResult PrepareFiles( ImportPlan plan, string stageDirectory, string destination,\n\t\tIProgress\u003CImportProgress\u003E progress, CancellationToken cancel, ExtractTextureChannel extractChannel, ExtractTextureChannels extractChannels = null )\n\t{\n\t\tcancel.ThrowIfCancellationRequested();\n\t\tvar archive = plan.Archive;\n\t\tvar options = plan.Options;\n\t\tvar selected = plan.Assets.Select( a =\u003E a.Asset ).ToArray();\n\t\tif ( selected.Length == 0 ) throw new InvalidOperationException( \u0022Select at least one supported asset.\u0022 );\n\t\tvar stage = Path.GetFullPath( stageDirectory );\n\t\tCheckDirectory( stage );\n\t\tconst string prefix = \u0022Imported/\u0022;\n\t\tvar warnings = new List\u003Cstring\u003E( archive.Warnings );\n\t\tvar information = new List\u003Cstring\u003E();\n\t\tvar unresolved = new List\u003CImportIssue\u003E( plan.Issues );\n\t\twarnings.AddRange( plan.Issues.Select( i =\u003E $\u0022{i.Asset}: {i.Message}\u0022 ) );\n\t\tvoid Issue( string asset, string code, string message )\n\t\t{\n\t\t\tunresolved.Add( new( asset, code, message ) );\n\t\t\twarnings.Add( $\u0022{asset}: {message}\u0022 );\n\t\t}\n\t\tvar files = new List\u003Cstring\u003E();\n\t\tvar outputs = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar textures = selected.Where( a =\u003E a.Kind == UnityAssetKind.Texture ).ToDictionary( a =\u003E a.Guid, a =\u003E prefix \u002B a.Path, StringComparer.OrdinalIgnoreCase );\n\t\tvar textureSources = selected.Where( a =\u003E a.Kind == UnityAssetKind.Texture ).ToDictionary( a =\u003E a.Guid, StringComparer.OrdinalIgnoreCase );\n\t\tvar generatedFiles = new GeneratedAssetFiles( plan, stage, destination );\n\t\tvar documents = new List\u003C(string File, string Text)\u003E();\n\t\tvar generatedChannels = new Dictionary\u003Cstring, string\u003E();\n\t\tvar channelRequests = new List\u003CTextureChannelRequest\u003E();\n\t\tvar channelTimer = new System.Diagnostics.Stopwatch();\n\t\tvar shaders = archive.Assets.Where( a =\u003E a.Path.EndsWith( \u0022.shader\u0022, StringComparison.OrdinalIgnoreCase ) ).ToDictionary( a =\u003E a.Guid, StringComparer.OrdinalIgnoreCase );\n\t\tvar shaderText = new Dictionary\u003Cstring, string\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar convertedMaterials = new Dictionary\u003Cstring, UnityMaterial\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar conversions = 0;\n\t\tvar operation = \u0022creating the staging folder\u0022;\n\t\tstring currentAsset = null;\n\t\tException failure = null;\n\t\tvar preserveStage = false;\n\t\ttry\n\t\t{\n\t\t\tDirectory.CreateDirectory( stage );\n\t\t\tstring Output( string relative )\n\t\t\t{\n\t\t\t\tif ( !outputs.Add( relative ) ) throw new InvalidDataException( $\u0022Two assets generate the same output: {relative}\u0022 );\n\t\t\t\tvar full = Path.GetFullPath( Path.Combine( stage, relative \u002B \u0022.iup\u0022 ) );\n\t\t\t\tif ( !full.StartsWith( stage \u002B Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase ) ) throw new InvalidDataException( \u0022Invalid output path.\u0022 );\n\t\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( full ) );\n\t\t\t\tfiles.Add( relative );\n\t\t\t\treturn full;\n\t\t\t}\n\t\t\tfor ( var i = 0; i \u003C selected.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\tvar asset = selected[i];\n\t\t\t\toperation = \u0022writing imported files\u0022;\n\t\t\t\tcurrentAsset = asset.Path;\n\t\t\t\tvar planned = plan.Find( asset );\n\t\t\t\tif ( asset.Kind == UnityAssetKind.Model \u0026\u0026 asset.ModelInfo.CollisionOnly )\n\t\t\t\t\tinformation.Add( $\u0022{asset.Path}: used only by Unity MeshColliders; kept as a collision source without a visible VMDL. Physics setup is not recreated.\u0022 );\n\t\t\t\tprogress?.Report( new( 0.8 * i / selected.Length, $\u0022Preparing {asset.Path}\u0022, \u0022Preparing assets\u0022 ) );\n\t\t\t\tusing ( var source = File.OpenRead( asset.Source ) )\n\t\t\t\tusing ( var target = new FileStream( Output( asset.Path ), FileMode.CreateNew ) )\n\t\t\t\t\t\tUnityArchive.Copy( source, target, cancel );\n\t\t\t\tif ( asset.Kind == UnityAssetKind.Texture \u0026\u0026 NeedsTextureResource( asset.Path ) )\n\t\t\t\t{\n\t\t\t\t\t// Keep source pixels/HDR data intact and give the editor a loadable texture resource.\n\t\t\t\t\tFile.WriteAllText( Output( asset.Path \u002B \u0022.vtex\u0022 ), JsonSerializer.Serialize( new\n\t\t\t\t\t{\n\t\t\t\t\t\tImages = new[] { prefix \u002B asset.Path }, InputColorSpace = \u0022Linear\u0022, OutputColorSpace = \u0022Linear\u0022,\n\t\t\t\t\t\tOutputFormat = asset.Path.EndsWith( \u0022.exr\u0022, StringComparison.OrdinalIgnoreCase ) ? \u0022RGBA16161616F\u0022 : \u0022BC7\u0022,\n\t\t\t\t\t\tOutputMipAlgorithm = \u0022Box\u0022, OutputTypeString = \u00222D\u0022\n\t\t\t\t\t}, new JsonSerializerOptions { WriteIndented = true } ) );\n\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t}\n\t\t\t\tif ( planned.Vmat || planned.Tmat )\n\t\t\t\t{\n\t\t\t\t\ttry\n\t\t\t\t\t{\n\t\t\t\t\t\tvar material = UnityMaterial.Parse( ReadText( asset.Source ) );\n\t\t\t\t\t\tif ( material.ShaderGuid != null \u0026\u0026 shaders.TryGetValue( material.ShaderGuid, out var shader ) )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( !shaderText.TryGetValue( shader.Guid, out var source ) ) shaderText[shader.Guid] = source = ReadText( shader.Source );\n\t\t\t\t\t\t\tmaterial.ConfigureShader( source );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( extractChannel != null || extractChannels != null )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmaterial.PrepareChannels( (guid, channel, scale, invert) =\u003E\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tif ( !textureSources.TryGetValue( guid, out var source ) )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tIssue( asset.Path, \u0022missing-texture\u0022, $\u0022Missing or unsupported texture {guid}.\u0022 );\n\t\t\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tvar key = $\u0022{guid}_{channel}_{scale.ToString( System.Globalization.CultureInfo.InvariantCulture )}_{invert}\u0022;\n\t\t\t\t\t\t\t\tif ( !generatedChannels.TryGetValue( key, out var generated ) )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvar purpose = channel switch { 0 =\u003E \u0022metallic\u0022, 1 =\u003E \u0022ao\u0022, 2 =\u003E \u0022height\u0022, _ =\u003E invert ? \u0022roughness\u0022 : \u0022opacity\u0022 };\n\t\t\t\t\t\t\t\t\tvar derived = generatedFiles.Add( source.Path, purpose, \u0022.png\u0022 );\n\t\t\t\t\t\t\t\t\tgenerated = derived.Reference;\n\t\t\t\t\t\t\t\t\tvar output = derived.File;\n\t\t\t\t\t\t\t\t\tif ( extractChannels != null ) channelRequests.Add( new( source.Source, output, channel, scale, invert ) );\n\t\t\t\t\t\t\t\t\telse File.WriteAllBytes( output, extractChannel( source.Source, channel, scale, invert, cancel ) );\n\t\t\t\t\t\t\t\t\tgeneratedChannels[key] = generated;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn generated;\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstring Resolve( string guid )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( textures.TryGetValue( guid, out var texture ) ) return texture;\n\t\t\t\t\t\t\tIssue( asset.Path, \u0022missing-texture\u0022, $\u0022Missing or unsupported texture {guid}.\u0022 );\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( planned.Vmat ) { documents.Add( (Output( Path.ChangeExtension( asset.Path, \u0022.vmat\u0022 ) ), material.ToVmat( Resolve )) ); conversions\u002B\u002B; }\n\t\t\t\t\t\tconvertedMaterials[asset.Guid] = material;\n\t\t\t\t\t\tif ( planned.Tmat ) { documents.Add( (Output( Path.ChangeExtension( asset.Path, \u0022.tmat\u0022 ) ), material.ToTmat( Resolve )) ); conversions\u002B\u002B; }\n\t\t\t\t\t\twarnings.AddRange( material.Warnings.Select( w =\u003E $\u0022{asset.Path}: {w}\u0022 ) );\n\t\t\t\t\t\tinformation.AddRange( material.Information.Select( m =\u003E $\u0022{asset.Path}: {m}\u0022 ) );\n\t\t\t\t\t}\n\t\t\t\t\tcatch ( InvalidDataException ex ) { Issue( asset.Path, \u0022conversion-skipped\u0022, $\u0022Conversion skipped. {ex.Message}\u0022 ); }\n\t\t\t\t}\n\t\t\t\tif ( planned.Vmdl )\n\t\t\t\t{\n\t\t\t\t\tvar meshPath = prefix \u002B asset.Path;\n\t\t\t\t\tif ( asset.Path.EndsWith( \u0022.fbx\u0022, StringComparison.OrdinalIgnoreCase ) \u0026\u0026 UnityFbxCompatibility.Normalize( asset.Source, cancel ) is byte[] normalized )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar repaired = generatedFiles.Add( asset.Path, \u0022repaired\u0022, \u0022.fbx\u0022 );\n\t\t\t\t\t\tFile.WriteAllBytes( repaired.File, normalized );\n\t\t\t\t\t\tmeshPath = repaired.Reference;\n\t\t\t\t\t\twarnings.Add( $\u0022{asset.Path}: removed an exact duplicate vertex array after a closing brace in a derived FBX; original source preserved.\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\tvar info = asset.ModelInfo;\n\t\t\t\t\tvar scale = asset.Path.EndsWith( \u0022.fbx\u0022, StringComparison.OrdinalIgnoreCase ) ? info.ImportScale( asset.Metadata == null ? null : ReadText( asset.Metadata ) ) : 1;\n\t\t\t\t\tvar remaps = files.Where( f =\u003E f.EndsWith( \u0022.vmat\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\t\t\t.GroupBy( Path.GetFileNameWithoutExtension, StringComparer.OrdinalIgnoreCase )\n\t\t\t\t\t\t.Where( g =\u003E g.Count() == 1 ).ToDictionary( g =\u003E g.Key.ToLowerInvariant() \u002B \u0022.vmat\u0022, g =\u003E prefix \u002B g.Single(), StringComparer.OrdinalIgnoreCase );\n\t\t\t\t\tvar meshName = Path.GetFileNameWithoutExtension( asset.Path ).ToLowerInvariant();\n\t\t\t\t\tif ( remaps.TryGetValue( meshName \u002B \u0022.vmat\u0022, out var matchingMaterial ) )\n\t\t\t\t\t{\n\t\t\t\t\t\t// Some exporters append an LOD suffix to slots that share the model\u0027s base material.\n\t\t\t\t\t\tfor ( var lod = 0; lod \u003C= 8; lod\u002B\u002B ) remaps.TryAdd( $\u0022{meshName}_lod{lod}.vmat\u0022, matchingMaterial );\n\t\t\t\t\t}\n\t\t\t\t\tvar prefabMaterials = selected.Where( a =\u003E info.PrefabMaterials.Contains( a.Guid, StringComparer.OrdinalIgnoreCase ) \u0026\u0026\n\t\t\t\t\t\toutputs.Contains( Path.ChangeExtension( a.Path, \u0022.vmat\u0022 ) ) ).ToArray();\n\t\t\t\t\tforeach ( var slot in info.Materials )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar semantic = prefabMaterials.Where( a =\u003E UnityModel.Normalize( Path.GetFileNameWithoutExtension( a.Path ) ) == UnityModel.Normalize( slot ) ).ToArray();\n\t\t\t\t\t\tif ( semantic.Length == 1 ) remaps[slot.ToLowerInvariant() \u002B \u0022.vmat\u0022] = prefix \u002B Path.ChangeExtension( semantic[0].Path, \u0022.vmat\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\tforeach ( var (slot, albedoFile) in info.MaterialAlbedoFiles )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( remaps.ContainsKey( slot.ToLowerInvariant() \u002B \u0022.vmat\u0022 ) ) continue;\n\t\t\t\t\t\tvar candidates = (prefabMaterials.Length \u003E 0 ? prefabMaterials : selected.Where( a =\u003E a.Kind == UnityAssetKind.Material ).ToArray())\n\t\t\t\t\t\t\t.Where( a =\u003E convertedMaterials.TryGetValue( a.Guid, out var material ) \u0026\u0026 material.ColorTextureGuids.Any( guid =\u003E\n\t\t\t\t\t\t\t\ttextureSources.TryGetValue( guid, out var texture ) \u0026\u0026 Path.GetFileNameWithoutExtension( texture.Path ).Equals(\n\t\t\t\t\t\t\t\t\tPath.GetFileNameWithoutExtension( albedoFile ), StringComparison.OrdinalIgnoreCase ) ) ).ToArray();\n\t\t\t\t\t\t// Unity often keeps the old diffuse filename as the material name\n\t\t\t\t\t\t// after repacking its texture. Require a unique prefab-assigned material.\n\t\t\t\t\t\tif ( candidates.Length == 0 ) candidates = prefabMaterials.Where( a =\u003E\n\t\t\t\t\t\t\tPath.GetFileNameWithoutExtension( a.Path ).Equals( Path.GetFileNameWithoutExtension( albedoFile ), StringComparison.OrdinalIgnoreCase ) ).ToArray();\n\t\t\t\t\t\tif ( candidates.Length == 1 \u0026\u0026 outputs.Contains( Path.ChangeExtension( candidates[0].Path, \u0022.vmat\u0022 ) ) )\n\t\t\t\t\t\t\tremaps[slot.ToLowerInvariant() \u002B \u0022.vmat\u0022] = prefix \u002B Path.ChangeExtension( candidates[0].Path, \u0022.vmat\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\tforeach ( var (slot, guid) in info.PrefabSlotMaterials )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar assigned = selected.FirstOrDefault( a =\u003E a.Guid.Equals( guid, StringComparison.OrdinalIgnoreCase ) \u0026\u0026 outputs.Contains( Path.ChangeExtension( a.Path, \u0022.vmat\u0022 ) ) );\n\t\t\t\t\t\tif ( assigned != null ) remaps[slot.ToLowerInvariant() \u002B \u0022.vmat\u0022] = prefix \u002B Path.ChangeExtension( assigned.Path, \u0022.vmat\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\tforeach ( var slot in info.UnresolvedPrefabSlots ) remaps.Remove( slot.ToLowerInvariant() \u002B \u0022.vmat\u0022 );\n\t\t\t\t\tvar defaultMaterial = prefabMaterials.Length == 1 \u0026\u0026 info.PrefabSlotMaterials.Count == 0 \u0026\u0026 info.AssignmentWarnings.Count == 0 ? prefix \u002B Path.ChangeExtension( prefabMaterials[0].Path, \u0022.vmat\u0022 ) : null;\n\t\t\t\t\tif ( options.Materials \u0026\u0026 options.Vmat \u0026\u0026 defaultMaterial == null \u0026\u0026 info.Materials.Count == 0 \u0026\u0026 asset.Path.EndsWith( \u0022.fbx\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar neutralPath = generatedFiles.Add( asset.Path, \u0022unassigned\u0022, \u0022.vmat\u0022 );\n\t\t\t\t\t\tvar neutral = new UnityMaterial();\n\t\t\t\t\t\tneutral.Colors[\u0022_Color\u0022] = new[] { 0.5, 0.5, 0.5, 1.0 };\n\t\t\t\t\t\tFile.WriteAllText( neutralPath.File, neutral.ToVmat( _ =\u003E null ) );\n\t\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t\t\tdefaultMaterial = neutralPath.Reference;\n\t\t\t\t\t\tIssue( asset.Path, \u0022unassigned-material\u0022, \u0022FBX contains no named material slots and no unambiguous prefab assignment was resolved; using a neutral material.\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\tif ( options.Materials \u0026\u0026 options.Vmat \u0026\u0026 defaultMaterial == null )\n\t\t\t\t\t\tforeach ( var slot in info.RenderedMaterials.Where( s =\u003E !remaps.ContainsKey( s.ToLowerInvariant() \u002B \u0022.vmat\u0022 ) ) )\n\t\t\t\t\t\t\tIssue( asset.Path, \u0022unassigned-material\u0022, $\u0022No converted material was resolved for slot \u0027{slot}\u0027.\u0022 );\n\t\t\t\t\tdocuments.Add( (Output( Path.ChangeExtension( asset.Path, \u0022.vmdl\u0022 ) ), ModelDocument( meshPath, remaps, defaultMaterial, info.HighestDetailMeshes, scale )) );\n\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t\tinformation.Add( $\u0022{asset.Path}: review scale, orientation, material assignments, collision and animations in ModelDoc.\u0022 );\n\t\t\t\t}\n\t\t\t}\n\t\t\toperation = \u0022extracting texture channels\u0022;\n\t\t\tcurrentAsset = null;\n\t\t\tprogress?.Report( new( 0.8, $\u0022Preparing {channelRequests.Count} texture channels\u0022, \u0022Converting textures\u0022 ) );\n\t\t\tchannelTimer.Start();\n\t\t\tif ( channelRequests.Count \u003E 0 ) extractChannels( channelRequests, cancel );\n\t\t\tchannelTimer.Stop();\n\t\t\tprogress?.Report( new( 0.95, \u0022Choosing generated filenames\u2026\u0022, \u0022Naming generated files\u0022 ) );\n\t\t\tgeneratedFiles.Complete( Output, cancel );\n\t\t\tforeach ( var document in documents )\n\t\t\t{\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\tFile.WriteAllText( document.File, generatedFiles.ResolveDocument( document.Text ) );\n\t\t\t}\n\t\t\tvar unsupported = archive.Assets.Count( a =\u003E a.Kind == UnityAssetKind.Unsupported );\n\t\t\tif ( unsupported \u003E 0 ) information.Add( $\u0022{unsupported} unsupported files (such as scripts, scenes and prefabs) were excluded.\u0022 );\n\t\t\tvar issues = unresolved.Distinct().ToArray();\n\t\t\tvar report = new\n\t\t\t{\n\t\t\t\tPackage = Path.GetFileName( archive.FileName ), Assets = selected.Length, Converted = conversions,\n\t\t\t\tFiles = files.ToArray(), Unresolved = issues,\n\t\t\t\tTextureProcessing = new { ChannelsGenerated = channelRequests.Count, SourceDecodes = channelRequests.Select( r =\u003E r.Source ).Distinct().Count(), ElapsedMilliseconds = channelTimer.ElapsedMilliseconds },\n\t\t\t\tSelection = plan.Assets.Select( a =\u003E new { Source = a.Asset.Path, a.Explicit, a.OutputLabel,\n\t\t\t\t\tRequiredBy = a.RequiredBy.Select( d =\u003E new { Source = d.Asset.Path, d.Reason } ).ToArray() } ).ToArray(),\n\t\t\t\tMaterialConversions = selected.Where( a =\u003E convertedMaterials.ContainsKey( a.Guid ) ).Select( a =\u003E new\n\t\t\t\t{\n\t\t\t\t\tSource = a.Path, convertedMaterials[a.Guid].ShaderName, convertedMaterials[a.Guid].ShaderGuid,\n\t\t\t\t\tConversion = \u0022Common material properties; shader programs are not translated\u0022,\n\t\t\t\t\tVmat = plan.Find( a ).Vmat, Tmat = plan.Find( a ).Tmat\n\t\t\t\t} ).ToArray(),\n\t\t\t\tInformation = information.ToArray(), Warnings = warnings.Distinct().ToArray()\n\t\t\t};\n\t\t\toperation = \u0022preparing import details\u0022;\n\t\t\tcurrentAsset = null;\n\t\t\tvar reportJson = JsonSerializer.Serialize( report, new JsonSerializerOptions { WriteIndented = true } );\n\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\tvar prepared = new ImportResult( stage, selected.Length, conversions, files.Select( f =\u003E Path.Combine( stage, f \u002B \u0022.iup\u0022 ) ).ToArray(), warnings.Distinct().ToArray() ) { Unresolved = issues, Information = information.ToArray(), ReportJson = reportJson };\n\t\t\tpreserveStage = true;\n\n\t\t\treturn prepared;\n\t\t}\n\t\tcatch ( Exception ex )\n\t\t{\n\t\t\tfailure = ex;\n\t\t\tif ( ex is IOException or UnauthorizedAccessException )\n\t\t\t{\n\t\t\t\tfailure = new IOException( $\u0022Import failed while {operation}\u0022 \u002B (currentAsset == null ? \u0022\u0022 : $\u0022 for \u0027{currentAsset}\u0027\u0022) \u002B\n\t\t\t\t\t$\u0022. Staging folder: \u0027{stage}\u0027. Windows error {ex.HResult \u0026 0xffff}: {ex.Message}\u0022, ex );\n\t\t\t\tthrow failure;\n\t\t\t}\n\t\t\tthrow;\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\tif ( !preserveStage \u0026\u0026 Directory.Exists( stage ) )\n\t\t\t{\n\t\t\t\ttry { ImportWorkspace.RemoveTree( stage ); }\n\t\t\t\tcatch ( Exception cleanup ) when ( cleanup is IOException or UnauthorizedAccessException )\n\t\t\t\t{\n\t\t\t\t\t// Preserve the original conversion/cancellation error if cleanup also fails.\n\t\t\t\t\tif ( failure == null ) throw new IOException( $\u0022Could not remove staging folder \u0027{stage}\u0027. {cleanup.Message}\u0022, cleanup );\n\t\t\t\t\tfailure.Data[\u0022StagingCleanupError\u0022] = $\u0022Could not remove \u0027{stage}\u0027: {cleanup}\u0022;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tinternal static void CheckDirectory( string path )\n\t{\n\t\tfor ( var current = new DirectoryInfo( Path.GetFullPath( path ) ); current != null; current = current.Parent )\n\t\t\tif ( (Directory.Exists( current.FullName ) || File.Exists( current.FullName )) \u0026\u0026 File.GetAttributes( current.FullName ).HasFlag( FileAttributes.ReparsePoint ) )\n\t\t\t\tthrow new IOException( $\u0022Import destination cannot pass through a symbolic link: {current.FullName}\u0022 );\n\t}\n\n\tstatic string ModelDocument( string mesh, Dictionary\u003Cstring, string\u003E remaps, string defaultMaterial, string[] highestDetailMeshes, double scale ) =\u003E $$\u0022\u0022\u0022\n\t\t\u003C!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc29:version{3cec427c-1b0e-4d48-a90a-0436f33a6041} --\u003E\n\t\t{\n\t\t\trootNode =\n\t\t\t{\n\t\t\t\t_class = \u0022RootNode\u0022\n\t\t\t\tchildren =\n\t\t\t\t[\n\t\t\t\t\t{\n\t\t\t\t\t\t_class = \u0022MaterialGroupList\u0022\n\t\t\t\t\t\tchildren =\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t_class = \u0022DefaultMaterialGroup\u0022\n\t\t\t\t\t\t\t\tremaps = [ {{string.Join( \u0022, \u0022, remaps.Select( r =\u003E \u0022{ from = \u0022 \u002B UnityMaterial.Quote( r.Key ) \u002B \u0022 to = \u0022 \u002B UnityMaterial.Quote( r.Value ) \u002B \u0022 }\u0022 ) )}} ]\n\t\t\t\t\t\t\t\tuse_global_default = {{(defaultMaterial != null ? \u0022true\u0022 : \u0022false\u0022)}}\n\t\t\t\t\t\t\t\tglobal_default_material = {{UnityMaterial.Quote( defaultMaterial ?? \u0022\u0022 )}}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t_class = \u0022RenderMeshList\u0022\n\t\t\t\t\t\tchildren =\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t_class = \u0022RenderMeshFile\u0022\n\t\t\t\t\t\t\t\tfilename = {{UnityMaterial.Quote( mesh )}}\n\t\t\t\t\t\t\t\timport_filter =\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\texclude_by_default = {{(highestDetailMeshes.Length \u003E 0 ? \u0022true\u0022 : \u0022false\u0022)}}\n\t\t\t\t\t\t\t\t\texception_list = [ {{string.Join( \u0022, \u0022, highestDetailMeshes.Select( UnityMaterial.Quote ) )}} ]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\timport_scale = {{scale.ToString( \u00220.#########\u0022, System.Globalization.CultureInfo.InvariantCulture )}}\n\t\t\t\t\t\t\t\timport_translation = [ 0.0, 0.0, 0.0 ]\n\t\t\t\t\t\t\t\timport_rotation = [ 0.0, 0.0, 0.0 ]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t\tmodel_archetype = \u0022\u0022\n\t\t\t}\n\t\t}\n\t\t\u0022\u0022\u0022;\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/ImportCompletion.cs","FileName":"ImportCompletion.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003EShort completion text, with detailed warnings and errors retained in the report.\u003C/summary\u003E\npublic sealed record ImportCompletion( int AssetCount, string[] Warnings, string[] Errors, bool Cancelled )\n{\n\tpublic string Title =\u003E Cancelled ? \u0022Import preparation cancelled\u0022 : Errors.Length \u003E 0 ? \u0022Import completed with errors\u0022 : \u0022Import complete\u0022;\n\tpublic string Counts =\u003E $\u0022Warnings: {Warnings.Length} \u00B7 Errors: {Errors.Length}\u0022;\n\tpublic string Message =\u003E Cancelled ? $\u0022Imported files kept. Resource preparation stopped.\\n{Counts}\u0022 : $\u0022{AssetCount} assets imported.\\n{Counts}\u0022;\n\tpublic string Status =\u003E $\u0022{(Errors.Length == 0 \u0026\u0026 !Cancelled ? \u0022\u2713 \u0022 : \u0022\u0022)}{Title}\\n{Message}\u0022;\n\n\tpublic static ImportCompletion Create( ImportResult result, IEnumerable\u003Cstring\u003E preparationWarnings, IEnumerable\u003Cstring\u003E preparationErrors, bool cancelled )\n\t{\n\t\tvar errors = result.Unresolved.Where( i =\u003E i.Severity == \u0022Error\u0022 ).Select( i =\u003E $\u0022{i.Asset}: {i.Message}\u0022 )\n\t\t\t.Concat( preparationErrors ?? Array.Empty\u003Cstring\u003E() ).Distinct().ToArray();\n\t\tvar warnings = result.Warnings.Concat( result.Unresolved.Where( i =\u003E i.Severity == \u0022Warning\u0022 ).Select( i =\u003E $\u0022{i.Asset}: {i.Message}\u0022 ) )\n\t\t\t.Concat( preparationWarnings ?? Array.Empty\u003Cstring\u003E() ).Except( errors ).Distinct().ToArray();\n\t\treturn new( result.AssetCount, warnings, errors, cancelled );\n\t}\n\n\tpublic string AppendReport( string json )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( json ) ) json = \u0022{}\u0022;\n\t\tvar report = JsonNode.Parse( json );\n\t\treport[\u0022Completion\u0022] = JsonSerializer.SerializeToNode( new { Title, AssetCount, Cancelled, Warnings, Errors } );\n\t\treturn report.ToJsonString( new JsonSerializerOptions { WriteIndented = true } );\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityFileId.cs","FileName":"UnityFileId.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Buffers.Binary;\nusing System.Text;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003EUnity\u0027s modern model subasset IDs use seed-zero XXH64 of a type and hierarchy path.\u003C/summary\u003E\ninternal static class UnityFileId\n{\n\t// XXH64 algorithm: https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md\n\t// Unity path format: https://discussions.unity.com/t/fbx-submesh-fileids/803882\n\tinternal static long Renderer( string path ) =\u003E Hash( \u0022Type:MeshRenderer-\u003E\u0022 \u002B path \u002B \u0022/MeshRenderer0\u0022 );\n\tinternal static long Hash( string text )\n\t{\n\t\tunchecked\n\t\t{\n\t\t\tconst ulong p1 = 0x9E3779B185EBCA87, p2 = 0xC2B2AE3D27D4EB4F, p3 = 0x165667B19E3779F9,\n\t\t\t\tp4 = 0x85EBCA77C2B2AE63, p5 = 0x27D4EB2F165667C5;\n\t\t\tstatic ulong Rotate( ulong v, int n ) =\u003E (v \u003C\u003C n) | (v \u003E\u003E (64 - n));\n\t\t\tstatic ulong Round( ulong v, ulong lane ) =\u003E unchecked( Rotate( v \u002B lane * p2, 31 ) * p1 );\n\t\t\tstatic ulong Merge( ulong h, ulong v ) =\u003E unchecked( (h ^ Round( 0, v )) * p1 \u002B p4 );\n\t\t\tReadOnlySpan\u003Cbyte\u003E data = Encoding.UTF8.GetBytes( text );\n\t\t\tint offset = 0;\n\t\t\tulong hash = p5;\n\t\t\tif ( data.Length \u003E= 32 )\n\t\t\t{\n\t\t\t\tulong a = p1 \u002B p2, b = p2, c = 0, d = 0UL - p1;\n\t\t\t\twhile ( offset \u003C= data.Length - 32 )\n\t\t\t\t{\n\t\t\t\t\ta = Round( a, BinaryPrimitives.ReadUInt64LittleEndian( data[offset..] ) );\n\t\t\t\t\tb = Round( b, BinaryPrimitives.ReadUInt64LittleEndian( data[(offset \u002B 8)..] ) );\n\t\t\t\t\tc = Round( c, BinaryPrimitives.ReadUInt64LittleEndian( data[(offset \u002B 16)..] ) );\n\t\t\t\t\td = Round( d, BinaryPrimitives.ReadUInt64LittleEndian( data[(offset \u002B 24)..] ) );\n\t\t\t\t\toffset \u002B= 32;\n\t\t\t\t}\n\t\t\t\thash = Rotate( a, 1 ) \u002B Rotate( b, 7 ) \u002B Rotate( c, 12 ) \u002B Rotate( d, 18 );\n\t\t\t\thash = Merge( Merge( Merge( Merge( hash, a ), b ), c ), d );\n\t\t\t}\n\t\t\thash \u002B= (ulong)data.Length;\n\t\t\twhile ( offset \u003C= data.Length - 8 )\n\t\t\t{\n\t\t\t\thash = Rotate( hash ^ Round( 0, BinaryPrimitives.ReadUInt64LittleEndian( data[offset..] ) ), 27 ) * p1 \u002B p4;\n\t\t\t\toffset \u002B= 8;\n\t\t\t}\n\t\t\tif ( offset \u003C= data.Length - 4 )\n\t\t\t{\n\t\t\t\thash = Rotate( hash ^ BinaryPrimitives.ReadUInt32LittleEndian( data[offset..] ) * p1, 23 ) * p2 \u002B p3;\n\t\t\t\toffset \u002B= 4;\n\t\t\t}\n\t\t\twhile ( offset \u003C data.Length ) hash = Rotate( hash ^ data[offset\u002B\u002B] * p5, 11 ) * p1;\n\t\t\thash ^= hash \u003E\u003E 33; hash *= p2; hash ^= hash \u003E\u003E 29; hash *= p3; hash ^= hash \u003E\u003E 32;\n\t\t\treturn (long)hash;\n\t\t}\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/ImportWorkspace.cs","FileName":"ImportWorkspace.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Threading;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003EShort-lived work under Assets/Imported/.iup-temp. Leases protect active runs during cleanup.\u003C/summary\u003E\npublic sealed class ImportWorkspace : IDisposable\n{\n\tpublic string DirectoryPath { get; }\n\tpublic List\u003Cstring\u003E Warnings { get; } = new();\n\treadonly string root;\n\tFileStream lease;\n\tpublic ImportWorkspace( string assetsDirectory )\n\t{\n\t\troot = Path.GetFullPath( Path.Combine( assetsDirectory, \u0022Imported\u0022, \u0022.iup-temp\u0022 ) );\n\t\tUnityImport.CheckDirectory( root );\n\t\tusing var gate = Enter( root );\n\t\tSweep( root, Warnings );\n\t\tDirectory.CreateDirectory( root );\n\t\tDirectoryPath = Path.Combine( root, Guid.NewGuid().ToString( \u0022N\u0022 ) );\n\t\tDirectory.CreateDirectory( DirectoryPath );\n\t\ttry { lease = new FileStream( Path.Combine( DirectoryPath, \u0022.active\u0022 ), FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None ); }\n\t\tcatch { RemoveTree( DirectoryPath ); RemoveEmptyRoot( root ); throw; }\n\t}\n\n\tpublic static string[] CleanupAbandoned( string assetsDirectory )\n\t{\n\t\tvar root = Path.GetFullPath( Path.Combine( assetsDirectory, \u0022Imported\u0022, \u0022.iup-temp\u0022 ) );\n\t\tUnityImport.CheckDirectory( root );\n\t\tvar warnings = new List\u003Cstring\u003E();\n\t\tusing var gate = Enter( root );\n\t\tSweep( root, warnings );\n\t\tRemoveEmptyRoot( root );\n\t\treturn warnings.ToArray();\n\t}\n\n\tstatic void Sweep( string root, List\u003Cstring\u003E warnings )\n\t{\n\t\tif ( !Directory.Exists( root ) ) return;\n\t\tforeach ( var run in Directory.EnumerateDirectories( root ) )\n\t\t{\n\t\t\tif ( !Guid.TryParseExact( Path.GetFileName( run ), \u0022N\u0022, out _ ) ) continue;\n\t\t\ttry\n\t\t\t{\n\t\t\t\tUnityImport.CheckDirectory( Path.Combine( run, \u0022.active\u0022 ) );\n\t\t\t\t// The exclusive lease stays open throughout an active run, including other editor processes.\n\t\t\t\tusing ( new FileStream( Path.Combine( run, \u0022.active\u0022 ), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None ) ) { }\n\t\t\t}\n\t\t\tcatch ( IOException ex ) when ( (ex.HResult \u0026 0xffff) is 32 or 33 ) { continue; }\n\t\t\tcatch ( Exception ex ) when ( ex is IOException or UnauthorizedAccessException )\n\t\t\t{\n\t\t\t\twarnings.Add( $\u0022Temporary folder could not be checked for cleanup: {run}. {ex.Message}\u0022 );\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry { RemoveTree( run ); }\n\t\t\tcatch ( Exception ex ) when ( ex is IOException or UnauthorizedAccessException ) { warnings.Add( $\u0022Temporary folder could not be removed: {run}. {ex.Message}\u0022 ); }\n\t\t}\n\t}\n\n\t// All recursive deletion is constrained to importer work and rejects linked directories/files.\n\tinternal static void RemoveTree( string path )\n\t{\n\t\tif ( !Directory.Exists( path ) ) return;\n\t\tpath = Path.GetFullPath( path );\n\t\tvar run = new DirectoryInfo( path );\n\t\twhile ( run != null \u0026\u0026 run.Parent?.Name != \u0022.iup-temp\u0022 ) run = run.Parent;\n\t\tif ( run == null || !Guid.TryParseExact( run.Name, \u0022N\u0022, out _ ) || !string.Equals( run.Parent.Parent?.Name, \u0022Imported\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\tthrow new IOException( \u0022Refusing to clean outside Assets/Imported/.iup-temp run folders.\u0022 );\n\t\tUnityImport.CheckDirectory( path );\n\t\tvoid Check( string directory )\n\t\t{\n\t\t\tforeach ( var item in Directory.EnumerateFileSystemEntries( directory ) )\n\t\t\t{\n\t\t\t\tvar flags = File.GetAttributes( item );\n\t\t\t\tif ( flags.HasFlag( FileAttributes.ReparsePoint ) ) throw new IOException( \u0022Refusing to clean linked temporary content: \u0022 \u002B item );\n\t\t\t\tif ( flags.HasFlag( FileAttributes.Directory ) ) Check( item );\n\t\t\t}\n\t\t}\n\t\tCheck( path );\n\t\tImportStorage.Retry( () =\u003E Directory.Delete( path, true ), CancellationToken.None );\n\t}\n\n\tstatic void RemoveEmptyRoot( string root )\n\t{\n\t\tif ( Directory.Exists( root ) \u0026\u0026 !Directory.EnumerateFileSystemEntries( root ).Any() ) Directory.Delete( root );\n\t}\n\n\tpublic void Dispose()\n\t{\n\t\tif ( lease == null ) return;\n\t\tusing var gate = Enter( root );\n\t\tlease.Dispose(); lease = null;\n\t\ttry { RemoveTree( DirectoryPath ); RemoveEmptyRoot( root ); }\n\t\tcatch ( Exception ex ) when ( ex is IOException or UnauthorizedAccessException )\n\t\t{\n\t\t\tWarnings.Add( $\u0022Temporary folder could not be removed: {DirectoryPath}. Cleanup will be retried next time the importer opens. {ex.Message}\u0022 );\n\t\t}\n\t}\n\n\tstatic IDisposable Enter( string root ) =\u003E new Gate( \u0022iup-temp-\u0022 \u002B Convert.ToHexString( SHA256.HashData( Encoding.UTF8.GetBytes( root.ToLowerInvariant() ) ) ) );\n\tsealed class Gate : IDisposable\n\t{\n\t\treadonly Mutex mutex;\n\t\tpublic Gate( string name ) { mutex = new Mutex( false, name ); try { mutex.WaitOne(); } catch ( AbandonedMutexException ) { } }\n\t\tpublic void Dispose() { mutex.ReleaseMutex(); mutex.Dispose(); }\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityModel.cs","FileName":"UnityModel.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace ImportUnityPackage;\n\npublic sealed class UnityModel\n{\n\tpublic sealed record RendererBinding( string Mesh, string[] Slots );\n\tpublic Dictionary\u003Clong, RendererBinding\u003E RendererSlots { get; } = new();\n\tpublic Dictionary\u003Cstring, string\u003E PrefabSlotMaterials { get; } = new( StringComparer.OrdinalIgnoreCase );\n\tpublic HashSet\u003Cstring\u003E UnresolvedPrefabSlots { get; } = new( StringComparer.OrdinalIgnoreCase );\n\tpublic List\u003Cstring\u003E AssignmentWarnings { get; } = new();\n\tpublic bool CollisionOnly { get; internal set; }\n\tpublic List\u003Cstring\u003E Materials { get; } = new();\n\tpublic List\u003Cstring\u003E Meshes { get; } = new();\n\tpublic List\u003Cstring\u003E PrefabMaterials { get; } = new();\n\tpublic Dictionary\u003Cstring, string\u003E MaterialAlbedoFiles { get; } = new( StringComparer.OrdinalIgnoreCase );\n\tpublic double? UnitScaleCentimeters { get; private set; }\n\tpublic double ImportScale( string metadata )\n\t{\n\t\tdouble Setting( string key, double fallback )\n\t\t{\n\t\t\tvar match = Regex.Match( metadata ?? \u0022\u0022, @\u0022(?m)^\\s*\u0022 \u002B key \u002B @\u0022:\\s*([-\u002B0-9.eE]\u002B)\u0022 );\n\t\t\treturn match.Success \u0026\u0026 double.TryParse( match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value ) \u0026\u0026 double.IsFinite( value ) ? value : fallback;\n\t\t}\n\t\tvar globalScale = Setting( \u0022globalScale\u0022, 1 );\n\t\tvar meters = Setting( \u0022useFileScale\u0022, 1 ) == 0 ? 1 : (UnitScaleCentimeters ?? 2.54) / 100;\n\t\tvar scale = globalScale * meters / 0.0254;\n\t\tif ( !double.IsFinite( scale ) || scale \u003C= 0 ) throw new InvalidDataException( \u0022Model import scale must be finite and positive.\u0022 );\n\t\treturn scale;\n\t}\n\tpublic string[] HighestDetailMeshes =\u003E Meshes.Where( n =\u003E Regex.IsMatch( n, @\u0022(?i)(?:^|[_ .-])LOD0(?:$|[_ .-])\u0022 ) ).ToArray();\n\tpublic string[] RenderedMaterials\n\t{\n\t\tget\n\t\t{\n\t\t\tvar meshes = HighestDetailMeshes;\n\t\t\tvar bindings = RendererSlots.Values.Where( b =\u003E meshes.Length == 0 || meshes.Contains( b.Mesh ) ).ToArray();\n\t\t\treturn bindings.Length \u003E 0 \u0026\u0026 bindings.All( b =\u003E b.Slots.Length \u003E 0 ) ? bindings.SelectMany( b =\u003E b.Slots ).Distinct( StringComparer.OrdinalIgnoreCase ).ToArray() : Materials.ToArray();\n\t\t}\n\t}\n\tpublic static string Normalize( string name ) =\u003E Regex.Replace( name.ToLowerInvariant(), \u0022[^a-z]\u0022, \u0022\u0022 );\n\n\tpublic static UnityModel Read( UnityAsset asset )\n\t{\n\t\tvar result = new UnityModel();\n\t\tif ( !asset.Path.EndsWith( \u0022.fbx\u0022, StringComparison.OrdinalIgnoreCase ) ) return result;\n\t\tusing var stream = File.OpenRead( asset.Source );\n\t\tusing var reader = new BinaryReader( stream, Encoding.UTF8 );\n\t\tvar header = Encoding.ASCII.GetString( reader.ReadBytes( 23 ) );\n\t\tif ( !header.StartsWith( \u0022Kaydara FBX Binary\u0022, StringComparison.Ordinal ) )\n\t\t{\n\t\t\tstream.Position = 0;\n\t\t\tusing var textReader = new StreamReader( stream );\n\t\t\tvar links = new UnityAsciiFbxLinks();\n\t\t\tvar asciiBindings = new UnityFbxBindings();\n\t\t\tstring line;\n\t\t\twhile ( (line = textReader.ReadLine()) != null )\n\t\t\t{\n\t\t\t\tlinks.Observe( line );\n\t\t\t\tasciiBindings.ObserveAscii( line );\n\t\t\t\tvar unit = Regex.Match( line, \u0022^\\\\s*(?:P|Property):\\\\s*\\\u0022UnitScaleFactor\\\u0022.*?,\\\\s*([-\u002B0-9.eE]\u002B)\\\\s*$\u0022 );\n\t\t\t\tif ( unit.Success \u0026\u0026 double.TryParse( unit.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var centimeters ) \u0026\u0026 centimeters \u003E 0 \u0026\u0026 double.IsFinite( centimeters ) ) result.UnitScaleCentimeters = centimeters;\n\t\t\t\tvar material = Regex.Match( line, \u0022^\\\\s*Material:\\\\s*(?:[-0-9]\u002B,\\\\s*)?\\\u0022Material::([^\\\u0022]\u002B)\\\u0022\u0022 );\n\t\t\t\tif ( material.Success \u0026\u0026 !result.Materials.Contains( material.Groups[1].Value ) ) result.Materials.Add( material.Groups[1].Value );\n\t\t\t\tvar mesh = Regex.Match( line, \u0022^\\\\s*Model:\\\\s*(?:[-0-9]\u002B,\\\\s*)?\\\u0022Model::([^\\\u0022]\u002B)\\\u0022,\\\\s*\\\u0022Mesh\\\u0022\u0022 );\n\t\t\t\tif ( mesh.Success \u0026\u0026 !result.Meshes.Contains( mesh.Groups[1].Value ) ) result.Meshes.Add( mesh.Groups[1].Value );\n\t\t\t}\n\t\t\tforeach ( var link in links.Resolve() ) result.MaterialAlbedoFiles[link.Key] = link.Value;\n\t\t\tasciiBindings.Apply( result, asset.Metadata != null \u0026\u0026 Regex.IsMatch( UnityImport.ReadText( asset.Metadata ), @\u0022(?m)^\\s*preserveHierarchy:\\s*1\\s*$\u0022 ) );\n\t\t\treturn result;\n\t\t}\n\t\tvar wide = reader.ReadUInt32() \u003E= 7500;\n\t\tvar materialNames = new Dictionary\u003Clong, string\u003E();\n\t\tvar textureFiles = new Dictionary\u003Clong, string\u003E();\n\t\tvar diffuseLinks = new List\u003C(long Texture, long Material)\u003E();\n\t\tvar bindings = new UnityFbxBindings();\n\t\tint nodes = 0;\n\t\tvoid ReadNodes( long limit, bool objects = false, bool connections = false, long textureId = 0, bool settings = false )\n\t\t{\n\t\t\twhile ( stream.Position \u002B (wide ? 25 : 13) \u003C= limit )\n\t\t\t{\n\t\t\t\tif ( \u002B\u002Bnodes \u003E 200000 ) throw new InvalidDataException( \u0022Too many FBX nodes.\u0022 );\n\t\t\t\tvar end = wide ? (long)reader.ReadUInt64() : reader.ReadUInt32();\n\t\t\t\tvar properties = wide ? (long)reader.ReadUInt64() : reader.ReadUInt32();\n\t\t\t\tvar propertyBytes = wide ? (long)reader.ReadUInt64() : reader.ReadUInt32();\n\t\t\t\tvar nameBytes = reader.ReadByte();\n\t\t\t\tif ( end == 0 ) return;\n\t\t\t\tif ( end \u003C= stream.Position || end \u003E stream.Length ) throw new InvalidDataException( \u0022Invalid FBX node offset.\u0022 );\n\t\t\t\tvar name = Encoding.UTF8.GetString( reader.ReadBytes( nameBytes ) );\n\t\t\t\tvar children = stream.Position \u002B propertyBytes;\n\t\t\t\tif ( children \u003E end ) throw new InvalidDataException( \u0022Invalid FBX property size.\u0022 );\n\t\t\t\tobject Property()\n\t\t\t\t{\n\t\t\t\t\t\tvar type = (char)reader.ReadByte();\n\t\t\t\t\t\tif ( type == \u0027L\u0027 ) return reader.ReadInt64();\n\t\t\t\t\t\tif ( type == \u0027I\u0027 ) return reader.ReadInt32();\n\t\t\t\t\t\tif ( type == \u0027D\u0027 ) return reader.ReadDouble();\n\t\t\t\t\t\tif ( type == \u0027F\u0027 ) return reader.ReadSingle();\n\t\t\t\t\t\tif ( type == \u0027S\u0027 )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar length = reader.ReadInt32();\n\t\t\t\t\t\t\tif ( length \u003C 0 || length \u003E 1024 * 1024 || stream.Position \u002B length \u003E children ) throw new InvalidDataException( \u0022Invalid FBX string.\u0022 );\n\t\t\t\t\t\t\treturn Encoding.UTF8.GetString( reader.ReadBytes( length ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow new InvalidDataException( \u0022Unsupported FBX object property.\u0022 );\n\t\t\t\t}\n\t\t\t\tif ( objects \u0026\u0026 name is \u0022Material\u0022 or \u0022Model\u0022 or \u0022Texture\u0022 \u0026\u0026 properties \u003E= 3 )\n\t\t\t\t{\n\t\t\t\t\tvar id = Convert.ToInt64( Property() );\n\t\t\t\t\tvar objectName = (Property() as string ?? \u0022\u0022).Split( \u0027\\0\u0027 )[0];\n\t\t\t\t\tvar typeName = Property() as string;\n\t\t\t\t\tif ( name == \u0022Material\u0022 ) { result.Materials.Add( objectName ); materialNames[id] = objectName; bindings.Materials[id] = objectName; }\n\t\t\t\t\tif ( name == \u0022Model\u0022 )\n\t\t\t\t\t{\n\t\t\t\t\t\tbindings.Nodes[id] = (objectName, typeName == \u0022Mesh\u0022);\n\t\t\t\t\t\tif ( typeName == \u0022Mesh\u0022 ) result.Meshes.Add( objectName );\n\t\t\t\t\t}\n\t\t\t\t\tif ( name == \u0022Texture\u0022 ) { stream.Position = children; ReadNodes( end, textureId: id ); }\n\t\t\t\t}\n\t\t\t\telse if ( textureId != 0 \u0026\u0026 name is \u0022FileName\u0022 or \u0022RelativeFilename\u0022 \u0026\u0026 properties \u003E 0 ) textureFiles[textureId] = Property() as string;\n\t\t\t\telse if ( settings \u0026\u0026 name is \u0022P\u0022 or \u0022Property\u0022 \u0026\u0026 properties \u003E= 4 \u0026\u0026 properties \u003C= 8 )\n\t\t\t\t{\n\t\t\t\t\tif ( Property() as string == \u0022UnitScaleFactor\u0022 )\n\t\t\t\t\t{\n\t\t\t\t\t\tobject value = null;\n\t\t\t\t\t\tfor ( var index = 1; index \u003C properties; index\u002B\u002B ) value = Property();\n\t\t\t\t\t\tvar centimeters = Convert.ToDouble( value, CultureInfo.InvariantCulture );\n\t\t\t\t\t\tif ( centimeters \u003E 0 \u0026\u0026 double.IsFinite( centimeters ) ) result.UnitScaleCentimeters = centimeters;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if ( connections \u0026\u0026 name == \u0022C\u0022 \u0026\u0026 properties \u003E= 3 )\n\t\t\t\t{\n\t\t\t\t\tvar kind = Property() as string;\n\t\t\t\t\tvar child = Convert.ToInt64( Property() );\n\t\t\t\t\tvar parent = Convert.ToInt64( Property() );\n\t\t\t\t\tvar channel = properties \u003E= 4 ? Property() as string ?? \u0022\u0022 : \u0022\u0022;\n\t\t\t\t\tif ( kind == \u0022OO\u0022 ) bindings.Connections.Add( (child, parent) );\n\t\t\t\t\tif ( kind == \u0022OP\u0022 \u0026\u0026 (channel == \u0022DiffuseColor\u0022 || channel.EndsWith( \u0022|base_color_map\u0022, StringComparison.Ordinal )) ) diffuseLinks.Add( (child, parent) );\n\t\t\t\t}\n\t\t\t\tif ( name == \u0022Objects\u0022 ) { stream.Position = children; ReadNodes( end, true ); }\n\t\t\t\tif ( name == \u0022Connections\u0022 ) { stream.Position = children; ReadNodes( end, connections: true ); }\n\t\t\t\tif ( name == \u0022GlobalSettings\u0022 || settings \u0026\u0026 name is \u0022Properties70\u0022 or \u0022Properties60\u0022 ) { stream.Position = children; ReadNodes( end, settings: true ); }\n\t\t\t\tstream.Position = end;\n\t\t\t}\n\t\t}\n\t\tReadNodes( stream.Length, false );\n\t\tbindings.Apply( result, asset.Metadata != null \u0026\u0026 Regex.IsMatch( UnityImport.ReadText( asset.Metadata ), @\u0022(?m)^\\s*preserveHierarchy:\\s*1\\s*$\u0022 ) );\n\t\tforeach ( var link in diffuseLinks )\n\t\t\tif ( materialNames.TryGetValue( link.Material, out var material ) \u0026\u0026 textureFiles.TryGetValue( link.Texture, out var file ) \u0026\u0026 !string.IsNullOrEmpty( file ) )\n\t\t\t\tresult.MaterialAlbedoFiles[material] = file.Replace( \u0027\\\\\u0027, \u0027/\u0027 ).Split( \u0027/\u0027 ).Last();\n\t\treturn result;\n\t}\n\n\tpublic static void AssignPrefabMaterials( UnityArchive archive ) =\u003E UnityPrefabBindings.Apply( archive );\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/UnityAssetTree.cs","FileName":"UnityAssetTree.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Editor;\nusing Sandbox;\n\nnamespace ImportUnityPackage;\n\npublic sealed class UnityAssetTree : TreeView\n{\n\treadonly Func\u003CImportPlan\u003E plan;\n\treadonly Action changed;\n\treadonly Action\u003CUnityAsset\u003E describe;\n\tpublic UnityAssetTree( Widget parent, Func\u003CImportPlan\u003E plan, Action changed, Action\u003CUnityAsset\u003E describe ) : base( parent )\n\t{\n\t\tthis.plan = plan;\n\t\tthis.changed = changed;\n\t\tthis.describe = describe;\n\t\tMultiSelect = false;\n\t}\n\n\tpublic void Load( IEnumerable\u003CUnityAsset\u003E assets )\n\t{\n\t\tvar root = new AssetNode( \u0022Assets\u0022, this );\n\t\tforeach ( var asset in assets )\n\t\t{\n\t\t\tvar parts = asset.Path.Split( \u0027/\u0027 );\n\t\t\tvar node = root;\n\t\t\tforeach ( var part in parts[..^1] )\n\t\t\t{\n\t\t\t\tvar folder = node.Children.OfType\u003CAssetNode\u003E().FirstOrDefault( c =\u003E c.Asset == null \u0026\u0026 c.Name == part );\n\t\t\t\tif ( folder == null ) { folder = new AssetNode( part, this ); node.AddItem( folder ); }\n\t\t\t\tnode = folder;\n\t\t\t}\n\t\t\tnode.AddItem( new AssetNode( parts[^1], this ) { Asset = asset } );\n\t\t}\n\t\tSetItems( new[] { root } );\n\t\tOpen( root );\n\t}\n\n\tpublic void SetExpanded( bool expanded )\n\t{\n\t\tforeach ( var root in Items.OfType\u003CTreeNode\u003E() )\n\t\t{\n\t\t\tif ( expanded ) Open( root, recursive: true );\n\t\t\telse Close( root, recursive: true );\n\t\t}\n\t}\n\n\tpublic void ChangeExpandedLayer( bool expand )\n\t{\n\t\tvar candidates = new List\u003C(TreeNode Node, int Depth)\u003E();\n\t\tvoid Visit( TreeNode node, int depth )\n\t\t{\n\t\t\tvar children = node.Children.ToArray();\n\t\t\tif ( children.Length == 0 ) return;\n\t\t\t// A child\u0027s layout position reflects the actual open state, including\n\t\t\t// changes made with folder arrows or the keyboard, even offscreen.\n\t\t\tvar open = TryGetItemRect( children[0], out _ );\n\t\t\tif ( expand != open ) candidates.Add( (node, depth) );\n\t\t\tif ( open ) foreach ( var child in children ) Visit( child, depth \u002B 1 );\n\t\t}\n\t\tforeach ( var root in Items.OfType\u003CTreeNode\u003E() ) Visit( root, 0 );\n\t\tif ( candidates.Count == 0 ) return;\n\t\tvar layer = expand ? candidates.Min( c =\u003E c.Depth ) : candidates.Max( c =\u003E c.Depth );\n\t\tforeach ( var (node, depth) in candidates.Where( c =\u003E c.Depth == layer ) )\n\t\t{\n\t\t\t// Clear remembered child expansion so opening one layer cannot reveal several.\n\t\t\tClose( node, recursive: true );\n\t\t\tif ( expand ) Open( node );\n\t\t}\n\t}\n\n\tprotected override bool OnItemPressed( VirtualWidget item, MouseEvent e )\n\t{\n\t\tif ( !base.OnItemPressed( item, e ) ) return false; // Leave folder expand arrows functional.\n\t\tif ( e.LeftMouseButton \u0026\u0026 item.Object is AssetNode node ) node.ToggleSelection();\n\t\treturn true;\n\t}\n\n\tsealed class AssetNode : TreeNode\n\t{\n\t\treadonly UnityAssetTree owner;\n\t\tpublic UnityAsset Asset { get; init; }\n\t\tpublic AssetNode( string name, UnityAssetTree owner ) { Name = name; this.owner = owner; }\n\t\tIEnumerable\u003CUnityAsset\u003E Leaves =\u003E Asset != null ? new[] { Asset } : Children.OfType\u003CAssetNode\u003E().SelectMany( c =\u003E c.Leaves );\n\t\tpublic void ToggleSelection()\n\t\t{\n\t\t\towner.describe( Asset );\n\t\t\t// Dependencies remain included until their last selected parent is removed.\n\t\t\tif ( Asset != null \u0026\u0026 !Asset.Selected \u0026\u0026 owner.plan()?.Find( Asset ) != null ) return;\n\t\t\tvar eligible = Leaves.Where( a =\u003E a.Kind != UnityAssetKind.Unsupported ).ToArray();\n\t\t\tvar select = !eligible.All( a =\u003E owner.plan()?.Find( a ) != null );\n\t\t\tforeach ( var asset in eligible ) asset.Selected = select;\n\t\t\towner.changed();\n\t\t}\n\t\tpublic override void OnKeyPress( KeyEvent e )\n\t\t{\n\t\t\tif ( e.Key == KeyCode.Space ) { ToggleSelection(); e.Accepted = true; }\n\t\t}\n\t\tpublic override void OnPaint( VirtualWidget item )\n\t\t{\n\t\t\tPaintSelection( item );\n\t\t\tvar plan = owner.plan();\n\t\t\tvar entry = Asset == null ? null : plan?.Find( Asset );\n\t\t\tvar eligible = Leaves.Where( a =\u003E a.Kind != UnityAssetKind.Unsupported ).ToArray();\n\t\t\tvar count = eligible.Count( a =\u003E plan?.Find( a ) != null );\n\t\t\tvar active = eligible.Length \u003E 0;\n\t\t\tvar checkbox = !active || count == 0 ? \u0022check_box_outline_blank\u0022 : count == eligible.Length ? \u0022check_box\u0022 : \u0022indeterminate_check_box\u0022;\n\t\t\tif ( entry is { Explicit: false } ) checkbox = \u0022link\u0022;\n\t\t\tPaint.SetPen( active ? Theme.Text : Theme.Text.WithAlpha( 0.35f ) );\n\t\t\tPaint.DrawIcon( item.Rect, checkbox, 18, TextFlag.LeftCenter );\n\t\t\tPaint.DrawIcon( item.Rect.Shrink( 24, 0, 0, 0 ), Asset == null ? \u0022folder\u0022 : Asset.Kind switch\n\t\t\t{\n\t\t\t\tUnityAssetKind.Material =\u003E \u0022palette\u0022, UnityAssetKind.Texture =\u003E \u0022image\u0022, UnityAssetKind.Model =\u003E \u0022view_in_ar\u0022, _ =\u003E \u0022description\u0022\n\t\t\t}, 18, TextFlag.LeftCenter );\n\t\t\tvar annotation = Asset?.Kind == UnityAssetKind.Unsupported ? \u0022 (unsupported)\u0022 : entry == null ? \u0022\u0022 :\n\t\t\t\t$\u0022 \u00B7 {entry.OutputLabel}\u0022 \u002B (!entry.Explicit ? \u0022 \u00B7 dependency\u0022 : \u0022\u0022) \u002B (plan.Issues.Any( i =\u003E i.Asset == Asset.Path ) ? \u0022 \u00B7 issue\u0022 : \u0022\u0022);\n\t\t\tPaint.DrawText( item.Rect.Shrink( 48, 0, 0, 0 ), Name \u002B annotation, TextFlag.LeftCenter );\n\t\t}\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityAsciiFbxLinks.cs","FileName":"UnityAsciiFbxLinks.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.RegularExpressions;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003ELegacy ASCII FBX attaches texture/material objects to the mesh by name.\u003C/summary\u003E\ninternal sealed class UnityAsciiFbxLinks\n{\n\treadonly Dictionary\u003Cstring, string\u003E textures = new( StringComparer.Ordinal );\n\treadonly List\u003C(string Child, string Parent)\u003E connections = new();\n\tint depth;\n\tint objectsDepth = -1;\n\tint textureDepth = -1;\n\tstring texture;\n\n\tpublic void Observe( string line )\n\t{\n\t\tif ( Regex.IsMatch( line, @\u0022^\\s*Objects:\\s*\\{\u0022 ) ) objectsDepth = depth \u002B 1;\n\t\tif ( objectsDepth \u003E 0 \u0026\u0026 depth == objectsDepth )\n\t\t{\n\t\t\tvar start = Regex.Match( line, \u0022^\\\\s*Texture:\\\\s*\\\u0022(Texture::[^\\\u0022]\u002B)\\\u0022\u0022 );\n\t\t\tif ( start.Success ) { texture = start.Groups[1].Value; textureDepth = depth \u002B 1; }\n\t\t}\n\t\tif ( texture != null \u0026\u0026 depth \u003E= textureDepth )\n\t\t{\n\t\t\tvar file = Regex.Match( line, \u0022^\\\\s*(?:FileName|Filename|RelativeFilename):\\\\s*\\\u0022([^\\\u0022]\u002B)\\\u0022\u0022 );\n\t\t\tif ( file.Success ) textures[texture] = file.Groups[1].Value.Replace( \u0027\\\\\u0027, \u0027/\u0027 ).Split( \u0027/\u0027 ).Last();\n\t\t}\n\t\tvar connection = Regex.Match( line, \u0022^\\\\s*Connect:\\\\s*\\\u0022OO\\\u0022,\\\\s*\\\u0022([^\\\u0022]\u002B)\\\u0022,\\\\s*\\\u0022([^\\\u0022]\u002B)\\\u0022\u0022 );\n\t\tif ( connection.Success ) connections.Add( (connection.Groups[1].Value, connection.Groups[2].Value) );\n\t\tvar structural = line.Contains( \u0027\u0022\u0027 ) ? Regex.Replace( line, \u0022\\\u0022[^\\\u0022]*\\\u0022\u0022, \u0022\u0022 ) : line;\n\t\tstructural = structural.Split( \u0027;\u0027 )[0];\n\t\tdepth \u002B= structural.Count( c =\u003E c == \u0027{\u0027 ) - structural.Count( c =\u003E c == \u0027}\u0027 );\n\t\tif ( depth \u003C textureDepth ) { texture = null; textureDepth = -1; }\n\t\tif ( depth \u003C objectsDepth ) objectsDepth = -1;\n\t}\n\n\tpublic IEnumerable\u003CKeyValuePair\u003Cstring, string\u003E\u003E Resolve()\n\t{\n\t\tvar candidates = new List\u003C(string Material, string File)\u003E();\n\t\tforeach ( var mesh in connections.Where( c =\u003E c.Parent.StartsWith( \u0022Model::\u0022, StringComparison.Ordinal ) ).GroupBy( c =\u003E c.Parent ) )\n\t\t{\n\t\t\tvar materials = mesh.Select( c =\u003E c.Child ).Where( c =\u003E c.StartsWith( \u0022Material::\u0022, StringComparison.Ordinal ) ).Distinct().ToArray();\n\t\t\tvar maps = mesh.Select( c =\u003E c.Child ).Where( textures.ContainsKey ).Distinct().ToArray();\n\t\t\t// Without polygon-slot information, multiple materials/textures are ambiguous.\n\t\t\tif ( materials.Length == 1 \u0026\u0026 maps.Length == 1 ) candidates.Add( (materials[0][\u0022Material::\u0022.Length..], textures[maps[0]]) );\n\t\t}\n\t\tforeach ( var material in candidates.GroupBy( c =\u003E c.Material ) )\n\t\t{\n\t\t\tvar files = material.Select( c =\u003E c.File ).Distinct( StringComparer.OrdinalIgnoreCase ).ToArray();\n\t\t\tif ( files.Length == 1 ) yield return new( material.Key, files[0] );\n\t\t}\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityFbxCompatibility.cs","FileName":"UnityFbxCompatibility.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003ERepairs a redundant array emitted by some legacy ASCII FBX exporters.\u003C/summary\u003E\npublic static class UnityFbxCompatibility\n{\n\tpublic static byte[] Normalize( string source, CancellationToken cancel )\n\t{\n\t\t// Ordinary/binary models remain byte-for-byte copies. Bound optional text processing.\n\t\tusing var stream = File.OpenRead( source );\n\t\tif ( stream.Length \u003E 32 * 1024 * 1024 ) return null;\n\t\tvar header = new byte[32];\n\t\tvar length = stream.Read( header );\n\t\tif ( !Encoding.ASCII.GetString( header, 0, length ).StartsWith( \u0022; FBX \u0022, StringComparison.Ordinal ) ) return null;\n\t\tstream.Position = 0;\n\t\tusing var reader = new StreamReader( stream, new UTF8Encoding( false, true ), false );\n\t\tstring text;\n\t\ttry { text = reader.ReadToEnd(); }\n\t\tcatch ( DecoderFallbackException ) { return null; }\n\t\treturn NormalizeText( text, cancel ) is string normalized ? Encoding.UTF8.GetBytes( normalized ) : null;\n\t}\n\n\tinternal static string NormalizeText( string text, CancellationToken cancel )\n\t{\n\t\tcancel.ThrowIfCancellationRequested();\n\t\tconst string numbers = @\u0022[-\u002B0-9.eE,\\s]\u002B\u0022;\n\t\tvar timeout = TimeSpan.FromSeconds( 2 );\n\t\tvar tails = Regex.Matches( text, @\u0022(?m)^[ \\t]*}(?\u003Ctail\u003E,[-\u002B0-9.eE, \\t]\u002B)(?=\\r?$)\u0022, RegexOptions.None, timeout );\n\t\tif ( tails.Count == 0 ) return null;\n\t\tstring Compact( string value ) =\u003E string.Concat( value.Where( c =\u003E !char.IsWhiteSpace( c ) ) ).Trim( \u0027,\u0027 );\n\t\tvar vertices = Regex.Matches( text, @\u0022(?m)^[ \\t]*Vertices:[ \\t]*(\u0022 \u002B numbers \u002B \u0022)\u0022, RegexOptions.None, timeout )\n\t\t\t.Select( m =\u003E Compact( m.Groups[1].Value ) ).Where( s =\u003E s.Length \u003E 0 ).ToHashSet( StringComparer.Ordinal );\n\t\tvar output = new StringBuilder( text );\n\t\tvar changed = false;\n\t\tforeach ( Match match in tails.Reverse() )\n\t\t{\n\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\tvar tail = match.Groups[\u0022tail\u0022];\n\t\t\t// Only discard an exact repeated vertex sequence at an invalid syntax position.\n\t\t\t// Different data is left untouched so an uncertain repair cannot alter geometry.\n\t\t\tif ( !vertices.Contains( Compact( tail.Value ) ) ) continue;\n\t\t\toutput.Remove( tail.Index, tail.Length );\n\t\t\tchanged = true;\n\t\t}\n\t\treturn changed ? output.ToString() : null;\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/TextureChannels.cs","FileName":"TextureChannels.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Sandbox;\nusing SkiaSharp;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003ECPU image work only. Every worker owns its decoder, bitmap and pixel arrays.\u003C/summary\u003E\npublic static class TextureChannels\n{\n\tpublic const long MemoryBudget = 1024L * 1024 * 1024;\n\tpublic const int WorkerLimit = 2;\n\n\tpublic static void ExtractBatch( IReadOnlyList\u003CTextureChannelRequest\u003E requests, CancellationToken cancel )\n\t\t=\u003E ProcessBatch( requests, cancel );\n\n\tpublic sealed record BatchStats( int Decodes, int Channels, int PeakWorkers, long PeakReservedBytes, long ElapsedMilliseconds );\n\n\tpublic static BatchStats ProcessBatch( IReadOnlyList\u003CTextureChannelRequest\u003E requests, CancellationToken cancel, int workers = WorkerLimit, long budget = MemoryBudget )\n\t{\n\t\tif ( workers \u003C 1 || workers \u003E WorkerLimit || budget \u003C= 0 ) throw new ArgumentOutOfRangeException();\n\t\tvar timer = System.Diagnostics.Stopwatch.StartNew();\n\t\tvar gate = new object();\n\t\tlong reserved = 0, peakReserved = 0;\n\t\tint active = 0, peakWorkers = 0, decodes = 0;\n\t\tvar groups = requests.GroupBy( r =\u003E r.Source, StringComparer.OrdinalIgnoreCase ).Select( g =\u003E g.ToArray() ).ToArray();\n\t\tParallel.ForEach( groups, new ParallelOptions { MaxDegreeOfParallelism = workers, CancellationToken = cancel }, group =\u003E\n\t\t{\n\t\t\t// Unknown codecs and images larger than the admission budget run exclusively.\n\t\t\t// This is an estimated working-set budget, not a hard cap on native allocations.\n\t\t\tvar reservation = Math.Min( budget, EstimateMemory( group[0].Source, budget ) );\n\t\t\tlock ( gate )\n\t\t\t{\n\t\t\t\twhile ( reserved \u002B reservation \u003E budget ) { cancel.ThrowIfCancellationRequested(); Monitor.Wait( gate, 50 ); }\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\treserved \u002B= reservation;\n\t\t\t\tpeakReserved = Math.Max( reserved, peakReserved );\n\t\t\t\tpeakWorkers = Math.Max( \u002B\u002Bactive, peakWorkers );\n\t\t\t}\n\t\t\ttry\n\t\t\t{\n\t\t\t\tusing var bitmap = Decode( group[0].Source, cancel );\n\t\t\t\tInterlocked.Increment( ref decodes );\n\t\t\t\tvar sourcePixels = bitmap.GetPixels();\n\t\t\t\tvar outputPixels = new Color[sourcePixels.Length];\n\t\t\t\tforeach ( var request in group )\n\t\t\t\t{\n\t\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\t\tConvertPixels( sourcePixels, outputPixels, request.Channel, request.Scale, request.Invert, cancel );\n\t\t\t\t\tbitmap.SetPixels( outputPixels );\n\t\t\t\t\tFile.WriteAllBytes( request.Destination, bitmap.ToPng() );\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch ( OperationCanceledException ) { throw; }\n\t\t\tcatch ( Exception ex ) { throw new InvalidDataException( $\u0022Texture channel extraction failed for \u0027{group[0].Source}\u0027: {ex.Message}\u0022, ex ); }\n\t\t\tfinally { lock ( gate ) { reserved -= reservation; active--; Monitor.PulseAll( gate ); } }\n\t\t} );\n\t\treturn new( decodes, requests.Count, peakWorkers, peakReserved, timer.ElapsedMilliseconds );\n\t}\n\n\tstatic long EstimateMemory( string source, long unknown )\n\t{\n\t\tusing var stream = File.OpenRead( source );\n\t\tusing var codec = SKCodec.Create( stream );\n\t\tif ( codec == null ) return unknown;\n\t\t// Decoded image, two float-color arrays, encoder buffers and compressed input.\n\t\treturn checked( (long)codec.Info.Width * codec.Info.Height * 64 \u002B stream.Length * 2 \u002B 16 * 1024 * 1024 );\n\t}\n\n\tstatic Bitmap Decode( string source, CancellationToken cancel )\n\t{\n\t\tcancel.ThrowIfCancellationRequested();\n\t\treturn Bitmap.CreateFromBytes( File.ReadAllBytes( source ) ) ?? throw new InvalidDataException( \u0022Unable to decode packed texture: \u0022 \u002B source );\n\t}\n\n\t// Retained for callers supplying a single-channel callback.\n\tpublic static byte[] Extract( string source, int channel, double scale, bool invert, CancellationToken cancel )\n\t{\n\t\tusing var bitmap = Decode( source, cancel );\n\t\tvar pixels = bitmap.GetPixels();\n\t\tConvertPixels( pixels, pixels, channel, scale, invert, cancel );\n\t\tbitmap.SetPixels( pixels );\n\t\treturn bitmap.ToPng();\n\t}\n\n\tstatic void ConvertPixels( Color[] source, Color[] output, int channel, double scale, bool invert, CancellationToken cancel )\n\t{\n\t\tfor ( var i = 0; i \u003C source.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( i % 65536 == 0 ) cancel.ThrowIfCancellationRequested();\n\t\t\tvar p = source[i];\n\t\t\tvar value = (float)Math.Clamp( (channel switch { 0 =\u003E p.r, 1 =\u003E p.g, 2 =\u003E p.b, _ =\u003E p.a }) * scale, 0, 1 );\n\t\t\tif ( invert ) value = 1 - value;\n\t\t\toutput[i] = new Color( value, value, value, 1 );\n\t\t}\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityMaterial.cs","FileName":"UnityMaterial.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003EBest-effort conversion of text-serialized Standard/URP material properties.\u003C/summary\u003E\npublic sealed class UnityMaterial\n{\n\tpublic Dictionary\u003Cstring, string\u003E Textures { get; } = new();\n\tpublic Dictionary\u003Cstring, double\u003E Numbers { get; } = new();\n\tpublic Dictionary\u003Cstring, double[]\u003E Colors { get; } = new();\n\tpublic List\u003Cstring\u003E Warnings { get; } = new();\n\tpublic List\u003Cstring\u003E Information { get; } = new();\n\treadonly Dictionary\u003Cstring, string\u003E channelImages = new();\n\treadonly Dictionary\u003Cstring, double[]\u003E textureScale = new();\n\treadonly Dictionary\u003Cstring, double[]\u003E textureOffset = new();\n\tbool shaderAlphaTest;\n\tbool shaderBackfaces;\n\tbool shaderTranslucent;\n\tbool terrainLayer;\n\tHashSet\u003Cstring\u003E declaredProperties;\n\tstatic readonly string[] ColorProperties = { \u0022_BaseMap\u0022, \u0022_Diffuse\u0022, \u0022_MainTex\u0022, \u0022_Albedo\u0022, \u0022_BaseColorMap\u0022 };\n\tpublic IEnumerable\u003Cstring\u003E ColorTextureGuids =\u003E ColorProperties.Where( Textures.ContainsKey ).Select( p =\u003E Textures[p] );\n\tbool AlphaTest =\u003E shaderAlphaTest || Number( \u0022_Mode\u0022, 0 ) == 1 || Number( \u0022_AlphaClip\u0022, 0 ) == 1;\n\tbool Translucent =\u003E shaderTranslucent || Number( \u0022_Mode\u0022, 0 ) \u003E= 2 || Number( \u0022_Surface\u0022, 0 ) == 1;\n\tpublic string ShaderName { get; private set; }\n\tpublic string ShaderGuid { get; private set; }\n\tstatic readonly Regex GuidPattern = new( @\u0022guid:\\s*([a-fA-F0-9]{32})\u0022 );\n\n\tpublic static IEnumerable\u003Cstring\u003E References( string text ) =\u003E GuidPattern.Matches( text )\n\t\t.Select( m =\u003E m.Groups[1].Value ).Where( g =\u003E g.Any( c =\u003E c != \u00270\u0027 ) ).Distinct( StringComparer.OrdinalIgnoreCase );\n\n\tpublic static UnityMaterial Parse( string text )\n\t{\n\t\tif ( text.Contains( \u0022TerrainLayer:\u0022 ) \u0026\u0026 !text.Contains( \u0027\\0\u0027 ) ) return ParseTerrainLayer( text );\n\t\tif ( !text.Contains( \u0022Material:\u0022 ) || text.Contains( \u0027\\0\u0027 ) )\n\t\t\tthrow new InvalidDataException( \u0022Material is not Unity text YAML. Re-export it with Asset Serialization set to Force Text in Unity.\u0022 );\n\t\tvar result = new UnityMaterial();\n\t\tresult.ShaderGuid = Regex.Match( text, @\u0022m_Shader:[^\\r\\n]*guid:\\s*([a-fA-F0-9]{32})\u0022 ).Groups[1].Value;\n\t\tstring property = null;\n\t\tforeach ( var line in text.Split( \u0027\\n\u0027 ) )\n\t\t{\n\t\t\tvar item = Regex.Match( line, @\u0022^\\s*-\\s*(_[A-Za-z0-9_]\u002B):\\s*(.*)$\u0022 );\n\t\t\tif ( item.Success )\n\t\t\t{\n\t\t\t\tproperty = item.Groups[1].Value;\n\t\t\t\tvar value = item.Groups[2].Value.Trim();\n\t\t\t\tif ( double.TryParse( value, NumberStyles.Float, CultureInfo.InvariantCulture, out var number ) \u0026\u0026 double.IsFinite( number ) )\n\t\t\t\t\tresult.Numbers[property] = number;\n\t\t\t\tif ( value.StartsWith( \u0022{\u0022 ) )\n\t\t\t\t{\n\t\t\t\t\tvar channels = Regex.Matches( value, @\u0022[rgba]:\\s*([-\u002B0-9.eE]\u002B)\u0022 );\n\t\t\t\t\tif ( channels.Count == 4 ) result.Colors[property] = channels.Select( m =\u003E double.Parse( m.Groups[1].Value, CultureInfo.InvariantCulture ) ).ToArray();\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( property != null \u0026\u0026 line.Contains( \u0022m_Texture:\u0022 ) )\n\t\t\t{\n\t\t\t\tvar guid = GuidPattern.Match( line );\n\t\t\t\tif ( guid.Success \u0026\u0026 guid.Groups[1].Value.Any( c =\u003E c != \u00270\u0027 ) ) result.Textures[property] = guid.Groups[1].Value;\n\t\t\t}\n\t\t\tif ( property != null \u0026\u0026 (line.Contains( \u0022m_Scale:\u0022 ) || line.Contains( \u0022m_Offset:\u0022 )) )\n\t\t\t{\n\t\t\t\tvar xy = Regex.Match( line, @\u0022x:\\s*([-\u002B0-9.eE]\u002B),\\s*y:\\s*([-\u002B0-9.eE]\u002B)\u0022 );\n\t\t\t\tif ( xy.Success ) (line.Contains( \u0022m_Scale:\u0022 ) ? result.textureScale : result.textureOffset)[property] =\n\t\t\t\t\tnew[] { double.Parse( xy.Groups[1].Value, CultureInfo.InvariantCulture ), double.Parse( xy.Groups[2].Value, CultureInfo.InvariantCulture ) };\n\t\t\t}\n\t\t\tif ( line.Contains( \u0022m_Scale:\u0022 ) \u0026\u0026 !Regex.IsMatch( line, @\u0022x:\\s*1(?:\\.0\u002B)?\\s*,\\s*y:\\s*1(?:\\.0\u002B)?\\s*}\u0022 ) ||\n\t\t\t\tline.Contains( \u0022m_Offset:\u0022 ) \u0026\u0026 !Regex.IsMatch( line, @\u0022x:\\s*0(?:\\.0\u002B)?\\s*,\\s*y:\\s*0(?:\\.0\u002B)?\\s*}\u0022 ) )\n\t\t\t\tresult.Warnings.Add( \u0022Texture tiling/offset needs manual adjustment.\u0022 );\n\t\t}\n\t\tif ( result.Textures.ContainsKey( \u0022_MetallicGlossMap\u0022 ) || result.Textures.ContainsKey( \u0022_MaskMap\u0022 ) )\n\t\t\tresult.Warnings.Add( \u0022Packed metallic/smoothness or HDRP mask maps need channel separation; scalar metallic/roughness values were used.\u0022 );\n\t\tresult.Information.Add( \u0022Review the converted material: custom shaders, normal-map conventions and advanced Unity settings are not reproduced exactly.\u0022 );\n\t\treturn result;\n\t}\n\n\tpublic void ConfigureShader( string source )\n\t{\n\t\tShaderName = Regex.Match( source, \u0022Shader\\\\s\u002B\\\u0022([^\\\u0022]\u002B)\\\u0022\u0022 ).Groups[1].Value;\n\t\tdeclaredProperties = Regex.Matches( source, \u0022(?m)^\\\\s*(?:\\\\[[^\\\\]\\\\r\\\\n]*\\\\]\\\\s*)*(_[A-Za-z0-9_]\u002B)\\\\s*\\\\(\\\\s*\\\u0022[^\\\u0022]*\\\u0022\\\\s*,\u0022 )\n\t\t\t.Select( m =\u003E m.Groups[1].Value ).ToHashSet();\n\t\tif ( declaredProperties.Count \u003E 0 )\n\t\t{\n\t\t\t// Unity retains old shader properties in materials. Only use properties declared by the active shader.\n\t\t\tforeach ( var stale in Textures.Keys.Where( k =\u003E !declaredProperties.Contains( k ) ).ToArray() ) Textures.Remove( stale );\n\t\t}\n\t\tshaderAlphaTest = Regex.IsMatch( source, \u0022\\\u0022RenderType\\\u0022\\\\s*=\\\\s*\\\u0022TransparentCutout\\\u0022\u0022 );\n\t\tshaderBackfaces = Regex.IsMatch( source, @\u0022(?m)^\\s*Cull\\s\u002BOff\\s*$\u0022, RegexOptions.IgnoreCase );\n\t\tshaderTranslucent = Regex.IsMatch( source, \u0022\\\u0022RenderType\\\u0022\\\\s*=\\\\s*\\\u0022Transparent\\\u0022\u0022 );\n\t\tif ( !string.IsNullOrEmpty( ShaderName ) ) Warnings.Add( $\u0022Shader \u0027{ShaderName}\u0027 is approximated using s\u0026box\u0027s complex shader; shader code and graph behavior are not translated.\u0022 );\n\t}\n\n\tpublic void PrepareChannels( Func\u003Cstring, int, double, bool, string\u003E extract )\n\t{\n\t\tvoid Channel( string target, string guid, int channel, double scale = 1, bool invert = false )\n\t\t{\n\t\t\tvar path = extract( guid, channel, scale, invert );\n\t\t\tif ( path != null ) channelImages[target] = path;\n\t\t}\n\t\tif ( Textures.TryGetValue( \u0022_MetallicGlossMap\u0022, out var packed ) )\n\t\t{\n\t\t\tChannel( \u0022metal\u0022, packed, 0 );\n\t\t\tChannel( \u0022rough\u0022, packed, 3, Number( \u0022_GlossMapScale\u0022, Number( \u0022_Smoothness\u0022, 1 ) ), true );\n\t\t}\n\t\tif ( Textures.TryGetValue( \u0022_MaskMap\u0022, out var mask ) )\n\t\t{\n\t\t\tChannel( \u0022metal\u0022, mask, 0 );\n\t\t\tChannel( \u0022ao\u0022, mask, 1 );\n\t\t\t// TerrainLit stores height in B; HDRP Lit stores a detail mask instead.\n\t\t\tif ( terrainLayer ) Channel( \u0022height\u0022, mask, 2 );\n\t\t\tChannel( \u0022rough\u0022, mask, 3, 1, true );\n\t\t}\n\t\tif ( Textures.TryGetValue( \u0022_OcclusionMap\u0022, out var ao ) ) Channel( \u0022ao\u0022, ao, 1 );\n\t\tif ( AlphaTest || Translucent )\n\t\t{\n\t\t\tforeach ( var property in ColorProperties )\n\t\t\t{\n\t\t\t\tif ( !Textures.TryGetValue( property, out var guid ) ) continue;\n\t\t\t\tChannel( \u0022opacity\u0022, guid, 3 );\n\t\t\t\tif ( channelImages.ContainsKey( \u0022opacity\u0022 ) ) break;\n\t\t\t}\n\t\t}\n\t\tif ( channelImages.Count \u003E 0 ) Warnings.RemoveAll( w =\u003E w.StartsWith( \u0022Packed metallic/\u0022 ) || w.StartsWith( \u0022Unity terrain mask channels\u0022 ) );\n\t}\n\n\tstatic UnityMaterial ParseTerrainLayer( string text )\n\t{\n\t\tvar result = new UnityMaterial { terrainLayer = true };\n\t\tforeach ( var (source, target) in new[] { (\u0022m_DiffuseTexture\u0022, \u0022_MainTex\u0022), (\u0022m_NormalMapTexture\u0022, \u0022_BumpMap\u0022), (\u0022m_MaskMapTexture\u0022, \u0022_MaskMap\u0022) } )\n\t\t{\n\t\t\tvar line = text.Split( \u0027\\n\u0027 ).FirstOrDefault( l =\u003E l.TrimStart().StartsWith( source \u002B \u0022:\u0022, StringComparison.Ordinal ) );\n\t\t\tif ( line != null \u0026\u0026 References( line ).FirstOrDefault() is string guid ) result.Textures[target] = guid;\n\t\t}\n\t\tforeach ( var (source, target) in new[] { (\u0022m_Metallic\u0022, \u0022_Metallic\u0022), (\u0022m_Smoothness\u0022, \u0022_Smoothness\u0022) } )\n\t\t{\n\t\t\tvar match = Regex.Match( text, @\u0022(?m)^\\s*\u0022 \u002B source \u002B @\u0022:\\s*([-\u002B0-9.eE]\u002B)\u0022 );\n\t\t\tif ( match.Success \u0026\u0026 double.TryParse( match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value ) ) result.Numbers[target] = value;\n\t\t}\n\t\tresult.Warnings.Add( \u0022Terrain layer converted; review world tiling, normal strength and remapping in the terrain material editor.\u0022 );\n\t\tif ( result.Textures.ContainsKey( \u0022_MaskMap\u0022 ) ) result.Warnings.Add( \u0022Unity terrain mask channels need separation; packed mask was preserved as a source texture.\u0022 );\n\t\treturn result;\n\t}\n\n\tstring Texture( Func\u003Cstring, string\u003E resolve, params string[] names )\n\t{\n\t\tforeach ( var name in names )\n\t\t\tif ( Textures.TryGetValue( name, out var guid ) \u0026\u0026 resolve( guid ) is string path ) return path;\n\t\treturn null;\n\t}\n\tpublic double Number( string name, double fallback ) =\u003E Numbers.GetValueOrDefault( name, fallback );\n\tstatic string N( double value ) =\u003E double.IsFinite( value ) ? value.ToString( \u00220.######\u0022, CultureInfo.InvariantCulture ) : \u00220\u0022;\n\tinternal static string Quote( string value ) =\u003E JsonSerializer.Serialize( value );\n\n\tpublic string ToVmat( Func\u003Cstring, string\u003E resolve )\n\t{\n\t\tvar text = new StringBuilder( \u0022Layer0\\n{\\n\\tshader \\\u0022shaders/complex.shader_c\\\u0022\\n\u0022 );\n\t\tvoid Set( string key, string value ) =\u003E text.AppendLine( $\u0022\\t{key} {Quote( value )}\u0022 );\n\t\tSet( \u0022TextureColor\u0022, Texture( resolve, \u0022_BaseMap\u0022, \u0022_Diffuse\u0022, \u0022_MainTex\u0022, \u0022_Albedo\u0022, \u0022_BaseColorMap\u0022 ) ?? \u0022materials/default/default_color.tga\u0022 );\n\t\tvar colorProperty = new[] { \u0022_BaseMap\u0022, \u0022_Diffuse\u0022, \u0022_MainTex\u0022, \u0022_Albedo\u0022, \u0022_BaseColorMap\u0022 }.FirstOrDefault( p =\u003E Textures.TryGetValue( p, out var guid ) \u0026\u0026 resolve( guid ) != null );\n\t\tif ( colorProperty != null )\n\t\t{\n\t\t\tif ( textureScale.TryGetValue( colorProperty, out var scale ) ) Set( \u0022g_vTexCoordScale\u0022, $\u0022[{N( scale[0] )} {N( scale[1] )}]\u0022 );\n\t\t\tif ( textureOffset.TryGetValue( colorProperty, out var offset ) ) Set( \u0022g_vTexCoordOffset\u0022, $\u0022[{N( offset[0] )} {N( offset[1] )}]\u0022 );\n\t\t}\n\t\tSet( \u0022TextureNormal\u0022, Texture( resolve, \u0022_BumpMap\u0022, \u0022_Normal\u0022, \u0022_NormalMap\u0022 ) ?? \u0022materials/default/default_normal.tga\u0022 );\n\t\tSet( \u0022TextureAmbientOcclusion\u0022, channelImages.GetValueOrDefault( \u0022ao\u0022 ) ?? Texture( resolve, \u0022_OcclusionMap\u0022, \u0022_Occlusion\u0022 ) ?? \u0022materials/default/default_ao.tga\u0022 );\n\t\tSet( \u0022TextureRoughness\u0022, channelImages.GetValueOrDefault( \u0022rough\u0022 ) ?? \u0022materials/default/default_rough.tga\u0022 );\n\t\tvar roughness = 1 - Math.Clamp( Number( \u0022_Smoothness\u0022, Number( \u0022_Glossiness\u0022, 0.5 ) ), 0, 1 );\n\t\tSet( \u0022g_flRoughnessScaleFactor\u0022, N( channelImages.ContainsKey( \u0022rough\u0022 ) ? 1 : roughness ) );\n\t\tif ( channelImages.TryGetValue( \u0022metal\u0022, out var metal ) )\n\t\t{\n\t\t\ttext.AppendLine( \u0022\\tF_METALNESS_TEXTURE 1\u0022 );\n\t\t\tSet( \u0022TextureMetalness\u0022, metal );\n\t\t}\n\t\tSet( \u0022g_flMetalness\u0022, N( Math.Clamp( Number( \u0022_Metallic\u0022, 0 ), 0, 1 ) ) );\n\t\tif ( Colors.TryGetValue( \u0022_BaseColor\u0022, out var color ) || colorProperty == \u0022_Diffuse\u0022 \u0026\u0026 Colors.TryGetValue( \u0022_MainColor\u0022, out color ) || Colors.TryGetValue( \u0022_Color\u0022, out color ) )\n\t\t\tSet( \u0022g_vColorTint\u0022, $\u0022[{N( color[0] )} {N( color[1] )} {N( color[2] )} {N( color[3] )}]\u0022 );\n\t\tif ( channelImages.TryGetValue( \u0022opacity\u0022, out var opacity ) ) Set( \u0022TextureTranslucency\u0022, opacity );\n\t\tif ( AlphaTest )\n\t\t{\n\t\t\ttext.AppendLine( \u0022\\tF_ALPHA_TEST 1\u0022 );\n\t\t\tSet( \u0022g_flAlphaTestReference\u0022, N( Number( \u0022_Cutoff\u0022, 0.5 ) ) );\n\t\t}\n\t\tif ( shaderBackfaces || Number( \u0022_Cull\u0022, 2 ) == 0 ) text.AppendLine( \u0022\\tF_RENDER_BACKFACES 1\u0022 );\n\t\tif ( Translucent ) text.AppendLine( \u0022\\tF_TRANSLUCENT 1\u0022 );\n\t\tvar emission = Texture( resolve, \u0022_EmissionMap\u0022, \u0022_EmissiveColorMap\u0022 );\n\t\tif ( emission != null )\n\t\t{\n\t\t\ttext.AppendLine( \u0022\\tF_SELF_ILLUM 1\u0022 );\n\t\t\tSet( \u0022TextureSelfIllumMask\u0022, emission );\n\t\t}\n\t\treturn text.AppendLine( \u0022}\u0022 ).ToString();\n\t}\n\n\tpublic string ToTmat( Func\u003Cstring, string\u003E resolve )\n\t{\n\t\t// TerrainMaterial image fields are source image paths, not .vmat references.\n\t\tvar values = new Dictionary\u003Cstring, object\u003E\n\t\t{\n\t\t\t[\u0022__version\u0022] = 1,\n\t\t\t[\u0022AlbedoImage\u0022] = \u0022materials/default/default_color.tga\u0022,\n\t\t\t[\u0022NormalImage\u0022] = \u0022materials/default/default_normal.tga\u0022,\n\t\t\t[\u0022RoughnessImage\u0022] = \u0022materials/default/default_rough.tga\u0022,\n\t\t\t[\u0022AOImage\u0022] = \u0022materials/default/default_ao.tga\u0022,\n\t\t\t[\u0022HeightImage\u0022] = \u0022materials/default/default_height.tga\u0022\n\t\t};\n\t\tvoid Set( string key, params string[] names )\n\t\t{\n\t\t\tvar value = Texture( resolve, names );\n\t\t\tif ( value != null ) values[key] = value;\n\t\t}\n\t\tSet( \u0022AlbedoImage\u0022, \u0022_BaseMap\u0022, \u0022_Diffuse\u0022, \u0022_MainTex\u0022, \u0022_Albedo\u0022, \u0022_BaseColorMap\u0022 );\n\t\tSet( \u0022NormalImage\u0022, \u0022_BumpMap\u0022, \u0022_Normal\u0022, \u0022_NormalMap\u0022 );\n\t\tSet( \u0022AOImage\u0022, \u0022_OcclusionMap\u0022, \u0022_Occlusion\u0022 );\n\t\tSet( \u0022HeightImage\u0022, \u0022_ParallaxMap\u0022, \u0022_HeightMap\u0022 );\n\t\tforeach ( var (channel, field) in new[] { (\u0022rough\u0022, \u0022RoughnessImage\u0022), (\u0022ao\u0022, \u0022AOImage\u0022), (\u0022height\u0022, \u0022HeightImage\u0022) } )\n\t\t\tif ( channelImages.TryGetValue( channel, out var image ) ) values[field] = image;\n\t\treturn JsonSerializer.Serialize( values, new JsonSerializerOptions { WriteIndented = true } );\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/ImportAssetPreparation.cs","FileName":"ImportAssetPreparation.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace ImportUnityPackage;\r\n\r\npublic record ImportPreparationResult( string[] Warnings, bool Cancelled, int Rebuilt )\r\n{\r\n\tpublic string[] Errors { get; init; } = Array.Empty\u003Cstring\u003E();\r\n}\r\n\r\n/// \u003Csummary\u003ERuns on the editor context after the imported folder has been committed.\u003C/summary\u003E\r\npublic static class ImportAssetPreparation\r\n{\r\n\tstatic Task repairTask;\r\n\r\n\t/// \u003Csummary\u003ERepair already compiled resources in an existing import without reimporting source files.\u003C/summary\u003E\r\n\t[ConCmd( \u0022unity_import_repair_resources\u0022 )]\r\n\tpublic static void Repair( string directory )\r\n\t{\r\n\t\tif ( repairTask is { IsCompleted: false } ) return;\r\n\t\trepairTask = RepairAsync( directory );\r\n\t}\r\n\r\n\tstatic async Task RepairAsync( string directory )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar assets = Project.Current.GetAssetsPath();\r\n\t\t\tvar root = Path.GetFullPath( Path.Combine( assets, \u0022imported\u0022 ) ) \u002B Path.DirectorySeparatorChar;\r\n\t\t\tvar target = Path.GetFullPath( Path.IsPathRooted( directory ) ? directory : Path.Combine( assets, directory ) );\r\n\t\t\tif ( !(target.Equals( root.TrimEnd( Path.DirectorySeparatorChar ), StringComparison.OrdinalIgnoreCase ) || target.StartsWith( root, StringComparison.OrdinalIgnoreCase )) || !Directory.Exists( target ) )\r\n\t\t\t\tthrow new ArgumentException( \u0022Choose an existing package folder under this project\u0027s Assets/imported.\u0022 );\r\n\t\t\tvar stale = Directory.EnumerateFiles( target, \u0022*\u0022, SearchOption.AllDirectories )\r\n\t\t\t\t.Where( f =\u003E ResourceOrder( f ) \u003E 0 )\r\n\t\t\t\t.Where( f =\u003E AssetSystem.FindByPath( f ) is { IsCompiled: true } asset \u0026\u0026 !asset.IsCompiledAndUpToDate ).ToArray();\r\n\t\t\tLog.Info( $\u0022UNITY_RESOURCE_REPAIR: preparing {stale.Length} out-of-date resources in {target}\u0022 );\r\n\t\t\tvar result = await Run( stale, null, CancellationToken.None );\r\n\t\t\tforeach ( var warning in result.Warnings ) Log.Warning( warning );\r\n\t\t\tforeach ( var error in result.Errors ) Log.Error( error );\r\n\t\t\tLog.Info( $\u0022UNITY_RESOURCE_REPAIR complete: {stale.Length} checked, {result.Rebuilt} full rebuilds, {result.Warnings.Length} warnings, {result.Errors.Length} errors.\u0022 );\r\n\t\t}\r\n\t\tcatch ( Exception ex ) { Log.Error( $\u0022UNITY_RESOURCE_REPAIR failed: {ex}\u0022 ); }\r\n\t}\r\n\r\n\tpublic static async Task\u003CImportPreparationResult\u003E RunImport( ImportResult result, IProgress\u003CImportProgress\u003E progress, CancellationToken cancel )\r\n\t{\r\n\t\tvar files = result.Files.ToHashSet( StringComparer.OrdinalIgnoreCase );\r\n\t\tvar warnings = new List\u003Cstring\u003E();\r\n\t\tforeach ( var changed in result.ChangedFiles )\r\n\t\t{\r\n\t\t\tif ( cancel.IsCancellationRequested ) break;\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tvar asset = AssetSystem.RegisterFile( changed );\r\n\t\t\t\tif ( asset == null ) continue;\r\n\t\t\t\tforeach ( var dependant in asset.GetDependants( true ) )\r\n\t\t\t\t\tif ( ResourceOrder( dependant.Path ) \u003E 0 \u0026\u0026 dependant.HasSourceFile ) files.Add( dependant.GetSourceFile( true ) );\r\n\t\t\t}\r\n\t\t\tcatch ( Exception ex ) { warnings.Add( $\u0022{changed}: could not discover affected resources: {ex.Message}\u0022 ); }\r\n\t\t}\r\n\t\tvar prepared = await Run( files, progress, cancel );\r\n\t\treturn prepared with { Warnings = warnings.Concat( prepared.Warnings ).ToArray() };\r\n\t}\r\n\r\n\tpublic static async Task\u003CImportPreparationResult\u003E Run( IEnumerable\u003Cstring\u003E files, IProgress\u003CImportProgress\u003E progress, CancellationToken cancel )\r\n\t{\r\n\t\tvar warnings = new List\u003Cstring\u003E();\r\n\t\tvar errors = new List\u003Cstring\u003E();\r\n\t\tvar resources = new List\u003CAsset\u003E();\r\n\t\tvar rebuilt = 0;\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// Register all sources before compiling textures, then materials, then models.\r\n\t\t\tforeach ( var file in files.Distinct( StringComparer.OrdinalIgnoreCase ).OrderBy( ResourceOrder ) )\r\n\t\t\t{\r\n\t\t\t\tcancel.ThrowIfCancellationRequested();\r\n\t\t\t\tif ( Path.GetExtension( file ).ToLowerInvariant() is \u0022.mat\u0022 or \u0022.json\u0022 or \u0022.mtl\u0022 ) continue;\r\n\t\t\t\ttry\r\n\t\t\t\t{\r\n\t\t\t\t\tvar asset = AssetSystem.RegisterFile( file );\r\n\t\t\t\t\tif ( ResourceOrder( file ) \u003E 0 )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( asset == null ) throw new InvalidOperationException( \u0022Asset registration returned no resource\u0022 );\r\n\t\t\t\t\t\tresources.Add( asset );\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tcatch ( Exception ex ) { errors.Add( $\u0022{file}: registration failed: {ex.Message}\u0022 ); }\r\n\t\t\t}\r\n\t\t\tfor ( var i = 0; i \u003C resources.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tcancel.ThrowIfCancellationRequested();\r\n\t\t\t\tvar asset = resources[i];\r\n\t\t\t\tprogress?.Report( new( (double)i / resources.Count, $\u0022Preparing resource {i \u002B 1}/{resources.Count}: {asset.Name}\u0022 ) );\r\n\t\t\t\ttry\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( !asset.IsCompiledAndUpToDate ) await asset.CompileIfNeededAsync().AsTask().WaitAsync( cancel );\n\t\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\t\t// Compilation and publishing generated children to the registry are separate steps.\n\t\t\t\t\t// Let the editor process completion notifications before checking dependency validity.\n\t\t\t\t\tawait WaitForReady( asset, TimeSpan.FromMilliseconds( 250 ), cancel );\n\t\t\t\t\t// Incremental compilation can leave generated children absent from the asset registry,\r\n\t\t\t\t\t// even when their _c files exist. One full rebuild restores those dependencies.\r\n\t\t\t\t\tif ( !asset.IsCompileFailed \u0026\u0026 !asset.IsCompiledAndUpToDate )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar accepted = asset.Compile( true );\n\t\t\t\t\t\trebuilt\u002B\u002B;\n\t\t\t\t\t\tif ( accepted ) await WaitForReady( asset, TimeSpan.FromSeconds( 30 ), cancel );\n\t\t\t\t\t}\n\t\t\t\t\tif ( asset.IsCompileFailed || !asset.IsCompiledAndUpToDate )\n\t\t\t\t\t\terrors.Add( $\u0022{asset.Path}: \u0022 \u002B (asset.IsCompileFailed\n\t\t\t\t\t\t\t? \u0022the resource compiler reported a failure; see its diagnostics in the editor console.\u0022\n\t\t\t\t\t\t\t: !asset.IsCompiled ? \u0022no compiled resource became available after compilation.\u0022\n\t\t\t\t\t\t\t: \u0022compiled resource dependencies did not become ready within 30 seconds. Reimport or run unity_import_repair_resources for this folder.\u0022) );\n\t\t\t\t}\r\n\t\t\t\tcatch ( OperationCanceledException ) { throw; }\r\n\t\t\t\tcatch ( Exception ex ) { errors.Add( $\u0022{asset.Path}: resource preparation failed: {ex.Message}\u0022 ); }\r\n\t\t\t\tawait Task.Delay( 1, cancel );\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( OperationCanceledException ) when ( cancel.IsCancellationRequested )\r\n\t\t{\r\n\t\t\treturn new( warnings.ToArray(), true, rebuilt ) { Errors = errors.ToArray() };\r\n\t\t}\r\n\t\tprogress?.Report( new( 1, \u0022Import complete\u0022 ) );\r\n\t\treturn new( warnings.ToArray(), false, rebuilt ) { Errors = errors.ToArray() };\r\n\t}\r\n\r\n\tstatic async Task WaitForReady( Asset asset, TimeSpan timeout, CancellationToken cancel )\n\t{\n\t\tvar started = System.Diagnostics.Stopwatch.StartNew();\n\t\twhile ( !asset.IsCompiledAndUpToDate \u0026\u0026 started.Elapsed \u003C timeout )\n\t\t\tawait Task.Delay( 50, cancel );\n\t}\n\n\tstatic int ResourceOrder( string file ) =\u003E Path.GetExtension( file ).ToLowerInvariant() switch\n\t{\r\n\t\t\u0022.vtex\u0022 =\u003E 1,\r\n\t\t\u0022.vmat\u0022 or \u0022.tmat\u0022 =\u003E 2,\r\n\t\t\u0022.vmdl\u0022 =\u003E 3,\r\n\t\t_ =\u003E 0\r\n\t};\r\n}\r\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityFbxBindings.cs","FileName":"UnityFbxBindings.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.RegularExpressions;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003EFBX object hierarchy and ordered material-to-model connections; no vertex decoding needed.\u003C/summary\u003E\ninternal sealed class UnityFbxBindings\n{\n\tinternal readonly Dictionary\u003Clong, (string Name, bool Mesh)\u003E Nodes = new();\n\tinternal readonly Dictionary\u003Clong, string\u003E Materials = new();\n\tinternal readonly List\u003C(long Child, long Parent)\u003E Connections = new();\n\treadonly Dictionary\u003Cstring, long\u003E legacyIds = new( StringComparer.Ordinal );\n\tlong Id( string value )\n\t{\n\t\tif ( long.TryParse( value, out var id ) ) return id;\n\t\tif ( value == \u0022Model::Scene\u0022 ) return 0;\n\t\tif ( !legacyIds.TryGetValue( value, out id ) ) legacyIds[value] = id = -legacyIds.Count - 1;\n\t\treturn id;\n\t}\n\tinternal void ObserveAscii( string line )\n\t{\n\t\tvar item = Regex.Match( line, \u0022^\\\\s*(Model|Material):\\\\s*(?:([-0-9]\u002B),\\\\s*)?\\\u0022(?:Model|Material)::([^\\\u0022]\u002B)\\\u0022,\\\\s*\\\u0022([^\\\u0022]*)\\\u0022\u0022 );\n\t\tif ( item.Success )\n\t\t{\n\t\t\tvar kind = item.Groups[1].Value;\n\t\t\tvar id = Id( item.Groups[2].Success ? item.Groups[2].Value : kind \u002B \u0022::\u0022 \u002B item.Groups[3].Value );\n\t\t\tif ( kind == \u0022Material\u0022 ) Materials[id] = item.Groups[3].Value;\n\t\t\telse Nodes[id] = (item.Groups[3].Value, item.Groups[4].Value == \u0022Mesh\u0022);\n\t\t}\n\t\tvar link = Regex.Match( line, \u0022^\\\\s*(?:C|Connect):\\\\s*\\\u0022OO\\\u0022,\\\\s*(?:\\\u0022([^\\\u0022]\u002B)\\\u0022|([-0-9]\u002B)),\\\\s*(?:\\\u0022([^\\\u0022]\u002B)\\\u0022|([-0-9]\u002B))\u0022 );\n\t\tif ( link.Success ) Connections.Add( (Id( link.Groups[1].Success ? link.Groups[1].Value : link.Groups[2].Value ), Id( link.Groups[3].Success ? link.Groups[3].Value : link.Groups[4].Value )) );\n\t}\n\n\tinternal void Apply( UnityModel model, bool preserveHierarchy )\n\t{\n\t\tvar parents = Connections.Where( c =\u003E Nodes.ContainsKey( c.Child ) \u0026\u0026 (c.Parent == 0 || Nodes.ContainsKey( c.Parent )) )\n\t\t\t.GroupBy( c =\u003E c.Child ).Where( g =\u003E g.Select( c =\u003E c.Parent ).Distinct().Count() == 1 ).ToDictionary( g =\u003E g.Key, g =\u003E g.First().Parent );\n\t\tvar roots = Nodes.Keys.Where( id =\u003E !parents.TryGetValue( id, out var parent ) || parent == 0 ).ToArray();\n\t\tvar ambiguous = new HashSet\u003Clong\u003E();\n\t\tforeach ( var (id, node) in Nodes.Where( p =\u003E p.Value.Mesh ) )\n\t\t{\n\t\t\tvar names = new List\u003Cstring\u003E();\n\t\t\tvar seen = new HashSet\u003Clong\u003E();\n\t\t\tvar current = id;\n\t\t\twhile ( Nodes.TryGetValue( current, out var ancestor ) \u0026\u0026 seen.Add( current ) )\n\t\t\t{\n\t\t\t\tnames.Add( !preserveHierarchy \u0026\u0026 roots.Length == 1 \u0026\u0026 roots[0] == current ? \u0022root\u0022 : ancestor.Name );\n\t\t\t\tcurrent = parents.GetValueOrDefault( current );\n\t\t\t}\n\t\t\tif ( current != 0 ) continue; // Cyclic/malformed hierarchy cannot identify a renderer.\n\t\t\tnames.Reverse();\n\t\t\tif ( preserveHierarchy || roots.Length != 1 ) names.Insert( 0, \u0022root\u0022 );\n\t\t\tvar path = \u0022//RootNode/\u0022 \u002B string.Join( \u0022/\u0022, names );\n\t\t\tvar slots = Connections.Where( c =\u003E c.Parent == id \u0026\u0026 Materials.ContainsKey( c.Child ) ).Select( c =\u003E Materials[c.Child] ).ToArray();\n\t\t\tvar renderer = UnityFileId.Renderer( path );\n\t\t\tif ( !model.RendererSlots.TryAdd( renderer, new( node.Name, slots ) ) ) ambiguous.Add( renderer );\n\t\t}\n\t\tforeach ( var id in ambiguous ) model.RendererSlots.Remove( id );\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityPrefabBindings.cs","FileName":"UnityPrefabBindings.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.RegularExpressions;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003EResolves renderer/slot assignments from prefab data, independently of publisher or material filenames.\u003C/summary\u003E\ninternal static class UnityPrefabBindings\n{\n\tsealed class Candidate( string path )\n\t{\n\t\tinternal readonly string Path = path;\n\t\tinternal readonly Dictionary\u003Cstring, HashSet\u003Cstring\u003E\u003E Slots = new( StringComparer.OrdinalIgnoreCase );\n\t\tinternal readonly HashSet\u003Cstring\u003E Materials = new( StringComparer.OrdinalIgnoreCase );\n\t\tinternal readonly List\u003Cstring\u003E Warnings = new();\n\t\tinternal bool HasOverrides;\n\t}\n\tstatic string GuidOf( string reference ) =\u003E Regex.Match( reference, @\u0022\\bguid:\\s*([a-fA-F0-9]{32})\u0022 ).Groups[1].Value;\n\tstatic long IdOf( string reference ) =\u003E long.TryParse( Regex.Match( reference, @\u0022\\bfileID:\\s*(-?[0-9]\u002B)\u0022 ).Groups[1].Value, out var id ) ? id : 0;\n\tstatic string Field( string text, string name ) =\u003E Regex.Match( text, @\u0022(?m)^\\s*\u0022 \u002B Regex.Escape( name ) \u002B @\u0022:\\s*\\{([^}\\r\\n]*)\\}\u0022 ).Groups[1].Value;\n\n\tinternal static void Apply( UnityArchive archive )\n\t{\n\t\tvar models = archive.Assets.Where( a =\u003E a.Kind == UnityAssetKind.Model ).ToDictionary( a =\u003E a.Guid, StringComparer.OrdinalIgnoreCase );\n\t\tvar materialIds = archive.Assets.Where( a =\u003E a.Kind == UnityAssetKind.Material ).Select( a =\u003E a.Guid ).ToHashSet( StringComparer.OrdinalIgnoreCase );\n\t\tvar candidates = new Dictionary\u003Cstring, List\u003CCandidate\u003E\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar visible = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar colliders = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tforeach ( var prefab in archive.Assets.Where( a =\u003E a.Path.EndsWith( \u0022.prefab\u0022, StringComparison.OrdinalIgnoreCase ) ) )\n\t\t{\n\t\t\tif ( new FileInfo( prefab.Source ).Length \u003E 16 * 1024 * 1024 ) continue;\n\t\t\tvar text = File.ReadAllText( prefab.Source );\n\t\t\tvar local = new Dictionary\u003Cstring, Candidate\u003E( StringComparer.OrdinalIgnoreCase );\n\t\t\tCandidate For( string guid )\n\t\t\t{\n\t\t\t\tif ( !local.TryGetValue( guid, out var candidate ) ) local[guid] = candidate = new( prefab.Path );\n\t\t\t\treturn candidate;\n\t\t\t}\n\t\t\tvoid Assign( string guid, UnityModel.RendererBinding binding, int index, string material )\n\t\t\t{\n\t\t\t\tvar candidate = For( guid ); candidate.HasOverrides = true;\n\t\t\t\tvar model = models[guid].ModelInfo;\n\t\t\t\tif ( binding == null ) { candidate.Warnings.Add( \u0022A prefab material override targets an unsupported or unidentified renderer; no slot was guessed.\u0022 ); return; }\n\t\t\t\t// ModelDoc currently imports only LOD0 when present. Other LOD overrides must not change its slots.\n\t\t\t\tif ( model.HighestDetailMeshes.Length \u003E 0 \u0026\u0026 !model.HighestDetailMeshes.Contains( binding.Mesh ) ) return;\n\t\t\t\tif ( index \u003C 0 || index \u003E= binding.Slots.Length ) { candidate.Warnings.Add( \u0022A prefab material override references a missing FBX material slot.\u0022 ); return; }\n\t\t\t\tvar slot = binding.Slots[index];\n\t\t\t\tif ( !candidate.Slots.TryGetValue( slot, out var values ) ) candidate.Slots[slot] = values = new( StringComparer.OrdinalIgnoreCase );\n\t\t\t\tvalues.Add( material );\n\t\t\t\tif ( materialIds.Contains( material ) ) candidate.Materials.Add( material );\n\t\t\t}\n\t\t\tvar blocks = Regex.Matches( text, @\u0022(?ms)^--- !u!(?\u003Ctype\u003E[0-9]\u002B) \u0026[^\\r\\n]\u002B\\r?\\n(?\u003Cbody\u003E.*?)(?=^--- !u!|\\z)\u0022 ).Cast\u003CMatch\u003E()\n\t\t\t\t.Select( m =\u003E (Type: m.Groups[\u0022type\u0022].Value, Body: m.Groups[\u0022body\u0022].Value) ).ToArray();\n\t\t\tvar filters = new Dictionary\u003Clong, List\u003Cstring\u003E\u003E();\n\t\t\tforeach ( var block in blocks )\n\t\t\t{\n\t\t\t\tvar mesh = Field( block.Body, \u0022m_Mesh\u0022 );\n\t\t\t\tvar guid = GuidOf( mesh );\n\t\t\t\tif ( block.Type == \u002264\u0022 \u0026\u0026 models.ContainsKey( guid ) ) colliders.Add( guid );\n\t\t\t\tif ( block.Type is \u002233\u0022 or \u0022137\u0022 \u0026\u0026 models.ContainsKey( guid ) )\n\t\t\t\t{\n\t\t\t\t\tvisible.Add( guid );\n\t\t\t\t\tif ( block.Type == \u002233\u0022 )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar gameObject = IdOf( Field( block.Body, \u0022m_GameObject\u0022 ) );\n\t\t\t\t\t\tif ( gameObject == 0 ) continue;\n\t\t\t\t\t\tif ( !filters.TryGetValue( gameObject, out var list ) ) filters[gameObject] = list = new();\n\t\t\t\t\t\tlist.Add( mesh );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif ( block.Type == \u00221001\u0022 )\n\t\t\t\t{\n\t\t\t\t\tvar source = GuidOf( Field( block.Body, \u0022m_SourcePrefab\u0022 ) );\n\t\t\t\t\tif ( models.ContainsKey( source ) ) visible.Add( source );\n\t\t\t\t}\n\t\t\t}\n\t\t\tforeach ( Match modification in Regex.Matches( text, @\u0022(?ms)^\\s*- target:\\s*\\{(?\u003Ctarget\u003E[^}]\u002B)\\}\\s*\\r?\\n(?\u003Cbody\u003E.*?)(?=^\\s*- target:|^--- !u!|\\z)\u0022 ) )\n\t\t\t{\n\t\t\t\tvar target = modification.Groups[\u0022target\u0022].Value;\n\t\t\t\tvar guid = GuidOf( target );\n\t\t\t\tvar property = Regex.Match( modification.Groups[\u0022body\u0022].Value, @\u0022propertyPath:\\s*m_Materials\\.Array\\.data\\[([0-9]\u002B)\\]\u0022 );\n\t\t\t\tif ( !property.Success || !models.TryGetValue( guid, out var asset ) ) continue;\n\t\t\t\tvisible.Add( guid );\n\t\t\t\tif ( !int.TryParse( property.Groups[1].Value, out var index ) ) continue;\n\t\t\t\tAssign( guid, asset.ModelInfo.RendererSlots.GetValueOrDefault( IdOf( target ) ), index,\n\t\t\t\t\tGuidOf( Field( modification.Groups[\u0022body\u0022].Value, \u0022objectReference\u0022 ) ) );\n\t\t\t}\n\t\t\t// Explicit MeshRenderer \u002B MeshFilter (or SkinnedMeshRenderer) blocks in non-variant prefabs.\n\t\t\tforeach ( var block in blocks.Where( b =\u003E b.Type is \u002223\u0022 or \u0022137\u0022 ) )\n\t\t\t{\n\t\t\t\tvar refs = block.Type == \u0022137\u0022 ? new[] { Field( block.Body, \u0022m_Mesh\u0022 ) } :\n\t\t\t\t\tfilters.GetValueOrDefault( IdOf( Field( block.Body, \u0022m_GameObject\u0022 ) ) )?.ToArray() ?? Array.Empty\u003Cstring\u003E();\n\t\t\t\tif ( refs.Length != 1 || !models.TryGetValue( GuidOf( refs[0] ), out var asset ) ) continue;\n\t\t\t\tvar slots = Regex.Match( block.Body, @\u0022(?m)^\\s*m_Materials:[ \\t]*\\r?\\n(?\u003Citems\u003E(?:[ \\t]*- \\{[^}\\r\\n]*\\}[ \\t]*\\r?\\n)\u002B)\u0022 );\n\t\t\t\tif ( !slots.Success ) continue;\n\t\t\t\tvar meshId = IdOf( refs[0] );\n\t\t\t\tvar bindings = asset.ModelInfo.RendererSlots.Values.Where( b =\u003E UnityFileId.Hash( \u0022Type:Mesh-\u003E\u0022 \u002B b.Mesh \u002B \u00220\u0022 ) == meshId ).ToArray();\n\t\t\t\tvar binding = bindings.Length == 1 ? bindings[0] : null;\n\t\t\t\tvar index = 0;\n\t\t\t\tforeach ( Match reference in Regex.Matches( slots.Groups[\u0022items\u0022].Value, @\u0022\\{([^}]\u002B)\\}\u0022 ) ) Assign( asset.Guid, binding, index\u002B\u002B, GuidOf( reference.Value ) );\n\t\t\t}\n\t\t\t// Keep the prior single-model fallback for legacy prefabs without renderer-slot evidence.\n\t\t\tvar references = UnityMaterial.References( text ).Distinct( StringComparer.OrdinalIgnoreCase ).ToArray();\n\t\t\tvar modelIds = references.Where( g =\u003E models.ContainsKey( g ) \u0026\u0026 (!colliders.Contains( g ) || visible.Contains( g )) ).ToArray();\n\t\t\tif ( modelIds.Length == 1 \u0026\u0026 !For( modelIds[0] ).HasOverrides )\n\t\t\t\tforeach ( var material in references.Where( materialIds.Contains ) ) For( modelIds[0] ).Materials.Add( material );\n\t\t\tforeach ( var (guid, candidate) in local )\n\t\t\t{\n\t\t\t\tif ( !candidates.TryGetValue( guid, out var list ) ) candidates[guid] = list = new();\n\t\t\t\tlist.Add( candidate );\n\t\t\t}\n\t\t}\n\t\tforeach ( var (guid, list) in candidates )\n\t\t{\n\t\t\tvar asset = models[guid]; var model = asset.ModelInfo;\n\t\t\tvar named = list.Where( p =\u003E Path.GetFileNameWithoutExtension( p.Path ).Equals( Path.GetFileNameWithoutExtension( asset.Path ), StringComparison.OrdinalIgnoreCase ) ).ToArray();\n\t\t\tvar chosen = named.Length \u003E 0 ? named : list.ToArray();\n\t\t\tforeach ( var slot in chosen.SelectMany( c =\u003E c.Slots.Keys ).Distinct( StringComparer.OrdinalIgnoreCase ) )\n\t\t\t{\n\t\t\t\tvar values = chosen.SelectMany( c =\u003E c.Slots.GetValueOrDefault( slot ) ?? new HashSet\u003Cstring\u003E() ).Distinct( StringComparer.OrdinalIgnoreCase ).ToArray();\n\t\t\t\tif ( values.Length == 1 \u0026\u0026 materialIds.Contains( values[0] ) ) model.PrefabSlotMaterials[slot] = values[0];\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tmodel.UnresolvedPrefabSlots.Add( slot );\n\t\t\t\t\tmodel.AssignmentWarnings.Add( $\u0022Prefab assignment for slot \u0027{slot}\u0027 is missing or conflicting; no material was guessed.\u0022 );\n\t\t\t\t}\n\t\t\t}\n\t\t\tmodel.PrefabMaterials.AddRange( chosen.SelectMany( c =\u003E c.Materials ).Distinct( StringComparer.OrdinalIgnoreCase ) );\n\t\t\tmodel.AssignmentWarnings.AddRange( chosen.SelectMany( c =\u003E c.Warnings ).Distinct() );\n\t\t}\n\t\tforeach ( var guid in colliders.Where( g =\u003E !visible.Contains( g ) ) ) models[guid].ModelInfo.CollisionOnly = true;\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/UnityPackageMenu.cs","FileName":"UnityPackageMenu.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using Editor;\n\nnamespace ImportUnityPackage;\n\npublic static class UnityPackageMenu\n{\n\t[Menu( \u0022Editor\u0022, \u0022Import Unity Package/Import .unitypackage...\u0022 )]\n\tpublic static void Open()\n\t{\n\t\tvar window = new UnityPackageWindow();\n\t\twindow.Show();\n\t\twindow.Browse();\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityArchive.cs","FileName":"UnityArchive.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Formats.Tar;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading;\n\nnamespace ImportUnityPackage;\n\npublic enum UnityAssetKind { Unsupported, Material, Texture, Model, ModelSupport }\n\npublic sealed class UnityAsset\n{\n\tpublic string Guid { get; init; }\n\tpublic string Path { get; init; }\n\tpublic string Source { get; init; }\n\tpublic string Metadata { get; init; }\n\tpublic UnityAssetKind Kind { get; init; }\n\tpublic bool Selected { get; set; }\n\tUnityModel modelInfo;\n\tpublic UnityModel ModelInfo =\u003E modelInfo ??= UnityModel.Read( this );\n}\n\npublic record ImportProgress( double Fraction, string Message, string Stage = null );\n\n/// \u003Csummary\u003EReads Unity\u0027s GUID/asset, GUID/pathname, GUID/asset.meta tar layout.\u003C/summary\u003E\npublic sealed class UnityArchive : IDisposable\n{\n\tconst long MaxEntryBytes = 2L * 1024 * 1024 * 1024;\n\tconst long MaxTotalBytes = 32L * 1024 * 1024 * 1024;\n\tImportWorkspace workspace;\n\tstring scratch =\u003E workspace?.DirectoryPath;\n\tpublic bool HasExtractedFiles { get; private set; }\n\tpublic string[] CleanupWarnings =\u003E workspace?.Warnings.ToArray() ?? Array.Empty\u003Cstring\u003E();\n\tpublic string FileName { get; private set; }\n\tpublic List\u003CUnityAsset\u003E Assets { get; } = new();\n\tpublic List\u003Cstring\u003E Warnings { get; } = new();\n\n\tpublic static UnityArchive Read( string file, IProgress\u003CImportProgress\u003E progress, CancellationToken cancel, string assetsDirectory )\n\t{\n\t\tArgumentException.ThrowIfNullOrWhiteSpace( assetsDirectory );\n\t\tif ( !System.IO.Path.IsPathFullyQualified( assetsDirectory ) ) throw new ArgumentException( \u0022An absolute project Assets path is required.\u0022, nameof( assetsDirectory ) );\n\t\tvar package = new UnityArchive { FileName = file };\n\t\ttry\n\t\t{\n\t\t\tpackage.workspace = new ImportWorkspace( assetsDirectory );\n\t\t\tpackage.HasExtractedFiles = true;\n\t\t\tusing var input = File.OpenRead( file );\n\t\t\tusing var gzip = new GZipStream( input, CompressionMode.Decompress );\n\t\t\tusing var tar = new TarReader( gzip );\n\t\t\tvar entries = new Dictionary\u003Cstring, Dictionary\u003Cstring, string\u003E\u003E( StringComparer.OrdinalIgnoreCase );\n\t\t\tlong total = 0;\n\t\t\tint count = 0;\n\t\t\tTarEntry entry;\n\t\t\twhile ( (entry = tar.GetNextEntry( false )) != null )\n\t\t\t{\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\tif ( \u002B\u002Bcount \u003E 200000 ) throw new InvalidDataException( \u0022Package has too many archive entries.\u0022 );\n\t\t\t\tif ( entry.EntryType == TarEntryType.Directory ) continue;\n\t\t\t\tif ( entry.EntryType != TarEntryType.RegularFile \u0026\u0026 entry.EntryType != TarEntryType.V7RegularFile )\n\t\t\t\t\tthrow new InvalidDataException( \u0022Package contains unsupported links or special archive entries.\u0022 );\n\t\t\t\tif ( entry.Length \u003E MaxEntryBytes || (total \u002B= entry.Length) \u003E MaxTotalBytes )\n\t\t\t\t\tthrow new InvalidDataException( \u0022Package exceeds the 2 GiB per file / 32 GiB extracted size limit.\u0022 );\n\t\t\t\tvar name = entry.Name.Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\t\t\t\tif ( name.StartsWith( \u0022./\u0022, StringComparison.Ordinal ) ) name = name[2..];\n\t\t\t\t// Asset Store downloads include a package thumbnail outside the GUID records.\n\t\t\t\tif ( name == \u0022.icon.png\u0022 ) continue;\n\t\t\t\tvar parts = name.Split( \u0027/\u0027 );\n\t\t\t\tif ( parts.Length == 2 \u0026\u0026 parts[0] == \u0022packagemanagermanifest\u0022 \u0026\u0026 parts[1] is \u0022asset\u0022 or \u0022pathname\u0022 or \u0022asset.meta\u0022 ) continue;\n\t\t\t\tif ( parts.Length != 2 || !Regex.IsMatch( parts[0], \u0022^[0-9a-fA-F]{32}$\u0022 ) )\n\t\t\t\t\tthrow new InvalidDataException( $\u0022Invalid Unity archive entry: {entry.Name}\u0022 );\n\t\t\t\tif ( parts[1] is not (\u0022asset\u0022 or \u0022asset.meta\u0022 or \u0022pathname\u0022 or \u0022preview.png\u0022) ) continue;\n\t\t\t\tif ( parts[1] == \u0022preview.png\u0022 ) continue;\n\t\t\t\tif ( parts[1] != \u0022asset\u0022 \u0026\u0026 entry.Length \u003E 16 * 1024 * 1024 )\n\t\t\t\t\tthrow new InvalidDataException( \u0022Package metadata is too large.\u0022 );\n\t\t\t\tif ( !entries.TryGetValue( parts[0], out var record ) ) entries[parts[0]] = record = new();\n\t\t\t\tif ( record.ContainsKey( parts[1] ) ) throw new InvalidDataException( $\u0022Duplicate archive entry: {name}\u0022 );\n\t\t\t\tvar target = System.IO.Path.Combine( package.scratch, parts[0] \u002B \u0022-\u0022 \u002B parts[1] \u002B \u0022.iup\u0022 );\n\t\t\t\tusing ( var output = new FileStream( target, FileMode.CreateNew ) )\n\t\t\t\t\tCopy( entry.DataStream, output, cancel );\n\t\t\t\trecord[parts[1]] = target;\n\t\t\t\tprogress?.Report( new( (double)input.Position / Math.Max( 1, input.Length ), $\u0022Reading package ({entries.Count:N0} entries)\u2026\u0022 ) );\n\t\t\t}\n\t\t\tvar paths = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase );\n\t\t\tforeach ( var (guid, record) in entries )\n\t\t\t{\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\tif ( !record.TryGetValue( \u0022asset\u0022, out var source ) ) continue; // Unity folder metadata.\n\t\t\t\tif ( !record.TryGetValue( \u0022pathname\u0022, out var pathname ) ) throw new InvalidDataException( $\u0022Asset {guid} has no pathname.\u0022 );\n\t\t\t\t// Unity writes the pathname on the first line, sometimes followed by a \u002200\u0022 trailer.\n\t\t\t\tvar path = SafePath( (File.ReadLines( pathname, Encoding.UTF8 ).FirstOrDefault() ?? \u0022\u0022).TrimEnd( \u0027\\0\u0027 ) );\n\t\t\t\tif ( !paths.Add( path ) ) throw new InvalidDataException( $\u0022Duplicate asset path: {path}\u0022 );\n\t\t\t\tvar kind = Classify( path );\n\t\t\t\tpackage.Assets.Add( new UnityAsset { Guid = guid, Path = path, Source = source,\n\t\t\t\t\tMetadata = record.GetValueOrDefault( \u0022asset.meta\u0022 ), Kind = kind,\n\t\t\t\t\tSelected = kind != UnityAssetKind.Unsupported } );\n\t\t\t}\n\t\t\tpackage.Assets.Sort( (a, b) =\u003E StringComparer.OrdinalIgnoreCase.Compare( a.Path, b.Path ) );\n\t\t\tUnityModel.AssignPrefabMaterials( package );\n\t\t\tif ( package.Assets.Count == 0 ) throw new InvalidDataException( \u0022This package contains no file assets.\u0022 );\n\t\t\treturn package;\n\t\t}\n\t\tcatch ( Exception ex )\n\t\t{\n\t\t\tpackage.Dispose();\n\t\t\tif ( package.CleanupWarnings.Length \u003E 0 ) ex.Data[\u0022StagingCleanupError\u0022] = string.Join( \u0022\\n\u0022, package.CleanupWarnings );\n\t\t\tthrow;\n\t\t}\n\t}\n\n\tpublic static string SafePath( string path )\n\t{\n\t\tpath = path.Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\t\tif ( !path.StartsWith( \u0022Assets/\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\tthrow new InvalidDataException( $\u0022Asset path must start with Assets/: {path}\u0022 );\n\t\tpath = path[7..];\n\t\tforeach ( var part in path.Split( \u0027/\u0027 ) )\n\t\t{\n\t\t\tvar stem = part.Split( \u0027.\u0027 )[0];\n\t\t\tif ( string.IsNullOrWhiteSpace( part ) || part is \u0022.\u0022 or \u0022..\u0022 || part.EndsWith( \u0027.\u0027 ) || part.EndsWith( \u0027 \u0027 ) ||\n\t\t\t\tpart.Any( c =\u003E c \u003C 32 || \u0022\u003C\u003E:\\\u0022|?*\u0022.Contains( c ) ) ||\n\t\t\t\tRegex.IsMatch( stem, \u0022^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$\u0022, RegexOptions.IgnoreCase ) )\n\t\t\t\tthrow new InvalidDataException( $\u0022Unsafe asset path: {path}\u0022 );\n\t\t}\n\t\treturn path;\n\t}\n\n\tpublic static UnityAssetKind Classify( string path ) =\u003E System.IO.Path.GetExtension( path ).ToLowerInvariant() switch\n\t{\n\t\t\u0022.mat\u0022 or \u0022.terrainlayer\u0022 =\u003E UnityAssetKind.Material,\n\t\t\u0022.png\u0022 or \u0022.jpg\u0022 or \u0022.jpeg\u0022 or \u0022.tga\u0022 or \u0022.tif\u0022 or \u0022.tiff\u0022 or \u0022.exr\u0022 or \u0022.psd\u0022 =\u003E UnityAssetKind.Texture,\n\t\t\u0022.fbx\u0022 or \u0022.obj\u0022 or \u0022.smd\u0022 or \u0022.dmx\u0022 or \u0022.vox\u0022 =\u003E UnityAssetKind.Model,\n\t\t\u0022.mtl\u0022 =\u003E UnityAssetKind.ModelSupport,\n\t\t_ =\u003E UnityAssetKind.Unsupported\n\t};\n\n\tinternal static void Copy( Stream source, Stream target, CancellationToken cancel )\n\t{\n\t\tif ( source == null ) return;\n\t\tvar buffer = new byte[128 * 1024];\n\t\tint read;\n\t\twhile ( (read = source.Read( buffer, 0, buffer.Length )) \u003E 0 )\n\t\t{\n\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\ttarget.Write( buffer, 0, read );\n\t\t}\n\t}\n\n\tpublic void Dispose()\n\t{\n\t\tworkspace?.Dispose();\n\t\tHasExtractedFiles = false;\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/ImportMerge.cs","FileName":"ImportMerge.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Security.Cryptography;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing System.Threading;\n\nnamespace ImportUnityPackage;\n\npublic sealed class ImportFileChoice\n{\n\tpublic string Path { get; init; }\n\tpublic string Status { get; init; }\n\tpublic string Detail { get; init; }\n\tpublic bool Conflict { get; init; }\n\tpublic bool Replace { get; set; }\n\tinternal string ExistingHash { get; init; }\n\tpublic string Action =\u003E ExistingHash == null ? \u0022Add\u0022 : !Conflict ? \u0022Reuse\u0022 : Replace ? \u0022Replace\u0022 : \u0022Keep existing\u0022;\n}\n\n/// \u003Csummary\u003EOne-off content comparison. No history, backups, automatic reports or rollback.\u003C/summary\u003E\npublic sealed class ImportMergePlan : IDisposable\n{\n\treadonly ImportPlan original;\n\treadonly ImportWorkspace workspace;\n\tImportResult prepared;\n\tbool committed;\n\tpublic string Destination { get; }\n\tpublic List\u003CImportFileChoice\u003E Outputs { get; } = new();\n\tpublic string[] CleanupWarnings =\u003E workspace.Warnings.ToArray();\n\n\tImportMergePlan( ImportPlan plan, string assets )\n\t{\n\t\toriginal = plan;\n\t\tDestination = Path.GetFullPath( Path.Combine( assets, \u0022Imported\u0022 ) );\n\t\tUnityImport.CheckDirectory( Destination );\n\t\tforeach ( var item in plan.Assets )\n\t\t\tif ( item.Asset.Path.Split( \u0027/\u0027 )[0].Equals( \u0022.iup-temp\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\tthrow new InvalidDataException( $\u0022Reserved importer output path: {item.Asset.Path}\u0022 );\n\t\tworkspace = new ImportWorkspace( assets );\n\t}\n\n\tpublic static ImportMergePlan Create( ImportPlan plan, string assets, CancellationToken cancel = default )\n\t{\n\t\tcancel.ThrowIfCancellationRequested();\n\t\treturn new( plan, assets );\n\t}\n\n\tpublic void Prepare( IProgress\u003CImportProgress\u003E progress, CancellationToken cancel, ExtractTextureChannel extract = null, ExtractTextureChannels extractChannels = null )\n\t{\n\t\tif ( prepared != null ) throw new InvalidOperationException( \u0022This import is already prepared.\u0022 );\n\t\tprepared = UnityImport.PrepareFiles( original, Path.Combine( workspace.DirectoryPath, \u0022output\u0022 ), Destination, progress, cancel, extract, extractChannels );\n\t\tprogress?.Report( new( 1, \u0022Comparing destination files\u2026\u0022, \u0022Checking conflicts\u0022 ) );\n\t\tforeach ( var file in prepared.Files )\n\t\t{\n\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\tvar path = Path.GetRelativePath( prepared.Directory, file ).Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\t\t\tif ( !path.EndsWith( \u0022.iup\u0022, StringComparison.Ordinal ) ) throw new InvalidDataException( \u0022Invalid staging file\u0022 );\n\t\t\tpath = path[..^4];\n\t\t\tvar existing = HashFile( Target( path ), cancel );\n\t\t\tvar incoming = HashFile( file, cancel );\n\t\t\tvar conflict = existing != null \u0026\u0026 existing != incoming;\n\t\t\tOutputs.Add( new() { Path = path, ExistingHash = existing, Conflict = conflict,\n\t\t\t\tStatus = existing == null ? \u0022New\u0022 : conflict ? \u0022Different\u0022 : \u0022Identical\u0022,\n\t\t\t\tDetail = conflict ? \u0022Keeping this file retains its current content; it may differ from the rest of the incoming package.\u0022 : \u0022\u0022 } );\n\t\t}\n\t}\n\n\tpublic ImportResult Commit( IProgress\u003CImportProgress\u003E progress, CancellationToken cancel )\n\t{\n\t\tif ( prepared == null || committed ) throw new InvalidOperationException( \u0022Prepare a new import before writing.\u0022 );\n\t\tcommitted = true;\n\t\t// Recheck all reviewed files before the first write. Individual writes check again below.\n\t\tforeach ( var choice in Outputs )\n\t\t\tif ( HashFile( Target( choice.Path ), cancel ) != choice.ExistingHash ) throw new IOException( $\u0022Destination changed since review: {choice.Path}. Review the import again.\u0022 );\n\t\tvar writes = Outputs.Where( c =\u003E c.ExistingHash == null || c.Conflict \u0026\u0026 c.Replace ).ToArray();\n\t\tvar createdDirectories = new List\u003Cstring\u003E();\n\t\ttry\n\t\t{\n\t\t\tfor ( var i = 0; i \u003C writes.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\tvar choice = writes[i];\n\t\t\t\tprogress?.Report( new( (double)i / Math.Max( 1, writes.Length ), $\u0022Writing {choice.Path}\u0022 ) );\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\tvar target = Target( choice.Path );\n\t\t\t\tCreateOutputDirectories( Path.GetDirectoryName( target ), createdDirectories );\n\t\t\t\t// Staging is on the same filesystem. Rename a complete file; never leave a partial copy.\n\t\t\t\tImportStorage.Retry( () =\u003E\n\t\t\t\t{\n\t\t\t\t\tif ( HashFile( target, cancel ) != choice.ExistingHash ) throw new IOException( $\u0022Destination changed while writing: {choice.Path}\u0022 );\n\t\t\t\t\tFile.Move( Path.Combine( prepared.Directory, choice.Path \u002B \u0022.iup\u0022 ), target, choice.ExistingHash != null );\n\t\t\t\t}, cancel );\n\t\t\t}\n\t\t}\n\t\tfinally { RemoveEmptyOutputDirectories( createdDirectories ); }\n\t\tvar report = JsonNode.Parse( prepared.ReportJson );\n\t\treport[\u0022Destination\u0022] = Destination;\n\t\treport[\u0022Files\u0022] = JsonSerializer.SerializeToNode( Outputs.Select( c =\u003E c.Path ).ToArray() );\n\t\treport[\u0022Conflicts\u0022] = JsonSerializer.SerializeToNode( Outputs.Where( c =\u003E c.Conflict ).Select( c =\u003E new { c.Path, c.Action } ).ToArray() );\n\t\tprogress?.Report( new( 1, \u0022Files imported\u0022 ) );\n\t\treturn prepared with { Directory = Destination, Files = Outputs.Select( c =\u003E Target( c.Path ) ).ToArray(),\n\t\t\tReportJson = report.ToJsonString( new JsonSerializerOptions { WriteIndented = true } ), ChangedFiles = writes.Select( c =\u003E Target( c.Path ) ).ToArray() };\n\t}\n\n\tvoid CreateOutputDirectories( string directory, List\u003Cstring\u003E created )\n\t{\n\t\tvar missing = new Stack\u003Cstring\u003E();\n\t\twhile ( !Directory.Exists( directory ) \u0026\u0026 directory.StartsWith( Destination \u002B Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase ) )\n\t\t{\n\t\t\tmissing.Push( directory );\n\t\t\tdirectory = Path.GetDirectoryName( directory );\n\t\t}\n\t\tforeach ( var path in missing )\n\t\t{\n\t\t\tUnityImport.CheckDirectory( path );\n\t\t\tif ( Directory.Exists( path ) ) continue;\n\t\t\tDirectory.CreateDirectory( path );\n\t\t\tcreated.Add( path );\n\t\t}\n\t}\n\n\tvoid RemoveEmptyOutputDirectories( List\u003Cstring\u003E created )\n\t{\n\t\t// Only this run\u0027s new directories, deepest first. Never delete recursively or undo completed writes.\n\t\tforeach ( var path in created.AsEnumerable().Reverse() )\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tUnityImport.CheckDirectory( path );\n\t\t\t\tif ( Directory.Exists( path ) \u0026\u0026 !Directory.EnumerateFileSystemEntries( path ).Any() )\n\t\t\t\t\tImportStorage.Retry( () =\u003E Directory.Delete( path, false ), CancellationToken.None );\n\t\t\t}\n\t\t\tcatch ( Exception ex ) when ( ex is IOException or UnauthorizedAccessException )\n\t\t\t{\n\t\t\t\tworkspace.Warnings.Add( $\u0022Empty output folder could not be removed: {path}. {ex.Message}\u0022 );\n\t\t\t}\n\t\t}\n\t}\n\n\tstring Target( string path )\n\t{\n\t\tvar safe = UnityArchive.SafePath( \u0022Assets/\u0022 \u002B path );\n\t\tvar target = Path.GetFullPath( Path.Combine( Destination, safe ) );\n\t\tUnityImport.CheckDirectory( target );\n\t\tif ( Directory.Exists( target ) ) throw new IOException( $\u0022A directory occupies the file destination: {path}\u0022 );\n\t\treturn target;\n\t}\n\n\tpublic static string HashFile( string path, CancellationToken cancel = default )\n\t{\n\t\tif ( !File.Exists( path ) ) return null;\n\t\tusing var input = File.OpenRead( path );\n\t\tusing var hash = IncrementalHash.CreateHash( HashAlgorithmName.SHA256 );\n\t\tvar buffer = new byte[128 * 1024];\n\t\tint count;\n\t\twhile ( (count = input.Read( buffer, 0, buffer.Length )) \u003E 0 ) { cancel.ThrowIfCancellationRequested(); hash.AppendData( buffer, 0, count ); }\n\t\treturn Convert.ToHexString( hash.GetHashAndReset() ).ToLowerInvariant();\n\t}\n\n\tpublic void Dispose() =\u003E workspace.Dispose();\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/ImportPlan.cs","FileName":"ImportPlan.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":381664,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Threading;\r\n\r\nnamespace ImportUnityPackage;\r\n\r\npublic sealed record ImportDependency( UnityAsset Asset, string Reason );\r\npublic sealed record PlannedAsset( UnityAsset Asset, bool Explicit, ImportDependency[] RequiredBy, bool Vmat, bool Tmat, bool Vmdl )\r\n{\r\n\tpublic string OutputLabel =\u003E Asset.Kind switch\r\n\t{\r\n\t\tUnityAssetKind.Model =\u003E Asset.ModelInfo.CollisionOnly ? \u0022Collision source (FBX)\u0022 : Vmdl ? \u0022Model \u2192 VMDL\u0022 : \u0022Model source\u0022,\n\t\tUnityAssetKind.Material =\u003E Vmat \u0026\u0026 Tmat ? \u0022Material \u2192 VMAT \u002B TMAT\u0022 : Vmat ? \u0022Material \u2192 VMAT\u0022 : Tmat ? \u0022Terrain material \u2192 TMAT\u0022 : \u0022Material source\u0022,\r\n\t\tUnityAssetKind.Texture =\u003E UnityImport.NeedsTextureResource( Asset.Path ) ? \u0022Texture \u2192 VTEX\u0022 : \u0022Texture\u0022,\r\n\t\t_ =\u003E \u0022Model support\u0022\r\n\t};\r\n}\r\n\r\n/// \u003Csummary\u003EA selection snapshot shared by the window, importer and import report.\u003C/summary\u003E\r\npublic sealed class ImportPlan\r\n{\r\n\tpublic UnityArchive Archive { get; }\r\n\tpublic ImportOptions Options { get; }\r\n\tpublic IReadOnlyList\u003CPlannedAsset\u003E Assets { get; }\r\n\tpublic IReadOnlyList\u003CImportIssue\u003E Issues { get; }\r\n\treadonly Dictionary\u003CUnityAsset, PlannedAsset\u003E byAsset;\r\n\tinternal ImportPlan( UnityArchive archive, ImportOptions options, PlannedAsset[] assets, ImportIssue[] issues )\r\n\t{\r\n\t\tArchive = archive; Options = options; Assets = Array.AsReadOnly( assets ); Issues = Array.AsReadOnly( issues );\r\n\t\tbyAsset = assets.ToDictionary( a =\u003E a.Asset );\r\n\t}\r\n\tpublic PlannedAsset Find( UnityAsset asset ) =\u003E byAsset.GetValueOrDefault( asset );\r\n}\r\n\r\n/// \u003Csummary\u003ERead dependency evidence once, then resolve selections without disk access.\u003C/summary\u003E\r\npublic sealed class ImportCatalog\r\n{\r\n\treadonly UnityArchive archive;\r\n\treadonly Dictionary\u003CUnityAsset, List\u003CImportDependency\u003E\u003E dependencies = new();\r\n\treadonly Dictionary\u003CUnityAsset, List\u003CImportIssue\u003E\u003E issues = new();\r\n\tImportCatalog( UnityArchive archive ) { this.archive = archive; }\r\n\r\n\tpublic static ImportCatalog Read( UnityArchive archive, CancellationToken cancel = default )\r\n\t{\r\n\t\tvar catalog = new ImportCatalog( archive );\r\n\t\tvar byGuid = archive.Assets.ToDictionary( a =\u003E a.Guid, StringComparer.OrdinalIgnoreCase );\r\n\t\tvar materials = new Dictionary\u003CUnityAsset, UnityMaterial\u003E();\r\n\t\tvoid Issue( UnityAsset asset, string code, string message )\r\n\t\t{\r\n\t\t\tif ( !catalog.issues.TryGetValue( asset, out var list ) ) catalog.issues[asset] = list = new();\r\n\t\t\tlist.Add( new( asset.Path, code, message ) );\r\n\t\t}\r\n\t\tvoid Link( UnityAsset parent, UnityAsset child, string reason )\r\n\t\t{\r\n\t\t\tif ( !catalog.dependencies.TryGetValue( parent, out var list ) ) catalog.dependencies[parent] = list = new();\r\n\t\t\tif ( !list.Any( d =\u003E d.Asset == child \u0026\u0026 d.Reason == reason ) ) list.Add( new( child, reason ) );\r\n\t\t}\r\n\t\tforeach ( var asset in archive.Assets.Where( a =\u003E a.Kind == UnityAssetKind.Material ) )\r\n\t\t{\r\n\t\t\tcancel.ThrowIfCancellationRequested();\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tvar material = UnityMaterial.Parse( UnityImport.ReadText( asset.Source ) );\r\n\t\t\t\tif ( !string.IsNullOrEmpty( material.ShaderGuid ) \u0026\u0026 byGuid.TryGetValue( material.ShaderGuid, out var shader ) \u0026\u0026 shader.Path.EndsWith( \u0022.shader\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\t\tmaterial.ConfigureShader( UnityImport.ReadText( shader.Source ) );\r\n\t\t\t\tmaterials[asset] = material;\r\n\t\t\t\tforeach ( var guid in material.Textures.Values.Distinct( StringComparer.OrdinalIgnoreCase ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( byGuid.TryGetValue( guid, out var texture ) \u0026\u0026 texture.Kind == UnityAssetKind.Texture ) Link( asset, texture, \u0022Texture reference\u0022 );\r\n\t\t\t\t\telse Issue( asset, \u0022missing-texture\u0022, $\u0022Missing or unsupported texture {guid}.\u0022 );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tcatch ( InvalidDataException ex ) { Issue( asset, \u0022conversion-skipped\u0022, $\u0022Conversion skipped. {ex.Message}\u0022 ); }\r\n\t\t}\r\n\t\tvar byName = archive.Assets.Where( a =\u003E a.Kind == UnityAssetKind.Material ).GroupBy( a =\u003E Path.GetFileNameWithoutExtension( a.Path ), StringComparer.OrdinalIgnoreCase )\r\n\t\t\t.ToDictionary( g =\u003E g.Key, g =\u003E g.ToArray(), StringComparer.OrdinalIgnoreCase );\r\n\t\tvar byTexture = materials.SelectMany( m =\u003E m.Value.ColorTextureGuids.Where( byGuid.ContainsKey ).Select( g =\u003E (Name: Path.GetFileNameWithoutExtension( byGuid[g].Path ), Asset: m.Key) ) )\r\n\t\t\t.GroupBy( m =\u003E m.Name, StringComparer.OrdinalIgnoreCase ).ToDictionary( g =\u003E g.Key, g =\u003E g.Select( m =\u003E m.Asset ).Distinct().ToArray(), StringComparer.OrdinalIgnoreCase );\r\n\t\tforeach ( var asset in archive.Assets.Where( a =\u003E a.Kind == UnityAssetKind.Model ) )\n\t\t{\n\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\tif ( asset.ModelInfo.CollisionOnly ) continue;\n\t\t\tforeach ( var warning in asset.ModelInfo.AssignmentWarnings ) Issue( asset, \u0022prefab-material-assignment\u0022, warning );\n\t\t\tvar referencedMaterials = new HashSet\u003CUnityAsset\u003E();\r\n\t\t\tvoid Match( UnityAsset[] candidates, string reason )\r\n\t\t\t{\r\n\t\t\t\tvar referenced = candidates.Where( referencedMaterials.Contains ).ToArray();\r\n\t\t\t\tif ( referenced.Length \u003E 0 ) candidates = referenced;\r\n\t\t\t\tif ( candidates.Length == 1 ) Link( asset, candidates[0], reason \u002B \u0022 (inferred)\u0022 );\r\n\t\t\t\telse if ( candidates.Length \u003E 1 ) Issue( asset, \u0022ambiguous-material\u0022, $\u0022{reason} matches multiple materials; no dependency was inferred.\u0022 );\r\n\t\t\t}\r\n\t\t\tvar refs = asset.ModelInfo.PrefabMaterials.AsEnumerable();\r\n\t\t\tif ( asset.Metadata != null ) refs = refs.Concat( UnityMaterial.References( UnityImport.ReadText( asset.Metadata ) ) );\r\n\t\t\tforeach ( var guid in refs.Distinct( StringComparer.OrdinalIgnoreCase ) )\r\n\t\t\t\tif ( byGuid.TryGetValue( guid, out var dependency ) \u0026\u0026 dependency.Kind is UnityAssetKind.Material or UnityAssetKind.Texture )\r\n\t\t\t\t{\r\n\t\t\t\t\tLink( asset, dependency, \u0022Model / prefab reference\u0022 );\r\n\t\t\t\t\tif ( dependency.Kind == UnityAssetKind.Material ) referencedMaterials.Add( dependency );\r\n\t\t\t\t}\r\n\t\t\tforeach ( var filename in asset.ModelInfo.MaterialAlbedoFiles.Values ) Match( byTexture.GetValueOrDefault( Path.GetFileNameWithoutExtension( filename ), Array.Empty\u003CUnityAsset\u003E() ), $\u0022Color texture {filename}\u0022 );\r\n\t\t\tforeach ( var slot in asset.ModelInfo.RenderedMaterials.Where( s =\u003E !asset.ModelInfo.PrefabSlotMaterials.ContainsKey( s ) \u0026\u0026 !asset.ModelInfo.UnresolvedPrefabSlots.Contains( s ) ) ) Match( byName.GetValueOrDefault( slot, Array.Empty\u003CUnityAsset\u003E() ), $\u0022Material slot {slot}\u0022 );\n\t\t\tMatch( byName.GetValueOrDefault( Path.GetFileNameWithoutExtension( asset.Path ), Array.Empty\u003CUnityAsset\u003E() ), \u0022Model filename\u0022 );\r\n\t\t}\r\n\t\treturn catalog;\r\n\t}\r\n\r\n\tpublic ImportPlan CreatePlan( ImportOptions options )\r\n\t{\r\n\t\tvar explicitAssets = archive.Assets.Where( a =\u003E a.Selected \u0026\u0026 UnityImport.IsEnabled( a, options ) ).ToHashSet();\r\n\t\tvar included = new HashSet\u003CUnityAsset\u003E( explicitAssets );\r\n\t\tvar requiredBy = new Dictionary\u003CUnityAsset, List\u003CImportDependency\u003E\u003E();\r\n\t\tvar queue = new Queue\u003CUnityAsset\u003E( included );\r\n\t\twhile ( queue.TryDequeue( out var parent ) )\r\n\t\t{\r\n\t\t\tif ( !options.Materials || !dependencies.TryGetValue( parent, out var children ) ) continue;\r\n\t\t\tforeach ( var child in children )\r\n\t\t\t{\r\n\t\t\t\tif ( !requiredBy.TryGetValue( child.Asset, out var parents ) ) requiredBy[child.Asset] = parents = new();\r\n\t\t\t\tparents.Add( new( parent, child.Reason ) );\r\n\t\t\t\tif ( included.Add( child.Asset ) ) queue.Enqueue( child.Asset );\r\n\t\t\t}\r\n\t\t}\r\n\t\tvar entries = included.OrderBy( a =\u003E a.Kind == UnityAssetKind.Model ? 1 : 0 ).ThenBy( a =\u003E a.Path, StringComparer.OrdinalIgnoreCase ).Select( a =\u003E\r\n\t\t{\r\n\t\t\tvar parents = requiredBy.GetValueOrDefault( a )?.ToArray() ?? Array.Empty\u003CImportDependency\u003E();\r\n\t\t\tvar terrain = a.Path.EndsWith( \u0022.terrainlayer\u0022, StringComparison.OrdinalIgnoreCase );\r\n\t\t\tvar modelUse = parents.Any( p =\u003E p.Asset.Kind == UnityAssetKind.Model );\r\n\t\t\treturn new PlannedAsset( a, explicitAssets.Contains( a ), parents,\r\n\t\t\t\ta.Kind == UnityAssetKind.Material \u0026\u0026 options.Vmat \u0026\u0026 (!options.Automatic || !terrain || modelUse),\r\n\t\t\t\ta.Kind == UnityAssetKind.Material \u0026\u0026 (options.Tmat || options.Automatic \u0026\u0026 terrain),\r\n\t\t\t\ta.Kind == UnityAssetKind.Model \u0026\u0026 options.Vmdl \u0026\u0026 !a.ModelInfo.CollisionOnly );\n\t\t} ).ToArray();\r\n\t\treturn new( archive, options, entries, entries.Where( a =\u003E a.Vmat || a.Tmat || a.Vmdl \u0026\u0026 options.Materials )\r\n\t\t\t.Where( a =\u003E issues.ContainsKey( a.Asset ) ).SelectMany( a =\u003E issues[a.Asset] ).Distinct().ToArray() );\r\n\t}\r\n}\r\n"},{"Ident":"grubs.importunitypackage","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":381664,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Import Unity Package\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022importunitypackage\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022grubs\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022grubs.importunitypackage\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002229\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-09-16T05:10:22.2627195Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.112.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.112.0\u0022)]"}]}