2025-10-22 10:20:34 +08:00

432 lines
15 KiB
C#

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("<Skipped>");
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;
}
}
}
}