F2 默认值 O1 观测配方:A-4 缺口收口报告

对象:Torchlight2.exe(静态基址 0x400000
证据等级:本报告完成的是“可执行 O1 配方”,不是 O1 实测结果
登记表:原版游戏分析/reversed_cpp/REGISTRY/o1_sampling_recipes.json
强制门:python tools/o1_recipe_gate.py

1. 结论

M3-1 判据 4 的“怎么观测”缺口已经收口:当前 35 个 O2_unsampled 字段全部拥有完整配方,门禁输出为:

text
证伪的配方 4 条,其中补了替代配方的 4 条
校验了 39 条配方,0 条不合格
O2_unsampled 配方缺口:0 条

需要区分两个状态:

  • 配方闭合:已经知道在何处停、读哪个返回值、用哪个缺键样本、怎样区分真假。A-4 已完成。
  • 默认值获 O1 证实:必须真正启动游戏并采到返回值,之后才能把对应字段从 O2_unsampled 升档。本报告没有虚报这一步已经完成。

旧 backlog 写的是“31 个字段没有观测方法”,那是较早快照。以本轮开始时的当前登记表重算:35 个 O2_unsampled 中,13 个已有配方、22 个没有配方;13 个里又有 ARMOR_MAGICALARMOR_ALL 两条旧配方已被证伪且尚无替代,因此本轮实际欠债是 22 + 2 = 24 条

2. 为什么统一观测 getter 返回值

“加载完后读对象字段”并不总能回答“缺键时 getter 的默认值是什么”。已经发现四类破坏判别力的情况:

  1. 哨兵值写入字段后立刻被派生值覆盖,例如 MANACOSTLEVEL_REQUIRED
  2. 条件分支跳过字段写入,例如元素护甲值为 -1 时不写槽位。
  3. 后续流程重新缩放并写回字段,例如精英怪的 XPARMOR
  4. 0、空串等默认值与构造函数初值同形,最终字段无法说明 getter 当时返回了什么。

因此,本轮 24 条配方全部把观测点前移到 CDataGroup__getXxx返回瞬间

text
缺少目标键的 DAT
  → CDataGroup__getXxx(key, hardcoded_default)
  → 在 call 的下一条指令处停住
  → 直接读取返回值

返回约定如下:

getter观测值
getIntEAX
getBoolAL
getFloatx87 ST(0)
getStringEAX 指向的 std::wstring;复制对象或读取其长度/内容

这条路径不经过字段写回、取整、钳位、曲线求值或精英缩放,所以候选默认值不同,返回读数必然不同。

3. 精确观测表

表内地址是静态 VA;运行时存在 ASLR,应使用 Torchlight2 + (VA - 0x400000)call bytes 同时作为版本/地址漂移保护。

3.1 怪物字段

字段call → returncall bytes返回值预期 / 排除缺键样本
MINHP0x55D0A4 → 0x55D0A9E8 F7 AE 11 00ST(0)1.0 / 0.0MONSTERS/QUESTUNITS/A3_GHOST_CART.DAT
MAXHP0x55D0DB → 0x55D0E0E8 C0 AE 11 00ST(0)1.0 / 0.0同上
MINDAMAGE0x55D8A9 → 0x55D8AEE8 D2 A6 11 00EAX0 / 1同上
MAXDAMAGE0x55D8ED → 0x55D8F2E8 8E A6 11 00EAX0 / 1同上
HEALTH_GRAPH0x55D23B → 0x55D240E8 C0 AC 11 00EAX → wstringlength=0 / length>0MONSTERS/ARMADAX/ARMADAX.DAT
ARMOR_GRAPH0x55D5D0 → 0x55D5D5E8 2B A9 11 00EAX → wstringlength=0 / length>0同上
DAMAGE_GRAPH0x55D947 → 0x55D94CE8 B4 A5 11 00EAX → wstringlength=0 / length>0同上
CHAMPION_SCALE0x522CAA → 0x522CAFE8 81 52 15 00AL1(true) / 0(false)同上
ARMOR_MAGICAL0x55D7DC → 0x55D7E1E8 9F A7 11 00EAX-1 / 0MONSTERS/ARMADAX/ARMADAX.DAT
ARMOR_FIRE0x55D7DC → 0x55D7E1同上EAX-1 / 0MONSTERS/ARMOR_RACK/ARMOR_RACK.DAT
ARMOR_ICE0x55D7DC → 0x55D7E1同上EAX-1 / 0同上
ARMOR_ELECTRIC0x55D7DC → 0x55D7E1同上EAX-1 / 0同上
ARMOR_POISON0x55D7DC → 0x55D7E1同上EAX-1 / 0同上
ARMOR_ALL0x55D7DC → 0x55D7E1同上EAX-1 / 0MONSTERS/ARMADAX/ARMADAX.DAT

六个 ARMOR_* 共享一个调用点不是抄写错误。可执行文件只保存 ARMOR_ 前缀,完整键在循环里动态拼接;采样时必须读取/记录本次传入的运行时键,并只接受与目标字段同名的那次命中。ARMOR_MAGICALARMOR_ALL 在 EXE 中没有完整宽字符串字面量,因此不能为它们伪造独立 literal xref。

3.2 物品字段

字段call → returncall bytes返回值预期 / 排除缺键样本
MINDAMAGE0x58F106 → 0x58F10BE8 75 8E 0E 00EAX0 / 1ITEMS/BASE.DAT
MAXDAMAGE0x58F14B → 0x58F150E8 30 8E 0E 00EAX0 / 1同上
RARITY_DMG_MOD0x58F686 → 0x58F68BE8 F5 88 0E 00EAX100 / 0同上
SPEED_DMG_MOD0x58F6D5 → 0x58F6DAE8 A6 88 0E 00EAX100 / 0ITEMS/2HAXE/BASE_2HAXE.DAT
SPEED0x58F45C → 0x58F461E8 1F 8B 0E 00EAX100 / 0同上
RARITY_AMR_MOD0x58F4AB → 0x58F4B0E8 D0 8A 0E 00EAX100 / 0同上
SPECIAL_AMR_MOD0x58F4FA → 0x58F4FFE8 81 8A 0E 00EAX100 / 0同上

四个百分比修正值的 100 是乘法单位元。不要用最终伤害/护甲反推:100% 与“某段乘法没有执行”可能同形;必须直接读 getter 的 EAX

3.3 技能字段

字段call → returncall bytes返回值预期 / 排除缺键样本
TARGET_TYPE0x6DC6F0 → 0x6DC6F5E8 0B B8 F9 FFEAX → wstringlength=0 / length>0SKILLS/ALCHEMIST/EMBERBEAM/EMBERBEAM.DAT
TARGET_UNITTYPE0x6DC88C → 0x6DC891E8 6F B6 F9 FFEAX → wstringlength=0 / length>0同上
REQUIREMENT_LEFT0x6DCFFA → 0x6DCFFFE8 01 AF F9 FFEAX → wstringlength=0 / length>0同上

这三个默认值都是运行时构造的空 std::wstring,其存储位于 BSS,不能靠 EXE 文件里的静态内容证明为空。正确的 O1 观测是在 getter 返回点复制字符串对象或读取其运行期长度。

4. 安全执行步骤

MINHP 为例,返回点的 RVA 是 0x15D0A9

text
bu Torchlight2+15D0A9
g

命中后再单独查看 ST(0)。整数/布尔字段分别查看 EAX/AL。字符串建议由现有 runtime-mod 的 detour 包装器复制返回的 std::wstring;若使用调试器,也必须先让断点停住,再单独检查 EAX 所指对象。

安全约束:

  • 调试器附加时使用 -pd,避免调试器退出连带终止游戏。
  • 断点命令链只负责停住;不要把 dddu 等内存读取与自动 gc 塞进命令链。
  • 每次观测先确认加载的是表中缺键样本,并按 BASEFILE 继承链复核目标键确实缺席。
  • 元素护甲的共享点必须过滤动态键名,否则一次循环中的其他元素会污染结果。
  • 采到值后保存:游戏版本/EXE 哈希、模块基址、静态 VA、运行时地址、样本路径、键是否经继承后缺席、原始返回值。

5. 门禁与可复现性

tools/o1_recipe_gate.py 现在会执行以下检查:

  1. 每个 O2_unsampled 行必须有 o1_recipeobservediscriminates,缺一即失败。
  2. 目录型配方必须同时给出 call、return、返回载体、call bytes 和样本路径。
  3. call bytes 必须与发行 EXE 逐字节相同,且 return == call + 指令长度
  4. 字面量键的 call 必须是该键实际的 getter 现场。
  5. 动态 ARMOR_* 键必须显式声明过滤键名,并验证共享调用点机器码。
  6. 已证伪配方不再是 O2_unsampled 的豁免符;四条证伪记录都必须附替代方案。

执行:

powershell
$env:PYTHONIOENCODING='utf-8'
python tools/o1_recipe_gate.py
python -m unittest tests.truth.test_o1_recipe_gate

6. 对重制实现的约束

在真正完成 O1 采样前,35 个字段仍应保留 O2_unsampled 身份,不得因为“配方已经写好”而升格为已验证默认值。实现侧可以继续使用静态反编译所得默认值,但必须保留证据等级;一旦 O1 返回与预期不同,应优先修正 F2 默认值和相关语义测试,而不是把不一致解释成采样噪声。

本轮最重要的工程产物不是又抄了一张地址表,而是把“还有没有观测方法”变成了会失败的门:以后新增 O2_unsampled 字段却没有完整配方,tools/o1_recipe_gate.py 会直接返回非零。