using System; using System.IO; using System.Text; namespace UnityFileApi { public class TextDumperTool { StringBuilder m_StringBuilder = new StringBuilder(1024); bool m_SkipLargeArrays; UnityFileReader m_Reader; SerializedFile m_SerializedFile; StreamWriter m_Writer; public int Dump(string path, string outputPath, bool skipLargeArrays) { m_SkipLargeArrays = skipLargeArrays; try { try { using var archive = UnityFileSystem.MountArchive(path, "/"); foreach (var node in archive.Nodes) { Console.WriteLine($"Processing {node.Path} {node.Size} {node.Flags}"); if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) { using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(node.Path) + ".txt"), false)) { OutputSerializedFile("/" + node.Path); } } } } catch (NotSupportedException) { // Try as SerializedFile using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(path) + ".txt"), false)) { OutputSerializedFile(path); } } } catch (Exception e) { Console.WriteLine("Error!"); Console.Write($"{e.GetType()}: "); Console.WriteLine(e.Message); Console.WriteLine(e.StackTrace); return 1; } return 0; } void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex = -1) { bool skipChildren = false; if (!node.IsArray) { m_StringBuilder.Append(' ', level * 2); if (level != 0) { m_StringBuilder.Append(node.Name); if (arrayIndex >= 0) { m_StringBuilder.Append('['); m_StringBuilder.Append(arrayIndex); m_StringBuilder.Append(']'); } m_StringBuilder.Append(' '); m_StringBuilder.Append('('); m_StringBuilder.Append(node.Type); m_StringBuilder.Append(')'); } else { m_StringBuilder.Append(node.Type); } // Basic data type. if (node.IsBasicType) { m_StringBuilder.Append(' '); m_StringBuilder.Append(ReadValue(node, offset)); offset += node.Size; } else if (node.Type == "string") { var stringSize = m_Reader.ReadInt32(offset); m_StringBuilder.Append(' '); m_StringBuilder.Append(m_Reader.ReadString(offset + 4, stringSize)); offset += stringSize + 4; // Skip child nodes as they were already processed here. skipChildren = true; } m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); if (node.IsManagedReferenceRegistry) { DumpManagedReferenceRegistry(node, ref offset, level + 1); // Skip child nodes as they were already processed here. skipChildren = true; } } else { DumpArray(node, ref offset, level); // Skip child nodes as they were already processed here. skipChildren = true; } if (!skipChildren) { foreach (var child in node.Children) { RecursiveDump(child, ref offset, level + 1); } } if ( ((int)node.MetaFlags & (int)TypeTreeMetaFlags.AlignBytes) != 0 || ((int)node.MetaFlags & (int)TypeTreeMetaFlags.AnyChildUsesAlignBytes) != 0 ) { offset = (offset + 3) & ~(3); } } void DumpArray(TypeTreeNode node, ref long offset, int level) { // First child contains array size. var sizeNode = node.Children[0]; // Second child contains array type information. var dataNode = node.Children[1]; if (sizeNode.Size != 4 || !sizeNode.IsLeaf) throw new Exception("Invalid array size"); var arraySize = m_Reader.ReadInt32(offset); offset += 4; m_StringBuilder.Append(' ', level * 2); m_StringBuilder.Append("Array"); m_StringBuilder.Append('<'); m_StringBuilder.Append(dataNode.Type); m_StringBuilder.Append(">["); m_StringBuilder.Append(arraySize); m_StringBuilder.Append(']'); m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); if (arraySize > 0) { if (dataNode.IsBasicType) { m_StringBuilder.Append(' ', (level + 1) * 2); if (arraySize > 256 && m_SkipLargeArrays) { m_StringBuilder.Append(""); offset += dataNode.Size * arraySize; } else { var array = ReadBasicTypeArray(dataNode, offset, arraySize); offset += dataNode.Size * arraySize; m_StringBuilder.Append(array.GetValue(0)); for (int i = 1; i < arraySize; ++i) { m_StringBuilder.Append(", "); m_StringBuilder.Append(array.GetValue(i)); } } m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); } else { ++level; for (int i = 0; i < arraySize; ++i) { RecursiveDump(dataNode, ref offset, level, i); } } } } void DumpManagedReferenceRegistry(TypeTreeNode node, ref long offset, int level) { if (node.Children.Count < 2) throw new Exception("Invalid ManagedReferenceRegistry"); // First child is version number. var version = m_Reader.ReadInt32(offset); RecursiveDump(node.Children[0], ref offset, level); TypeTreeNode refTypeNode; TypeTreeNode refObjData; if (version == 1) { // Second child is the ReferencedObject. var refObjNode = node.Children[1]; // And its children are the referenced type and data nodes. refTypeNode = refObjNode.Children[0]; refObjData = refObjNode.Children[1]; int i = 0; while (DumpManagedReferenceData(refTypeNode, refObjData, ref offset, level, i++)) { } } else if (version == 2) { // Second child is the RefIds vector. var refIdsVectorNode = node.Children[1]; if (refIdsVectorNode.Children.Count < 1 || refIdsVectorNode.Name != "RefIds") throw new Exception("Invalid ManagedReferenceRegistry RefIds vector"); var refIdsArrayNode = refIdsVectorNode.Children[0]; if (refIdsArrayNode.Children.Count != 2 || !refIdsArrayNode.IsArray) throw new Exception("Invalid ManagedReferenceRegistry RefIds array"); // First child is the array size. int arraySize = m_Reader.ReadInt32(offset); offset += 4; // Second child is the ReferencedObject. var refObjNode = refIdsArrayNode.Children[1]; for (int i = 0; i < arraySize; ++i) { // First child is the rid. long rid = m_Reader.ReadInt64(offset); offset += 8; // And the next children are the referenced type and data nodes. refTypeNode = refObjNode.Children[1]; refObjData = refObjNode.Children[2]; DumpManagedReferenceData(refTypeNode, refObjData, ref offset, level, rid); } } else { throw new Exception($"Unsupported ManagedReferenceRegistry version {version}"); } } bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedTypeDataNode, ref long offset, int level, long id) { if (refTypeNode.Children.Count < 3) throw new Exception("Invalid ReferencedManagedType"); m_StringBuilder.Append(' ', level * 2); m_StringBuilder.Append($"rid("); m_StringBuilder.Append(id); m_StringBuilder.Append(") ReferencedObject"); m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); ++level; var refTypeOffset = offset; var stringSize = m_Reader.ReadInt32(offset); var className = m_Reader.ReadString(offset + 4, stringSize); offset += stringSize + 4; offset = (offset + 3) & ~(3); stringSize = m_Reader.ReadInt32(offset); var namespaceName = m_Reader.ReadString(offset + 4, stringSize); offset += stringSize + 4; offset = (offset + 3) & ~(3); stringSize = m_Reader.ReadInt32(offset); var assemblyName = m_Reader.ReadString(offset + 4, stringSize); offset += stringSize + 4; offset = (offset + 3) & ~(3); if (className == "Terminus" && namespaceName == "UnityEngine.DMAT" && assemblyName == "FAKE_ASM") return false; // Not the most efficient way, but it simplifies the code. RecursiveDump(refTypeNode, ref refTypeOffset, level); m_StringBuilder.Append(' ', level * 2); m_StringBuilder.Append(referencedTypeDataNode.Name); m_StringBuilder.Append(' '); m_StringBuilder.Append(referencedTypeDataNode.Type); m_StringBuilder.Append(' '); m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); if (id == -1 || id == -2) { m_StringBuilder.Append(' ', level * 2); m_StringBuilder.Append(id == -1 ? " unknown" : " null"); m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); return true; } var refTypeRoot = m_SerializedFile.GetRefTypeTypeTreeRoot(className, namespaceName, assemblyName); // Dump the ReferencedObject using its own TypeTree, but skip the root. foreach (var child in refTypeRoot.Children) { RecursiveDump(child, ref offset, level + 1); } return true; } void OutputSerializedFile(string path) { using (m_Reader = new UnityFileReader(path, 64 * 1024 * 1024)) using (m_SerializedFile = UnityFileSystem.OpenSerializedFile(path)) { var i = 1; m_Writer.WriteLine("External References"); foreach (var extRef in m_SerializedFile.ExternalReferences) { m_Writer.WriteLine($"path({i}): \"{extRef.Path}\" GUID: {extRef.Guid} Type: {(int)extRef.Type}"); ++i; } m_Writer.WriteLine(); foreach (var obj in m_SerializedFile.Objects) { var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); var offset = obj.Offset; m_Writer.Write($"ID: {obj.Id} (ClassID: {obj.TypeId}) "); RecursiveDump(root, ref offset, 0); m_Writer.WriteLine(); } } } string ReadValue(TypeTreeNode node, long offset) { switch (Type.GetTypeCode(node.CSharpType)) { case TypeCode.Int32: return m_Reader.ReadInt32(offset).ToString(); case TypeCode.UInt32: return m_Reader.ReadUInt32(offset).ToString(); case TypeCode.Single: return m_Reader.ReadFloat(offset).ToString(); case TypeCode.Double: return m_Reader.ReadDouble(offset).ToString(); case TypeCode.Int16: return m_Reader.ReadInt16(offset).ToString(); case TypeCode.UInt16: return m_Reader.ReadUInt16(offset).ToString(); case TypeCode.Int64: return m_Reader.ReadInt64(offset).ToString(); case TypeCode.UInt64: return m_Reader.ReadUInt64(offset).ToString(); case TypeCode.SByte: return m_Reader.ReadUInt8(offset).ToString(); case TypeCode.Byte: case TypeCode.Char: return m_Reader.ReadUInt8(offset).ToString(); case TypeCode.Boolean: return (m_Reader.ReadUInt8(offset) != 0).ToString(); default: throw new Exception($"Can't get value of {node.Type} type"); } } Array ReadBasicTypeArray(TypeTreeNode node, long offset, int arraySize) { // Special case for boolean arrays. if (node.CSharpType == typeof(bool)) { var tmpArray = new byte[arraySize]; var boolArray = new bool[arraySize]; m_Reader.ReadArray(offset, arraySize * node.Size, tmpArray); for (int i = 0; i < arraySize; ++i) { boolArray[i] = tmpArray[i] != 0; } return boolArray; } else { var array = Array.CreateInstance(node.CSharpType, arraySize); m_Reader.ReadArray(offset, arraySize * node.Size, array); return array; } } } }