parser 与元数据:.psc 反射、XML/RBF/PSO、.meta 加载
RAGE parser 是贯穿整个代码库的反射与序列化系统:游戏几乎所有可调数据(.meta 文件、tunables、ped/vehicle/weapon 配置、地图 ymap/ytyp 的元数据部分、存档的若干结构)都由 .psc 模式描述,经代码生成为 parStructure/parMember 元数据,再由统一的加载器读入 XML、RBF 或 PSO 二进制。本页解释该系统的核心类型、代码生成契约、三种文件格式的读写路径、版本兼容与 PSO 就地加载,并以 pedhealth.meta 为例端到端走一遍。
背景与相关页面#
.psc的构建步骤(projgen 如何调用parCodeGen、生成文件放在哪里)见 00-overview/build-and-config.md。- 游戏侧元数据存储(
fwMetaDataStore、fwPsoStoreLoader、ymt/ymap/ytyp资源流式加载)见 04-rage-framework/framework-systems.md 与 05-world/map-data-and-lod.md。本页只说明它们调用的 parser API。 - 资源(
pgBase、datResource)机制见 paging-and-resources.md;PSO 资源形式psoResourceData继承自pgBasePlatformNeutral。 - 哈希算法(
atLiteralStringHash/atStringHash)与atHashString见 math-and-containers.md。 - bank 控件见 system-and-diagnostics.md。
相关源码文件#
| 区域 | 关键文件(相对 src/dev_ng/rage/base/) |
|---|---|
| 宏与入口 | src/parser/macros.h、src/parser/manager.h/.cpp、src/parser/codegenutils.h |
| 反射核心 | src/parser/structure.h/.cpp、src/parser/member*.h/.cpp、src/parser/memberdata.h、src/parser/rtstructure.h、src/parser/extstructure.h |
| DOM 与访问者 | src/parser/tree.h、src/parser/treenode.h、src/parser/visitor*.h/.cpp |
| 流格式(parsercore) | src/parsercore/stream.h、streamxml.h/.cpp、streamrbf.cpp、settings.h、versioninfo.h/.cpp、attribute.h、element.h |
| PSO | src/parser/psoconsts.h、psofile.h/.cpp、psofile_fixup.cpp、psofile_parser.cpp、psodata.h、psoresource.h、psoschema*.h、psobuilder*.h、psorscbuilder*.h、ifffile.h |
| 代码生成器 | tools/cli/parCodeGen/parCodeGen.py、structtype.py、basetype.py、arraytype.py、template_runtimemetadata.py、atHash.py |
| 示例 | src/dev_ng/game/Peds/HealthConfig.psc/.h/.cpp、src/dev_ng/game/scene/FileLoader.cpp |
1. 核心概念#
| 概念 | 类型 | 职责 | 证据 |
|---|---|---|---|
| 解析器单例 | parManager,宏 PARSER | 注册表(结构/枚举)、格式注册、LoadObject/SaveObject/LoadTree、设置 | src/dev_ng/rage/base/src/parser/macros.h:72-102、src/dev_ng/rage/base/src/parser/manager.h:53-478 |
| 结构描述 | parStructure | 一个 C++ 类的反射:名称哈希、基类与偏移、m_Size、标志、版本、成员数组、工厂/放置/析构委托、命名委托表(PreLoad/PostLoad/PreSet/PostSet/...) | src/dev_ng/rage/base/src/parser/structure.h:30-60、src/dev_ng/rage/base/src/parser/structure.h:302-329 |
| 成员描述 | parMember 及子类 parMemberSimple/Vector/Matrix/String/Struct/Array/Enum/Map/Bitset | 每个成员一份静态 Data(名称哈希、偏移、类型、子类型、标志、访问者标志、额外属性) | src/dev_ng/rage/base/src/parser/memberdata.h:26-36 |
| 运行时结构 | parRTStructure | 不经代码生成、运行时临时拼装的结构(绝对地址成员),用于脚本/工具 | src/dev_ng/rage/base/src/parser/rtstructure.h:34-60 |
| 中间 DOM | parTree / parTreeNode(parElement + parAttribute) | XML/RBF 读入后的树,LoadObject 先建树再映射到对象 | src/dev_ng/rage/base/src/parser/tree.h:18、src/dev_ng/rage/base/src/parser/treenode.h:35 |
| 流 | parStreamIn/parStreamOut,parStreamInXml、parStreamInRbf | 逐元素读写,支持回调式读取 ReadWithCallbacks | src/dev_ng/rage/base/src/parsercore/stream.h:104-220、src/dev_ng/rage/base/src/parsercore/streamxml.h:24-44 |
| 访问者 | parInstanceVisitor 派生:parInitVisitor(按 init= 初始化)、parBuildTreeVisitor、parXmlWriterVisitor、parBuildWidgetsVisitor(生成 bank 控件)、parDeepCopyVisitor、parDeleteAllVisitor、parValidateVisitor 等 | 对任意已反射对象做通用遍历 | src/dev_ng/rage/base/src/parser/visitor.h:94、src/dev_ng/rage/base/src/parser/visitorinit.h:18、src/dev_ng/rage/base/src/parser/visitortree.h:34、src/dev_ng/rage/base/src/parser/visitorxml.h:19、src/dev_ng/rage/base/src/parser/visitorwidgets.h:19、src/dev_ng/rage/base/src/parser/visitorutils.h:310-375、src/dev_ng/rage/base/src/parser/visitorvalidate.h:35 |
| PSO | psoFile、psoStruct、psoStructId、psoSchemaCatalog、psoResourceData | 二进制序列化实例 + 模式目录 + 签名,可就地加载 | src/dev_ng/rage/base/src/parser/psofile.h:191-300 |
1.1 成员类型#
parMemberType::Enum 列出所有可反射的原子/复合类型:bool、8/16/32/64 位整型、float/double/float16、旧数学库 Vector2/3/4、Matrix34/44,vectormath 的 Vec2V/Vec3V/Vec4V/Mat33V/Mat34V/Mat44V/ScalarV/BoolV/VecBoolV、size_t/ptrdiff_t,以及复合的 STRING/STRUCT/ARRAY/ENUM/BITSET/MAP src/dev_ng/rage/base/src/parser/memberdata.h:69-113。复合类型通过子类型区分具体容器:
| 类型 | 子类型 | 证据 |
|---|---|---|
| ARRAY | ATARRAY、ATFIXEDARRAY、ATRANGEARRAY、POINTER、MEMBER(C 数组)、ATARRAY_32BIT_IDX、POINTER_WITH_COUNT(_8/_16BIT_IDX)、VIRTUAL | src/dev_ng/rage/base/src/parser/memberarraydata.h:27-36 |
| MAP | ATMAP、ATBINARYMAP | src/dev_ng/rage/base/src/parser/membermapdata.h:24-25 |
| STRING | MEMBER、POINTER、CONST_STRING、ATSTRING、宽字符三种、ATNONFINALHASHSTRING、ATFINALHASHSTRING、ATHASHVALUE、ATPARTIALHASHVALUE、ATNSHASHSTRING、ATNSHASHVALUE | src/dev_ng/rage/base/src/parser/memberstringdata.h:22-34 |
| STRUCT | STRUCTURE(内嵌)、POINTER(拥有)、SIMPLE_POINTER、EXTERNAL_NAMED_POINTER(_USERNULL)(按名字引用外部对象) | src/dev_ng/rage/base/src/parser/memberstructdata.h:21-25 |
| ENUM / BITSET | 8/16/32 位;bitset 有固定宽度与 atBitSet | src/dev_ng/rage/base/src/parser/memberenumdata.h:21-23、src/dev_ng/rage/base/src/parser/memberenumdata.h:107-110 |
成员标志 parMemberFlags(MEMBER_STATIC/READONLY/NO_INIT/ADD_GROUP_WIDGET 及 4 位数组对齐)与访问者掩码 parMemberVisitorFlags(VISIT_FILE/NET/SCRIPT/TOOL/BANK,游戏可从 VISIT_USER0=8 起自定义)控制某成员是否被某类访问者看到 src/dev_ng/rage/base/src/parser/memberdata.h:43-171。
1.2 名称与构建配置#
PARSER_ALL_METADATA_HAS_NAMES = __BANK || __DEV || __RESOURCECOMPILER || !__NO_OUTPUT || __TOOL:Final(无输出)构建中结构与成员只保留哈希不保留字符串,除非.psc中标记preserveNames="true"src/dev_ng/rage/base/src/parser/macros.h:14-15。parStructure::m_NameHash因此在两种构建中分别是atLiteralHashString/atLiteralHashValuesrc/dev_ng/rage/base/src/parser/structure.h:304-308。PARSER_USES_EXTERNAL_STRUCTURE_DEFNS(__DEV/资源编译器/工具)允许parStructure自身也可反射,支持-parsercatalog输出所有结构定义src/dev_ng/rage/base/src/parser/macros.h:17-18、src/dev_ng/rage/base/src/parser/manager.cpp:71-84。- SPU 上 parser 整体编译为空
src/dev_ng/rage/base/src/parser/macros.h:26-36。
2. PAR_PARSABLE 与代码生成契约#
2.1 类内宏#
PAR_PARSABLE 向类注入:静态 parser_Register()、虚函数 parser_GetStructure()(用于多态:基类指针取到派生类结构)、parser_GetPointer()、静态 parser_Data(parStructureStaticData)与 parser_MemberOffsets[]。PAR_SIMPLE_PARSABLE 相同但 parser_GetStructure() 非虚,适用于无 vtable 的 POD 类,且不能作为可解析基类 src/dev_ng/rage/base/src/parser/macros.h:127-172。 parStructureStaticData 保存名称哈希、名称、构建后的 parStructure*、成员数据表、偏移表、成员名表、是否可继承、是否总保留名字 src/dev_ng/rage/base/src/parser/macros.h:215-229。
2.2 parCodeGen:.psc → _parser.h#
parCodeGen.py(打包为 tools_ng/bin/coding/python/parCodeGen.exe)读取 .psc(XML,根元素 ParserSchema),默认生成同名 <base>_parser.h 与 <base>_parser.xsd;当 .psc 要求生成类定义时还会生成 <base>.h/.cpp;若声明脚本导出则写入 game/script_headers 下的 .sch;只有当输入或生成器自身更新时才重新生成 src/dev_ng/rage/base/tools/cli/parCodeGen/parCodeGen.py:239-261、 src/dev_ng/rage/base/tools/cli/parCodeGen/parCodeGen.py:330-433。
生成的 _parser.h 由对应 .cpp 在顶部 #include,内容(由 template_runtimemetadata.py 输出):
- 每个成员一个静态
parMember???::Data初始化器,及void* const parser_<Mangled>__Members[](以 NULL 结尾)。 parser_MemberOffsets[]:对每个成员取地址偏移——若.psc中的成员在 C++ 类中不存在,此处编译失败,这是.psc与.h一致性的编译期校验。- 在
PARSER_ALL_METADATA_HAS_NAMES(或preserveNames)时输出成员名表与结构名。 parser_Data初始化(NameHash由生成器预先算好)。parser_Register():已注册则返回;否则parCguStructure::CreateStructure(sizeof(T)),按constructible设置工厂/放置/析构函数(atPtrCreator、atPlacePtrCreator、parCguDestructor),设置GetStructure回调,按.psc的onPreLoad/onPostLoad/onPreSet/onPostSet/onPreAddWidgets...安装委托,最后BuildStructureFromStaticData(newStructure, parser_Data, majorVersion, buildNumber)。- 文件末尾为每个
autoregister的结构生成全局parCguAutoRegistrationNode autoreg_X(&X::parser_Register),可带#if cond。
证据:src/dev_ng/rage/base/tools/cli/parCodeGen/template_runtimemetadata.py:210-300、src/dev_ng/rage/base/tools/cli/parCodeGen/template_runtimemetadata.py:395-406、src/dev_ng/rage/base/tools/cli/parCodeGen/template_runtimemetadata.py:520-540。
parCguAutoRegistrationNode 的构造函数把自身压入静态链表 sm_Head src/dev_ng/rage/base/src/parser/codegenutils.h:138-150, parManager::RegisterAutoStructs() 在初始化时遍历链表调用所有注册函数,然后清空链表;期间临时关闭“乱序注册/重复注册”警告 src/dev_ng/rage/base/src/parser/manager.cpp:180-215。
2.3 名称规则(XML 元素名 ⇄ 成员)#
- 结构的解析器名默认是 C++ 类型名的 mangled 形式(
::→__),可用name=覆盖;NameHash = atLiteralStringHash(ParserName)src/dev_ng/rage/base/tools/cli/parCodeGen/structtype.py:150-165。 - 成员名默认去掉
m_前缀(若剩余部分是合法 XML 名),可用parName=覆盖src/dev_ng/rage/base/tools/cli/parCodeGen/basetype.py:372-385、src/dev_ng/rage/base/tools/cli/parCodeGen/basetype.py:405-422。 - 数组元素子节点统一命名为
Itemsrc/dev_ng/rage/base/tools/cli/parCodeGen/arraytype.py:20-24。 - 同一 structdef 内两个成员名或名称哈希相同会在生成期报错
src/dev_ng/rage/base/tools/cli/parCodeGen/structtype.py:172-179。 structdef支持的属性包括type, version, base, constructible, autoregister, template, cond, rsc, preserveNames, generate, name, layoutHint, simple, platform, usedi与各种on*回调src/dev_ng/rage/base/tools/cli/parCodeGen/structtype.py:100-126、src/dev_ng/rage/base/tools/cli/parCodeGen/structtype.py:191-200。
2.4 头文件过期检测#
非 Final、非 Orbis 构建中 PAR_CHECK_OUT_OF_DATE_HEADER / PAR_HEADER_GUID 通过“引用一个只在新生成文件中定义的符号”把头文件与生成代码不同步变成链接错误 src/dev_ng/rage/base/src/parser/macros.h:194-213。
3. 初始化与注册#
INIT_PARSER → 若无单例则 new parManager → parSettings::CreateStandardSettings() → PARSER.Initialize(settings, true);Initialize 有引用计数,只有第一次调用会注册内建格式、执行自动注册与外部结构初始化;SHUTDOWN_PARSER 对称递减,归零时删除所有注册结构 src/dev_ng/rage/base/src/parser/macros.h:75-102、 src/dev_ng/rage/base/src/parser/manager.cpp:54-122。游戏最早的调用位于 CSystem 初始化中(注释说明 parser 必须先于 fwConfigManager 初始化) src/dev_ng/game/system/system.cpp:469-470;此后大量子系统在使用前各自再调用 INIT_PARSER(例如 game/system/control.cpp、src/dev_ng/game/control/leveldata.cpp:197、src/dev_ng/game/text/FontDef.cpp:62),依赖这一引用计数语义(由 rg INIT_PARSER 得出)。
内建格式注册:
| 格式 | 头部检测 | 读写实现 | 证据 |
|---|---|---|---|
XML | parStreamInXml::IsValidXmlStart | parStreamInXml / parStreamOutXml | src/dev_ng/rage/base/src/parser/manager.cpp:143-156 |
BINARY(RBF) | 前 4 字节 "RBF0" | parStreamInRbf / parStreamOutRbf | src/dev_ng/rage/base/src/parser/manager.cpp:159-171 |
PSO | 前 4 字节 "PSIN" | 仅用于识别;LoadTree/OpenInputStream 遇到 PSO 返回 NULL,需走 pso* 专用 API | src/dev_ng/rage/base/src/parser/manager.cpp:173-177、src/dev_ng/rage/base/src/parser/manager.cpp:320-323 |
格式枚举 parManager::Format { XML, BINARY, PSO, ... } 见 src/dev_ng/rage/base/src/parser/manager.h:243-248。
4. XML/RBF 读路径(PARSER.LoadObject)#
图证据:LoadObject 模板 src/dev_ng/rage/base/src/parser/manager.h:559-560;LoadTree src/dev_ng/rage/base/src/parser/manager.cpp:305-358;FindInputFormat src/dev_ng/rage/base/src/parser/manager.cpp:551-575;LoadFromStructure src/dev_ng/rage/base/src/parser/manager.cpp:643-725;ReadMembersFromTree src/dev_ng/rage/base/src/parser/structure.cpp:309-412。
关键行为:
- 先建 DOM 再映射:XML/RBF 路径总是先把整个文件读成
parTree(使用AutoUseTempMemory临时堆),再写入对象,最后删除树。LoadTree可在设置位允许时处理 XInclude 与“剔除其它平台数据”节点src/dev_ng/rage/base/src/parser/manager.cpp:336-345。 - 类型校验:根元素名必须是已注册结构,且目标结构
IsSubclassOf文件结构;LOAD_UNKNOWN_TYPES设置可放宽src/dev_ng/rage/base/src/parser/manager.cpp:652-669。 - 成员匹配:从最远的基类开始,对每个成员用
atLiteralStringHash(元素名)与GetNameHash()比较;搜索从上一个匹配节点的下一个开始环形查找,因此文件中元素顺序与声明顺序一致时为线性复杂度,乱序也能加载;断言检查哈希碰撞src/dev_ng/rage/base/src/parser/structure.cpp:347-382。 - 缺失与多余数据:
WARN_ON_MISSING_DATA/INITIALIZE_MISSING_DATA(用parInitVisitor按.psc的init=值初始化)/WARN_ON_UNUSED_DATAsrc/dev_ng/rage/base/src/parser/structure.cpp:384-405、src/dev_ng/rage/base/src/parsercore/settings.h:41-70。 - 回调与线程:
PostLoad受 TLSparStructure::sm_SkipPostloadCallbacks控制——非主线程加载时可跳过、稍后补调src/dev_ng/rage/base/src/parser/structure.h:292-293、src/dev_ng/rage/base/src/parser/structure.cpp:408-411。 - 补丁文件已停用:
LoadPatchFile若在content.xml补丁列表中找到对应文件,会断言并忽略(注释说明 NG 不再应用补丁),旧的 REST 风格补丁代码被注释掉src/dev_ng/rage/base/src/parser/manager.cpp:727-750。 LoadObjectPtr由 parser 依据文件中的类型分配对象(要求传入 NULL 指针),InitAndLoadObject先InitObject再加载src/dev_ng/rage/base/src/parser/manager.h:513-607。
写路径 SaveObject → SaveFromStructure:同样先 BuildTreeNodeFromStructure 构建树再 SaveTree;保存到文件时会尝试版本控制签出 src/dev_ng/rage/base/src/parser/manager.h:638-640、src/dev_ng/rage/base/src/parser/manager.cpp:1075-1120。
4.1 XML 方言#
简单类型以 value 属性写出(如 <DefaultHealth value="200.0"/>) src/dev_ng/rage/base/src/parser/visitorxml.cpp:44;结构版本写为 v="major.minor" 属性;多态指针用 type= 属性指定实际类型 src/dev_ng/rage/base/src/parser/memberstruct.cpp:253。 PROCESS_SPECIAL_ATTRS 设置决定 v=、content=、value= 是否被特殊处理 src/dev_ng/rage/base/src/parsercore/settings.h:28-31。
4.2 RBF(Rockstar Binary Format)#
记录式二进制:记录 ID 的高 4 位为记录类型(CONTAINER/INT/BOOL_TRUE/BOOL_FALSE/FLOAT/VECTOR3/STRING,0xF 为特殊记录),低 12 位为名字表 ID;特殊 ID 0xFFFF 弹出、0xFFFE 扩展、0xFFFD 数据容器、0xFFFC 注释 src/dev_ng/rage/base/src/parsercore/streamrbf.cpp:27-56。数据容器支持 ASCII/UTF16/二进制与各类数值数组、矩阵编码(src/dev_ng/rage/base/src/parsercore/streamrbf.cpp:416-497)。
4.3 版本兼容#
parVersionInfo{u16 major, u16 minor}:文件版本 0.0 视为未版本化;主版本不同 → INCOMPATABLE;主版本相同则报告 FILE_IS_NEWER / CODE_IS_NEWER / EXACT_MATCH src/dev_ng/rage/base/src/parsercore/versioninfo.h:13-66、src/dev_ng/rage/base/src/parsercore/versioninfo.cpp:47-60。 ReadMembersFromTree 仅在结构主版本 > 0 时检查 v 属性:次版本差异只按 WARN_ON_MINOR_VERSION_MISMATCH 警告,主版本不兼容则断言并跳过整个结构 src/dev_ng/rage/base/src/parser/structure.cpp:277-323。除此之外的兼容手段是“按名称匹配 + 缺失成员初始化 + 多余元素忽略”。仓库中未找到名为 parRestructure 的类型(rg parRestructure 在 parser/parsercore 中无结果)。
5. PSO(Parser Serialized Object)#
5.1 文件结构#
PSO 是 IFF 风格的块文件:每块 8 字节头 {u32 magic(BE), u32 size(BE)} src/dev_ng/rage/base/src/parser/ifffile.h:18-28。块类型:
| 魔数 | 内容 |
|---|---|
PSIN | 序列化实例数据(必须位于文件开头,用于格式识别) |
PMAP | 结构数组映射表(每个结构数组的类型、偏移、大小,及根对象 ID) |
PSCH | 模式目录(结构与枚举的布局描述) |
PSIG | 结构签名 |
STRF / STRS / STRE | Final 字符串 / 调试字符串 / 加密的调试字符串 |
CHKS | 校验和 |
证据:src/dev_ng/rage/base/src/parser/psoconsts.h:52-61。资源化(放入 RPF 的 pgBase 资源)形式为 psoResourceData,标识 'PRD0',记录创建平台(决定端序与指针宽度)、校验和、根对象 ID、结构/枚举模式表与结构数组表,RESOURCE_VERSION = 2;ENCRYPT_NONFINAL_STRINGS 标志表示非 Final 字符串被加密(存在一个名为 NonfinalStringKey 的非 Final 密钥常量,本页不记录其值) src/dev_ng/rage/base/src/parser/psoresource.h:26-88。
对象引用不用指针而用 psoStructId:12 位结构数组表索引 + 20 位字节偏移(OBJECT_IDENTIFIER_*_BITS),表索引存储时加 1,使 0 表示空 src/dev_ng/rage/base/src/parser/psoconsts.h:43-44、src/dev_ng/rage/base/src/parser/psodata.h:245-251。类型 ID 空间:0–255 为 POD 类型、256–3327 为文件内匿名类型、3328–3583 为命名空间哈希串、3584–4095 保留,≥4096 为类型名的字面哈希(因此类型名哈希若 < 4096 不被允许) src/dev_ng/rage/base/src/parser/psoconsts.h:21-37。
5.2 加载 API 与就地加载#
psoLoadFile / psoLoadFileFromBuffer / psoLoadResource / psoInitFromResource返回psoFile*;psoLoadAction决定是否在加载时与运行时结构比较(PSOLOAD_PREP_FOR_PARSER_LOADING)或直接假定全部匹配(PSOLOAD_ASSUME_IN_PLACE_LOADING);psoLoadFileStatus枚举报告缺块、校验和缺失/不匹配、大小不符等失败src/dev_ng/rage/base/src/parser/psofile.h:33-77、src/dev_ng/rage/base/src/parser/psofile.cpp:486-519。psoFile::InitFlags:NO_BYTE_SWAP_DATA、NO_INIT_HASH_STRING、IGNORE_CHECKSUM、REQUIRE_CHECKSUMsrc/dev_ng/rage/base/src/parser/psofile.h:204-210。- 签名匹配:
CompareSchemasWithRuntime对文件中每个非匿名结构模式,找到同名哈希的运行时parStructure,用psoBuildSchemaFromStructure从运行时结构构建临时模式并计算签名(缓存为结构的额外属性psosig);签名一致则给该模式设FLAG_USE_FAST_LOADING,含虚函数的再设FLAG_NEEDS_VPTR_FIXUP。不可构造或标记NOT_IN_PLACE_LOADABLE的结构签名记为 0(永不快速加载)src/dev_ng/rage/base/src/parser/psofile.cpp:608-730。 psoLoadObjectInPlace(file, inPlaceHeap, notInPlaceHeap):所有模式都匹配时做就地修复——PSO_DONT_ALLOCATE直接在 PSO 缓冲中把psoStructId改回真实指针(DirectFixupMatchingPsoContents),否则复制到指定堆再修复(只支持 IFF 文件,不支持资源化 PSO);不匹配时退化为逐对象构造(notInPlaceHeap),若其为PSO_DONT_ALLOCATE则失败src/dev_ng/rage/base/src/parser/psofile.h:145-187、src/dev_ng/rage/base/src/parser/psofile.cpp:922-990。- 也可走
psoLoadObject(file, obj)→psoLoadFromStructure,与 XML 路径一样逐成员写入现有对象src/dev_ng/rage/base/src/parser/psofile.h:90-128、src/dev_ng/rage/base/src/parser/psofile.h:363-371。 - 32/64 位:
PSO_FORCE_32BIT = 0,PSO_USING_NATIVE_BIT_SIZE宏用于屏蔽非原生位宽下不可用的 APIsrc/dev_ng/rage/base/src/parser/macros.h:20-24。
PSO 的构建代码位于 psobuilder*、psorscbuilder*、psoschemabuilder*(推断:离线由资产流水线调用这些构建器把 XML 转成 PSO,本页未追踪具体工具入口,见 24-toolchain/asset-pipeline.md);psoRscBuilder 也在运行时用于计算签名(见上)。
5.3 framework 侧调用(简述)#
fwPsoStoreLoader::Load 用前 4 字节是否为 PSIN 判断 PSO(注释自称“HACK”),PSO 走 LoadPsoFile(psoLoadFileFromBuffer + psoLoadObjectInPlace,布局不匹配时按 WARN_/ASSERT_ON_LAYOUT_MISMATCH 或 -psoMismatchIsFatal 处理),否则若允许 XML 则包装成 memory: 设备文件后走 PARSER.CreateAndLoadAnyType src/dev_ng/rage/framework/src/fwscene/stores/psostore.cpp:145-175、 src/dev_ng/rage/framework/src/fwscene/stores/psostore.cpp:250-350、 src/dev_ng/rage/framework/src/fwscene/stores/psostore.cpp:454-485。 fwMetaDataStore 默认开启 LOAD_XML_FILES | LOAD_IN_PLACE | WARN_ON_LAYOUT_MISMATCH src/dev_ng/rage/framework/src/fwsys/metadatastore.cpp:32-35。详见 04-rage-framework/framework-systems.md。
6. 端到端示例:pedhealth.meta 与 CHealthConfigInfo#
1) 模式:game/Peds/HealthConfig.psc 定义两个结构。CHealthConfigInfo 有 atHashString 类型的 m_Name(ui_key="true")、若干带 init/min/max 的 float 阈值与两个 bool;CHealthConfigInfoManager 有 atArray<CHealthConfigInfo> m_aHealthConfig 和一个 policy="external_named" 的指针 m_DefaultSet,其 toString/fromString 指向管理器静态函数 src/dev_ng/game/Peds/HealthConfig.psc:4-24。
2) C++ 类:两类均以 PAR_SIMPLE_PARSABLE 声明(无虚函数),成员与 .psc 一一对应;m_Name 的 C++ 类型是 atHashWithStringNotFinal(Final 中只剩哈希,GetName() 仅非 Final 可用) src/dev_ng/game/Peds/HealthConfig.h:18-61、src/dev_ng/game/Peds/HealthConfig.h:63-124。 GetInfoFromName(name) = GetInfo(atStringHash(name)),线性遍历数组比较哈希,找不到返回 m_DefaultSet——这正是 external_named 指针在加载时把 XML 中的名字解析为对象指针所用的函数 src/dev_ng/game/Peds/HealthConfig.h:77-98。
3) 生成代码:HealthConfig.cpp 在类头之后 #include "HealthConfig_parser.h"(由 parCodeGen 生成,不在仓库中),其中包含两结构的 parser_Data、偏移表与 parser_Register,以及自动注册节点 src/dev_ng/game/Peds/HealthConfig.cpp:12-13。
4) 加载触发:CFileLoader::LoadHealthConfigFile 遍历 CDataFileMgr::PED_HEALTH_FILE 类型的数据文件条目(来自 content/datafile 配置)并调用 CHealthConfigInfoManager::Init(filename) src/dev_ng/game/scene/FileLoader.cpp:1501-1517; Load 断言尚未加载后执行 PARSER.LoadObject(pFileName, "meta", m_HealthConfigManagerInstance) src/dev_ng/game/Peds/HealthConfig.cpp:47-57。于是第 4 节的流程运行:根元素应为 <CHealthConfigInfoManager>,子元素 <aHealthConfig> 下每个 <Item> 是一个 CHealthConfigInfo,其字段为 <Name>、<DefaultHealth value="..."/> 等(名称规则见 2.3);<DefaultSet> 文本经 GetInfoFromName 解析。(XML 结构为依据名称规则推出,未在仓库中找到 pedhealth.meta 数据文件本身。)
5) 调试与保存:__BANK 下 InitWidgets 在 “A.I.” bank 中为每个条目调用 PARSER.AddWidgets(parBuildWidgetsVisitor 依据 min/max 等属性生成滑条),Save 用 PARSER.SaveObject("common:/data/pedhealth", "meta", ..., parManager::XML) 回写 src/dev_ng/game/Peds/HealthConfig.cpp:66-93。
7. 其它组件#
| 组件 | 说明 | 证据 |
|---|---|---|
parExternalStructureManager | 在支持外部结构定义的构建中管理从目录加载的结构定义(工具与调试用途) | src/dev_ng/rage/base/src/parser/extstructure.h:32 |
restparserservices | 把已注册结构/单例通过 diag REST 服务暴露(Schema、GUID、单例读写) | src/dev_ng/rage/base/src/parser/restparserservices.h:64-118 |
parSettings 其它标志 | RESIZE_EXISTING_ARRAYS、REALLOCATE_MISTYPED_POINTERS、ALWAYS_REALLOCATE、USE_TEMPORARY_HEAP、USE_SMALL_DATA_CHUNK_SIZE(1 KB 而非 16 KB 读缓冲)等 | src/dev_ng/rage/base/src/parsercore/settings.h:57-110 |
parser 诊断通道 | RAGE_DECLARE_CHANNEL(Parser) 与 parAssertf/parErrorf/... | src/dev_ng/rage/base/src/parser/macros.h:233-246 |
约束与边界行为#
.psc与 C++ 成员不一致 →parser_MemberOffsets编译错误;头文件与生成文件不同步 → 链接错误(非 Final/非 Orbis)。- Final 构建中名字只剩哈希:调试输出、
SaveObject、按名字查找结构可能不可用;需要字符串的结构必须preserveNames="true"。 - XML/RBF 加载需要临时堆中容纳整个 DOM;大文件会产生临时内存峰值。
- 主版本不兼容时整个结构被跳过(只断言),对象保留原值。
- PSO 就地加载只在所有非匿名模式签名匹配时发生;匿名结构不做签名验证(源码中留有 TODO)。不可构造或
NOT_IN_PLACE_LOADABLE的结构强制走逐对象路径。 psoLoadObjectInPlace以PSO_DONT_ALLOCATE就地修复后,psoFile的部分 API 不再可用(指针已替换 ID)。content.xml中的补丁文件机制在本代码中被禁用。
未决问题 / 证据不足#
parCodeGen由构建系统调用的具体命令行与输出目录未在本页核实(见 00-overview/build-and-config.md)。psofile_fixup.cpp中修复算法(vptr 修复、atArray等容器的就地重建细节)与psorscbuilder的布局规则只做了存在性确认,未逐行分析。- 未确认运行时
.meta在发行包中是 XML 还是 PSO(取决于资产流水线与-loadreleasepsos等参数,见src/dev_ng/rage/framework/src/fwscene/stores/psostore.cpp:25-26)。