{"TotalCount":34,"Files":[{"Ident":"grubs.importunitypackage","Path":"Editor/Core/ImportStorage.cs","FileName":"ImportStorage.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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\t/// \u003Csummary\u003EDivides the retry waits; tests that hold files locked on purpose raise it.\u003C/summary\u003E\r\n\tinternal static int RetryWaitDivisor = 1;\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 ) / RetryWaitDivisor ) ) cancel.ThrowIfCancellationRequested();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityMesh.cs","FileName":"UnityMesh.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003E\n/// A Unity Mesh object (class 43) read from its serialized vertex and index buffers: positions, normals, the first UV\n/// set and one triangle list per submesh, in Unity\u0027s left-handed object space. Meshes come from text files (buffers as\n/// hex strings) or from the binary reader (byte arrays); both use the same field names. Also Unity\u0027s built-in\n/// primitives (cube, sphere, capsule, cylinder, plane, quad) at Unity\u0027s sizes.\n/// \u003C/summary\u003E\ninternal sealed class UnityMesh\n{\n\tpublic string Name { get; init; }\n\tpublic List\u003Cdouble[]\u003E Positions { get; } = new();\n\tpublic List\u003Cdouble[]\u003E Normals { get; } = new();\n\tpublic List\u003Cdouble[]\u003E Uvs { get; } = new();\n\tpublic List\u003CList\u003Cint\u003E\u003E Submeshes { get; } = new();\n\n\t/// \u003Csummary\u003EThe GUID Unity uses for its built-in resources, whose meshes are addressed by file ID.\u003C/summary\u003E\n\tpublic const string BuiltinGuid = \u00220000000000000000e000000000000000\u0022;\n\tpublic static readonly IReadOnlyDictionary\u003Clong, string\u003E Primitives = new Dictionary\u003Clong, string\u003E\n\t{\n\t\t[10202] = \u0022Cube\u0022, [10206] = \u0022Cylinder\u0022, [10207] = \u0022Sphere\u0022, [10208] = \u0022Capsule\u0022, [10209] = \u0022Plane\u0022, [10210] = \u0022Quad\u0022\n\t};\n\n\tstatic object Field( object node, string key ) =\u003E node switch\n\t{\n\t\tYamlNode yaml =\u003E yaml[key],\n\t\tDictionary\u003Cstring, object\u003E map =\u003E map.GetValueOrDefault( key ),\n\t\t_ =\u003E null\n\t};\n\tstatic double Number( object value, double fallback = 0 ) =\u003E value switch\n\t{\n\t\tYamlNode yaml =\u003E yaml.Number ?? fallback,\n\t\tlong l =\u003E l, double d =\u003E d, bool b =\u003E b ? 1 : 0,\n\t\t_ =\u003E fallback\n\t};\n\tstatic IEnumerable\u003Cobject\u003E Items( object value ) =\u003E value switch\n\t{\n\t\tYamlNode yaml =\u003E yaml.Items,\n\t\tList\u003Cobject\u003E list =\u003E list,\n\t\t_ =\u003E Array.Empty\u003Cobject\u003E()\n\t};\n\tstatic byte[] Bytes( object value )\n\t{\n\t\tif ( value is byte[] bytes ) return bytes;\n\t\tif ( value is not YamlNode yaml || string.IsNullOrEmpty( yaml.Value ) ) return Array.Empty\u003Cbyte\u003E();\n\t\tvar hex = yaml.Value.Trim();\n\t\tif ( hex.Length % 2 != 0 ) throw new InvalidDataException( \u0022A mesh buffer has an odd number of hex digits.\u0022 );\n\t\treturn Convert.FromHexString( hex );\n\t}\n\n\t// Vertex attribute sizes: Unity 2019\u002B VertexAttributeFormat, and the older channel formats (Unity 5 to 2018).\n\tstatic readonly int[] NewSizes = { 4, 2, 1, 1, 2, 2, 1, 1, 2, 2, 4, 4 };\n\tstatic readonly int[] OldSizes = { 4, 2, 1, 1, 4 };\n\n\t/// \u003Csummary\u003EReads a Mesh object\u0027s body (a YAML node or a binary reader dictionary).\u003C/summary\u003E\n\tpublic static UnityMesh Read( object body )\n\t{\n\t\tvar mesh = new UnityMesh { Name = (Field( body, \u0022m_Name\u0022 ) as YamlNode)?.Value ?? Field( body, \u0022m_Name\u0022 ) as string ?? \u0022Mesh\u0022 };\n\t\tvar vertexData = Field( body, \u0022m_VertexData\u0022 );\n\t\tvar count = (int)Number( Field( vertexData, \u0022m_VertexCount\u0022 ) );\n\t\tvar data = Bytes( Field( vertexData, \u0022_typelessdata\u0022 ) );\n\t\tif ( data.Length == 0 \u0026\u0026 Field( vertexData, \u0022m_DataSize\u0022 ) is byte[] binary ) data = binary;\n\t\tif ( count == 0 || data.Length == 0 )\n\t\t{\n\t\t\tvar compressed = Number( Field( Field( Field( body, \u0022m_CompressedMesh\u0022 ), \u0022m_Vertices\u0022 ), \u0022m_NumItems\u0022 ) );\n\t\t\tthrow new InvalidDataException( compressed \u003E 0 ? \u0022The mesh uses Unity\u0027s mesh compression, which is not read.\u0022 : \u0022The mesh has no vertices.\u0022 );\n\t\t}\n\t\tvar channels = Items( Field( vertexData, \u0022m_Channels\u0022 ) )\n\t\t\t.Select( c =\u003E (Stream: (int)Number( Field( c, \u0022stream\u0022 ) ), Offset: (int)Number( Field( c, \u0022offset\u0022 ) ), Format: (int)Number( Field( c, \u0022format\u0022 ) ), Dimension: (int)Number( Field( c, \u0022dimension\u0022 ) ) \u0026 0xF) )\n\t\t\t.ToList();\n\t\t// Unity 5 to 2017 have 8 channels (position, normal, color, uv0-3, tangent); later versions 14 (position, normal, tangent, color, uv0-7, weights, indices).\n\t\tvar newLayout = channels.Count \u003E 8;\n\t\tint UvChannel = newLayout ? 4 : 3;\n\t\tvar layout = Layout( channels, count, NewSizes );\n\t\tif ( layout.Total \u003E data.Length ) layout = Layout( channels, count, OldSizes );\n\t\tif ( layout.Total \u003E data.Length ) throw new InvalidDataException( \u0022The mesh\u0027s vertex data is shorter than its channels describe.\u0022 );\n\t\tdouble Read( int channel, int vertex, int component )\n\t\t{\n\t\t\tvar c = channels[channel];\n\t\t\tvar at = layout.StreamStart[c.Stream] \u002B vertex * layout.Stride[c.Stream] \u002B c.Offset \u002B component * layout.Sizes( c.Format );\n\t\t\treturn c.Format switch\n\t\t\t{\n\t\t\t\t0 =\u003E BitConverter.ToSingle( data, at ),\n\t\t\t\t1 =\u003E (double)BitConverter.ToHalf( data, at ),\n\t\t\t\t2 =\u003E data[at] / 255.0,\n\t\t\t\t_ =\u003E 0\n\t\t\t};\n\t\t}\n\t\tbool Has( int channel ) =\u003E channel \u003C channels.Count \u0026\u0026 channels[channel].Dimension \u003E 0 \u0026\u0026 channels[channel].Format is 0 or 1 or 2;\n\t\tfor ( var v = 0; v \u003C count; v\u002B\u002B )\n\t\t{\n\t\t\tmesh.Positions.Add( new[] { Read( 0, v, 0 ), Read( 0, v, 1 ), Read( 0, v, 2 ) } );\n\t\t\tif ( Has( 1 ) ) mesh.Normals.Add( new[] { Read( 1, v, 0 ), Read( 1, v, 1 ), Read( 1, v, 2 ) } );\n\t\t\tif ( Has( UvChannel ) \u0026\u0026 channels[UvChannel].Dimension \u003E= 2 ) mesh.Uvs.Add( new[] { Read( UvChannel, v, 0 ), Read( UvChannel, v, 1 ) } );\n\t\t}\n\t\tvar indices = Bytes( Field( body, \u0022m_IndexBuffer\u0022 ) );\n\t\tvar wide = Number( Field( body, \u0022m_IndexFormat\u0022 ) ) == 1;\n\t\tvar indexSize = wide ? 4 : 2;\n\t\tforeach ( var submesh in Items( Field( body, \u0022m_SubMeshes\u0022 ) ) )\n\t\t{\n\t\t\tvar triangles = new List\u003Cint\u003E();\n\t\t\tvar first = (int)Number( Field( submesh, \u0022firstByte\u0022 ) ) / indexSize;\n\t\t\tvar indexCount = (int)Number( Field( submesh, \u0022indexCount\u0022 ) );\n\t\t\tvar topology = (int)Number( Field( submesh, \u0022topology\u0022 ) );\n\t\t\tvar baseVertex = (int)Number( Field( submesh, \u0022baseVertex\u0022 ) );\n\t\t\tint Index( int i ) =\u003E (wide ? (int)BitConverter.ToUInt32( indices, (first \u002B i) * 4 ) : BitConverter.ToUInt16( indices, (first \u002B i) * 2 )) \u002B baseVertex;\n\t\t\tif ( (first \u002B indexCount) * indexSize \u003E indices.Length ) throw new InvalidDataException( \u0022A submesh lies outside the mesh\u0027s index buffer.\u0022 );\n\t\t\tif ( topology == 0 ) for ( var i = 0; i \u002B 2 \u003C indexCount; i \u002B= 3 ) triangles.AddRange( new[] { Index( i ), Index( i \u002B 1 ), Index( i \u002B 2 ) } );\n\t\t\telse if ( topology == 2 ) for ( var i = 0; i \u002B 3 \u003C indexCount; i \u002B= 4 ) triangles.AddRange( new[] { Index( i ), Index( i \u002B 1 ), Index( i \u002B 2 ), Index( i ), Index( i \u002B 2 ), Index( i \u002B 3 ) } );\n\t\t\tif ( triangles.Any( t =\u003E t \u003C 0 || t \u003E= count ) ) throw new InvalidDataException( \u0022A submesh index points past the mesh\u0027s vertices.\u0022 );\n\t\t\tmesh.Submeshes.Add( triangles );\n\t\t}\n\t\tif ( mesh.Submeshes.All( s =\u003E s.Count == 0 ) ) throw new InvalidDataException( \u0022The mesh has no triangles.\u0022 );\n\t\treturn mesh;\n\t}\n\n\tsealed record VertexLayout( int[] StreamStart, int[] Stride, int Total, int[] FormatSizes )\n\t{\n\t\tpublic int Sizes( int format ) =\u003E format \u003C FormatSizes.Length ? FormatSizes[format] : 4;\n\t}\n\n\t/// \u003Csummary\u003EStreams hold their channels interleaved; each stream starts at a 16-byte boundary after the previous one.\u003C/summary\u003E\n\tstatic VertexLayout Layout( List\u003C(int Stream, int Offset, int Format, int Dimension)\u003E channels, int count, int[] sizes )\n\t{\n\t\tvar streams = channels.Where( c =\u003E c.Dimension \u003E 0 ).Select( c =\u003E c.Stream ).DefaultIfEmpty( 0 ).Max() \u002B 1;\n\t\tvar start = new int[streams]; var stride = new int[streams];\n\t\tforeach ( var c in channels.Where( c =\u003E c.Dimension \u003E 0 ) )\n\t\t\tstride[c.Stream] = Math.Max( stride[c.Stream], c.Offset \u002B (c.Format \u003C sizes.Length ? sizes[c.Format] : 4) * c.Dimension );\n\t\tvar total = 0;\n\t\tfor ( var s = 0; s \u003C streams; s\u002B\u002B )\n\t\t{\n\t\t\tstart[s] = total;\n\t\t\ttotal \u002B= stride[s] * count;\n\t\t\tif ( s \u002B 1 \u003C streams ) total = (total \u002B 15) \u0026 ~15;\n\t\t}\n\t\treturn new( start, stride, total, sizes );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The mesh as OBJ text in a model file\u0027s axes, which the model pipeline maps to Unity as (-x, y, z): X is mirrored.\n\t/// Unity and OBJ both face a triangle along cross(b - a, c - a), and the mirror reverses that, so each triangle is\n\t/// written in reverse order to keep its front (as Unity reverses FBX triangles when it mirrors). Each submesh is a material slot\n\t/// named Submesh0, Submesh1 and so on, matching the renderer\u0027s material order. Units are meters.\n\t/// \u003C/summary\u003E\n\tpublic string ToObj()\n\t{\n\t\tvar text = new StringBuilder();\n\t\tstring F( double v ) =\u003E v.ToString( \u00220.######\u0022, CultureInfo.InvariantCulture );\n\t\ttext.Append( \u0022# Converted from a Unity mesh\\ng \u0022 ).Append( SafeName( Name ) ).Append( \u0027\\n\u0027 );\n\t\tforeach ( var p in Positions ) text.Append( \u0022v \u0022 ).Append( F( -p[0] ) ).Append( \u0027 \u0027 ).Append( F( p[1] ) ).Append( \u0027 \u0027 ).Append( F( p[2] ) ).Append( \u0027\\n\u0027 );\n\t\tforeach ( var n in Normals ) text.Append( \u0022vn \u0022 ).Append( F( -n[0] ) ).Append( \u0027 \u0027 ).Append( F( n[1] ) ).Append( \u0027 \u0027 ).Append( F( n[2] ) ).Append( \u0027\\n\u0027 );\n\t\tforeach ( var t in Uvs ) text.Append( \u0022vt \u0022 ).Append( F( t[0] ) ).Append( \u0027 \u0027 ).Append( F( t[1] ) ).Append( \u0027\\n\u0027 );\n\t\tbool normals = Normals.Count == Positions.Count, uvs = Uvs.Count == Positions.Count;\n\t\tstring Corner( int i ) =\u003E (i \u002B 1) \u002B (uvs || normals ? \u0022/\u0022 \u002B (uvs ? (i \u002B 1).ToString( CultureInfo.InvariantCulture ) : \u0022\u0022) \u002B (normals ? \u0022/\u0022 \u002B (i \u002B 1) : \u0022\u0022) : \u0022\u0022);\n\t\tfor ( var s = 0; s \u003C Submeshes.Count; s\u002B\u002B )\n\t\t{\n\t\t\tif ( Submeshes[s].Count == 0 ) continue;\n\t\t\ttext.Append( \u0022usemtl Submesh\u0022 ).Append( s ).Append( \u0027\\n\u0027 );\n\t\t\tvar triangles = Submeshes[s];\n\t\t\tfor ( var i = 0; i \u002B 2 \u003C triangles.Count; i \u002B= 3 )\n\t\t\t\ttext.Append( \u0022f \u0022 ).Append( Corner( triangles[i] ) ).Append( \u0027 \u0027 ).Append( Corner( triangles[i \u002B 2] ) ).Append( \u0027 \u0027 ).Append( Corner( triangles[i \u002B 1] ) ).Append( \u0027\\n\u0027 );\n\t\t}\n\t\treturn text.ToString();\n\t}\n\n\tstatic string SafeName( string name ) =\u003E new( (string.IsNullOrWhiteSpace( name ) ? \u0022Mesh\u0022 : name).Select( c =\u003E char.IsLetterOrDigit( c ) || c is \u0027_\u0027 or \u0027-\u0027 ? c : \u0027_\u0027 ).ToArray() );\n\n\t/// \u003Csummary\u003EOne of Unity\u0027s built-in primitive meshes, with Unity\u0027s sizes, UV layout and single submesh.\u003C/summary\u003E\n\tpublic static UnityMesh Primitive( string name )\n\t{\n\t\tvar mesh = new UnityMesh { Name = name };\n\t\tvar triangles = new List\u003Cint\u003E();\n\t\tmesh.Submeshes.Add( triangles );\n\t\t// A quad from four corners (counter-clockwise seen from the front, in Unity\u0027s left-handed view: clockwise).\n\t\tvoid Quad( double[] a, double[] b, double[] c, double[] d, double[] normal, double u0 = 0, double v0 = 0, double u1 = 1, double v1 = 1 )\n\t\t{\n\t\t\tvar i = mesh.Positions.Count;\n\t\t\tforeach ( var (p, uv) in new[] { (a, new[] { u0, v0 }), (b, new[] { u1, v0 }), (c, new[] { u1, v1 }), (d, new[] { u0, v1 }) } )\n\t\t\t{\n\t\t\t\tmesh.Positions.Add( p ); mesh.Normals.Add( normal ); mesh.Uvs.Add( uv );\n\t\t\t}\n\t\t\t// Unity\u0027s front faces wind clockwise as seen from the normal side.\n\t\t\ttriangles.AddRange( new[] { i, i \u002B 2, i \u002B 1, i, i \u002B 3, i \u002B 2 } );\n\t\t}\n\t\tswitch ( name )\n\t\t{\n\t\t\tcase \u0022Quad\u0022: // 1 x 1 in the XY plane, facing -Z (towards a camera looking along \u002BZ).\n\t\t\t\tQuad( new[] { -0.5, -0.5, 0.0 }, new[] { 0.5, -0.5, 0.0 }, new[] { 0.5, 0.5, 0.0 }, new[] { -0.5, 0.5, 0.0 }, new[] { 0.0, 0.0, -1.0 } );\n\t\t\t\tbreak;\n\t\t\tcase \u0022Plane\u0022: // 10 x 10 in the XZ plane, facing \u002BY, 10 x 10 cells.\n\t\t\t\tfor ( var z = 0; z \u003C 10; z\u002B\u002B )\n\t\t\t\t\tfor ( var x = 0; x \u003C 10; x\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tdouble x0 = 5 - x, x1 = 4 - x, z0 = 5 - z, z1 = 4 - z;\n\t\t\t\t\t\tQuad( new[] { x0, 0, z0 }, new[] { x1, 0, z0 }, new[] { x1, 0, z1 }, new[] { x0, 0, z1 }, new[] { 0.0, 1.0, 0.0 }, x / 10.0, z / 10.0, (x \u002B 1) / 10.0, (z \u002B 1) / 10.0 );\n\t\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \u0022Cube\u0022: // 1 x 1 x 1.\n\t\t\t\tforeach ( var (n, u, v) in new (double[] N, double[] U, double[] V)[]\n\t\t\t\t{\n\t\t\t\t\t(new[] { 0.0, 0, -1 }, new[] { 1.0, 0, 0 }, new[] { 0.0, 1, 0 }), (new[] { 0.0, 0, 1 }, new[] { -1.0, 0, 0 }, new[] { 0.0, 1, 0 }),\n\t\t\t\t\t(new[] { -1.0, 0, 0 }, new[] { 0.0, 0, -1 }, new[] { 0.0, 1, 0 }), (new[] { 1.0, 0, 0 }, new[] { 0.0, 0, 1 }, new[] { 0.0, 1, 0 }),\n\t\t\t\t\t(new[] { 0.0, 1, 0 }, new[] { 1.0, 0, 0 }, new[] { 0.0, 0, 1 }), (new[] { 0.0, -1, 0 }, new[] { 1.0, 0, 0 }, new[] { 0.0, 0, -1 })\n\t\t\t\t} )\n\t\t\t\t{\n\t\t\t\t\tdouble[] P( double a, double b ) =\u003E new[] { n[0] * 0.5 \u002B u[0] * a \u002B v[0] * b, n[1] * 0.5 \u002B u[1] * a \u002B v[1] * b, n[2] * 0.5 \u002B u[2] * a \u002B v[2] * b };\n\t\t\t\t\tQuad( P( -0.5, -0.5 ), P( 0.5, -0.5 ), P( 0.5, 0.5 ), P( -0.5, 0.5 ), n );\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \u0022Sphere\u0022: // Diameter 1.\n\t\t\t\tRevolve( mesh, triangles, 24, 16, t =\u003E (0.5 * Math.Sin( Math.PI * t ), -0.5 * Math.Cos( Math.PI * t )), ( t, y ) =\u003E (Math.Sin( Math.PI * t ), -Math.Cos( Math.PI * t )) );\n\t\t\t\tbreak;\n\t\t\tcase \u0022Capsule\u0022: // Height 2, diameter 1: hemispheres joined by a 1 m cylinder.\n\t\t\t\tRevolve( mesh, triangles, 24, 18, t =\u003E\n\t\t\t\t{\n\t\t\t\t\tif ( t \u003C 1 / 3.0 ) { var a = t * 1.5 * Math.PI; return (0.5 * Math.Sin( a ), -0.5 - 0.5 * Math.Cos( a )); }\n\t\t\t\t\tif ( t \u003E 2 / 3.0 ) { var a = (t - 2 / 3.0) * 1.5 * Math.PI \u002B Math.PI / 2; return (0.5 * Math.Sin( a ), 0.5 - 0.5 * Math.Cos( a )); }\n\t\t\t\t\treturn (0.5, (t - 0.5) * 3);\n\t\t\t\t}, ( t, y ) =\u003E\n\t\t\t\t{\n\t\t\t\t\tif ( t \u003C 1 / 3.0 ) { var a = t * 1.5 * Math.PI; return (Math.Sin( a ), -Math.Cos( a )); }\n\t\t\t\t\tif ( t \u003E 2 / 3.0 ) { var a = (t - 2 / 3.0) * 1.5 * Math.PI \u002B Math.PI / 2; return (Math.Sin( a ), -Math.Cos( a )); }\n\t\t\t\t\treturn (1, 0);\n\t\t\t\t} );\n\t\t\t\tbreak;\n\t\t\tcase \u0022Cylinder\u0022: // Height 2, diameter 1, with caps.\n\t\t\t\tRevolve( mesh, triangles, 24, 1, t =\u003E (0.5, t * 2 - 1), ( t, y ) =\u003E (1, 0) );\n\t\t\t\tforeach ( var top in new[] { 1.0, -1.0 } )\n\t\t\t\t{\n\t\t\t\t\tvar center = mesh.Positions.Count;\n\t\t\t\t\tmesh.Positions.Add( new[] { 0.0, top, 0 } ); mesh.Normals.Add( new[] { 0.0, top, 0 } ); mesh.Uvs.Add( new[] { 0.5, 0.5 } );\n\t\t\t\t\tfor ( var i = 0; i \u003C= 24; i\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar a = 2 * Math.PI * i / 24;\n\t\t\t\t\t\tmesh.Positions.Add( new[] { 0.5 * Math.Cos( a ), top, 0.5 * Math.Sin( a ) } ); mesh.Normals.Add( new[] { 0.0, top, 0 } );\n\t\t\t\t\t\tmesh.Uvs.Add( new[] { 0.5 \u002B 0.5 * Math.Cos( a ), 0.5 \u002B 0.5 * Math.Sin( a ) } );\n\t\t\t\t\t}\n\t\t\t\t\tfor ( var i = 0; i \u003C 24; i\u002B\u002B )\n\t\t\t\t\t\ttriangles.AddRange( top \u003E 0 ? new[] { center, center \u002B i \u002B 2, center \u002B i \u002B 1 } : new[] { center, center \u002B i \u002B 1, center \u002B i \u002B 2 } );\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tdefault: throw new InvalidDataException( $\u0022Unknown built-in mesh \u0027{name}\u0027.\u0022 );\n\t\t}\n\t\treturn mesh;\n\t}\n\n\t/// \u003Csummary\u003EA surface of revolution about Y: profile(t) gives (radius, height) and normal(t) (radial, vertical) for t in 0..1.\u003C/summary\u003E\n\tstatic void Revolve( UnityMesh mesh, List\u003Cint\u003E triangles, int segments, int rings, Func\u003Cdouble, (double R, double Y)\u003E profile, Func\u003Cdouble, double, (double R, double Y)\u003E normal )\n\t{\n\t\tvar start = mesh.Positions.Count;\n\t\tfor ( var ring = 0; ring \u003C= rings; ring\u002B\u002B )\n\t\t{\n\t\t\tvar t = (double)ring / rings;\n\t\t\tvar (r, y) = profile( t );\n\t\t\tvar (nr, ny) = normal( t, y );\n\t\t\tfor ( var s = 0; s \u003C= segments; s\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = 2 * Math.PI * s / segments;\n\t\t\t\tmesh.Positions.Add( new[] { r * Math.Cos( a ), y, r * Math.Sin( a ) } );\n\t\t\t\tmesh.Normals.Add( new[] { nr * Math.Cos( a ), ny, nr * Math.Sin( a ) } );\n\t\t\t\tmesh.Uvs.Add( new[] { (double)s / segments, t } );\n\t\t\t}\n\t\t}\n\t\tfor ( var ring = 0; ring \u003C rings; ring\u002B\u002B )\n\t\t\tfor ( var s = 0; s \u003C segments; s\u002B\u002B )\n\t\t\t{\n\t\t\t\tint a = start \u002B ring * (segments \u002B 1) \u002B s, b = a \u002B segments \u002B 1;\n\t\t\t\t// Outward faces, clockwise as seen from outside (Unity\u0027s front-face winding).\n\t\t\t\ttriangles.AddRange( new[] { a, b, a \u002B 1, a \u002B 1, b, b \u002B 1 } );\n\t\t\t}\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/GeneratedAssetFiles.cs","FileName":"GeneratedAssetFiles.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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( UnityImport.ModelOutput( item.Asset.Path ) );\n\t\t\tif ( item.Sound ) reserved.Add( Path.ChangeExtension( item.Asset.Path, \u0022.sound\u0022 ) );\n\t\t\tif ( item.Vtex ) reserved.Add( item.Asset.Path \u002B \u0022.vtex\u0022 );\n\t\t\tif ( item.Prefab ) reserved.Add( UnityImport.PlacementOutput( item.Asset.Path, \u0022.prefab\u0022 ) );\n\t\t\tif ( item.Scene ) reserved.Add( UnityImport.PlacementOutput( item.Asset.Path, \u0022.scene\u0022 ) );\n\t\t\tif ( item.Terrain ) { reserved.Add( UnityImport.PlacementOutput( item.Asset.Path, \u0022.terrain\u0022 ) ); reserved.Add( UnityImport.PlacementOutput( item.Asset.Path, \u0022.terrain\u0022 ) \u002B \u0022_d\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":410869,"IsPrivate":false,"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\n/// \u003Csummary\u003EMeshes from one file, filtered to the named nodes (all when empty), at an import scale.\u003C/summary\u003E\ninternal sealed record MeshPart( string File, string[] Meshes, double Scale );\npublic record ImportOptions( bool Materials, bool Models, bool Vmat, bool Tmat, bool Vmdl )\n{\n\tpublic bool Automatic { get; init; }\n\tpublic bool TerrainDefinitions { get; init; } = true;\n\tpublic bool Vtex { get; init; } = true;\n\tpublic bool Sound { get; init; } = true;\n\tpublic bool Prefabs { get; init; } = true;\n\tpublic bool Scenes { get; init; } = true;\n\tpublic bool Terrains { get; init; } = true;\n\t// Keep programmatic callers compatible; the editor window chooses its destination explicitly.\n\tpublic bool ImportToAssets { get; init; } = true;\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 is \u0022conversion-skipped\u0022 or \u0022unreadable-model\u0022 or \u0022texture-processing-failed\u0022 ? \u0022Error\u0022 : \u0022Warning\u0022;\n}\npublic record ImportResult( string Directory, int AssetCount, int ConvertedCount, string[] Files, string[] Warnings )\n{\n\tpublic bool RequiresResourcePreparation { get; init; } = true;\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\t/// \u003Csummary\u003EWritten files that replaced an existing file (the only ones whose dependants may need rebuilding).\u003C/summary\u003E\n\tpublic string[] ReplacedFiles { get; init; } = Array.Empty\u003Cstring\u003E();\n\tpublic string[] Information { get; init; } = Array.Empty\u003Cstring\u003E();\n}\n/// \u003Csummary\u003E\n/// One derived image from Source: a channel (Channel 0-3, scaled and optionally inverted), the whole image (-1), the\n/// image with its green channel flipped (-3), or a normal map from the image as height (-4: Scale is Unity\u0027s bumpiness,\n/// Invert selects the Sobel filter); resized first when Width and Height are set. With Faces, a sky panorama composed\n/// from six cube faces (Width 0 picks the size).\n/// \u003C/summary\u003E\npublic record TextureChannelRequest( string Source, string Destination, int Channel, double Scale, bool Invert, int Width = 0, int Height = 0, SkyFace[] Faces = null );\npublic delegate void ExtractTextureChannels( IReadOnlyList\u003CTextureChannelRequest\u003E requests, CancellationToken cancel );\n\n/// \u003Csummary\u003E\n/// Thrown by a texture batch after every request was processed, listing the requests whose source could not be read or\n/// converted; each of those destinations holds a neutral placeholder.\n/// \u003C/summary\u003E\npublic sealed class TextureBatchException : Exception\n{\n\tpublic sealed record Failure( string Source, string Destination, string Message );\n\tpublic IReadOnlyList\u003CFailure\u003E Failures { get; }\n\tpublic TextureBatchException( IReadOnlyList\u003CFailure\u003E failures ) : base( $\u0022{failures.Count} texture outputs could not be generated.\u0022 ) { Failures = failures; }\n}\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\tUnityAssetKind.Sound =\u003E true,\n\t\tUnityAssetKind.Prefab =\u003E options.Models \u0026\u0026 options.Prefabs,\n\t\tUnityAssetKind.Scene =\u003E options.Models \u0026\u0026 options.Scenes,\n\t\tUnityAssetKind.Terrain =\u003E options.Terrains,\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, ExtractTextureChannels extractChannels = null )\n\t\t=\u003E Run( ImportCatalog.Read( archive, cancel ).CreatePlan( options ), assetsDirectory, progress, cancel, extractChannels );\n\n\tpublic static ImportResult Run( ImportPlan plan, string assetsDirectory,\n\t\tIProgress\u003CImportProgress\u003E progress, CancellationToken cancel, ExtractTextureChannels extractChannels = null )\n\t{\n\t\tusing var merge = ImportMergePlan.Create( plan, assetsDirectory, cancel );\n\t\tmerge.Prepare( progress, cancel, 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, ExtractTextureChannels extractChannels )\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\tif ( !options.ImportToAssets ) information.Add( \u0022Stored outside Assets. Use Import to assets to make these files available in the editor and game.\u0022 );\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\t// Lookups by GUID and path instead of scans over every asset (large packages have tens of thousands).\n\t\tvar selectedByGuid = selected.GroupBy( a =\u003E a.Guid, StringComparer.OrdinalIgnoreCase ).ToDictionary( g =\u003E g.Key, g =\u003E g.First(), StringComparer.OrdinalIgnoreCase );\n\t\tvar assetsByGuid = archive.Assets.GroupBy( a =\u003E a.Guid, StringComparer.OrdinalIgnoreCase ).ToDictionary( g =\u003E g.Key, g =\u003E g.First(), StringComparer.OrdinalIgnoreCase );\n\t\tvar assetPaths = archive.Assets.Select( a =\u003E a.Path ).ToHashSet( StringComparer.OrdinalIgnoreCase );\n\t\t// The selected material with this GUID when its VMAT is written by this import.\n\t\tUnityAsset SelectedVmat( string guid ) =\u003E guid != null \u0026\u0026 selectedByGuid.TryGetValue( guid, out var a ) \u0026\u0026 outputs.Contains( Path.ChangeExtension( a.Path, \u0022.vmat\u0022 ) ) ? a : null;\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\t// Time per import stage, for the report: each mark records the time since the previous one.\n\t\tvar stageTimer = System.Diagnostics.Stopwatch.StartNew();\n\t\tvar stageTimes = new List\u003C(string Stage, long Milliseconds)\u003E();\n\t\tvoid Mark( string stage ) { stageTimes.Add( (stage, stageTimer.ElapsedMilliseconds) ); stageTimer.Restart(); }\n\t\tvar shaders = archive.Assets.Where( a =\u003E a.Path.EndsWith( \u0022.shader\u0022, StringComparison.OrdinalIgnoreCase ) || a.Path.EndsWith( \u0022.shadergraph\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 skies = new Dictionary\u003Cstring, ConvertedSky\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar conversions = 0;\n\t\tvar assignments = new List\u003Cobject\u003E();\n\t\tvar ruleCounts = new SortedDictionary\u003Cstring, int\u003E();\n\t\tvar idCounts = new SortedDictionary\u003Cstring, int\u003E();\n\t\tvar materialGroups = 0;\n\t\tvar hiddenMeshes = 0;\n\t\t// Imported models as prefabs and scenes place them, and the prefab/scene conversion results.\n\t\tvar prefabModels = new Dictionary\u003Cstring, PrefabModel\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar placements = new List\u003CUnityPrefabConverter.Summary\u003E();\n\t\tvar terrains = new List\u003Cobject\u003E();\n\t\tvar materialPaths = archive.Assets.Where( a =\u003E a.Kind == UnityAssetKind.Material ).ToDictionary( a =\u003E a.Guid, a =\u003E a.Path, StringComparer.OrdinalIgnoreCase );\n\t\tobject ReportEvidence( UnityModel.PrefabSlotEvidence e ) =\u003E new\n\t\t{\n\t\t\te.Prefab, e.Applied, Renderer = e.Id, e.IdSource, e.Mesh, e.Index,\n\t\t\tMaterial = materialPaths.GetValueOrDefault( e.Material ?? \u0022\u0022 ) ?? e.Material, e.Problem\n\t\t};\n\t\t// Audio files of one name in different formats compile to the same sound resource: one is kept (WAV, then OGG,\n\t\t// then MP3) and the others are skipped and reported, as is a sound whose name another format already uses.\n\t\tvar soundResources = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar skippedSounds = new HashSet\u003CUnityAsset\u003E();\n\t\tvar formats = new[] { \u0022.wav\u0022, \u0022.ogg\u0022, \u0022.mp3\u0022 };\n\t\tforeach ( var sound in selected.Where( a =\u003E a.Kind == UnityAssetKind.Sound ).OrderBy( a =\u003E Array.IndexOf( formats, Path.GetExtension( a.Path ).ToLowerInvariant() ) ) )\n\t\t{\n\t\t\tvar existing = formats.FirstOrDefault( extension =\u003E !Path.GetExtension( sound.Path ).Equals( extension, StringComparison.OrdinalIgnoreCase ) \u0026\u0026\n\t\t\t\tFile.Exists( Path.Combine( destination, Path.ChangeExtension( sound.Path, extension ) ) ) );\n\t\t\tif ( soundResources.Add( Path.ChangeExtension( sound.Path, \u0022.vsnd\u0022 ) ) \u0026\u0026 existing == null ) continue;\n\t\t\tskippedSounds.Add( sound );\n\t\t\tIssue( sound.Path, \u0022sound-name-conflict\u0022, existing != null\n\t\t\t\t? $\u0022The project already has \u0027{Path.ChangeExtension( sound.Path, existing )}\u0027, which compiles to the same sound resource; this file was not imported.\u0022\n\t\t\t\t: \u0022Another selected audio file of the same name compiles to the same sound resource; this file was not imported.\u0022 );\n\t\t}\n\t\t// Model files that would write the same VMDL (Rock.fbx and Rock.obj, or names that fold to the same ASCII name): one\n\t\t// keeps the VMDL (FBX first, then by path) and the others are reported; their source files are still imported.\n\t\tvar skippedModels = new HashSet\u003CUnityAsset\u003E();\n\t\tforeach ( var clash in selected.Where( a =\u003E plan.Find( a ).Vmdl ).GroupBy( a =\u003E ModelOutput( a.Path ), StringComparer.OrdinalIgnoreCase ).Where( g =\u003E g.Count() \u003E 1 ) )\n\t\t{\n\t\t\tvar kept = clash.OrderBy( a =\u003E Path.GetExtension( a.Path ).Equals( \u0022.fbx\u0022, StringComparison.OrdinalIgnoreCase ) ? 0 : 1 ).ThenBy( a =\u003E a.Path, StringComparer.Ordinal ).First();\n\t\t\tforeach ( var other in clash.Where( a =\u003E a != kept ) )\n\t\t\t{\n\t\t\t\tskippedModels.Add( other );\n\t\t\t\tIssue( other.Path, \u0022output-name-conflict\u0022, $\u0022\u0027{kept.Path}\u0027 writes the same model resource ({clash.Key}); this model\u0027s source is imported without a VMDL.\u0022 );\n\t\t\t}\n\t\t}\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\t// Model VMDLs that Unity colliders can put their physics shape in, as s\u0026box models carry their own collision.\n\t\t\t// Their text is rendered again with the shape before files are written.\n\t\t\tvar physicsModels = new Dictionary\u003Cstring, PhysicsModel\u003E( StringComparer.OrdinalIgnoreCase );\n\t\t\tvoid AddModel( string relative, Func\u003CMeshPart, bool, string\u003E text, MeshPart physics = null, bool convex = false )\n\t\t\t{\n\t\t\t\tvar document = text( physics, convex ); documents.Add( (Output( relative ), document) );\n\t\t\t\tphysicsModels[prefix \u002B relative] = new PhysicsModel { Document = documents.Count - 1, Text = text, Physics = physics, Convex = convex };\n\t\t\t}\n\t\t\t// A collider\u0027s shape in the model it uses: accepted unless that model already holds a different shape.\n\t\t\tbool EmbedPhysics( string model, MeshPart shape, bool convex )\n\t\t\t{\n\t\t\t\tif ( !physicsModels.TryGetValue( model, out var target ) ) return false;\n\t\t\t\tif ( target.Physics == null ) { target.Physics = shape; target.Convex = convex; return true; }\n\t\t\t\treturn target.Convex == convex \u0026\u0026 target.Physics.File == shape.File \u0026\u0026 target.Physics.Meshes.SequenceEqual( shape.Meshes );\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\t// Prefabs and scenes are converted after every model; their Unity YAML sources are not copied.\n\t\t\t\tif ( planned.Prefab || planned.Scene || planned.Terrain || skippedSounds.Contains( asset ) ) continue;\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 ( planned.Sound )\n\t\t\t\t{\n\t\t\t\t\t// Sound events reference the native compiled sound resource, not an absolute source path.\n\t\t\t\t\tdocuments.Add( (Output( Path.ChangeExtension( asset.Path, \u0022.sound\u0022 ) ), JsonSerializer.Serialize( new\n\t\t\t\t\t{\n\t\t\t\t\t\t__version = 1, UI = false, Volume = \u00221\u0022, Pitch = \u00221\u0022,\n\t\t\t\t\t\tSounds = new[] { prefix \u002B Path.ChangeExtension( asset.Path, \u0022.vsnd\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.Vtex )\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, Path.GetFileNameWithoutExtension( shader.Path ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Unity rounds non-power-of-two textures to the nearest power of two (nPOTScale) before use. s\u0026box\u0027s mip\n\t\t\t\t\t\t// generators for alpha-weighted color and for roughness reject other sizes, so material textures are resized too.\n\t\t\t\t\t\t(int Width, int Height) PowerOfTwo( UnityAsset texture )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( extractChannels == null ||\n\t\t\t\t\t\t\t\tPath.GetExtension( texture.Path ).ToLowerInvariant() is not (\u0022.png\u0022 or \u0022.jpg\u0022 or \u0022.jpeg\u0022 or \u0022.tga\u0022 or \u0022.psd\u0022 or \u0022.tif\u0022 or \u0022.tiff\u0022) ||\n\t\t\t\t\t\t\t\t(texture.MetadataDocument?.Find( \u0022nPOTScale\u0022 )?.Number ?? 1) == 0 || UnityImageSize.Read( texture.Source ) is not var (width, height) ||\n\t\t\t\t\t\t\t\tUnityImageSize.IsPowerOfTwo( width ) \u0026\u0026 UnityImageSize.IsPowerOfTwo( height ) ) return default;\n\t\t\t\t\t\t\treturn (UnityImageSize.NearestPowerOfTwo( width ), UnityImageSize.NearestPowerOfTwo( height ));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstring Derive( UnityAsset source, int channel, double scale, bool invert, string purpose )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar size = PowerOfTwo( source );\n\t\t\t\t\t\t\tvar key = $\u0022{source.Guid}_{channel}_{scale.ToString( System.Globalization.CultureInfo.InvariantCulture )}_{invert}_{size.Width}x{size.Height}\u0022;\n\t\t\t\t\t\t\tif ( !generatedChannels.TryGetValue( key, out var generated ) )\n\t\t\t\t\t\t\t{\n\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\tgenerated = derived.Reference;\n\t\t\t\t\t\t\t\tchannelRequests.Add( new( source.Source, derived.File, channel, scale, invert, size.Width, size.Height ) );\n\t\t\t\t\t\t\t\tgeneratedChannels[key] = generated;\n\t\t\t\t\t\t\t\tif ( channel == -1 ) information.Add( $\u0022{asset.Path}: resized \u0027{source.Path}\u0027 to {size.Width}x{size.Height}, as Unity does for non-power-of-two textures.\u0022 );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn generated;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( 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, material.MissingTexture( guid ) );\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\treturn Derive( source, channel, scale, invert, 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} );\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, material.MissingTexture( guid ) );\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstring ResolveVmat( string guid ) =\u003E\n\t\t\t\t\t\t\ttextureSources.TryGetValue( guid, out var source ) \u0026\u0026 PowerOfTwo( source ) != default ? Derive( source, -1, 1, false, \u0022pot\u0022 ) : Resolve( guid );\n\t\t\t\t\t\t// Normal maps Unity prepares on import: Flip Green Channel turns a DirectX-style map into Unity\u0027s OpenGL style,\n\t\t\t\t\t\t// which s\u0026box shares (measured in the engine), and Create from Grayscale makes a normal map from a height image.\n\t\t\t\t\t\tstring ResolveNormal( string guid )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( extractChannels == null || !textureSources.TryGetValue( guid, out var source ) ) return ResolveVmat( guid );\n\t\t\t\t\t\t\tvar settings = source.MetadataDocument;\n\t\t\t\t\t\t\tif ( (settings?.Find( \u0022convertToNormalMap\u0022 )?.Number ?? 0) == 1 )\n\t\t\t\t\t\t\t\treturn Derive( source, -4, settings.Find( \u0022heightScale\u0022 )?.Number ?? 0.25, (settings.Find( \u0022normalMapFilter\u0022 )?.Number ?? 0) == 1, \u0022normal\u0022 );\n\t\t\t\t\t\t\treturn (settings?.Find( \u0022flipGreenChannel\u0022 )?.Number ?? 0) == 1 ? Derive( source, -3, 1, false, \u0022normal\u0022 ) : ResolveVmat( guid );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( planned.Vmat \u0026\u0026 UnitySky.Detect( material ) is { } sky )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// Skybox materials become sky materials, which scenes\u0027 environments use.\n\t\t\t\t\t\t\tvar problem = sky.Problem;\n\t\t\t\t\t\t\tstring image = null;\n\t\t\t\t\t\t\tSkyFace[] faces = null;\n\t\t\t\t\t\t\tstatic bool Decodable( UnityAsset texture ) =\u003E Path.GetExtension( texture.Path ).ToLowerInvariant() is \u0022.png\u0022 or \u0022.jpg\u0022 or \u0022.jpeg\u0022 or \u0022.tga\u0022 or \u0022.psd\u0022 or \u0022.tif\u0022 or \u0022.tiff\u0022;\n\t\t\t\t\t\t\tif ( problem == null \u0026\u0026 sky.Type == UnitySky.Kind.Faces )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvar sources = sky.Faces.Select( g =\u003E textureSources.GetValueOrDefault( g ) ).ToArray();\n\t\t\t\t\t\t\t\tif ( sources.Any( t =\u003E t == null ) ) problem = material.MissingTexture( sky.Faces.First( g =\u003E !textureSources.ContainsKey( g ) ) );\n\t\t\t\t\t\t\t\telse if ( sources.FirstOrDefault( t =\u003E !Decodable( t ) ) is { } hdr ) problem = $\u0022The sky face \u0027{hdr.Path}\u0027 is not an image format the editor can compose.\u0022;\n\t\t\t\t\t\t\t\telse faces = sources.Select( t =\u003E new SkyFace( t.Source, 0, 0, 0, 0, false ) ).ToArray();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if ( problem == null \u0026\u0026 sky.Type == UnitySky.Kind.Panorama )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tif ( !textureSources.TryGetValue( sky.Texture, out var texture ) ) problem = material.MissingTexture( sky.Texture );\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvar size = UnityImageSize.Read( texture.Source );\n\t\t\t\t\t\t\t\t\tvar layout = sky.Panoramic \u0026\u0026 !sky.Frames ? UnitySky.Layout.LatLong : UnitySky.CubeLayout( size, (int)(texture.MetadataDocument?.Find( \u0022generateCubemap\u0022 )?.Number ?? 6), sky.Frames );\n\t\t\t\t\t\t\t\t\tif ( layout == null ) problem = $\u0022The sky texture \u0027{texture.Path}\u0027 uses a cube layout that is not converted (sphere maps and unrecognized aspect ratios).\u0022;\n\t\t\t\t\t\t\t\t\telse if ( layout == UnitySky.Layout.LatLong ) image = Resolve( sky.Texture );\n\t\t\t\t\t\t\t\t\telse if ( !Decodable( texture ) ) problem = $\u0022The sky texture \u0027{texture.Path}\u0027 is a cube layout in an image format the editor cannot compose.\u0022;\n\t\t\t\t\t\t\t\t\telse faces = UnitySky.Regions( layout.Value, texture.Source, size.Value.Width );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif ( problem == null \u0026\u0026 faces != null )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tif ( extractChannels == null ) problem = \u0022Composing a sky panorama from cube faces needs the editor.\u0022;\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvar derived = generatedFiles.Add( asset.Path, \u0022sky\u0022, \u0022.png\u0022 );\n\t\t\t\t\t\t\t\t\tchannelRequests.Add( new( faces[0].Source, derived.File, -2, 1, false, Faces: faces ) );\n\t\t\t\t\t\t\t\t\timage = derived.Reference;\n\t\t\t\t\t\t\t\t\tinformation.Add( $\u0022{asset.Path}: composed a sky panorama from the skybox\u0027s cube faces.\u0022 );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif ( problem != null ) Issue( asset.Path, \u0022sky-not-converted\u0022, $\u0022Skybox not converted; scenes show s\u0026box\u0027s default sky. {problem}\u0022 );\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tif ( sky.Type == UnitySky.Kind.Procedural )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t// A procedural sky has no image to convert; scenes use s\u0026box\u0027s standard day sky, which lights them.\n\t\t\t\t\t\t\t\t\tskies[asset.Guid] = new( UnitySky.DefaultSky, null, 0 );\n\t\t\t\t\t\t\t\t\tinformation.Add( $\u0022{asset.Path}: procedural skybox shown as s\u0026box\u0027s standard day sky, which lights the scene; Unity\u0027s computed sky colors and sun-following sky are not reproduced.\u0022 );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdocuments.Add( (Output( Path.ChangeExtension( asset.Path, \u0022.vmat\u0022 ) ), UnitySky.Vmat( image )) );\n\t\t\t\t\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t\t\t\t\t\tskies[asset.Guid] = new( prefix \u002B Path.ChangeExtension( asset.Path, \u0022.vmat\u0022 ), sky.Tint, sky.Yaw );\n\t\t\t\t\t\t\t\t\tinformation.Add( $\u0022{asset.Path}: skybox converted to an s\u0026box sky material.\u0022 );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( planned.Vmat ) { var vmatText = material.ToVmat( ResolveVmat, ResolveNormal ); documents.Add( (Output( Path.ChangeExtension( asset.Path, \u0022.vmat\u0022 ) ), vmatText) ); conversions\u002B\u002B; } // Converted before the output is reserved.\n\t\t\t\t\t\t\tconvertedMaterials[asset.Guid] = material;\n\t\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\t\twarnings.AddRange( material.Warnings.Select( w =\u003E $\u0022{asset.Path}: {w}\u0022 ) );\n\t\t\t\t\t\t\tinformation.AddRange( material.Information.Select( m =\u003E $\u0022{asset.Path}: {m}\u0022 ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcatch ( Exception ex ) when ( Skippable( ex ) ) { Issue( asset.Path, \u0022conversion-skipped\u0022, $\u0022Conversion skipped. {Describe( ex )}\u0022 ); }\n\t\t\t\t}\n\t\t\t\t// The mesh file a VMDL reads: the source, or a repaired or ASCII-named copy of it.\n\t\t\t\tstring MeshSource()\n\t\t\t\t{\n\t\t\t\t\tvar meshPath = prefix \u002B asset.Path;\n\t\t\t\t\t// The model compiler cannot open source paths with non-ASCII characters, so such models use an ASCII-named copy.\n\t\t\t\t\tvar nonAscii = asset.Path.Any( c =\u003E c \u003E 127 );\n\t\t\t\t\tvar derivedName = nonAscii ? AsciiPath( asset.Path ) : 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( derivedName, \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\telse if ( nonAscii )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar copy = generatedFiles.Add( derivedName, \u0022ascii\u0022, Path.GetExtension( asset.Path ).ToLowerInvariant() );\n\t\t\t\t\t\tFile.Copy( asset.Source, copy.File, true );\n\t\t\t\t\t\tmeshPath = copy.Reference;\n\t\t\t\t\t\tinformation.Add( $\u0022{asset.Path}: the model compiler cannot open paths with non-ASCII characters, so the VMDL uses an ASCII-named copy of the model.\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\treturn meshPath;\n\t\t\t\t}\n\t\t\t\t// The VMDL a Unity MeshCollider uses, per mesh node (null for the whole model) and shape kind. The shape goes in the\n\t\t\t\t// model that shows that mesh, as s\u0026box models carry their collision: the model itself when it shows only that mesh\n\t\t\t\t// (or the collider uses the whole model), else the mesh\u0027s single-mesh VMDL. A model that already holds a different\n\t\t\t\t// shape keeps it, and so does a mesh with no visible model; those colliders get a physics-only VMDL.\n\t\t\t\tFunc\u003Cstring, bool, string\u003E CollisionFactory( string meshPath, UnityModel info, double scale, string modelVmdl = null, Func\u003Cstring, string\u003E meshVmdl = null )\n\t\t\t\t{\n\t\t\t\t\tvar made = new Dictionary\u003C(string, bool), string\u003E();\n\t\t\t\t\tvar modelPath = AsciiPath( asset.Path );\n\t\t\t\t\treturn ( node, convex ) =\u003E\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( made.TryGetValue( (node, convex), out var existing ) ) return existing;\n\t\t\t\t\t\tif ( modelVmdl != null )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar whole = node == null || info.ImportedMeshes.SequenceEqual( new[] { node } );\n\t\t\t\t\t\t\tvar target = whole ? modelVmdl : meshVmdl( node );\n\t\t\t\t\t\t\tif ( EmbedPhysics( target, new MeshPart( meshPath, whole ? info.FilteredMeshes : new[] { node }, scale ), convex ) ) return made[(node, convex)] = target;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvar part = (node == null ? \u0022\u0022 : \u0022_\u0022 \u002B Regex.Replace( node.Trim(), \u0022[^A-Za-z0-9_-]\u002B\u0022, \u0022_\u0022 ).Trim( \u0027_\u0027 )) \u002B (convex ? \u0022_hull\u0022 : \u0022\u0022);\n\t\t\t\t\t\tvar relative = Path.ChangeExtension( modelPath, null ) \u002B \u0022_iup_collision\u0022 \u002B part \u002B \u0022.vmdl\u0022;\n\t\t\t\t\t\tfor ( var n = 2; outputs.Contains( relative ); n\u002B\u002B ) relative = Path.ChangeExtension( modelPath, null ) \u002B \u0022_iup_collision\u0022 \u002B part \u002B \u0022_\u0022 \u002B n \u002B \u0022.vmdl\u0022;\n\t\t\t\t\t\tvar meshes = node != null ? new[] { node } : info.FilteredMeshes;\n\t\t\t\t\t\tdocuments.Add( (Output( relative ), CollisionDocument( meshPath, meshes, scale, convex )) );\n\t\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t\t\treturn made[(node, convex)] = prefix \u002B relative;\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif ( planned.Vmdl \u0026\u0026 !skippedModels.Contains( asset ) )\n\t\t\t\t{\n\t\t\t\t\tvar meshPath = MeshSource();\n\t\t\t\t\tvar info = asset.ModelInfo;\n\t\t\t\t\tvar scale = ModelScale( asset, info );\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\t// Which evidence produced each remap; reported per slot so inferred matches stay visible.\n\t\t\t\t\tvar rules = remaps.Keys.ToDictionary( k =\u003E k, _ =\u003E \u0022slot-filename\u0022, StringComparer.OrdinalIgnoreCase );\n\t\t\t\t\tvoid Remap( string slot, string material, string rule ) { remaps[slot.ToLowerInvariant() \u002B \u0022.vmat\u0022] = material; rules[slot.ToLowerInvariant() \u002B \u0022.vmat\u0022] = rule; }\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 ) if ( remaps.TryAdd( $\u0022{meshName}_lod{lod}.vmat\u0022, matchingMaterial ) ) rules[$\u0022{meshName}_lod{lod}.vmat\u0022] = \u0022model-filename-lod\u0022;\n\t\t\t\t\t}\n\t\t\t\t\tvar prefabMaterials = info.PrefabMaterials.Distinct( StringComparer.OrdinalIgnoreCase ).Select( SelectedVmat ).Where( a =\u003E a != null ).OrderBy( a =\u003E Array.IndexOf( selected, a ) ).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 ) Remap( slot, prefix \u002B Path.ChangeExtension( semantic[0].Path, \u0022.vmat\u0022 ), \u0022prefab-material-name\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\tRemap( slot, prefix \u002B Path.ChangeExtension( candidates[0].Path, \u0022.vmat\u0022 ), \u0022albedo-texture\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\t// The model\u0027s own Unity import settings replace inferred matches; prefab slot evidence overrides them.\n\t\t\t\t\tforeach ( var (slot, source) in info.ImporterMaterials )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar assigned = SelectedVmat( source.Guid );\n\t\t\t\t\t\tif ( assigned != null ) Remap( slot, prefix \u002B Path.ChangeExtension( assigned.Path, \u0022.vmat\u0022 ), source.Rule );\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 = SelectedVmat( guid );\n\t\t\t\t\t\tif ( assigned != null ) Remap( slot, prefix \u002B Path.ChangeExtension( assigned.Path, \u0022.vmat\u0022 ), info.MostCommonSlots.Contains( slot ) ? \u0022prefab-slot-most-common\u0022 : \u0022prefab-slot\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 lodLevels = info.LodLevels;\n\t\t\t\t\tstring[] SlotsOf( string mesh ) =\u003E info.RendererSlots.Values.FirstOrDefault( b =\u003E b.Mesh == mesh )?.EffectiveSlots.ToArray() ?? Array.Empty\u003Cstring\u003E();\n\t\t\t\t\tstatic string LodKey( string mesh ) =\u003E Regex.Replace( mesh, \u0022(?i)LOD[0-9]\u0022, \u0022LOD\u0022 );\n\t\t\t\t\tvar defaultMaterial = prefabMaterials.Length == 1 \u0026\u0026 info.PrefabSlotMaterials.Count == 0 \u0026\u0026 info.ImporterMaterials.Count == 0 \u0026\u0026 info.AssignmentWarnings.Count == 0 ? prefix \u002B Path.ChangeExtension( prefabMaterials[0].Path, \u0022.vmat\u0022 ) : null;\n\t\t\t\t\tvar defaultRule = defaultMaterial == null ? null : \u0022prefab-single-material\u0022;\n\t\t\t\t\t// Embedded materials (materialLocation 1, or a prefab choosing the model\u0027s own) are generated by Unity from\n\t\t\t\t\t// the FBX rather than stored as .mat files. A neutral material stands in, and the cause is reported as such.\n\t\t\t\t\tvar embeddedSlots = info.RenderedMaterials.Where( s =\u003E !remaps.ContainsKey( s.ToLowerInvariant() \u002B \u0022.vmat\u0022 ) \u0026\u0026\n\t\t\t\t\t\t(info.MaterialImport == \u0022embedded\u0022 || info.EmbeddedSlots.Contains( s )) ).ToArray();\n\t\t\t\t\tif ( options.Materials \u0026\u0026 options.Vmat \u0026\u0026 defaultMaterial == null \u0026\u0026 embeddedSlots.Length \u003E 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar embeddedPath = generatedFiles.Add( asset.Path, \u0022embedded\u0022, \u0022.vmat\u0022 );\n\t\t\t\t\t\tvar standIn = new UnityMaterial();\n\t\t\t\t\t\tstandIn.Colors[\u0022_Color\u0022] = new[] { 0.5, 0.5, 0.5, 1.0 };\n\t\t\t\t\t\tFile.WriteAllText( embeddedPath.File, standIn.ToVmat( _ =\u003E null ) );\n\t\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t\t\tforeach ( var slot in embeddedSlots ) Remap( slot, embeddedPath.Reference, \u0022embedded-material\u0022 );\n\t\t\t\t\t\tvar textured = embeddedSlots.Where( info.MaterialAlbedoFiles.ContainsKey ).ToArray();\n\t\t\t\t\t\tIssue( asset.Path, \u0022embedded-material\u0022, $\u0022Unity generates the material for {string.Join( \u0022, \u0022, embeddedSlots.Select( s =\u003E $\u0022\u0027{s}\u0027\u0022 ) )} from the FBX; a neutral material stands in\u0022 \u002B\n\t\t\t\t\t\t\t(textured.Length \u003E 0 ? $\u0022 (the FBX names a texture for {string.Join( \u0022, \u0022, textured.Select( s =\u003E $\u0022\u0027{s}\u0027\u0022 ) )}, which is not applied).\u0022 : \u0022; the FBX provides no texture.\u0022) );\n\t\t\t\t\t}\n\t\t\t\t\tvar missingSlots = info.RenderedMaterials.Where( s =\u003E !remaps.ContainsKey( s.ToLowerInvariant() \u002B \u0022.vmat\u0022 ) ).ToArray();\n\t\t\t\t\t// Slots without any material evidence get a neutral s\u0026box standard material instead of a missing one.\n\t\t\t\t\tvar noSlots = info.RendererSlots.Count == 0 \u0026\u0026 info.Materials.Count == 0;\n\t\t\t\t\tif ( options.Materials \u0026\u0026 options.Vmat \u0026\u0026 defaultMaterial == null \u0026\u0026 (noSlots || missingSlots.Length \u003E 0) \u0026\u0026\n\t\t\t\t\t\tPath.GetExtension( asset.Path ).ToLowerInvariant() is \u0022.fbx\u0022 or \u0022.obj\u0022 )\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\tif ( noSlots ) (defaultMaterial, defaultRule) = (neutralPath.Reference, \u0022neutral-fallback\u0022);\n\t\t\t\t\t\telse foreach ( var slot in missingSlots ) Remap( slot, neutralPath.Reference, \u0022neutral-fallback\u0022 );\n\t\t\t\t\t\tIssue( asset.Path, \u0022unassigned-material\u0022, info.Materials.Count == 0\n\t\t\t\t\t\t\t? \u0022The model contains no named material slots and no unambiguous prefab assignment was resolved; using a neutral material.\u0022\n\t\t\t\t\t\t\t: $\u0022No material was found for {string.Join( \u0022, \u0022, missingSlots.Select( s =\u003E $\u0022\u0027{s}\u0027\u0022 ) )}; a neutral standard material stands in\u0022 \u002B\n\t\t\t\t\t\t\t\t(info.MaterialImport == \u0022none\u0022 ? \u0022, as Unity shows its default material for models that import no materials.\u0022 : \u0022.\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\t// Lower LOD meshes repeat their LOD0 mesh\u0027s materials, as their Unity renderers almost always do: a slot without\n\t\t\t\t\t// its own assignment takes the material of the LOD0 counterpart\u0027s slot in the same position.\n\t\t\t\t\tforeach ( var mesh in lodLevels.Skip( 1 ).SelectMany( l =\u003E l ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar top = lodLevels[0].Length == 1 ? lodLevels[0][0] : lodLevels[0].FirstOrDefault( m =\u003E LodKey( m ).Equals( LodKey( mesh ), StringComparison.OrdinalIgnoreCase ) );\n\t\t\t\t\t\tif ( top == null ) continue;\n\t\t\t\t\t\tvar (own, source) = (SlotsOf( mesh ), SlotsOf( top ));\n\t\t\t\t\t\tfor ( var s = 0; s \u003C own.Length \u0026\u0026 s \u003C source.Length; s\u002B\u002B )\n\t\t\t\t\t\t\tif ( !remaps.ContainsKey( own[s].ToLowerInvariant() \u002B \u0022.vmat\u0022 ) \u0026\u0026 remaps.TryGetValue( source[s].ToLowerInvariant() \u002B \u0022.vmat\u0022, out var lodMaterial ) )\n\t\t\t\t\t\t\t\tRemap( own[s], lodMaterial, \u0022lod0-slot\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\t// Slots of lower LOD meshes that the model file does not name fall back to LOD0\u0027s material when it has only one.\n\t\t\t\t\t// The global default replaces every material, so it is not used when a lower level has a material of its own\n\t\t\t\t\t// (an impostor level\u0027s atlas).\n\t\t\t\t\tif ( defaultMaterial == null \u0026\u0026 lodLevels.Count \u003E 1 \u0026\u0026\n\t\t\t\t\t\tlodLevels[0].SelectMany( SlotsOf ).Select( s =\u003E remaps.GetValueOrDefault( s.ToLowerInvariant() \u002B \u0022.vmat\u0022 ) ).Distinct( StringComparer.OrdinalIgnoreCase ).ToArray() is [string lodDefault] \u0026\u0026\n\t\t\t\t\t\tlodLevels.Skip( 1 ).SelectMany( l =\u003E l ).SelectMany( SlotsOf ).All( s =\u003E !remaps.TryGetValue( s.ToLowerInvariant() \u002B \u0022.vmat\u0022, out var own ) || own.Equals( lodDefault, StringComparison.OrdinalIgnoreCase ) ) )\n\t\t\t\t\t\t(defaultMaterial, defaultRule) = (lodDefault, \u0022lod0-material\u0022);\n\t\t\t\t\t// Each other Unity prefab variant becomes a ModelDoc material group. A group replaces the default group\u0027s\n\t\t\t\t\t// resolved materials, so slots that share one default material change together; such cases are noted.\n\t\t\t\t\tstring VariantMaterial( string guid ) =\u003E SelectedVmat( guid ) is UnityAsset variantAsset ? prefix \u002B Path.ChangeExtension( variantAsset.Path, \u0022.vmat\u0022 ) : null;\n\t\t\t\t\tstring DefaultOf( string slot ) =\u003E remaps.GetValueOrDefault( slot.ToLowerInvariant() \u002B \u0022.vmat\u0022 ) ?? defaultMaterial;\n\t\t\t\t\tvar groups = new List\u003CVariantGroup\u003E();\n\t\t\t\t\tvar groupNames = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase ) { \u0022default\u0022 };\n\t\t\t\t\tforeach ( var variant in info.PrefabVariants )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar groupRemaps = new Dictionary\u003Cstring, string\u003E( StringComparer.OrdinalIgnoreCase );\n\t\t\t\t\t\tvar notes = new List\u003Cstring\u003E();\n\t\t\t\t\t\tforeach ( var (slot, guid) in variant.Slots )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar from = DefaultOf( slot );\n\t\t\t\t\t\t\tvar to = VariantMaterial( guid );\n\t\t\t\t\t\t\tif ( from == null || to == null ) { notes.Add( $\u0022Slot \u0027{slot}\u0027 has no {(from == null ? \u0022default\u0022 : \u0022imported variant\u0022)} material.\u0022 ); continue; }\n\t\t\t\t\t\t\tif ( from.Equals( to, StringComparison.OrdinalIgnoreCase ) ) continue;\n\t\t\t\t\t\t\tif ( groupRemaps.TryGetValue( from, out var kept ) \u0026\u0026 !kept.Equals( to, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tnotes.Add( $\u0022Slot \u0027{slot}\u0027 shares its default material with another slot that this variant changes differently; kept {kept}.\u0022 );\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tgroupRemaps[from] = to;\n\t\t\t\t\t\t\tforeach ( var other in info.RenderedMaterials.Where( s =\u003E !variant.Slots.ContainsKey( s ) \u0026\u0026 from.Equals( DefaultOf( s ), StringComparison.OrdinalIgnoreCase ) ) )\n\t\t\t\t\t\t\t\tnotes.Add( $\u0022Slot \u0027{other}\u0027 shares the default material of \u0027{slot}\u0027, so it also changes in this group.\u0022 );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( groupRemaps.Count == 0 ) continue; // Same as the default group.\n\t\t\t\t\t\tvar same = groups.FirstOrDefault( g =\u003E g.Remaps.Count == groupRemaps.Count \u0026\u0026 g.Remaps.All( r =\u003E groupRemaps.TryGetValue( r.Key, out var m ) \u0026\u0026 m.Equals( r.Value, StringComparison.OrdinalIgnoreCase ) ) );\n\t\t\t\t\t\tif ( same != null ) { same.Prefabs.AddRange( variant.Prefabs ); continue; }\n\t\t\t\t\t\tvar baseName = Regex.Replace( Path.GetFileNameWithoutExtension( variant.Prefabs[0] ), \u0022[^A-Za-z0-9_-]\u002B\u0022, \u0022_\u0022 );\n\t\t\t\t\t\tvar name = baseName;\n\t\t\t\t\t\tfor ( var n = 2; !groupNames.Add( name ); n\u002B\u002B ) name = $\u0022{baseName}_{n}\u0022;\n\t\t\t\t\t\tgroups.Add( new( name, variant.Prefabs.ToList(), groupRemaps, notes ) );\n\t\t\t\t\t}\n\t\t\t\t\tmaterialGroups \u002B= groups.Count;\n\t\t\t\t\tinformation.AddRange( info.AssignmentNotes.Select( n =\u003E $\u0022{asset.Path}: {n}\u0022 ) );\n\t\t\t\t\tif ( info.HiddenMeshes.Count \u003E 0 )\n\t\t\t\t\t\tinformation.Add( $\u0022{asset.Path}: left out {info.HiddenMeshes.Count} mesh(es) that no Unity prefab or scene places: {string.Join( \u0022, \u0022, info.HiddenMeshes.Order( StringComparer.Ordinal ) )}.\u0022 );\n\t\t\t\t\thiddenMeshes \u002B= info.HiddenMeshes.Count;\n\t\t\t\t\t// Neutral fallbacks are reported without their generated placeholder path.\n\t\t\t\t\tstring Reported( string material, string rule ) =\u003E rule == \u0022neutral-fallback\u0022 || material?.StartsWith( \u0022iup:generated:\u0022, StringComparison.Ordinal ) == true ? null : material;\n\t\t\t\t\tstring Shown( string material ) =\u003E material.StartsWith( \u0022iup:generated:\u0022, StringComparison.Ordinal ) ? \u0022(neutral fallback)\u0022 : material;\n\t\t\t\t\tvar slotRows = info.RenderedMaterials.Concat( info.PrefabSlotMaterials.Keys ).Concat( info.UnresolvedPrefabSlots ).Distinct( StringComparer.OrdinalIgnoreCase ).Select( slot =\u003E\n\t\t\t\t\t{\n\t\t\t\t\t\tvar key = slot.ToLowerInvariant() \u002B \u0022.vmat\u0022;\n\t\t\t\t\t\tvar rule = remaps.ContainsKey( key ) ? rules.GetValueOrDefault( key, \u0022slot-filename\u0022 ) : defaultRule ?? \u0022unresolved\u0022;\n\t\t\t\t\t\treturn new\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSlot = slot, Rule = rule, Material = Reported( remaps.GetValueOrDefault( key ) ?? defaultMaterial, rule ),\n\t\t\t\t\t\t\tEvidence = info.PrefabEvidence.Where( e =\u003E string.Equals( e.Slot, slot, StringComparison.OrdinalIgnoreCase ) ).Select( ReportEvidence ).ToArray()\n\t\t\t\t\t\t};\n\t\t\t\t\t} ).ToArray();\n\t\t\t\t\tassignments.Add( new\n\t\t\t\t\t{\n\t\t\t\t\t\tModel = asset.Path,\n\t\t\t\t\t\tinfo.MaterialImport,\n\t\t\t\t\t\tDefault = defaultRule == null ? null : new { Material = Reported( defaultMaterial, defaultRule ), Rule = defaultRule },\n\t\t\t\t\t\tSlots = slotRows,\n\t\t\t\t\t\tMaterialGroups = groups.Select( g =\u003E new { g.Name, g.Prefabs, Remaps = g.Remaps.Select( r =\u003E new { From = Shown( r.Key ), To = r.Value } ).ToArray(), g.Notes } ).ToArray(),\n\t\t\t\t\t\tNotes = info.AssignmentNotes.ToArray(),\n\t\t\t\t\t\tHiddenMeshes = info.HiddenMeshes.Order( StringComparer.Ordinal ).ToArray(),\n\t\t\t\t\t\t// Prefab references that could not be tied to an imported slot.\n\t\t\t\t\t\tUnmatched = info.PrefabEvidence.Where( e =\u003E e.Problem != null ).Select( ReportEvidence ).ToArray()\n\t\t\t\t\t} );\n\t\t\t\t\tforeach ( var rule in slotRows.Length \u003E 0 ? slotRows.Select( r =\u003E r.Rule ) : new[] { defaultRule ?? \u0022no-slot-information\u0022 } )\n\t\t\t\t\t\truleCounts[rule] = ruleCounts.GetValueOrDefault( rule ) \u002B 1;\n\t\t\t\t\tforeach ( var evidence in info.PrefabEvidence.Where( e =\u003E e.Applied ) )\n\t\t\t\t\t\tidCounts[evidence.Problem ?? evidence.IdSource] = idCounts.GetValueOrDefault( evidence.Problem ?? evidence.IdSource ) \u002B 1;\n\t\t\t\t\t// Unity\u0027s \u0022Generate Colliders\u0022 gives every mesh an exact collider, so the VMDL gets the matching physics mesh.\n\t\t\t\t\tvar lodThresholds = info.LodThresholds( asset.MetadataDocument, scale );\n\t\t\t\t\t// Only the model\u0027s own slots: its material names, renderer slots and mesh names (a slotless mesh\u0027s slot is its\n\t\t\t\t\t// name). Remaps of other slots would only bloat the VMDL (every material of the package).\n\t\t\t\t\tvar ownSlots = info.Materials.Concat( info.Meshes ).Concat( info.RendererSlots.Values.SelectMany( b =\u003E b.Slots ) ).Concat( info.PrefabSlotMaterials.Keys )\n\t\t\t\t\t\t.Select( n =\u003E n.ToLowerInvariant() \u002B \u0022.vmat\u0022 ).ToHashSet( StringComparer.OrdinalIgnoreCase );\n\t\t\t\t\tforeach ( var other in remaps.Keys.Where( k =\u003E !ownSlots.Contains( k ) ).ToArray() ) remaps.Remove( other );\n\t\t\t\t\tAddModel( ModelOutput( asset.Path ), ( shape, convex ) =\u003E ModelDocument( meshPath, remaps, defaultMaterial, groups, info.FilteredMeshes, scale, shape, convex, info.LodLevels, lodThresholds ),\n\t\t\t\t\t\tinfo.GenerateColliders ? new MeshPart( meshPath, info.FilteredMeshes, scale ) : null );\n\t\t\t\t\tif ( info.LodLevels.Count \u003E 1 ) information.Add( $\u0022{asset.Path}: {info.LodLevels.Count} LOD levels from the mesh names go into the VMDL\u0027s LOD group\u0022 \u002B\n\t\t\t\t\t\t(info.LodSize( scale ) == null ? \u0022; the levels\u0027 bounds could not be read, so the switch distances assume a 1 m model.\u0022 : \u0022.\u0022) );\n\t\t\t\t\tif ( info.GenerateColliders ) information.Add( $\u0022{asset.Path}: Unity generates colliders for this model, so its VMDL has a physics mesh.\u0022 );\n\t\t\t\t\t// Renderers that show one mesh of a multi-mesh model get a VMDL holding only that mesh.\n\t\t\t\t\tvar meshModels = new Dictionary\u003Cstring, string\u003E( StringComparer.Ordinal );\n\t\t\t\t\tvar (modelPath, modelMesh, modelRemaps, modelDefault, modelScale) = (AsciiPath( asset.Path ), meshPath, remaps, defaultMaterial, scale);\n\t\t\t\t\tvar modelGroups = groups;\n\t\t\t\t\tstring MeshVmdl( string node )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( meshModels.TryGetValue( node, out var existing ) ) return existing;\n\t\t\t\t\t\tvar relative = Path.ChangeExtension( modelPath, null ) \u002B \u0022_iup_mesh_\u0022 \u002B Regex.Replace( node.Trim(), \u0022[^A-Za-z0-9_-]\u002B\u0022, \u0022_\u0022 ).Trim( \u0027_\u0027 ) \u002B \u0022.vmdl\u0022;\n\t\t\t\t\t\tfor ( var n = 2; outputs.Contains( relative ); n\u002B\u002B ) relative = Path.ChangeExtension( modelPath, null ) \u002B \u0022_iup_mesh_\u0022 \u002B Regex.Replace( node.Trim(), \u0022[^A-Za-z0-9_-]\u002B\u0022, \u0022_\u0022 ).Trim( \u0027_\u0027 ) \u002B \u0022_\u0022 \u002B n \u002B \u0022.vmdl\u0022;\n\t\t\t\t\t\tAddModel( relative, ( shape, convex ) =\u003E ModelDocument( modelMesh, modelRemaps, modelDefault, modelGroups, new[] { node }, modelScale, shape, convex ) );\n\t\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t\t\treturn meshModels[node] = prefix \u002B relative;\n\t\t\t\t\t}\n\t\t\t\t\tprefabModels[asset.Guid] = new PrefabModel\n\t\t\t\t\t{\n\t\t\t\t\t\tVmdl = prefix \u002B ModelOutput( asset.Path ), Info = info, Scale = scale, Name = Path.GetFileNameWithoutExtension( asset.Path ), MeshFile = meshPath,\n\t\t\t\t\t\tMeshVmdl = MeshVmdl,\n\t\t\t\t\t\tGroup = slots =\u003E ChooseGroup( slots, DefaultOf, VariantMaterial, modelGroups ),\n\t\t\t\t\t\tCollisionVmdl = CollisionFactory( meshPath, info, scale, prefix \u002B ModelOutput( asset.Path ), MeshVmdl )\n\t\t\t\t\t};\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\telse if ( asset.Kind == UnityAssetKind.Model \u0026\u0026 asset.ModelInfo.CollisionOnly \u0026\u0026 selected.Any( a =\u003E plan.Find( a ) is { Prefab: true } or { Scene: true } ) )\n\t\t\t\t{\n\t\t\t\t\t// A mesh used only by Unity MeshColliders has no visible VMDL; prefabs and scenes get collision VMDLs from it.\n\t\t\t\t\tvar info = asset.ModelInfo;\n\t\t\t\t\tvar scale = ModelScale( asset, info );\n\t\t\t\t\tprefabModels[asset.Guid] = new PrefabModel\n\t\t\t\t\t{\n\t\t\t\t\t\tInfo = info, Scale = scale, Name = Path.GetFileNameWithoutExtension( asset.Path ), Group = _ =\u003E (\u0022\u0022, true),\n\t\t\t\t\t\tCollisionVmdl = CollisionFactory( MeshSource(), info, scale )\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\tMark( \u0022Assets (copies, sounds, materials, models)\u0022 );\n\t\t\t// Unity blends terrain layers by height only when the placing terrain\u0027s material enables it.\n\t\t\tbool HeightBlend( UnityAsset terrain )\n\t\t\t{\n\t\t\t\tforeach ( var placement in selected.Where( a =\u003E plan.Find( a ) is { Prefab: true } or { Scene: true } ) )\n\t\t\t\t{\n\t\t\t\t\tstring text;\n\t\t\t\t\ttry { text = ReadPlacement( placement.Source ); }\n\t\t\t\t\tcatch ( InvalidDataException ) { continue; }\n\t\t\t\t\tif ( !text.Contains( terrain.Guid, StringComparison.OrdinalIgnoreCase ) ) continue;\n\t\t\t\t\tforeach ( var component in UnityYaml.Parse( text ).Where( o =\u003E o.ClassId == 218 \u0026\u0026 !o.Stripped ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( !string.Equals( component.Body[\u0022m_TerrainData\u0022]?.Reference.Guid, terrain.Guid, StringComparison.OrdinalIgnoreCase ) ) continue;\n\t\t\t\t\t\tvar template = component.Body[\u0022m_MaterialTemplate\u0022]?.Reference ?? default;\n\t\t\t\t\t\tif ( template.HasGuid \u0026\u0026 assetsByGuid.GetValueOrDefault( template.Guid ) is { Kind: UnityAssetKind.Material } material )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttry { return UnityMaterial.Parse( ReadText( material.Source ) ).Number( \u0022_EnableHeightBlend\u0022, 0 ) != 0; }\n\t\t\t\t\t\t\tcatch ( InvalidDataException ) { }\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t// Terrains reference the terrain layers\u0027 TMATs; scenes and prefabs place them.\n\t\t\tvar prefabTerrains = new Dictionary\u003Cstring, PrefabTerrain\u003E( StringComparer.OrdinalIgnoreCase );\n\t\t\tforeach ( var asset in selected.Where( a =\u003E plan.Find( a ).Terrain ) )\n\t\t\t{\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\toperation = \u0022converting terrains\u0022;\n\t\t\t\tcurrentAsset = asset.Path;\n\t\t\t\ttry\n\t\t\t\t{\n\t\t\t\t\tvar data = UnityTerrainData.Read( asset.Source );\n\t\t\t\t\tvar storage = data.ToStorage();\n\t\t\t\t\t// Layers keep Unity\u0027s order, so painted layer indices match; a layer without a TMAT paints nothing.\n\t\t\t\t\tvar materials = data.Layers.Select( l =\u003E l.HasGuid \u0026\u0026 selectedByGuid.GetValueOrDefault( l.Guid ) is { } layer \u0026\u0026 plan.Find( layer ).Tmat\n\t\t\t\t\t\t? prefix \u002B Path.ChangeExtension( layer.Path, \u0022.tmat\u0022 ) : null ).ToList();\n\t\t\t\t\tif ( materials.Any( m =\u003E m == null ) ) warnings.Add( $\u0022{asset.Path}: {materials.Count( m =\u003E m == null )} terrain layer(s) have no imported terrain material; those areas are unpainted.\u0022 );\n\t\t\t\t\tvar (json, blob) = UnityTerrainData.TerrainFile( storage, materials, HeightBlend( asset ) );\n\t\t\t\t\tvar output = PlacementOutput( asset.Path, \u0022.terrain\u0022 );\n\t\t\t\t\tdocuments.Add( (Output( output ), json) );\n\t\t\t\t\tFile.WriteAllBytes( Output( output \u002B \u0022_d\u0022 ), blob );\n\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t\tdata.ReleaseMaps();\n\t\t\t\t\tprefabTerrains[asset.Guid] = new PrefabTerrain { Path = prefix \u002B output, Size = storage.Size, Height = storage.Height, Data = data };\n\t\t\t\t\tterrains.Add( new { Source = asset.Path, storage.Resolution, Size = Math.Round( storage.Size, 1 ), Height = Math.Round( storage.Height, 1 ), Layers = data.Layers.Count, Trees = data.Trees.Count, TreePrototypes = data.TreePrototypes.Count,\n\t\t\t\t\t\tDetailLayersNotConverted = data.DetailPrototypes, Notes = data.Notes.Distinct().ToArray() } );\n\t\t\t\t\tinformation.Add( $\u0022{asset.Path}: {storage.Resolution}x{storage.Resolution} terrain with {data.Layers.Count} layers and {data.Trees.Count} trees\u0022 \u002B\n\t\t\t\t\t\t(data.DetailPrototypes \u003E 0 ? $\u0022; its {data.DetailPrototypes} grass and detail layers are not converted.\u0022 : \u0022.\u0022) );\n\t\t\t\t\twarnings.AddRange( data.Notes.Distinct().Select( n =\u003E $\u0022{asset.Path}: {n}\u0022 ) );\n\t\t\t\t}\n\t\t\t\tcatch ( Exception ex ) when ( Skippable( ex ) ) { Issue( asset.Path, \u0022conversion-skipped\u0022, $\u0022Conversion skipped. {Describe( ex )}\u0022 ); }\n\t\t\t}\n\t\t\tMark( \u0022Terrains\u0022 );\n\t\t\t// Meshes the importer writes itself: Unity\u0027s built-in primitives and meshes stored in prefabs, scenes and .asset files.\n\t\t\tvar meshObjs = new Dictionary\u003C(string, long), GeneratedMesh\u003E();\n\t\t\tvar meshFiles = new Dictionary\u003Cstring, IReadOnlyList\u003CUnityYamlObject\u003E\u003E( StringComparer.OrdinalIgnoreCase );\n\t\t\t// Built-in meshes go in the package\u0027s own folder (the deepest folder holding all its prefabs and scenes, whatever is\n\t\t\t// selected), so a package\u0027s import stays under its folder. Every package\u0027s copy is identical.\n\t\t\tvar primitivesFolder = PackageFolder( archive.Assets.Where( a =\u003E a.Kind is UnityAssetKind.Prefab or UnityAssetKind.Scene ).Select( a =\u003E a.Path ) ) \u002B \u0022Unity Primitives/\u0022;\n\t\t\tGeneratedMesh MeshObj( string guid, long fileId )\n\t\t\t{\n\t\t\t\tif ( meshObjs.TryGetValue( (guid, fileId), out var known ) ) return known;\n\t\t\t\tvar owner = assetsByGuid.GetValueOrDefault( guid );\n\t\t\t\tGeneratedMesh result;\n\t\t\t\ttry\n\t\t\t\t{\n\t\t\t\t\tUnityMesh mesh;\n\t\t\t\t\tstring relative;\n\t\t\t\t\tif ( guid.Equals( UnityMesh.BuiltinGuid, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( !UnityMesh.Primitives.TryGetValue( fileId, out var primitive ) ) throw new InvalidDataException( $\u0022Unity\u0027s built-in mesh {fileId} is not one of its primitives.\u0022 );\n\t\t\t\t\t\tmesh = UnityMesh.Primitive( primitive );\n\t\t\t\t\t\trelative = primitivesFolder \u002B primitive \u002B \u0022.obj\u0022;\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( owner == null ) throw new InvalidDataException( \u0022its file is not in the package.\u0022 );\n\t\t\t\t\t\tobject body;\n\t\t\t\t\t\tif ( UnitySerializedFile.Is( owner.Source ) )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar file = UnitySerializedFile.Read( owner.Source );\n\t\t\t\t\t\t\tvar info = file.Find( fileId );\n\t\t\t\t\t\t\tif ( info == null || info.ClassId != 43 ) throw new InvalidDataException( \u0022the file holds no such mesh.\u0022 );\n\t\t\t\t\t\t\tbody = file.ReadObject( info );\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( !meshFiles.TryGetValue( guid, out var objects ) ) meshFiles[guid] = objects = UnityYaml.Parse( ReadPlacement( owner.Source ) );\n\t\t\t\t\t\t\tbody = objects.FirstOrDefault( o =\u003E o.FileId == fileId \u0026\u0026 o.ClassId == 43 \u0026\u0026 !o.Stripped )?.Body ?? throw new InvalidDataException( \u0022the file holds no such mesh.\u0022 );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmesh = UnityMesh.Read( body );\n\t\t\t\t\t\tvar stem = Path.ChangeExtension( owner.Path, null );\n\t\t\t\t\t\tvar meshName = Regex.Replace( mesh.Name ?? \u0022Mesh\u0022, \u0022[^A-Za-z0-9_-]\u002B\u0022, \u0022_\u0022 ).Trim( \u0027_\u0027 );\n\t\t\t\t\t\trelative = owner.Path.EndsWith( \u0022.asset\u0022, StringComparison.OrdinalIgnoreCase ) ? stem \u002B \u0022_iup_mesh.obj\u0022 : stem \u002B \u0022_iup_mesh_\u0022 \u002B (meshName.Length \u003E 0 ? meshName : \u0022Mesh\u0022) \u002B \u0022.obj\u0022;\n\t\t\t\t\t\tif ( meshObjs.Values.Any( m =\u003E m.Relative != null \u0026\u0026 m.Relative.Equals( AsciiPath( relative ), StringComparison.OrdinalIgnoreCase ) ) )\n\t\t\t\t\t\t\trelative = Path.ChangeExtension( relative, null ) \u002B \u0022_\u0022 \u002B (fileId \u003C 0 ? \u0022n\u0022 \u002B (-fileId) : fileId.ToString( System.Globalization.CultureInfo.InvariantCulture )) \u002B \u0022.obj\u0022;\n\t\t\t\t\t}\n\t\t\t\t\trelative = AsciiPath( relative );\n\t\t\t\t\tif ( !outputs.Contains( relative ) ) { var obj = mesh.ToObj(); File.WriteAllText( Output( relative ), obj ); conversions\u002B\u002B; }\n\t\t\t\t\tresult = new( relative, prefix \u002B relative, mesh.Submeshes.Count, null );\n\t\t\t\t}\n\t\t\t\tcatch ( Exception ex ) when ( Skippable( ex ) ) { result = new( null, null, 0, $\u0022A mesh in \u0027{owner?.Path ?? guid}\u0027 could not be read: {Describe( ex )}\u0022 ); }\n\t\t\t\treturn meshObjs[(guid, fileId)] = result;\n\t\t\t}\n\t\t\t// Composed VMDLs (generated meshes, LOD groups, collision); identical documents are written once. They use no\n\t\t\t// global default material: ModelDoc\u0027s global default replaces every material, remapped or not (measured).\n\t\t\tvar composedModels = new Dictionary\u003Cstring, string\u003E( StringComparer.Ordinal );\n\t\t\tstring Compose( ComposedModel model )\n\t\t\t{\n\t\t\t\tvar text = model.Lods.Count == 0 \u0026\u0026 model.Physics != null ? CollisionDocument( model.Physics.File, model.Physics.Meshes, model.Physics.Scale, model.Convex )\n\t\t\t\t\t: ComposedDocument( model.Remaps, null, null, model.Lods, model.Thresholds, model.Physics, model.Convex );\n\t\t\t\tif ( composedModels.TryGetValue( text, out var existing ) ) return existing;\n\t\t\t\tvar name = AsciiPath( model.Name );\n\t\t\t\tvar relative = name \u002B \u0022.vmdl\u0022;\n\t\t\t\tfor ( var n = 2; outputs.Contains( relative ) || assetPaths.Contains( relative ); n\u002B\u002B ) relative = $\u0022{name}_{n}.vmdl\u0022;\n\t\t\t\t// Render models can take a collider\u0027s shape later; physics-only models are complete.\n\t\t\t\tif ( model.Lods.Count \u003E 0 )\n\t\t\t\t\tAddModel( relative, ( shape, convex ) =\u003E ComposedDocument( model.Remaps, null, null, model.Lods, model.Thresholds, shape, convex ), model.Physics, model.Convex );\n\t\t\t\telse documents.Add( (Output( relative ), text) );\n\t\t\t\tconversions\u002B\u002B;\n\t\t\t\treturn composedModels[text] = prefix \u002B relative;\n\t\t\t}\n\t\t\t// Prefabs and scenes reference the models converted above.\n\t\t\tvar converter = new UnityPrefabConverter( archive.Assets.ToDictionary( a =\u003E a.Guid, StringComparer.OrdinalIgnoreCase ), prefabModels,\n\t\t\t\tselected.Where( a =\u003E plan.Find( a ).Prefab ).ToDictionary( a =\u003E a.Guid, a =\u003E prefix \u002B PlacementOutput( a.Path, \u0022.prefab\u0022 ), StringComparer.OrdinalIgnoreCase ),\n\t\t\t\tguid =\u003E UnityYaml.Parse( ReadPlacement( archive.Assets.First( a =\u003E a.Guid.Equals( guid, StringComparison.OrdinalIgnoreCase ) ).Source ) ) )\n\t\t\t{\n\t\t\t\tTerrains = prefabTerrains,\n\t\t\t\tMaterials = selected.Where( a =\u003E plan.Find( a ).Vmat ).ToDictionary( a =\u003E a.Guid, a =\u003E prefix \u002B Path.ChangeExtension( a.Path, \u0022.vmat\u0022 ), StringComparer.OrdinalIgnoreCase ),\n\t\t\t\tMeshObj = MeshObj, Compose = Compose, Skies = skies, EmbedPhysics = EmbedPhysics,\n\t\t\t\tSounds = selected.Where( a =\u003E plan.Find( a ).Sound \u0026\u0026 !skippedSounds.Contains( a ) ).ToDictionary( a =\u003E a.Guid, a =\u003E prefix \u002B Path.ChangeExtension( a.Path, \u0022.sound\u0022 ), StringComparer.OrdinalIgnoreCase )\n\t\t\t};\n\t\t\tforeach ( var asset in selected.Where( a =\u003E plan.Find( a ) is { Prefab: true } or { Scene: true } ) )\n\t\t\t{\n\t\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\t\toperation = \u0022converting prefabs and scenes\u0022;\n\t\t\t\tcurrentAsset = asset.Path;\n\t\t\t\tvar summary = new UnityPrefabConverter.Summary { Source = asset.Path };\n\t\t\t\ttry\n\t\t\t\t{\n\t\t\t\t\t// Convert before reserving the output, so a skipped conversion leaves no unwritten file behind.\n\t\t\t\t\tvar prefabFile = plan.Find( asset ).Prefab;\n\t\t\t\t\tvar text = prefabFile ? converter.ConvertPrefab( asset, summary ) : converter.ConvertScene( asset, summary, Path.GetFileNameWithoutExtension( asset.Path ) );\n\t\t\t\t\tdocuments.Add( (Output( PlacementOutput( asset.Path, prefabFile ? \u0022.prefab\u0022 : \u0022.scene\u0022 ) ), text) );\n\t\t\t\t\tconversions\u002B\u002B;\n\t\t\t\t\tplacements.Add( summary );\n\t\t\t\t\twarnings.AddRange( summary.Notes.Distinct().Select( n =\u003E $\u0022{asset.Path}: {n}\u0022 ) );\n\t\t\t\t}\n\t\t\t\tcatch ( Exception ex ) when ( Skippable( ex ) ) { Issue( asset.Path, \u0022conversion-skipped\u0022, $\u0022Conversion skipped. {Describe( ex )}\u0022 ); }\n\t\t\t}\n\t\t\tif ( placements.Count \u003E 0 )\n\t\t\t\tinformation.Add( $\u0022{placements.Count} prefab(s) and scene(s) converted: {placements.Sum( p =\u003E p.Renderers )} renderers, {placements.Sum( p =\u003E p.PrefabInstances )} nested prefab references, \u0022 \u002B\n\t\t\t\t\t$\u0022{placements.Sum( p =\u003E p.InlinedInstances )} inlined instances, {placements.Sum( p =\u003E p.Lights )} lights, {placements.Sum( p =\u003E p.Cameras )} cameras, {placements.Sum( p =\u003E p.Probes )} reflection probes, \u0022 \u002B\n\t\t\t\t\t$\u0022{placements.Sum( p =\u003E p.Sounds )} audio sources. Scripts, particle effects and animation are not converted.\u0022 );\n\t\t\tMark( \u0022Prefabs and scenes\u0022 );\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\ttry { if ( channelRequests.Count \u003E 0 ) extractChannels( channelRequests, cancel ); }\n\t\t\tcatch ( TextureBatchException failed )\n\t\t\t{\n\t\t\t\t// Paths in the destination are relative to the staging folder; the report names the package source.\n\t\t\t\tvar sources = textureSources.Values.ToDictionary( t =\u003E Path.GetFullPath( t.Source ), t =\u003E t.Path, StringComparer.OrdinalIgnoreCase );\n\t\t\t\tforeach ( var bySource in failed.Failures.GroupBy( f =\u003E f.Source, StringComparer.OrdinalIgnoreCase ) )\n\t\t\t\t\tIssue( sources.GetValueOrDefault( Path.GetFullPath( bySource.Key ), Path.GetFileName( bySource.Key ) ), \u0022texture-processing-failed\u0022,\n\t\t\t\t\t\t$\u0022The image could not be processed ({bySource.First().Message}); {bySource.Count()} derived textures use a neutral placeholder.\u0022 );\n\t\t\t}\n\t\t\tchannelTimer.Stop();\n\t\t\tMark( \u0022Texture processing\u0022 );\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 model in physicsModels.Values.Where( m =\u003E m.Physics != null ) )\n\t\t\t\tdocuments[model.Document] = (documents[model.Document].File, model.Text( model.Physics, model.Convex ));\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\tMark( \u0022Writing files\u0022 );\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, shaders, animations and generic assets) were excluded.\u0022 );\n\t\t\tif ( archive.Skipped.Count \u003E 0 ) information.Add( $\u0022{archive.Skipped.Count} package entries were left out while reading: {string.Join( \u0022; \u0022, archive.Skipped.Take( 10 ) )}{(archive.Skipped.Count \u003E 10 ? \u0022; \u2026\u0022 : \u0022\u0022)}\u0022 );\n\t\t\tvar issues = unresolved.Distinct().ToArray();\n\t\t\t// Each package shader the conversion approximates: its materials and the effects their shader properties name.\n\t\t\tvar shaderApproximations = selected.Where( a =\u003E convertedMaterials.TryGetValue( a.Guid, out var m ) \u0026\u0026 shaders.ContainsKey( m.ShaderGuid ?? \u0022\u0022 ) )\n\t\t\t\t.GroupBy( a =\u003E convertedMaterials[a.Guid].ShaderName ?? convertedMaterials[a.Guid].ShaderGuid, StringComparer.Ordinal ).OrderBy( g =\u003E g.Key, StringComparer.Ordinal )\n\t\t\t\t.Select( g =\u003E new\n\t\t\t\t{\n\t\t\t\t\tShader = g.Key, Materials = g.Count(),\n\t\t\t\t\tNotReproduced = g.SelectMany( a =\u003E convertedMaterials[a.Guid].Unreproduced ).GroupBy( f =\u003E f.Key ).OrderBy( f =\u003E f.Key, StringComparer.Ordinal )\n\t\t\t\t\t\t.Select( f =\u003E new { Feature = f.Key, Materials = f.Count(), Properties = f.SelectMany( p =\u003E p.Value ).Distinct().Order( StringComparer.Ordinal ).ToArray() } ).ToArray()\n\t\t\t\t} ).ToArray();\n\t\t\tif ( shaderApproximations.Length \u003E 0 )\n\t\t\t\tinformation.Add( $\u0022{shaderApproximations.Sum( s =\u003E s.Materials )} material(s) use {shaderApproximations.Length} package shader(s), approximated with s\u0026box\u0027s complex shader\u0022 \u002B\n\t\t\t\t\t(shaderApproximations.Any( s =\u003E s.NotReproduced.Length \u003E 0 ) ? \u0022; effects not reproduced: \u0022 \u002B string.Join( \u0022, \u0022, shaderApproximations.SelectMany( s =\u003E s.NotReproduced )\n\t\t\t\t\t\t.GroupBy( f =\u003E f.Feature ).OrderByDescending( f =\u003E f.Sum( x =\u003E x.Materials ) ).Select( f =\u003E $\u0022{f.Key} ({f.Sum( x =\u003E x.Materials )})\u0022 ) ) : \u0022\u0022) \u002B\n\t\t\t\t\t\u0022. See ShaderApproximations in the report.\u0022 );\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\t// Package reading happens before this preparation; committing and engine preparation are added later.\n\t\t\t\tTimings = new[] { new { Stage = \u0022Reading the package\u0022, Milliseconds = archive.ReadMilliseconds }, new { Stage = \u0022Cataloguing dependencies\u0022, Milliseconds = plan.CatalogMilliseconds } }.Concat( stageTimes.Select( t =\u003E new { t.Stage, t.Milliseconds } ) ).ToArray(),\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\tAssignmentSummary = new { Rules = ruleCounts, PrefabReferences = idCounts, MaterialGroups = materialGroups, HiddenMeshes = hiddenMeshes },\n\t\t\t\tAssignments = assignments.ToArray(),\n\t\t\t\tPlacements = placements.Select( p =\u003E new { p.Source, p.Objects, p.Renderers, p.ModelInstances, p.PrefabInstances, p.InlinedInstances, p.Colliders, p.Terrains, p.Trees, p.Lights, p.Cameras, p.Probes, p.Sounds, p.Skies, p.GeneratedMeshes, p.LodGroups, NotConverted = p.Skipped, Notes = p.Notes.Distinct().ToArray() } ).ToArray(),\n\t\t\t\tTerrains = terrains,\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\tTextures = convertedMaterials[a.Guid].Textures.Count,\n\t\t\t\t\tValues = convertedMaterials[a.Guid].Numbers.Count \u002B convertedMaterials[a.Guid].Colors.Count,\n\t\t\t\t\tConversion = \u0022Common material properties; shader programs are not translated\u0022,\n\t\t\t\t\tNotReproduced = convertedMaterials[a.Guid].Unreproduced,\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\tShaderApproximations = shaderApproximations,\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\t/// \u003Csummary\u003EThe material group whose materials match a renderer\u0027s slot assignment (\u0022\u0022 for the default group).\u003C/summary\u003E\n\tstatic (string Group, bool Exact) ChooseGroup( IReadOnlyDictionary\u003Cstring, string\u003E slots, Func\u003Cstring, string\u003E defaultOf, Func\u003Cstring, string\u003E variantMaterial, IReadOnlyList\u003CVariantGroup\u003E groups )\n\t{\n\t\tvar wanted = slots.Select( p =\u003E (Default: defaultOf( p.Key ), Want: variantMaterial( p.Value ) ?? defaultOf( p.Key )) ).Where( w =\u003E w.Default != null ).ToArray();\n\t\tint Score( IReadOnlyDictionary\u003Cstring, string\u003E remaps ) =\u003E wanted.Count( w =\u003E string.Equals( remaps?.GetValueOrDefault( w.Default ) ?? w.Default, w.Want, StringComparison.OrdinalIgnoreCase ) );\n\t\tvar best = (Name: \u0022\u0022, Score: Score( null ));\n\t\tif ( best.Score == wanted.Length ) return (\u0022\u0022, true);\n\t\tforeach ( var group in groups )\n\t\t{\n\t\t\tvar score = Score( group.Remaps );\n\t\t\tif ( score == wanted.Length ) return (group.Name, true);\n\t\t\tif ( score \u003E best.Score ) best = (group.Name, score);\n\t\t}\n\t\treturn (best.Name, false);\n\t}\n\n\t/// \u003Csummary\u003EWhere a model\u0027s VMDL is written: beside its source, with a name the engine can load (\u003Csee cref=\u0022AsciiPath\u0022/\u003E).\u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// A model\u0027s import scale into s\u0026amp;box inches: FBX from its unit and Unity\u0027s scale settings; OBJ, which has no unit,\n\t/// as meters (Unity\u0027s OBJ import) times globalScale; other formats as they are.\n\t/// \u003C/summary\u003E\n\tinternal static double ModelScale( UnityAsset asset, UnityModel info ) =\u003E Path.GetExtension( asset.Path ).ToLowerInvariant() switch\n\t{\n\t\t\u0022.fbx\u0022 =\u003E info.ImportScale( asset.MetadataDocument ),\n\t\t\u0022.obj\u0022 =\u003E (asset.MetadataDocument?.Find( \u0022globalScale\u0022 )?.Number is double global \u0026\u0026 global \u003E 0 \u0026\u0026 double.IsFinite( global ) ? global : 1) * UnityTransform.InchesPerMeter,\n\t\t_ =\u003E 1,\n\t};\n\n\tpublic static string ModelOutput( string source ) =\u003E AsciiPath( Path.ChangeExtension( source, \u0022.vmdl\u0022 ) );\n\n\t/// \u003Csummary\u003EWhere a converted prefab, scene or terrain is written: beside its source, with a name the engine can load (\u003Csee cref=\u0022AsciiPath\u0022/\u003E).\u003C/summary\u003E\n\tpublic static string PlacementOutput( string source, string extension ) =\u003E AsciiPath( Path.ChangeExtension( source, extension ) );\n\n\t/// \u003Csummary\u003E\n\t/// The path with each non-ASCII character replaced by an underscore, and spaces ending the file name (before its\n\t/// extension) too: the engine\u0027s compilers cannot open non-ASCII paths, and its resource lookup cannot find a name\n\t/// ending in a space (\u0022Terrain .prefab\u0022).\n\t/// \u003C/summary\u003E\n\tinternal static string AsciiPath( string path )\n\t{\n\t\tvar ascii = new string( path.Select( c =\u003E c \u003E 127 ? \u0027_\u0027 : c ).ToArray() );\n\t\tvar extension = Path.GetExtension( ascii );\n\t\tvar stem = ascii[..^extension.Length];\n\t\tvar trimmed = stem.TrimEnd( \u0027 \u0027 );\n\t\treturn trimmed.Length == stem.Length || trimmed.EndsWith( \u0027/\u0027 ) ? ascii : trimmed \u002B new string( \u0027_\u0027, stem.Length - trimmed.Length ) \u002B extension;\n\t}\n\n\tstatic string ReadPlacement( string path )\n\t{\n\t\tif ( new FileInfo( path ).Length \u003E 256L * 1024 * 1024 ) throw new InvalidDataException( \u0022Prefab or scene exceeds the 256 MiB limit.\u0022 );\n\t\tvar text = File.ReadAllText( path );\n\t\tif ( !text.TrimStart().StartsWith( \u0022%YAML\u0022, StringComparison.Ordinal ) \u0026\u0026 text.Contains( \u0027\\0\u0027 ) )\n\t\t\tthrow new InvalidDataException( \u0022The prefab or scene uses Unity\u0027s binary serialization; re-export it with Force Text serialization to convert it.\u0022 );\n\t\treturn text;\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\t/// \u003Csummary\u003EA named ModelDoc material group replacing default materials for one Unity prefab variant.\u003C/summary\u003E\n\tsealed record VariantGroup( string Name, List\u003Cstring\u003E Prefabs, Dictionary\u003Cstring, string\u003E Remaps, List\u003Cstring\u003E Notes );\n\n\t/// \u003Csummary\u003EA written VMDL that colliders can add a physics shape to: its document, how to render it and the shape.\u003C/summary\u003E\n\tsealed class PhysicsModel\n\t{\n\t\tpublic int Document { get; init; }\n\t\tpublic Func\u003CMeshPart, bool, string\u003E Text { get; init; }\n\t\tpublic MeshPart Physics { get; set; }\n\t\tpublic bool Convex { get; set; }\n\t}\n\n\tstatic string Remaps( IEnumerable\u003CKeyValuePair\u003Cstring, string\u003E\u003E remaps ) =\u003E 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\n\tstatic string Scale( double scale ) =\u003E scale.ToString( \u00220.#########\u0022, System.Globalization.CultureInfo.InvariantCulture );\n\n\t/// \u003Csummary\u003EA ModelDoc physics node: one convex hull (as Unity\u0027s convex MeshCollider builds) or the exact mesh.\u003C/summary\u003E\n\tstatic string PhysicsNode( string mesh, string[] includedMeshes, double scale, bool convex ) =\u003E\n\t\t\u0022{ _class = \\\u0022\u0022 \u002B (convex ? \u0022PhysicsHullFile\u0022 : \u0022PhysicsMeshFile\u0022) \u002B \u0022\\\u0022 filename = \u0022 \u002B UnityMaterial.Quote( mesh ) \u002B \u0022 surface_prop = \\\u0022default\\\u0022 collision_tags = \\\u0022solid\\\u0022\u0022 \u002B\n\t\t(convex ? \u0022 import_mode = \\\u0022SingleHull\\\u0022 maxHullVertices = 255\u0022 : \u0022\u0022) \u002B \u0022 import_scale = \u0022 \u002B Scale( scale ) \u002B\n\t\t\u0022 import_filter = { exclude_by_default = \u0022 \u002B (includedMeshes.Length \u003E 0 ? \u0022true\u0022 : \u0022false\u0022) \u002B \u0022 exception_list = [ \u0022 \u002B string.Join( \u0022, \u0022, includedMeshes.Select( UnityMaterial.Quote ) ) \u002B \u0022 ] } }\u0022;\n\n\t/// \u003Csummary\u003EA physics-only VMDL for a Unity MeshCollider.\u003C/summary\u003E\n\tstatic string CollisionDocument( string mesh, string[] includedMeshes, double scale, bool convex ) =\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{ _class = \u0022PhysicsShapeList\u0022 children = [ {{PhysicsNode( mesh, includedMeshes, scale, convex )}} ] }\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\tstatic string ModelDocument( string mesh, Dictionary\u003Cstring, string\u003E remaps, string defaultMaterial, IReadOnlyList\u003CVariantGroup\u003E groups, string[] includedMeshes, double scale, MeshPart physics = null, bool convex = false, IReadOnlyList\u003Cstring[]\u003E lods = null, IReadOnlyList\u003Cdouble\u003E thresholds = null ) =\u003E\n\t\tComposedDocument( remaps, defaultMaterial, groups,\n\t\t\tlods is { Count: \u003E 1 }\n\t\t\t\t// Meshes without a LOD suffix are not part of Unity\u0027s LOD group: they show at every level.\n\t\t\t\t? lods.Select( l =\u003E (IReadOnlyList\u003CMeshPart\u003E)new[] { new MeshPart( mesh, l.Concat( includedMeshes.Where( m =\u003E !lods.Any( level =\u003E level.Contains( m ) ) ) ).ToArray(), scale ) } ).ToList()\n\t\t\t\t: new List\u003CIReadOnlyList\u003CMeshPart\u003E\u003E { new[] { new MeshPart( mesh, includedMeshes, scale ) } },\n\t\t\tthresholds, physics, convex );\n\n\t/// \u003Csummary\u003E\n\t/// A ModelDoc document: render meshes from one or more files, as LOD levels when there are several (each level\u0027s\n\t/// meshes switch in at its threshold), material remaps and groups, and an optional physics shape. Without render\n\t/// meshes (no LOD levels) it is a physics-only model.\n\t/// \u003C/summary\u003E\n\tstatic string ComposedDocument( IEnumerable\u003CKeyValuePair\u003Cstring, string\u003E\u003E remaps, string defaultMaterial, IReadOnlyList\u003CVariantGroup\u003E groups,\n\t\tIReadOnlyList\u003CIReadOnlyList\u003CMeshPart\u003E\u003E lods, IReadOnlyList\u003Cdouble\u003E thresholds, MeshPart physics, bool convex )\n\t{\n\t\tconst string Tab = \u0022\\t\u0022;\n\t\tstring Indent( int n ) =\u003E string.Concat( Enumerable.Repeat( Tab, n ) );\n\t\tstatic string Filter( string[] meshes ) =\u003E \u0022{ exclude_by_default = \u0022 \u002B (meshes.Length \u003E 0 ? \u0022true\u0022 : \u0022false\u0022) \u002B \u0022 exception_list = [ \u0022 \u002B string.Join( \u0022, \u0022, meshes.Select( UnityMaterial.Quote ) ) \u002B \u0022 ] }\u0022;\n\t\tvar named = lods.Count \u003E 1;\n\t\tvar renderMeshes = new List\u003Cstring\u003E();\n\t\tvar lodGroups = new List\u003Cstring\u003E();\n\t\tfor ( var l = 0; l \u003C lods.Count; l\u002B\u002B )\n\t\t{\n\t\t\tvar names = new List\u003Cstring\u003E();\n\t\t\tfor ( var p = 0; p \u003C lods[l].Count; p\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar part = lods[l][p];\n\t\t\t\tvar name = named ? $\u0022LOD{l}\u0022 \u002B (p \u003E 0 ? $\u0022_{p}\u0022 : \u0022\u0022) : null;\n\t\t\t\tif ( name != null ) names.Add( name );\n\t\t\t\trenderMeshes.Add( Indent( 6 ) \u002B \u0022{ _class = \\\u0022RenderMeshFile\\\u0022\u0022 \u002B (name != null ? \u0022 name = \u0022 \u002B UnityMaterial.Quote( name ) : \u0022\u0022) \u002B \u0022 filename = \u0022 \u002B UnityMaterial.Quote( part.File ) \u002B\n\t\t\t\t\t\u0022 import_filter = \u0022 \u002B Filter( part.Meshes ) \u002B \u0022 import_scale = \u0022 \u002B Scale( part.Scale ) \u002B \u0022 import_translation = [ 0.0, 0.0, 0.0 ] import_rotation = [ 0.0, 0.0, 0.0 ] }\u0022 );\n\t\t\t}\n\t\t\tvar threshold = l == 0 ? 0 : thresholds != null \u0026\u0026 l - 1 \u003C thresholds.Count ? thresholds[l - 1] : 50.0 * l;\n\t\t\tif ( named ) lodGroups.Add( Indent( 6 ) \u002B \u0022{ _class = \\\u0022LODGroup\\\u0022 switch_threshold = \u0022 \u002B threshold.ToString( \u00220.0##\u0022, System.Globalization.CultureInfo.InvariantCulture ) \u002B \u0022 meshes = [ \u0022 \u002B string.Join( \u0022, \u0022, names.Select( UnityMaterial.Quote ) ) \u002B \u0022 ] }\u0022 );\n\t\t}\n\t\tvar text = new System.Text.StringBuilder();\n\t\ttext.Append( \u0022\u003C!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc29:version{3cec427c-1b0e-4d48-a90a-0436f33a6041} --\u003E\\n\u0022 );\n\t\ttext.Append( \u0022{\\n\u0022 ).Append( Tab ).Append( \u0022rootNode =\\n\u0022 ).Append( Tab ).Append( \u0022{\\n\u0022 ).Append( Indent( 2 ) ).Append( \u0022_class = \\\u0022RootNode\\\u0022\\n\u0022 ).Append( Indent( 2 ) ).Append( \u0022children =\\n\u0022 ).Append( Indent( 2 ) ).Append( \u0022[\\n\u0022 );\n\t\tvar children = new List\u003Cstring\u003E();\n\t\tvar materials = new System.Text.StringBuilder();\n\t\tmaterials.Append( Indent( 3 ) ).Append( \u0022{\\n\u0022 ).Append( Indent( 4 ) ).Append( \u0022_class = \\\u0022MaterialGroupList\\\u0022\\n\u0022 ).Append( Indent( 4 ) ).Append( \u0022children =\\n\u0022 ).Append( Indent( 4 ) ).Append( \u0022[\\n\u0022 );\n\t\tmaterials.Append( Indent( 5 ) ).Append( \u0022{\\n\u0022 ).Append( Indent( 6 ) ).Append( \u0022_class = \\\u0022DefaultMaterialGroup\\\u0022\\n\u0022 );\n\t\tmaterials.Append( Indent( 6 ) ).Append( \u0022remaps = [ \u0022 ).Append( Remaps( remaps ) ).Append( \u0022 ]\\n\u0022 );\n\t\tmaterials.Append( Indent( 6 ) ).Append( \u0022use_global_default = \u0022 ).Append( defaultMaterial != null ? \u0022true\u0022 : \u0022false\u0022 ).Append( \u0027\\n\u0027 );\n\t\tmaterials.Append( Indent( 6 ) ).Append( \u0022global_default_material = \u0022 ).Append( UnityMaterial.Quote( defaultMaterial ?? \u0022\u0022 ) ).Append( \u0027\\n\u0027 ).Append( Indent( 5 ) ).Append( \u0027}\u0027 );\n\t\tforeach ( var g in groups ?? Array.Empty\u003CVariantGroup\u003E() )\n\t\t\tmaterials.Append( \u0022,\\n\u0022 ).Append( Indent( 5 ) ).Append( \u0022{ _class = \\\u0022MaterialGroup\\\u0022 name = \u0022 ).Append( UnityMaterial.Quote( g.Name ) ).Append( \u0022 remaps = [ \u0022 ).Append( Remaps( g.Remaps ) ).Append( \u0022 ] }\u0022 );\n\t\tmaterials.Append( \u0027\\n\u0027 ).Append( Indent( 4 ) ).Append( \u0022]\\n\u0022 ).Append( Indent( 3 ) ).Append( \u0027}\u0027 );\n\t\tchildren.Add( materials.ToString() );\n\t\tif ( renderMeshes.Count \u003E 0 )\n\t\t\tchildren.Add( Indent( 3 ) \u002B \u0022{\\n\u0022 \u002B Indent( 4 ) \u002B \u0022_class = \\\u0022RenderMeshList\\\u0022\\n\u0022 \u002B Indent( 4 ) \u002B \u0022children =\\n\u0022 \u002B Indent( 4 ) \u002B \u0022[\\n\u0022 \u002B string.Join( \u0022,\\n\u0022, renderMeshes ) \u002B \u0022\\n\u0022 \u002B Indent( 4 ) \u002B \u0022]\\n\u0022 \u002B Indent( 3 ) \u002B \u0022}\u0022 );\n\t\tif ( named )\n\t\t\tchildren.Add( Indent( 3 ) \u002B \u0022{\\n\u0022 \u002B Indent( 4 ) \u002B \u0022_class = \\\u0022LODGroupList\\\u0022\\n\u0022 \u002B Indent( 4 ) \u002B \u0022children =\\n\u0022 \u002B Indent( 4 ) \u002B \u0022[\\n\u0022 \u002B string.Join( \u0022,\\n\u0022, lodGroups ) \u002B \u0022\\n\u0022 \u002B Indent( 4 ) \u002B \u0022]\\n\u0022 \u002B Indent( 3 ) \u002B \u0022}\u0022 );\n\t\tif ( physics != null )\n\t\t\tchildren.Add( Indent( 3 ) \u002B \u0022{ _class = \\\u0022PhysicsShapeList\\\u0022 children = [ \u0022 \u002B PhysicsNode( physics.File, physics.Meshes, physics.Scale, convex ) \u002B \u0022 ] }\u0022 );\n\t\ttext.Append( string.Join( \u0022,\\n\u0022, children ) ).Append( \u0027\\n\u0027 ).Append( Indent( 2 ) ).Append( \u0022]\\n\u0022 ).Append( Indent( 2 ) ).Append( \u0022model_archetype = \\\u0022\\\u0022\\n\u0022 ).Append( Tab ).Append( \u0022}\\n}\\n\u0022 );\n\t\treturn text.ToString();\n\t}\n\n\t/// \u003Csummary\u003EThe deepest folder shared by the paths, with a trailing slash; empty when they share none.\u003C/summary\u003E\n\tinternal static string PackageFolder( IEnumerable\u003Cstring\u003E paths )\n\t{\n\t\tstring[] shared = null;\n\t\tforeach ( var path in paths )\n\t\t{\n\t\t\tvar folders = path.Replace( \u0027\\\\\u0027, \u0027/\u0027 ).Split( \u0027/\u0027 )[..^1];\n\t\t\tshared = shared == null ? folders : shared.Zip( folders ).TakeWhile( p =\u003E p.First.Equals( p.Second, StringComparison.OrdinalIgnoreCase ) ).Select( p =\u003E p.First ).ToArray();\n\t\t}\n\t\treturn shared is { Length: \u003E 0 } ? string.Join( \u0022/\u0022, shared ) \u002B \u0022/\u0022 : \u0022\u0022;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Unity\u0027s LOD bias, which divides every LOD group\u0027s switch heights. Packages carry no quality settings; this is the\n\t/// bias of Unity\u0027s default desktop quality level (built-in Ultra, URP High Fidelity).\n\t/// \u003C/summary\u003E\n\tinternal const double UnityLodBias = 2;\n\t/// \u003Csummary\u003EThe LOD group size assumed when a model\u0027s bounds are unknown, in inches (one meter).\u003C/summary\u003E\n\tinternal const double DefaultLodSize = UnityTransform.InchesPerMeter;\n\n\t/// \u003Csummary\u003E\n\t/// Errors that skip one asset instead of stopping the import: malformed or unexpected source data. Cancellation, file\n\t/// system and memory failures still stop it.\n\t/// \u003C/summary\u003E\n\tstatic bool Skippable( Exception ex ) =\u003E ex is not (OperationCanceledException or IOException or UnauthorizedAccessException or OutOfMemoryException);\n\tstatic string Describe( Exception ex ) =\u003E ex is InvalidDataException ? ex.Message : $\u0022{ex.GetType().Name}: {ex.Message}\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// Unity\u0027s screen-relative LOD height as an s\u0026amp;box switch threshold, for a LOD group of the given size (Unity\u0027s\n\t/// largest bounds axis, in inches). Measured in the engine, a level switches in at a camera distance of 0.0201 \u00D7\n\t/// threshold \u00D7 the camera\u0027s focal length in pixels, whatever the model\u0027s size or shape (a 40 and a 400 inch cube switch\n\t/// at the same distance). Unity switches where the group\u0027s size covers that height \u00F7 LOD bias of the screen height, at a\n\t/// distance of size \u00D7 bias \u00D7 focal length \u00F7 (height \u00D7 screen height). Equal on a 1080-pixel-high screen: threshold =\n\t/// size \u00D7 bias \u00F7 (21.7 \u00D7 height). s\u0026amp;box switches move with resolution; Unity\u0027s do not.\n\t/// \u003C/summary\u003E\n\tinternal static double LodThreshold( double screenRelativeHeight, double size ) =\u003E\n\t\tMath.Round( Math.Max( 1, size ) * UnityLodBias / (0.0201 * 1080 * Math.Max( 0.001, screenRelativeHeight )), 2 );\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/ImportCompletion.cs","FileName":"ImportCompletion.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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\t/// \u003Csummary\u003EFiles written to the project, and outputs left as they were (identical, or kept by choice); -1 when unknown.\u003C/summary\u003E\n\tpublic int Written { get; init; } = -1;\n\tpublic int Unchanged { get; init; }\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 :\n\t\t$\u0022{AssetCount} assets imported{(Written \u003C 0 ? \u0022\u0022 : $\u0022: {Written} files written, {Unchanged} left as they were\u0022)}.\\n{Counts}\u0022;\n\t/// \u003Csummary\u003EThe completion dialog\u0027s icon: errors, then warnings or a cancelled preparation, else a clean import.\u003C/summary\u003E\n\tpublic string Icon =\u003E Errors.Length \u003E 0 ? \u0022\u274C\u0022 : Warnings.Length \u003E 0 || Cancelled ? \u0022\u26A0\uFE0F\u0022 : \u0022\u2705\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\tvar written = result.ChangedFiles.Length;\n\t\treturn new( result.AssetCount, warnings, errors, cancelled ) { Written = written, Unchanged = Math.Max( 0, result.Files.Length - written ) };\n\t}\n\n\t/// \u003Csummary\u003EThe report with the completion summary, and the engine preparation time when resources were prepared.\u003C/summary\u003E\n\tpublic string AppendReport( string json, long? preparationMilliseconds = null )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( json ) ) json = \u0022{}\u0022;\n\t\tvar report = JsonNode.Parse( json );\n\t\tif ( preparationMilliseconds is long prepared )\n\t\t\t(report[\u0022Timings\u0022] as JsonArray)?.Add( JsonSerializer.SerializeToNode( new { Stage = \u0022Preparing s\u0026box resources\u0022, Milliseconds = prepared } ) );\n\t\treport[\u0022Completion\u0022] = JsonSerializer.SerializeToNode( new { Title, AssetCount, Written, Unchanged, Cancelled, Warnings, Errors } );\n\t\treturn report.ToJsonString( new JsonSerializerOptions { WriteIndented = true } );\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityKnownShaders.cs","FileName":"UnityKnownShaders.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003E\n/// Unity shaders that never ship inside packages (built-in, URP and HDRP), so materials using them cannot be checked\n/// against shader source. Identities come from shader references and property layouts observed in real packages.\n/// \u003C/summary\u003E\ninternal static class UnityKnownShaders\n{\n\tinternal const string BuiltinGuid = \u00220000000000000000f000000000000000\u0022;\n\tinternal enum Workflow { Metallic, Specular, Other }\n\t/// \u003Csummary\u003E\n\t/// KeywordDriven: material keywords decide which features are active (Standard and URP Lit).\n\t/// AlphaTest/Backfaces: fixed render state of shaders that are always cut out or double-sided. Unlit: ignores scene\n\t/// lighting. LegacyParticle: Unity\u0027s built-in particle shaders, always blended and tinted by twice \u003Cc\u003E_TintColor\u003C/c\u003E.\n\t/// \u003C/summary\u003E\n\tinternal sealed record Shader( string Name, Workflow Workflow, bool KeywordDriven, bool Universal, bool AlphaTest = false, bool Backfaces = false,\n\t\tbool Unlit = false, bool LegacyParticle = false );\n\n\tstatic readonly Dictionary\u003Cstring, Shader\u003E Pipeline = new( StringComparer.OrdinalIgnoreCase )\n\t{\n\t\t[\u0022933532a4fcc9baf4fa0491de14d08ed7\u0022] = new( \u0022Universal Render Pipeline/Lit\u0022, Workflow.Metallic, true, true ),\n\t\t[\u002269c1f799e772cb6438f56c23efccb782\u0022] = new( \u0022Universal Render Pipeline/Terrain/Lit\u0022, Workflow.Other, false, true ),\n\t\t// Particle materials show the property layouts: Lit has _Smoothness/_Metallic, Unlit has neither.\n\t\t[\u0022b7839dad95683814aa64166edc107ae2\u0022] = new( \u0022Universal Render Pipeline/Particles/Lit\u0022, Workflow.Other, false, true ),\n\t\t[\u00220406db5a14f94604a8c57ccfbc9f3b46\u0022] = new( \u0022Universal Render Pipeline/Particles/Unlit\u0022, Workflow.Other, false, true, Unlit: true ),\n\t\t[\u0022650dd9526735d5b46b79224bc6e94025\u0022] = new( \u0022Universal Render Pipeline/Unlit\u0022, Workflow.Other, false, true, Unlit: true ),\n\t\t[\u00226e4ae4064600d784cac1e41a9e6f2e59\u0022] = new( \u0022HDRP/Lit\u0022, Workflow.Metallic, false, false ),\n\t\t[\u00221d64af84bdc970c4fae0c1e06dd95b73\u0022] = new( \u0022HDRP/Decal\u0022, Workflow.Other, false, false ),\n\t};\n\n\tinternal static Shader Identify( UnityReference shader )\n\t{\n\t\tif ( !(shader.Guid ?? \u0022\u0022).Equals( BuiltinGuid, StringComparison.OrdinalIgnoreCase ) ) return Pipeline.GetValueOrDefault( shader.Guid ?? \u0022\u0022 );\n\t\treturn shader.FileId switch\n\t\t{\n\t\t\t46 =\u003E new( \u0022Standard\u0022, Workflow.Metallic, true, false ),\n\t\t\t45 =\u003E new( \u0022Standard (Specular setup)\u0022, Workflow.Specular, true, false ),\n\t\t\t// Legacy foliage shaders observed only on leaf/plant materials, all with _Cutoff. 10601 has the Tree Creator\n\t\t\t// Leaves layout (_TranslucencyMap, _Cutoff), which renders both sides.\n\t\t\t10601 =\u003E new( \u0022Unity built-in tree leaves shader\u0022, Workflow.Other, false, false, AlphaTest: true, Backfaces: true ),\n\t\t\t10511 or 51 =\u003E new( $\u0022Unity built-in cutout shader {shader.FileId}\u0022, Workflow.Other, false, false, AlphaTest: true ),\n\t\t\t// Legacy particle shaders, observed only on particle/FX materials with the _TintColor/_InvFade layout.\n\t\t\t\u003E= 200 and \u003C= 205 =\u003E new( $\u0022Unity built-in particle shader {shader.FileId}\u0022, Workflow.Other, false, false, Unlit: true, LegacyParticle: true ),\n\t\t\t// Skybox shaders, converted to sky materials (UnitySky).\n\t\t\t103 =\u003E new( \u0022Skybox/Cubemap\u0022, Workflow.Other, false, false, Unlit: true ),\n\t\t\t104 =\u003E new( \u0022Skybox/6 Sided\u0022, Workflow.Other, false, false, Unlit: true ),\n\t\t\t106 =\u003E new( \u0022Skybox/Procedural\u0022, Workflow.Other, false, false, Unlit: true ),\n\t\t\t108 =\u003E new( \u0022Skybox/Panoramic\u0022, Workflow.Other, false, false, Unlit: true ),\n\t\t\t_ =\u003E new( $\u0022Unity built-in shader {shader.FileId}\u0022, Workflow.Other, false, false ),\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Texture properties declared by Unity-shipped shaders. A known-shader material\u0027s texture outside this list is a\n\t/// leftover from a previous (custom) shader: Unity keeps such properties but no Unity shader reads them.\n\t/// \u003C/summary\u003E\n\tinternal static readonly HashSet\u003Cstring\u003E TextureProperties = new( StringComparer.Ordinal )\n\t{\n\t\t// Standard, Standard (Specular setup) and URP Lit/Simple Lit\n\t\t\u0022_MainTex\u0022, \u0022_BaseMap\u0022, \u0022_BumpMap\u0022, \u0022_DetailAlbedoMap\u0022, \u0022_DetailNormalMap\u0022, \u0022_DetailMask\u0022, \u0022_EmissionMap\u0022, \u0022_MetallicGlossMap\u0022,\n\t\t\u0022_SpecGlossMap\u0022, \u0022_OcclusionMap\u0022, \u0022_ParallaxMap\u0022, \u0022_ClearCoatMap\u0022,\n\t\t// Legacy, nature/tree, particle, water and skybox shaders\n\t\t\u0022_Illum\u0022, \u0022_DecalTex\u0022, \u0022_DetailTex\u0022, \u0022_Detail\u0022, \u0022_LightMap\u0022, \u0022_Cube\u0022, \u0022_ReflectionTex\u0022, \u0022_RefractionTex\u0022, \u0022_ReflectiveColor\u0022,\n\t\t\u0022_ReflectiveColorCube\u0022, \u0022_Fresnel\u0022, \u0022_BumpSpecMap\u0022, \u0022_TranslucencyMap\u0022, \u0022_GlossMap\u0022, \u0022_ShadowOffset\u0022, \u0022_ShadowTex\u0022, \u0022_NoiseTex\u0022,\n\t\t\u0022_MaskTex\u0022, \u0022_AlphaTex\u0022, \u0022_SecondTex\u0022, \u0022_ThirdTex\u0022, \u0022_FrontTex\u0022, \u0022_BackTex\u0022, \u0022_LeftTex\u0022, \u0022_RightTex\u0022, \u0022_UpTex\u0022, \u0022_DownTex\u0022, \u0022_Tex\u0022,\n\t\t// Terrain\n\t\t\u0022_Control\u0022, \u0022_Splat0\u0022, \u0022_Splat1\u0022, \u0022_Splat2\u0022, \u0022_Splat3\u0022, \u0022_Normal0\u0022, \u0022_Normal1\u0022, \u0022_Normal2\u0022, \u0022_Normal3\u0022, \u0022_Mask0\u0022, \u0022_Mask1\u0022, \u0022_Mask2\u0022,\n\t\t\u0022_Mask3\u0022, \u0022_TerrainHolesTexture\u0022,\n\t\t// HDRP Lit, Unlit and Decal\n\t\t\u0022_BaseColorMap\u0022, \u0022_MaskMap\u0022, \u0022_NormalMap\u0022, \u0022_NormalMapOS\u0022, \u0022_BentNormalMap\u0022, \u0022_BentNormalMapOS\u0022, \u0022_HeightMap\u0022, \u0022_DetailMap\u0022,\n\t\t\u0022_TangentMap\u0022, \u0022_TangentMapOS\u0022, \u0022_AnisotropyMap\u0022, \u0022_SubsurfaceMaskMap\u0022, \u0022_TransmissionMaskMap\u0022, \u0022_ThicknessMap\u0022,\n\t\t\u0022_IridescenceThicknessMap\u0022, \u0022_IridescenceMaskMap\u0022, \u0022_SpecularColorMap\u0022, \u0022_EmissiveColorMap\u0022, \u0022_CoatMaskMap\u0022,\n\t\t\u0022_TransmittanceColorMap\u0022, \u0022_DistortionVectorMap\u0022, \u0022_UnlitColorMap\u0022,\n\t};\n\t/// \u003Csummary\u003ETexture names only HDRP shaders read: leftovers on a built-in or URP material.\u003C/summary\u003E\n\tinternal static readonly HashSet\u003Cstring\u003E HighDefinitionOnly = new( StringComparer.Ordinal ) { \u0022_BaseColorMap\u0022, \u0022_MaskMap\u0022, \u0022_NormalMap\u0022, \u0022_NormalMapOS\u0022, \u0022_EmissiveColorMap\u0022, \u0022_HeightMap\u0022, \u0022_DetailMap\u0022, \u0022_UnlitColorMap\u0022 };\n\t/// \u003Csummary\u003ETexture names only built-in and URP shaders read: leftovers on an HDRP material.\u003C/summary\u003E\n\tinternal static readonly HashSet\u003Cstring\u003E BuiltinUniversalOnly = new( StringComparer.Ordinal ) { \u0022_MainTex\u0022, \u0022_BaseMap\u0022, \u0022_BumpMap\u0022, \u0022_MetallicGlossMap\u0022, \u0022_SpecGlossMap\u0022, \u0022_OcclusionMap\u0022, \u0022_EmissionMap\u0022, \u0022_ParallaxMap\u0022, \u0022_DetailAlbedoMap\u0022, \u0022_DetailNormalMap\u0022, \u0022_DetailMask\u0022 };\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityFileId.cs","FileName":"UnityFileId.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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\t// Components hash their GameObject path plus the component type; repeated identical paths count up from 0.\n\tinternal static long Renderer( string path, int index = 0, string type = \u0022MeshRenderer\u0022 ) =\u003E Hash( $\u0022Type:{type}-\u003E{path}/{type}{index}\u0022 );\n\tinternal static long Mesh( string name, int index = 0 ) =\u003E Hash( $\u0022Type:Mesh-\u003E{name}{index}\u0022 );\n\tinternal static long Material( string name, int index = 0 ) =\u003E Hash( $\u0022Type:Material-\u003E{name}{index}\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/UnityShaderDeclarations.cs","FileName":"UnityShaderDeclarations.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003E\n/// Property declarations of a package\u0027s own shader: ShaderLab \u003Cc\u003EProperties\u003C/c\u003E blocks and Shader Graph files.\n/// Explicit markers (\u003Cc\u003E[MainTexture]\u003C/c\u003E, \u003Cc\u003E[MainColor]\u003C/c\u003E, \u003Cc\u003E[Normal]\u003C/c\u003E, \u003Cc\u003E\u0022bump\u0022\u003C/c\u003E defaults, Shader Graph normal-map\n/// textures) are evidence of a property\u0027s role; display and reference names are only inferences.\n/// \u003C/summary\u003E\ninternal sealed class UnityShaderDeclarations\n{\n\tinternal sealed record Property( string Name, string Display, bool Texture, bool Color, bool Normal, bool MainTexture, bool MainColor, bool Hidden = false );\n\tinternal List\u003CProperty\u003E Properties { get; } = new();\n\t/// \u003Csummary\u003EEvery declared property name, of any type.\u003C/summary\u003E\n\tinternal HashSet\u003Cstring\u003E Declared { get; } = new( StringComparer.Ordinal );\n\tinternal string Name { get; private set; }\n\tinternal bool AlphaClip { get; private set; }\n\tinternal bool Transparent { get; private set; }\n\tinternal bool TwoSided { get; private set; }\n\tinternal bool Graph { get; private set; }\n\t/// \u003Csummary\u003E\n\t/// Color properties whose alpha weights a blend in the shader code (\u003Cc\u003Elerp( a, b, _MainColor.a * mask )\u003C/c\u003E): the color\n\t/// is mixed in through a mask (a texture channel or vertex color), so it is neither a tint of the whole surface nor its\n\t/// opacity.\n\t/// \u003C/summary\u003E\n\tinternal HashSet\u003Cstring\u003E MaskedColors { get; } = new( StringComparer.Ordinal );\n\n\t// Many materials share one shader: the declarations are read once per distinct source (they are not changed after).\n\tstatic readonly System.Runtime.CompilerServices.ConditionalWeakTable\u003Cstring, UnityShaderDeclarations\u003E parsed = new();\n\n\tinternal static UnityShaderDeclarations Parse( string source ) =\u003E parsed.GetValue( source, Read );\n\n\tstatic UnityShaderDeclarations Read( string source )\n\t{\n\t\tvar result = new UnityShaderDeclarations();\n\t\tif ( source.TrimStart().StartsWith( \u0027{\u0027 ) ) result.ReadGraph( source );\n\t\telse result.ReadShaderLab( source );\n\t\treturn result;\n\t}\n\n\tvoid ReadShaderLab( string source )\n\t{\n\t\tName = Regex.Match( source, \u0022Shader\\\\s\u002B\\\u0022([^\\\u0022]\u002B)\\\u0022\u0022 ).Groups[1].Value;\n\t\tvar timeout = TimeSpan.FromSeconds( 2 );\n\t\tforeach ( Match match in Regex.Matches( source, @\u0022(?m)^\\s*((?:\\[[^\\]\\r\\n]*\\]\\s*)*)([A-Za-z_][A-Za-z0-9_]*)\\s*\\(\\s*\u0022\u0022([^\u0022\u0022]*)\u0022\u0022\\s*,\\s*([A-Za-z0-9]\u002B)[^)\\r\\n]*\\)\\s*(?:=\\s*\u0022\u0022?([A-Za-z]*))?\u0022, RegexOptions.None, timeout ) )\n\t\t{\n\t\t\tvar attributes = match.Groups[1].Value;\n\t\t\tvar type = match.Groups[4].Value;\n\t\t\tbool Has( string attribute ) =\u003E Regex.IsMatch( attributes, @\u0022\\[\\s*\u0022 \u002B attribute \u002B @\u0022\\s*\\]\u0022, RegexOptions.IgnoreCase );\n\t\t\tvar texture = type.Equals( \u00222D\u0022, StringComparison.OrdinalIgnoreCase );\n\t\t\tProperties.Add( new( match.Groups[2].Value, match.Groups[3].Value, texture, type.Equals( \u0022Color\u0022, StringComparison.OrdinalIgnoreCase ),\n\t\t\t\ttexture \u0026\u0026 (Has( \u0022Normal\u0022 ) || match.Groups[5].Value.Equals( \u0022bump\u0022, StringComparison.OrdinalIgnoreCase )), Has( \u0022MainTexture\u0022 ), Has( \u0022MainColor\u0022 ), Has( \u0022HideInInspector\u0022 ) ) );\n\t\t}\n\t\t// Any \u0022_Name ( \u0022Display\u0022, ...\u0022 line declares a property, whatever its type.\n\t\tDeclared.UnionWith( Regex.Matches( source, @\u0022(?m)^\\s*(?:\\[[^\\]\\r\\n]*\\]\\s*)*([A-Za-z_][A-Za-z0-9_]*)\\s*\\(\\s*\u0022\u0022[^\u0022\u0022]*\u0022\u0022\\s*,\u0022, RegexOptions.None, timeout ).Select( m =\u003E m.Groups[1].Value ) );\n\t\tAlphaClip = Regex.IsMatch( source, \u0022\\\u0022RenderType\\\u0022\\\\s*=\\\\s*\\\u0022TransparentCutout\\\u0022\u0022 );\n\t\tTransparent = Regex.IsMatch( source, \u0022\\\u0022RenderType\\\u0022\\\\s*=\\\\s*\\\u0022Transparent\\\u0022\u0022 );\n\t\tTwoSided = Regex.IsMatch( source, @\u0022(?m)^\\s*Cull\\s\u002BOff\\s*$\u0022, RegexOptions.IgnoreCase );\n\t\t// Only the blend weight (a lerp\u0027s third argument) counts: \u0022lerp( a, b, t ) * _Color.a\u0022 is an ordinary opacity use.\n\t\tforeach ( Match call in Regex.Matches( source, @\u0022\\blerp\\s*\\(\u0022, RegexOptions.None, timeout ) )\n\t\t\tif ( LerpWeight( source, call.Index \u002B call.Length ) is { } weight )\n\t\t\t\tforeach ( Match use in Regex.Matches( weight, @\u0022\\*\\s*\\(?\\s*([A-Za-z_][A-Za-z0-9_]*)\\.[aw]\\b|\\b([A-Za-z_][A-Za-z0-9_]*)\\.[aw]\\s*\\*\u0022, RegexOptions.None, timeout ) )\n\t\t\t\t\tMaskedColors.Add( use.Groups[1].Success ? use.Groups[1].Value : use.Groups[2].Value );\n\t\tMaskedColors.IntersectWith( Properties.Where( p =\u003E p.Color ).Select( p =\u003E p.Name ) );\n\t}\n\n\t/// \u003Csummary\u003EThe third argument of a call whose arguments start at \u003Cparamref name=\u0022start\u0022/\u003E, or null.\u003C/summary\u003E\n\tstatic string LerpWeight( string source, int start )\n\t{\n\t\tvar depth = 0;\n\t\tvar argument = 0;\n\t\tvar weightStart = -1;\n\t\tfor ( var i = start; i \u003C source.Length \u0026\u0026 i \u003C start \u002B 4096; i\u002B\u002B )\n\t\t{\n\t\t\tvar c = source[i];\n\t\t\tif ( c == \u0027(\u0027 ) depth\u002B\u002B;\n\t\t\telse if ( c == \u0027)\u0027 )\n\t\t\t{\n\t\t\t\tif ( depth == 0 ) return weightStart \u003C 0 ? null : source[weightStart..i];\n\t\t\t\tdepth--;\n\t\t\t}\n\t\t\telse if ( c == \u0027,\u0027 \u0026\u0026 depth == 0 \u0026\u0026 \u002B\u002Bargument == 2 ) weightStart = i \u002B 1;\n\t\t\telse if ( c is \u0027;\u0027 or \u0027{\u0027 or \u0027}\u0027 ) return null;\n\t\t}\n\t\treturn null;\n\t}\n\n\tvoid ReadGraph( string source )\n\t{\n\t\tGraph = true;\n\t\t// Shader Graph 10\u002B stores one JSON document per object; older versions nest properties as JSON strings.\n\t\tvar documents = new List\u003CJsonElement\u003E();\n\t\tvar reader = new Utf8JsonReader( System.Text.Encoding.UTF8.GetBytes( source ),\n\t\t\tnew JsonReaderOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip, AllowMultipleValues = true } );\n\t\ttry\n\t\t{\n\t\t\twhile ( reader.Read() )\n\t\t\t{\n\t\t\t\tif ( reader.TokenType != JsonTokenType.StartObject ) continue;\n\t\t\t\tusing var document = JsonDocument.ParseValue( ref reader );\n\t\t\t\tdocuments.Add( document.RootElement.Clone() );\n\t\t\t}\n\t\t}\n\t\tcatch ( JsonException ) { }\n\t\tforeach ( var root in documents )\n\t\t{\n\t\t\tif ( root.TryGetProperty( \u0022m_SerializedProperties\u0022, out var legacy ) \u0026\u0026 legacy.ValueKind == JsonValueKind.Array )\n\t\t\t\tforeach ( var item in legacy.EnumerateArray() )\n\t\t\t\t\tif ( item.TryGetProperty( \u0022JSONnodeData\u0022, out var data ) \u0026\u0026 data.ValueKind == JsonValueKind.String )\n\t\t\t\t\t{\n\t\t\t\t\t\ttry { using var nested = JsonDocument.Parse( data.GetString() ); Add( nested.RootElement, item.TryGetProperty( \u0022typeInfo\u0022, out var info ) \u0026\u0026 info.TryGetProperty( \u0022fullName\u0022, out var full ) ? full.GetString() : \u0022\u0022 ); }\n\t\t\t\t\t\tcatch ( JsonException ) { }\n\t\t\t\t\t}\n\t\t\tvar type = root.TryGetProperty( \u0022m_Type\u0022, out var typeName ) ? typeName.GetString() ?? \u0022\u0022 : \u0022\u0022;\n\t\t\tAdd( root, type );\n\t\t\tif ( type.EndsWith( \u0022.UniversalTarget\u0022, StringComparison.Ordinal ) || type.EndsWith( \u0022.BuiltInTarget\u0022, StringComparison.Ordinal ) )\n\t\t\t{\n\t\t\t\tAlphaClip |= root.TryGetProperty( \u0022m_AlphaClip\u0022, out var clip ) \u0026\u0026 clip.ValueKind == JsonValueKind.True;\n\t\t\t\tTransparent |= root.TryGetProperty( \u0022m_SurfaceType\u0022, out var surface ) \u0026\u0026 surface.TryGetInt32( out var kind ) \u0026\u0026 kind == 1;\n\t\t\t\tTwoSided |= root.TryGetProperty( \u0022m_RenderFace\u0022, out var face ) \u0026\u0026 face.TryGetInt32( out var faces ) \u0026\u0026 faces == 0;\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid Add( JsonElement element, string type )\n\t{\n\t\tif ( !type.EndsWith( \u0022ShaderProperty\u0022, StringComparison.Ordinal ) ) return;\n\t\tvar texture = type.EndsWith( \u0022Texture2DShaderProperty\u0022, StringComparison.Ordinal );\n\t\tvar color = type.EndsWith( \u0022ColorShaderProperty\u0022, StringComparison.Ordinal );\n\t\tstring Text( string key ) =\u003E element.TryGetProperty( key, out var value ) \u0026\u0026 value.ValueKind == JsonValueKind.String ? value.GetString() : null;\n\t\tbool Flag( string key ) =\u003E element.TryGetProperty( key, out var value ) \u0026\u0026 value.ValueKind == JsonValueKind.True;\n\t\tvar name = Text( \u0022m_OverrideReferenceName\u0022 ) is { Length: \u003E 0 } custom ? custom : Text( \u0022m_DefaultReferenceName\u0022 ) ?? Text( \u0022m_Name\u0022 );\n\t\tif ( string.IsNullOrEmpty( name ) ) return;\n\t\tDeclared.Add( name );\n\t\tif ( !texture \u0026\u0026 !color ) return;\n\t\t// Texture2DShaderProperty.DefaultType 3 is the normal-map (\u0022bump\u0022) default.\n\t\tvar normal = texture \u0026\u0026 element.TryGetProperty( \u0022m_DefaultType\u0022, out var defaultType ) \u0026\u0026 defaultType.TryGetInt32( out var value ) \u0026\u0026 value == 3;\n\t\tProperties.Add( new( name, Text( \u0022m_Name\u0022 ) ?? name, texture, color, normal, Flag( \u0022isMainTexture\u0022 ), Flag( \u0022isMainColor\u0022 ), Flag( \u0022m_Hidden\u0022 ) || Flag( \u0022hidden\u0022 ) ) );\n\t}\n\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/ImportWorkspace.cs","FileName":"ImportWorkspace.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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 project-root Imported/.iup-temp, outside Assets. 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 = GetRoot( assetsDirectory );\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, Warnings ); throw; }\n\t}\n\n\tpublic static string[] CleanupAbandoned( string assetsDirectory )\n\t{\n\t\tvar root = GetRoot( assetsDirectory );\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, warnings );\n\t\treturn warnings.ToArray();\n\t}\n\n\t// Always use the project\u0027s storage root, independent of the final import destination.\n\tstatic string GetRoot( string assetsDirectory ) =\u003E Path.Combine( ImportMergePlan.GetDestination( assetsDirectory, false ), \u0022.iup-temp\u0022 );\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 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, List\u003Cstring\u003E warnings )\n\t{\n\t\t// Only prune .iup-temp and its project-level Imported parent, never their contents.\n\t\tforeach ( var directory in new[] { root, Path.GetDirectoryName( root ) } )\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tUnityImport.CheckDirectory( directory );\n\t\t\t\tImportStorage.Retry( () =\u003E\n\t\t\t\t{\n\t\t\t\t\tif ( Directory.Exists( directory ) \u0026\u0026 !Directory.EnumerateFileSystemEntries( directory ).Any() )\n\t\t\t\t\t\tDirectory.Delete( directory, false );\n\t\t\t\t}, 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\twarnings.Add( $\u0022Empty import folder could not be removed: {directory}. Cleanup will be retried next time the importer opens. {ex.Message}\u0022 );\n\t\t\t}\n\t\t}\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, Warnings ); }\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":410869,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.IO.Compression;\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\t{\n\t\t// Without material objects the model compiler names the implicit slot after the mesh.\n\t\tpublic IEnumerable\u003Cstring\u003E EffectiveSlots =\u003E Slots.Length == 0 ? new[] { Mesh } : Slots;\n\t}\n\t/// \u003Csummary\u003EOne prefab material reference and how its renderer/slot was identified, for the import report.\u003C/summary\u003E\n\tpublic sealed record PrefabSlotEvidence( string Prefab, string Slot, long Id, string Mesh, int Index, string Material, string IdSource, string Problem )\n\t{\n\t\tpublic bool Applied { get; init; }\n\t}\n\tpublic Dictionary\u003Clong, RendererBinding\u003E RendererSlots { get; } = new();\n\treadonly Dictionary\u003Clong, RendererBinding\u003E metadataMeshes = new();\n\treadonly HashSet\u003Clong\u003E metadataRenderers = new();\n\t/// \u003Csummary\u003EEvery sub-object ID listed in the model\u0027s metadata tables, whether or not it was matched.\u003C/summary\u003E\n\tinternal readonly HashSet\u003Clong\u003E MetadataObjectIds = new();\n\tinternal readonly Dictionary\u003Clong, RendererBinding\u003E ReconstructedMeshes = new();\n\t/// \u003Csummary\u003EEach uniquely named mesh node\u0027s transform relative to the model root, in file units and axes (as ModelDoc bakes it).\u003C/summary\u003E\n\tinternal readonly Dictionary\u003Cstring, UnityTransform\u003E MeshNodes = new( StringComparer.Ordinal );\n\t/// \u003Csummary\u003EEach uniquely named mesh\u0027s bounds relative to the model root, in file units and axes.\u003C/summary\u003E\n\tinternal readonly Dictionary\u003Cstring, (double[] Min, double[] Max)\u003E MeshBounds = new( StringComparer.Ordinal );\n\t/// \u003Csummary\u003EThe file\u0027s axis system as a basis change into FBX\u0027s default Y-up axes.\u003C/summary\u003E\n\tinternal UnityTransform FileAxes { get; set; } = UnityTransform.Identity;\n\t/// \u003Csummary\u003EThe transform (file units and axes) of the file\u0027s only top node, which Unity makes the prefab root; null otherwise.\u003C/summary\u003E\n\tinternal UnityTransform CollapsedRoot { get; set; }\n\t/// \u003Csummary\u003EReconstructed IDs of interchangeable same-path objects, numbered in assumed traversal order.\u003C/summary\u003E\n\tinternal readonly HashSet\u003Clong\u003E DuplicateIds = new();\n\t/// \u003Csummary\u003ETrue when every mesh node received reconstructed renderer IDs (no malformed or ambiguous nodes).\u003C/summary\u003E\n\tinternal bool RendererIdsComplete { get; set; }\n\t// IDs read from model metadata tables are distinguished from IDs reconstructed from the FBX hierarchy.\n\tinternal string RendererIdSource( long id ) =\u003E metadataRenderers.Contains( id ) ? \u0022metadata\u0022 : DuplicateIds.Contains( id ) ? \u0022reconstructed-duplicate\u0022 : \u0022reconstructed\u0022;\n\tinternal string MeshIdSource( long id ) =\u003E metadataMeshes.ContainsKey( id ) ? \u0022metadata\u0022 : DuplicateIds.Contains( id ) ? \u0022reconstructed-duplicate\u0022 : \u0022reconstructed\u0022;\n\treadonly Dictionary\u003Clong, string\u003E metadataMaterials = new();\n\t/// \u003Csummary\u003EThe FBX material whose embedded Unity material has this sub-asset ID, or null.\u003C/summary\u003E\n\tinternal string EmbeddedMaterialName( long id ) =\u003E metadataMaterials.GetValueOrDefault( id ) ?? Materials.FirstOrDefault( name =\u003E UnityFileId.Material( name ) == id );\n\t/// \u003Csummary\u003ESlots whose prefab assignment is the model\u0027s own embedded material, generated by Unity from the FBX.\u003C/summary\u003E\n\tpublic HashSet\u003Cstring\u003E EmbeddedSlots { get; } = new( StringComparer.OrdinalIgnoreCase );\n\tinternal RendererBinding ResolveMesh( long id ) =\u003E metadataMeshes.GetValueOrDefault( id ) ?? ReconstructedMeshes.GetValueOrDefault( id );\n\n\t/// \u003Csummary\u003ESub-object IDs recorded in model metadata, as (Unity class ID, file ID, object name).\u003C/summary\u003E\n\tinternal static IEnumerable\u003C(int Class, long Id, string Name)\u003E MetadataIds( YamlNode metadata )\n\t{\n\t\t// Before Unity 2019.3: \u0022fileID: name\u0022. Legacy IDs encode the class in their leading digits (2300000 = MeshRenderer).\n\t\tforeach ( var (key, value) in metadata?.Find( \u0022fileIDToRecycleName\u0022 )?.Pairs ?? Array.Empty\u003CKeyValuePair\u003Cstring, YamlNode\u003E\u003E() )\n\t\t\tif ( long.TryParse( key, out var id ) \u0026\u0026 value.Value is string name ) yield return ((int)(id / 100000), id, name);\n\t\t// Unity 2019.3\u002B: \u0022- first: {classID: fileID}, second: name\u0022, keeping IDs from earlier imports.\n\t\tforeach ( var entry in metadata?.Find( \u0022internalIDToNameTable\u0022 )?.Items ?? Array.Empty\u003CYamlNode\u003E() )\n\t\t\tif ( entry[\u0022first\u0022]?.Pairs is [var (type, value)] \u0026\u0026 int.TryParse( type, out var classId ) \u0026\u0026 value.Integer is long id \u0026\u0026 entry[\u0022second\u0022]?.Value is string name )\n\t\t\t\tyield return (classId, id, name);\n\t}\n\n\tvoid ApplyMetadataIds( YamlNode metadata, RendererBinding[] bindings = null )\n\t{\n\t\tbindings ??= RendererSlots.Values.Distinct().ToArray();\n\t\tforeach ( var (classId, id, name) in MetadataIds( metadata ) )\n\t\t{\n\t\t\tMetadataObjectIds.Add( id );\n\t\t\tif ( classId == 21 ) metadataMaterials.TryAdd( id, name ); // Material sub-asset generated inside the model\n\t\t\tvar matches = name == \u0022//RootNode\u0022 ? bindings : bindings.Where( b =\u003E b.Mesh == name ).ToArray();\n\t\t\t// Older Unity names repeated objects \u0022Name 1\u0022, \u0022Name 2\u0022; only interchangeable duplicates share one binding.\n\t\t\tif ( matches.Length == 0 \u0026\u0026 Regex.Match( name, @\u0022^(.\u002B) [0-9]\u002B$\u0022 ) is { Success: true } numbered )\n\t\t\t\tmatches = bindings.Where( b =\u003E b.Mesh == numbered.Groups[1].Value ).ToArray();\n\t\t\tif ( matches.Length != 1 ) continue;\n\t\t\tif ( classId is 23 or 137 \u0026\u0026 RendererSlots.TryAdd( id, matches[0] ) ) metadataRenderers.Add( id ); // MeshRenderer or SkinnedMeshRenderer\n\t\t\tif ( classId == 43 ) metadataMeshes.TryAdd( id, matches[0] ); // Mesh\n\t\t}\n\t}\n\t/// \u003Csummary\u003EA slot material chosen by the model\u0027s own import settings (externalObjects or Unity\u0027s legacy material search).\u003C/summary\u003E\n\tpublic sealed record ImporterMaterial( string Guid, string Rule );\n\tpublic Dictionary\u003Cstring, ImporterMaterial\u003E ImporterMaterials { get; } = new( StringComparer.OrdinalIgnoreCase );\n\t/// \u003Csummary\u003EHow Unity imports this model\u0027s materials: none, embedded, or external with its naming/search settings.\u003C/summary\u003E\n\tpublic string MaterialImport { get; internal set; }\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\t/// \u003Csummary\u003EAssignment decisions that are not problems, such as which prefab variant became the default.\u003C/summary\u003E\n\tpublic List\u003Cstring\u003E AssignmentNotes { get; } = new();\n\t/// \u003Csummary\u003EOne distinct set of slot materials used by Unity prefab placements, and the prefabs that use it.\u003C/summary\u003E\n\tpublic sealed record PrefabVariant( string[] Prefabs, IReadOnlyDictionary\u003Cstring, string\u003E Slots );\n\tpublic List\u003CPrefabVariant\u003E PrefabVariants { get; } = new();\n\t/// \u003Csummary\u003ESlots whose default is the most common of several conflicting prefab assignments.\u003C/summary\u003E\n\tpublic HashSet\u003Cstring\u003E MostCommonSlots { get; } = new( StringComparer.OrdinalIgnoreCase );\n\tpublic List\u003CPrefabSlotEvidence\u003E PrefabEvidence { get; } = new();\n\tpublic bool CollisionOnly { get; internal set; }\n\t/// \u003Csummary\u003EThe model has no faces (an empty OBJ), so the model compiler cannot load it.\u003C/summary\u003E\n\tpublic bool Empty { get; internal set; }\n\t/// \u003Csummary\u003EWhy the model file could not be read (it is then \u003Csee cref=\u0022Empty\u0022/\u003E), or null.\u003C/summary\u003E\n\tpublic string ReadError { get; internal set; }\n\t/// \u003Csummary\u003EUnity\u0027s \u0022Generate Colliders\u0022 import setting: every mesh of the model gets an exact MeshCollider.\u003C/summary\u003E\n\tpublic bool GenerateColliders { 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 ) =\u003E ImportScale( metadata == null ? null : UnityYaml.Document( metadata ) );\n\tinternal double ImportScale( YamlNode metadata )\n\t{\n\t\tdouble Setting( string key, double fallback ) =\u003E metadata?.Find( key )?.Number ?? fallback;\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\t/// \u003Csummary\u003EMeshes Unity never shows: every prefab or scene placement names its meshes explicitly and none names these.\u003C/summary\u003E\n\tpublic HashSet\u003Cstring\u003E HiddenMeshes { get; } = new( StringComparer.Ordinal );\n\t/// \u003Csummary\u003EMeshes the VMDL imports: the highest LOD when LODs exist, without hidden meshes.\u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// The meshes a whole-model VMDL shows: with LOD-named meshes, LOD0 and every mesh without a LOD suffix (Unity renders\n\t/// those at every level); the lower levels come in as LOD levels instead.\n\t/// \u003C/summary\u003E\n\tpublic string[] ImportedMeshes =\u003E (HighestDetailMeshes.Length \u003E 0 ? Meshes.Where( m =\u003E !LowerLod.IsMatch( m ) ).ToArray() : Meshes.ToArray()).Where( m =\u003E !HiddenMeshes.Contains( m ) ).ToArray();\n\tstatic readonly Regex LowerLod = new( @\u0022(?i)(?:^|[_ .-])LOD0*[1-9][0-9]*(?:$|[_ .-])\u0022 );\n\t/// \u003Csummary\u003EWhether the mesh is a lower LOD level (LOD1 and up) of a model that has LOD0 meshes.\u003C/summary\u003E\n\tpublic bool IsLowerLod( string mesh ) =\u003E HighestDetailMeshes.Length \u003E 0 \u0026\u0026 LowerLod.IsMatch( mesh );\n\t/// \u003Csummary\u003E\n\t/// Meshes grouped by LOD level, from names ending in LOD0, LOD1 and so on (Unity builds a LOD group from them), when the\n\t/// file has a LOD0 and at least one lower level; otherwise empty.\n\t/// \u003C/summary\u003E\n\tpublic IReadOnlyList\u003Cstring[]\u003E LodLevels\n\t{\n\t\tget\n\t\t{\n\t\t\tvar levels = Meshes.Where( m =\u003E !HiddenMeshes.Contains( m ) ).Select( m =\u003E (Mesh: m, Match: Regex.Match( m, @\u0022(?i)(?:^|[_ .-])LOD([0-9])(?:$|[_ .-])\u0022 )) )\n\t\t\t\t.Where( x =\u003E x.Match.Success ).GroupBy( x =\u003E int.Parse( x.Match.Groups[1].Value, CultureInfo.InvariantCulture ) ).OrderBy( g =\u003E g.Key ).ToList();\n\t\t\treturn levels.Count \u003C 2 || levels[0].Key != 0 ? Array.Empty\u003Cstring[]\u003E() : levels.Select( g =\u003E g.Select( x =\u003E x.Mesh ).ToArray() ).ToArray();\n\t\t}\n\t}\n\t/// \u003Csummary\u003E\n\t/// Switch thresholds for \u003Csee cref=\u0022LodLevels\u0022/\u003E: the model\u0027s LOD screen percentages, or Unity\u0027s defaults (60%, 30%,\n\t/// 10%, then halving), as s\u0026amp;box switch distances for the LOD group Unity builds: its size is the largest axis of\n\t/// all levels\u0027 bounds, scaled to inches by the model\u0027s import scale.\n\t/// \u003C/summary\u003E\n\tpublic double[] LodThresholds( YamlNode metadata, double scale )\n\t{\n\t\tvar levels = LodLevels.Count;\n\t\tvar size = LodSize( scale ) ?? UnityImport.DefaultLodSize;\n\t\t// Unity serializes m_LODScreenPercentages as \u0022lODScreenPercentages\u0022 (the older spelling is read too).\n\t\tvar percentages = ((metadata?.Find( \u0022lODScreenPercentages\u0022 ) ?? metadata?.Find( \u0022lodScreenPercentages\u0022 ))?.Items ?? Array.Empty\u003CYamlNode\u003E()).Select( i =\u003E i.Number ?? 0 ).Where( v =\u003E v \u003E 0 ).ToList();\n\t\tvar defaults = new List\u003Cdouble\u003E { 0.6, 0.3, 0.1 };\n\t\twhile ( defaults.Count \u003C levels ) defaults.Add( defaults[^1] / 2 );\n\t\treturn Enumerable.Range( 0, Math.Max( 0, levels - 1 ) ).Select( i =\u003E UnityImport.LodThreshold( i \u003C percentages.Count ? percentages[i] : defaults[i], size ) ).ToArray();\n\t}\n\t/// \u003Csummary\u003EThe largest axis of the LOD levels\u0027 combined bounds in inches, or null when a level\u0027s bounds are unknown.\u003C/summary\u003E\n\tinternal double? LodSize( double scale )\n\t{\n\t\tvar meshes = LodLevels.SelectMany( l =\u003E l ).ToArray();\n\t\tif ( meshes.Length == 0 || meshes.Any( m =\u003E !MeshBounds.ContainsKey( m ) ) ) return null;\n\t\tvar size = Enumerable.Range( 0, 3 ).Max( a =\u003E meshes.Max( m =\u003E MeshBounds[m].Max[a] ) - meshes.Min( m =\u003E MeshBounds[m].Min[a] ) ) * scale;\n\t\treturn size \u003E 0 \u0026\u0026 double.IsFinite( size ) ? size : null;\n\t}\n\t/// \u003Csummary\u003EThe import filter\u0027s included meshes, or empty when every mesh is imported.\u003C/summary\u003E\n\tpublic string[] FilteredMeshes =\u003E HighestDetailMeshes.Length \u003E 0 || HiddenMeshes.Count \u003E 0 ? ImportedMeshes : Array.Empty\u003Cstring\u003E();\n\tpublic string[] RenderedMaterials\n\t{\n\t\tget\n\t\t{\n\t\t\tvar meshes = FilteredMeshes;\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 ? bindings.SelectMany( b =\u003E b.EffectiveSlots ).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\t/// \u003Csummary\u003EOBJ meshes and slots: one mesh per object (or per group when the file has no objects), a slot per material used.\u003C/summary\u003E\n\tstatic UnityModel ReadObj( UnityAsset asset )\n\t{\n\t\tvar result = new UnityModel();\n\t\tvar objects = new List\u003C(string Name, List\u003Cstring\u003E Slots, bool Faces)\u003E();\n\t\tvar groups = new List\u003C(string Name, List\u003Cstring\u003E Slots, bool Faces)\u003E();\n\t\tint Open( List\u003C(string Name, List\u003Cstring\u003E Slots, bool Faces)\u003E list, string name )\n\t\t{\n\t\t\tvar index = list.FindIndex( e =\u003E e.Name == name );\n\t\t\tif ( index \u003C 0 ) { list.Add( (name, new List\u003Cstring\u003E(), false) ); index = list.Count - 1; }\n\t\t\treturn index;\n\t\t}\n\t\tint currentObject = -1, currentGroup = -1;\n\t\tstring material = null;\n\t\tvar faces = false;\n\t\t// Vertex bounds per object and per group; OBJ vertices belong to the object or group they follow.\n\t\tvar bounds = new Dictionary\u003Cstring, (double[] Min, double[] Max)\u003E();\n\t\tvoid Extend( string key, double[] v )\n\t\t{\n\t\t\tif ( key == null ) return;\n\t\t\tif ( !bounds.TryGetValue( key, out var b ) ) bounds[key] = b = (v.ToArray(), v.ToArray());\n\t\t\tfor ( var a = 0; a \u003C 3; a\u002B\u002B ) { b.Min[a] = Math.Min( b.Min[a], v[a] ); b.Max[a] = Math.Max( b.Max[a], v[a] ); }\n\t\t}\n\t\tvoid Use( List\u003C(string Name, List\u003Cstring\u003E Slots, bool Faces)\u003E list, int index )\n\t\t{\n\t\t\tif ( index \u003C 0 ) return;\n\t\t\tvar entry = list[index];\n\t\t\tif ( material != null \u0026\u0026 !entry.Slots.Contains( material ) ) entry.Slots.Add( material );\n\t\t\tlist[index] = entry with { Faces = true };\n\t\t}\n\t\tusing ( var reader = new StreamReader( asset.Source ) )\n\t\t{\n\t\t\tstring line;\n\t\t\twhile ( (line = reader.ReadLine()) != null )\n\t\t\t{\n\t\t\t\tvar parts = line.Trim().Split( (char[])null, 2, StringSplitOptions.RemoveEmptyEntries );\n\t\t\t\tif ( parts.Length == 0 ) continue;\n\t\t\t\tvar value = parts.Length \u003E 1 ? parts[1].Trim() : \u0022\u0022;\n\t\t\t\tif ( parts[0] == \u0022o\u0022 ) currentObject = Open( objects, value );\n\t\t\t\telse if ( parts[0] == \u0022g\u0022 ) currentGroup = Open( groups, value );\n\t\t\t\telse if ( parts[0] == \u0022usemtl\u0022 \u0026\u0026 value.Length \u003E 0 )\n\t\t\t\t{\n\t\t\t\t\tmaterial = value;\n\t\t\t\t\tif ( !result.Materials.Contains( value ) ) result.Materials.Add( value );\n\t\t\t\t}\n\t\t\t\telse if ( parts[0] == \u0022f\u0022 ) { Use( objects, currentObject ); Use( groups, currentGroup ); faces = true; }\n\t\t\t\telse if ( parts[0] == \u0022v\u0022 \u0026\u0026 value.Split( (char[])null, StringSplitOptions.RemoveEmptyEntries ) is { Length: \u003E= 3 } xyz \u0026\u0026\n\t\t\t\t\txyz.Take( 3 ).Select( t =\u003E double.TryParse( t, NumberStyles.Float, CultureInfo.InvariantCulture, out var n ) ? n : double.NaN ).ToArray() is var v \u0026\u0026 v.All( double.IsFinite ) )\n\t\t\t\t{\n\t\t\t\t\tExtend( currentObject \u003E= 0 ? \u0022o:\u0022 \u002B objects[currentObject].Name : null, v );\n\t\t\t\t\tExtend( currentGroup \u003E= 0 ? \u0022g:\u0022 \u002B groups[currentGroup].Name : null, v );\n\t\t\t\t\tExtend( \u0022file\u0022, v );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tvar meshes = (objects.Count \u003E 0 ? objects : groups).Where( e =\u003E e.Faces \u0026\u0026 e.Name.Length \u003E 0 ).ToList();\n\t\t// Without named objects or groups the whole file is one mesh, named by Unity after the file.\n\t\tresult.Empty = !faces;\n\t\tif ( meshes.Count == 0 ) meshes.Add( (Path.GetFileNameWithoutExtension( asset.Path ), result.Materials, true) );\n\t\tresult.Meshes.AddRange( meshes.Select( m =\u003E m.Name ) );\n\t\tvar prefix = objects.Count \u003E 0 ? \u0022o:\u0022 : \u0022g:\u0022;\n\t\tforeach ( var mesh in result.Meshes )\n\t\t\tif ( bounds.TryGetValue( result.Meshes.Count == 1 \u0026\u0026 !bounds.ContainsKey( prefix \u002B mesh ) ? \u0022file\u0022 : prefix \u002B mesh, out var box ) ) result.MeshBounds[mesh] = box;\n\t\tresult.ApplyMetadataIds( asset.MetadataDocument, meshes.Select( m =\u003E new RendererBinding( m.Name, m.Slots.ToArray() ) ).ToArray() );\n\t\treturn result;\n\t}\n\n\tpublic static UnityModel Read( UnityAsset asset )\n\t{\n\t\tvar result = new UnityModel();\n\t\tif ( asset.Path.EndsWith( \u0022.obj\u0022, StringComparison.OrdinalIgnoreCase ) ) return ReadObj( asset );\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.FinishAscii();\n\t\t\tasciiBindings.Apply( result, asset.MetadataDocument?.Find( \u0022preserveHierarchy\u0022 )?.Value == \u00221\u0022 );\n\t\t\tresult.ApplyMetadataIds( asset.MetadataDocument );\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\t// Polygon material indices, read only for their first-use order (Unity\u0027s submesh order).\n\t\tList\u003Cint\u003E MaterialIndices( long end )\n\t\t{\n\t\t\tvar type = (char)reader.ReadByte();\n\t\t\tif ( type != \u0027i\u0027 ) return null;\n\t\t\tvar count = reader.ReadUInt32();\n\t\t\tvar encoding = reader.ReadUInt32();\n\t\t\tvar length = reader.ReadUInt32();\n\t\t\tif ( count \u003E 64 * 1024 * 1024 || stream.Position \u002B length \u003E end ) throw new InvalidDataException( \u0022Invalid FBX material array.\u0022 );\n\t\t\tvar bytes = reader.ReadBytes( (int)length );\n\t\t\tusing var data = encoding == 1 ? (Stream)new ZLibStream( new MemoryStream( bytes ), CompressionMode.Decompress ) : new MemoryStream( bytes );\n\t\t\tusing var values = new BinaryReader( data );\n\t\t\tvar order = new List\u003Cint\u003E();\n\t\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar index = values.ReadInt32();\n\t\t\t\tif ( !order.Contains( index ) ) order.Add( index );\n\t\t\t}\n\t\t\treturn order;\n\t\t}\n\t\t// Control points, read only for their bounds.\n\t\tvoid VertexBounds( long owner, long end )\n\t\t{\n\t\t\tvar type = (char)reader.ReadByte();\n\t\t\tif ( type is not (\u0027d\u0027 or \u0027f\u0027) ) return;\n\t\t\tvar count = reader.ReadUInt32();\n\t\t\tvar encoding = reader.ReadUInt32();\n\t\t\tvar length = reader.ReadUInt32();\n\t\t\tif ( count \u003E 256 * 1024 * 1024 || stream.Position \u002B length \u003E end ) throw new InvalidDataException( \u0022Invalid FBX vertex array.\u0022 );\n\t\t\tvar bytes = reader.ReadBytes( (int)length );\n\t\t\tusing var data = encoding == 1 ? (Stream)new ZLibStream( new MemoryStream( bytes ), CompressionMode.Decompress ) : new MemoryStream( bytes );\n\t\t\tusing var values = new BinaryReader( data );\n\t\t\tvar bounds = new UnityFbxBindings.BoundsBuilder( owner );\n\t\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B ) bounds.Add( type == \u0027d\u0027 ? values.ReadDouble() : values.ReadSingle() );\n\t\t\tbounds.Finish( bindings );\n\t\t}\n\t\tvoid ReadNodes( long limit, bool objects = false, bool connections = false, long textureId = 0, bool settings = false, long owner = 0, bool layer = false, long node = 0 )\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 2000000 ) 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\tif ( type == \u0027C\u0027 ) return reader.ReadByte() != 0;\n\t\t\t\t\t\tif ( type == \u0027Y\u0027 ) return reader.ReadInt16();\n\t\t\t\t\t\tif ( type is \u0027f\u0027 or \u0027d\u0027 or \u0027l\u0027 or \u0027i\u0027 or \u0027b\u0027 )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\treader.ReadUInt32(); reader.ReadUInt32();\n\t\t\t\t\t\t\tvar bytes = reader.ReadUInt32();\n\t\t\t\t\t\t\tif ( stream.Position \u002B bytes \u003E children ) throw new InvalidDataException( \u0022Invalid FBX array.\u0022 );\n\t\t\t\t\t\t\tstream.Position \u002B= bytes;\n\t\t\t\t\t\t\treturn null;\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 or \u0022Geometry\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\t// FBX 7 keeps polygon materials on Geometry objects; FBX 6 keeps them on the mesh model.\n\t\t\t\t\t// Every model node also carries its transform properties.\n\t\t\t\t\tif ( name == \u0022Model\u0022 || name == \u0022Geometry\u0022 ) { stream.Position = children; ReadNodes( end, owner: id ); }\n\t\t\t\t}\n\t\t\t\telse if ( owner != 0 \u0026\u0026 !layer \u0026\u0026 name == \u0022LayerElementMaterial\u0022 \u0026\u0026 !bindings.MaterialOrder.ContainsKey( owner ) ) { stream.Position = children; ReadNodes( end, owner: owner, layer: true ); }\n\t\t\t\telse if ( layer \u0026\u0026 name == \u0022Materials\u0022 \u0026\u0026 properties \u003E= 1 \u0026\u0026 MaterialIndices( end ) is { } order ) bindings.MaterialOrder[owner] = order;\n\t\t\t\telse if ( owner != 0 \u0026\u0026 !layer \u0026\u0026 name == \u0022Vertices\u0022 \u0026\u0026 properties \u003E= 1 \u0026\u0026 !bindings.VertexBounds.ContainsKey( owner ) )\n\t\t\t\t{\n\t\t\t\t\t// Bounds only size LOD groups: an unreadable vertex array leaves the mesh without bounds, not the model unread.\n\t\t\t\t\ttry { VertexBounds( owner, end ); }\n\t\t\t\t\tcatch ( Exception ex ) when ( ex is InvalidDataException or EndOfStreamException ) { }\n\t\t\t\t}\n\t\t\t\telse if ( owner != 0 \u0026\u0026 !layer \u0026\u0026 name is \u0022Properties70\u0022 or \u0022Properties60\u0022 \u0026\u0026 bindings.Nodes.ContainsKey( owner ) ) { stream.Position = children; ReadNodes( end, node: owner ); }\n\t\t\t\telse if ( node != 0 \u0026\u0026 name is \u0022P\u0022 or \u0022Property\u0022 \u0026\u0026 properties \u003E= 2 \u0026\u0026 properties \u003C= 16 )\n\t\t\t\t{\n\t\t\t\t\tvar propertyName = Property() as string;\n\t\t\t\t\tvar values = new List\u003Cdouble\u003E();\n\t\t\t\t\tfor ( var index = 1; index \u003C properties; index\u002B\u002B ) if ( Property() is var value \u0026\u0026 value is double or float or int or long or short ) values.Add( Convert.ToDouble( value, CultureInfo.InvariantCulture ) );\n\t\t\t\t\tif ( propertyName != null ) bindings.Property( node, propertyName, values.ToArray() );\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\tvar propertyName = Property() as string;\n\t\t\t\t\tobject value = null;\n\t\t\t\t\tfor ( var index = 1; index \u003C properties; index\u002B\u002B ) value = Property();\n\t\t\t\t\tif ( value is double or float or int or long or short )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar number = Convert.ToDouble( value, CultureInfo.InvariantCulture );\n\t\t\t\t\t\tif ( propertyName == \u0022UnitScaleFactor\u0022 \u0026\u0026 number \u003E 0 \u0026\u0026 double.IsFinite( number ) ) result.UnitScaleCentimeters = number;\n\t\t\t\t\t\telse if ( propertyName != null ) bindings.Property( 0, propertyName, new[] { number } );\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.MetadataDocument?.Find( \u0022preserveHierarchy\u0022 )?.Value == \u00221\u0022 );\n\t\tresult.ApplyMetadataIds( asset.MetadataDocument );\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 )\n\t{\n\t\t// Model import settings first: prefab conflicts fall back to them.\n\t\tUnityModelMaterials.Apply( archive );\n\t\tUnityPrefabBindings.Apply( archive );\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnitySerializedFile.cs","FileName":"UnitySerializedFile.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"Code":"using System;\nusing System.Buffers.Binary;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003E\n/// Reads Unity\u0027s binary serialization (a SerializedFile), as the editor saves assets in binary mode, such as terrain data:\n/// the header, the type trees that describe each class\u0027s fields, the object table and the external references. Objects\n/// are read through their type tree into plain values: dictionaries for structures, lists for arrays of structures, typed\n/// arrays (byte[], short[], float[] and so on) for arrays of numbers, and numbers, booleans and strings. Format versions\n/// 12 to 22 (Unity 5 to 6) are supported when the file includes type trees, as editor files do.\n/// \u003C/summary\u003E\ninternal sealed class UnitySerializedFile\n{\n\tpublic sealed class TypeNode\n\t{\n\t\tpublic string Type { get; init; }\n\t\tpublic string Name { get; init; }\n\t\tpublic bool IsArray { get; init; }\n\t\t/// \u003Csummary\u003EUnity\u0027s \u0022align after reading\u0022 meta flag.\u003C/summary\u003E\n\t\tpublic bool Align { get; init; }\n\t\tpublic List\u003CTypeNode\u003E Children { get; } = new();\n\t}\n\tpublic sealed record ObjectInfo( long PathId, int ClassId, long Offset, long Size, TypeNode Tree );\n\t/// \u003Csummary\u003EA file referenced by PPtr file index (1-based): the asset GUID as Unity writes it in text files.\u003C/summary\u003E\n\tpublic sealed record External( string Guid, int Type, string Path );\n\n\tpublic int Version { get; private set; }\n\tpublic IReadOnlyList\u003CObjectInfo\u003E Objects { get; private set; }\n\tpublic IReadOnlyList\u003CExternal\u003E Externals { get; private set; }\n\treadonly string path;\n\tbool bigEndian;\n\tUnitySerializedFile( string path ) { this.path = path; }\n\n\tconst int MinimumVersion = 12, MaximumVersion = 22;\n\n\t/// \u003Csummary\u003EWhether the file starts with a SerializedFile header (rather than Unity\u0027s text serialization).\u003C/summary\u003E\n\tpublic static bool Is( string path )\n\t{\n\t\ttry\n\t\t{\n\t\t\tusing var stream = File.OpenRead( path );\n\t\t\tvar header = new byte[48];\n\t\t\tvar read = stream.Read( header, 0, header.Length );\n\t\t\treturn read \u003E= 20 \u0026\u0026 Header( header, stream.Length ) != null;\n\t\t}\n\t\tcatch ( IOException ) { return false; }\n\t}\n\n\tstatic (int Version, long MetadataSize, long DataOffset, int Start, bool BigEndian)? Header( byte[] header, long length )\n\t{\n\t\tlong metadataSize = BinaryPrimitives.ReadUInt32BigEndian( header ), fileSize = BinaryPrimitives.ReadUInt32BigEndian( header.AsSpan( 4 ) );\n\t\tvar version = (int)BinaryPrimitives.ReadUInt32BigEndian( header.AsSpan( 8 ) );\n\t\tlong dataOffset = BinaryPrimitives.ReadUInt32BigEndian( header.AsSpan( 12 ) );\n\t\tif ( version \u003C 9 || version \u003E 64 ) return null;\n\t\tvar bigEndian = header[16] != 0;\n\t\tvar start = 20;\n\t\tif ( version \u003E= 22 )\n\t\t{\n\t\t\tmetadataSize = BinaryPrimitives.ReadUInt32BigEndian( header.AsSpan( 20 ) );\n\t\t\tfileSize = BinaryPrimitives.ReadInt64BigEndian( header.AsSpan( 24 ) );\n\t\t\tdataOffset = BinaryPrimitives.ReadInt64BigEndian( header.AsSpan( 32 ) );\n\t\t\tstart = 48;\n\t\t}\n\t\tif ( fileSize != length || dataOffset \u003E fileSize || metadataSize \u003C= 0 || start \u002B metadataSize \u003E fileSize ) return null;\n\t\treturn (version, metadataSize, dataOffset, start, bigEndian);\n\t}\n\n\t/// \u003Csummary\u003EReads the header, type trees, object table and external references; object data is read on demand.\u003C/summary\u003E\n\tpublic static UnitySerializedFile Read( string path )\n\t{\n\t\tusing var stream = File.OpenRead( path );\n\t\tvar head = new byte[48];\n\t\tif ( stream.Read( head, 0, head.Length ) \u003C 20 || Header( head, stream.Length ) is not { } header )\n\t\t\tthrow new InvalidDataException( \u0022Not a Unity binary serialized file.\u0022 );\n\t\tif ( header.Version \u003C MinimumVersion || header.Version \u003E MaximumVersion )\n\t\t\tthrow new InvalidDataException( $\u0022Unity serialized file format {header.Version} is not supported (supported: {MinimumVersion}-{MaximumVersion}).\u0022 );\n\t\tif ( header.MetadataSize \u003E 64 * 1024 * 1024 ) throw new InvalidDataException( \u0022The serialized file\u0027s metadata is too large.\u0022 );\n\t\tvar metadata = new byte[header.Start \u002B header.MetadataSize];\n\t\tstream.Position = 0;\n\t\tstream.ReadExactly( metadata );\n\t\tvar file = new UnitySerializedFile( path ) { Version = header.Version, bigEndian = header.BigEndian };\n\t\tvar reader = new Reader( metadata, header.BigEndian ) { Position = header.Start };\n\t\tvar v = header.Version;\n\t\treader.CString(); // Unity version\n\t\treader.Int32(); // target platform\n\t\tvar typeTrees = reader.Byte() != 0;\n\t\tif ( !typeTrees ) throw new InvalidDataException( \u0022The serialized file has no type trees (a player build file), which are needed to read it.\u0022 );\n\t\tvar types = new List\u003C(int ClassId, TypeNode Tree)\u003E();\n\t\tvar typeCount = reader.Count();\n\t\tfor ( var i = 0; i \u003C typeCount; i\u002B\u002B )\n\t\t{\n\t\t\tvar classId = reader.Int32();\n\t\t\tif ( v \u003E= 16 ) reader.Byte(); // stripped\n\t\t\tif ( v \u003E= 17 ) reader.Int16(); // script type index\n\t\t\tif ( v \u003E= 16 ? classId == 114 : classId \u003C 0 ) reader.Skip( 16 ); // script ID\n\t\t\treader.Skip( 16 ); // type hash\n\t\t\tvar tree = ReadTypeTree( reader, v );\n\t\t\tif ( v \u003E= 21 ) reader.Skip( 4 * reader.Count() ); // type dependencies\n\t\t\ttypes.Add( (classId, tree) );\n\t\t}\n\t\tif ( v \u003C 14 ) reader.Int32(); // 64-bit path IDs (format 14 always uses them)\n\t\tvar objectCount = reader.Count();\n\t\tvar objects = new List\u003CObjectInfo\u003E( objectCount );\n\t\tfor ( var i = 0; i \u003C objectCount; i\u002B\u002B )\n\t\t{\n\t\t\tif ( v \u003E= 14 ) reader.Align();\n\t\t\tvar pathId = v \u003E= 14 ? reader.Int64() : reader.Int32();\n\t\t\tvar offset = (v \u003E= 22 ? reader.Int64() : reader.UInt32()) \u002B header.DataOffset;\n\t\t\tlong size = reader.UInt32();\n\t\t\tvar typeId = reader.Int32();\n\t\t\t(int ClassId, TypeNode Tree) type;\n\t\t\tif ( v \u003C 16 )\n\t\t\t{\n\t\t\t\tvar classId = reader.UInt16();\n\t\t\t\ttype = types.FirstOrDefault( t =\u003E t.ClassId == typeId );\n\t\t\t\tif ( type.Tree == null ) type = (classId, null);\n\t\t\t\tif ( v \u003E= 11 ) reader.Int16(); // script type index\n\t\t\t\tif ( v == 15 ) reader.Byte(); // stripped\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tif ( typeId \u003C 0 || typeId \u003E= types.Count ) throw new InvalidDataException( \u0022A serialized object names an unknown type.\u0022 );\n\t\t\t\ttype = types[typeId];\n\t\t\t\tif ( v == 16 ) reader.Byte(); // stripped\n\t\t\t}\n\t\t\tif ( offset \u003C 0 || size \u003C 0 || offset \u002B size \u003E stream.Length ) throw new InvalidDataException( \u0022A serialized object lies outside the file.\u0022 );\n\t\t\tobjects.Add( new( pathId, type.ClassId, offset, size, type.Tree ) );\n\t\t}\n\t\tvar scriptCount = reader.Count();\n\t\tfor ( var i = 0; i \u003C scriptCount; i\u002B\u002B )\n\t\t{\n\t\t\treader.Int32();\n\t\t\tif ( v \u003E= 14 ) { reader.Align(); reader.Int64(); }\n\t\t\telse reader.Int32();\n\t\t}\n\t\tvar externals = new List\u003CExternal\u003E();\n\t\tvar externalCount = reader.Count();\n\t\tfor ( var i = 0; i \u003C externalCount; i\u002B\u002B )\n\t\t{\n\t\t\treader.CString(); // unused\n\t\t\tvar guid = reader.Bytes( 16 );\n\t\t\tvar type = reader.Int32();\n\t\t\texternals.Add( new( Guid( guid ), type, reader.CString() ) );\n\t\t}\n\t\tfile.Objects = objects;\n\t\tfile.Externals = externals;\n\t\treturn file;\n\t}\n\n\t/// \u003Csummary\u003EUnity writes each GUID byte low nibble first in text files and .meta files.\u003C/summary\u003E\n\tstatic string Guid( byte[] bytes ) =\u003E string.Concat( bytes.Select( b =\u003E \u00220123456789abcdef\u0022[b \u0026 15].ToString() \u002B \u00220123456789abcdef\u0022[b \u003E\u003E 4] ) );\n\n\tstatic TypeNode ReadTypeTree( Reader reader, int version )\n\t{\n\t\tvar count = reader.Count();\n\t\tvar stringSize = reader.Count();\n\t\tvar nodeSize = version \u003E= 19 ? 32 : 24;\n\t\tvar nodes = reader.Bytes( count * nodeSize );\n\t\tvar strings = reader.Bytes( stringSize );\n\t\tstring Text( uint offset )\n\t\t{\n\t\t\tif ( (offset \u0026 0x80000000) != 0 ) return CommonStrings.GetValueOrDefault( (int)(offset \u0026 0x7FFFFFFF) ) ?? $\u0022\u003Ccommon {offset \u0026 0x7FFFFFFF}\u003E\u0022;\n\t\t\tif ( offset \u003E= strings.Length ) throw new InvalidDataException( \u0022A type tree string lies outside its buffer.\u0022 );\n\t\t\tvar end = System.Array.IndexOf( strings, (byte)0, (int)offset );\n\t\t\treturn Encoding.UTF8.GetString( strings, (int)offset, (end \u003C 0 ? strings.Length : end) - (int)offset );\n\t\t}\n\t\tvar node = new Reader( nodes, reader.BigEndian );\n\t\tvar stack = new List\u003CTypeNode\u003E();\n\t\tTypeNode root = null;\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B )\n\t\t{\n\t\t\tnode.Position = i * nodeSize;\n\t\t\tnode.UInt16(); // version\n\t\t\tint level = node.Byte();\n\t\t\tvar flags = node.Byte();\n\t\t\tvar type = Text( node.UInt32() );\n\t\t\tvar name = Text( node.UInt32() );\n\t\t\tnode.Int32(); // byte size\n\t\t\tnode.Int32(); // index\n\t\t\tvar meta = node.Int32();\n\t\t\tvar current = new TypeNode { Type = type, Name = name, IsArray = (flags \u0026 1) != 0, Align = (meta \u0026 0x4000) != 0 };\n\t\t\tif ( level == 0 ) { if ( root != null ) throw new InvalidDataException( \u0022A type tree has several roots.\u0022 ); root = current; }\n\t\t\telse if ( level \u003E stack.Count ) throw new InvalidDataException( \u0022A type tree skips a level.\u0022 );\n\t\t\telse stack[level - 1].Children.Add( current );\n\t\t\tif ( stack.Count \u003E level ) stack.RemoveRange( level, stack.Count - level );\n\t\t\tstack.Add( current );\n\t\t}\n\t\treturn root ?? throw new InvalidDataException( \u0022A type tree is empty.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EReads one object through its type tree.\u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, object\u003E ReadObject( ObjectInfo info )\n\t{\n\t\tif ( info.Tree == null ) throw new InvalidDataException( $\u0022Object {info.PathId} has no type tree.\u0022 );\n\t\tif ( info.Size \u003E int.MaxValue ) throw new InvalidDataException( $\u0022Object {info.PathId} is too large.\u0022 );\n\t\tvar data = new byte[info.Size];\n\t\tusing ( var stream = File.OpenRead( path ) )\n\t\t{\n\t\t\tstream.Position = info.Offset;\n\t\t\tstream.ReadExactly( data );\n\t\t}\n\t\tvar reader = new Reader( data, bigEndian );\n\t\treturn Value( info.Tree, reader ) as Dictionary\u003Cstring, object\u003E ?? throw new InvalidDataException( $\u0022Object {info.PathId} is not a structure.\u0022 );\n\t}\n\n\tpublic ObjectInfo Find( long pathId ) =\u003E Objects.FirstOrDefault( o =\u003E o.PathId == pathId );\n\n\tstatic object Value( TypeNode node, Reader reader, int depth = 0 )\n\t{\n\t\t// Unity\u0027s type trees nest a few dozen levels; a deeper one is corrupt or crafted and stops with an error.\n\t\tif ( depth \u003E 256 ) throw new InvalidDataException( \u0022The serialized type tree is nested too deeply.\u0022 );\n\t\tvar align = node.Align;\n\t\tobject value;\n\t\tswitch ( node.Type )\n\t\t{\n\t\t\tcase \u0022SInt8\u0022: value = (long)(sbyte)reader.Byte(); break;\n\t\t\tcase \u0022UInt8\u0022 or \u0022char\u0022: value = (long)reader.Byte(); break;\n\t\t\tcase \u0022bool\u0022: value = reader.Byte() != 0; break;\n\t\t\tcase \u0022SInt16\u0022 or \u0022short\u0022: value = (long)reader.Int16(); break;\n\t\t\tcase \u0022UInt16\u0022 or \u0022unsigned short\u0022: value = (long)reader.UInt16(); break;\n\t\t\tcase \u0022SInt32\u0022 or \u0022int\u0022 or \u0022Type*\u0022: value = (long)reader.Int32(); break;\n\t\t\tcase \u0022UInt32\u0022 or \u0022unsigned int\u0022: value = (long)reader.UInt32(); break;\n\t\t\tcase \u0022SInt64\u0022 or \u0022long long\u0022 or \u0022FileSize\u0022: value = reader.Int64(); break;\n\t\t\tcase \u0022UInt64\u0022 or \u0022unsigned long long\u0022: value = (long)reader.UInt64(); break;\n\t\t\tcase \u0022float\u0022: value = (double)reader.Single(); break;\n\t\t\tcase \u0022double\u0022: value = reader.Double(); break;\n\t\t\tcase \u0022string\u0022:\n\t\t\t\tvalue = Encoding.UTF8.GetString( reader.Bytes( reader.Count() ) );\n\t\t\t\talign |= node.Children.Count \u003E 0 \u0026\u0026 node.Children[0].Align;\n\t\t\t\tbreak;\n\t\t\tcase \u0022TypelessData\u0022:\n\t\t\t\tvalue = reader.Bytes( reader.Count() );\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tif ( node.Children.Count \u003E 0 \u0026\u0026 node.Children[0].IsArray )\n\t\t\t\t{\n\t\t\t\t\t// vector, map, set and similar containers: one Array child holding size and data.\n\t\t\t\t\tvar array = node.Children[0];\n\t\t\t\t\talign |= array.Align;\n\t\t\t\t\tvalue = ReadArray( array, reader, depth );\n\t\t\t\t}\n\t\t\t\telse if ( node.IsArray ) value = ReadArray( node, reader, depth );\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tvar fields = new Dictionary\u003Cstring, object\u003E( node.Children.Count, StringComparer.Ordinal );\n\t\t\t\t\tforeach ( var child in node.Children ) fields[child.Name] = Value( child, reader, depth \u002B 1 );\n\t\t\t\t\tvalue = fields;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\t\tif ( align ) reader.Align();\n\t\treturn value;\n\t}\n\n\tstatic object ReadArray( TypeNode array, Reader reader, int depth )\n\t{\n\t\tvar count = reader.Count();\n\t\tif ( array.Children.Count \u003C 2 ) throw new InvalidDataException( \u0022A type tree array has no element type.\u0022 );\n\t\tvar element = array.Children[1];\n\t\t// Arrays of numbers are read in bulk into typed arrays.\n\t\tif ( element.Children.Count == 0 \u0026\u0026 !element.Align )\n\t\t{\n\t\t\tswitch ( element.Type )\n\t\t\t{\n\t\t\t\tcase \u0022UInt8\u0022 or \u0022char\u0022 or \u0022bool\u0022 or \u0022SInt8\u0022: return reader.Bytes( count );\n\t\t\t\tcase \u0022SInt16\u0022 or \u0022short\u0022: return reader.Array\u003Cshort\u003E( count, 2 );\n\t\t\t\tcase \u0022UInt16\u0022 or \u0022unsigned short\u0022: return reader.Array\u003Cushort\u003E( count, 2 );\n\t\t\t\tcase \u0022SInt32\u0022 or \u0022int\u0022: return reader.Array\u003Cint\u003E( count, 4 );\n\t\t\t\tcase \u0022UInt32\u0022 or \u0022unsigned int\u0022: return reader.Array\u003Cuint\u003E( count, 4 );\n\t\t\t\tcase \u0022float\u0022: return reader.Array\u003Cfloat\u003E( count, 4 );\n\t\t\t\tcase \u0022SInt64\u0022 or \u0022long long\u0022: return reader.Array\u003Clong\u003E( count, 8 );\n\t\t\t\tcase \u0022UInt64\u0022 or \u0022unsigned long long\u0022: return reader.Array\u003Culong\u003E( count, 8 );\n\t\t\t\tcase \u0022double\u0022: return reader.Array\u003Cdouble\u003E( count, 8 );\n\t\t\t}\n\t\t}\n\t\tvar items = new List\u003Cobject\u003E( Math.Min( count, 1 \u003C\u003C 20 ) );\n\t\tfor ( var i = 0; i \u003C count; i\u002B\u002B ) items.Add( Value( element, reader, depth \u002B 1 ) );\n\t\treturn items;\n\t}\n\n\tsealed class Reader\n\t{\n\t\treadonly byte[] data;\n\t\tpublic readonly bool BigEndian;\n\t\tpublic int Position;\n\t\tpublic Reader( byte[] data, bool bigEndian ) { this.data = data; BigEndian = bigEndian; }\n\t\tReadOnlySpan\u003Cbyte\u003E Take( int count )\n\t\t{\n\t\t\tif ( count \u003C 0 || Position \u002B count \u003E data.Length ) throw new InvalidDataException( \u0022Unexpected end of serialized data.\u0022 );\n\t\t\tvar span = data.AsSpan( Position, count );\n\t\t\tPosition \u002B= count;\n\t\t\treturn span;\n\t\t}\n\t\tpublic byte Byte() =\u003E Take( 1 )[0];\n\t\tpublic short Int16() =\u003E BigEndian ? BinaryPrimitives.ReadInt16BigEndian( Take( 2 ) ) : BinaryPrimitives.ReadInt16LittleEndian( Take( 2 ) );\n\t\tpublic ushort UInt16() =\u003E BigEndian ? BinaryPrimitives.ReadUInt16BigEndian( Take( 2 ) ) : BinaryPrimitives.ReadUInt16LittleEndian( Take( 2 ) );\n\t\tpublic int Int32() =\u003E BigEndian ? BinaryPrimitives.ReadInt32BigEndian( Take( 4 ) ) : BinaryPrimitives.ReadInt32LittleEndian( Take( 4 ) );\n\t\tpublic uint UInt32() =\u003E BigEndian ? BinaryPrimitives.ReadUInt32BigEndian( Take( 4 ) ) : BinaryPrimitives.ReadUInt32LittleEndian( Take( 4 ) );\n\t\tpublic long Int64() =\u003E BigEndian ? BinaryPrimitives.ReadInt64BigEndian( Take( 8 ) ) : BinaryPrimitives.ReadInt64LittleEndian( Take( 8 ) );\n\t\tpublic ulong UInt64() =\u003E BigEndian ? BinaryPrimitives.ReadUInt64BigEndian( Take( 8 ) ) : BinaryPrimitives.ReadUInt64LittleEndian( Take( 8 ) );\n\t\tpublic float Single() =\u003E BigEndian ? BinaryPrimitives.ReadSingleBigEndian( Take( 4 ) ) : BinaryPrimitives.ReadSingleLittleEndian( Take( 4 ) );\n\t\tpublic double Double() =\u003E BigEndian ? BinaryPrimitives.ReadDoubleBigEndian( Take( 8 ) ) : BinaryPrimitives.ReadDoubleLittleEndian( Take( 8 ) );\n\t\t/// \u003Csummary\u003EA non-negative 32-bit count or length.\u003C/summary\u003E\n\t\tpublic int Count()\n\t\t{\n\t\t\tvar count = Int32();\n\t\t\tif ( count \u003C 0 || count \u003E data.Length - Position ) throw new InvalidDataException( \u0022A serialized count is out of range.\u0022 );\n\t\t\treturn count;\n\t\t}\n\t\tpublic byte[] Bytes( int count ) =\u003E Take( count ).ToArray();\n\t\tpublic void Skip( int count ) =\u003E Take( count );\n\t\tpublic void Align() =\u003E Position = Math.Min( data.Length, (Position \u002B 3) \u0026 ~3 );\n\t\tpublic string CString()\n\t\t{\n\t\t\tvar end = System.Array.IndexOf( data, (byte)0, Position );\n\t\t\tif ( end \u003C 0 ) throw new InvalidDataException( \u0022Unterminated string in serialized data.\u0022 );\n\t\t\tvar text = Encoding.UTF8.GetString( data, Position, end - Position );\n\t\t\tPosition = end \u002B 1;\n\t\t\treturn text;\n\t\t}\n\t\tpublic T[] Array\u003CT\u003E( int count, int size ) where T : struct\n\t\t{\n\t\t\tvar bytes = Take( checked(count * size) );\n\t\t\tvar result = new T[count];\n\t\t\tvar target = System.Runtime.InteropServices.MemoryMarshal.AsBytes( result.AsSpan() );\n\t\t\tbytes.CopyTo( target );\n\t\t\tif ( BigEndian \u0026\u0026 size \u003E 1 )\n\t\t\t\tfor ( var i = 0; i \u003C target.Length; i \u002B= size ) target.Slice( i, size ).Reverse();\n\t\t\treturn result;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EUnity\u0027s built-in type tree strings, addressed by offset (with the high bit set) from every type tree.\u003C/summary\u003E\n\tstatic readonly Dictionary\u003Cint, string\u003E CommonStrings = BuildCommonStrings(\n\t\t\u0022AABB|AnimationClip|AnimationCurve|AnimationState|Array|Base|BitField|bitset|bool|char|ColorRGBA|Component|\u0022 \u002B\n\t\t\u0022data|deque|double|dynamic_array|FastPropertyName|first|float|Font|GameObject|Generic Mono|GradientNEW|GUID|\u0022 \u002B\n\t\t\u0022GUIStyle|int|list|long long|map|Matrix4x4f|MdFour|MonoBehaviour|MonoScript|m_ByteSize|m_Curve|\u0022 \u002B\n\t\t\u0022m_EditorClassIdentifier|m_EditorHideFlags|m_Enabled|m_ExtensionPtr|m_GameObject|m_Index|m_IsArray|m_IsStatic|\u0022 \u002B\n\t\t\u0022m_MetaFlag|m_Name|m_ObjectHideFlags|m_PrefabInternal|m_PrefabParentObject|m_Script|m_StaticEditorFlags|m_Type|\u0022 \u002B\n\t\t\u0022m_Version|Object|pair|PPtr\u003CComponent\u003E|PPtr\u003CGameObject\u003E|PPtr\u003CMaterial\u003E|PPtr\u003CMonoBehaviour\u003E|PPtr\u003CMonoScript\u003E|\u0022 \u002B\n\t\t\u0022PPtr\u003CObject\u003E|PPtr\u003CPrefab\u003E|PPtr\u003CSprite\u003E|PPtr\u003CTextAsset\u003E|PPtr\u003CTexture\u003E|PPtr\u003CTexture2D\u003E|PPtr\u003CTransform\u003E|Prefab|\u0022 \u002B\n\t\t\u0022Quaternionf|Rectf|RectInt|RectOffset|second|set|short|size|SInt16|SInt32|SInt64|SInt8|staticvector|string|\u0022 \u002B\n\t\t\u0022TextAsset|TextMesh|Texture|Texture2D|Transform|TypelessData|UInt16|UInt32|UInt64|UInt8|unsigned int|\u0022 \u002B\n\t\t\u0022unsigned long long|unsigned short|vector|Vector2f|Vector3f|Vector4f|m_ScriptingClassIdentifier|Gradient|Type*|\u0022 \u002B\n\t\t\u0022int2_storage|int3_storage|BoundsInt|m_CorrespondingSourceObject|m_PrefabInstance|m_PrefabAsset|FileSize|\u0022 \u002B\n\t\t\u0022Hash128|RenderingLayerMask|fixed_array|EntityId|LoadableObjectId|LoadableSceneId\u0022 );\n\n\tstatic Dictionary\u003Cint, string\u003E BuildCommonStrings( string names )\n\t{\n\t\tvar result = new Dictionary\u003Cint, string\u003E();\n\t\tvar offset = 0;\n\t\tforeach ( var name in names.Split( \u0027|\u0027 ) ) { result[offset] = name; offset \u002B= Encoding.UTF8.GetByteCount( name ) \u002B 1; }\n\t\treturn result;\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityAsciiFbxLinks.cs","FileName":"UnityAsciiFbxLinks.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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":410869,"IsPrivate":false,"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\t// The stray array starts after the closing brace, on its line or the next, and can continue over following lines\n\t\t// that each start with a comma.\n\t\tvar tails = Regex.Matches( text, @\u0022(?m)^[ \\t]*}(?:[ \\t]*\\r?\\n[ \\t]*)?(?\u003Ctail\u003E,[-\u002B0-9.eE, \\t]\u002B(?:\\r?\\n[ \\t]*,[-\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/UnityAssetTree.cs","FileName":"UnityAssetTree.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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\t// Folder nodes by path, so a folder with many files is found without scanning its children.\n\t\tvar folders = new Dictionary\u003Cstring, AssetNode\u003E( StringComparer.Ordinal );\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\tvar path = \u0022\u0022;\n\t\t\tforeach ( var part in parts[..^1] )\n\t\t\t{\n\t\t\t\tpath \u002B= part \u002B \u0022/\u0022;\n\t\t\t\tif ( !folders.TryGetValue( path, out var folder ) ) { folders[path] = 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, UnityAssetKind.Sound =\u003E \u0022audio_file\u0022, UnityAssetKind.Terrain =\u003E \u0022landscape\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/UnityMaterial.cs","FileName":"UnityMaterial.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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\t/// \u003Csummary\u003EThe package shader\u0027s colors blended in through a mask: not the tint or opacity.\u003C/summary\u003E\n\tHashSet\u003Cstring\u003E maskedColors = new();\n\t// Package shader properties that InferRoles gave a converted role.\n\treadonly HashSet\u003Cstring\u003E aliased = new( StringComparer.Ordinal );\n\t/// \u003Csummary\u003E\n\t/// For a package shader: its visible declared properties that the conversion does not read, grouped by the effect their\n\t/// names suggest (for example wind, texture layers or triplanar mapping). Empty for Unity\u0027s own shaders.\n\t/// \u003C/summary\u003E\n\tpublic IReadOnlyDictionary\u003Cstring, string[]\u003E Unreproduced { get; private set; } = new Dictionary\u003Cstring, string[]\u003E();\n\tstatic readonly string[] ColorProperties = { \u0022_BaseMap\u0022, \u0022_Diffuse\u0022, \u0022_MainTex\u0022, \u0022_Albedo\u0022, \u0022_BaseColorMap\u0022 };\n\t// Properties the conversion reads (ToVmat, PrepareChannels and the render-state checks).\n\tstatic readonly HashSet\u003Cstring\u003E ReadProperties = new( ColorProperties.Concat( new[] { \u0022_BumpMap\u0022, \u0022_Normal\u0022, \u0022_NormalMap\u0022, \u0022_OcclusionMap\u0022, \u0022_Occlusion\u0022,\n\t\t\u0022_MetallicGlossMap\u0022, \u0022_SpecGlossMap\u0022, \u0022_MaskMap\u0022, \u0022_EmissionMap\u0022, \u0022_EmissiveColorMap\u0022, \u0022_Color\u0022, \u0022_BaseColor\u0022, \u0022_MainColor\u0022, \u0022_TintColor\u0022, \u0022_EmissionColor\u0022,\n\t\t\u0022_Cutoff\u0022, \u0022_Cull\u0022, \u0022_Mode\u0022, \u0022_Surface\u0022, \u0022_AlphaClip\u0022, \u0022_Blend\u0022, \u0022_SurfaceType\u0022, \u0022_AlphaCutoffEnable\u0022, \u0022_AlphaCutoff\u0022, \u0022_DoubleSidedEnable\u0022, \u0022_Smoothness\u0022, \u0022_Glossiness\u0022, \u0022_Metallic\u0022, \u0022_GlossMapScale\u0022, \u0022_SmoothnessTextureChannel\u0022, \u0022_WorkflowMode\u0022 } ),\n\t\tStringComparer.Ordinal );\n\t// Effects named by unread properties, first match wins; words are matched in lowercased display and reference names.\n\tstatic readonly (string Feature, string[] Words)[] FeatureWords =\n\t{\n\t\t(\u0022vertex animation (wind, waves)\u0022, new[] { \u0022wind\u0022, \u0022sway\u0022, \u0022wave\u0022, \u0022bend\u0022, \u0022flutter\u0022, \u0022gust\u0022, \u0022wiggle\u0022, \u0022jitter\u0022, \u0022shake\u0022 }),\n\t\t(\u0022water (foam, refraction, caustics)\u0022, new[] { \u0022foam\u0022, \u0022refract\u0022, \u0022caustic\u0022, \u0022shore\u0022, \u0022underwater\u0022, \u0022depthcolor\u0022, \u0022shallow\u0022 }),\n\t\t(\u0022depth fade or soft edges\u0022, new[] { \u0022depth\u0022, \u0022softparticle\u0022, \u0022invfade\u0022 }),\n\t\t(\u0022triplanar or world-space mapping\u0022, new[] { \u0022triplanar\u0022, \u0022worldspace\u0022, \u0022worldproj\u0022, \u0022worlduv\u0022 }),\n\t\t(\u0022scrolling or flowing UVs\u0022, new[] { \u0022flow\u0022, \u0022scroll\u0022, \u0022speed\u0022, \u0022pann\u0022 }),\n\t\t(\u0022texture layers and blending\u0022, new[] { \u0022layer\u0022, \u0022blend\u0022, \u0022splat\u0022, \u0022second\u0022, \u0022overlay\u0022, \u0022snow\u0022, \u0022moss\u0022, \u0022dirt\u0022, \u0022puddle\u0022, \u0022wet\u0022, \u0022detail\u0022, \u0022cover\u0022, \u0022mask\u0022, \u0022noise\u0022 }),\n\t\t(\u0022rim or fresnel lighting\u0022, new[] { \u0022rim\u0022, \u0022fresnel\u0022 }),\n\t\t(\u0022translucency or subsurface scattering\u0022, new[] { \u0022translucen\u0022, \u0022subsurface\u0022, \u0022sss\u0022, \u0022transmission\u0022, \u0022backlight\u0022 }),\n\t\t(\u0022color variation\u0022, new[] { \u0022variation\u0022, \u0022hue\u0022, \u0022gradient\u0022, \u0022random\u0022, \u0022colorshift\u0022 }),\n\t\t(\u0022distortion\u0022, new[] { \u0022distort\u0022 }),\n\t\t(\u0022parallax or height\u0022, new[] { \u0022parallax\u0022, \u0022height\u0022, \u0022displace\u0022, \u0022tessell\u0022 }),\n\t};\n\n\tDictionary\u003Cstring, string[]\u003E UnreadFeatures( UnityShaderDeclarations shader )\n\t{\n\t\tvar visible = shader.Properties.Where( p =\u003E !p.Hidden ).GroupBy( p =\u003E p.Name, StringComparer.Ordinal ).ToDictionary( g =\u003E g.Key, g =\u003E g.First(), StringComparer.Ordinal );\n\t\tvar hidden = shader.Properties.Where( p =\u003E p.Hidden ).Select( p =\u003E p.Name ).ToHashSet( StringComparer.Ordinal );\n\t\tvar groups = new Dictionary\u003Cstring, List\u003Cstring\u003E\u003E();\n\t\t// Only what the material uses: an assigned texture or a stored value, and not a switched-off toggle.\n\t\tbool Used( string name ) =\u003E (Textures.ContainsKey( name ) || Numbers.ContainsKey( name ) || Colors.ContainsKey( name )) \u0026\u0026\n\t\t\t!(Regex.IsMatch( name, \u0022(?i)enable|toggle|^_use\u0022 ) \u0026\u0026 Numbers.TryGetValue( name, out var toggle ) \u0026\u0026 toggle == 0);\n\t\tforeach ( var name in shader.Declared.Where( n =\u003E !hidden.Contains( n ) \u0026\u0026 !ReadProperties.Contains( n ) \u0026\u0026 !aliased.Contains( n ) \u0026\u0026 Used( n ) ).Order( StringComparer.Ordinal ) )\n\t\t{\n\t\t\tvar property = visible.GetValueOrDefault( name );\n\t\t\tvar words = Regex.Replace( ((property?.Display ?? \u0022\u0022) \u002B name).ToLowerInvariant(), \u0022[^a-z0-9]\u0022, \u0022\u0022 );\n\t\t\t// Unread textures that name no known effect are still listed; other unmatched values are usually settings.\n\t\t\tvar feature = FeatureWords.FirstOrDefault( f =\u003E f.Words.Any( words.Contains ) ).Feature ?? (property?.Texture == true ? \u0022other textures\u0022 : null);\n\t\t\tif ( feature == null ) continue;\n\t\t\tif ( !groups.TryGetValue( feature, out var list ) ) groups[feature] = list = new();\n\t\t\tlist.Add( name );\n\t\t}\n\t\treturn groups.ToDictionary( g =\u003E g.Key, g =\u003E g.Value.ToArray() );\n\t}\n\t/// \u003Csummary\u003ETexture properties the conversion reads; a missing texture elsewhere does not change the output.\u003C/summary\u003E\n\tinternal static readonly HashSet\u003Cstring\u003E ConvertedTextures = new( ColorProperties.Concat( new[] { \u0022_BumpMap\u0022, \u0022_Normal\u0022, \u0022_NormalMap\u0022, \u0022_OcclusionMap\u0022, \u0022_Occlusion\u0022,\n\t\t\u0022_MetallicGlossMap\u0022, \u0022_SpecGlossMap\u0022, \u0022_MaskMap\u0022, \u0022_EmissionMap\u0022, \u0022_EmissiveColorMap\u0022, \u0022_ParallaxMap\u0022, \u0022_HeightMap\u0022 } ), StringComparer.Ordinal );\n\treadonly HashSet\u003Cstring\u003E keywords = new( StringComparer.Ordinal );\n\tbool specularWorkflow;\n\tbool smoothnessFromAlbedo;\n\t// Set when a keyword-driven Unity shader has emission enabled, so an emission color applies even without a map.\n\tbool emissionEnabled;\n\tbool unlit;\n\tbool legacyParticle;\n\tbool additive;\n\t// Unity\u0027s default texture is white; s\u0026box\u0027s default color texture is grey. This engine texture is plain white.\n\tconst string White = \u0022materials/default/default_mask.tga\u0022;\n\t// s\u0026box\u0027s default roughness texture: 0.5.\n\tconst string MidGrey = \u0022materials/default/default_rough.tga\u0022;\n\t// Smoothness from the material\u0027s own pipeline: built-in reads _Glossiness and scales maps by _GlossMapScale, URP and\n\t// HDRP read _Smoothness (URP\u0027s hidden legacy _GlossMapScale is 0). Package and unknown shaders take either.\n\tdouble MapSmoothnessScale =\u003E pipeline switch\n\t{\n\t\tPipeline.Builtin =\u003E Number( \u0022_GlossMapScale\u0022, 1 ),\n\t\tPipeline.Universal =\u003E Number( \u0022_Smoothness\u0022, 1 ),\n\t\t_ =\u003E Number( \u0022_GlossMapScale\u0022, Number( \u0022_Smoothness\u0022, 1 ) ),\n\t};\n\tdouble ScalarSmoothness =\u003E pipeline switch\n\t{\n\t\tPipeline.Builtin =\u003E Number( \u0022_Glossiness\u0022, 0.5 ),\n\t\tPipeline.Universal or Pipeline.HighDefinition =\u003E Number( \u0022_Smoothness\u0022, 0.5 ),\n\t\t_ =\u003E Number( \u0022_Smoothness\u0022, Number( \u0022_Glossiness\u0022, 0.5 ) ),\n\t};\n\tpublic long ShaderFileId { get; private set; }\n\t/// \u003Csummary\u003EThe missing-texture message naming every property that references this texture.\u003C/summary\u003E\n\tinternal string MissingTexture( string guid ) =\u003E\n\t\t$\u0022Texture for {string.Join( \u0022, \u0022, Textures.Where( p =\u003E p.Value.Equals( guid, StringComparison.OrdinalIgnoreCase ) ).Select( p =\u003E p.Key ) )} is missing from the package or unsupported ({guid}).\u0022;\n\tpublic IEnumerable\u003Cstring\u003E ColorTextureGuids =\u003E ColorOrder.Where( Textures.ContainsKey ).Select( p =\u003E Textures[p] );\n\t// Render state comes from the properties of the material\u0027s own pipeline: Unity keeps values from earlier shaders, so a\n\t// Standard material switched from URP can still carry _Surface. Package shaders keep only declared values.\n\tenum Pipeline { Unknown, Builtin, Universal, HighDefinition }\n\tPipeline pipeline;\n\t// Built-in shaders other than Standard (particles, for example) can declare _Cull; Standard does not.\n\tbool builtinCull;\n\tstatic readonly HashSet\u003Cstring\u003E RenderStateProperties = new( StringComparer.Ordinal ) { \u0022_Surface\u0022, \u0022_Blend\u0022, \u0022_AlphaClip\u0022, \u0022_Cull\u0022, \u0022_Cutoff\u0022, \u0022_AlphaCutoff\u0022, \u0022_SurfaceType\u0022, \u0022_AlphaCutoffEnable\u0022, \u0022_DoubleSidedEnable\u0022 };\n\tbool Flag( string name, double value ) =\u003E Number( name, double.NaN ) == value;\n\tbool AlphaTest =\u003E shaderAlphaTest || pipeline switch\n\t{\n\t\tPipeline.Builtin =\u003E Flag( \u0022_Mode\u0022, 1 ),\n\t\tPipeline.Universal =\u003E Flag( \u0022_AlphaClip\u0022, 1 ),\n\t\tPipeline.HighDefinition =\u003E Flag( \u0022_AlphaCutoffEnable\u0022, 1 ),\n\t\t_ =\u003E Flag( \u0022_Mode\u0022, 1 ) || Flag( \u0022_AlphaClip\u0022, 1 ) || Flag( \u0022_AlphaCutoffEnable\u0022, 1 ),\n\t};\n\tbool Translucent =\u003E shaderTranslucent || pipeline switch\n\t{\n\t\tPipeline.Builtin =\u003E Number( \u0022_Mode\u0022, 0 ) \u003E= 2,\n\t\tPipeline.Universal =\u003E Flag( \u0022_Surface\u0022, 1 ),\n\t\tPipeline.HighDefinition =\u003E Flag( \u0022_SurfaceType\u0022, 1 ),\n\t\t_ =\u003E Number( \u0022_Mode\u0022, 0 ) \u003E= 2 || Flag( \u0022_Surface\u0022, 1 ) || Flag( \u0022_SurfaceType\u0022, 1 ),\n\t};\n\tbool TwoSided =\u003E shaderBackfaces || pipeline switch\n\t{\n\t\tPipeline.Builtin =\u003E builtinCull \u0026\u0026 Flag( \u0022_Cull\u0022, 0 ),\n\t\tPipeline.Universal =\u003E Flag( \u0022_Cull\u0022, 0 ),\n\t\tPipeline.HighDefinition =\u003E Flag( \u0022_DoubleSidedEnable\u0022, 1 ),\n\t\t_ =\u003E Flag( \u0022_Cull\u0022, 0 ) || Flag( \u0022_DoubleSidedEnable\u0022, 1 ),\n\t};\n\tdouble AlphaCutoff =\u003E pipeline == Pipeline.HighDefinition ? Number( \u0022_AlphaCutoff\u0022, 0.5 ) : Number( \u0022_Cutoff\u0022, 0.5 );\n\t/// \u003Csummary\u003EBase color properties in the order the material\u0027s pipeline reads them.\u003C/summary\u003E\n\tstring[] ColorOrder =\u003E pipeline switch\n\t{\n\t\tPipeline.Builtin =\u003E new[] { \u0022_MainTex\u0022, \u0022_BaseMap\u0022, \u0022_Diffuse\u0022, \u0022_Albedo\u0022, \u0022_BaseColorMap\u0022 },\n\t\tPipeline.HighDefinition =\u003E new[] { \u0022_BaseColorMap\u0022, \u0022_BaseMap\u0022, \u0022_MainTex\u0022, \u0022_Diffuse\u0022, \u0022_Albedo\u0022 },\n\t\t_ =\u003E ColorProperties,\n\t};\n\tinternal bool UsesAlpha =\u003E AlphaTest || Translucent;\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\tconst string NotText = \u0022Material is not Unity text YAML. Re-export it with Asset Serialization set to Force Text in Unity.\u0022;\n\n\tpublic static UnityMaterial Parse( string text )\n\t{\n\t\tif ( text.Contains( \u0027\\0\u0027 ) ) throw new InvalidDataException( NotText );\n\t\tvar objects = UnityYaml.Parse( text );\n\t\tif ( UnityYaml.Objects( objects, \u0022TerrainLayer\u0022 ).FirstOrDefault() is YamlNode layer ) return ParseTerrainLayer( layer );\n\t\tvar body = UnityYaml.Objects( objects, \u0022Material\u0022 ).FirstOrDefault() ?? throw new InvalidDataException( NotText );\n\t\tvar shaderReference = body[\u0022m_Shader\u0022]?.Reference ?? default;\n\t\tvar result = new UnityMaterial { ShaderGuid = shaderReference.Guid ?? \u0022\u0022, ShaderFileId = shaderReference.FileId };\n\t\t// m_ShaderKeywords (a space-separated string) became m_ValidKeywords in Unity 2021.2. m_InvalidKeywords belong to\n\t\t// previous shaders and are ignored.\n\t\tvar keywordsKnown = body[\u0022m_ShaderKeywords\u0022] != null || body[\u0022m_ValidKeywords\u0022] != null;\n\t\tresult.keywords.UnionWith( (body[\u0022m_ShaderKeywords\u0022]?.Value ?? \u0022\u0022).Split( \u0027 \u0027, StringSplitOptions.RemoveEmptyEntries ) );\n\t\tresult.keywords.UnionWith( (body[\u0022m_ValidKeywords\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Select( k =\u003E k.Value ).Where( k =\u003E !string.IsNullOrEmpty( k ) ) );\n\t\tvar saved = body[\u0022m_SavedProperties\u0022];\n\t\tforeach ( var section in new[] { \u0022m_TexEnvs\u0022, \u0022m_Floats\u0022, \u0022m_Colors\u0022, \u0022m_Ints\u0022 } )\n\t\t{\n\t\t\tforeach ( var (name, value) in Properties( saved?[section] ) )\n\t\t\t{\n\t\t\t\tvar named = Regex.IsMatch( name, \u0022^[A-Za-z_][A-Za-z0-9_]*$\u0022 );\n\t\t\t\tif ( value[\u0022m_Texture\u0022] is YamlNode texture )\n\t\t\t\t{\n\t\t\t\t\tvar scale = Pair( value[\u0022m_Scale\u0022] );\n\t\t\t\t\tvar offset = Pair( value[\u0022m_Offset\u0022] );\n\t\t\t\t\tif ( !named ) continue;\n\t\t\t\t\tif ( texture.Reference.HasGuid ) result.Textures[name] = texture.Reference.Guid;\n\t\t\t\t\tif ( scale != null ) result.textureScale[name] = scale;\n\t\t\t\t\tif ( offset != null ) result.textureOffset[name] = offset;\n\t\t\t\t}\n\t\t\t\telse if ( named \u0026\u0026 value.Number is double number ) result.Numbers[name] = number;\n\t\t\t\telse if ( named \u0026\u0026 new[] { \u0022r\u0022, \u0022g\u0022, \u0022b\u0022, \u0022a\u0022 }.Select( c =\u003E value[c]?.Number ).ToArray() is var rgba \u0026\u0026 rgba.All( c =\u003E c != null ) )\n\t\t\t\t\tresult.Colors[name] = rgba.Select( c =\u003E c.Value ).ToArray();\n\t\t\t}\n\t\t}\n\t\tif ( UnityKnownShaders.Identify( shaderReference ) is { } known ) result.ApplyKnownShader( known, keywordsKnown );\n\t\tif ( result.Textures.ContainsKey( \u0022_MetallicGlossMap\u0022 ) || result.Textures.ContainsKey( \u0022_MaskMap\u0022 ) || result.Textures.ContainsKey( \u0022_SpecGlossMap\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\t/// \u003Csummary\u003EApplies what is known about a Unity-shipped shader: its real properties, active keywords and workflow.\u003C/summary\u003E\n\tvoid ApplyKnownShader( UnityKnownShaders.Shader shader, bool keywordsKnown )\n\t{\n\t\tShaderName = shader.Name;\n\t\tpipeline = shader.Name.StartsWith( \u0022HDRP/\u0022, StringComparison.Ordinal ) ? Pipeline.HighDefinition : shader.Universal ? Pipeline.Universal : Pipeline.Builtin;\n\t\tbuiltinCull = !shader.KeywordDriven;\n\t\tshaderAlphaTest |= shader.AlphaTest;\n\t\tshaderBackfaces |= shader.Backfaces;\n\t\tunlit = shader.Unlit;\n\t\tlegacyParticle = shader.LegacyParticle;\n\t\tshaderTranslucent |= shader.LegacyParticle;\n\t\t// URP blending: _Blend 2 is additive on transparent surfaces.\n\t\tadditive = shader.Universal \u0026\u0026 Number( \u0022_Surface\u0022, 0 ) == 1 \u0026\u0026 Number( \u0022_Blend\u0022, 0 ) == 2;\n\t\tif ( unlit ) Information.Add( \u0022Unlit shading approximated with self-illumination from the base color; scene lighting still adds to it.\u0022 );\n\t\tvar stale = Textures.Keys.Where( k =\u003E !UnityKnownShaders.TextureProperties.Contains( k ) ||\n\t\t\t(pipeline == Pipeline.HighDefinition ? UnityKnownShaders.BuiltinUniversalOnly : UnityKnownShaders.HighDefinitionOnly).Contains( k ) ).ToArray();\n\t\tforeach ( var property in stale ) Textures.Remove( property );\n\t\tif ( stale.Length \u003E 0 ) Information.Add( $\u0022Ignored texture properties left over from a previous shader: {string.Join( \u0022, \u0022, stale.Order( StringComparer.Ordinal ) )}.\u0022 );\n\t\tif ( shader.KeywordDriven \u0026\u0026 keywordsKnown )\n\t\t{\n\t\t\t// Keyword-driven shaders only read a feature\u0027s textures when its keyword is enabled.\n\t\t\tbool On( params string[] names ) =\u003E names.Any( keywords.Contains );\n\t\t\tvar inactive = new List\u003Cstring\u003E();\n\t\t\tvoid Gate( bool enabled, string feature, params string[] properties )\n\t\t\t{\n\t\t\t\tif ( enabled || !properties.Any( Textures.ContainsKey ) ) return;\n\t\t\t\tforeach ( var property in properties ) Textures.Remove( property );\n\t\t\t\tinactive.Add( feature );\n\t\t\t}\n\t\t\t// A black emission color multiplies the map to nothing.\n\t\t\temissionEnabled = On( \u0022_EMISSION\u0022 ) \u0026\u0026 !(Colors.TryGetValue( \u0022_EmissionColor\u0022, out var emissive ) \u0026\u0026 emissive.Take( 3 ).Max() \u003C= 0);\n\t\t\tGate( emissionEnabled, \u0022emission\u0022, \u0022_EmissionMap\u0022 );\n\t\t\tGate( On( \u0022_DETAIL_MULX2\u0022, \u0022_DETAIL_SCALED\u0022 ), \u0022detail maps\u0022, \u0022_DetailAlbedoMap\u0022, \u0022_DetailNormalMap\u0022, \u0022_DetailMask\u0022 );\n\t\t\tGate( On( \u0022_PARALLAXMAP\u0022 ), \u0022parallax\u0022, \u0022_ParallaxMap\u0022 );\n\t\t\tif ( shader.Universal )\n\t\t\t{\n\t\t\t\tGate( On( \u0022_METALLICSPECGLOSSMAP\u0022 ), \u0022metallic/specular map\u0022, \u0022_MetallicGlossMap\u0022, \u0022_SpecGlossMap\u0022 );\n\t\t\t\tGate( On( \u0022_OCCLUSIONMAP\u0022 ), \u0022occlusion map\u0022, \u0022_OcclusionMap\u0022 );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tGate( On( \u0022_METALLICGLOSSMAP\u0022 ), \u0022metallic map\u0022, \u0022_MetallicGlossMap\u0022 );\n\t\t\t\tGate( On( \u0022_SPECGLOSSMAP\u0022 ), \u0022specular map\u0022, \u0022_SpecGlossMap\u0022 );\n\t\t\t}\n\t\t\tif ( inactive.Count \u003E 0 ) Information.Add( $\u0022Unity features disabled on this material were not converted: {string.Join( \u0022, \u0022, inactive )}.\u0022 );\n\t\t\tsmoothnessFromAlbedo = On( \u0022_SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A\u0022 );\n\t\t}\n\t\telse if ( shader.KeywordDriven ) smoothnessFromAlbedo = Number( \u0022_SmoothnessTextureChannel\u0022, 0 ) == 1;\n\t\t// URP Lit selects its workflow per material; Standard has a separate specular shader.\n\t\tspecularWorkflow = shader.Workflow == UnityKnownShaders.Workflow.Specular ||\n\t\t\tshader.Universal \u0026\u0026 shader.KeywordDriven \u0026\u0026 (keywords.Contains( \u0022_SPECULAR_SETUP\u0022 ) || Number( \u0022_WorkflowMode\u0022, 1 ) == 0);\n\t\tif ( specularWorkflow ) Information.Add( \u0022Unity specular workflow approximated: specular color is not converted and metalness is 0.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EMaterial properties as (name, value) in any serialized layout.\u003C/summary\u003E\n\tstatic IEnumerable\u003C(string Name, YamlNode Value)\u003E Properties( YamlNode section )\n\t{\n\t\tif ( section == null ) yield break;\n\t\t// Current Unity: \u0022- _Name: value\u0022. Unity 5: \u0022- first: {name: _Name}, second: value\u0022.\n\t\t// Unity 4: repeated \u0022data:\u0022 entries holding the same first/second pairs.\n\t\tvar entries = section.IsList ? section.Items : section.All( \u0022data\u0022 );\n\t\tforeach ( var entry in entries )\n\t\t{\n\t\t\tif ( entry[\u0022first\u0022]?[\u0022name\u0022]?.Value is string name ) yield return (name, entry[\u0022second\u0022] ?? YamlNode.Scalar( \u0022\u0022 ));\n\t\t\telse if ( entry.Pairs.Count == 1 ) yield return (entry.Pairs[0].Key, entry.Pairs[0].Value);\n\t\t}\n\t}\n\n\tstatic double[] Pair( YamlNode node ) =\u003E node?[\u0022x\u0022]?.Number is double x \u0026\u0026 node[\u0022y\u0022]?.Number is double y ? new[] { x, y } : null;\n\n\t/// \u003Csummary\u003EApplies a package shader\u0027s declarations (ShaderLab source or a Shader Graph file named \u003Cparamref name=\u0022graphName\u0022/\u003E).\u003C/summary\u003E\n\tpublic void ConfigureShader( string source, string graphName = null )\n\t{\n\t\tvar shader = UnityShaderDeclarations.Parse( source );\n\t\tShaderName = shader.Graph ? graphName : shader.Name;\n\t\tdeclaredProperties = shader.Declared;\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\tforeach ( var stale in Colors.Keys.Where( k =\u003E !declaredProperties.Contains( k ) ).ToArray() ) Colors.Remove( stale );\n\t\t\t// Shader Graph materials can override the target\u0027s render state (\u0022Allow Material Override\u0022) with values the graph\n\t\t\t// does not declare.\n\t\t\tforeach ( var stale in Numbers.Keys.Where( k =\u003E !declaredProperties.Contains( k ) \u0026\u0026 !(shader.Graph \u0026\u0026 RenderStateProperties.Contains( k )) ).ToArray() ) Numbers.Remove( stale );\n\t\t}\n\t\tshaderAlphaTest = shader.AlphaClip;\n\t\tshaderBackfaces = shader.TwoSided;\n\t\tshaderTranslucent = shader.Transparent;\n\t\tmaskedColors = shader.MaskedColors;\n\t\tforeach ( var masked in maskedColors.Where( Colors.ContainsKey ) )\n\t\t\tInformation.Add( $\u0022Shader property \u0027{masked}\u0027 is blended in through a mask (its alpha weights a blend in the shader code), which is not reproduced; it is not used as the tint or opacity.\u0022 );\n\t\tInferRoles( shader );\n\t\tUnreproduced = UnreadFeatures( shader );\n\t\tif ( !string.IsNullOrEmpty( ShaderName ) )\n\t\t\tWarnings.Add( $\u0022Shader \u0027{ShaderName}\u0027 is approximated using s\u0026box\u0027s complex shader; shader code and graph behavior are not translated\u0022 \u002B (Unreproduced.Count == 0 ? \u0022.\u0022 :\n\t\t\t\t$\u0022. Not reproduced: {string.Join( \u0022; \u0022, Unreproduced.Select( f =\u003E $\u0022{f.Key} ({string.Join( \u0022, \u0022, f.Value )})\u0022 ) )}.\u0022) );\n\t}\n\n\t// Words that name a texture\u0027s role in shader property names (display or reference, lowercased, letters and digits only).\n\tstatic readonly string[] BaseColorWords = { \u0022albedo\u0022, \u0022basecolor\u0022, \u0022basecolour\u0022, \u0022basemap\u0022, \u0022basetexture\u0022, \u0022basetex\u0022, \u0022maintex\u0022, \u0022maintexture\u0022, \u0022diffuse\u0022, \u0022colormap\u0022, \u0022colortexture\u0022, \u0022colourmap\u0022, \u0022colourtexture\u0022 };\n\tstatic readonly string[] NotColorWords = { \u0022normal\u0022, \u0022bump\u0022, \u0022emissi\u0022, \u0022mask\u0022, \u0022noise\u0022, \u0022metal\u0022, \u0022smooth\u0022, \u0022rough\u0022, \u0022spec\u0022, \u0022occlusion\u0022, \u0022height\u0022, \u0022detail\u0022, \u0022overlay\u0022, \u0022triplanar\u0022, \u0022flow\u0022, \u0022distort\u0022, \u0022cube\u0022, \u0022reflect\u0022, \u0022parallax\u0022 };\n\tstatic readonly string[] TintWords = { \u0022basecolor\u0022, \u0022basecolour\u0022, \u0022maincolor\u0022, \u0022maincolour\u0022, \u0022albedocolor\u0022, \u0022albedocolour\u0022, \u0022tint\u0022 };\n\t// Words of effects (FeatureWords) and other non-surface maps: a property named after one never takes a role, since a\n\t// wrong role (foam as the base color, a detail normal as the normal map, a rim tint as the tint) looks worse than none.\n\tstatic readonly string[] EffectWords = FeatureWords.SelectMany( f =\u003E f.Words ).Concat( new[] { \u0022ramp\u0022, \u0022lut\u0022, \u0022matcap\u0022, \u0022dissolve\u0022, \u0022spec\u0022, \u0022gloss\u0022 } ).ToArray();\n\t// Effect words that are word stems: they also match longer words (Emission, Translucency, Specular, Panning).\n\tstatic readonly HashSet\u003Cstring\u003E EffectStems = new( StringComparer.Ordinal ) { \u0022emissi\u0022, \u0022translucen\u0022, \u0022refract\u0022, \u0022caustic\u0022, \u0022displace\u0022, \u0022tessell\u0022, \u0022distort\u0022, \u0022pann\u0022, \u0022scroll\u0022, \u0022spec\u0022, \u0022gloss\u0022, \u0022subsurface\u0022, \u0022variation\u0022, \u0022parallax\u0022, \u0022triplanar\u0022 };\n\n\t/// \u003Csummary\u003E\n\t/// Whether a property is named after an effect: its display and reference names split into words (separators and\n\t/// camelCase), matched as whole words or two- and three-word phrases (plurals and stems allowed), so \u0022Primary\u0022 is not\n\t/// \u0022rim\u0022 and \u0022Window\u0022 is not \u0022wind\u0022.\n\t/// \u003C/summary\u003E\n\tstatic bool IsEffect( UnityShaderDeclarations.Property p )\n\t{\n\t\tvar words = new List\u003Cstring\u003E();\n\t\tforeach ( var part in Regex.Split( p.Display \u002B \u0022 \u0022 \u002B p.Name, \u0022[^A-Za-z0-9]\u002B\u0022 ) )\n\t\t\twords.AddRange( Regex.Split( part, \u0022(?\u003C=[a-z])(?=[A-Z])|(?\u003C=[A-Za-z])(?=[0-9])|(?\u003C=[0-9])(?=[A-Za-z])\u0022 ).Where( w =\u003E w.Length \u003E 0 ).Select( w =\u003E w.ToLowerInvariant() ) );\n\t\tvar phrases = new List\u003Cstring\u003E( words );\n\t\tfor ( var i = 0; i \u002B 1 \u003C words.Count; i\u002B\u002B ) phrases.Add( words[i] \u002B words[i \u002B 1] );\n\t\tfor ( var i = 0; i \u002B 2 \u003C words.Count; i\u002B\u002B ) phrases.Add( words[i] \u002B words[i \u002B 1] \u002B words[i \u002B 2] );\n\t\treturn phrases.Any( w =\u003E EffectWords.Any( e =\u003E w == e || w == e \u002B \u0022s\u0022 || EffectStems.Contains( e ) \u0026\u0026 w.StartsWith( e, StringComparison.Ordinal ) ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Gives an unknown shader\u0027s properties the base color, normal, emission and tint roles the conversion reads, when the\n\t/// material has none of the usual property names. Explicit declarations come first; name matches must be unique.\n\t/// \u003C/summary\u003E\n\tvoid InferRoles( UnityShaderDeclarations shader )\n\t{\n\t\tstatic string Words( UnityShaderDeclarations.Property p ) =\u003E Regex.Replace( (p.Display \u002B \u0022 \u0022 \u002B p.Name).ToLowerInvariant(), \u0022[^a-z0-9 ]\u0022, \u0022\u0022 );\n\t\tstatic string Joined( UnityShaderDeclarations.Property p ) =\u003E Words( p ).Replace( \u0022 \u0022, \u0022\u0022 );\n\t\tbool Any( string text, IEnumerable\u003Cstring\u003E words ) =\u003E words.Any( text.Contains );\n\t\tvar textures = shader.Properties.Where( p =\u003E p.Texture \u0026\u0026 !p.Hidden \u0026\u0026 Textures.ContainsKey( p.Name ) ).ToArray();\n\t\tvar colors = shader.Properties.Where( p =\u003E p.Color \u0026\u0026 !p.Hidden \u0026\u0026 Colors.ContainsKey( p.Name ) \u0026\u0026 !maskedColors.Contains( p.Name ) ).ToArray();\n\t\t// A usual property name the shader only declares as hidden (Amplify adds a hidden _EmissionColor) does not hold the role.\n\t\tbool Present( string name, bool texture ) =\u003E (texture ? Textures.ContainsKey( name ) : Colors.ContainsKey( name )) \u0026\u0026 !shader.Properties.Any( p =\u003E p.Name == name \u0026\u0026 p.Hidden );\n\t\tvoid Alias( string role, string canonical, UnityShaderDeclarations.Property property, string evidence, bool texture )\n\t\t{\n\t\t\tif ( texture )\n\t\t\t{\n\t\t\t\tTextures[canonical] = Textures[property.Name];\n\t\t\t\tif ( textureScale.TryGetValue( property.Name, out var scale ) ) textureScale[canonical] = scale;\n\t\t\t\tif ( textureOffset.TryGetValue( property.Name, out var offset ) ) textureOffset[canonical] = offset;\n\t\t\t}\n\t\t\telse Colors[canonical] = Colors[property.Name];\n\t\t\taliased.Add( property.Name );\n\t\t\tInformation.Add( $\u0022Shader property \u0027{property.Name}\u0027 ({property.Display}) used as the {role} ({evidence}).\u0022 );\n\t\t}\n\t\t// Picks the property marked explicitly, else the only name match; several matches are left unresolved and reported.\n\t\tUnityShaderDeclarations.Property Pick( string role, IEnumerable\u003CUnityShaderDeclarations.Property\u003E marked, IEnumerable\u003CUnityShaderDeclarations.Property\u003E named, out string evidence, string prefer = null )\n\t\t{\n\t\t\tevidence = \u0022declared\u0022;\n\t\t\tif ( marked.FirstOrDefault() is { } explicitProperty ) return explicitProperty;\n\t\t\tvar matches = named.ToArray();\n\t\t\t// Several matches: prefer the one sharing the base color texture\u0027s first word (Leaf Texture \u2192 Leaf Base Color).\n\t\t\tif ( matches.Length \u003E 1 \u0026\u0026 prefer != null ) matches = matches.Where( p =\u003E Words( p ).Split( \u0027 \u0027 )[0] == prefer ).ToArray();\n\t\t\tevidence = \u0022inferred from its name\u0022;\n\t\t\tif ( matches.Length == 1 ) return matches[0];\n\t\t\tif ( matches.Length \u003E 1 ) Information.Add( $\u0022Several shader properties could be the {role} ({string.Join( \u0022, \u0022, matches.Select( p =\u003E p.Name ) )}); none was chosen.\u0022 );\n\t\t\treturn null;\n\t\t}\n\t\tstring colorWord = null;\n\t\tif ( !ColorProperties.Any( p =\u003E Present( p, true ) ) )\n\t\t{\n\t\t\tvar candidates = textures.Where( p =\u003E !p.Normal \u0026\u0026 !Any( Joined( p ), NotColorWords ) \u0026\u0026 !IsEffect( p ) ).ToArray();\n\t\t\tvar named = candidates.Where( p =\u003E Any( Joined( p ), BaseColorWords ) ).ToArray();\n\t\t\t// A single remaining texture that names no other role is the base color.\n\t\t\tif ( named.Length == 0 \u0026\u0026 candidates.Length == 1 ) named = candidates;\n\t\t\telse if ( named.Length == 0 \u0026\u0026 candidates.Length \u003E 1 ) Information.Add( $\u0022Several shader textures could be the base color ({string.Join( \u0022, \u0022, candidates.Select( p =\u003E p.Name ) )}); none was chosen.\u0022 );\n\t\t\tif ( Pick( \u0022base color\u0022, textures.Where( p =\u003E p.MainTexture ), named, out var evidence ) is { } color )\n\t\t\t{\n\t\t\t\tAlias( \u0022base color\u0022, \u0022_MainTex\u0022, color, evidence, true );\n\t\t\t\tcolorWord = Words( color ).Split( \u0027 \u0027 )[0];\n\t\t\t}\n\t\t}\n\t\tif ( !new[] { \u0022_BumpMap\u0022, \u0022_Normal\u0022, \u0022_NormalMap\u0022 }.Any( p =\u003E Present( p, true ) ) \u0026\u0026\n\t\t\tPick( \u0022normal map\u0022, textures.Where( p =\u003E p.Normal \u0026\u0026 !IsEffect( p ) ), textures.Where( p =\u003E Any( Joined( p ), new[] { \u0022normal\u0022, \u0022bump\u0022 } ) \u0026\u0026 !IsEffect( p ) ), out var normalEvidence, colorWord ) is { } normal )\n\t\t\tAlias( \u0022normal map\u0022, \u0022_BumpMap\u0022, normal, normalEvidence, true );\n\t\tif ( !Present( \u0022_EmissionMap\u0022, true ) \u0026\u0026 Pick( \u0022emission map\u0022, Array.Empty\u003CUnityShaderDeclarations.Property\u003E(),\n\t\t\ttextures.Where( p =\u003E Any( Joined( p ), new[] { \u0022emissi\u0022 } ) ), out var emissionEvidence, colorWord ) is { } emissionMap )\n\t\t\tAlias( \u0022emission map\u0022, \u0022_EmissionMap\u0022, emissionMap, emissionEvidence, true );\n\t\t// Specific tint names first (Base Color, Color Tint); a plain \u0022Color\u0022 only when nothing more specific exists.\n\t\tvar tints = colors.Where( p =\u003E !Any( Joined( p ), new[] { \u0022emissi\u0022 } ) \u0026\u0026 !IsEffect( p ) ).ToArray();\n\t\tvar tintNames = tints.Where( p =\u003E Any( Joined( p ), TintWords ) ).ToArray();\n\t\tif ( tintNames.Length == 0 ) tintNames = tints.Where( p =\u003E Regex.Replace( p.Display.ToLowerInvariant(), \u0022[^a-z]\u0022, \u0022\u0022 ) is \u0022color\u0022 or \u0022colour\u0022 ).ToArray();\n\t\tif ( !new[] { \u0022_BaseColor\u0022, \u0022_Color\u0022 }.Any( p =\u003E Present( p, false ) ) \u0026\u0026 Pick( \u0022color tint\u0022, colors.Where( p =\u003E p.MainColor ), tintNames, out var tintEvidence, colorWord ) is { } tint )\n\t\t\tAlias( \u0022color tint\u0022, \u0022_BaseColor\u0022, tint, tintEvidence, false );\n\t\tif ( !Present( \u0022_EmissionColor\u0022, false ) \u0026\u0026 Pick( \u0022emission color\u0022, Array.Empty\u003CUnityShaderDeclarations.Property\u003E(),\n\t\t\tcolors.Where( p =\u003E Any( Joined( p ), new[] { \u0022emissi\u0022 } ) ), out var glowEvidence, colorWord ) is { } glow )\n\t\t{\n\t\t\tAlias( \u0022emission color\u0022, \u0022_EmissionColor\u0022, glow, glowEvidence, false );\n\t\t\t// No keyword gates a custom shader\u0027s emission: a non-black color glows.\n\t\t\temissionEnabled = Colors[\u0022_EmissionColor\u0022].Take( 3 ).Max() \u003E 0;\n\t\t}\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, MapSmoothnessScale, 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\tvar smoothnessScale = MapSmoothnessScale;\n\t\t// Specular workflow: smoothness is the specular map\u0027s alpha. Metalness stays 0.\n\t\tif ( specularWorkflow \u0026\u0026 Textures.TryGetValue( \u0022_SpecGlossMap\u0022, out var specular ) ) Channel( \u0022rough\u0022, specular, 3, smoothnessScale, true );\n\t\t// Standard/URP can take smoothness from the base color\u0027s alpha instead of the metallic/specular map.\n\t\tif ( smoothnessFromAlbedo \u0026\u0026 ColorOrder.FirstOrDefault( Textures.ContainsKey ) is string albedo ) Channel( \u0022rough\u0022, Textures[albedo], 3, smoothnessScale, true );\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 ColorOrder )\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( YamlNode body )\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\tif ( body[source]?.Reference is { HasGuid: true } texture ) result.Textures[target] = texture.Guid;\n\t\tforeach ( var (source, target) in new[] { (\u0022m_Metallic\u0022, \u0022_Metallic\u0022), (\u0022m_Smoothness\u0022, \u0022_Smoothness\u0022), (\u0022m_NormalScale\u0022, \u0022_NormalScale\u0022) } )\n\t\t\tif ( body[source]?.Number is double value ) result.Numbers[target] = value;\n\t\t// The size in meters one repeat of the layer\u0027s textures covers on the terrain.\n\t\tif ( body[\u0022m_TileSize\u0022] is { } tile \u0026\u0026 tile[\u0022x\u0022]?.Number is double x \u0026\u0026 tile[\u0022y\u0022]?.Number is double y \u0026\u0026 x \u003E 0 \u0026\u0026 y \u003E 0 ) result.Numbers[\u0022_TileSize\u0022] = (x \u002B y) / 2;\n\t\tresult.Warnings.Add( \u0022Terrain layer converted; review remapping and tile offset 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\t/// \u003Csummary\u003EThe VMAT text. \u003Cparamref name=\u0022resolveNormal\u0022/\u003E resolves normal-map textures when they need their own preparation (Unity import settings such as Flip Green Channel).\u003C/summary\u003E\n\tpublic string ToVmat( Func\u003Cstring, string\u003E resolve, Func\u003Cstring, string\u003E resolveNormal = null )\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\tvar colorOrder = ColorOrder;\n\t\tSet( \u0022TextureColor\u0022, Texture( resolve, colorOrder ) ?? White );\n\t\tvar colorProperty = colorOrder.FirstOrDefault( p =\u003E Textures.TryGetValue( p, out var guid ) \u0026\u0026 resolve( guid ) != null );\n\t\t// s\u0026box has one tiling per material, the base color\u0027s: other textures with a different tiling are reported.\n\t\tvar tiling = colorProperty == null ? null : textureScale.GetValueOrDefault( colorProperty ) ?? new[] { 1.0, 1.0 };\n\t\tvar shifted = colorProperty == null ? null : textureOffset.GetValueOrDefault( colorProperty ) ?? new[] { 0.0, 0.0 };\n\t\tforeach ( var other in ConvertedTextures.Where( p =\u003E p != colorProperty \u0026\u0026 Textures.ContainsKey( p ) \u0026\u0026 resolve( Textures[p] ) != null ) )\n\t\t\tif ( !(textureScale.GetValueOrDefault( other ) ?? new[] { 1.0, 1.0 }).SequenceEqual( tiling ?? new[] { 1.0, 1.0 } ) ||\n\t\t\t\t!(textureOffset.GetValueOrDefault( other ) ?? new[] { 0.0, 0.0 }).SequenceEqual( shifted ?? new[] { 0.0, 0.0 } ) )\n\t\t\t\tWarnings.Add( $\u0022Texture \u0027{other}\u0027 has its own tiling or offset; s\u0026box uses the base color\u0027s for every texture.\u0022 );\n\t\tif ( colorProperty != null \u0026\u0026 (textureScale.ContainsKey( colorProperty ) || textureOffset.ContainsKey( colorProperty )) )\n\t\t{\n\t\t\tvar scale = textureScale.GetValueOrDefault( colorProperty ) ?? new[] { 1.0, 1.0 };\n\t\t\tvar offset = textureOffset.GetValueOrDefault( colorProperty ) ?? new[] { 0.0, 0.0 };\n\t\t\t// Unity\u0027s texture V runs up from the bottom and s\u0026box\u0027s down from the top, so Unity\u0027s vertical tiling is anchored\n\t\t\t// at the other edge: the s\u0026box offset is 1 - scale - offset (measured in the engine). Horizontal is the same.\n\t\t\tvar vertical = 1 - scale[1] - offset[1];\n\t\t\tSet( \u0022g_vTexCoordScale\u0022, $\u0022[{N( scale[0] )} {N( scale[1] )}]\u0022 );\n\t\t\tSet( \u0022g_vTexCoordOffset\u0022, $\u0022[{N( offset[0] )} {N( Math.Round( vertical - Math.Floor( vertical ), 6 ) )}]\u0022 );\n\t\t}\n\t\tSet( \u0022TextureNormal\u0022, Texture( resolveNormal ?? 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\t// The complex shader raises the roughness texture to the power of g_flRoughnessScaleFactor. A scalar roughness r\n\t\t// therefore uses s\u0026box\u0027s mid-grey roughness texture with the exponent ln r / ln 0.5. A pure white roughness texture\n\t\t// renders mirror-smooth (measured in the engine), so it is never used; generated roughness maps stop at 254/255.\n\t\tif ( channelImages.TryGetValue( \u0022rough\u0022, out var roughMap ) )\n\t\t{\n\t\t\tSet( \u0022TextureRoughness\u0022, roughMap );\n\t\t\tSet( \u0022g_flRoughnessScaleFactor\u0022, \u00221\u0022 );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tvar roughness = Math.Clamp( 1 - ScalarSmoothness, 0.02, 1 );\n\t\t\tSet( \u0022TextureRoughness\u0022, MidGrey );\n\t\t\tSet( \u0022g_flRoughnessScaleFactor\u0022, N( Math.Round( Math.Log( roughness ) / Math.Log( 0.5 ), 4 ) ) );\n\t\t}\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( specularWorkflow ? 0 : Math.Clamp( Number( \u0022_Metallic\u0022, 0 ), 0, 1 ) ) );\n\t\t// Unity keeps colors from earlier shaders: built-in shaders read _Color, URP and HDRP _BaseColor, so a material\n\t\t// switched between pipelines can carry both, and only its own shader\u0027s one applies.\n\t\tvar builtinShader = (ShaderGuid ?? \u0022\u0022).Equals( UnityKnownShaders.BuiltinGuid, StringComparison.OrdinalIgnoreCase );\n\t\t// A package shader\u0027s masked color is not a tint (see ConfigureShader).\n\t\tdouble[] Tint( string name ) =\u003E maskedColors.Contains( name ) ? null : Colors.GetValueOrDefault( name );\n\t\tvar color = (builtinShader ? Tint( \u0022_Color\u0022 ) : null) ?? Tint( \u0022_BaseColor\u0022 ) ?? (colorProperty == \u0022_Diffuse\u0022 ? Tint( \u0022_MainColor\u0022 ) : null) ?? Tint( \u0022_Color\u0022 );\n\t\t// Built-in particle shaders multiply by twice the tint color.\n\t\tif ( legacyParticle \u0026\u0026 Colors.TryGetValue( \u0022_TintColor\u0022, out var particleTint ) ) color = particleTint.Select( c =\u003E Math.Min( 1, 2 * c ) ).ToArray();\n\t\tif ( color != null ) Set( \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\telse if ( UsesAlpha ) Set( \u0022TextureTranslucency\u0022, White );\n\t\t// Unity\u0027s opacity is the base color alpha times the color\u0027s alpha (glass is often an untextured color with low alpha).\n\t\tif ( UsesAlpha \u0026\u0026 color != null \u0026\u0026 color.Length \u003E 3 \u0026\u0026 color[3] \u003C 1 ) Set( \u0022g_flOpacityScale\u0022, N( Math.Clamp( color[3], 0, 1 ) ) );\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( AlphaCutoff ) );\n\t\t}\n\t\tif ( TwoSided ) text.AppendLine( \u0022\\tF_RENDER_BACKFACES 1\u0022 );\n\t\tif ( Translucent ) text.AppendLine( \u0022\\tF_TRANSLUCENT 1\u0022 );\n\t\tif ( additive \u0026\u0026 Translucent ) text.AppendLine( \u0022\\tF_ADDITIVE_BLEND 1\u0022 );\n\t\tif ( unlit )\n\t\t{\n\t\t\t// Self-illumination scaled by the base color stands in for unlit shading.\n\t\t\ttext.AppendLine( \u0022\\tF_SELF_ILLUM 1\u0022 );\n\t\t\tSet( \u0022TextureSelfIllumMask\u0022, White );\n\t\t\tSet( \u0022g_flSelfIllumAlbedoFactor\u0022, \u00221\u0022 );\n\t\t\treturn text.AppendLine( \u0022}\u0022 ).ToString();\n\t\t}\n\t\tvar emission = Texture( resolve, \u0022_EmissionMap\u0022, \u0022_EmissiveColorMap\u0022 );\n\t\t// Unity multiplies the emission map (white when absent) by an HDR color; s\u0026box takes a tint and a brightness scale.\n\t\tvar emissionColor = Colors.GetValueOrDefault( \u0022_EmissionColor\u0022 );\n\t\tvar glow = emissionColor?.Take( 3 ).Max() ?? 0;\n\t\t// A known black emission color multiplies any map to nothing.\n\t\tif ( emission != null \u0026\u0026 (emissionColor == null || glow \u003E 0) || emissionEnabled \u0026\u0026 glow \u003E 0 )\n\t\t{\n\t\t\ttext.AppendLine( \u0022\\tF_SELF_ILLUM 1\u0022 );\n\t\t\tSet( \u0022TextureSelfIllumMask\u0022, emission ?? White );\n\t\t\tif ( glow \u003E 0 )\n\t\t\t{\n\t\t\t\tvar tint = emissionColor.Take( 3 ).Select( c =\u003E Math.Clamp( c / Math.Max( glow, 1 ), 0, 1 ) ).ToArray();\n\t\t\t\tSet( \u0022g_vSelfIllumTint\u0022, $\u0022[{N( tint[0] )} {N( tint[1] )} {N( tint[2] )} 1]\u0022 );\n\t\t\t\t// The shader clamps g_flSelfIllumScale to 1; brightness above that is g_flSelfIllumBrightness, in stops.\n\t\t\t\tif ( glow \u003E 1 ) Set( \u0022g_flSelfIllumBrightness\u0022, N( Math.Round( Math.Min( 10, Math.Log2( glow ) ), 4 ) ) );\n\t\t\t}\n\t\t\t// Unity adds emission on top of the surface; s\u0026box multiplies it by the base color unless this is 0.\n\t\t\tSet( \u0022g_flSelfIllumAlbedoFactor\u0022, \u00220\u0022 );\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\tif ( terrainLayer )\n\t\t{\n\t\t\t// s\u0026box repeats a terrain material every 32 units times UVScale; Unity repeats a layer every tile size in meters.\n\t\t\tif ( Numbers.TryGetValue( \u0022_TileSize\u0022, out var tile ) ) values[\u0022UVScale\u0022] = Math.Round( tile / 0.0254 / 32, 4 );\n\t\t\tif ( Numbers.TryGetValue( \u0022_Metallic\u0022, out var metallic ) ) values[\u0022Metalness\u0022] = metallic;\n\t\t\tif ( Numbers.TryGetValue( \u0022_NormalScale\u0022, out var normal ) \u0026\u0026 Textures.ContainsKey( \u0022_BumpMap\u0022 ) ) values[\u0022NormalStrength\u0022] = normal;\n\t\t}\n\t\treturn JsonSerializer.Serialize( values, new JsonSerializerOptions { WriteIndented = true } );\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnitySky.cs","FileName":"UnitySky.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003E\n/// Unity skybox materials (Skybox/6 Sided, Cubemap, Panoramic and Procedural, or a custom shader with one of their\n/// property layouts) as s\u0026amp;box sky materials. s\u0026amp;box skies sample a latitude-longitude panorama. Unity\u0027s panorama\n/// layout is s\u0026amp;box\u0027s turned a quarter around the vertical axis, which the sky object\u0027s yaw restores together with\n/// Unity\u0027s \u003Cc\u003E_Rotation\u003C/c\u003E, so Unity panoramas (HDR ones included) are used unchanged and cube faces are composed into\n/// Unity\u0027s panorama layout.\n/// \u003C/summary\u003E\ninternal sealed class UnitySky\n{\n\tinternal enum Kind { Procedural, Panorama, Faces }\n\t/// \u003Csummary\u003EHow a cube texture or panoramic image lays out the sky.\u003C/summary\u003E\n\tinternal enum Layout { LatLong, HorizontalCross, VerticalCross, HorizontalStrip, VerticalStrip }\n\n\tpublic Kind Type { get; private init; }\n\t/// \u003Csummary\u003EPanorama: the texture GUID.\u003C/summary\u003E\n\tpublic string Texture { get; private init; }\n\t/// \u003Csummary\u003EPanorama: true for Skybox/Panoramic\u0027s 6 Frames Layout, which reads the image as a cube layout.\u003C/summary\u003E\n\tpublic bool Frames { get; private init; }\n\t/// \u003Csummary\u003ESkybox/Panoramic: its texture is a 2D image, latitude-longitude at any aspect unless \u003Csee cref=\u0022Frames\u0022/\u003E.\u003C/summary\u003E\n\tpublic bool Panoramic { get; private init; }\n\t/// \u003Csummary\u003EFaces: texture GUIDs for \u002BX, \u2212X, \u002BY, \u2212Y, \u002BZ, \u2212Z.\u003C/summary\u003E\n\tpublic string[] Faces { get; private init; }\n\t/// \u003Csummary\u003EUnity\u0027s rotation around the vertical axis, in degrees.\u003C/summary\u003E\n\tpublic double Rotation { get; private init; }\n\t/// \u003Csummary\u003ELinear color multiplier: Unity\u0027s _Tint in linear color times 4.59 (Unity\u0027s linear-space doubling, so its grey default is neutral) times _Exposure.\u003C/summary\u003E\n\tpublic double[] Tint { get; private init; }\n\t/// \u003Csummary\u003EWhy the material is a sky that cannot be converted, or null.\u003C/summary\u003E\n\tpublic string Problem { get; private init; }\n\t/// \u003Csummary\u003EThe yaw of the s\u0026amp;box sky object.\u003C/summary\u003E\n\tpublic double Yaw =\u003E Type == Kind.Procedural ? 0 : Math.Round( 90 - Rotation, 3 );\n\n\t// Skybox/6 Sided draws _LeftTex on \u002BX and _RightTex on \u2212X.\n\tstatic readonly string[] FaceProperties = { \u0022_LeftTex\u0022, \u0022_RightTex\u0022, \u0022_UpTex\u0022, \u0022_DownTex\u0022, \u0022_FrontTex\u0022, \u0022_BackTex\u0022 };\n\n\t/// \u003Csummary\u003EThe sky a material describes, or null when it is not a skybox material.\u003C/summary\u003E\n\tinternal static UnitySky Detect( UnityMaterial material )\n\t{\n\t\tvar builtin = (material.ShaderGuid ?? \u0022\u0022).Equals( UnityKnownShaders.BuiltinGuid, StringComparison.OrdinalIgnoreCase ) ? material.ShaderFileId : 0;\n\t\tvar declaredSky = (material.ShaderName ?? \u0022\u0022).StartsWith( \u0022Skybox/\u0022, StringComparison.OrdinalIgnoreCase );\n\t\tvar tint = Color( material );\n\t\tvar rotation = material.Number( \u0022_Rotation\u0022, 0 );\n\t\tif ( builtin == 104 || FaceProperties.All( material.Textures.ContainsKey ) )\n\t\t{\n\t\t\tvar missing = FaceProperties.Where( p =\u003E !material.Textures.ContainsKey( p ) ).ToArray();\n\t\t\treturn new()\n\t\t\t{\n\t\t\t\tType = Kind.Faces, Faces = FaceProperties.Select( p =\u003E material.Textures.GetValueOrDefault( p ) ).ToArray(), Tint = tint, Rotation = rotation,\n\t\t\t\tProblem = missing.Length \u003E 0 ? $\u0022The 6-sided skybox has no texture for {string.Join( \u0022, \u0022, missing )}.\u0022 : null\n\t\t\t};\n\t\t}\n\t\tif ( builtin == 103 || declaredSky \u0026\u0026 material.Textures.ContainsKey( \u0022_Tex\u0022 ) )\n\t\t\treturn new() { Type = Kind.Panorama, Texture = material.Textures.GetValueOrDefault( \u0022_Tex\u0022 ), Tint = tint, Rotation = rotation, Problem = material.Textures.ContainsKey( \u0022_Tex\u0022 ) ? null : \u0022The cubemap skybox has no cubemap.\u0022 };\n\t\tif ( builtin == 108 || declaredSky \u0026\u0026 material.Textures.ContainsKey( \u0022_MainTex\u0022 ) \u0026\u0026 material.Numbers.ContainsKey( \u0022_Mapping\u0022 ) )\n\t\t{\n\t\t\tvar problem = !material.Textures.ContainsKey( \u0022_MainTex\u0022 ) ? \u0022The panoramic skybox has no texture.\u0022 :\n\t\t\t\tmaterial.Number( \u0022_ImageType\u0022, 0 ) == 1 ? \u0022The panoramic skybox covers 180 degrees; only 360-degree panoramas are converted.\u0022 : null;\n\t\t\treturn new() { Type = Kind.Panorama, Panoramic = true, Texture = material.Textures.GetValueOrDefault( \u0022_MainTex\u0022 ), Frames = material.Number( \u0022_Mapping\u0022, 1 ) == 0, Tint = tint, Rotation = rotation, Problem = problem };\n\t\t}\n\t\t// Skybox/Procedural, and custom atmosphere skies with its layout, compute the sky from the sun.\n\t\tif ( builtin == 106 || material.Textures.Count == 0 \u0026\u0026 material.Numbers.ContainsKey( \u0022_AtmosphereThickness\u0022 ) \u0026\u0026 material.Colors.ContainsKey( \u0022_SkyTint\u0022 ) )\n\t\t\treturn new() { Type = Kind.Procedural };\n\t\treturn null;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Unity skybox shaders multiply by _Tint times unity_ColorSpaceDouble (2 in gamma, about 4.59 linear) and _Exposure,\n\t/// so the default grey tint is neutral. Material colors are stored in gamma space.\n\t/// \u003C/summary\u003E\n\tstatic double[] Color( UnityMaterial material )\n\t{\n\t\tvar tint = material.Colors.GetValueOrDefault( \u0022_Tint\u0022 ) ?? new[] { 0.5, 0.5, 0.5, 1 };\n\t\tvar exposure = Math.Max( 0, material.Number( \u0022_Exposure\u0022, 1 ) );\n\t\tstatic double Linear( double c ) =\u003E c \u003C= 0.04045 ? c / 12.92 : Math.Pow( (c \u002B 0.055) / 1.055, 2.4 );\n\t\treturn tint.Take( 3 ).Select( c =\u003E Math.Round( Linear( Math.Clamp( c, 0, 1 ) ) * 4.59479 * exposure, 4 ) ).ToArray();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// How Unity reads a cube texture (generateCubemap: 1 sphere map, 2 cylindrical, 5 full cubemap and 6 automatic,\n\t/// which go by aspect ratio) or a 6 Frames Layout panorama. Images of unknown size (HDR files) are taken as\n\t/// latitude-longitude, as Unity HDRIs are; null when Unity\u0027s layout is a sphere map or not recognized.\n\t/// \u003C/summary\u003E\n\tinternal static Layout? CubeLayout( (int Width, int Height)? size, int generate, bool frames = false )\n\t{\n\t\tif ( !frames \u0026\u0026 generate == 2 ) return Layout.LatLong;\n\t\tif ( !frames \u0026\u0026 generate is 1 or 3 or 4 ) return null;\n\t\tif ( size is not var (w, h) ) return frames ? null : Layout.LatLong;\n\t\tif ( !frames \u0026\u0026 w == 2 * h ) return Layout.LatLong;\n\t\tif ( w * 3 == h * 4 ) return Layout.HorizontalCross;\n\t\tif ( w * 4 == h * 3 ) return Layout.VerticalCross;\n\t\tif ( w == 6 * h ) return Layout.HorizontalStrip;\n\t\tif ( h == 6 * w ) return Layout.VerticalStrip;\n\t\treturn null;\n\t}\n\n\t/// \u003Csummary\u003EThe six faces (\u002BX, \u2212X, \u002BY, \u2212Y, \u002BZ, \u2212Z) of a cube layout in one image.\u003C/summary\u003E\n\tinternal static SkyFace[] Regions( Layout layout, string source, int width )\n\t{\n\t\tvar (columns, rows, cells) = layout switch\n\t\t{\n\t\t\tLayout.HorizontalStrip =\u003E (6, 1, new[] { (0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0) }),\n\t\t\tLayout.VerticalStrip =\u003E (1, 6, new[] { (0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5) }),\n\t\t\t// \u002BY above \u002BZ, with \u2212X, \u002BZ, \u002BX, \u2212Z left to right and \u2212Y below.\n\t\t\tLayout.HorizontalCross =\u003E (4, 3, new[] { (2, 1), (0, 1), (1, 0), (1, 2), (1, 1), (3, 1) }),\n\t\t\t// \u2212Z hangs below \u2212Y, upside down.\n\t\t\tLayout.VerticalCross =\u003E (3, 4, new[] { (2, 1), (0, 1), (1, 0), (1, 2), (1, 1), (1, 3) }),\n\t\t\t_ =\u003E throw new ArgumentOutOfRangeException( nameof( layout ) )\n\t\t};\n\t\tvar size = width / columns;\n\t\treturn cells.Select( ( c, i ) =\u003E new SkyFace( source, c.Item1 * size, c.Item2 * size, size, size, layout == Layout.VerticalCross \u0026\u0026 i == 5 ) ).ToArray();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// s\u0026amp;box\u0027s standard day sky, for procedural skies (Unity\u0027s Default-Skybox and skies with its layout). s\u0026amp;box\u0027s\n\t/// atmosphere sky would follow the sun like Unity\u0027s, but it has no sky texture, so a SkyBox2D\u0027s indirect lighting would\n\t/// give the scene no ambient light or reflections.\n\t/// \u003C/summary\u003E\n\tinternal const string DefaultSky = \u0022materials/skybox/skybox_day_01.vmat\u0022;\n\n\t/// \u003Csummary\u003EA sky material showing a latitude-longitude panorama.\u003C/summary\u003E\n\tinternal static string Vmat( string image ) =\u003E $\u0022Layer0\\n{{\\n\\tshader \\\u0022shaders/sky.shader\\\u0022\\n\\tSkyTexture {UnityMaterial.Quote( image )}\\n}}\\n\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// The panorama pixel\u0027s direction in Unity space (Skybox/Panoramic\u0027s mapping: u = 0.5 \u2212 atan2(z, x) / 2\u03C0, the top\n\t/// row looking up) and the cube face texel it reads (the Direct3D face table Unity uses, each face upright from inside).\n\t/// \u003C/summary\u003E\n\tinternal static (int Face, double U, double V) Sample( double u, double v )\n\t{\n\t\tvar azimuth = (0.5 - u) * 2 * Math.PI;\n\t\tvar polar = v * Math.PI;\n\t\tvar (x, y, z) = (Math.Sin( polar ) * Math.Cos( azimuth ), Math.Cos( polar ), Math.Sin( polar ) * Math.Sin( azimuth ));\n\t\tdouble ax = Math.Abs( x ), ay = Math.Abs( y ), az = Math.Abs( z ), major, s, t;\n\t\tint face;\n\t\tif ( ax \u003E= ay \u0026\u0026 ax \u003E= az ) { face = x \u003E 0 ? 0 : 1; major = ax; s = x \u003E 0 ? -z : z; t = -y; }\n\t\telse if ( ay \u003E= az ) { face = y \u003E 0 ? 2 : 3; major = ay; s = x; t = y \u003E 0 ? z : -z; }\n\t\telse { face = z \u003E 0 ? 4 : 5; major = az; s = z \u003E 0 ? x : -x; t = -y; }\n\t\treturn (face, (s / major \u002B 1) / 2, (t / major \u002B 1) / 2);\n\t}\n}\n\n/// \u003Csummary\u003EA cube face read from a source image: its pixel rectangle (the whole image when empty), upside down when Flip.\u003C/summary\u003E\npublic sealed record SkyFace( string Source, int X, int Y, int Width, int Height, bool Flip );\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/TextureChannels.cs","FileName":"TextureChannels.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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\u003E\n/// CPU image work only. Every worker owns its decoder, bitmaps and pixel buffers. Pixels stay as 8-bit RGBA bytes from\n/// decoding to encoding, single channels are written as greyscale PNGs, and PNGs use fast compression: the engine\u0027s\n/// float color arrays and default PNG compression made this most of a texture-heavy import\u0027s time.\n/// \u003C/summary\u003E\npublic static class TextureChannels\n{\n\t/// \u003Csummary\u003EParallel images: 80% of the logical processors, leaving the rest for the editor.\u003C/summary\u003E\n\tpublic static int WorkerLimit =\u003E Math.Max( 1, (int)(Environment.ProcessorCount * 0.8) );\n\t/// \u003Csummary\u003EEstimated working-set budget: a quarter of the memory available to the process, between 1 and 8 GiB.\u003C/summary\u003E\n\tpublic static long MemoryBudget =\u003E Math.Clamp( GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 4, 1L \u003C\u003C 30, 8L \u003C\u003C 30 );\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\t/// \u003Csummary\u003EProcesses the requests in parallel. A workers or budget of 0 uses \u003Csee cref=\u0022WorkerLimit\u0022/\u003E or \u003Csee cref=\u0022MemoryBudget\u0022/\u003E.\u003C/summary\u003E\n\tpublic static BatchStats ProcessBatch( IReadOnlyList\u003CTextureChannelRequest\u003E requests, CancellationToken cancel, int workers = 0, long budget = 0 )\n\t{\n\t\tvar workerCount = workers == 0 ? WorkerLimit : workers;\n\t\tvar memory = budget == 0 ? MemoryBudget : budget;\n\t\tif ( workerCount \u003C 1 || workerCount \u003E Math.Max( WorkerLimit, 2 ) || memory \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.Where( r =\u003E r.Faces == null ).GroupBy( r =\u003E r.Source, StringComparer.OrdinalIgnoreCase ).Select( g =\u003E g.ToArray() ).ToArray();\n\t\t// A source that cannot be processed gets neutral placeholders, so one bad image does not stop the import; the\n\t\t// failures are thrown together once every other image is done.\n\t\tvar failures = new System.Collections.Concurrent.ConcurrentBag\u003CTextureBatchException.Failure\u003E();\n\t\tvoid Fail( IEnumerable\u003CTextureChannelRequest\u003E failed, Exception ex )\n\t\t{\n\t\t\tforeach ( var request in failed )\n\t\t\t{\n\t\t\t\tfailures.Add( new( request.Source, request.Destination, ex.Message ) );\n\t\t\t\ttry { File.WriteAllBytes( request.Destination, Placeholder( request ) ); }\n\t\t\t\tcatch ( IOException ) { }\n\t\t\t}\n\t\t}\n\t\tParallel.ForEach( groups, new ParallelOptions { MaxDegreeOfParallelism = workerCount, 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\tlong reservation;\n\t\t\ttry { reservation = Math.Min( memory, EstimateMemory( group[0].Source, memory ) ); }\n\t\t\tcatch ( IOException ) { reservation = memory; } // An unreadable header runs exclusively; decoding reports it.\n\t\t\tlock ( gate )\n\t\t\t{\n\t\t\t\twhile ( reserved \u002B reservation \u003E memory ) { 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\tvar written = new List\u003CTextureChannelRequest\u003E();\n\t\t\ttry\n\t\t\t{\n\t\t\t\tusing var source = DecodeRgba( group[0].Source, cancel );\n\t\t\t\tInterlocked.Increment( ref decodes );\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\tFile.WriteAllBytes( request.Destination, Process( source, request, cancel ) );\n\t\t\t\t\twritten.Add( request );\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch ( OperationCanceledException ) { throw; }\n\t\t\tcatch ( Exception ex ) { Fail( group.Except( written ), ex ); }\n\t\t\tfinally { lock ( gate ) { reserved -= reservation; active--; Monitor.PulseAll( gate ); } }\n\t\t} );\n\t\t// Sky panoramas read several sources each; they run one at a time after the channel work.\n\t\tforeach ( var request in requests.Where( r =\u003E r.Faces != null ) )\n\t\t{\n\t\t\ttry { decodes \u002B= ComposeSky( request, cancel ); }\n\t\t\tcatch ( OperationCanceledException ) { throw; }\n\t\t\tcatch ( Exception ex ) { Fail( new[] { request }, ex ); }\n\t\t}\n\t\tif ( !failures.IsEmpty ) throw new TextureBatchException( failures.OrderBy( f =\u003E f.Destination, StringComparer.OrdinalIgnoreCase ).ToArray() );\n\t\treturn new( decodes, requests.Count, peakWorkers, peakReserved, timer.ElapsedMilliseconds );\n\t}\n\n\t/// \u003Csummary\u003EOne request\u0027s PNG from the decoded source, resized first when the request sets a size.\u003C/summary\u003E\n\tstatic byte[] Process( SKBitmap source, TextureChannelRequest request, CancellationToken cancel )\n\t{\n\t\tvar resize = request.Width \u003E 0 \u0026\u0026 request.Height \u003E 0 \u0026\u0026 (request.Width != source.Width || request.Height != source.Height);\n\t\t// The same resampling as the engine\u0027s Bitmap.Resize.\n\t\tusing var resized = resize ? source.Resize( new SKImageInfo( request.Width, request.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul ),\n\t\t\tnew SKSamplingOptions( SKFilterMode.Linear, SKMipmapMode.Linear ) ) : null;\n\t\tvar image = resized ?? source;\n\t\tif ( request.Channel == -1 ) return Encode( image );\n\t\tif ( request.Channel \u003E= 0 )\n\t\t{\n\t\t\tusing var gray = new SKBitmap( image.Width, image.Height, SKColorType.Gray8, SKAlphaType.Opaque );\n\t\t\tConvertChannel( image.GetPixelSpan(), gray.GetPixelSpan(), request.Channel, request.Scale, request.Invert, cancel );\n\t\t\treturn Encode( gray );\n\t\t}\n\t\tusing var output = new SKBitmap( image.Width, image.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul );\n\t\tif ( request.Channel == -3 ) FlipGreen( image.GetPixelSpan(), output.GetPixelSpan() );\n\t\telse HeightToNormal( image.GetPixelSpan(), output.GetPixelSpan(), image.Width, image.Height, request.Scale, request.Invert );\n\t\treturn Encode( output );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A neutral 1\u00D71 PNG for an output whose source could not be processed: a flat normal for normal maps, mid-grey for\n\t/// roughness (an inverted smoothness channel), black for metalness (red) and height (blue), white for color,\n\t/// occlusion (green) and opacity (alpha), as Unity\u0027s default texture is white.\n\t/// \u003C/summary\u003E\n\tstatic byte[] Placeholder( TextureChannelRequest request )\n\t{\n\t\tusing var bitmap = new SKBitmap( 1, 1, SKColorType.Rgba8888, SKAlphaType.Unpremul );\n\t\tbitmap.SetPixel( 0, 0, request.Channel switch\n\t\t{\n\t\t\t-3 or -4 =\u003E new SKColor( 128, 128, 255 ),\n\t\t\t\u003E= 0 when request.Invert =\u003E new SKColor( 128, 128, 128 ),\n\t\t\t0 or 2 =\u003E SKColors.Black,\n\t\t\t_ =\u003E SKColors.White,\n\t\t} );\n\t\treturn Encode( bitmap );\n\t}\n\n\tstatic byte[] Encode( SKBitmap bitmap )\n\t{\n\t\tusing var stream = new SKDynamicMemoryWStream();\n\t\tusing var pixmap = bitmap.PeekPixels();\n\t\t// zlib level 3: about half the time of the default level 6 for 10-20% larger files.\n\t\tif ( pixmap == null || !pixmap.Encode( stream, new SKPngEncoderOptions( SKPngEncoderFilterFlags.AllFilters, 3 ) ) ) throw new InvalidDataException( \u0022PNG encoding failed.\u0022 );\n\t\tusing var data = stream.DetachAsData();\n\t\treturn data.ToArray();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The image as 8-bit RGBA: Skia decodes PNG, JPEG, BMP and WebP directly; TGA, PSD, TIFF and the engine\u0027s other\n\t/// formats go through the engine\u0027s decoder.\n\t/// \u003C/summary\u003E\n\tstatic SKBitmap DecodeRgba( string source, CancellationToken cancel )\n\t{\n\t\tcancel.ThrowIfCancellationRequested();\n\t\tvar bytes = File.ReadAllBytes( source );\n\t\tvar info = default( SKImageInfo );\n\t\tusing ( var codec = SKCodec.Create( new SKMemoryStream( bytes ) ) )\n\t\t{\n\t\t\tif ( codec != null )\n\t\t\t{\n\t\t\t\tinfo = new SKImageInfo( codec.Info.Width, codec.Info.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul );\n\t\t\t\tif ( SKBitmap.Decode( codec, info ) is { } decoded ) return decoded;\n\t\t\t}\n\t\t}\n\t\tusing var bitmap = Bitmap.CreateFromBytes( bytes ) ?? throw new InvalidDataException( \u0022Unable to decode packed texture: \u0022 \u002B source );\n\t\tvar colors = bitmap.GetPixels32();\n\t\tvar result = new SKBitmap( bitmap.Width, bitmap.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul );\n\t\tvar pixels = result.GetPixelSpan();\n\t\tfor ( var i = 0; i \u003C colors.Length; i\u002B\u002B )\n\t\t{\n\t\t\tpixels[i * 4] = colors[i].r; pixels[i * 4 \u002B 1] = colors[i].g; pixels[i * 4 \u002B 2] = colors[i].b; pixels[i * 4 \u002B 3] = colors[i].a;\n\t\t}\n\t\treturn result;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Composes a latitude-longitude panorama in Unity\u0027s layout from six cube faces (\u002BX, \u2212X, \u002BY, \u2212Y, \u002BZ, \u2212Z), sampling\n\t/// each face bilinearly. The panorama is four face widths wide (a power of two, at most 4096) unless Width is set.\n\t/// \u003C/summary\u003E\n\tstatic int ComposeSky( TextureChannelRequest request, CancellationToken cancel )\n\t{\n\t\tvar images = new Dictionary\u003Cstring, (Color[] Pixels, int Width, int Height)\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tforeach ( var source in request.Faces.Select( f =\u003E f.Source ).Distinct( StringComparer.OrdinalIgnoreCase ) )\n\t\t{\n\t\t\tcancel.ThrowIfCancellationRequested();\n\t\t\tusing var bitmap = Bitmap.CreateFromBytes( File.ReadAllBytes( source ) ) ?? throw new InvalidDataException( \u0022Unable to decode sky face: \u0022 \u002B source );\n\t\t\timages[source] = (bitmap.GetPixels(), bitmap.Width, bitmap.Height);\n\t\t}\n\t\tvar faces = request.Faces.Select( f =\u003E\n\t\t{\n\t\t\tvar image = images[f.Source];\n\t\t\treturn f.Width \u003E 0 ? (image.Pixels, Stride: image.Width, f.X, f.Y, f.Width, f.Height, f.Flip) : (image.Pixels, Stride: image.Width, 0, 0, image.Width, image.Height, f.Flip);\n\t\t} ).ToArray();\n\t\tvar faceSize = faces.Max( f =\u003E Math.Max( f.Item5, f.Item6 ) );\n\t\tvar width = request.Width \u003E 0 ? request.Width : Math.Min( 4096, UnityImageSize.NearestPowerOfTwo( 4 * faceSize ) );\n\t\tvar height = width / 2;\n\t\tvar output = new Color[width * height];\n\t\tParallel.For( 0, height, new ParallelOptions { MaxDegreeOfParallelism = WorkerLimit, CancellationToken = cancel }, y =\u003E\n\t\t{\n\t\t\tfor ( var x = 0; x \u003C width; x\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar (index, u, v) = UnitySky.Sample( (x \u002B 0.5) / width, (y \u002B 0.5) / height );\n\t\t\t\tvar (pixels, stride, left, top, w, h, flip) = faces[index];\n\t\t\t\tif ( flip ) { u = 1 - u; v = 1 - v; }\n\t\t\t\t// Bilinear, clamped inside the face\u0027s rectangle.\n\t\t\t\tvar fx = Math.Clamp( u * w - 0.5, 0, w - 1 );\n\t\t\t\tvar fy = Math.Clamp( v * h - 0.5, 0, h - 1 );\n\t\t\t\tint x0 = (int)fx, y0 = (int)fy, x1 = Math.Min( x0 \u002B 1, w - 1 ), y1 = Math.Min( y0 \u002B 1, h - 1 );\n\t\t\t\tfloat tx = (float)(fx - x0), ty = (float)(fy - y0);\n\t\t\t\tColor At( int px, int py ) =\u003E pixels[(top \u002B py) * stride \u002B left \u002B px];\n\t\t\t\tvar a = Color.Lerp( At( x0, y0 ), At( x1, y0 ), tx );\n\t\t\t\tvar b = Color.Lerp( At( x0, y1 ), At( x1, y1 ), tx );\n\t\t\t\tvar c = Color.Lerp( a, b, ty );\n\t\t\t\toutput[y * width \u002B x] = c.WithAlpha( 1 );\n\t\t\t}\n\t\t} );\n\t\tusing var panorama = new Bitmap( width, height );\n\t\tpanorama.SetPixels( output );\n\t\tFile.WriteAllBytes( request.Destination, panorama.ToPng() );\n\t\treturn images.Count;\n\t}\n\n\tstatic long EstimateMemory( string source, long unknown )\n\t{\n\t\t// Skia reads PNG, JPEG and WebP headers; the importer\u0027s own reader covers TGA, PSD and TIFF. Skia closes the stream\n\t\t// when it cannot read it, so the file length comes from the file system.\n\t\tvar length = new FileInfo( source ).Length;\n\t\tusing var stream = File.OpenRead( source );\n\t\tusing var codec = SKCodec.Create( stream );\n\t\tvar size = codec != null ? (codec.Info.Width, codec.Info.Height) : UnityImageSize.Read( source );\n\t\tif ( size is not var (width, height) ) return unknown;\n\t\t// Decoded RGBA, a resized copy, an output image, encoder buffers and compressed input; engine-decoded formats\n\t\t// also hold the engine\u0027s bitmap and its 32-bit color array while converting.\n\t\treturn checked( (long)width * height * (codec != null ? 16 : 28) \u002B length * 2 \u002B 16 * 1024 * 1024 );\n\t}\n\n\t// One channel of one image, without batching: the reference output for the batch checks.\n\tpublic static byte[] Extract( string source, int channel, double scale, bool invert, CancellationToken cancel )\n\t{\n\t\tusing var bitmap = DecodeRgba( source, cancel );\n\t\treturn Process( bitmap, new TextureChannelRequest( source, null, channel, scale, invert ), cancel );\n\t}\n\n\t// A DirectX-style normal map (green down) as the OpenGL style Unity and s\u0026box use.\n\tstatic void FlipGreen( ReadOnlySpan\u003Cbyte\u003E source, Span\u003Cbyte\u003E output )\n\t{\n\t\tsource.CopyTo( output );\n\t\tfor ( var i = 1; i \u003C output.Length; i \u002B= 4 ) output[i] = (byte)(255 - output[i]);\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Unity\u0027s Create from Grayscale: the image\u0027s luminance as height, its slopes as an OpenGL-style normal map. Bumpiness\n\t/// scales the slopes (Unity\u0027s default is 0.25); Sobel averages the slopes over the neighbouring rows and columns. An\n\t/// approximation of Unity\u0027s filter, which is not documented exactly.\n\t/// \u003C/summary\u003E\n\tstatic void HeightToNormal( ReadOnlySpan\u003Cbyte\u003E source, Span\u003Cbyte\u003E output, int width, int height, double bumpiness, bool sobel )\n\t{\n\t\tvar heights = new float[width * height];\n\t\tfor ( var i = 0; i \u003C heights.Length; i\u002B\u002B ) heights[i] = (0.299f * source[i * 4] \u002B 0.587f * source[i * 4 \u002B 1] \u002B 0.114f * source[i * 4 \u002B 2]) / 255f;\n\t\tfloat H( int x, int y ) =\u003E heights[((y % height \u002B height) % height) * width \u002B (x % width \u002B width) % width];\n\t\tvar strength = (float)(bumpiness * 64);\n\t\tfor ( var y = 0; y \u003C height; y\u002B\u002B )\n\t\t\tfor ( var x = 0; x \u003C width; x\u002B\u002B )\n\t\t\t{\n\t\t\t\t// Rows run down the image, so the slope toward the image top is H(row - 1) - H(row \u002B 1).\n\t\t\t\tfloat dx = H( x \u002B 1, y ) - H( x - 1, y ), dy = H( x, y - 1 ) - H( x, y \u002B 1 );\n\t\t\t\tif ( sobel )\n\t\t\t\t{\n\t\t\t\t\tdx = (dx * 2 \u002B H( x \u002B 1, y - 1 ) - H( x - 1, y - 1 ) \u002B H( x \u002B 1, y \u002B 1 ) - H( x - 1, y \u002B 1 )) / 4;\n\t\t\t\t\tdy = (dy * 2 \u002B H( x - 1, y - 1 ) - H( x - 1, y \u002B 1 ) \u002B H( x \u002B 1, y - 1 ) - H( x \u002B 1, y \u002B 1 )) / 4;\n\t\t\t\t}\n\t\t\t\tvar n = new Vector3( -dx * strength / 2, -dy * strength / 2, 1 ).Normal;\n\t\t\t\tvar at = (y * width \u002B x) * 4;\n\t\t\t\toutput[at] = Byte( n.x * 0.5f \u002B 0.5f ); output[at \u002B 1] = Byte( n.y * 0.5f \u002B 0.5f ); output[at \u002B 2] = Byte( n.z * 0.5f \u002B 0.5f ); output[at \u002B 3] = 255;\n\t\t\t}\n\t}\n\n\tstatic byte Byte( float value ) =\u003E (byte)Math.Clamp( MathF.Round( value * 255 ), 0, 255 );\n\n\tstatic void ConvertChannel( ReadOnlySpan\u003Cbyte\u003E source, Span\u003Cbyte\u003E output, int channel, double scale, bool invert, CancellationToken cancel )\n\t{\n\t\t// Every byte value maps to one output value, so the conversion is a table lookup.\n\t\tSpan\u003Cbyte\u003E table = stackalloc byte[256];\n\t\tfor ( var v = 0; v \u003C 256; v\u002B\u002B )\n\t\t{\n\t\t\tvar value = (float)Math.Clamp( v / 255.0 * scale, 0, 1 );\n\t\t\t// Inverted channels are roughness from smoothness. s\u0026box renders a pure white roughness texel mirror-smooth, so\n\t\t\t// fully rough stops at 254/255.\n\t\t\tif ( invert ) value = Math.Min( 1 - value, 254f / 255f );\n\t\t\ttable[v] = Byte( value );\n\t\t}\n\t\tvar offset = Math.Clamp( channel, 0, 3 );\n\t\tfor ( var i = 0; i \u003C output.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( (i \u0026 0xFFFF) == 0 ) cancel.ThrowIfCancellationRequested();\n\t\t\toutput[i] = table[source[i * 4 \u002B offset]];\n\t\t}\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityFbxBindings.cs","FileName":"UnityFbxBindings.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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\t/// \u003Csummary\u003EPer geometry (or legacy mesh model): the node\u0027s material indices in the order its polygons first use them.\u003C/summary\u003E\n\tinternal readonly Dictionary\u003Clong, List\u003Cint\u003E\u003E MaterialOrder = new();\n\t/// \u003Csummary\u003EPer model node: transform properties (Lcl Translation, PreRotation, RotationOrder...) in file units and degrees.\u003C/summary\u003E\n\tinternal readonly Dictionary\u003Clong, Dictionary\u003Cstring, double[]\u003E\u003E NodeProperties = new();\n\t/// \u003Csummary\u003EPer geometry (or legacy mesh model): the bounds of its control points, in its own space and file units.\u003C/summary\u003E\n\tinternal readonly Dictionary\u003Clong, (double[] Min, double[] Max)\u003E VertexBounds = new();\n\t/// \u003Csummary\u003EGlobalSettings axis system (UpAxis, UpAxisSign, FrontAxis...).\u003C/summary\u003E\n\tinternal readonly Dictionary\u003Cstring, double\u003E Axes = new( StringComparer.Ordinal );\n\tinternal static readonly string[] TransformProperties = { \u0022Lcl Translation\u0022, \u0022Lcl Rotation\u0022, \u0022Lcl Scaling\u0022, \u0022PreRotation\u0022, \u0022PostRotation\u0022, \u0022RotationOrder\u0022,\n\t\t\u0022RotationPivot\u0022, \u0022RotationOffset\u0022, \u0022ScalingPivot\u0022, \u0022ScalingOffset\u0022, \u0022GeometricTranslation\u0022, \u0022GeometricRotation\u0022, \u0022GeometricScaling\u0022 };\n\tinternal static readonly string[] AxisProperties = { \u0022UpAxis\u0022, \u0022UpAxisSign\u0022, \u0022FrontAxis\u0022, \u0022FrontAxisSign\u0022, \u0022CoordAxis\u0022, \u0022CoordAxisSign\u0022 };\n\tinternal void Property( long node, string name, double[] values )\n\t{\n\t\tif ( values.Length == 0 ) return;\n\t\tif ( Array.IndexOf( AxisProperties, name ) \u003E= 0 ) Axes[name] = values[0];\n\t\telse if ( node != 0 \u0026\u0026 Array.IndexOf( TransformProperties, name ) \u003E= 0 )\n\t\t{\n\t\t\tif ( !NodeProperties.TryGetValue( node, out var properties ) ) NodeProperties[node] = properties = new( StringComparer.Ordinal );\n\t\t\tproperties[name] = values;\n\t\t}\n\t}\n\treadonly Dictionary\u003Cstring, long\u003E legacyIds = new( StringComparer.Ordinal );\n\tlong asciiModel;\n\tlong asciiOwner;\n\tbool asciiLayer;\n\tList\u003Cint\u003E asciiOrder;\n\n\t/// \u003Csummary\u003ERecords the first use of each material index in a polygon material array.\u003C/summary\u003E\n\tinternal static void FirstUse( List\u003Cint\u003E order, IEnumerable\u003Cint\u003E indices )\n\t{\n\t\tforeach ( var index in indices )\n\t\t\tif ( !order.Contains( index ) ) order.Add( index );\n\t}\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; asciiModel = 0; }\n\t\t\telse { Nodes[id] = (item.Groups[3].Value, item.Groups[4].Value == \u0022Mesh\u0022); asciiModel = id; }\n\t\t}\n\t\telse if ( Regex.IsMatch( line, @\u0022^\\s*(?:Geometry|NodeAttribute|Texture|Video|Deformer|Pose|Material):\u0022 ) ) asciiModel = 0;\n\t\t// \u0022P: \u0022Lcl Translation\u0022, \u0022Lcl Translation\u0022, \u0022\u0022, \u0022A\u0022,1,2,3\u0022 (FBX 7) or \u0022Property: \u0022Lcl Translation\u0022, \u0022Lcl Translation\u0022, \u0022A\u002B\u0022,1,2,3\u0022 (FBX 6).\n\t\tvar property = Regex.Match( line, @\u0022^\\s*(?:P|Property):\\s*\u0022\u0022([^\u0022\u0022]\u002B)\u0022\u0022(.*)$\u0022 );\n\t\tif ( property.Success )\n\t\t{\n\t\t\tvar values = Regex.Matches( Regex.Replace( property.Groups[2].Value, @\u0022\u0022\u0022[^\u0022\u0022]*\u0022\u0022\u0022, \u0022\u0022 ), @\u0022-?[0-9]*\\.?[0-9]\u002B(?:[eE][-\u002B]?[0-9]\u002B)?\u0022 )\n\t\t\t\t.Select( m =\u003E double.Parse( m.Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture ) ).ToArray();\n\t\t\tProperty( asciiModel, property.Groups[1].Value, values );\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\tObserveAsciiMaterialLayer( line, item );\n\t\tObserveAsciiVertices( line );\n\t}\n\n\t// Polygon material indices: \u0022Materials: *N { a: 0,1,... }\u0022 (FBX 7) or \u0022Materials: 0,1,...\u0022 continued on comma-led lines (FBX 6),\n\t// inside the first LayerElementMaterial of a Geometry object (FBX 7) or of the mesh model itself (FBX 6).\n\tvoid ObserveAsciiMaterialLayer( string line, Match item )\n\t{\n\t\tstatic IEnumerable\u003Cint\u003E Numbers( string text ) =\u003E Regex.Matches( Regex.Replace( text, @\u0022\\*\\d\u002B\\s*\\{\u0022, \u0022\u0022 ), \u0022-?[0-9]\u002B\u0022 ).Select( m =\u003E int.Parse( m.Value ) );\n\t\tif ( asciiOrder != null )\n\t\t{\n\t\t\tvar continued = Regex.Match( line, @\u0022^\\s*(?:a:\\s*)?(?=[-0-9,])(.*)$\u0022 );\n\t\t\tif ( continued.Success ) { FirstUse( asciiOrder, Numbers( continued.Groups[1].Value ) ); return; }\n\t\t\tMaterialOrder[asciiOwner] = asciiOrder;\n\t\t\tasciiOrder = null;\n\t\t\tasciiLayer = false;\n\t\t}\n\t\tvar geometry = Regex.Match( line, \u0022^\\\\s*Geometry:\\\\s*([-0-9]\u002B),\u0022 );\n\t\tif ( geometry.Success ) asciiOwner = Id( geometry.Groups[1].Value );\n\t\telse if ( item.Success \u0026\u0026 item.Groups[1].Value == \u0022Model\u0022 \u0026\u0026 item.Groups[4].Value == \u0022Mesh\u0022 )\n\t\t\tasciiOwner = Id( item.Groups[2].Success ? item.Groups[2].Value : \u0022Model::\u0022 \u002B item.Groups[3].Value );\n\t\tif ( Regex.IsMatch( line, @\u0022^\\s*LayerElementMaterial:\u0022 ) ) asciiLayer = !MaterialOrder.ContainsKey( asciiOwner );\n\t\tvar materials = asciiLayer ? Regex.Match( line, @\u0022^\\s*Materials:\\s*(.*)$\u0022 ) : Match.Empty;\n\t\tif ( materials.Success ) { asciiOrder = new(); FirstUse( asciiOrder, Numbers( materials.Groups[1].Value ) ); }\n\t}\n\n\tinternal void FinishAscii()\n\t{\n\t\tif ( asciiOrder != null ) MaterialOrder[asciiOwner] = asciiOrder;\n\t\tasciiOrder = null;\n\t\tasciiVertices?.Finish( this );\n\t\tasciiVertices = null;\n\t}\n\n\t/// \u003Csummary\u003EAccumulates control-point bounds from a flat x, y, z sequence; the first array per owner counts.\u003C/summary\u003E\n\tinternal sealed class BoundsBuilder\n\t{\n\t\treadonly long owner;\n\t\treadonly double[] min = { double.MaxValue, double.MaxValue, double.MaxValue }, max = { double.MinValue, double.MinValue, double.MinValue };\n\t\tint component;\n\t\tinternal BoundsBuilder( long owner ) { this.owner = owner; }\n\t\tinternal void Add( double value )\n\t\t{\n\t\t\tif ( double.IsFinite( value ) ) { min[component] = Math.Min( min[component], value ); max[component] = Math.Max( max[component], value ); }\n\t\t\tcomponent = (component \u002B 1) % 3;\n\t\t}\n\t\tinternal void Finish( UnityFbxBindings bindings )\n\t\t{\n\t\t\tif ( min[0] \u003C= max[0] \u0026\u0026 min[1] \u003C= max[1] \u0026\u0026 min[2] \u003C= max[2] ) bindings.VertexBounds.TryAdd( owner, (min, max) );\n\t\t}\n\t}\n\tBoundsBuilder asciiVertices;\n\n\t// Control points: \u0022Vertices: *N { a: x,y,z,... }\u0022 (FBX 7, on Geometry) or \u0022Vertices: x,y,z,...\u0022 continued on comma-led\n\t// lines (FBX 6, on the mesh model).\n\tvoid ObserveAsciiVertices( string line )\n\t{\n\t\tstatic IEnumerable\u003Cdouble\u003E Numbers( string text ) =\u003E Regex.Matches( Regex.Replace( text, @\u0022\\*\\d\u002B\\s*\\{\u0022, \u0022\u0022 ), @\u0022[-\u002B]?(?:[0-9]\u002B\\.?[0-9]*|\\.[0-9]\u002B)(?:[eE][-\u002B]?[0-9]\u002B)?\u0022 )\n\t\t\t.Select( m =\u003E double.Parse( m.Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture ) );\n\t\tif ( asciiVertices != null )\n\t\t{\n\t\t\tvar continued = Regex.Match( line, @\u0022^\\s*(?:a:\\s*)?(?=[-\u002B0-9.,])(.*)$\u0022 );\n\t\t\tif ( continued.Success ) { foreach ( var value in Numbers( continued.Groups[1].Value ) ) asciiVertices.Add( value ); return; }\n\t\t\tasciiVertices.Finish( this );\n\t\t\tasciiVertices = null;\n\t\t}\n\t\tvar start = Regex.Match( line, @\u0022^\\s*Vertices:\\s*(.*)$\u0022 );\n\t\tif ( !start.Success || asciiOwner == 0 || VertexBounds.ContainsKey( asciiOwner ) ) return;\n\t\tasciiVertices = new BoundsBuilder( asciiOwner );\n\t\tforeach ( var value in Numbers( start.Groups[1].Value ) ) asciiVertices.Add( 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\t// Unity numbers nodes that share a path in traversal order, assumed here to be file connection order.\n\t\tvar order = new Dictionary\u003Clong, int\u003E();\n\t\tfor ( var i = 0; i \u003C Connections.Count; i\u002B\u002B ) order.TryAdd( Connections[i].Child, i );\n\t\tvar complete = true;\n\t\tvar meshes = new List\u003C(string Path, UnityModel.RendererBinding Binding)\u003E();\n\t\tforeach ( var (id, node) in Nodes.Where( p =\u003E p.Value.Mesh ).Select( (p, index) =\u003E (p, index) )\n\t\t\t.OrderBy( x =\u003E order.GetValueOrDefault( x.p.Key, int.MaxValue ) ).ThenBy( x =\u003E x.index ).Select( x =\u003E x.p ) )\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 ) { complete = false; 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 slots = Connections.Where( c =\u003E c.Parent == id \u0026\u0026 Materials.ContainsKey( c.Child ) ).Select( c =\u003E Materials[c.Child] ).ToArray();\n\t\t\t// Unity numbers a mesh\u0027s submeshes (renderer material slots) in the order its polygons first use each\n\t\t\t// material, and leaves out materials no polygon uses; the connection order is only the FBX material list.\n\t\t\tvar firstUse = MaterialOrder.GetValueOrDefault( id ) ?? Connections.Where( c =\u003E c.Parent == id \u0026\u0026 MaterialOrder.ContainsKey( c.Child ) ).Select( c =\u003E MaterialOrder[c.Child] ).FirstOrDefault();\n\t\t\tvar used = firstUse?.Where( i =\u003E i \u003E= 0 \u0026\u0026 i \u003C slots.Length ).Select( i =\u003E slots[i] ).ToArray();\n\t\t\tif ( used is { Length: \u003E 0 } ) slots = used;\n\t\t\tmeshes.Add( (\u0022//RootNode/\u0022 \u002B string.Join( \u0022/\u0022, names ), new( node.Name, slots )) );\n\t\t}\n\t\t// Node transforms (FBX\u0027s TRS with pivots and pre/post rotations) up the hierarchy, for placing single meshes.\n\t\tUnityTransform Local( long node )\n\t\t{\n\t\t\tif ( !NodeProperties.TryGetValue( node, out var p ) ) return UnityTransform.Identity;\n\t\t\tdouble[] V( string key, double fallback ) =\u003E p.TryGetValue( key, out var v ) \u0026\u0026 v.Length \u003E= 3 ? v : new[] { fallback, fallback, fallback };\n\t\t\tUnityTransform T( double[] v ) =\u003E UnityTransform.Translation( v[0], v[1], v[2] );\n\t\t\tUnityTransform E( double[] v, int order = 0 ) =\u003E UnityTransform.Euler( v[0], v[1], v[2], order );\n\t\t\tvar order = p.TryGetValue( \u0022RotationOrder\u0022, out var o ) \u0026\u0026 o.Length \u003E 0 ? (int)o[0] : 0;\n\t\t\tdouble[] rp = V( \u0022RotationPivot\u0022, 0 ), sp = V( \u0022ScalingPivot\u0022, 0 ), s = V( \u0022Lcl Scaling\u0022, 1 );\n\t\t\treturn T( V( \u0022Lcl Translation\u0022, 0 ) ) * T( V( \u0022RotationOffset\u0022, 0 ) ) * T( rp ) * E( V( \u0022PreRotation\u0022, 0 ) ) * E( V( \u0022Lcl Rotation\u0022, 0 ), order ) *\n\t\t\t\tE( V( \u0022PostRotation\u0022, 0 ) ).Inverse() * T( rp ).Inverse() * T( V( \u0022ScalingOffset\u0022, 0 ) ) * T( sp ) * UnityTransform.Scaling( s[0], s[1], s[2] ) * T( sp ).Inverse();\n\t\t}\n\t\tUnityTransform Geometric( long node )\n\t\t{\n\t\t\tif ( !NodeProperties.TryGetValue( node, out var p ) ) return UnityTransform.Identity;\n\t\t\tdouble[] V( string key, double fallback ) =\u003E p.TryGetValue( key, out var v ) \u0026\u0026 v.Length \u003E= 3 ? v : new[] { fallback, fallback, fallback };\n\t\t\tdouble[] t = V( \u0022GeometricTranslation\u0022, 0 ), r = V( \u0022GeometricRotation\u0022, 0 ), s = V( \u0022GeometricScaling\u0022, 1 );\n\t\t\treturn UnityTransform.Translation( t[0], t[1], t[2] ) * UnityTransform.Euler( r[0], r[1], r[2] ) * UnityTransform.Scaling( s[0], s[1], s[2] );\n\t\t}\n\t\tUnityTransform Global( long node, int depth = 0 ) =\u003E\n\t\t\tdepth \u003E 64 || !parents.TryGetValue( node, out var parent ) || parent == 0 || !Nodes.ContainsKey( parent ) ? Local( node ) : Global( parent, depth \u002B 1 ) * Local( node );\n\t\t// Without Preserve Hierarchy, Unity makes a file\u0027s only top node the prefab root.\n\t\tif ( !preserveHierarchy \u0026\u0026 roots.Length == 1 ) model.CollapsedRoot = Local( roots[0] );\n\t\tforeach ( var group in Nodes.Where( n =\u003E n.Value.Mesh ).GroupBy( n =\u003E n.Value.Name, StringComparer.Ordinal ) )\n\t\t{\n\t\t\tif ( group.Count() != 1 ) continue;\n\t\t\tvar id = group.First().Key;\n\t\t\tvar global = model.MeshNodes[group.Key] = Global( id );\n\t\t\t// The mesh\u0027s bounds in the file\u0027s space: its control points\u0027 box, with corners through the node transforms.\n\t\t\tvar geometry = VertexBounds.ContainsKey( id ) ? id : Connections.Where( c =\u003E c.Parent == id \u0026\u0026 VertexBounds.ContainsKey( c.Child ) ).Select( c =\u003E c.Child ).FirstOrDefault();\n\t\t\tif ( !VertexBounds.TryGetValue( geometry, out var box ) ) continue;\n\t\t\t// The geometric transform (an exporter\u0027s pivot offset) moves only this node\u0027s mesh, not its children.\n\t\t\tvar placed = global * Geometric( id );\n\t\t\tvar corners = Enumerable.Range( 0, 8 ).Select( i =\u003E placed.Point( (i \u0026 1) == 0 ? box.Min[0] : box.Max[0], (i \u0026 2) == 0 ? box.Min[1] : box.Max[1], (i \u0026 4) == 0 ? box.Min[2] : box.Max[2] ) ).ToArray();\n\t\t\tmodel.MeshBounds[group.Key] = (Enumerable.Range( 0, 3 ).Select( a =\u003E corners.Min( c =\u003E c[a] ) ).ToArray(), Enumerable.Range( 0, 3 ).Select( a =\u003E corners.Max( c =\u003E c[a] ) ).ToArray());\n\t\t}\n\t\t// The file\u0027s axis system as a basis change into FBX\u0027s default (Y up, Z front, X across, right-handed).\n\t\tdouble Axis( string key, double fallback ) =\u003E Axes.TryGetValue( key, out var value ) ? value : fallback;\n\t\tint up = (int)Axis( \u0022UpAxis\u0022, 1 ), front = (int)Axis( \u0022FrontAxis\u0022, 2 ), coord = (int)Axis( \u0022CoordAxis\u0022, 0 );\n\t\tif ( new[] { up, front, coord }.Distinct().Count() == 3 \u0026\u0026 new[] { up, front, coord }.All( a =\u003E a is \u003E= 0 and \u003C= 2 ) )\n\t\t{\n\t\t\tvar basis = new double[3, 3];\n\t\t\tbasis[0, coord] = Math.Sign( Axis( \u0022CoordAxisSign\u0022, 1 ) ) is 0 ? 1 : Math.Sign( Axis( \u0022CoordAxisSign\u0022, 1 ) );\n\t\t\tbasis[1, up] = Math.Sign( Axis( \u0022UpAxisSign\u0022, 1 ) ) is 0 ? 1 : Math.Sign( Axis( \u0022UpAxisSign\u0022, 1 ) );\n\t\t\tbasis[2, front] = Math.Sign( Axis( \u0022FrontAxisSign\u0022, 1 ) ) is 0 ? 1 : Math.Sign( Axis( \u0022FrontAxisSign\u0022, 1 ) );\n\t\t\tmodel.FileAxes = UnityTransform.Linear( basis );\n\t\t}\n\t\tvar collisions = new HashSet\u003Clong\u003E();\n\t\tbool Register( Dictionary\u003Clong, UnityModel.RendererBinding\u003E table, IEnumerable\u003C(string Key, UnityModel.RendererBinding Binding)\u003E items, Func\u003Cstring, int, IEnumerable\u003Clong\u003E\u003E ids )\n\t\t{\n\t\t\tvar identified = true;\n\t\t\tforeach ( var group in items.GroupBy( m =\u003E m.Key ) )\n\t\t\t{\n\t\t\t\tvar bindings = group.Select( m =\u003E m.Binding ).ToArray();\n\t\t\t\t// Same-named objects are only identified when they are interchangeable, so an unverified order cannot change slots.\n\t\t\t\tif ( bindings.Skip( 1 ).Any( b =\u003E !b.Slots.SequenceEqual( bindings[0].Slots ) ) ) { identified = false; continue; }\n\t\t\t\tfor ( var index = 0; index \u003C bindings.Length; index\u002B\u002B )\n\t\t\t\t\tforeach ( var id in ids( group.Key, index ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( !table.TryAdd( id, bindings[0] ) ) collisions.Add( id );\n\t\t\t\t\t\tif ( bindings.Length \u003E 1 ) model.DuplicateIds.Add( id );\n\t\t\t\t\t}\n\t\t\t}\n\t\t\treturn identified;\n\t\t}\n\t\t// Unity creates a SkinnedMeshRenderer instead of a MeshRenderer for skinned or blend-shape meshes.\n\t\tvar renderersIdentified = Register( model.RendererSlots, meshes, (path, index) =\u003E new[] { UnityFileId.Renderer( path, index ), UnityFileId.Renderer( path, index, \u0022SkinnedMeshRenderer\u0022 ) } );\n\t\tRegister( model.ReconstructedMeshes, meshes.Select( m =\u003E (m.Binding.Mesh, m.Binding) ), (name, index) =\u003E new[] { UnityFileId.Mesh( name, index ) } );\n\t\tforeach ( var id in collisions ) { model.RendererSlots.Remove( id ); model.ReconstructedMeshes.Remove( id ); }\n\t\tmodel.RendererIdsComplete = complete \u0026\u0026 renderersIdentified \u0026\u0026 collisions.Count == 0 \u0026\u0026 meshes.Count \u003E 0;\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityPrefabBindings.cs","FileName":"UnityPrefabBindings.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"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 readonly List\u003CUnityModel.PrefabSlotEvidence\u003E Evidence = new();\n\t\t// Resolved slot materials per placed instance (a PrefabInstance document or an explicit renderer block).\n\t\tinternal readonly Dictionary\u003Cstring, Dictionary\u003Cstring, string\u003E\u003E Units = new();\n\t\t// Slots one placement assigns two different materials: malformed data that cannot be a variant.\n\t\tinternal readonly HashSet\u003Cstring\u003E Contradicted = new( StringComparer.OrdinalIgnoreCase );\n\t\tinternal readonly HashSet\u003Cstring\u003E Embedded = new( StringComparer.OrdinalIgnoreCase );\n\t\t// Slots where different renderers of one placement need different materials.\n\t\tinternal readonly HashSet\u003Cstring\u003E Shared = new( StringComparer.OrdinalIgnoreCase );\n\t\tinternal readonly Dictionary\u003C(string Unit, string Slot), long\u003E Renderers = new();\n\t\tinternal bool HasOverrides;\n\t}\n\tconst string Unidentified = \u0022unidentified renderer\u0022;\n\tstatic UnityReference Ref( YamlNode body, string key ) =\u003E body?[key]?.Reference ?? default;\n\tstatic readonly Regex MaterialSlot = new( @\u0022^m_Materials\\.Array\\.data\\[([0-9]\u002B)\\]$\u0022 );\n\n\t// Placement files read for evidence: the same limit as the conversion\u0027s (larger files are not converted either).\n\tconst long MaxPlacementBytes = 256L * 1024 * 1024;\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 materialPaths = archive.Assets.Where( a =\u003E a.Kind == UnityAssetKind.Material ).ToDictionary( a =\u003E a.Guid, a =\u003E System.IO.Path.GetFileNameWithoutExtension( a.Path ), 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\t// How each model is placed: a prefab instance renders every mesh except renderers it removes; explicit renderers name their meshes.\n\t\tvar instanceRemovals = new Dictionary\u003Cstring, List\u003CHashSet\u003Clong\u003E\u003E\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar placedMeshes = new Dictionary\u003Cstring, HashSet\u003Clong\u003E\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tbool IsModel( UnityReference reference ) =\u003E reference.Guid is { Length: \u003E 0 } guid \u0026\u0026 models.ContainsKey( guid );\n\t\tvoid Placements( IEnumerable\u003CUnityYamlObject\u003E objects )\n\t\t{\n\t\t\tforeach ( var item in objects )\n\t\t\t{\n\t\t\t\tif ( item.ClassId is 33 or 137 or 199 \u0026\u0026 Ref( item.Body, \u0022m_Mesh\u0022 ) is var mesh \u0026\u0026 IsModel( mesh ) )\n\t\t\t\t{\n\t\t\t\t\tvisible.Add( mesh.Guid );\n\t\t\t\t\tif ( !placedMeshes.TryGetValue( mesh.Guid, out var ids ) ) placedMeshes[mesh.Guid] = ids = new();\n\t\t\t\t\tids.Add( mesh.FileId );\n\t\t\t\t}\n\t\t\t\tif ( item.ClassId == 1001 \u0026\u0026 Ref( item.Body, \u0022m_SourcePrefab\u0022 ) is var source \u0026\u0026 IsModel( source ) )\n\t\t\t\t{\n\t\t\t\t\tvisible.Add( source.Guid );\n\t\t\t\t\tif ( !instanceRemovals.TryGetValue( source.Guid, out var removals ) ) instanceRemovals[source.Guid] = removals = new();\n\t\t\t\t\tremovals.Add( (item.Body[\u0022m_Modification\u0022]?[\u0022m_RemovedComponents\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Select( r =\u003E r.Reference )\n\t\t\t\t\t\t.Where( r =\u003E source.Guid.Equals( r.Guid, StringComparison.OrdinalIgnoreCase ) ).Select( r =\u003E r.FileId ).ToHashSet() );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tvar prefabFiles = archive.Assets.Where( a =\u003E a.Path.EndsWith( \u0022.prefab\u0022, StringComparison.OrdinalIgnoreCase ) \u0026\u0026 new FileInfo( a.Source ).Length \u003C= MaxPlacementBytes )\n\t\t\t.ToDictionary( a =\u003E a.Guid, StringComparer.OrdinalIgnoreCase );\n\t\tvar parsedPrefabs = new Dictionary\u003Cstring, IReadOnlyList\u003CUnityYamlObject\u003E\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tIReadOnlyList\u003CUnityYamlObject\u003E PrefabObjects( string guid )\n\t\t{\n\t\t\tif ( !parsedPrefabs.TryGetValue( guid, out var objects ) ) parsedPrefabs[guid] = objects = UnityYaml.Parse( File.ReadAllText( prefabFiles[guid].Source ) );\n\t\t\treturn objects;\n\t\t}\n\t\t// Follows a reference into nested prefabs until it reaches a model\u0027s renderer (or an explicit renderer\u0027s mesh).\n\t\t// A stripped object names its source; an object inside a nested instance has the ID (instanceID XOR sourceID)\n\t\t// with the sign bit cleared (Unity 2018.3\u002B), so both sign variants of the source ID are tried.\n\t\t// Per parsed prefab: objects by file ID, its prefab instances and mesh filters by GameObject (lookups, not scans).\n\t\tvar indexes = new Dictionary\u003Cstring, (Dictionary\u003Clong, UnityYamlObject\u003E ById, List\u003CUnityYamlObject\u003E Instances, Dictionary\u003Clong, UnityYamlObject\u003E Filters)\u003E( StringComparer.OrdinalIgnoreCase );\n\t\t// Resolved references; a miss is only remembered when it was resolved from the top (deeper misses can be depth cut-offs).\n\t\tvar resolvedReferences = new Dictionary\u003C(string, long), (string Model, long Id, bool Mesh)?\u003E();\n\t\t(string Model, long Id, bool Mesh)? Resolve( UnityReference reference, int depth = 0 )\n\t\t{\n\t\t\tif ( IsModel( reference ) ) return (reference.Guid, reference.FileId, false);\n\t\t\tif ( depth \u003E 8 || reference.Guid is not { Length: \u003E 0 } guid || !prefabFiles.ContainsKey( guid ) ) return null;\n\t\t\tif ( resolvedReferences.TryGetValue( (guid, reference.FileId), out var known ) ) return known;\n\t\t\tvar result = ResolveIn( guid, reference, depth );\n\t\t\tif ( result != null || depth == 0 ) resolvedReferences[(guid, reference.FileId)] = result;\n\t\t\treturn result;\n\t\t}\n\t\t(string Model, long Id, bool Mesh)? ResolveIn( string guid, UnityReference reference, int depth )\n\t\t{\n\t\t\tif ( !indexes.TryGetValue( guid, out var index ) )\n\t\t\t{\n\t\t\t\tIReadOnlyList\u003CUnityYamlObject\u003E parsed;\n\t\t\t\ttry { parsed = PrefabObjects( guid ); }\n\t\t\t\tcatch ( InvalidDataException ) { return null; } // Reported where the prefab itself is read.\n\t\t\t\tvar byId = new Dictionary\u003Clong, UnityYamlObject\u003E();\n\t\t\t\tforeach ( var o in parsed ) byId.TryAdd( o.FileId, o );\n\t\t\t\tvar filters = new Dictionary\u003Clong, UnityYamlObject\u003E();\n\t\t\t\tforeach ( var o in parsed.Where( o =\u003E o.ClassId == 33 ) ) filters.TryAdd( Ref( o.Body, \u0022m_GameObject\u0022 ).FileId, o );\n\t\t\t\tindexes[guid] = index = (byId, parsed.Where( o =\u003E o.ClassId == 1001 ).ToList(), filters);\n\t\t\t}\n\t\t\tif ( index.ById.TryGetValue( reference.FileId, out var item ) )\n\t\t\t{\n\t\t\t\tif ( item.Stripped ) return Resolve( Ref( item.Body, \u0022m_CorrespondingSourceObject\u0022 ), depth \u002B 1 );\n\t\t\t\tvar mesh = item.ClassId is 137 or 199 ? Ref( item.Body, \u0022m_Mesh\u0022 ) : item.ClassId == 23\n\t\t\t\t\t? Ref( index.Filters.GetValueOrDefault( Ref( item.Body, \u0022m_GameObject\u0022 ).FileId )?.Body, \u0022m_Mesh\u0022 ) : default;\n\t\t\t\treturn IsModel( mesh ) ? (mesh.Guid, mesh.FileId, true) : null;\n\t\t\t}\n\t\t\tforeach ( var instance in index.Instances )\n\t\t\t{\n\t\t\t\tvar source = Ref( instance.Body, \u0022m_SourcePrefab\u0022 );\n\t\t\t\tvar masked = (reference.FileId ^ instance.FileId) \u0026 long.MaxValue;\n\t\t\t\tforeach ( var id in new[] { masked, masked | long.MinValue } )\n\t\t\t\t{\n\t\t\t\t\tvar inner = new UnityReference( id, source.Guid );\n\t\t\t\t\tvar found = IsModel( source ) ? models[source.Guid].ModelInfo.RendererSlots.ContainsKey( id ) ? Resolve( inner, depth \u002B 1 ) : null : Resolve( inner, depth \u002B 1 );\n\t\t\t\t\tif ( found != null ) return found;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t\t// Scenes can also place models; they count for visibility (so a model shown only in scenes is not collision-only),\n\t\t// not for material assignments.\n\t\t// A file that cannot be read is left out of the evidence and listed; the package still loads.\n\t\tforeach ( var scene in archive.Assets.Where( a =\u003E a.Path.EndsWith( \u0022.unity\u0022, StringComparison.OrdinalIgnoreCase ) \u0026\u0026 new FileInfo( a.Source ).Length \u003C= MaxPlacementBytes ) )\n\t\t{\n\t\t\ttry { Placements( UnityYaml.Parse( File.ReadAllText( scene.Source ) ) ); }\n\t\t\tcatch ( InvalidDataException ex ) { archive.Skipped.Add( $\u0022{scene.Path}: not read for placement evidence ({ex.Message})\u0022 ); }\n\t\t}\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 MaxPlacementBytes ) 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, long id, string idSource, int index, UnityReference reference, string unit )\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\tvar material = reference.Guid ?? \u0022\u0022;\n\t\t\t\t// A reference into the model itself is one of its embedded materials, which Unity generates from the FBX.\n\t\t\t\tvar embedded = material.Equals( guid, StringComparison.OrdinalIgnoreCase );\n\t\t\t\tif ( embedded ) material = \u0022embedded material \u0022 \u002B (model.EmbeddedMaterialName( reference.FileId ) ?? reference.FileId.ToString());\n\t\t\t\tvoid Evidence( string slot, string problem ) =\u003E\n\t\t\t\t\tcandidate.Evidence.Add( new( prefab.Path, slot, id, binding?.Mesh, index, material, binding == null ? \u0022unresolved\u0022 : idSource, problem ) );\n\t\t\t\tif ( binding == null ) { Evidence( null, Unidentified ); return; } // Classified once the whole prefab is read.\n\t\t\t\t// Lower LOD meshes are LOD levels of the VMDL, whose material remaps are per slot name: their renderers assign\n\t\t\t\t// only the slots the always-shown meshes (LOD0 and meshes without a LOD suffix) do not have, such as an impostor\n\t\t\t\t// level\u0027s own material. A lower LOD mesh outside the levels (LOD2 without LOD1) is not imported.\n\t\t\t\tif ( model.IsLowerLod( binding.Mesh ) )\n\t\t\t\t{\n\t\t\t\t\tvar lowerLevel = model.LodLevels.Any( l =\u003E l.Contains( binding.Mesh ) );\n\t\t\t\t\tvar slotName = index \u003E= 0 \u0026\u0026 index \u003C binding.Slots.Length ? binding.Slots[index] : binding.Slots.Length == 0 ? binding.Mesh : null;\n\t\t\t\t\tvar lodZeroSlots = model.RendererSlots.Values.Where( b =\u003E !model.IsLowerLod( b.Mesh ) ).SelectMany( b =\u003E b.Slots.Length == 0 ? new[] { b.Mesh } : b.Slots );\n\t\t\t\t\tif ( !lowerLevel ) { Evidence( null, \u0022lower LOD not imported\u0022 ); return; }\n\t\t\t\t\tif ( slotName == null || lodZeroSlots.Contains( slotName, StringComparer.OrdinalIgnoreCase ) ) { Evidence( null, \u0022lower LOD slot shared with LOD0\u0022 ); return; }\n\t\t\t\t}\n\t\t\t\tif ( index \u003C 0 || index \u003E= Math.Max( 1, binding.Slots.Length ) )\n\t\t\t\t{\n\t\t\t\t\tEvidence( null, \u0022missing slot\u0022 );\n\t\t\t\t\tcandidate.Warnings.Add( \u0022A prefab material override references a missing FBX material slot.\u0022 );\n\t\t\t\t\t// Extra Unity material passes cannot be represented by this mesh\u0027s implicit slot.\n\t\t\t\t\tif ( binding.Slots.Length == 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( !candidate.Slots.TryGetValue( binding.Mesh, out var invalid ) ) candidate.Slots[binding.Mesh] = invalid = new();\n\t\t\t\t\t\tinvalid.Add( \u0022\u0022 );\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvar slot = binding.Slots.Length == 0 ? binding.Mesh : binding.Slots[index];\n\t\t\t\tEvidence( slot, null );\n\t\t\t\tif ( embedded ) { candidate.Embedded.Add( slot ); return; } // Not a material asset, so no conflict or variant.\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 ) ) return;\n\t\t\t\tcandidate.Materials.Add( material );\n\t\t\t\tif ( !candidate.Units.TryGetValue( unit, out var assignments ) ) candidate.Units[unit] = assignments = new( StringComparer.OrdinalIgnoreCase );\n\t\t\t\tif ( assignments.TryGetValue( slot, out var previous ) \u0026\u0026 !previous.Equals( material, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\t{\n\t\t\t\t\t// The same renderer given two materials is malformed. Different renderers sharing a slot name are real,\n\t\t\t\t\t// but one ModelDoc slot cannot separate them.\n\t\t\t\t\tif ( candidate.Renderers.GetValueOrDefault( (unit, slot) ) == id ) candidate.Contradicted.Add( slot );\n\t\t\t\t\telse candidate.Shared.Add( slot );\n\t\t\t\t}\n\t\t\t\tassignments[slot] = material;\n\t\t\t\tcandidate.Renderers[(unit, slot)] = id;\n\t\t\t}\n\t\t\tIReadOnlyList\u003CUnityYamlObject\u003E objects;\n\t\t\ttry { objects = prefabFiles.ContainsKey( prefab.Guid ) ? PrefabObjects( prefab.Guid ) : UnityYaml.Parse( text ); }\n\t\t\tcatch ( InvalidDataException ex ) { archive.Skipped.Add( $\u0022{prefab.Path}: not read for material evidence ({ex.Message})\u0022 ); continue; }\n\t\t\tvar filters = new Dictionary\u003Clong, List\u003CUnityReference\u003E\u003E();\n\t\t\tforeach ( var item in objects )\n\t\t\t{\n\t\t\t\tvar mesh = Ref( item.Body, \u0022m_Mesh\u0022 );\n\t\t\t\tif ( item.ClassId == 64 \u0026\u0026 IsModel( mesh ) ) colliders.Add( mesh.Guid );\n\t\t\t\tif ( item.ClassId is 33 or 137 or 199 \u0026\u0026 IsModel( mesh ) )\n\t\t\t\t{\n\t\t\t\t\tvisible.Add( mesh.Guid );\n\t\t\t\t\tif ( item.ClassId == 33 \u0026\u0026 Ref( item.Body, \u0022m_GameObject\u0022 ).FileId is var gameObject \u0026\u0026 gameObject != 0 )\n\t\t\t\t\t{\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 ( item.ClassId == 1001 \u0026\u0026 Ref( item.Body, \u0022m_SourcePrefab\u0022 ) is var source \u0026\u0026 IsModel( source ) ) visible.Add( source.Guid );\n\t\t\t}\n\t\t\tPlacements( objects );\n\t\t\t// Material overrides on placed models: PrefabInstance (and pre-2018.3 Prefab) modification lists.\n\t\t\tforeach ( var instance in objects.Where( o =\u003E o.ClassId == 1001 ) )\n\t\t\t{\n\t\t\t\tforeach ( var modification in instance.Body[\u0022m_Modification\u0022]?[\u0022m_Modifications\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E() )\n\t\t\t\t{\n\t\t\t\t\tvar target = Ref( modification, \u0022target\u0022 );\n\t\t\t\t\tvar property = MaterialSlot.Match( modification[\u0022propertyPath\u0022]?.Value ?? \u0022\u0022 );\n\t\t\t\t\t// A target inside a nested prefab (a prefab variant or a prefab placed in a prefab) is followed to its model.\n\t\t\t\t\tif ( !property.Success || Resolve( target ) is not var (model, id, isMesh) ) continue;\n\t\t\t\t\tvar asset = models[model];\n\t\t\t\t\tvisible.Add( asset.Guid );\n\t\t\t\t\tif ( !int.TryParse( property.Groups[1].Value, out var index ) ) continue;\n\t\t\t\t\tvar nested = !IsModel( target );\n\t\t\t\t\tvar idSource = isMesh ? asset.ModelInfo.MeshIdSource( id ) : asset.ModelInfo.RendererIdSource( id );\n\t\t\t\t\tAssign( asset.Guid, isMesh ? asset.ModelInfo.ResolveMesh( id ) : asset.ModelInfo.RendererSlots.GetValueOrDefault( id ), id, nested ? $\u0022nested prefab ({idSource})\u0022 : idSource, index,\n\t\t\t\t\t\tRef( modification, \u0022objectReference\u0022 ), \u0022instance \u0022 \u002B instance.FileId );\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Explicit MeshRenderer \u002B MeshFilter blocks, and renderers naming their own mesh (SkinnedMeshRenderer 137, ParticleSystemRenderer 199).\n\t\t\tforeach ( var renderer in objects.Where( o =\u003E o.ClassId is 23 or 137 or 199 ) )\n\t\t\t{\n\t\t\t\tvar refs = renderer.ClassId is 137 or 199 ? new[] { Ref( renderer.Body, \u0022m_Mesh\u0022 ) } :\n\t\t\t\t\tfilters.GetValueOrDefault( Ref( renderer.Body, \u0022m_GameObject\u0022 ).FileId )?.ToArray() ?? Array.Empty\u003CUnityReference\u003E();\n\t\t\t\tif ( refs.Length != 1 || !IsModel( refs[0] ) ) continue;\n\t\t\t\tvar asset = models[refs[0].Guid];\n\t\t\t\tvar meshId = refs[0].FileId;\n\t\t\t\tvar binding = asset.ModelInfo.ResolveMesh( meshId );\n\t\t\t\tvar index = 0;\n\t\t\t\tforeach ( var slot in renderer.Body[\u0022m_Materials\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E() )\n\t\t\t\t\tAssign( asset.Guid, binding, meshId, asset.ModelInfo.MeshIdSource( meshId ), index\u002B\u002B, slot.Reference, \u0022renderer \u0022 \u002B renderer.FileId );\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\t// Unity keeps modifications whose target was later removed from the model, and ignores them. An unknown ID is\n\t\t\t\t// treated as such a stale override only when it cannot be a renderer we failed to identify: every mesh node has\n\t\t\t\t// reconstructed IDs, metadata does not list the ID, and this prefab\u0027s other references to the model resolved.\n\t\t\t\tvar model = models[guid].ModelInfo;\n\t\t\t\tvar resolved = candidate.Evidence.Any( e =\u003E e.Problem != Unidentified );\n\t\t\t\tfor ( var i = 0; i \u003C candidate.Evidence.Count; i\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar evidence = candidate.Evidence[i];\n\t\t\t\t\tif ( evidence.Problem != Unidentified ) continue;\n\t\t\t\t\tif ( resolved \u0026\u0026 model.RendererIdsComplete \u0026\u0026 !model.MetadataObjectIds.Contains( evidence.Id ) ) candidate.Evidence[i] = evidence with { Problem = \u0022stale override (no such object in model)\u0022 };\n\t\t\t\t\telse candidate.Warnings.Add( \u0022A prefab material override targets an unsupported or unidentified renderer; no slot was guessed.\u0022 );\n\t\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\tmodel.PrefabVariants.AddRange( Variants( list ) );\n\t\t\tmodel.EmbeddedSlots.UnionWith( chosen.SelectMany( c =\u003E c.Embedded ) );\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 if ( model.ImporterMaterials.ContainsKey( slot ) )\n\t\t\t\t\tmodel.AssignmentNotes.Add( $\u0022Prefabs assign different materials to slot \u0027{slot}\u0027; the model\u0027s own import material is the default and the prefab variants are material groups.\u0022 );\n\t\t\t\telse if ( values.Length \u003E 1 \u0026\u0026 values.All( materialIds.Contains ) \u0026\u0026 !chosen.Any( c =\u003E c.Contradicted.Contains( slot ) ) )\n\t\t\t\t{\n\t\t\t\t\t// Every value is a real Unity assignment: the most placed one becomes the default, the rest stay as groups.\n\t\t\t\t\tvar counts = chosen.SelectMany( c =\u003E c.Units.Values ).Select( u =\u003E u.GetValueOrDefault( slot ) ).Where( m =\u003E m != null ).GroupBy( m =\u003E m, StringComparer.OrdinalIgnoreCase );\n\t\t\t\t\tmodel.PrefabSlotMaterials[slot] = counts.OrderByDescending( g =\u003E g.Count() ).ThenBy( g =\u003E g.Key, StringComparer.OrdinalIgnoreCase ).First().Key;\n\t\t\t\t\tmodel.MostCommonSlots.Add( slot );\n\t\t\t\t\tmodel.AssignmentNotes.Add( chosen.Any( c =\u003E c.Shared.Contains( slot ) )\n\t\t\t\t\t\t? $\u0022Renderers sharing slot \u0027{slot}\u0027 need different materials ({string.Join( \u0022, \u0022, values.Select( v =\u003E materialPaths.GetValueOrDefault( v, v ) ) )}); one ModelDoc slot cannot separate them, so the most common is used.\u0022\n\t\t\t\t\t\t: $\u0022Prefabs assign different materials to slot \u0027{slot}\u0027; the most common is the default and the others are material groups.\u0022 );\n\t\t\t\t}\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\t// Prefabs not chosen (another prefab shares the model\u0027s filename) are kept as unapplied evidence.\n\t\t\tmodel.PrefabEvidence.AddRange( list.SelectMany( c =\u003E c.Evidence.Select( e =\u003E e with { Applied = chosen.Contains( c ) } ) ) );\n\t\t}\n\t\tforeach ( var guid in colliders.Where( g =\u003E !visible.Contains( g ) ) ) models[guid].ModelInfo.CollisionOnly = true;\n\t\t// Meshes no placement shows (such as collision hulls exported inside the visual model) are left out of the VMDL.\n\t\tforeach ( var guid in placedMeshes.Keys.Union( instanceRemovals.Keys, StringComparer.OrdinalIgnoreCase ) )\n\t\t{\n\t\t\tvar model = models[guid].ModelInfo;\n\t\t\tvar shown = new HashSet\u003Cstring\u003E( StringComparer.Ordinal );\n\t\t\tif ( placedMeshes.TryGetValue( guid, out var ids ) )\n\t\t\t{\n\t\t\t\tvar bindings = ids.Select( model.ResolveMesh ).ToArray();\n\t\t\t\tif ( bindings.Any( b =\u003E b == null ) ) continue; // An unidentified placement could be any mesh.\n\t\t\t\tshown.UnionWith( bindings.Select( b =\u003E b.Mesh ) );\n\t\t\t}\n\t\t\tforeach ( var removed in instanceRemovals.GetValueOrDefault( guid ) ?? new() )\n\t\t\t{\n\t\t\t\t// Removing one of several same-named renderers would hide its twins too, so such removals are not used.\n\t\t\t\tvar gone = removed.Overlaps( model.DuplicateIds ) ? new HashSet\u003Cstring\u003E() :\n\t\t\t\t\tremoved.Select( id =\u003E model.RendererSlots.GetValueOrDefault( id )?.Mesh ).Where( m =\u003E m != null ).ToHashSet( StringComparer.Ordinal );\n\t\t\t\tshown.UnionWith( model.Meshes.Where( m =\u003E !gone.Contains( m ) ) );\n\t\t\t}\n\t\t\tmodel.HiddenMeshes.UnionWith( model.Meshes.Where( m =\u003E !shown.Contains( m ) ) );\n\t\t\tif ( model.ImportedMeshes.Length == 0 ) model.HiddenMeshes.Clear(); // Never hide everything the VMDL would import.\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EDistinct slot-material sets across every prefab placement of a model, so no Unity variant is lost.\u003C/summary\u003E\n\tstatic IEnumerable\u003CUnityModel.PrefabVariant\u003E Variants( List\u003CCandidate\u003E list )\n\t{\n\t\tstatic bool Consistent( Dictionary\u003Cstring, string\u003E a, Dictionary\u003Cstring, string\u003E b ) =\u003E\n\t\t\ta.All( p =\u003E !b.TryGetValue( p.Key, out var other ) || other.Equals( p.Value, StringComparison.OrdinalIgnoreCase ) );\n\t\tvar variants = new List\u003C(SortedSet\u003Cstring\u003E Prefabs, Dictionary\u003Cstring, string\u003E Slots)\u003E();\n\t\tforeach ( var candidate in list )\n\t\t{\n\t\t\t// Within one prefab, placements that agree are one variant; disagreeing placements are separate variants.\n\t\t\tvar local = new List\u003CDictionary\u003Cstring, string\u003E\u003E();\n\t\t\tforeach ( var unit in candidate.Units.Values.Select( u =\u003E u.Where( p =\u003E !candidate.Contradicted.Contains( p.Key ) \u0026\u0026 !candidate.Shared.Contains( p.Key ) ).ToDictionary( p =\u003E p.Key, p =\u003E p.Value, StringComparer.OrdinalIgnoreCase ) ) )\n\t\t\t{\n\t\t\t\tif ( unit.Count == 0 ) continue;\n\t\t\t\tvar target = local.FirstOrDefault( v =\u003E Consistent( unit, v ) );\n\t\t\t\tif ( target == null ) local.Add( target = new( StringComparer.OrdinalIgnoreCase ) );\n\t\t\t\tforeach ( var (slot, material) in unit ) target[slot] = material;\n\t\t\t}\n\t\t\tforeach ( var slots in local ) variants.Add( (new SortedSet\u003Cstring\u003E( new[] { candidate.Path }, StringComparer.OrdinalIgnoreCase ), slots) );\n\t\t}\n\t\t// Across prefabs, a set contained in another (same materials for all its slots) adds nothing; fold it in.\n\t\tvar distinct = new List\u003C(SortedSet\u003Cstring\u003E Prefabs, Dictionary\u003Cstring, string\u003E Slots)\u003E();\n\t\tforeach ( var variant in variants.OrderByDescending( v =\u003E v.Slots.Count ) )\n\t\t{\n\t\t\tvar container = distinct.FirstOrDefault( d =\u003E variant.Slots.All( p =\u003E d.Slots.TryGetValue( p.Key, out var m ) \u0026\u0026 m.Equals( p.Value, StringComparison.OrdinalIgnoreCase ) ) );\n\t\t\tif ( container.Slots != null ) container.Prefabs.UnionWith( variant.Prefabs );\n\t\t\telse distinct.Add( variant );\n\t\t}\n\t\treturn distinct.OrderBy( d =\u003E d.Prefabs.Min, StringComparer.OrdinalIgnoreCase ).Select( d =\u003E new UnityModel.PrefabVariant( d.Prefabs.ToArray(), d.Slots ) );\n\t}\n}\n"},{"Ident":"grubs.importunitypackage","Path":"Editor/Core/UnityPrefabConverter.cs","FileName":"UnityPrefabConverter.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":410869,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing System.Text.RegularExpressions;\n\nnamespace ImportUnityPackage;\n\n/// \u003Csummary\u003EAn imported model as prefabs and scenes reference it.\u003C/summary\u003E\ninternal sealed class PrefabModel\n{\n\tpublic string Vmdl { get; init; }\n\tpublic UnityModel Info { get; init; }\n\tpublic double Scale { get; init; }\n\tpublic string Name { get; init; }\n\t/// \u003Csummary\u003EThe mesh file the VMDLs read (the source, or a repaired or ASCII-named copy).\u003C/summary\u003E\n\tpublic string MeshFile { get; init; }\n\t/// \u003Csummary\u003EA VMDL holding only one mesh node, for renderers that show one mesh of a multi-mesh model.\u003C/summary\u003E\n\tpublic Func\u003Cstring, string\u003E MeshVmdl { get; init; }\n\t/// \u003Csummary\u003EThe material group matching a slot \u2192 material GUID assignment (\u0022\u0022 for the default group), and whether it matched exactly.\u003C/summary\u003E\n\tpublic Func\u003CIReadOnlyDictionary\u003Cstring, string\u003E, (string Group, bool Exact)\u003E Group { get; init; }\n\t/// \u003Csummary\u003EA physics-only VMDL for a mesh node (null for the whole model), as a convex hull or the exact mesh.\u003C/summary\u003E\n\tpublic Func\u003Cstring, bool, string\u003E CollisionVmdl { get; init; }\n}\n\n/// \u003Csummary\u003E\n/// A VMDL to compose from mesh files: LOD levels of render meshes (empty for a physics-only model), switch thresholds\n/// between levels, material remaps (slot.vmat to material) and an optional physics shape. Name is the output path\n/// without extension, relative to the import destination.\n/// \u003C/summary\u003E\ninternal sealed record ComposedModel( string Name, IReadOnlyList\u003CIReadOnlyList\u003CMeshPart\u003E\u003E Lods, IReadOnlyList\u003Cdouble\u003E Thresholds, IReadOnlyDictionary\u003Cstring, string\u003E Remaps, MeshPart Physics = null, bool Convex = false );\n\n/// \u003Csummary\u003EA converted skybox: its sky material, the SkyBox2D tint (null for none) and the sky object\u0027s yaw in degrees.\u003C/summary\u003E\ninternal sealed record ConvertedSky( string Material, double[] Tint, double Yaw );\n\n/// \u003Csummary\u003EAn OBJ written from a Unity mesh: its path relative to the destination, its resource path and submesh count, or why it could not be.\u003C/summary\u003E\ninternal sealed record GeneratedMesh( string Relative, string Reference, int Submeshes, string Error );\n\n/// \u003Csummary\u003EAn imported terrain as scenes and prefabs place it.\u003C/summary\u003E\ninternal sealed class PrefabTerrain\n{\n\t/// \u003Csummary\u003EThe .terrain resource.\u003C/summary\u003E\n\tpublic string Path { get; init; }\n\t/// \u003Csummary\u003ESide length and height range in s\u0026amp;box units.\u003C/summary\u003E\n\tpublic double Size { get; init; }\n\tpublic double Height { get; init; }\n\t/// \u003Csummary\u003EThe Unity terrain\u0027s size and trees (its maps are released).\u003C/summary\u003E\n\tpublic UnityTerrainData Data { get; init; }\n}\n\n/// \u003Csummary\u003E\n/// Converts Unity prefabs and scenes into s\u0026amp;box prefab/scene JSON: GameObject hierarchy and transforms, mesh renderers as\n/// ModelRenderers (with the material group matching their materials), colliders, terrains, lights, cameras, reflection\n/// probes and audio sources, nested prefabs as prefab references and model instances as whole-model renderers. Scripts\n/// and effects are not converted.\n/// \u003C/summary\u003E\ninternal sealed partial class UnityPrefabConverter\n{\n\treadonly Dictionary\u003Cstring, UnityAsset\u003E assets;\n\treadonly Dictionary\u003Cstring, PrefabModel\u003E models;\n\treadonly Dictionary\u003Cstring, string\u003E prefabOutputs;\n\treadonly Func\u003Cstring, IReadOnlyList\u003CUnityYamlObject\u003E\u003E parse;\n\tpublic UnityPrefabConverter( Dictionary\u003Cstring, UnityAsset\u003E assets, Dictionary\u003Cstring, PrefabModel\u003E models, Dictionary\u003Cstring, string\u003E prefabOutputs, Func\u003Cstring, IReadOnlyList\u003CUnityYamlObject\u003E\u003E parse )\n\t{\n\t\tthis.assets = assets; this.models = models; this.prefabOutputs = prefabOutputs; this.parse = parse;\n\t}\n\n\t/// \u003Csummary\u003EConverted materials by material GUID (VMAT resource paths).\u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, string\u003E Materials { get; init; } = new( StringComparer.OrdinalIgnoreCase );\n\t/// \u003Csummary\u003EWrites a Unity mesh (by owning file GUID and file ID, or a built-in mesh) as an OBJ.\u003C/summary\u003E\n\tpublic Func\u003Cstring, long, GeneratedMesh\u003E MeshObj { get; init; }\n\t/// \u003Csummary\u003EWrites a composed VMDL and returns its resource path.\u003C/summary\u003E\n\tpublic Func\u003CComposedModel, string\u003E Compose { get; init; }\n\t/// \u003Csummary\u003EPuts a collider\u0027s physics shape (exact, or one convex hull) in a VMDL written by this import; false when it holds a different shape.\u003C/summary\u003E\n\tpublic Func\u003Cstring, MeshPart, bool, bool\u003E EmbedPhysics { get; init; }\n\n\t/// \u003Csummary\u003EConverted skybox materials by material GUID.\u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, ConvertedSky\u003E Skies { get; init; } = new( StringComparer.OrdinalIgnoreCase );\n\n\t/// \u003Csummary\u003EConverted terrains by TerrainData GUID.\u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, PrefabTerrain\u003E Terrains { get; init; } = new( StringComparer.OrdinalIgnoreCase );\n\n\t/// \u003Csummary\u003EWhat one conversion produced, for the import report.\u003C/summary\u003E\n\tpublic sealed class Summary\n\t{\n\t\tpublic string Source { get; init; }\n\t\tpublic int Objects { get; set; }\n\t\tpublic int Renderers { get; set; }\n\t\tpublic int ModelInstances { get; set; }\n\t\tpublic int PrefabInstances { get; set; }\n\t\tpublic int InlinedInstances { get; set; }\n\t\tpublic int Colliders { get; set; }\n\t\tpublic int Terrains { get; set; }\n\t\tpublic int GeneratedMeshes { get; set; }\n\t\tpublic int LodGroups { get; set; }\n\t\tpublic int Lights { get; set; }\n\t\tpublic int Cameras { get; set; }\n\t\tpublic int Probes { get; set; }\n\t\tpublic int Sounds { get; set; }\n\t\tpublic int Skies { get; set; }\n\t\tpublic int Trees { get; set; }\n\t\tpublic SortedDictionary\u003Cstring, int\u003E Skipped { get; } = new( StringComparer.Ordinal );\n\t\tpublic List\u003Cstring\u003E Notes { get; } = new();\n\t\tinternal void Skip( string type ) =\u003E Skipped[type] = Skipped.GetValueOrDefault( type ) \u002B 1;\n\t}\n\n\t/// \u003Csummary\u003EGUIDs of the meshes, prefabs, terrains and audio clips a prefab or scene places.\u003C/summary\u003E\n\tinternal static IEnumerable\u003Cstring\u003E PlacedAssets( string text ) =\u003E\n\t\tRegex.Matches( text, @\u0022(?:m_Mesh|m_SourcePrefab|m_TerrainData|m_audioClip):\\s*\\{[^}\\r\\n]*guid:\\s*([0-9a-fA-F]{32})\u0022 ).Select( m =\u003E m.Groups[1].Value.ToLowerInvariant() ).Distinct();\n\n\t/// \u003Csummary\u003E\n\t/// Materials of renderers whose meshes the importer writes itself (built-in primitives, meshes stored in prefabs,\n\t/// scenes or .asset files), so a plan imports them; model meshes bring their materials through the model.\n\t/// \u003C/summary\u003E\n\tinternal static IEnumerable\u003Cstring\u003E GeneratedMeshMaterials( IReadOnlyList\u003CUnityYamlObject\u003E objects, Func\u003Cstring, bool\u003E isModel )\n\t{\n\t\tvar filters = objects.Where( o =\u003E o.ClassId == 33 \u0026\u0026 !o.Stripped ).GroupBy( o =\u003E Ref( o.Body, \u0022m_GameObject\u0022 ).FileId ).ToDictionary( g =\u003E g.Key, g =\u003E g.First() );\n\t\tforeach ( var renderer in objects.Where( o =\u003E o.ClassId is 23 or 137 \u0026\u0026 !o.Stripped ) )\n\t\t{\n\t\t\tvar mesh = renderer.ClassId == 137 ? Ref( renderer.Body, \u0022m_Mesh\u0022 ) : filters.TryGetValue( Ref( renderer.Body, \u0022m_GameObject\u0022 ).FileId, out var filter ) ? Ref( filter.Body, \u0022m_Mesh\u0022 ) : default;\n\t\t\tif ( mesh.FileId == 0 || mesh.HasGuid \u0026\u0026 !mesh.Guid.Equals( UnityMesh.BuiltinGuid, StringComparison.OrdinalIgnoreCase ) \u0026\u0026 isModel( mesh.Guid ) ) continue;\n\t\t\tforeach ( var material in renderer.Body[\u0022m_Materials\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E() )\n\t\t\t\tif ( material.Reference.HasGuid ) yield return material.Reference.Guid;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EDeterministic GUIDs, so an unchanged source converts to identical files and references stay stable.\u003C/summary\u003E\n\tinternal static string Id( string guid, long fileId, string part )\n\t{\n\t\tvar hash = MD5.HashData( Encoding.UTF8.GetBytes( $\u0022{guid}:{fileId}:{part}\u0022 ) );\n\t\thash[6] = (byte)((hash[6] \u0026 0x0F) | 0x30); hash[8] = (byte)((hash[8] \u0026 0x3F) | 0x80);\n\t\treturn new Guid( hash ).ToString();\n\t}\n\n\tstatic UnityReference Ref( YamlNode body, string key ) =\u003E body?[key]?.Reference ?? default;\n\tstatic string F( double v ) =\u003E (Math.Abs( v ) \u003C 1e-9 ? 0 : v).ToString( \u00220.######\u0022, CultureInfo.InvariantCulture );\n\tstatic string Vec( double[] v ) =\u003E string.Join( \u0022,\u0022, v.Select( F ) );\n\n\t/// \u003Csummary\u003EOne parsed file: objects by ID, GameObjects\u0027 components and transforms.\u003C/summary\u003E\n\tsealed class Document\n\t{\n\t\tpublic string Guid;\n\t\tpublic Dictionary\u003Clong, UnityYamlObject\u003E Objects = new();\n\t\tpublic Dictionary\u003Clong, List\u003CUnityYamlObject\u003E\u003E Components = new();\n\t\tpublic Dictionary\u003Clong, UnityYamlObject\u003E TransformOf = new();\n\t\tpublic HashSet\u003Clong\u003E HiddenRenderers = new();\n\t\t/// \u003Csummary\u003ELOD groups by their LOD0 renderer (when LOD0 has one renderer): each level\u0027s screen-relative height and renderers.\u003C/summary\u003E\n\t\tpublic Dictionary\u003Clong, List\u003C(double Height, List\u003Clong\u003E Renderers)\u003E\u003E LodGroups = new();\n\t\t/// \u003Csummary\u003EEach LOD group\u0027s size (Unity\u0027s local m_Size, its largest bounds axis) in inches, by its LOD0 renderer.\u003C/summary\u003E\n\t\tpublic Dictionary\u003Clong, double\u003E LodSizes = new();\n\t\t/// \u003Csummary\u003EThe HDRP exposure reference of this file, once computed.\u003C/summary\u003E\n\t\tpublic double? HdrpReference;\n\t\t/// \u003Csummary\u003EVMDLs of generated meshes by GameObject, mesh GUID and file ID, for colliders on the same object.\u003C/summary\u003E\n\t\tpublic Dictionary\u003C(long, string, long), string\u003E GeneratedRenders = new();\n\t\t// Built once per file, so per-object lookups do not scan the whole document (scenes hold tens of thousands of objects).\n\t\t/// \u003Csummary\u003ERoot transforms (no father), in file order.\u003C/summary\u003E\n\t\tpublic List\u003CUnityYamlObject\u003E RootTransforms = new();\n\t\t/// \u003Csummary\u003EPrefab instances by the transform they are placed under (0 for the file\u0027s roots), in file order.\u003C/summary\u003E\n\t\tpublic Dictionary\u003Clong, List\u003CUnityYamlObject\u003E\u003E InstancesByParent = new();\n\t\t/// \u003Csummary\u003EPer prefab instance: the source objects of the stripped transforms this file keeps for it.\u003C/summary\u003E\n\t\tpublic Dictionary\u003Clong, List\u003Clong\u003E\u003E StrippedTransformSources = new();\n\t\t/// \u003Csummary\u003EPer prefab instance: transforms this file adds under that instance\u0027s objects.\u003C/summary\u003E\n\t\tpublic Dictionary\u003Clong, List\u003CUnityYamlObject\u003E\u003E AddedUnderInstance = new();\n\t\t/// \u003Csummary\u003EPer prefab instance: the stripped GameObjects this file keeps for it (components added to its objects hang off them).\u003C/summary\u003E\n\t\tpublic Dictionary\u003Clong, List\u003Clong\u003E\u003E StrippedGameObjects = new();\n\t\tpublic IEnumerable\u003CUnityYamlObject\u003E InstancesUnder( long parent ) =\u003E InstancesByParent.GetValueOrDefault( parent ) ?? Enumerable.Empty\u003CUnityYamlObject\u003E();\n\t\tpublic IEnumerable\u003CUnityYamlObject\u003E AddedUnder( long instance ) =\u003E AddedUnderInstance.GetValueOrDefault( instance ) ?? Enumerable.Empty\u003CUnityYamlObject\u003E();\n\t\tpublic Document( string guid, IReadOnlyList\u003CUnityYamlObject\u003E objects )\n\t\t{\n\t\t\tGuid = guid;\n\t\t\tforeach ( var o in objects ) Objects.TryAdd( o.FileId, o );\n\t\t\tstatic void Add\u003CT\u003E( Dictionary\u003Clong, List\u003CT\u003E\u003E index, long key, T value )\n\t\t\t{\n\t\t\t\tif ( !index.TryGetValue( key, out var list ) ) index[key] = list = new();\n\t\t\t\tlist.Add( value );\n\t\t\t}\n\t\t\tforeach ( var o in Objects.Values )\n\t\t\t{\n\t\t\t\tif ( o.ClassId == 1001 \u0026\u0026 o.Type == \u0022PrefabInstance\u0022 \u0026\u0026 !o.Stripped ) Add( InstancesByParent, Ref( o.Body[\u0022m_Modification\u0022], \u0022m_TransformParent\u0022 ).FileId, o );\n\t\t\t\tif ( o.ClassId == 1 \u0026\u0026 o.Stripped ) Add( StrippedGameObjects, Ref( o.Body, \u0022m_PrefabInstance\u0022 ).FileId, o.FileId );\n\t\t\t\tif ( o.ClassId is not (4 or 224) ) continue;\n\t\t\t\tif ( o.Stripped ) { Add( StrippedTransformSources, Ref( o.Body, \u0022m_PrefabInstance\u0022 ).FileId, Ref( o.Body, \u0022m_CorrespondingSourceObject\u0022 ).FileId ); continue; }\n\t\t\t\tvar father = Ref( o.Body, \u0022m_Father\u0022 ).FileId;\n\t\t\t\tif ( father == 0 ) RootTransforms.Add( o );\n\t\t\t\telse if ( Objects.TryGetValue( father, out var parent ) \u0026\u0026 parent.Stripped ) Add( AddedUnderInstance, Ref( parent.Body, \u0022m_PrefabInstance\u0022 ).FileId, o );\n\t\t\t}\n\t\t\tforeach ( var o in objects.Where( o =\u003E !o.Stripped \u0026\u0026 o.ClassId != 1 \u0026\u0026 o.Body?[\u0022m_GameObject\u0022] != null ) )\n\t\t\t{\n\t\t\t\tvar owner = Ref( o.Body, \u0022m_GameObject\u0022 ).FileId;\n\t\t\t\tif ( !Components.TryGetValue( owner, out var list ) ) Components[owner] = list = new();\n\t\t\t\tlist.Add( o );\n\t\t\t\tif ( o.ClassId is 4 or 224 ) TransformOf[owner] = o;\n\t\t\t}\n\t\t\t// LODGroup: renderers only in lower levels are not converted on their own; a LOD0 renderer whose levels line up\n\t\t\t// gets one VMDL holding every level (LodModel). A renderer also in LOD0 stays.\n\t\t\tforeach ( var group in objects.Where( o =\u003E o.ClassId == 205 \u0026\u0026 !o.Stripped ) )\n\t\t\t{\n\t\t\t\tvar lods = group.Body[\u0022m_LODs\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E();\n\t\t\t\tvar levels = lods.Select( l =\u003E (Height: l[\u0022screenRelativeHeight\u0022]?.Number ?? 0, Renderers: (l[\u0022renderers\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Select( r =\u003E Ref( r, \u0022renderer\u0022 ).FileId ).Where( id =\u003E id != 0 ).ToList()) ).ToList();\n\t\t\t\t// A trailing level without renderers is Unity\u0027s culled range, not a level to show.\n\t\t\t\twhile ( levels.Count \u003E 1 \u0026\u0026 levels[^1].Renderers.Count == 0 ) levels.RemoveAt( levels.Count - 1 );\n\t\t\t\tfor ( var i = 1; i \u003C levels.Count; i\u002B\u002B )\n\t\t\t\t\tforeach ( var renderer in levels[i].Renderers )\n\t\t\t\t\t\tif ( levels.Count == 0 || !levels[0].Renderers.Contains( renderer ) ) HiddenRenderers.Add( renderer );\n\t\t\t\tif ( levels.Count \u003E 1 \u0026\u0026 levels[0].Renderers.Count == 1 \u0026\u0026 LodGroups.TryAdd( levels[0].Renderers[0], levels ) )\n\t\t\t\t\tLodSizes[levels[0].Renderers[0]] = (group.Body[\u0022m_Size\u0022]?.Number ?? 1) * UnityTransform.InchesPerMeter;\n\t\t\t}\n\t\t}\n\t\tpublic long GameObjectOf( UnityYamlObject transform ) =\u003E Ref( transform.Body, \u0022m_GameObject\u0022 ).FileId;\n\t}\n\n\treadonly Dictionary\u003Cstring, Document\u003E documents = new( StringComparer.OrdinalIgnoreCase );\n\tDocument Load( string guid )\n\t{\n\t\tif ( !documents.TryGetValue( guid, out var document ) ) documents[guid] = document = new( guid, parse( guid ) );\n\t\treturn document;\n\t}\n\n\t/// \u003Csummary\u003EUnity modifications grouped by the source object they target.\u003C/summary\u003E\n\tsealed class Overrides\n\t{\n\t\treadonly Dictionary\u003Clong, Dictionary\u003Cstring, YamlNode\u003E\u003E byTarget = new();\n\t\tpublic Overrides( YamlNode modifications )\n\t\t{\n\t\t\tforeach ( var m in modifications?.Items ?? Array.Empty\u003CYamlNode\u003E() )\n\t\t\t{\n\t\t\t\tvar target = Ref( m, \u0022target\u0022 ).FileId;\n\t\t\t\tvar path = m[\u0022propertyPath\u0022]?.Value;\n\t\t\t\tif ( path == null ) continue;\n\t\t\t\tif ( !byTarget.TryGetValue( target, out var list ) ) byTarget[target] = list = new( StringComparer.Ordinal );\n\t\t\t\tlist[path] = m;\n\t\t\t}\n\t\t}\n\t\tpublic IReadOnlyDictionary\u003Cstring, YamlNode\u003E For( long target ) =\u003E byTarget.GetValueOrDefault( target ) ?? (IReadOnlyDictionary\u003Cstring, YamlNode\u003E)new Dictionary\u003Cstring, YamlNode\u003E();\n\t\tpublic IEnumerable\u003Clong\u003E Targets =\u003E byTarget.Keys;\n\n\t\t/// \u003Csummary\u003EThe name the outermost placement gives the instance root, when it renames it.\u003C/summary\u003E\n\t\tpublic string RootName { get; private set; }\n\t\t/// \u003Csummary\u003EWhether an outer modification applied to this instance.\u003C/summary\u003E\n\t\tpublic bool Merged { get; private set; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// This instance\u0027s modifications with an outer file\u0027s modifications on top, as Unity applies them (after and over\n\t\t/// the instance\u0027s own). The outer ones target objects of this instance as \u003Cparamref name=\u0022document\u0022/\u003E sees them:\n\t\t/// a stripped copy naming its source object, or (Unity 2018.3\u002B) an unserialized ID equal to instance ID XOR source\n\t\t/// ID with the sign bit cleared. Of the two possible source IDs, one this instance already modifies or one\n\t\t/// \u003Cparamref name=\u0022known\u0022/\u003E recognizes is used.\n\t\t/// \u003C/summary\u003E\n\t\tpublic Overrides With( Overrides outer, Document document, long instance, Func\u003Clong, bool\u003E known )\n\t\t{\n\t\t\tif ( outer == null ) return this;\n\t\t\tRootName = outer.RootName;\n\t\t\tforeach ( var target in outer.Targets )\n\t\t\t{\n\t\t\t\tlong source;\n\t\t\t\tif ( document.Objects.TryGetValue( target, out var stripped ) )\n\t\t\t\t{\n\t\t\t\t\tif ( !stripped.Stripped || Ref( stripped.Body, \u0022m_PrefabInstance\u0022 ).FileId != instance ) continue;\n\t\t\t\t\tsource = Ref( stripped.Body, \u0022m_CorrespondingSourceObject\u0022 ).FileId;\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tvar masked = (target ^ instance) \u0026 long.MaxValue;\n\t\t\t\t\tvar match = new[] { masked, masked | long.MinValue }.Where( c =\u003E byTarget.ContainsKey( c ) || known( c ) ).ToArray();\n\t\t\t\t\tif ( match.Length != 1 ) continue;\n\t\t\t\t\tsource = match[0];\n\t\t\t\t}\n\t\t\t\tif ( !byTarget.TryGetValue( source, out var list ) ) byTarget[source] = list = new( StringComparer.Ordinal );\n\t\t\t\tforeach ( var (path, m) in outer.For( target ) ) list[path] = m;\n\t\t\t\tMerged = true;\n\t\t\t\tif ( RootName == null \u0026\u0026 Text( outer.For( target ), \u0022m_Name\u0022 ) is string name ) RootName = name;\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tpublic static double? Number( IReadOnlyDictionary\u003Cstring, YamlNode\u003E values, string path ) =\u003E values.TryGetValue( path, out var m ) \u0026\u0026 m[\u0022value\u0022]?.Number is double v ? v : null;\n\t\tpublic static string Text( IReadOnlyDictionary\u003Cstring, YamlNode\u003E values, string path ) =\u003E values.TryGetValue( path, out var m ) ? m[\u0022value\u0022]?.Value : null;\n\t\tpublic static UnityReference Object( IReadOnlyDictionary\u003Cstring, YamlNode\u003E values, string path ) =\u003E values.TryGetValue( path, out var m ) ? Ref( m, \u0022objectReference\u0022 ) : default;\n\t}\n\n\tstatic (double[] Position, double[] Rotation, double[] Scale) Local( YamlNode transform, IReadOnlyDictionary\u003Cstring, YamlNode\u003E overrides,\n\t\t(double[] Position, double[] Rotation, double[] Scale)? defaults = null )\n\t{\n\t\tvar fallback = defaults ?? (new double[] { 0, 0, 0 }, new double[] { 0, 0, 0, 1 }, new double[] { 1, 1, 1 });\n\t\tdouble[] Axis( string property, double[] fallbacks, params string[] axes ) =\u003E\n\t\t\taxes.Select( ( a, i ) =\u003E overrides != null \u0026\u0026 Overrides.Number( overrides, $\u0022{property}.{a}\u0022 ) is double v ? v : transform?[property]?[a]?.Number ?? fallbacks[i] ).ToArray();\n\t\tvar rotation = Axis( \u0022m_LocalRotation\u0022, fallback.Rotation, \u0022x\u0022, \u0022y\u0022, \u0022z\u0022, \u0022w\u0022 );\n\t\treturn (Axis( \u0022m_LocalPosition\u0022, fallback.Position, \u0022x\u0022, \u0022y\u0022, \u0022z\u0022 ), rotation.All( c =\u003E c == 0 ) ? new double[] { 0, 0, 0, 1 } : rotation, Axis( \u0022m_LocalScale\u0022, fallback.Scale, \u0022x\u0022, \u0022y\u0022, \u0022z\u0022 ));\n\t}\n\n\t/// \u003Csummary\u003EThe change of basis from a model\u0027s file space (its units and FBX axes) into s\u0026amp;box model space, as the VMDL import applies it.\u003C/summary\u003E\n\tstatic UnityTransform Basis( PrefabModel model ) =\u003E\n\t\tUnityTransform.Scaling( model.Scale, model.Scale, model.Scale ) * UnityTransform.Linear( new double[,] { { 0, 0, 1 }, { 1, 0, 0 }, { 0, 1, 0 } } );\n\n\tstatic void SetTransform( JsonObject target, (double[] Position, double[] Rotation, double[] Scale) unity )\n\t{\n\t\tvar (position, rotation, scale) = UnityTransform.FromUnity( unity.Position, unity.Rotation, unity.Scale );\n\t\ttarget[\u0022Position\u0022] = Vec( position ); target[\u0022Rotation\u0022] = Vec( rotation ); target[\u0022Scale\u0022] = Vec( scale );\n\t}\n\n\tstatic JsonObject GameObject( string id, string name, bool enabled ) =\u003E new()\n\t{\n\t\t[\u0022__guid\u0022] = id, [\u0022__version\u0022] = 2, [\u0022Flags\u0022] = 0, [\u0022Name\u0022] = name, [\u0022Position\u0022] = \u00220,0,0\u0022, [\u0022Rotation\u0022] = \u00220,0,0,1\u0022, [\u0022Scale\u0022] = \u00221,1,1\u0022,\n\t\t[\u0022Tags\u0022] = \u0022\u0022, [\u0022Enabled\u0022] = enabled, [\u0022Components\u0022] = new JsonArray(), [\u0022Children\u0022] = new JsonArray()\n\t};\n\tstatic JsonObject Component( string type, string id, bool enabled ) =\u003E new() { [\u0022__type\u0022] = \u0022Sandbox.\u0022 \u002B type, [\u0022__guid\u0022] = id, [\u0022__enabled\u0022] = enabled, [\u0022Flags\u0022] = 0 };\n\n\tpublic string ConvertPrefab( UnityAsset prefab, Summary summary )\n\t{\n\t\tvar document = Load( prefab.Guid );\n\t\tvar root = document.RootTransforms.FirstOrDefault();\n\t\tJsonObject rootObject;\n\t\tif ( root != null ) rootObject = Node( document, root, summary, document.Guid, 0 );\n\t\telse if ( document.InstancesUnder( 0 ).FirstOrDefault() is { } variant )\n\t\t\trootObject = Inline( document, variant, summary, document.Guid, 0 ) ?? GameObject( Id( prefab.Guid, 0, \u0022go\u0022 ), System.IO.Path.GetFileNameWithoutExtension( prefab.Path ), true );\n\t\telse throw new System.IO.InvalidDataException( \u0022The prefab has no root object.\u0022 );\n\t\t// Whatever places the prefab sets its position; the root keeps its rotation and scale, as Unity instantiates them.\n\t\trootObject[\u0022Position\u0022] = \u00220,0,0\u0022;\n\t\tconvertedRoots[prefab.Guid] = rootObject;\n\t\tvar file = new JsonObject { [\u0022RootObject\u0022] = rootObject, [\u0022ShowInMenu\u0022] = false, [\u0022MenuPath\u0022] = null, [\u0022MenuIcon\u0022] = null, [\u0022DontBreakAsTemplate\u0022] = false, [\u0022ResourceVersion\u0022] = 2, [\u0022__references\u0022] = new JsonArray(), [\u0022__version\u0022] = 2 };\n\t\treturn file.ToJsonString( new JsonSerializerOptions { WriteIndented = true } );\n\t}\n\n\treadonly Dictionary\u003Cstring, JsonObject\u003E convertedRoots = new( StringComparer.OrdinalIgnoreCase );\n\treadonly HashSet\u003Cstring\u003E converting = new( StringComparer.OrdinalIgnoreCase );\n\n\t/// \u003Csummary\u003EThe root object of a converted prefab (converting it when needed), whose GUID instances refer to.\u003C/summary\u003E\n\tJsonObject PrefabRoot( string guid )\n\t{\n\t\tif ( convertedRoots.TryGetValue( guid, out var root ) ) return root;\n\t\tif ( !assets.TryGetValue( guid, out var prefab ) || !converting.Add( guid ) ) return null;\n\t\ttry { ConvertPrefab( prefab, new Summary { Source = prefab.Path } ); }\n\t\tcatch ( System.IO.InvalidDataException ) { }\n\t\tfinally { converting.Remove( guid ); }\n\t\treturn convertedRoots.GetValueOrDefault( guid );\n\t}\n\n\t/// \u003Csummary\u003EA Unity scene as an s\u0026amp;box scene: its placed objects, lights, cameras and environment, without scripts.\u003C/summary\u003E\n\tpublic string ConvertScene( UnityAsset scene, Summary summary, string title )\n\t{\n\t\tvar document = Load( scene.Guid );\n\t\tvar objects = new JsonArray();\n\t\tif ( Environment( document, summary, document.Guid ) is { } environment ) objects.Add( environment );\n\t\tvar roots = document.RootTransforms\n\t\t\t.OrderBy( o =\u003E o.Body[\u0022m_RootOrder\u0022]?.Integer ?? 0 ).ToList();\n\t\tforeach ( var root in roots ) if ( Node( document, root, summary, document.Guid, 0 ) is { } node \u0026\u0026 Keep( node ) ) objects.Add( node );\n\t\tforeach ( var instance in document.InstancesUnder( 0 ) )\n\t\t\tif ( Instance( document, instance, summary, document.Guid, 0 ) is { } node ) objects.Add( node );\n\t\tvar file = new JsonObject\n\t\t{\n\t\t\t[\u0022__guid\u0022] = Id( scene.Guid, 0, \u0022scene\u0022 ),\n\t\t\t[\u0022GameObjects\u0022] = objects,\n\t\t\t[\u0022SceneProperties\u0022] = new JsonObject { [\u0022NetworkInterpolation\u0022] = true, [\u0022PhysicsMode\u0022] = \u0022Physics3D\u0022, [\u0022TimeScale\u0022] = 1, [\u0022WantsSystemScene\u0022] = true,\n\t\t\t\t[\u0022Metadata\u0022] = new JsonObject { [\u0022Title\u0022] = title }, [\u0022NavMesh\u0022] = new JsonObject { [\u0022Enabled\u0022] = false } },\n\t\t\t[\u0022ResourceVersion\u0022] = 2, [\u0022Title\u0022] = null, [\u0022Description\u0022] = null, [\u0022__references\u0022] = new JsonArray(), [\u0022__version\u0022] = 2\n\t\t};\n\t\treturn file.ToJsonString( new JsonSerializerOptions { WriteIndented = true } );\n\t}\n\n\t// Scene roots that hold nothing convertible (cameras, lights, managers) are left out.\n\tstatic bool Keep( JsonObject node ) =\u003E (node[\u0022Components\u0022] as JsonArray)?.Count \u003E 0 || ((node[\u0022Children\u0022] as JsonArray)?.Any( c =\u003E c is JsonObject child \u0026\u0026 Keep( child ) ) ?? false) || node.ContainsKey( \u0022__Prefab\u0022 );\n\n\t/// \u003Csummary\u003E\n\t/// Converts one transform\u0027s GameObject and its children. \u003Cparamref name=\u0022space\u0022/\u003E/\u003Cparamref name=\u0022salt\u0022/\u003E name the GUID\n\t/// namespace (the file, or an inlined instance), and \u003Cparamref name=\u0022overrides\u0022/\u003E applies an inlined instance\u0027s modifications.\n\t/// \u003C/summary\u003E\n\t// Hierarchy depth while converting: a cyclic hierarchy or a prefab that contains itself stops with an error.\n\tint nesting;\n\tvoid Enter()\n\t{\n\t\tif ( \u002B\u002Bnesting \u003E 512 ) throw new System.IO.InvalidDataException( \u0022The object hierarchy is cyclic or nested too deeply.\u0022 );\n\t}\n\n\tJsonObject Node( Document document, UnityYamlObject transform, Summary summary, string space, long salt, Overrides overrides = null )\n\t{\n\t\tEnter();\n\t\ttry { return NodeBody( document, transform, summary, space, salt, overrides ); }\n\t\tfinally { nesting--; }\n\t}\n\n\tJsonObject NodeBody( Document document, UnityYamlObject transform, Summary summary, string space, long salt, Overrides overrides )\n\t{\n\t\tvar goId = document.GameObjectOf( transform );\n\t\tvar go = document.Objects.GetValueOrDefault( goId );\n\t\tvar goOverrides = overrides?.For( goId );\n\t\tvar name = Overrides.Text( goOverrides ?? new Dictionary\u003Cstring, YamlNode\u003E(), \u0022m_Name\u0022 ) ?? go?.Body[\u0022m_Name\u0022]?.Value ?? \u0022GameObject\u0022;\n\t\tvar active = Overrides.Number( goOverrides ?? new Dictionary\u003Cstring, YamlNode\u003E(), \u0022m_IsActive\u0022 ) ?? go?.Body[\u0022m_IsActive\u0022]?.Number ?? 1;\n\t\tvar node = GameObject( Id( space, goId ^ salt, \u0022go\u0022 ), name.Trim(), active != 0 );\n\t\tSetTransform( node, Local( transform.Body, overrides?.For( transform.FileId ) ) );\n\t\tsummary.Objects\u002B\u002B;\n\t\tAddComponents( document, goId, go, transform, document.Components.GetValueOrDefault( goId ) ?? new(), node, overrides, summary, space, salt );\n\t\tvar children = node[\u0022Children\u0022].AsArray();\n\t\t// Children in Unity\u0027s order, then nested prefab instances parented here.\n\t\tvar childIds = (transform.Body[\u0022m_Children\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Select( c =\u003E c.Reference.FileId ).ToList();\n\t\tforeach ( var childId in childIds )\n\t\t{\n\t\t\tif ( !document.Objects.TryGetValue( childId, out var child ) ) continue;\n\t\t\tif ( !child.Stripped ) { if ( Node( document, child, summary, space, salt, overrides ) is { } converted ) children.Add( converted ); }\n\t\t}\n\t\t// Instances nested here also receive the outer modifications aimed at their objects.\n\t\tforeach ( var instance in document.InstancesUnder( transform.FileId ) )\n\t\t{\n\t\t\tvar nestedOverrides = overrides == null ? null : new Overrides( instance.Body[\u0022m_Modification\u0022]?[\u0022m_Modifications\u0022] ).With( overrides, document, instance.FileId, id =\u003E SourceHas( instance, id ) );\n\t\t\tif ( nestedOverrides is { Merged: false } ) nestedOverrides = null;\n\t\t\tif ( Instance( document, instance, summary, space, salt, nestedOverrides ) is { } placed ) children.Add( placed );\n\t\t}\n\t\treturn node;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Converts a GameObject\u0027s components onto \u003Cparamref name=\u0022node\u0022/\u003E. \u003Cparamref name=\u0022transform\u0022/\u003E is null for components\n\t/// a placement adds to the root object of a nested prefab or model (Unity keeps them on a stripped copy of the object).\n\t/// \u003C/summary\u003E\n\tvoid AddComponents( Document document, long goId, UnityYamlObject go, UnityYamlObject transform, List\u003CUnityYamlObject\u003E parts, JsonObject node,\n\t\tOverrides overrides, Summary summary, string space, long salt )\n\t{\n\t\tvar components = node[\u0022Components\u0022].AsArray();\n\t\tvar children = node[\u0022Children\u0022].AsArray();\n\t\tvar filter = parts.FirstOrDefault( c =\u003E c.ClassId == 33 );\n\t\t// Mesh colliders after renderers, so a collider can put its shape in the model the object\u0027s renderer shows.\n\t\tforeach ( var part in parts.OrderBy( p =\u003E p.ClassId == 64 ) )\n\t\t{\n\t\t\tswitch ( part.ClassId )\n\t\t\t{\n\t\t\t\tcase 4 or 224 or 33: break;\n\t\t\t\tcase 23 or 137:\n\t\t\t\t{\n\t\t\t\t\tif ( document.HiddenRenderers.Contains( part.FileId ) ) { summary.Skip( \u0022LOD1\u002B renderer\u0022 ); break; }\n\t\t\t\t\tvar mesh = part.ClassId == 137 ? Ref( part.Body, \u0022m_Mesh\u0022 ) : Ref( filter?.Body, \u0022m_Mesh\u0022 );\n\t\t\t\t\tif ( part.ClassId == 23 \u0026\u0026 overrides != null \u0026\u0026 filter != null \u0026\u0026 Overrides.Object( overrides.For( filter.FileId ), \u0022m_Mesh\u0022 ) is { HasGuid: true } swapped ) mesh = swapped;\n\t\t\t\t\tvar materials = (part.Body[\u0022m_Materials\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Select( m =\u003E m.Reference.Guid ).ToList();\n\t\t\t\t\tvar renderOverrides = overrides?.For( part.FileId );\n\t\t\t\t\tif ( renderOverrides != null )\n\t\t\t\t\t\tforeach ( var (path, m) in renderOverrides )\n\t\t\t\t\t\t\tif ( Regex.Match( path, @\u0022^m_Materials\\.Array\\.data\\[([0-9]{1,6})\\]$\u0022 ) is { Success: true } slot \u0026\u0026 int.Parse( slot.Groups[1].Value ) is var index \u0026\u0026 index \u003C 64 )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\twhile ( materials.Count \u003C= index ) materials.Add( null );\n\t\t\t\t\t\t\t\tmaterials[index] = Ref( m, \u0022objectReference\u0022 ).Guid;\n\t\t\t\t\t\t\t}\n\t\t\t\t\tvar enabled = (Overrides.Number( renderOverrides ?? new Dictionary\u003Cstring, YamlNode\u003E(), \u0022m_Enabled\u0022 ) ?? part.Body[\u0022m_Enabled\u0022]?.Number ?? 1) != 0;\n\t\t\t\t\t// Unity\u0027s Cast Shadows: 0 Off, 1 On, 2 Two Sided, 3 Shadows Only (invisible, casting only).\n\t\t\t\t\tvar castShadows = Overrides.Number( renderOverrides ?? new Dictionary\u003Cstring, YamlNode\u003E(), \u0022m_CastShadows\u0022 ) ?? part.Body[\u0022m_CastShadows\u0022]?.Number ?? 1;\n\t\t\t\t\tRenderer( document, part, components, children, mesh, materials, enabled, summary, space, goId ^ salt, castShadows );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 64:\n\t\t\t\t\tMeshCollider( document, components, children, part, overrides?.For( part.FileId ), summary, space, salt );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 218:\n\t\t\t\t\t// A terrain is drawn from its object\u0027s own transform, which an added component\u0027s stripped object doesn\u0027t have.\n\t\t\t\t\tif ( transform == null ) summary.Notes.Add( \u0022A terrain added to an object inside a nested prefab or model is not converted.\u0022 );\n\t\t\t\t\telse Terrain( children, part, parts, transform, overrides, summary, space, salt );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 108:\n\t\t\t\t\tif ( Light( document, part, parts, overrides?.For( part.FileId ), summary, space, salt ) is { } light ) components.Add( light );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 20:\n\t\t\t\t\tcomponents.Add( Camera( part, go, overrides?.For( part.FileId ), summary, space, salt ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 215:\n\t\t\t\t\tcomponents.Add( ReflectionProbe( part, overrides?.For( part.FileId ), summary, space, salt ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 82:\n\t\t\t\t\tif ( AudioSource( part, overrides?.For( part.FileId ), summary, space, salt ) is { } sound ) components.Add( sound );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 154:\n\t\t\t\t\t// A terrain\u0027s collider becomes its EnableCollision setting.\n\t\t\t\t\tif ( !parts.Any( p =\u003E p.ClassId == 218 ) ) summary.Skip( \u0022TerrainCollider without a terrain\u0022 );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 65 or 135 or 136:\n\t\t\t\t\tif ( Collider( part, overrides?.For( part.FileId ), space, salt ) is { } collider ) { components.Add( collider ); summary.Colliders\u002B\u002B; }\n\t\t\t\t\tbreak;\n\t\t\t\tdefault: summary.Skip( part.Type ?? $\u0022class {part.ClassId}\u0022 ); break;\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A Unity MeshCollider as a static ModelCollider using a collision VMDL of the same mesh: one convex hull when the\n\t/// collider is convex, else the exact mesh. It sits at the renderer offset for that mesh, so both line up.\n\t/// \u003C/summary\u003E\n\tvoid MeshCollider( Document document, JsonArray components, JsonArray children, UnityYamlObject collider, IReadOnlyDictionary\u003Cstring, YamlNode\u003E overrides, Summary summary, string space, long salt )\n\t{\n\t\tvar ownerGuid = document.Guid;\n\t\toverrides ??= new Dictionary\u003Cstring, YamlNode\u003E();\n\t\tvar mesh = Ref( collider.Body, \u0022m_Mesh\u0022 );\n\t\tif ( Overrides.Object( overrides, \u0022m_Mesh\u0022 ) is { FileId: not 0 } swapped ) mesh = swapped;\n\t\tbool Flag( string property, double fallback ) =\u003E (Overrides.Number( overrides, property ) ?? collider.Body[property]?.Number ?? fallback) != 0;\n\t\tif ( mesh.FileId == 0 \u0026\u0026 !mesh.HasGuid )\n\t\t{\n\t\t\tsummary.Skip( \u0022MeshCollider without a mesh\u0022 );\n\t\t\treturn;\n\t\t}\n\t\tif ( Generated( mesh ) )\n\t\t{\n\t\t\t// Built-in, embedded and .asset meshes: a collision VMDL from the OBJ written for the mesh.\n\t\t\tvar generated = GeneratedObj( mesh, ownerGuid, summary );\n\t\t\tif ( generated == null ) { summary.Skip( \u0022MeshCollider with an unreadable mesh\u0022 ); return; }\n\t\t\tvar convexShape = Flag( \u0022m_Convex\u0022, 0 );\n\t\t\tvar shape = Component( \u0022ModelCollider\u0022, Id( space, collider.FileId ^ salt, \u0022collider\u0022 ), Flag( \u0022m_Enabled\u0022, 1 ) );\n\t\t\tvar shapePart = new MeshPart( generated.Reference, Array.Empty\u003Cstring\u003E(), UnityTransform.InchesPerMeter );\n\t\t\t// The shape goes in the model this object\u0027s renderer shows for the same mesh; otherwise a physics-only VMDL.\n\t\t\tshape[\u0022Model\u0022] = document.GeneratedRenders.TryGetValue( (Ref( collider.Body, \u0022m_GameObject\u0022 ).FileId, mesh.Guid ?? \u0022\u0022, mesh.FileId), out var rendered ) \u0026\u0026\n\t\t\t\tEmbedPhysics != null \u0026\u0026 EmbedPhysics( rendered, shapePart, convexShape )\n\t\t\t\t? rendered\n\t\t\t\t: Compose( new ComposedModel( ModelBase( mesh, ownerGuid, generated ) \u002B \u0022_iup_collision\u0022 \u002B (convexShape ? \u0022_hull\u0022 : \u0022\u0022), Array.Empty\u003CIReadOnlyList\u003CMeshPart\u003E\u003E(), null,\n\t\t\t\t\tnew Dictionary\u003Cstring, string\u003E(), shapePart, convexShape ) );\n\t\t\tshape[\u0022Static\u0022] = true;\n\t\t\tshape[\u0022IsTrigger\u0022] = Flag( \u0022m_IsTrigger\u0022, 0 );\n\t\t\tsummary.Colliders\u002B\u002B;\n\t\t\tcomponents.Add( shape );\n\t\t\treturn;\n\t\t}\n\t\tif ( !models.TryGetValue( mesh.Guid, out var model ) || model.CollisionVmdl == null )\n\t\t{\n\t\t\tsummary.Skip( \u0022MeshCollider with a mesh that is not a model\u0022 );\n\t\t\treturn;\n\t\t}\n\t\tvar convex = Flag( \u0022m_Convex\u0022, 0 );\n\t\tvar binding = model.Info.ResolveMesh( mesh.FileId );\n\t\tif ( binding == null ) summary.Notes.Add( $\u0022A MeshCollider\u0027s mesh in \u0027{model.Name}\u0027 could not be identified; the whole model\u0027s collision is used.\u0022 );\n\t\tvar offset = binding != null \u0026\u0026 model.Info.MeshNodes.TryGetValue( binding.Mesh, out var nodeTransform )\n\t\t\t? (model.Info.FileAxes * nodeTransform).Inverse().In( Basis( model ) ) : UnityTransform.Identity;\n\t\tvar component = Component( \u0022ModelCollider\u0022, Id( space, collider.FileId ^ salt, \u0022collider\u0022 ), Flag( \u0022m_Enabled\u0022, 1 ) );\n\t\tcomponent[\u0022Model\u0022] = model.CollisionVmdl( binding?.Mesh, convex );\n\t\tcomponent[\u0022Static\u0022] = true;\n\t\tcomponent[\u0022IsTrigger\u0022] = Flag( \u0022m_IsTrigger\u0022, 0 );\n\t\tsummary.Colliders\u002B\u002B;\n\t\tif ( offset.IsIdentity() ) { components.Add( component ); return; }\n\t\tvar holder = GameObject( Id( space, collider.FileId ^ salt, \u0022collision\u0022 ), \u0022collision\u0022, true );\n\t\tvar (position, rotation, scale) = offset.Decompose();\n\t\tholder[\u0022Position\u0022] = Vec( position ); holder[\u0022Rotation\u0022] = Vec( rotation ); holder[\u0022Scale\u0022] = Vec( scale );\n\t\tholder[\u0022Components\u0022].AsArray().Add( component );\n\t\tchildren.Add( holder );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Whether a mesh reference is to a mesh the importer writes itself: a Unity built-in primitive, a mesh stored in this\n\t/// file or in a prefab or scene, or a mesh asset (.asset). Model files are imported as models instead.\n\t/// \u003C/summary\u003E\n\tbool Generated( UnityReference mesh ) =\u003E\n\t\tmesh.FileId != 0 \u0026\u0026 (!mesh.HasGuid || mesh.Guid.Equals( UnityMesh.BuiltinGuid, StringComparison.OrdinalIgnoreCase ) ||\n\t\t\t!models.ContainsKey( mesh.Guid ) \u0026\u0026 assets.GetValueOrDefault( mesh.Guid ) is { Kind: not UnityAssetKind.Model });\n\n\tstatic bool Builtin( UnityReference mesh ) =\u003E mesh.HasGuid \u0026\u0026 mesh.Guid.Equals( UnityMesh.BuiltinGuid, StringComparison.OrdinalIgnoreCase );\n\n\t/// \u003Csummary\u003E\n\t/// Where a VMDL made from a generated mesh goes, without extension: beside the mesh\u0027s OBJ, except for built-in meshes,\n\t/// whose OBJs are shared in one folder while their VMDLs go beside the prefab or scene that places them.\n\t/// \u003C/summary\u003E\n\tstring ModelBase( UnityReference mesh, string ownerGuid, GeneratedMesh generated )\n\t{\n\t\tif ( !Builtin( mesh ) || !assets.TryGetValue( ownerGuid ?? \u0022\u0022, out var owner ) ) return System.IO.Path.ChangeExtension( generated.Relative, null );\n\t\tvar folder = (System.IO.Path.GetDirectoryName( owner.Path ) ?? \u0022\u0022).Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\t\treturn (folder.Length \u003E 0 ? folder \u002B \u0022/\u0022 : \u0022\u0022) \u002B System.IO.Path.GetFileNameWithoutExtension( generated.Relative );\n\t}\n\n\tGeneratedMesh GeneratedObj( UnityReference mesh, string ownerGuid, Summary summary )\n\t{\n\t\tif ( MeshObj == null || Compose == null ) return null;\n\t\tvar generated = MeshObj( mesh.HasGuid ? mesh.Guid : ownerGuid, mesh.FileId );\n\t\tif ( generated.Error != null ) { summary.Notes.Add( generated.Error ); return null; }\n\t\treturn generated;\n\t}\n\n\t/// \u003Csummary\u003EA VMDL for a generated mesh with the renderer\u0027s materials on its submeshes, in order.\u003C/summary\u003E\n\tstring GeneratedModel( UnityReference mesh, string ownerGuid, List\u003Cstring\u003E materials, Summary summary )\n\t{\n\t\tvar generated = GeneratedObj( mesh, ownerGuid, summary );\n\t\tif ( generated == null ) return null;\n\t\tvar remaps = new Dictionary\u003Cstring, string\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tfor ( var i = 0; i \u003C generated.Submeshes \u0026\u0026 i \u003C materials.Count; i\u002B\u002B )\n\t\t\tif ( materials[i] != null \u0026\u0026 Materials.TryGetValue( materials[i], out var vmat ) ) remaps[$\u0022submesh{i}.vmat\u0022] = vmat;\n\t\t// A built-in mesh is shared by every placement, so its VMDL is also named after the material it shows.\n\t\tvar name = ModelBase( mesh, ownerGuid, generated );\n\t\tif ( Builtin( mesh ) \u0026\u0026 remaps.Count \u003E 0 ) name \u002B= \u0022_\u0022 \u002B System.IO.Path.GetFileNameWithoutExtension( remaps.Values.First() );\n\t\treturn Compose( new ComposedModel( name, new[] { (IReadOnlyList\u003CMeshPart\u003E)new[] { new MeshPart( generated.Reference, Array.Empty\u003Cstring\u003E(), UnityTransform.InchesPerMeter ) } }, null, remaps ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A VMDL holding every level of the renderer\u0027s LOD group, when each lower level is one renderer on the same object\n\t/// or a sibling with the same transform, showing one identified mesh of an imported model at the same offset. Each\n\t/// level\u0027s materials come from its renderer; thresholds from Unity\u0027s screen-relative heights. Otherwise null.\n\t/// \u003C/summary\u003E\n\tstring LodModel( Document document, UnityYamlObject renderer, List\u003C(double Height, List\u003Clong\u003E Renderers)\u003E levels, PrefabModel model, UnityModel.RendererBinding binding, UnityTransform offset, List\u003Cstring\u003E materials )\n\t{\n\t\tif ( Compose == null || model.MeshFile == null ) return null;\n\t\tvar lods = new List\u003CIReadOnlyList\u003CMeshPart\u003E\u003E();\n\t\tvar thresholds = new List\u003Cdouble\u003E();\n\t\tvar remaps = new Dictionary\u003Cstring, string\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvoid Remap( UnityModel.RendererBinding b, List\u003Cstring\u003E mats )\n\t\t{\n\t\t\tfor ( var i = 0; i \u003C mats.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar slot = b.Slots.Length == 0 ? (i == 0 ? b.Mesh : null) : i \u003C b.Slots.Length ? b.Slots[i] : null;\n\t\t\t\tif ( slot != null \u0026\u0026 mats[i] != null \u0026\u0026 Materials.TryGetValue( mats[i], out var vmat ) ) remaps.TryAdd( slot.ToLowerInvariant() \u002B \u0022.vmat\u0022, vmat );\n\t\t\t}\n\t\t}\n\t\tlods.Add( new[] { new MeshPart( model.MeshFile, new[] { binding.Mesh }, model.Scale ) } );\n\t\tRemap( binding, materials );\n\t\tvar go = Ref( renderer.Body, \u0022m_GameObject\u0022 ).FileId;\n\t\tvar transform = document.TransformOf.GetValueOrDefault( go );\n\t\tfor ( var i = 1; i \u003C levels.Count; i\u002B\u002B )\n\t\t{\n\t\t\tif ( levels[i].Renderers.Count != 1 || !document.Objects.TryGetValue( levels[i].Renderers[0], out var lower ) || lower.Stripped || lower.ClassId is not (23 or 137) ) return null;\n\t\t\tvar lowerGo = Ref( lower.Body, \u0022m_GameObject\u0022 ).FileId;\n\t\t\tif ( lowerGo != go )\n\t\t\t{\n\t\t\t\tvar lowerTransform = document.TransformOf.GetValueOrDefault( lowerGo );\n\t\t\t\tif ( transform == null || lowerTransform == null || Ref( lowerTransform.Body, \u0022m_Father\u0022 ).FileId != Ref( transform.Body, \u0022m_Father\u0022 ).FileId ) return null;\n\t\t\t\tvar a = Local( transform.Body, null ); var b = Local( lowerTransform.Body, null );\n\t\t\t\tif ( a.Position.Zip( b.Position ).Concat( a.Rotation.Zip( b.Rotation ) ).Concat( a.Scale.Zip( b.Scale ) ).Any( p =\u003E Math.Abs( p.First - p.Second ) \u003E 1e-4 ) ) return null;\n\t\t\t}\n\t\t\tvar filter = (document.Components.GetValueOrDefault( lowerGo ) ?? new()).FirstOrDefault( c =\u003E c.ClassId == 33 );\n\t\t\tvar mesh = lower.ClassId == 137 ? Ref( lower.Body, \u0022m_Mesh\u0022 ) : Ref( filter?.Body, \u0022m_Mesh\u0022 );\n\t\t\tif ( !mesh.HasGuid || !models.TryGetValue( mesh.Guid, out var lowerModel ) || lowerModel.Vmdl == null || lowerModel.MeshFile == null ) return null;\n\t\t\tvar lowerBinding = lowerModel.Info.ResolveMesh( mesh.FileId );\n\t\t\tif ( lowerBinding == null ) return null;\n\t\t\tvar lowerOffset = lowerModel.Info.MeshNodes.TryGetValue( lowerBinding.Mesh, out var node ) ? (lowerModel.Info.FileAxes * node).Inverse().In( Basis( lowerModel ) ) : UnityTransform.Identity;\n\t\t\tif ( !(lowerOffset * offset.Inverse()).IsIdentity() ) return null;\n\t\t\tlods.Add( new[] { new MeshPart( lowerModel.MeshFile, new[] { lowerBinding.Mesh }, lowerModel.Scale ) } );\n\t\t\tRemap( lowerBinding, (lower.Body[\u0022m_Materials\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Select( m =\u003E m.Reference.Guid ).ToList() );\n\t\t\tthresholds.Add( UnityImport.LodThreshold( levels[i - 1].Height, document.LodSizes.GetValueOrDefault( renderer.FileId, UnityImport.DefaultLodSize ) ) );\n\t\t}\n\t\tvar name = System.IO.Path.ChangeExtension( model.Vmdl.StartsWith( \u0022Imported/\u0022, StringComparison.Ordinal ) ? model.Vmdl[\u0022Imported/\u0022.Length..] : model.Vmdl, null ) \u002B \u0022_iup_lod_\u0022 \u002B Regex.Replace( binding.Mesh.Trim(), \u0022[^A-Za-z0-9_-]\u002B\u0022, \u0022_\u0022 ).Trim( \u0027_\u0027 );\n\t\treturn Compose( new ComposedModel( name, lods, thresholds, remaps ) );\n\t}\n\n\tstatic string ShadowMode( double castShadows ) =\u003E castShadows switch { 0 =\u003E \u0022Off\u0022, 3 =\u003E \u0022ShadowsOnly\u0022, _ =\u003E \u0022On\u0022 };\n\n\tvoid Renderer( Document document, UnityYamlObject part, JsonArray components, JsonArray children, UnityReference mesh, List\u003Cstring\u003E materials, bool enabled, Summary summary, string space, long id, double castShadows = 1 )\n\t{\n\t\tif ( mesh.HasGuid \u0026\u0026 models.TryGetValue( mesh.Guid, out var collisionOnly ) \u0026\u0026 collisionOnly.Vmdl == null )\n\t\t{\n\t\t\tsummary.Skip( \u0022renderer of a collision-only mesh\u0022 );\n\t\t\treturn;\n\t\t}\n\t\tif ( Generated( mesh ) )\n\t\t{\n\t\t\tif ( GeneratedModel( mesh, document.Guid, materials, summary ) is not { } generatedVmdl ) return;\n\t\t\tdocument.GeneratedRenders[(Ref( part.Body, \u0022m_GameObject\u0022 ).FileId, mesh.Guid ?? \u0022\u0022, mesh.FileId)] = generatedVmdl;\n\t\t\tvar generatedRenderer = Component( \u0022ModelRenderer\u0022, Id( space, id, \u0022renderer\u0022 ), enabled );\n\t\t\tgeneratedRenderer[\u0022Model\u0022] = generatedVmdl; generatedRenderer[\u0022MaterialGroup\u0022] = null;\n\t\t\tgeneratedRenderer[\u0022Tint\u0022] = \u00221,1,1,1\u0022; generatedRenderer[\u0022RenderType\u0022] = ShadowMode( castShadows ); generatedRenderer[\u0022CreateAttachments\u0022] = false;\n\t\t\tsummary.Renderers\u002B\u002B; summary.GeneratedMeshes\u002B\u002B;\n\t\t\tcomponents.Add( generatedRenderer );\n\t\t\treturn;\n\t\t}\n\t\tif ( !mesh.HasGuid || !models.TryGetValue( mesh.Guid, out var model ) )\n\t\t{\n\t\t\tif ( mesh.HasGuid ) summary.Notes.Add( $\u0022A renderer uses a mesh from \u0027{assets.GetValueOrDefault( mesh.Guid )?.Path ?? mesh.Guid}\u0027, which is not imported as a model.\u0022 );\n\t\t\telse summary.Skip( \u0022renderer without a model mesh\u0022 );\n\t\t\treturn;\n\t\t}\n\t\tvar binding = model.Info.ResolveMesh( mesh.FileId );\n\t\tvar slots = new Dictionary\u003Cstring, string\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tif ( binding != null )\n\t\t\tfor ( var i = 0; i \u003C materials.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar slot = binding.Slots.Length == 0 ? (i == 0 ? binding.Mesh : null) : i \u003C binding.Slots.Length ? binding.Slots[i] : null;\n\t\t\t\tif ( slot != null \u0026\u0026 !string.IsNullOrEmpty( materials[i] ) ) slots[slot] = materials[i];\n\t\t\t}\n\t\tvar (group, exact) = model.Group( slots );\n\t\tif ( !exact ) summary.Notes.Add( $\u0022No material group of \u0027{model.Name}\u0027 matches a renderer\u0027s materials exactly; the closest is used.\u0022 );\n\t\t// One mesh of a multi-mesh model: a VMDL with only that mesh, offset to undo the node transform ModelDoc bakes in.\n\t\tvar vmdl = model.Vmdl;\n\t\tvar offset = UnityTransform.Identity;\n\t\tif ( binding == null ) summary.Notes.Add( $\u0022A renderer\u0027s mesh in \u0027{model.Name}\u0027 could not be identified; the whole model is shown.\u0022 );\n\t\telse\n\t\t{\n\t\t\tif ( model.Info.ImportedMeshes.Length \u003E 1 || !model.Info.ImportedMeshes.Contains( binding.Mesh ) ) vmdl = model.MeshVmdl( binding.Mesh );\n\t\t\tif ( model.Info.MeshNodes.TryGetValue( binding.Mesh, out var nodeTransform ) )\n\t\t\t{\n\t\t\t\t// Unity converts a file\u0027s axis system with one rotation at the model root, folded into the node transforms\n\t\t\t\t// (a Z-up file\u0027s \u002B90 degree X node becomes unrotated); the mesh itself stays in the node\u0027s space.\n\t\t\t\toffset = (model.Info.FileAxes * nodeTransform).Inverse().In( Basis( model ) );\n\t\t\t}\n\t\t\telse summary.Notes.Add( $\u0022Mesh \u0027{binding.Mesh}\u0027 of \u0027{model.Name}\u0027 has no unique node transform; it is placed without compensation.\u0022 );\n\t\t}\n\t\t// A LOD group: one VMDL with every level, when the levels line up.\n\t\tif ( binding != null \u0026\u0026 document.LodGroups.TryGetValue( part.FileId, out var levels ) )\n\t\t{\n\t\t\tif ( LodModel( document, part, levels, model, binding, offset, materials ) is { } lodVmdl ) { vmdl = lodVmdl; group = null; summary.LodGroups\u002B\u002B; }\n\t\t\telse summary.Notes.Add( $\u0022A LOD group of \u0027{model.Name}\u0027 shows only LOD0: its lower levels are not one renderer each at the same place, or not identified meshes.\u0022 );\n\t\t}\n\t\tvar renderer = Component( \u0022ModelRenderer\u0022, Id( space, id, \u0022renderer\u0022 ), enabled );\n\t\trenderer[\u0022Model\u0022] = vmdl; renderer[\u0022MaterialGroup\u0022] = string.IsNullOrEmpty( group ) ? null : group;\n\t\trenderer[\u0022Tint\u0022] = \u00221,1,1,1\u0022; renderer[\u0022RenderType\u0022] = ShadowMode( castShadows ); renderer[\u0022CreateAttachments\u0022] = false;\n\t\tsummary.Renderers\u002B\u002B;\n\t\tif ( offset.IsIdentity() ) { components.Add( renderer ); return; }\n\t\tvar holder = GameObject( Id( space, id, \u0022mesh\u0022 ), \u0022mesh\u0022, true );\n\t\tvar (position, rotation, scale) = offset.Decompose();\n\t\tholder[\u0022Position\u0022] = Vec( position ); holder[\u0022Rotation\u0022] = Vec( rotation ); holder[\u0022Scale\u0022] = Vec( scale );\n\t\tholder[\u0022Components\u0022].AsArray().Add( renderer );\n\t\tchildren.Add( holder );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A Unity terrain as an s\u0026amp;box Terrain, with its painted trees. Unity draws a terrain from its object\u0027s position\n\t/// only, ignoring rotation and scale, with the terrain\u0027s corner there; s\u0026amp;box terrains start at the corner on the\n\t/// Unity terrain\u0027s \u002BX edge (their local X runs along Unity Z and Y along Unity -X). The terrain and its trees sit on\n\t/// children that undo the object\u0027s rotation and scale. Trees are instances of their prototype prefabs or models with\n\t/// the tree\u0027s position, rotation about the up axis and width/height scale, which Unity uses in place of the\n\t/// prototype root\u0027s own transform.\n\t/// \u003C/summary\u003E\n\tvoid Terrain( JsonArray children, UnityYamlObject component, List\u003CUnityYamlObject\u003E parts, UnityYamlObject transform, Overrides overrides, Summary summary, string space, long salt )\n\t{\n\t\tvar values = overrides?.For( component.FileId ) ?? new Dictionary\u003Cstring, YamlNode\u003E();\n\t\tvar reference = Ref( component.Body, \u0022m_TerrainData\u0022 );\n\t\tif ( Overrides.Object( values, \u0022m_TerrainData\u0022 ) is { HasGuid: true } swapped ) reference = swapped;\n\t\tif ( !reference.HasGuid || !Terrains.TryGetValue( reference.Guid, out var terrain ) )\n\t\t{\n\t\t\tsummary.Notes.Add( reference.HasGuid ? $\u0022Terrain data \u0027{assets.GetValueOrDefault( reference.Guid )?.Path ?? reference.Guid}\u0027 is not imported; the terrain was left out.\u0022 : \u0022A terrain without terrain data was left out.\u0022 );\n\t\t\treturn;\n\t\t}\n\t\tvar local = Local( transform.Body, overrides?.For( transform.FileId ) );\n\t\tvar (_, ownRotation, ownScale) = UnityTransform.FromUnity( local.Position, local.Rotation, local.Scale );\n\t\tvar counter = (UnityTransform.Rotation( ownRotation[0], ownRotation[1], ownRotation[2], ownRotation[3] ) * UnityTransform.Scaling( ownScale[0], ownScale[1], ownScale[2] )).Inverse();\n\t\tJsonObject Holder( string part, string name, UnityTransform offset )\n\t\t{\n\t\t\tvar holder = GameObject( Id( space, component.FileId ^ salt, part ), name, true );\n\t\t\tvar (position, rotation, scale) = offset.Decompose();\n\t\t\tholder[\u0022Position\u0022] = Vec( position ); holder[\u0022Rotation\u0022] = Vec( rotation ); holder[\u0022Scale\u0022] = Vec( scale );\n\t\t\tchildren.Add( holder );\n\t\t\treturn holder;\n\t\t}\n\t\tvar size = terrain.Data.Size;\n\t\tvar surface = Holder( \u0022terrain\u0022, \u0022terrain\u0022, counter * UnityTransform.Translation( 0, -size[0] * UnityTransform.InchesPerMeter, 0 ) );\n\t\tvar result = Component( \u0022Terrain\u0022, Id( space, component.FileId ^ salt, \u0022component\u0022 ), (Overrides.Number( values, \u0022m_Enabled\u0022 ) ?? component.Body[\u0022m_Enabled\u0022]?.Number ?? 1) != 0 );\n\t\tresult[\u0022Storage\u0022] = terrain.Path;\n\t\tresult[\u0022TerrainSize\u0022] = Math.Round( terrain.Size, 3 );\n\t\tresult[\u0022TerrainHeight\u0022] = Math.Round( terrain.Height, 3 );\n\t\tresult[\u0022EnableCollision\u0022] = parts.Any( p =\u003E p.ClassId == 154 \u0026\u0026 (Overrides.Number( overrides?.For( p.FileId ) ?? new Dictionary\u003Cstring, YamlNode\u003E(), \u0022m_Enabled\u0022 ) ?? p.Body[\u0022m_Enabled\u0022]?.Number ?? 1) != 0 );\n\t\tresult[\u0022Static\u0022] = true;\n\t\tsurface[\u0022Components\u0022].AsArray().Add( result );\n\t\tsummary.Terrains\u002B\u002B;\n\t\tif ( terrain.Data.Trees.Count == 0 ) return;\n\t\tvar trees = Holder( \u0022trees\u0022, \u0022Trees\u0022, counter )[\u0022Children\u0022].AsArray();\n\t\tvar missing = new HashSet\u003Cstring\u003E();\n\t\tfor ( var i = 0; i \u003C terrain.Data.Trees.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar tree = terrain.Data.Trees[i];\n\t\t\tvar prototype = tree.Prototype \u003E= 0 \u0026\u0026 tree.Prototype \u003C terrain.Data.TreePrototypes.Count ? terrain.Data.TreePrototypes[tree.Prototype] : default;\n\t\t\tvar half = tree.Rotation / 2;\n\t\t\tvar placement = UnityTransform.FromUnity( new[] { tree.X * size[0], tree.Y * size[1], tree.Z * size[2] }, new[] { 0, Math.Sin( half ), 0, Math.Cos( half ) }, new[] { tree.Width, tree.Height, tree.Width } );\n\t\t\tvar id = Id( space, component.FileId ^ salt, \u0022tree:\u0022 \u002B i );\n\t\t\tJsonObject placed = null;\n\t\t\tif ( prototype.HasGuid \u0026\u0026 models.TryGetValue( prototype.Guid, out var model ) \u0026\u0026 model.Vmdl != null )\n\t\t\t{\n\t\t\t\tplaced = GameObject( id, model.Name, true );\n\t\t\t\tplaced[\u0022Position\u0022] = Vec( placement.Position ); placed[\u0022Rotation\u0022] = Vec( placement.Rotation ); placed[\u0022Scale\u0022] = Vec( placement.Scale );\n\t\t\t\tAddModel( placed, model, \u0022\u0022, summary, space, component.FileId ^ salt ^ ((i \u002B 1L) \u003C\u003C 32) );\n\t\t\t\tsummary.ModelInstances\u002B\u002B;\n\t\t\t}\n\t\t\telse if ( prototype.HasGuid \u0026\u0026 prefabOutputs.TryGetValue( prototype.Guid, out var path ) \u0026\u0026 PrefabRoot( prototype.Guid ) is { } root )\n\t\t\t{\n\t\t\t\tvar rootId = root[\u0022__guid\u0022].GetValue\u003Cstring\u003E();\n\t\t\t\tvar properties = new JsonArray();\n\t\t\t\tvoid Set( string property, JsonNode value ) =\u003E\n\t\t\t\t\tproperties.Add( new JsonObject { [\u0022Target\u0022] = new JsonObject { [\u0022Type\u0022] = \u0022GameObject\u0022, [\u0022IdValue\u0022] = rootId }, [\u0022Property\u0022] = property, [\u0022Value\u0022] = value } );\n\t\t\t\tSet( \u0022Name\u0022, root[\u0022Name\u0022]?.GetValue\u003Cstring\u003E() ?? \u0022Tree\u0022 );\n\t\t\t\tSet( \u0022Position\u0022, Vec( placement.Position ) ); Set( \u0022Rotation\u0022, Vec( placement.Rotation ) ); Set( \u0022Scale\u0022, Vec( placement.Scale ) );\n\t\t\t\tplaced = PrefabReference( id, path, rootId, properties );\n\t\t\t\tsummary.PrefabInstances\u002B\u002B;\n\t\t\t}\n\t\t\telse missing.Add( prototype.HasGuid ? assets.GetValueOrDefault( prototype.Guid )?.Path ?? prototype.Guid : \u0022a missing prototype\u0022 );\n\t\t\tif ( placed != null ) { trees.Add( placed ); summary.Trees\u002B\u002B; }\n\t\t}\n\t\tforeach ( var name in missing ) summary.Notes.Add( $\u0022Trees using \u0027{name}\u0027 were left out; the prototype is not imported.\u0022 );\n\t}\n\n\tstatic JsonObject PrefabReference( string instanceId, string path, string rootId, JsonArray properties ) =\u003E new()\n\t{\n\t\t[\u0022__guid\u0022] = instanceId, [\u0022__version\u0022] = 2, [\u0022__Prefab\u0022] = path,\n\t\t[\u0022__PrefabInstancePatch\u0022] = new JsonObject { [\u0022AddedObjects\u0022] = new JsonArray(), [\u0022RemovedObjects\u0022] = new JsonArray(), [\u0022PropertyOverrides\u0022] = properties, [\u0022MovedObjects\u0022] = new JsonArray() },\n\t\t[\u0022__PrefabIdToInstanceId\u0022] = new JsonObject { [rootId] = instanceId }\n\t};\n\n\tstatic JsonObject Collider( UnityYamlObject collider, IReadOnlyDictionary\u003Cstring, YamlNode\u003E overrides, string space, long salt )\n\t{\n\t\t// Prefab instances can override any of these values.\n\t\tdouble N( string property, double fallback ) =\u003E Num( collider, overrides, property, fallback );\n\t\tdouble[] V( string property, double fallback ) =\u003E new[] { \u0022x\u0022, \u0022y\u0022, \u0022z\u0022 }.Select( a =\u003E N( property \u002B \u0022.\u0022 \u002B a, fallback ) ).ToArray();\n\t\tdouble[] Center() =\u003E V( \u0022m_Center\u0022, 0 );\n\t\tdouble[] S( double[] u ) =\u003E new[] { u[2] * UnityTransform.InchesPerMeter, -u[0] * UnityTransform.InchesPerMeter, u[1] * UnityTransform.InchesPerMeter };\n\t\tvar enabled = N( \u0022m_Enabled\u0022, 1 ) != 0;\n\t\tvar trigger = N( \u0022m_IsTrigger\u0022, 0 ) != 0;\n\t\tJsonObject result;\n\t\tswitch ( collider.ClassId )\n\t\t{\n\t\t\tcase 65:\n\t\t\t\tvar size = V( \u0022m_Size\u0022, 1 );\n\t\t\t\tresult = Component( \u0022BoxCollider\u0022, Id( space, collider.FileId ^ salt, \u0022collider\u0022 ), enabled );\n\t\t\t\tresult[\u0022Center\u0022] = Vec( S( Center() ) );\n\t\t\t\tresult[\u0022Scale\u0022] = Vec( new[] { size[2], size[0], size[1] }.Select( v =\u003E Math.Abs( v ) * UnityTransform.InchesPerMeter ).ToArray() );\n\t\t\t\tbreak;\n\t\t\tcase 135:\n\t\t\t\tresult = Component( \u0022SphereCollider\u0022, Id( space, collider.FileId ^ salt, \u0022collider\u0022 ), enabled );\n\t\t\t\tresult[\u0022Center\u0022] = Vec( S( Center() ) );\n\t\t\t\tresult[\u0022Radius\u0022] = F( N( \u0022m_Radius\u0022, 0.5 ) * UnityTransform.InchesPerMeter );\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tvar radius = N( \u0022m_Radius\u0022, 0.5 );\n\t\t\t\tvar half = Math.Max( 0, N( \u0022m_Height\u0022, 2 ) / 2 - radius );\n\t\t\t\tvar axis = new double[3];\n\t\t\t\taxis[(int)Math.Clamp( N( \u0022m_Direction\u0022, 1 ), 0, 2 )] = half;\n\t\t\t\tvar center = Center();\n\t\t\t\tresult = Component( \u0022CapsuleCollider\u0022, Id( space, collider.FileId ^ salt, \u0022collider\u0022 ), enabled );\n\t\t\t\tresult[\u0022Start\u0022] = Vec( S( center.Zip( axis, ( c, a ) =\u003E c - a ).ToArray() ) );\n\t\t\t\tresult[\u0022End\u0022] = Vec( S( center.Zip( axis, ( c, a ) =\u003E c \u002B a ).ToArray() ) );\n\t\t\t\tresult[\u0022Radius\u0022] = F( radius * UnityTransform.InchesPerMeter );\n\t\t\t\tbreak;\n\t\t}\n\t\tresult[\u0022IsTrigger\u0022] = trigger; result[\u0022Static\u0022] = true;\n\t\treturn result;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Components a placement adds to objects inside its nested prefab or model. Unity keeps each such object as a stripped\n\t/// copy, naming its source object, with the added components on it. Returns the copy, its added components, and whether\n\t/// it is the source\u0027s root object (by \u003Cparamref name=\u0022isRoot\u0022/\u003E, given the source object ID).\n\t/// \u003C/summary\u003E\n\tIEnumerable\u003C(UnityYamlObject Go, List\u003CUnityYamlObject\u003E Parts, bool Root)\u003E AddedComponents( Document document, UnityYamlObject instance, Func\u003Clong, bool\u003E isRoot )\n\t{\n\t\tforeach ( var id in document.StrippedGameObjects.GetValueOrDefault( instance.FileId ) ?? new List\u003Clong\u003E() )\n\t\t\tif ( document.Objects.GetValueOrDefault( id ) is { } go \u0026\u0026 document.Components.GetValueOrDefault( id ) is { Count: \u003E 0 } parts )\n\t\t\t\tyield return (go, parts, isRoot( Ref( go.Body, \u0022m_CorrespondingSourceObject\u0022 ).FileId ));\n\t}\n\n\t// A model\u0027s root GameObject: the legacy IDs, or the hashed path of the root node, as for RootTransformIds.\n\tstatic readonly HashSet\u003Clong\u003E RootGameObjectIds = new() { 100000, 100002, UnityFileId.Hash( \u0022Type:GameObject-\u003E//RootNode0\u0022 ), UnityFileId.Hash( \u0022Type:GameObject-\u003E//RootNode/root0\u0022 ) };\n\n\t// Component classes the conversion handles on an object of its own: those of AddComponents, and LOD groups (205), which\n\t// hide their lower levels\u0027 renderers in the file that holds them.\n\tstatic readonly HashSet\u003Cint\u003E ConvertedClasses = new() { 20, 23, 33, 64, 65, 82, 108, 135, 136, 137, 154, 205, 215, 218 };\n\n\t/// \u003Csummary\u003E\n\t/// Notes the changes a placed instance makes inside its prefab or model that the conversion does not apply. Components\n\t/// added to the source\u0027s root object are converted onto the placed object (see AddComponents), so only those added to\n\t/// objects deeper inside count: as warnings when the conversion would convert them on an object of their own, else like\n\t/// any other unconverted component type. An added LOD group always counts, as its renderers are inside the source, so\n\t/// every level shows. Also GameObjects removed from the source (m_RemovedGameObjects, Unity 2022.2\u002B).\n\t/// \u003C/summary\u003E\n\tvoid ReportUnappliedChanges( Document document, UnityYamlObject instance, string sourcePath, Func\u003Clong, bool\u003E isRoot, Summary summary )\n\t{\n\t\tvar modification = instance.Body[\u0022m_Modification\u0022];\n\t\tvar seen = new HashSet\u003Clong\u003E();\n\t\tvar lost = new List\u003Cstring\u003E();\n\t\tforeach ( var (_, parts, root) in AddedComponents( document, instance, isRoot ) )\n\t\t\tforeach ( var part in parts )\n\t\t\t{\n\t\t\t\tseen.Add( part.FileId );\n\t\t\t\tif ( root \u0026\u0026 part.ClassId != 205 ) continue;\n\t\t\t\tif ( ConvertedClasses.Contains( part.ClassId ) ) lost.Add( part.Type ?? $\u0022class {part.ClassId}\u0022 );\n\t\t\t\telse summary.Skip( part.Type ?? $\u0022class {part.ClassId}\u0022 );\n\t\t\t}\n\t\t// Unity 2022.2\u002B also lists added components; any not on a stripped copy here can\u0027t be placed.\n\t\tforeach ( var entry in modification?[\u0022m_AddedComponents\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E() )\n\t\t\tif ( Ref( entry, \u0022addedObject\u0022 ).FileId is var id \u0026\u0026 id != 0 \u0026\u0026 seen.Add( id ) \u0026\u0026 document.Objects.GetValueOrDefault( id ) is { } part )\n\t\t\t{\n\t\t\t\tif ( ConvertedClasses.Contains( part.ClassId ) ) lost.Add( part.Type ?? $\u0022class {part.ClassId}\u0022 );\n\t\t\t\telse summary.Skip( part.Type ?? $\u0022class {part.ClassId}\u0022 );\n\t\t\t}\n\t\tif ( lost.Count \u003E 0 )\n\t\t\tsummary.Notes.Add( $\u0022{lost.Count} component(s) added to objects inside an instance of \u0027{sourcePath}\u0027 are not converted: \u0022 \u002B\n\t\t\t\tstring.Join( \u0022, \u0022, lost.GroupBy( t =\u003E t ).Select( g =\u003E g.Count() \u003E 1 ? $\u0022{g.Key} \u00D7{g.Count()}\u0022 : g.Key ) ) \u002B \u0022.\u0022 );\n\t\tvar removedObjects = (modification?[\u0022m_RemovedGameObjects\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Count;\n\t\tif ( removedObjects \u003E 0 ) summary.Notes.Add( $\u0022{removedObjects} GameObject(s) removed from an instance of \u0027{sourcePath}\u0027 are still shown.\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EThe root GameObject of a nested prefab (0 for a variant, which has no root object of its own).\u003C/summary\u003E\n\tlong PrefabRootObject( string guid ) =\u003E Load( guid ) is { } source \u0026\u0026 source.RootTransforms.FirstOrDefault() is { } root ? source.GameObjectOf( root ) : 0;\n\n\t/// \u003Csummary\u003E\n\t/// A nested prefab or model placed by \u003Cparamref name=\u0022instance\u0022/\u003E. \u003Cparamref name=\u0022given\u0022/\u003E are the instance\u0027s\n\t/// modifications already merged with those of the files placing it, when it sits inside another instance.\n\t/// \u003C/summary\u003E\n\tJsonObject Instance( Document document, UnityYamlObject instance, Summary summary, string space, long salt, Overrides given = null )\n\t{\n\t\tvar source = Ref( instance.Body, \u0022m_SourcePrefab\u0022 );\n\t\tvar overrides = given ?? new Overrides( instance.Body[\u0022m_Modification\u0022]?[\u0022m_Modifications\u0022] );\n\t\tvar sourcePath = assets.GetValueOrDefault( source.Guid ?? \u0022\u0022 )?.Path ?? source.Guid;\n\t\tif ( models.TryGetValue( source.Guid ?? \u0022\u0022, out var model ) )\n\t\t{\n\t\t\tReportUnappliedChanges( document, instance, sourcePath, RootGameObjectIds.Contains, summary );\n\t\t\treturn model.Vmdl == null ? null : ModelInstance( document, instance, model, overrides, summary, space, salt );\n\t\t}\n\t\tif ( !assets.TryGetValue( source.Guid ?? \u0022\u0022, out var nested ) || !nested.Path.EndsWith( \u0022.prefab\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t{\n\t\t\tsummary.Notes.Add( $\u0022A nested prefab or model \u0027{sourcePath}\u0027 is not imported; it was left out.\u0022 );\n\t\t\treturn null;\n\t\t}\n\t\tvar nestedRoot = PrefabRootObject( nested.Guid );\n\t\tReportUnappliedChanges( document, instance, sourcePath, id =\u003E id == nestedRoot, summary );\n\t\t// Objects added under objects inside the nested prefab, and components added to its root, need the nested prefab\u0027s\n\t\t// content here, so it is inlined.\n\t\tvar addsInside = document.AddedUnder( instance.FileId ).Any() ||\n\t\t\tAddedComponents( document, instance, id =\u003E id == nestedRoot ).Any( a =\u003E a.Root \u0026\u0026 a.Parts.Any( p =\u003E ConvertedClasses.Contains( p.ClassId ) \u0026\u0026 p.ClassId != 205 ) );\n\t\tvar removed = instance.Body[\u0022m_Modification\u0022]?[\u0022m_RemovedComponents\u0022]?.Items.Count \u003E 0;\n\t\tif ( !prefabOutputs.TryGetValue( nested.Guid, out var path ) || addsInside || removed || given != null ) return Inline( document, instance, summary, space, salt, overrides );\n\t\tvar sourceDocument = Load( nested.Guid );\n\t\tvar sourceRoot = sourceDocument.RootTransforms.FirstOrDefault();\n\t\t// A prefab variant has no root object of its own to override, so it is copied in place with these overrides on top.\n\t\tif ( sourceRoot == null ) return Inline( document, instance, summary, space, salt, overrides );\n\t\tvar instanceId = Id( space, instance.FileId ^ salt, \u0022go\u0022 );\n\t\tvar rootGo = sourceDocument.GameObjectOf( sourceRoot );\n\t\tvar rootId = Id( sourceDocument.Guid, rootGo, \u0022go\u0022 );\n\t\tvar properties = new JsonArray();\n\t\tvoid Set( string type, string target, string property, JsonNode value ) =\u003E\n\t\t\tproperties.Add( new JsonObject { [\u0022Target\u0022] = new JsonObject { [\u0022Type\u0022] = type, [\u0022IdValue\u0022] = target }, [\u0022Property\u0022] = property, [\u0022Value\u0022] = value } );\n\t\tsummary.PrefabInstances\u002B\u002B;\n\t\t// Root: name and transform always, as s\u0026box prefab instances store them.\n\t\tvar rootGoOverrides = overrides.For( rootGo );\n\t\tSet( \u0022GameObject\u0022, rootId, \u0022Name\u0022, (Overrides.Text( rootGoOverrides, \u0022m_Name\u0022 ) ?? sourceDocument.Objects.GetValueOrDefault( rootGo )?.Body[\u0022m_Name\u0022]?.Value ?? nested.Path).Trim() );\n\t\tvar (position, rotation, scale) = UnityTransform.FromUnity( Local( sourceRoot.Body, overrides.For( sourceRoot.FileId ) ).Position, Local( sourceRoot.Body, overrides.For( sourceRoot.FileId ) ).Rotation, Local( sourceRoot.Body, overrides.For( sourceRoot.FileId ) ).Scale );\n\t\tSet( \u0022GameObject\u0022, rootId, \u0022Position\u0022, Vec( position ) ); Set( \u0022GameObject\u0022, rootId, \u0022Rotation\u0022, Vec( rotation ) ); Set( \u0022GameObject\u0022, rootId, \u0022Scale\u0022, Vec( scale ) );\n\t\tif ( Overrides.Number( rootGoOverrides, \u0022m_IsActive\u0022 ) is double rootActive ) Set( \u0022GameObject\u0022, rootId, \u0022Enabled\u0022, rootActive != 0 );\n\t\t// Other modifications on objects defined directly in the nested prefab.\n\t\tforeach ( var target in overrides.Targets.Where( t =\u003E t != rootGo \u0026\u0026 t != sourceRoot.FileId ) )\n\t\t{\n\t\t\tvar values = overrides.For( target );\n\t\t\tif ( !sourceDocument.Objects.TryGetValue( target, out var targeted ) || targeted.Stripped )\n\t\t\t{\n\t\t\t\tif ( values.Keys.Any( k =\u003E k.StartsWith( \u0022m_Materials\u0022, StringComparison.Ordinal ) || k.StartsWith( \u0022m_Local\u0022, StringComparison.Ordinal ) ) )\n\t\t\t\t\tsummary.Notes.Add( $\u0022A modification of an object nested deeper inside \u0027{nested.Path}\u0027 is not applied.\u0022 );\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tswitch ( targeted.ClassId )\n\t\t\t{\n\t\t\t\tcase 1:\n\t\t\t\t\tif ( Overrides.Text( values, \u0022m_Name\u0022 ) is string name ) Set( \u0022GameObject\u0022, Id( sourceDocument.Guid, target, \u0022go\u0022 ), \u0022Name\u0022, name.Trim() );\n\t\t\t\t\tif ( Overrides.Number( values, \u0022m_IsActive\u0022 ) is double active ) Set( \u0022GameObject\u0022, Id( sourceDocument.Guid, target, \u0022go\u0022 ), \u0022Enabled\u0022, active != 0 );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 4 or 224:\n\t\t\t\t\tif ( values.Keys.Any( k =\u003E k.StartsWith( \u0022m_Local\u0022, StringComparison.Ordinal ) ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar local = Local( targeted.Body, values );\n\t\t\t\t\t\tvar converted = UnityTransform.FromUnity( local.Position, local.Rotation, local.Scale );\n\t\t\t\t\t\tvar go = Id( sourceDocument.Guid, sourceDocument.GameObjectOf( targeted ), \u0022go\u0022 );\n\t\t\t\t\t\tSet( \u0022GameObject\u0022, go, \u0022Position\u0022, Vec( converted.Position ) ); Set( \u0022GameObject\u0022, go, \u0022Rotation\u0022, Vec( converted.Rotation ) ); Set( \u0022GameObject\u0022, go, \u0022Scale\u0022, Vec( converted.Scale ) );\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase 23 or 137:\n\t\t\t\t\tif ( values.Keys.Any( k =\u003E k.StartsWith( \u0022m_Materials\u0022, StringComparison.Ordinal ) ) \u0026\u0026 MaterialGroupFor( sourceDocument, targeted, values ) is var (group, known) \u0026\u0026 known )\n\t\t\t\t\t\tSet( \u0022Component\u0022, Id( sourceDocument.Guid, sourceDocument.GameObjectOf( targeted ), \u0022renderer\u0022 ), \u0022MaterialGroup\u0022, string.IsNullOrEmpty( group ) ? null : group );\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn PrefabReference( instanceId, path, rootId, properties );\n\t}\n\n\t(string Group, bool Known) MaterialGroupFor( Document document, UnityYamlObject renderer, IReadOnlyDictionary\u003Cstring, YamlNode\u003E values )\n\t{\n\t\tvar filter = (document.Components.GetValueOrDefault( document.GameObjectOf( renderer ) ) ?? new()).FirstOrDefault( c =\u003E c.ClassId == 33 );\n\t\tvar mesh = renderer.ClassId == 137 ? Ref( renderer.Body, \u0022m_Mesh\u0022 ) : Ref( filter?.Body, \u0022m_Mesh\u0022 );\n\t\tif ( !mesh.HasGuid || !models.TryGetValue( mesh.Guid, out var model ) || model.Info.ResolveMesh( mesh.FileId ) is not { } binding ) return (null, false);\n\t\tvar materials = (renderer.Body[\u0022m_Materials\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Select( m =\u003E m.Reference.Guid ).ToList();\n\t\tforeach ( var (path, m) in values )\n\t\t\tif ( Regex.Match( path, @\u0022^m_Materials\\.Array\\.data\\[([0-9]{1,6})\\]$\u0022 ) is { Success: true } slot \u0026\u0026 int.Parse( slot.Groups[1].Value ) is var index \u0026\u0026 index \u003C 64 )\n\t\t\t{\n\t\t\t\twhile ( materials.Count \u003C= index ) materials.Add( null );\n\t\t\t\tmaterials[index] = Ref( m, \u0022objectReference\u0022 ).Guid;\n\t\t\t}\n\t\tvar slots = new Dictionary\u003Cstring, string\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tfor ( var i = 0; i \u003C materials.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar name = binding.Slots.Length == 0 ? (i == 0 ? binding.Mesh : null) : i \u003C binding.Slots.Length ? binding.Slots[i] : null;\n\t\t\tif ( name != null \u0026\u0026 !string.IsNullOrEmpty( materials[i] ) ) slots[name] = materials[i];\n\t\t}\n\t\treturn (model.Group( slots ).Group, true);\n\t}\n\n\t/// \u003Csummary\u003EA nested prefab copied in place with the instance\u0027s modifications applied.\u003C/summary\u003E\n\tJsonObject Inline( Document document, UnityYamlObject instance, Summary summary, string space, long salt, Overrides given = null )\n\t{\n\t\tEnter();\n\t\ttry { return InlineBody( document, instance, summary, space, salt, given ); }\n\t\tfinally { nesting--; }\n\t}\n\n\tJsonObject InlineBody( Document document, UnityYamlObject instance, Summary summary, string space, long salt, Overrides given )\n\t{\n\t\tvar source = Ref( instance.Body, \u0022m_SourcePrefab\u0022 );\n\t\tif ( !assets.TryGetValue( source.Guid ?? \u0022\u0022, out var nested ) || !nested.Path.EndsWith( \u0022.prefab\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t\treturn models.ContainsKey( source.Guid ?? \u0022\u0022 ) ? ModelInstance( document, instance, models[source.Guid], given ?? new Overrides( instance.Body[\u0022m_Modification\u0022]?[\u0022m_Modifications\u0022] ), summary, space, salt )\n\t\t\t\t: Instance( document, instance, summary, space, salt, given );\n\t\tvar sourceDocument = Load( nested.Guid );\n\t\tvar sourceRoot = sourceDocument.RootTransforms.FirstOrDefault();\n\t\tvar overrides = given ?? new Overrides( instance.Body[\u0022m_Modification\u0022]?[\u0022m_Modifications\u0022] );\n\t\tsummary.InlinedInstances\u002B\u002B;\n\t\t// Unity names objects inside an instance (instance ID XOR source ID) with the sign bit cleared; the same derivation keeps GUIDs unique.\n\t\tvar nestedSalt = salt ^ instance.FileId;\n\t\tJsonObject result;\n\t\tif ( sourceRoot != null ) result = Node( sourceDocument, sourceRoot, summary, space, nestedSalt, overrides );\n\t\telse if ( sourceDocument.InstancesUnder( 0 ).FirstOrDefault() is { } variant )\n\t\t\tresult = Inline( sourceDocument, variant, summary, space, nestedSalt, new Overrides( variant.Body[\u0022m_Modification\u0022]?[\u0022m_Modifications\u0022] ).With( overrides, sourceDocument, variant.FileId, id =\u003E SourceHas( variant, id ) ) );\n\t\telse return null;\n\t\tif ( result == null ) return null;\n\t\tvar removed = (instance.Body[\u0022m_Modification\u0022]?[\u0022m_RemovedComponents\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E()).Count;\n\t\tif ( removed \u003E 0 ) summary.Notes.Add( $\u0022{removed} component(s) removed by an instance of \u0027{nested.Path}\u0027 are still shown.\u0022 );\n\t\t// Components this file adds to the nested prefab\u0027s root object go on the inlined copy\u0027s root.\n\t\tvar rootObject = sourceRoot != null ? sourceDocument.GameObjectOf( sourceRoot ) : 0;\n\t\tforeach ( var (go, parts, root) in AddedComponents( document, instance, id =\u003E id == rootObject ) )\n\t\t\tif ( root ) AddComponents( document, go.FileId, go, null, parts, result, null, summary, space, salt );\n\t\t// Objects this file adds under objects of the nested prefab go under the inlined copy\u0027s root.\n\t\tforeach ( var added in document.AddedUnder( instance.FileId ) )\n\t\t\tif ( Node( document, added, summary, space, salt ) is { } node ) result[\u0022Children\u0022].AsArray().Add( node );\n\t\treturn result;\n\t}\n\n\t// A model\u0027s root transform: the legacy IDs, or the hashed path of the root node (named \u0022root\u0022 when the file has one top node).\n\tstatic readonly HashSet\u003Clong\u003E RootTransformIds = new() { 400000, 400002, UnityFileId.Hash( \u0022Type:Transform-\u003E//RootNode/Transform0\u0022 ), UnityFileId.Hash( \u0022Type:Transform-\u003E//RootNode/root/Transform0\u0022 ) };\n\n\t/// \u003Csummary\u003EWhether the source of \u003Cparamref name=\u0022instance\u0022/\u003E (a prefab or model) has an object with this ID.\u003C/summary\u003E\n\tbool SourceHas( UnityYamlObject instance, long id )\n\t{\n\t\tvar source = Ref( instance.Body, \u0022m_SourcePrefab\u0022 ).Guid ?? \u0022\u0022;\n\t\tif ( models.TryGetValue( source, out var model ) )\n\t\t\treturn RootTransformIds.Contains( id ) || model.Info.MetadataObjectIds.Contains( id ) || model.Info.RendererSlots.ContainsKey( id ) || model.Info.ResolveMesh( id ) != null;\n\t\treturn assets.TryGetValue( source, out var prefab ) \u0026\u0026 prefab.Kind == UnityAssetKind.Prefab \u0026\u0026 Load( prefab.Guid ).Objects.ContainsKey( id );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The whole model\u0027s renderer (and its generated collider) on a placed model\u0027s root object. When the model\u0027s single\n\t/// top node is the root, the VMDL bakes that node\u0027s transform into the mesh, so the parts sit under its inverse.\n\t/// \u003C/summary\u003E\n\tvoid AddModel( JsonObject node, PrefabModel model, string group, Summary summary, string space, long id )\n\t{\n\t\tvar collapsed = model.Info.CollapsedRoot is { } top ? (model.Info.FileAxes * top).In( Basis( model ) ) : null;\n\t\tvar renderer = Component( \u0022ModelRenderer\u0022, Id( space, id, \u0022renderer\u0022 ), true );\n\t\trenderer[\u0022Model\u0022] = model.Vmdl; renderer[\u0022MaterialGroup\u0022] = string.IsNullOrEmpty( group ) ? null : group;\n\t\trenderer[\u0022Tint\u0022] = \u00221,1,1,1\u0022; renderer[\u0022RenderType\u0022] = \u0022On\u0022; renderer[\u0022CreateAttachments\u0022] = false;\n\t\tvar parts = new List\u003CJsonObject\u003E { renderer };\n\t\t// Unity\u0027s \u0022Generate Colliders\u0022 puts an exact collider on every mesh; the VMDL carries that physics mesh.\n\t\tif ( model.Info.GenerateColliders )\n\t\t{\n\t\t\tvar collider = Component( \u0022ModelCollider\u0022, Id( space, id, \u0022collider\u0022 ), true );\n\t\t\tcollider[\u0022Model\u0022] = model.Vmdl; collider[\u0022Static\u0022] = true; collider[\u0022IsTrigger\u0022] = false;\n\t\t\tparts.Add( collider );\n\t\t\tsummary.Colliders\u002B\u002B;\n\t\t}\n\t\tif ( collapsed == null || collapsed.IsIdentity() ) foreach ( var part in parts ) node[\u0022Components\u0022].AsArray().Add( part );\n\t\telse\n\t\t{\n\t\t\tvar holder = GameObject( Id( space, id, \u0022mesh\u0022 ), \u0022mesh\u0022, true );\n\t\t\tvar (position, rotation, scale) = collapsed.Inverse().Decompose();\n\t\t\tholder[\u0022Position\u0022] = Vec( position ); holder[\u0022Rotation\u0022] = Vec( rotation ); holder[\u0022Scale\u0022] = Vec( scale );\n\t\t\tforeach ( var part in parts ) holder[\u0022Components\u0022].AsArray().Add( part );\n\t\t\tnode[\u0022Children\u0022].AsArray().Add( holder );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA model placed as a prefab instance: the whole model\u0027s VMDL with the material group of its overrides.\u003C/summary\u003E\n\tJsonObject ModelInstance( Document document, UnityYamlObject instance, PrefabModel model, Overrides overrides, Summary summary, string space, long salt )\n\t{\n\t\t// The instance root: the stripped transform this file keeps for it, or the model\u0027s root transform IDs.\n\t\tvar rootIds = RootTransformIds;\n\t\tvar strippedRoot = document.StrippedTransformSources.GetValueOrDefault( instance.FileId ) ?? new List\u003Clong\u003E();\n\t\t// The root transform: a known root ID, else the transform carrying m_RootOrder (only an instance root has one),\n\t\t// else a stripped transform this file keeps for the instance, else the only transformed target.\n\t\tvar transformed = overrides.Targets.Where( t =\u003E overrides.For( t ).Keys.Any( k =\u003E k.StartsWith( \u0022m_Local\u0022, StringComparison.Ordinal ) || k == \u0022m_RootOrder\u0022 ) ).ToList();\n\t\tvar rootTransform = strippedRoot.Concat( transformed ).FirstOrDefault( rootIds.Contains, 0 );\n\t\tif ( rootTransform == 0 ) rootTransform = transformed.FirstOrDefault( t =\u003E overrides.For( t ).ContainsKey( \u0022m_RootOrder\u0022 ) );\n\t\tif ( rootTransform == 0 ) rootTransform = strippedRoot.FirstOrDefault();\n\t\tif ( rootTransform == 0 \u0026\u0026 transformed.Count == 1 ) rootTransform = transformed[0];\n\t\tvar named = overrides.Targets.FirstOrDefault( t =\u003E Overrides.Text( overrides.For( t ), \u0022m_Name\u0022 ) != null );\n\t\tvar name = overrides.RootName ?? (named != 0 ? Overrides.Text( overrides.For( named ), \u0022m_Name\u0022 ) : model.Name);\n\t\t// The root GameObject: the renamed target (Unity records the instance root\u0027s name), else the only GameObject target\n\t\t// that is switched on or off (not a transform or renderer of the model).\n\t\tvar activeTargets = overrides.Targets.Where( t =\u003E Overrides.Number( overrides.For( t ), \u0022m_IsActive\u0022 ) != null \u0026\u0026 !rootIds.Contains( t ) \u0026\u0026 !model.Info.RendererSlots.ContainsKey( t ) ).ToList();\n\t\tvar rootObject = named != 0 ? named : activeTargets.Count == 1 ? activeTargets[0] : 0;\n\t\tvar active = rootObject != 0 ? Overrides.Number( overrides.For( rootObject ), \u0022m_IsActive\u0022 ) ?? 1 : 1;\n\t\tvar node = GameObject( Id( space, instance.FileId ^ salt, \u0022go\u0022 ), name.Trim(), active != 0 );\n\t\t// A file with one top node: Unity makes that node the prefab root, so its transform is the root\u0027s default, which the\n\t\t// placement\u0027s overrides replace. The VMDL bakes it into the mesh, so the renderer sits under its inverse.\n\t\tvar collapsed = model.Info.CollapsedRoot is { } top ? (model.Info.FileAxes * top).In( Basis( model ) ) : null;\n\t\tSetTransform( node, Local( null, overrides.For( rootTransform ), collapsed == null ? null : UnityTransform.ToUnity( collapsed.Decompose() ) ) );\n\t\t// Material overrides on the model\u0027s renderers give the slot assignment for this placement.\n\t\tvar slots = new Dictionary\u003Cstring, string\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tforeach ( var target in overrides.Targets )\n\t\t{\n\t\t\tvar binding = model.Info.RendererSlots.GetValueOrDefault( target );\n\t\t\tforeach ( var (path, m) in overrides.For( target ) )\n\t\t\t\tif ( binding != null \u0026\u0026 Regex.Match( path, @\u0022^m_Materials\\.Array\\.data\\[([0-9]{1,6})\\]$\u0022 ) is { Success: true } slot \u0026\u0026 int.Parse( slot.Groups[1].Value ) is var index )\n\t\t\t\t{\n\t\t\t\t\tvar slotName = binding.Slots.Length == 0 ? (index == 0 ? binding.Mesh : null) : index \u003C binding.Slots.Length ? binding.Slots[index] : null;\n\t\t\t\t\tif ( slotName != null \u0026\u0026 Ref( m, \u0022objectReference\u0022 ).HasGuid ) slots[slotName] = Ref( m, \u0022objectReference\u0022 ).Guid;\n\t\t\t\t}\n\t\t}\n\t\tvar (group, exact) = model.Group( slots );\n\t\tif ( !exact ) summary.Notes.Add( $\u0022No material group of \u0027{model.Name}\u0027 matches an instance\u0027s materials exactly; the closest is used.\u0022 );\n\t\tAddModel( node, model, group, summary, space, instance.FileId ^ salt );\n\t\tsummary.ModelInstances\u002B\u002B;\n\t\t// Components this file adds to the model\u0027s root object (often a MeshCollider) go on the placed object.\n\t\tforeach ( var (go, parts, root) in AddedComponents( document, instance, RootGameObjectIds.Contains ) )\n\t\t\tif ( root ) AddComponents( document, go.FileId, go, null, parts, node, null, summary, space, salt );\n\t\tif ( overrides.Targets.Any( t =\u003E t != rootTransform \u0026\u0026 overrides.For( t ).Keys.Any( k =\u003E k.StartsWith( \u0022m_Local\u0022, StringComparison.Ordinal ) ) ) )\n\t\t\tsummary.Notes.Add( $\u0022Moved parts inside an instance of \u0027{model.Name}\u0027 keep their model positions.\u0022 );\n\t\t// Removed renderers matter only when their meshes are still in the VMDL (meshes no placement shows are left out of it).\n\t\tvar removedMeshes = (instance.Body[\u0022m_Modification\u0022]?[\u0022m_RemovedComponents\u0022]?.Items ?? Array.Empty\u003CYamlNode\u003E())\n\t\t\t.Select( r =\u003E model.Info.RendererSlots.GetValueOrDefault( r.Reference.FileId )?.Mesh ).Where( m =\u003E m != null \u0026\u0026 model.Info.ImportedMeshes.Contains( m ) ).Distinct().ToArray();\n\t\tif ( removedMeshes.Length \u003E 0 ) summary.Notes.Add( $\u0022Meshes removed from an instance of \u0027{model.Name}\u0027 are still shown: {string.Join( \u0022, \u0022, removedMeshes )}.\u0022 );\n\t\t// Objects this file adds under the model\u0027s objects.\n\t\tforeach ( var added in document.AddedUnder( instance.FileId ) )\n\t\t\tif ( Node( document, added, summary, space, salt ) is { } child ) node[\u0022Children\u0022].AsArray().Add( child );\n\t\treturn node;\n\t}\n}\n"}]}