原版 Group 普通 CHOICE、主题与模式选择

本批结果与范围

新增 core_sim/NativeGroupChoice.cs,实现 509E50 的普通 Group 选择,以及主题门、子树禁用、首个源 ID 查找和整数权重随机池。

新增 core_sim.io/F2NativeGroupChoices.cs,将完整 BINLAYOUT 尾部投影成该选择核心的输入。ProjectRaw 不代替解析期 LEVEL UNIQUE 剔除或 feature-tag 预选。

  • 6144 组原版选择控制及整数权重归一化参考全部一致。
  • 覆盖 x87 PC24/PC53/PC64、普通/NG+、主题查询返回 0/1/2、零/负/大整数权重、既有访问标记。
  • 1290 条核心断言、171 项相关 Python 检查通过。
  • Godot C# 构建零警告、零错误;coresimlint 通过。

本批完成的是 RNG 恢复之后的普通 CHOICE 路径。前面的 feature-tag 配额预选 50A730、解析期唯一性剔除、实例属性门 61F660 的完整绑定和具体世界宿主仍未全部完成。全武器目标保持未完成。

两个选择阶段不能合并

前批装载器确认的顺序:

text
读取 Group pool
保存同步 RNG
50A730 feature-tag 配额预选
恢复同步 RNG
509E50 普通 CHOICE/主题/模式选择
创建实际对象

本批没有用转换器的 Python random 或已有选择结果替代 509E50,也没有在普通选择结束后额外恢复 RNG。普通选择的随机消耗要留给后续对象 ID/视觉变体等消费者。

主题门:509A80

  • 没有主题提供者时直接通过。
  • ACTIVE THEMES 非空时,按顺序查询;任意返回字节恰好等于 1就立即通过。
  • 这个立即通过不会再检查 DEACTIVE THEMES。
  • ACTIVE THEMES 未命中时,继续查询 DEACTIVE THEMES;任意返回 1 则拒绝。
  • 两组查询都未触发提前返回时,仅在 ACTIVE THEMES 原本为空时通过。

因此“ACTIVE 命中并且 DEACTIVE 不命中”不是原版算法。两组同时包含匹配项时,ACTIVE 的提前返回优先。

原入口比较的是 byte==1,不把所有非零返回值都当成功。测试保留了主题提供者返回 2 的情况。

普通选择入口:509E50

进入时设置 Visited byte+97=true;不清除旧的 Allowed byte+96,也不将其强行设回 true。

原 41ED30 返回的全局字节启用时:

  • 直接递归处理子项。
  • 跳过当前节点的主题、模式及 CHOICE。
  • 此分支不做普通 LEVEL UNIQUE 登记。

该字节以明确 BypassSelection 输入表示,没有猜成任意通用编辑器开关。

否则先执行主题门,再检查 GAME MODE:

  • 1:只允许 NORMAL,即不在新游戏+。
  • 2:只允许 NEW GAME PLUS。
  • 其他编码不在此入口施加模式限制。

模式依据原 41ECC0 的结果;既有 Rust 编译器亦将 NORMAL 编为 1、NEW GAME PLUS 编为 2。它不是困难度或硬核模式。

主题或模式拒绝时,通过 509B20 将自身及整个子树置为 Allowed=false、Visited=true。

三种 CHOICE

0:全部递归

按原子数组顺序递归。存在唯一性记录器且当前子项 LEVEL UNIQUE 非零时,递归之后登记该子项 ID。

登记不以递归后的 Allowed=true 为额外条件,不能擅自增加这个门。

1:加权、不放回

  1. 按子数组下标,将通过主题门的子项加入权重池。
  2. 子项 RANDOMIZATION 经 signed int32 转为 double 权重,不按 float 位模式解释。
  3. 当前 NUMBER 被改为 max(1, NUMBER)。
  4. 每轮先检查归一化后是否还有正权重;没有则本轮不取随机数。
  5. 有正权重时执行 pick,随后按返回的 ID 移除,移除使用末项补位。
  6. 返回 ID 小于子数量时递归选择对应子项。
  7. 最后禁用尚未 Visited 的子项;预先 Visited 的子项不会被这轮清理重新分类。

权重为零的整个池不会被改成均匀抽取;NUMBER 仍被钳为至少 1,但不产生无意义随机消耗。

2:逐子项百分比判定

每个子项:

  • 先过主题门。
  • 通过后调用同步整数随机闭区间 [0,100]。
  • 以 unsigned 比较 random<=子项 RANDOMIZATION。
  • 通过则递归,并按当前子项登记 LEVEL UNIQUE;否则禁用子树。

