NOTE
本文收口 HANDOFF_RE_BACKLOG.md 的 A-3。这里严格区分三种数据域:LAYOUT 文件的序列化类型、descriptor 注册表的编辑器/运行时属性类型、实例构造后的真实缺省值。同名键跨 descriptor 不能合并,DAT 的加载器缺省值也不属于本文这张表。
0. 结论
- 原版
MEDIA/**/*.LAYOUT共 8,985 份,当前宽类型标签扫描得到 638 个真实键名、142 种实际出现的 descriptor、1,943 组(descriptor, key)。 - backlog 所写的 628 不是当前真值:旧正则漏掉 13 个
<UNSIGNED INTEGER>键,同时旧普查误收 3 个正文字符串中的伪键。因此628 - 3 + 13 = 638。 - 类型不能只按键名判断。16 个键名跨 descriptor 或块路径具有多种标签;但按
(descriptor, key)后,1,943 组中没有一组类型冲突。 - descriptor 权威文法来自
EffectDescriptorRegistry_build_all @ 0x469D00及其属性注册器:sub_4CC930(标量)与sub_4CCFA0(枚举)。当前descriptors.json有 160 个 descriptor / 3,214 个继承展开字段。 - 缺省值不再从“语料只写 false,所以默认 true”之类写入形状推断。权威来源是活 EditorGuts 中
captureDefaults @ sub_100CD160生成的 descriptor clone 快照:159/159 descriptor、2,350/2,350 属性,每条都保留原始十六进制字节;1,927 条有解释后的值,423 条为空对象/空串。 - 逻辑引脚必须用
(descriptor, direction, pin name),需要索引时还要带该 descriptor 的表内位置。当前注册表有 853 个输入槽、1,029 个输出槽,共 1,882 槽 / 813 个不同名字;77 个名字同时出现在输入和输出侧,证明“只按名字”会混淆。 logic_eval.py已按 descriptor 区分引脚,并实现 Layout Link 粒子分支的Show/Hide → Start Particle/Force Stop Particle重写。原版语料 36,244 条引脚引用在大小写折叠后没有冲突;但注册表本身有 8 组仅大小写不同的名字,所以该折叠只对原版语料安全,不应无条件推广到 MOD。
1. 三层真值不能混用
| 层 | 回答的问题 | 权威来源 | 不能推出什么 |
|---|---|---|---|
| LAYOUT 序列化标签 | 文件中这个值以什么标记写出 | layout_coverage.json 对全量文件的 <TYPE>KEY: 扫描 | 不能仅凭标签推出运行时枚举含义或缺省值 |
| descriptor 属性注册表 | 哪个 descriptor 接受哪些属性、属性控件/存储类型、getter/setter、输入输出引脚 | descriptors.json,源自 0x469D00、sub_4CC930/sub_4CCFA0 | 不能拿它给 DAT 的 [BLOCK] 键提供缺省值 |
| descriptor clone 快照 | 属性缺席时,新实例实际携带什么值 | descriptor_defaults.jsonl,源自 sub_100CD160 的 live clone 表 | 空 raw 不等于数值 0;同名键也不能跨 descriptor 合并 |
最重要的查表主键是:
(layout descriptor, field key)例如 START ON LOAD 在 Animation Controller 上默认 false,在 Generic Model / Timeline 等对象上可以是 true。把它压成 default_by_key["START ON LOAD"] 会直接制造错误。
2. 628 为什么变成 638
旧门使用:
re.compile(r"^\s*<([A-Z0-9_]+)>([^:]+):", re.MULTILINE)类型字符类不允许空格,因而整行 <UNSIGNED INTEGER>KEY:value 都不会命中。宽标签扫描发现 13 个此前漏掉的真键,共 20,548 次字段出现:
| 键 | 主要 descriptor | 出现数 |
|---|---|---|
VISUAL QUOTA | Particle | 12,210 |
SEGMENTS | Particle | 3,409 |
RANDOMIZATION | Group | 1,711 |
NUMBER REQUIRED | Unit Trigger | 1,305 |
BRIGHTNESS | Light | 1,205 |
MAX ALLOWED | Unit Spawner | 320 |
CURRENT STATE | Image States / Resize Frame | 180 |
NUM RICOCHETS | Missile | 147 |
RENDER ORDER | Menu Definition | 21 |
MAX LENGTH | Edit Box | 15 |
INDEX | Quest Item/Description Controller | 12 |
NUMBER OF POINTS | Pathing | 10 |
SELECTED INDEX | Shopping List Controller | 3 |
旧 628 键表中还有三个并非字段的项目:') Size'、'\nBATCH UI COUNT'、'in'。它们来自旧正则跨入 <STRING>TEXT: 的正文值。
所以数量关系是:
旧审计 628
- 文本伪键 3
+ <UNSIGNED INTEGER> 漏扫真键 13
= 当前真实键面 638这不是原版资源变化,而是度量定义修正。修正后的门直接把现场重扫集合与 layout_coverage.json.all_field_keys 做集合相等比较。
3. 类型系统
3.1 文件侧标签
638 个键按“一个键可能属于多个标签集合”计数如下:
| 规范化标签 | 涉及的不同键名数 |
|---|---|
STRING | 231 |
BOOL | 201 |
FLOAT | 164 |
INTEGER | 36 |
UNSIGNED INT(源文本可写 UNSIGNED INTEGER) | 18 |
INTEGER64 | 4 |
仅按键名统计时,16 个名字具有多类型,例如:
ID:INTEGER64/UNSIGNED INTCOUNT:STRING/UNSIGNED INTONE、TWO:BOOL/INTEGERANGLE、HEIGHT、WIDTH、VELOCITY:FLOAT/STRINGGOLD、FAME、XP:STRING/UNSIGNED INT
这些不是数据冲突,而是同名键在不同 descriptor 或嵌套块中承担不同角色。按 (descriptor, key) 统计后,1,943 组的标签集合大小全部为 1。
3.2 注册表 typecode
当前注册表的类型码含义:
| typecode | 注册语义 |
|---|---|
| 2 | 整数 spinner:计数、等级、索引,如 EQUALS VALUE、LOOP COUNT、SKILL LEVEL |
| 3 | float |
| 4 | int-backed 枚举或普通 int;枚举常由 sub_4CCFA0 注册 |
| 5 | wstring,也用于字符串型动态枚举/资源引用 |
| 6 | bool / _BYTE |
| 7 | vec2 / pair 类编辑器字段 |
| 8 | Ogre::Vector3 |
| 10 | list / array 类属性 |
WARNING
老版本 descriptors.json 曾把 typecode 2 的显示名写成 color。这只是提取器早期标签,不是序列化真值;当前 _meta.typecode_legend 已明确它是整数 spinner。比如 Counter.EQUALS VALUE 在真 LAYOUT 中是 <INTEGER>,clone raw 0000000A 对应默认 10。
3.3 覆盖验收
运行:
python tools/audit_layout_coverage.py --reconcile 原版游戏分析/reversed_cpp/REGISTRY/descriptors.json结果为:
descriptor 缺 0 · 基类字段缺 0 · 专属字段缺 0其中 ID/NAME/PARENTID 与 POSITION/FORWARD/RIGHT/UP 等基类字段已经沿继承链展开;vec3 在注册表中是一项,在 LAYOUT 中拆成 X/Y/Z 三项,对账时显式展开,不能误报成缺字段。
4. 缺省值从哪里来
4.1 clone 快照机制
EditorGuts 创建 descriptor 对象后调用:
sub_100CD160(desc, obj) // captureDefaults该函数把新建、尚未应用 LAYOUT 属性的实例值按属性表复制到 descriptor + 0xF4 的 clone/default 表,并以 descriptor + 0xF8 作幂等门。由此得到的 descriptor_defaults.jsonl 不是语料频率,也不是“看到一条 mov 就猜整个对象”,而是引擎自己为属性系统保存的构造后快照。
完整性自检:
- descriptor: 159 / 159;
cloneCount == propCount: 159 / 159;- 属性:2,350 / 2,350;
- 带解释值:1,927;
- 空对象/空串:423;
- 每条属性都保留
raw=或raw[n]=...十六进制证据。
4.2 逻辑节点实例
| descriptor.field | type | 默认值 | raw |
|---|---|---|---|
Counter.ENABLED | bool | true | 00000001 |
Counter.EQUALS VALUE | int | 10 | 0000000A |
Counter.STARTING VALUE | int | 0 | 00000000 |
Counter.LOGIC | enum | Activate only once | UTF-16 raw |
Counter.SENDS | bool | true | 00000001 |
Logic Gate.ONE/TWO | bool | false/false | 00000000 |
Logic Gate.EVAL ON CHANGE | bool | true | 00000001 |
Logic Gate.ALWAYS EVAL ON CHANGE | bool | true | 00000001 |
Logic Gate.SEND DATA | bool | true | 00000001 |
Logic Gate.TYPE | enum | AND | UTF-16 raw |
Random Choice.TYPE | enum | WEIGHT | UTF-16 raw |
Random Choice.COUNT | int | 1 | 00000001 |
Random Choice.ONE..FIVE | int | 全部 0 | 00000000 |
Timer.TIME | float | 1.0 | 3F800000 |
Timer.MAXTIME | float | -1.0 | BF800000 |
Timer.LOOP COUNT | int | 1 | 00000001 |
Timer.LOOPS FOREVER | bool | false | 00000000 |
这张表解决的是“键缺席时对象里有什么”。它不自动等于消费语义:例如 -1 可能是 sentinel,空字符串可能表示跳过资源绑定;真正使用方式仍要看消费函数。
5. 逻辑引脚语义
5.1 引脚的身份
descriptor 构造阶段的 sub_4CBA00 / sub_4CBAF0 分别注册输入动作名与输出事件名;底层名称表来自 unk_3142BC0(输入)和 unk_313ED68(输出)。继承链展开后写入每个 descriptor 的 inputs[] / outputs[]。
因此逻辑连接应保存:
source descriptor + output name
target descriptor + input name而不是全局 name → handler。当前 77 个名字同时是输入和输出,Stop 就是最直观的例子;Play 更在 9 个 descriptor 槽位出现、占 4 个不同的 (direction, position)。
5.2 常用节点
| descriptor | 输入 | 输出 |
|---|---|---|
| Counter | Enable, Disable, Reset, Add, Subtract, Broadcast | Enabled, Disabled, Activated, Reset |
| Logic Gate | Input One/Two True/False, Toggle Input One/Two, Evaluate | Evaluated True, Evaluated False |
| Random Choice | Enable, Disable, Roll | One, Two, Three, Four, Five |
| Timer | Enable, Disable, Reset, Enable and No Reset | Enabled, Disabled, Activated |
| Quest Controller | Show, Hide, Interact, Force Accept, Force Complete, Abandon, Broadcast, Refresh Icons | Quest Active/Complete/Abandoned、任务检查等 18 个输出 |
| Logic Group | Stop, Start, Pause, Input1..5 | Level Activated, Stop/Start/Pause, Output 1..5, Post player spawn, Cinematic Complete |
5.3 Layout Link 的内部编号不是数组位置
旧笔记中的“input 17 = Start Particle”不是 descriptors.json.inputs[17]。它是 CLayoutDescriptor::handleInput 内部 switch 的 handler 编号:粒子类 Layout Link 会在分派前把 Show 的编号 0 改写为 17,把 Hide 的编号 1 改写为 19。
logic_eval.py 以目标 descriptor 是否为 Layout Link Particle 为门,等价映射为:
Show -> Start Particle
Hide -> Force Stop ParticleEnable and Show / Disable and Hide 不走这条重写。普通 Layout Link 对未由自身 switch 处理的输入还会广播给链接布局的所有子对象;这是跨文件逻辑图不能只看本地边的原因。
5.4 大小写
注册表有 8 组仅大小写不同的名字,例如 CANCEL/Cancel、Reset To End/Reset to End、IS FRIEND/Is Friend。原版 36,244 条 INPUTNAME/OUTPUTNAME 引用按 (direction, lowercase name) 分组后冲突为 0,所以当前 evaluator 的小写匹配对原版语料安全。
对 MOD 则应保留原拼法并在发生折叠碰撞时报警,因为 registry 已证明潜在冲突真实存在。
6. 代码与门测试回填
本次修正:
tools/m3_layout_probe.py的类型标签正则允许空格;- 当前键面改与
layout_coverage.json的 638 键比对; - 旧 628 键 census 降级为历史 verdict/consumer 来源,不再充当分母;
tests/truth/test_m3_layout_closure.py新增数量恒等式与 13/3 差额断言;- descriptor 覆盖门按 alias 和 vec3 分量展开后断言零缺口;
m3_layout_closure.json记录 638 新口径及完整差额解释。
现场验证包括:
8,985 个 LAYOUT 严格 UTF-16LE+BOM:0 失败
真实键面:638,与 layout_coverage.json 集合完全相等
旧口径差额:13 个 UNSIGNED INTEGER 真键 / 3 个文本伪键
descriptor 对账:0 / 0 / 0
descriptor clone:159 / 2,350,cloneCount 全等 propCount
逻辑引脚:160 descriptor / 1,882 slots / 813 names7. 对重制实现的约束
- schema 查找必须带 descriptor;同名键不能全局合并。
- LAYOUT 序列化 parser 必须接受带空格的类型标签,至少包括
<UNSIGNED INTEGER>。 - vec3 注册字段与 X/Y/Z 序列化分量应在 schema 层显式映射。
- 缺键时先查该 descriptor 的 clone 默认;不要从“全库只出现 false”反推。
- 空 raw、空字符串、数值 sentinel 与普通数值应分开建模,消费语义不足时标 unknown,而不是强制转 0。
- 逻辑边按 source output / target input 分方向解析;引脚名字和 handler 内部编号不是同一套索引。
- 对原版可沿用大小写折叠,但 MOD 输入若出现 registry 已知的大小写碰撞组,应拒绝或显式消歧。
8. 证据索引
| 证据 | 路径 / 地址 |
|---|---|
| descriptor 总注册入口 | Torchlight2.exe: sub_469D00 |
| 标量 / 枚举属性注册 | sub_4CC930 / sub_4CCFA0 |
| 输入 / 输出引脚注册 | sub_4CBA00 / sub_4CBAF0 |
| EditorGuts 缺省快照 | sub_100CD160, descriptor +0xF4 clone 表、+0xF8 门 |
| 全量字段/类型覆盖 | 原版游戏分析/layout_coverage.json |
| descriptor 字段与引脚 | 原版游戏分析/reversed_cpp/REGISTRY/descriptors.json |
| ctor 缺省值与 raw | 原版游戏分析/reversed_cpp/REGISTRY/descriptor_defaults.jsonl |
| 引脚量化与歧义 | 原版游戏分析/reversed_cpp/REGISTRY/dat_tags_and_pins.json |
| 口径修正记录 | 原版游戏分析/reversed_cpp/REGISTRY/m3_layout_closure.json |
| 现场门 | tests/truth/test_m3_layout_closure.py、tests/truth/test_dat_tags_and_pins.py、tests/truth/test_m0_recorded_facts.py |
至此,A-3 的“类型 / 缺省值来源 / 引脚语义”三部分均有独立数据产物和可执行门;旧 628 数字也不是静默改写,而是有可重复计算的 638 新口径。