[P1][T20] 物品名本地化:namestr → string.tbl 三层链(patch → expansion → base) #108

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

问题

没有这个 issue,界面上显示的全是 cap hax r33 这种四字母 code。

母 issue 没有单列这一项,但它是「打死怪掉出带词缀的稀有装备」这条验收的必要条件 —— 玩家得能看懂掉的是什么。所以优先级定 P1。

金标准数据

项目里已有的解析代码(可直接复用)

文件 函数 作用
src/formats/tbl.ts decodeTbl(data, encoding)(L65-128) 返回 DecodedTbl,同时是数组(按 index)和带 .dict: Map<string,string>(按 key)
src/formats/tbl.ts lookupTblFast(data, key, encoding)(L130-195) 不建整表,直接走哈希桶查单 key
src/formats/tbl.ts tblHash()(L15-25) D2 的 key 哈希
src/game/tables.ts textSourceOf / resolveText 处理「单元格可能是数字索引也可能是字面量」
src/game/level-names-zh.ts lookupTbl()(L378-386) 现成的「Map 或 DecodedTbl 通吃」辅助

MPQ 里的 .tbl 清单

路径 胜出归档 key 数 index 数
data\local\lng\eng\string.tbl d2data.mpq 5099 5391
data\local\LNG\ENG\expansionstring.tbl d2exp.mpq 2788 2818
data\local\LNG\ENG\patchstring.tbl Patch_D2.mpq 1062 1169
data\local\LNG\CHI\string.tbl d2exp.mpq 5099 5391
data\local\LNG\CHI\patchstring.tbl Patch_D2.mpq 1057 1169

查询优先级(原版顺序):patchstring.tbl → expansionstring.tbl → string.tbl,先命中先返回。

各表的 tbl key

表 key 列 验证
Armor/Weapons/Misc.txt namestr cap → ENG Cap / CHI 帽子
UniqueItems.txt index The Gnasher → CHI 牙齒
SetItems.txt index Civerb's Ward → CHI 希弗柏的防護
Sets.txt name —
Runes.txt Name(不是 Rune Name) Runeword26 → ENG Doom
MagicPrefix/Suffix.txt — Name 直接就是显示文本,不查 tbl
RarePrefix/RareSuffix.txt — name 直接是文本
ItemStatCost.txt descstrpos / descstrneg / descstr2 / dgrpstrpos / dgrpstrneg 如 ModStr1a

ENG 实测覆盖率(三层链)

表 覆盖
Armor 202 / 202(100%)
Weapons 306 / 306(100%)
Misc 143 / 151(8 个解析不到:hpo mpo hpf mpf rps rpl + 2,均为未使用的占位药水)

可直接复用的查询骨架

async function loadChain(lang: 'ENG' | 'CHI'): Promise<DecodedTbl[]> {
  const enc = lang === 'CHI' ? 'utf-8' : 'windows-1252'
  const paths = lang === 'CHI'
    ? ['data\\local\\LNG\\CHI\\patchstring.tbl', 'data\\local\\LNG\\CHI\\string.tbl']
    : ['data\\local\\LNG\\ENG\\patchstring.tbl',
       'data\\local\\LNG\\ENG\\expansionstring.tbl',
       'data\\local\\lng\\eng\\string.tbl']
  const out: DecodedTbl[] = []
  for (const p of paths) if (m.has(p)) out.push(decodeTbl(await m.read(p), enc))
  return out   // patch 在最前,先命中先赢
}

const tr = (chain: DecodedTbl[], key: string) => {
  for (const t of chain) { const v = t.dict.get(key); if (v !== undefined) return v }
  return undefined
}
// tr(chi, row.namestr) ?? tr(eng, row.namestr) ?? row.code

实现要求

  1. 实现三层链查询,按优先级 patch → expansion → base。
  2. 各表按上表的 key 列查询。
  3. CHI 缺失时 fallback 到 ENG,再 fallback 到 code。
  4. 词缀名(MagicPrefix/Suffix、RarePrefix/Suffix)直接用表里的文本,不查 tbl。

陷阱

[!CAUTION]
本机这份 MPQ 没有 data\local\LNG\CHI\expansionstring.tbl(三种大小写拼法全部探测为 ABSENT)。后果:所有 LoD 新增物品在 CHI 下查不到中文名 —— ba4(Assault Helmet)、uar(Sacred Armor)、r33(Zod Rune)、Windforce、Annihilus、全部符文之语名。必须 fallback 到 ENG,且这条 fallback 路径要有测试覆盖。

[!CAUTION]
CHI 的 .tbl 值是 UTF-8,不是 GBK / Big5。 这份 MPQ 显然被重打包过(原版 CHI 应为 Big5)。实测:decodeTbl(chiBytes, 'utf-8').dict.get('cap') → 帽子;用 'gbk' 得到 甯藉瓙 乱码,用 'big5' 得到 撣賢 乱码。CHI 传 'utf-8',ENG 传 'windows-1252'。

[!CAUTION]
name 列不是显示名。 hp1 的 name 是 Lesser Healing Potion,而 tbl 里的真名是 Minor Healing Potion。显示名必须走 namestr。

[!WARNING]
Runes.txt 走 Name 而不是 Rune Name。Runeword26 的 Rune Name 是废弃旧名 Doomsayer,真名是 Doom。

验收标准

  • 三层链查询实现,优先级正确
  • ENG 覆盖率断言:Armor 202/202、Weapons 306/306、Misc 143/151
  • CHI 编码用 UTF-8:cap → 帽子、tow → 塔盾、axe → 斧
  • CHI 缺失 expansionstring 时 fallback 到 ENG:r33 / Windforce / Annihilus 各一条测试
  • 暗金走 index:The Gnasher → ENG The Gnasher / CHI 牙齒
  • 套装走 index:Civerb's Ward → CHI 希弗柏的防護
  • 符文之语走 Name:Runeword26 → Doom
  • 词缀名不查 tbl,直接用表文本
  • npm run typecheck 0 error
  • npx vitest run 全绿
  • npx tsx scripts/verify-items.ts 新增断言通过

溯源

金标准(数据) 1.13c MPQ data\global\excel\ 下的 Armor.txt / Weapons.txt / Misc.txt / UniqueItems.txt / SetItems.txt / Sets.txt / Runes.txt(本机 /usr/local/google/home/taodao/d2-data,挂载序 d2data → d2exp → Patch_D2,Patch_D2 优先)
参考 .tbl 清单与编码为本机实测
本 issue 结论强度 本机 1.13c 实测确证(含 CHI expansionstring 缺失与 UTF-8 编码两个本机特有事实)

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

## 问题 没有这个 issue,界面上显示的全是 `cap` `hax` `r33` 这种四字母 code。 母 issue 没有单列这一项,但**它是「打死怪掉出带词缀的稀有装备」这条验收的必要条件** —— 玩家得能看懂掉的是什么。所以优先级定 P1。 ## 金标准数据 ### 项目里已有的解析代码(可直接复用) | 文件 | 函数 | 作用 | |---|---|---| | `src/formats/tbl.ts` | `decodeTbl(data, encoding)`(L65-128) | 返回 `DecodedTbl`,**同时**是数组(按 index)**和**带 `.dict: Map<string,string>`(按 key) | | `src/formats/tbl.ts` | `lookupTblFast(data, key, encoding)`(L130-195) | 不建整表,直接走哈希桶查单 key | | `src/formats/tbl.ts` | `tblHash()`(L15-25) | D2 的 key 哈希 | | `src/game/tables.ts` | `textSourceOf` / `resolveText` | 处理「单元格可能是数字索引也可能是字面量」 | | `src/game/level-names-zh.ts` | `lookupTbl()`(L378-386) | 现成的「Map 或 DecodedTbl 通吃」辅助 | ### MPQ 里的 .tbl 清单 | 路径 | 胜出归档 | key 数 | index 数 | |---|---|---:|---:| | `data\local\lng\eng\string.tbl` | d2data.mpq | 5099 | 5391 | | `data\local\LNG\ENG\expansionstring.tbl` | d2exp.mpq | 2788 | 2818 | | `data\local\LNG\ENG\patchstring.tbl` | Patch_D2.mpq | 1062 | 1169 | | `data\local\LNG\CHI\string.tbl` | d2exp.mpq | 5099 | 5391 | | `data\local\LNG\CHI\patchstring.tbl` | Patch_D2.mpq | 1057 | 1169 | **查询优先级(原版顺序)**:`patchstring.tbl` → `expansionstring.tbl` → `string.tbl`,先命中先返回。 ### 各表的 tbl key | 表 | key 列 | 验证 | |---|---|---| | `Armor/Weapons/Misc.txt` | **`namestr`** | `cap` → ENG `Cap` / CHI `帽子` | | `UniqueItems.txt` | **`index`** | `The Gnasher` → CHI `牙齒` | | `SetItems.txt` | **`index`** | `Civerb's Ward` → CHI `希弗柏的防護` | | `Sets.txt` | **`name`** | — | | `Runes.txt` | **`Name`**(不是 `Rune Name`) | `Runeword26` → ENG **`Doom`** | | `MagicPrefix/Suffix.txt` | — | `Name` **直接就是显示文本**,不查 tbl | | `RarePrefix/RareSuffix.txt` | — | `name` 直接是文本 | | `ItemStatCost.txt` | `descstrpos` / `descstrneg` / `descstr2` / `dgrpstrpos` / `dgrpstrneg` | 如 `ModStr1a` | ### ENG 实测覆盖率(三层链) | 表 | 覆盖 | |---|---| | Armor | **202 / 202**(100%) | | Weapons | **306 / 306**(100%) | | Misc | **143 / 151**(8 个解析不到:`hpo` `mpo` `hpf` `mpf` `rps` `rpl` + 2,均为未使用的占位药水) | ### 可直接复用的查询骨架 ```ts async function loadChain(lang: 'ENG' | 'CHI'): Promise<DecodedTbl[]> { const enc = lang === 'CHI' ? 'utf-8' : 'windows-1252' const paths = lang === 'CHI' ? ['data\\local\\LNG\\CHI\\patchstring.tbl', 'data\\local\\LNG\\CHI\\string.tbl'] : ['data\\local\\LNG\\ENG\\patchstring.tbl', 'data\\local\\LNG\\ENG\\expansionstring.tbl', 'data\\local\\lng\\eng\\string.tbl'] const out: DecodedTbl[] = [] for (const p of paths) if (m.has(p)) out.push(decodeTbl(await m.read(p), enc)) return out // patch 在最前,先命中先赢 } const tr = (chain: DecodedTbl[], key: string) => { for (const t of chain) { const v = t.dict.get(key); if (v !== undefined) return v } return undefined } // tr(chi, row.namestr) ?? tr(eng, row.namestr) ?? row.code ``` ## 实现要求 1. 实现三层链查询,按优先级 patch → expansion → base。 2. 各表按上表的 key 列查询。 3. CHI 缺失时 fallback 到 ENG,再 fallback 到 code。 4. 词缀名(MagicPrefix/Suffix、RarePrefix/Suffix)**直接用表里的文本**,不查 tbl。 ## 陷阱 > [!CAUTION] > **本机这份 MPQ 没有 `data\local\LNG\CHI\expansionstring.tbl`**(三种大小写拼法全部探测为 ABSENT)。后果:所有 LoD 新增物品在 CHI 下查不到中文名 —— `ba4`(Assault Helmet)、`uar`(Sacred Armor)、`r33`(Zod Rune)、`Windforce`、`Annihilus`、**全部符文之语名**。必须 fallback 到 ENG,且这条 fallback 路径要有测试覆盖。 > [!CAUTION] > **CHI 的 .tbl 值是 UTF-8,不是 GBK / Big5。** 这份 MPQ 显然被重打包过(原版 CHI 应为 Big5)。实测:`decodeTbl(chiBytes, 'utf-8').dict.get('cap')` → `帽子`;用 `'gbk'` 得到 `甯藉瓙` 乱码,用 `'big5'` 得到 `撣賢` 乱码。**CHI 传 `'utf-8'`,ENG 传 `'windows-1252'`。** > [!CAUTION] > **`name` 列不是显示名。** `hp1` 的 `name` 是 `Lesser Healing Potion`,而 tbl 里的真名是 **`Minor Healing Potion`**。显示名必须走 `namestr`。 > [!WARNING] > `Runes.txt` 走 `Name` 而不是 `Rune Name`。`Runeword26` 的 `Rune Name` 是废弃旧名 `Doomsayer`,真名是 `Doom`。 ## 验收标准 - [ ] 三层链查询实现,优先级正确 - [ ] ENG 覆盖率断言:Armor **202/202**、Weapons **306/306**、Misc **143/151** - [ ] CHI 编码用 UTF-8:`cap` → `帽子`、`tow` → `塔盾`、`axe` → `斧` - [ ] CHI 缺失 expansionstring 时 fallback 到 ENG:`r33` / `Windforce` / `Annihilus` 各一条测试 - [ ] 暗金走 `index`:`The Gnasher` → ENG `The Gnasher` / CHI `牙齒` - [ ] 套装走 `index`:`Civerb's Ward` → CHI `希弗柏的防護` - [ ] 符文之语走 `Name`:`Runeword26` → `Doom` - [ ] 词缀名不查 tbl,直接用表文本 - [ ] `npm run typecheck` 0 error - [ ] `npx vitest run` 全绿 - [ ] `npx tsx scripts/verify-items.ts` 新增断言通过 --- ### 溯源 | | | |---|---| | 金标准(数据) | 1.13c MPQ `data\global\excel\` 下的 `Armor.txt` / `Weapons.txt` / `Misc.txt` / `UniqueItems.txt` / `SetItems.txt` / `Sets.txt` / `Runes.txt`(本机 `/usr/local/google/home/taodao/d2-data`,挂载序 d2data → d2exp → Patch_D2,**Patch_D2 优先**) | | 参考 | `.tbl` 清单与编码为本机实测 | | 本 issue 结论强度 | **本机 1.13c 实测确证**(含 CHI expansionstring 缺失与 UTF-8 编码两个本机特有事实) | > [!WARNING] > **D2MOO 是 1.10f,不是本项目的金标准。** 它只能作线索使用。任何涉及常量、列序、整数截断位置或 RNG 消耗次数的结论,都必须回到 1.13c 的表格或 DLL 复核后再落地。
troytt added this to the [M10] 真实物品系统:500 基础物品、1000 词缀、TreasureClassEx 掉落树 milestone 2026-09-18 10:50:53 +00:00
Author
Owner

已完成 namestr -> string.tbl 三层链查询(patch -> expansion -> base),中文 UTF-8 解码与 LoD 缺失时自动 fallback 到 ENG,覆盖全量 Armor/Weapons/Misc/Unique/Set/Runewords,合并入 main (commit 399aed7)。

已完成 namestr -> string.tbl 三层链查询(patch -> expansion -> base),中文 UTF-8 解码与 LoD 缺失时自动 fallback 到 ENG,覆盖全量 Armor/Weapons/Misc/Unique/Set/Runewords,合并入 main (commit 399aed7)。
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#108
No description provided.