[P1][T14] 实现 ItemRatio 品质判定:严格复刻整数截断顺序 #102

Closed
opened 2026-09-18 10:50:50 +00:00 by troytt · 1 comment
Owner

问题

掉出基础物品后要决定它是普通 / 精良 / 魔法 / 稀有 / 套装 / 暗金。这套判定在 D2Game!6FC2FC40,本 issue 逐指令复刻。

这个 issue 的价值全在「严格」二字 —— 社区流传的公式有至少两个版本,都不完全对。照抄任何一份都会得到位级不一致的结果。

金标准数据

判定顺序(立即数直接坐实)

unique(7) → set(5) → rare(6) → magic(4) → superior(3) → normal(2) / low(1)

代码里 mov $0x7 / $0x5 / $0x6 / $0x4 / $0x3 / $0x2 的立即数顺序就是这个。

前置短路:类型记录 +0x16 != 0 → 直接 normal;ItemsTxt +0x129 != 0 → 直接 unique;类型 +0x14 != 0 且 ItemsTxt +0x12A != 0 → unique。(三个标志对应的表列名未逐一比对,【高可信推断】为「不可附魔 / 任务物品」与「唯一基底」。)

ItemRatio.txt 全 6 行(列序 = 内存序)

Function Ver Uber ClsSpec Unique Div Min Rare Div Min Set Div Min Magic Div Min HiQ Div Norm Div
Ratio 0 0 0 400 2 6400 160 3 3200 125 6 5600 30 16 192 12 16 4 8
Uber 0 1 0 240 2 6400 96 3 3200 96 6 5600 3 100 192 4 16 1 8
Ratio 1 0 0 400 1 6400 100 2 3200 160 2 5600 34 3 192 12 8 2 2
Uber 1 1 0 400 1 6400 100 2 3200 160 2 5600 34 3 192 12 8 1 1
Class Specific 1 0 1 240 3 6400 80 3 3200 120 3 5600 17 6 192 9 8 2 2
Class Specific Uber 1 1 1 240 3 6400 80 3 3200 120 3 5600 17 6 192 9 8 1 1

内存偏移(由 idivl 0x04/0x10/0x1C/0x28(%edi) 反推):

+0x00 Unique +0x04 UniqueDiv +0x08 UniqueMin
+0x0C Rare   +0x10 RareDiv   +0x14 RareMin
+0x18 Set    +0x1C SetDiv    +0x20 SetMin
+0x24 Magic  +0x28 MagicDiv  +0x2C MagicMin
+0x30 HiQual +0x34 HiQualDiv +0x38 Normal +0x3C NormalDiv

选行:按 (Version, Uber, Class Specific) 三元组精确匹配。Function 列只是人类可读名字,游戏不解析。

  • Version:0 = Classic,1 = LoD
  • Uber:1 = 用于 exceptional / elite 基底(ubercode / ultracode 档)
  • Class Specific:1 = 职业专属物品(亚马逊弓、法师护身符、德鲁伊盔、圣骑士盾、刺客爪、死灵头颅)

⭐ 确切公式

D = ilvl - qlvl                  // 带符号!qlvl = ItemsTxt +0xFD = 表里的 level 列
mf = GetMagicFind()              // stat 80 item_magicbonus,玩家 + 随从各取一次相加
hasMF = (mf != 0) && (mf > -100)

// ── unique / set / rare(factor 依次 250 / 500 / 600)──
chance = (base - D / divisor) * 128       // 整数除;*128 是 shl 7
if (hasMF) {
    e = EffectiveMF(mf, factor)
    if (e) chance = chance * 100 / e
}
if (chance < min) chance = min            // 钳制在 MF 之后、TC 因子之前
chance -= (f * chance) / 1024             // f = 沿 TC 链取 max 的品质因子;【减法】
if (chance <= 0)                      → 命中
if (Rand(monster.seed, chance) < 128) → 命中

// ── EffectiveMF(D2Game!6FC2E130)──
EffectiveMF(mf, factor):
    x = mf + 100
    if (x <= 110) return x                // 即 mf <= 10 完全不衰减
    return 100 + (mf * factor) / (mf + factor)     // 整数除

其余四档:

品质 公式 MF Min TC 因子
magic (Magic - D/MagicDiv) * 128,然后 * 100 / (mf + 100) 不衰减 有 有
superior(3) (HiQuality - D/HiQualityDiv) * 128 无 无 无
normal(2) / low(1) (Normal - D/NormalDiv) * 128(用最初保存的 D) 无 无 无

<= 0 → normal(2);否则 Rand(chance) < 128 ? normal(2) : low(1)(低质子类型见 LowQualityItems.txt,仅 1 行)。

实现要求

  1. 加载 ItemRatio.txt(6 行),按 (Version, Uber, ClassSpecific) 建索引。
  2. 严格按上面的伪代码实现,每一步的整数除都要用 Math.trunc 显式截断(JS 的 / 是浮点)。
  3. Rand 走 T-23 的 D2Rng,用怪物的 seed。
  4. TC 因子由 T-13 沿链传入。
  5. MF 从玩家 + 随从的 stat 80 取,相加。

陷阱

[!CAUTION]
三处与社区流传版本的关键差异:

  1. 不是 * 128 / (100 + MF效果),而是 * 128 之后 * 100 / EffectiveMF,且 EffectiveMF 自身已含 +100。数学上等价,但整数截断发生的位置不同,结果会差。
  2. MF 递减有门槛:mf + 100 <= 110(即 mf <= 10)时完全不衰减。
  3. magic 不做递减(直接用 mf + 100),superior / normal 完全不吃 MF。

[!CAUTION]
表的列顺序是 Unique → Rare → Set → Magic,但判定顺序是 Unique → Set → Rare → Magic。 照列顺序写循环会静默把 Set 和 Rare 对调,而两者数值接近(100/160 vs 160/125),单测极难发现。

[!CAUTION]
min 是概率的下界,但 chance 越小概率越高,所以 min 实际是概率上限。 UniqueMin = 6400 → 单次判定暗金概率上限 128/6400 = 2%。不要把它当成「保底概率」写反方向。

[!WARNING]
TC 因子是减法 chance -= f*chance/1024,所以 f = 1024 意味着 chance 变 0 → 必定命中。1.13c 里 240 行 Act N Citem X 的 Magic 列就是 1024。

[!WARNING]
D = ilvl - qlvl 带符号。ilvl < qlvl 时 D 为负,- D/divisor 变成加法,chance 变大 → 概率变低。这是正确行为(低等级怪掉高等级基底时不容易出好货)。

验收标准

  • 6 行 ItemRatio 全部加载,(Version, Uber, ClsSpec) 选行正确
  • 判定顺序单测:构造一个 chance 全部命中的场景,验证返回 unique 而非 set
  • Set / Rare 不对调:构造一个只有 Set 能命中的场景,验证不会返回 rare
  • MF 门槛单测:mf = 10 与 mf = 11 的 EffectiveMF 分别为 110 和衰减值
  • magic 不衰减、superior / normal 不吃 MF,各有单测
  • 整数截断位置:用一组精心构造的 (base, D, divisor, mf, f) 手算出期望 chance,逐位比对
  • f = 1024 时必定命中
  • D 为负时行为正确
  • npm run typecheck 0 error
  • npx vitest run 全绿
  • npx tsx scripts/verify-items.ts 新增断言通过

溯源

金标准(数据) 1.13c MPQ data\global\excel\ 下的 ItemRatio.txt / LowQualityItems.txt(本机 /usr/local/google/home/taodao/d2-data,挂载序 d2data → d2exp → Patch_D2,Patch_D2 优先)
金标准(算法) 1.13c 反汇编:D2Game!6FC2FC40 / D2Game!6FC2E130 / D2Game!6FC2F2F0(objdump -d,D2Common 基址 0x6FD50000、D2Game 0x6FC10000)
本 issue 结论强度 逐指令确证(公式、截断顺序、判定顺序、MF 门槛均直接读自反汇编);三个前置短路标志的列名为高可信推断

[!WARNING]
D2MOO 是 1.10f,不是本项目的金标准。 它只能作线索使用。任何涉及常量、列序、整数截断位置或 RNG 消耗次数的结论,都必须回到 1.13c 的表格或 DLL 复核后再落地。