0 不是必然失败:随机值为 0 时仍通过。大于 100 的原始 unsigned 值不被擅自钳位。

NUMBER 不控制这条分支。

其他编码

不进一步处理子项;保留已设置的当前 Visited 标记。不能将未知编码默认为“选择全部”。

LEVEL UNIQUE 的下标差异

加权分支在 50A0CA 检查的是被选中子项的 LEVEL UNIQUE;但在 50A0D2 起取登记 ID 时,用的是 EBX 抽取轮次下标,而不是 EDI 选中下标。

例如第一轮选中 child[1] 且该子项为唯一项,登记的却是 child[0] 的 ID。核心保留该发行行为,测试直接验证了这个分支。

当异常权重/fallthrough 组合使轮次超出有效子数量、原版将读取容量范围内的未定义内容时,移植端明确报缺输入,不编造该内存中的 ID。此类未定义容量内容不在等价性声明内。

x87 权重数学

本批没有修改既有掉落 Randomizer,而是为 Group 的整数权重、mode=0 路径提供独立实现:

  • 675AF0 的 int32→double 存储。
  • 675B50 按当前 x87 精度累加并归一化,结果落为 float32。
  • 675C60 检查是否至少一个归一化权重严格大于 0。
  • 675CB0 的同步随机数、按顺序累加和严格 sum>target。
  • 6758B0 按 ID 查找并末项补位删除。

Group 加权入口最多来自 ushort 子项数组,整数总和在 double 的精确整数范围内;不将本实现扩展宣称为任意 double 权重或衰减模式的完整随机池。

pick 未命中时,原版返回 ID 0;本实现保留返回值,不将其改成“没抽到就跳过”。Group 的 NUMBER 循环有上限,不能照搬掉落调用方可能死循环时采用的另一种异常策略。

原参考还单独导出归一化 float bits。特设 [16777217,1,1,1] 等输入,使 PC24 与 PC53 的结果差异可见,验证不是把同一份结果复制为三种精度。

投影与已有状态

NativeGroupChoiceNode 保留 SourceId、Choice、Number、Randomization、主题列表、模式、唯一性和运行时 Allowed/Visited/限制标记。

  • Find 按自身优先、子项预序返回第一个 SourceId 匹配项,不合并重复 ID。
  • Deactivate 对子树设置 Allowed=false、Visited=true。
  • RestrictToLinks 单独递归设置 byte+98 对应限制,不与禁用状态混为一谈。
  • HasFeatureConstraint 来自原 TAG/NO TAG FOUND/主题列表存在性,后续可对应装载器获取的 byte+85。
  • ProjectRaw 不执行 feature claim,也不假造“唯一项尚未使用”。

已读取真实 PASS_JD_A 的 Group 尾部,核对投影中的选择值、数量、整数权重、模式和两个主题列表。完整上下文仍须由实际世界宿主提供。

参考与验证

原 EXE SHA256: 186472c3057b38f4cdff4696959a943c396ae6166995f7418997b5ea853e8a5e

参考直接执行:

  • 509E50/509A80/509B20。
  • 原 Randomizer 构造、添加、归一化、正权重检查、pick 和移除。
  • 原同步整数/浮点 RNG 与整数乘法辅助函数。
  • 原模式查询路径。

分配器、主题提供者和唯一性记录边界为明确 spy;有限整数权重范围内的 isnan 边界为明确测试桩。不宣称真实主题管理器、唯一性存档或原堆管理已完成。

产物:

  • tools/build_native_group_choice_oracle.py/tools/native_group_choice_oracle.c。
  • build/native_group_choice_oracle/results.json:6144 组状态、随机状态、调用记录及归一化位值。
  • build/native_group_choice_core.log:1290 断言。
  • build/native_group_choice_python.log:171 检查。
  • build/native_group_choice_godot.log:零警告、零错误。

另已追到 feature-tag 预选及 claim 的 50A730/5FDBB0/5FD8D0/5FD9C0/5FDAB0,和创建后实例门 61F660 的四个分支;这些取证不能替代对应实现,保留为下一批必要工作。

按 godot-master 的分层方式,本批未替换正式地图画面。main 原地工作,未提交,未 attach 原游戏,快照 v29 未改。默认存档 SHA256 未变: 9840fe858b6e311c73f8c2d4ed49912d59d71365f6278392811730e012d32a7f

后续优先完成 feature-tag 配额预选/唯一性上下文,再把普通选择接进具体装载宿主。世界分区、完整攻击型触发技能和所有武器验收仍未完成。