尝试解析 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++ 的运行时反射,构建了一套文件的序列化和反序列化方法。相关的论述和整理已经有非常多的文章了,这里记录几个对我帮助较大的内容:
- https://www.jianshu.com/p/9fea500aaa4d
- http://www.aclockworkberry.com/custom-struct-serialization-for-networking-in-unreal-engine/
- https://zhuanlan.zhihu.com/p/617464719
简单总结是,绝大部分 UE 中的对象都直接或间接继承了 FArchive
基类,通过重载 <<
符号,实现对象的序列化和反序列化过程。uasset 文件也是类似的一个对象序列化结果,只是它更复杂一些,被包裹在一个名为 Package
包中。
2. FLinkerLoad:加载 uasset 过程
从上面的文章大概知道,一个 uasset 文件的内容排布如下图。一个大概的猜想是,只要我们能把 File Summary
和 Name Table
区正确解读,应该能够满足我们抽取 meta 信息的需求。
关键源码:
Engine\Source\Runtime\CoreUObject\Public\UObject\LinkerLoad.h
Engine\Source\Runtime\CoreUObject\Private\UObject\LinkerLoad.cpp
FLinkerLoad::Tick
函数是主要入口,在FLinkerLoad::ProcessPackageSummary
函数里能看到完整的 uasset 文件分布情况:
与参考文章中的分布一致,可以确认这里便是核心的读取过程了。
第一次看感觉很奇怪,这里 LinkerLoad 应用于加载过程,应该是反序列化才是,为何这里是“序列化”过程呢?具体原因可以看这里:双向的流导向符号
接着细读其中过程。
FLinkerLoad::SerializePackageFileSummary()
FLinkerLoad::SerializePackageFileSummaryInternal()
...
// Read summary from file.
StructuredArchiveRootRecord.GetValue() << SA_VALUE(TEXT("Summary"), Summary);
这里读入整个 Summary 内容,因此 Summary 对象的类:FPackageFileSummary,就是对应前文里的 Summary Table。因此很顺利找到其序列化代码:
我们可以对着其二进制内容尝试解读一下。注意这个 Tag 值是0x9E2A83C1
,表明它是 little-endian,因此需要反序解读,如第三个 4 字节 60030000
,需要拆为 4 份:60
、03
、00
、00
,然后反序拼起来得到:0x00000360
,因此得到 864:
而根据代码,可以解读出的信息有:
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 等等:
接着,我们可以把 NameMap
、ImportMap
、ExportMap
以同样的方式实现出来。比如 NameMap
,我们根据 Summary
中的 NameCount
和 NameOffset
字段可以知道它的具体位置和数量,于是也能顺利把内容解读出来了:
完整的解析代码请见我的 repo:https://github.com/ay27/uasset-parser-py
使用这份代码,可以把 uasset 文件中我需要的 meta 信息全部解读出来:
3. 附:双向的流导向符号
UE 里所有序列化和反序列化的类都直接或间接的继承自 FArchive,FArchive 的一个很特别的设计是,<<
是双向的:
如其中一个实现,当处于“序列化”阶段,则是 左 <-- 右
的方向;在“反序列化”阶段,则是 左 --> 右
: