NOTE

本文收口 HANDOFF_RE_BACKLOG.md 的 A-3。这里严格区分三种数据域:LAYOUT 文件的序列化类型、descriptor 注册表的编辑器/运行时属性类型、实例构造后的真实缺省值。同名键跨 descriptor 不能合并,DAT 的加载器缺省值也不属于本文这张表。

0. 结论

  1. 原版 MEDIA/**/*.LAYOUT8,985 份,当前宽类型标签扫描得到 638 个真实键名、142 种实际出现的 descriptor、1,943 组 (descriptor, key)
  2. backlog 所写的 628 不是当前真值:旧正则漏掉 13 个 <UNSIGNED INTEGER> 键,同时旧普查误收 3 个正文字符串中的伪键。因此 628 - 3 + 13 = 638
  3. 类型不能只按键名判断。16 个键名跨 descriptor 或块路径具有多种标签;但按 (descriptor, key) 后,1,943 组中没有一组类型冲突
  4. descriptor 权威文法来自 EffectDescriptorRegistry_build_all @ 0x469D00 及其属性注册器:sub_4CC930(标量)与 sub_4CCFA0(枚举)。当前 descriptors.json160 个 descriptor / 3,214 个继承展开字段
  5. 缺省值不再从“语料只写 false,所以默认 true”之类写入形状推断。权威来源是活 EditorGuts 中 captureDefaults @ sub_100CD160 生成的 descriptor clone 快照:159/159 descriptor、2,350/2,350 属性,每条都保留原始十六进制字节;1,927 条有解释后的值,423 条为空对象/空串。
  6. 逻辑引脚必须用 (descriptor, direction, pin name),需要索引时还要带该 descriptor 的表内位置。当前注册表有 853 个输入槽、1,029 个输出槽,共 1,882 槽 / 813 个不同名字;77 个名字同时出现在输入和输出侧,证明“只按名字”会混淆。
  7. 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,源自 0x469D00sub_4CC930/sub_4CCFA0不能拿它给 DAT 的 [BLOCK] 键提供缺省值
descriptor clone 快照属性缺席时,新实例实际携带什么值descriptor_defaults.jsonl,源自 sub_100CD160 的 live clone 表空 raw 不等于数值 0;同名键也不能跨 descriptor 合并

最重要的查表主键是:

text
(layout descriptor, field key)

例如 START ON LOADAnimation Controller 上默认 false,在 Generic Model / Timeline 等对象上可以是 true。把它压成 default_by_key["START ON LOAD"] 会直接制造错误。

2. 628 为什么变成 638

旧门使用:

python
re.compile(r"^\s*<([A-Z0-9_]+)>([^:]+):", re.MULTILINE)

类型字符类不允许空格,因而整行 <UNSIGNED INTEGER>KEY:value 都不会命中。宽标签扫描发现 13 个此前漏掉的真键,共 20,548 次字段出现:

主要 descriptor出现数
VISUAL QUOTAParticle12,210
SEGMENTSParticle3,409
RANDOMIZATIONGroup1,711
NUMBER REQUIREDUnit Trigger1,305
BRIGHTNESSLight1,205
MAX ALLOWEDUnit Spawner320
CURRENT STATEImage States / Resize Frame180
NUM RICOCHETSMissile147
RENDER ORDERMenu Definition21
MAX LENGTHEdit Box15
INDEXQuest Item/Description Controller12
NUMBER OF POINTSPathing10
SELECTED INDEXShopping List Controller3

旧 628 键表中还有三个并非字段的项目:') Size''\nBATCH UI COUNT''in'。它们来自旧正则跨入 <STRING>TEXT: 的正文值。

所以数量关系是:

text
旧审计 628
- 文本伪键 3
+ <UNSIGNED INTEGER> 漏扫真键 13
= 当前真实键面 638

这不是原版资源变化,而是度量定义修正。修正后的门直接把现场重扫集合与 layout_coverage.json.all_field_keys 做集合相等比较。

3. 类型系统

3.1 文件侧标签

638 个键按“一个键可能属于多个标签集合”计数如下:

规范化标签涉及的不同键名数
STRING231
BOOL201
FLOAT164
INTEGER36
UNSIGNED INT(源文本可写 UNSIGNED INTEGER18
INTEGER644

仅按键名统计时,16 个名字具有多类型,例如:

  • ID: INTEGER64 / UNSIGNED INT
  • COUNT: STRING / UNSIGNED INT
  • ONETWO: BOOL / INTEGER
  • ANGLEHEIGHTWIDTHVELOCITY: FLOAT / STRING
  • GOLDFAMEXP: STRING / UNSIGNED INT

这些不是数据冲突,而是同名键在不同 descriptor 或嵌套块中承担不同角色。按 (descriptor, key) 统计后,1,943 组的标签集合大小全部为 1。

3.2 注册表 typecode

当前注册表的类型码含义:

typecode注册语义
2整数 spinner:计数、等级、索引,如 EQUALS VALUELOOP COUNTSKILL LEVEL
3float
4int-backed 枚举或普通 int;枚举常由 sub_4CCFA0 注册
5wstring,也用于字符串型动态枚举/资源引用
6bool / _BYTE
7vec2 / pair 类编辑器字段
8Ogre::Vector3
10list / array 类属性

WARNING

老版本 descriptors.json 曾把 typecode 2 的显示名写成 color。这只是提取器早期标签,不是序列化真值;当前 _meta.typecode_legend 已明确它是整数 spinner。比如 Counter.EQUALS VALUE 在真 LAYOUT 中是 <INTEGER>,clone raw 0000000A 对应默认 10。

3.3 覆盖验收

运行:

powershell
python tools/audit_layout_coverage.py --reconcile 原版游戏分析/reversed_cpp/REGISTRY/descriptors.json

结果为:

text
descriptor 缺 0 · 基类字段缺 0 · 专属字段缺 0

其中 ID/NAME/PARENTIDPOSITION/FORWARD/RIGHT/UP 等基类字段已经沿继承链展开;vec3 在注册表中是一项,在 LAYOUT 中拆成 X/Y/Z 三项,对账时显式展开,不能误报成缺字段。

4. 缺省值从哪里来

4.1 clone 快照机制

EditorGuts 创建 descriptor 对象后调用:

text
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.fieldtype默认值raw
Counter.ENABLEDbooltrue00000001
Counter.EQUALS VALUEint100000000A
Counter.STARTING VALUEint000000000
Counter.LOGICenumActivate only onceUTF-16 raw
Counter.SENDSbooltrue00000001
Logic Gate.ONE/TWOboolfalse/false00000000
Logic Gate.EVAL ON CHANGEbooltrue00000001
Logic Gate.ALWAYS EVAL ON CHANGEbooltrue00000001
Logic Gate.SEND DATAbooltrue00000001
Logic Gate.TYPEenumANDUTF-16 raw
Random Choice.TYPEenumWEIGHTUTF-16 raw
Random Choice.COUNTint100000001
Random Choice.ONE..FIVEint全部 000000000
Timer.TIMEfloat1.03F800000
Timer.MAXTIMEfloat-1.0BF800000
Timer.LOOP COUNTint100000001
Timer.LOOPS FOREVERboolfalse00000000

这张表解决的是“键缺席时对象里有什么”。它不自动等于消费语义:例如 -1 可能是 sentinel,空字符串可能表示跳过资源绑定;真正使用方式仍要看消费函数。

5. 逻辑引脚语义

5.1 引脚的身份

descriptor 构造阶段的 sub_4CBA00 / sub_4CBAF0 分别注册输入动作名与输出事件名;底层名称表来自 unk_3142BC0(输入)和 unk_313ED68(输出)。继承链展开后写入每个 descriptor 的 inputs[] / outputs[]

因此逻辑连接应保存:

text
source descriptor + output name
target descriptor + input name

而不是全局 name → handler。当前 77 个名字同时是输入和输出,Stop 就是最直观的例子;Play 更在 9 个 descriptor 槽位出现、占 4 个不同的 (direction, position)

5.2 常用节点

descriptor输入输出
CounterEnable, Disable, Reset, Add, Subtract, BroadcastEnabled, Disabled, Activated, Reset
Logic GateInput One/Two True/False, Toggle Input One/Two, EvaluateEvaluated True, Evaluated False
Random ChoiceEnable, Disable, RollOne, Two, Three, Four, Five
TimerEnable, Disable, Reset, Enable and No ResetEnabled, Disabled, Activated
Quest ControllerShow, Hide, Interact, Force Accept, Force Complete, Abandon, Broadcast, Refresh IconsQuest Active/Complete/Abandoned、任务检查等 18 个输出
Logic GroupStop, Start, Pause, Input1..5Level Activated, Stop/Start/Pause, Output 1..5, Post player spawn, Cinematic Complete

旧笔记中的“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 为门,等价映射为:

text
Show -> Start Particle
Hide -> Force Stop Particle

Enable and Show / Disable and Hide 不走这条重写。普通 Layout Link 对未由自身 switch 处理的输入还会广播给链接布局的所有子对象;这是跨文件逻辑图不能只看本地边的原因。

5.4 大小写

注册表有 8 组仅大小写不同的名字,例如 CANCEL/CancelReset To End/Reset to EndIS 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 新口径及完整差额解释。

现场验证包括:

text
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 names

7. 对重制实现的约束

  1. schema 查找必须带 descriptor;同名键不能全局合并。
  2. LAYOUT 序列化 parser 必须接受带空格的类型标签,至少包括 <UNSIGNED INTEGER>
  3. vec3 注册字段与 X/Y/Z 序列化分量应在 schema 层显式映射。
  4. 缺键时先查该 descriptor 的 clone 默认;不要从“全库只出现 false”反推。
  5. 空 raw、空字符串、数值 sentinel 与普通数值应分开建模,消费语义不足时标 unknown,而不是强制转 0。
  6. 逻辑边按 source output / target input 分方向解析;引脚名字和 handler 内部编号不是同一套索引。
  7. 对原版可沿用大小写折叠,但 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.pytests/truth/test_dat_tags_and_pins.pytests/truth/test_m0_recorded_facts.py

至此,A-3 的“类型 / 缺省值来源 / 引脚语义”三部分均有独立数据产物和可执行门;旧 628 数字也不是静默改写,而是有可重复计算的 638 新口径。