尝试解析 uasset 文件

最近需要解析 UnrealEngine 中的 uasset 文件,希望从中解析得到一些 meta 信息,如 object name,package path,UE version 等信息。unreal 的 python 接口并不能很准确获取到这些信息,且使用有较多限制,如它需要依赖一个完整的项目环境。最关键的是,当我们拿到一个孤立的 uasset 文件时,我们甚至不知道应该把它放置到项目中的哪个位置。而 UE Editor 中,一旦我们把 uasset 文件错误放置,若有其他 uasset 引用了它,轻则闪退,重则卡死。而即使我们位置摆放对了,它所依赖的 reference 若不存在,那么它的载入也会成为问题,比如它是一个 StaticMesh,那它所依赖的 Material 或 MaterialInstanceConstant 缺失时,会默认赋予它一个灰模材质,这并非我们希望的。

这里的解决办法是直接解析 uasset 文件的二进制内容,尝试从中提取一些需要的信息。

具体实现代码请见:https://github.com/ay27/uasset-parser-py

1. UE 中的文件序列化方法

UE 利用 C++ 的运行时反射,构建了一套文件的序列化和反序列化方法。相关的论述和整理已经有非常多的文章了,这里记录几个对我帮助较大的内容:

  1. https://www.jianshu.com/p/9fea500aaa4d
  2. http://www.aclockworkberry.com/custom-struct-serialization-for-networking-in-unreal-engine/
  3. https://zhuanlan.zhihu.com/p/617464719

简单总结是,绝大部分 UE 中的对象都直接或间接继承了 FArchive 基类,通过重载 << 符号,实现对象的序列化和反序列化过程。uasset 文件也是类似的一个对象序列化结果,只是它更复杂一些,被包裹在一个名为 Package 包中。

2. FLinkerLoad:加载 uasset 过程

从上面的文章大概知道,一个 uasset 文件的内容排布如下图。一个大概的猜想是,只要我们能把 File SummaryName Table 区正确解读,应该能够满足我们抽取 meta 信息的需求。

img

关键源码:

  1. Engine\Source\Runtime\CoreUObject\Public\UObject\LinkerLoad.h
  2. Engine\Source\Runtime\CoreUObject\Private\UObject\LinkerLoad.cpp

FLinkerLoad::Tick 函数是主要入口,在FLinkerLoad::ProcessPackageSummary函数里能看到完整的 uasset 文件分布情况:

image-20231018233551593

与参考文章中的分布一致,可以确认这里便是核心的读取过程了。

第一次看感觉很奇怪,这里 LinkerLoad 应用于加载过程,应该是反序列化才是,为何这里是“序列化”过程呢?具体原因可以看这里:双向的流导向符号

接着细读其中过程。

FLinkerLoad::SerializePackageFileSummary()
	FLinkerLoad::SerializePackageFileSummaryInternal()
		...
    	// Read summary from file.
		StructuredArchiveRootRecord.GetValue() << SA_VALUE(TEXT("Summary"), Summary);

这里读入整个 Summary 内容,因此 Summary 对象的类:FPackageFileSummary,就是对应前文里的 Summary Table。因此很顺利找到其序列化代码:

image-20231021164907350

我们可以对着其二进制内容尝试解读一下。注意这个 Tag 值是0x9E2A83C1,表明它是 little-endian,因此需要反序解读,如第三个 4 字节 60030000,需要拆为 4 份:60030000,然后反序拼起来得到:0x00000360,因此得到 864:

image-20231021174206171

而根据代码,可以解读出的信息有:

Tag=0x9E2A83C1
LegacyFileVersion=-8
LegacyUE3version=864
FileVersionUE4=522
FileVersionUE5=1009

对比源码定义,这些都是有效值,因此我们的解读是正确的。

于是接下来就可以照猫画虎,把这段 C++ 代码转写为 python,代码较长,请看这里:https://github.com/ay27/uasset-parser-py/blob/main/structures.py#L168

其中一个花了不少时间的坑是,在 UE 里,bool 的序列化是 4 字节的,而不是我们通常认为的 1 字节。在 FArchive 类中有绝大部分基础类型的序列化方式,如这里的 bool,或int16,int32,uint32 等等:

image-20231021164441224

接着,我们可以把 NameMapImportMapExportMap 以同样的方式实现出来。比如 NameMap,我们根据 Summary 中的 NameCountNameOffset 字段可以知道它的具体位置和数量,于是也能顺利把内容解读出来了:

image-20231021181703979

完整的解析代码请见我的 repo:https://github.com/ay27/uasset-parser-py

使用这份代码,可以把 uasset 文件中我需要的 meta 信息全部解读出来:

image-20231021175942979

3. 附:双向的流导向符号

UE 里所有序列化和反序列化的类都直接或间接的继承自 FArchive,FArchive 的一个很特别的设计是,<< 是双向的:

image-20231020205952962

如其中一个实现,当处于“序列化”阶段,则是 左 <-- 右 的方向;在“反序列化”阶段,则是 左 --> 右

image-20231020210008881