## 问题 掉出基础物品后要决定它是普通 / 精良 / 魔法 / 稀有 / 套装 / 暗金。这套判定在 `D2Game!6FC2FC40`,本 issue 逐指令复刻。 **这个 issue 的价值全在「严格」二字** —— 社区流传的公式有至少两个版本,都不完全对。照抄任何一份都会得到位级不一致的结果。 ## 金标准数据 ### 判定顺序(立即数直接坐实) ``` unique(7) → set(5) → rare(6) → magic(4) → superior(3) → normal(2) / low(1) ``` 代码里 `mov $0x7 / $0x5 / $0x6 / $0x4 / $0x3 / $0x2` 的立即数顺序就是这个。 **前置短路**:类型记录 `+0x16 != 0` → 直接 normal;ItemsTxt `+0x129 != 0` → 直接 unique;类型 `+0x14 != 0` 且 ItemsTxt `+0x12A != 0` → unique。(三个标志对应的表列名未逐一比对,【高可信推断】为「不可附魔 / 任务物品」与「唯一基底」。) ### ItemRatio.txt 全 6 行(列序 = 内存序) | Function | Ver | Uber | ClsSpec | Unique | Div | Min | Rare | Div | Min | Set | Div | Min | Magic | Div | Min | HiQ | Div | Norm | Div | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| | Ratio | 0 | 0 | 0 | 400 | 2 | 6400 | 160 | 3 | 3200 | 125 | 6 | 5600 | 30 | 16 | 192 | 12 | 16 | 4 | 8 | | Uber | 0 | 1 | 0 | 240 | 2 | 6400 | 96 | 3 | 3200 | 96 | 6 | 5600 | 3 | 100 | 192 | 4 | 16 | 1 | 8 | | **Ratio** | **1** | **0** | **0** | **400** | **1** | **6400** | **100** | **2** | **3200** | **160** | **2** | **5600** | **34** | **3** | **192** | **12** | **8** | **2** | **2** | | Uber | 1 | 1 | 0 | 400 | 1 | 6400 | 100 | 2 | 3200 | 160 | 2 | 5600 | 34 | 3 | 192 | 12 | 8 | 1 | 1 | | Class Specific | 1 | 0 | 1 | 240 | 3 | 6400 | 80 | 3 | 3200 | 120 | 3 | 5600 | 17 | 6 | 192 | 9 | 8 | 2 | 2 | | Class Specific Uber | 1 | 1 | 1 | 240 | 3 | 6400 | 80 | 3 | 3200 | 120 | 3 | 5600 | 17 | 6 | 192 | 9 | 8 | 1 | 1 | 内存偏移(由 `idivl 0x04/0x10/0x1C/0x28(%edi)` 反推): ``` +0x00 Unique +0x04 UniqueDiv +0x08 UniqueMin +0x0C Rare +0x10 RareDiv +0x14 RareMin +0x18 Set +0x1C SetDiv +0x20 SetMin +0x24 Magic +0x28 MagicDiv +0x2C MagicMin +0x30 HiQual +0x34 HiQualDiv +0x38 Normal +0x3C NormalDiv ``` 选行:按 `(Version, Uber, Class Specific)` 三元组精确匹配。`Function` 列只是人类可读名字,游戏不解析。 - `Version`:0 = Classic,1 = LoD - `Uber`:1 = 用于 **exceptional / elite 基底**(`ubercode` / `ultracode` 档) - `Class Specific`:1 = 职业专属物品(亚马逊弓、法师护身符、德鲁伊盔、圣骑士盾、刺客爪、死灵头颅) ### ⭐ 确切公式 ``` D = ilvl - qlvl // 带符号!qlvl = ItemsTxt +0xFD = 表里的 level 列 mf = GetMagicFind() // stat 80 item_magicbonus,玩家 + 随从各取一次相加 hasMF = (mf != 0) && (mf > -100) // ── unique / set / rare(factor 依次 250 / 500 / 600)── chance = (base - D / divisor) * 128 // 整数除;*128 是 shl 7 if (hasMF) { e = EffectiveMF(mf, factor) if (e) chance = chance * 100 / e } if (chance < min) chance = min // 钳制在 MF 之后、TC 因子之前 chance -= (f * chance) / 1024 // f = 沿 TC 链取 max 的品质因子;【减法】 if (chance <= 0) → 命中 if (Rand(monster.seed, chance) < 128) → 命中 // ── EffectiveMF(D2Game!6FC2E130)── EffectiveMF(mf, factor): x = mf + 100 if (x <= 110) return x // 即 mf <= 10 完全不衰减 return 100 + (mf * factor) / (mf + factor) // 整数除 ``` 其余四档: | 品质 | 公式 | MF | Min | TC 因子 | |---|---|---|---|---| | magic | `(Magic - D/MagicDiv) * 128`,然后 `* 100 / (mf + 100)` | **不衰减** | 有 | 有 | | superior(3) | `(HiQuality - D/HiQualityDiv) * 128` | 无 | 无 | 无 | | normal(2) / low(1) | `(Normal - D/NormalDiv) * 128`(用最初保存的 D) | 无 | 无 | 无 | `<= 0` → normal(2);否则 `Rand(chance) < 128 ? normal(2) : low(1)`(低质子类型见 `LowQualityItems.txt`,仅 1 行)。 ## 实现要求 1. 加载 `ItemRatio.txt`(6 行),按 `(Version, Uber, ClassSpecific)` 建索引。 2. 严格按上面的伪代码实现,**每一步的整数除都要用 `Math.trunc` 显式截断**(JS 的 `/` 是浮点)。 3. `Rand` 走 T-23 的 `D2Rng`,用**怪物的 seed**。 4. TC 因子由 T-13 沿链传入。 5. MF 从玩家 + 随从的 stat 80 取,相加。 ## 陷阱 > [!CAUTION] > **三处与社区流传版本的关键差异:** > 1. **不是** `* 128 / (100 + MF效果)`,而是 `* 128` 之后 `* 100 / EffectiveMF`,且 `EffectiveMF` 自身**已含 `+100`**。数学上等价,但**整数截断发生的位置不同**,结果会差。 > 2. MF 递减有**门槛**:`mf + 100 <= 110`(即 `mf <= 10`)时**完全不衰减**。 > 3. **magic 不做递减**(直接用 `mf + 100`),**superior / normal 完全不吃 MF**。 > [!CAUTION] > **表的列顺序是 Unique → Rare → Set → Magic,但判定顺序是 Unique → Set → Rare → Magic。** 照列顺序写循环会静默把 Set 和 Rare 对调,而两者数值接近(100/160 vs 160/125),单测极难发现。 > [!CAUTION] > **`min` 是概率的下界,但 chance 越小概率越高,所以 `min` 实际是概率上限。** `UniqueMin = 6400` → 单次判定暗金概率上限 `128/6400 = 2%`。不要把它当成「保底概率」写反方向。 > [!WARNING] > **TC 因子是减法** `chance -= f*chance/1024`,所以 `f = 1024` 意味着 `chance` 变 0 → **必定命中**。1.13c 里 240 行 `Act N Citem X` 的 `Magic` 列就是 1024。 > [!WARNING] > `D = ilvl - qlvl` **带符号**。ilvl < qlvl 时 D 为负,`- D/divisor` 变成加法,chance 变大 → 概率变低。这是正确行为(低等级怪掉高等级基底时不容易出好货)。 ## 验收标准 - [ ] 6 行 ItemRatio 全部加载,`(Version, Uber, ClsSpec)` 选行正确 - [ ] 判定顺序单测:构造一个 chance 全部命中的场景,验证返回 unique 而非 set - [ ] **Set / Rare 不对调**:构造一个只有 Set 能命中的场景,验证不会返回 rare - [ ] MF 门槛单测:`mf = 10` 与 `mf = 11` 的 EffectiveMF 分别为 110 和衰减值 - [ ] magic 不衰减、superior / normal 不吃 MF,各有单测 - [ ] 整数截断位置:用一组精心构造的 `(base, D, divisor, mf, f)` 手算出期望 chance,逐位比对 - [ ] `f = 1024` 时必定命中 - [ ] `D` 为负时行为正确 - [ ] `npm run typecheck` 0 error - [ ] `npx vitest run` 全绿 - [ ] `npx tsx scripts/verify-items.ts` 新增断言通过 --- ### 溯源 | | | |---|---| | 金标准(数据) | 1.13c MPQ `data\global\excel\` 下的 `ItemRatio.txt` / `LowQualityItems.txt`(本机 `/usr/local/google/home/taodao/d2-data`,挂载序 d2data → d2exp → Patch_D2,**Patch_D2 优先**) | | 金标准(算法) | 1.13c 反汇编:`D2Game!6FC2FC40` / `D2Game!6FC2E130` / `D2Game!6FC2F2F0`(`objdump -d`,D2Common 基址 `0x6FD50000`、D2Game `0x6FC10000`) | | 本 issue 结论强度 | **逐指令确证**(公式、截断顺序、判定顺序、MF 门槛均直接读自反汇编);三个前置短路标志的列名为高可信推断 | > [!WARNING] > **D2MOO 是 1.10f,不是本项目的金标准。** 它只能作线索使用。任何涉及常量、列序、整数截断位置或 RNG 消耗次数的结论,都必须回到 1.13c 的表格或 DLL 复核后再落地。
troytt added this to the [M10] 真实物品系统:500 基础物品、1000 词缀、TreasureClassEx 掉落树 milestone 2026-09-18 10:50:50 +00:00
Author
Owner

已完成并通过 1.13c ItemRatio.txt 判定实测验证(Unique/Set/Rare/Magic/Superior/Normal 6层阶梯判定、Diminishing MF 惩罚曲线、class-specific 调整、fallback 保底降级与 durability 衰减计算),合并入 main (commit 6991ffa)。

已完成并通过 1.13c ItemRatio.txt 判定实测验证(Unique/Set/Rare/Magic/Superior/Normal 6层阶梯判定、Diminishing MF 惩罚曲线、class-specific 调整、fallback 保底降级与 durability 衰减计算),合并入 main (commit 6991ffa)。
Sign in to join this conversation.
No Label
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#102
No description provided.