[P0][T17] 冻结物品数据结构:一次预留 .d2s 位对齐编码所需的全部字段 #88

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

问题

M10 一旦开始改 src/game/items.ts 的 Item / ItemBase 结构,M13(.d2s 存档互通)就被锁死了。.d2s 的物品段是位对齐的变长编码,字段一旦定错,round-trip 比对就过不去,届时要把整个物品模型翻一遍。

母 issue 已经明确提醒了这一点。本 issue 的任务是在写任何加载 / 序列化代码之前,把字段清单一次定死。

金标准数据

关键事实:.d2s 的物品记录与网络 0x9C 包用的是同一个序列化函数 —— D2Common!6FD77180,逐字段调 WriteBits(D2Common!6FD592C4)。所以从这一个函数就能读出完整字段表。

整体:"JM" + 物品数(u16),随后每件物品一条位对齐变长记录,LSB-first 位序、跨字节连续、整条记录不按字节对齐(下一件紧接着写)。

头部(所有物品)

字段 位宽 备注
"JM" 16 外层写
flags 位图 32 identified / switchIn / switchOut / broken / socketed / new / badEquip / ear / startItem / simple(compact) / ethereal / personalized / runeword …
version 10 ItemData+0x30,钳 [0,1023]
location/mode 3 Unit+0x10,钳 [0,7](0 stored / 1 equipped / 2 belt / 3 ground / 4 cursor / 5 dropping / 6 socketed)
↳ mode==3/5(地上) → worldX 16
↳ mode==3/5(地上) → worldY 16
↳ 否则 → equipped slot 4 ItemData+0x44
↳ 否则 → grid X 4
↳ 否则 → grid Y 4 ⚠ 部分解析器写 3 bit,1.13c 这里是 4
↳ 否则 → storage page 3 ItemData+0x45 + 1(inventory / cube / stash)
item code 32 4 字符打包 dword(如 "gld ")
已镶嵌件数 3 钳 [0,7]

若外层传入「简单物品」标志(宝石 / 药水 / 符文等),写完这里再写一个 32 位值就返回 —— 对应 .d2s 的 simple / compact item。

扩展部分(非 simple item)

字段 位宽 备注
unique id 32 Unit+0x28(dwInitSeed)—— 词缀数值靠它复原
ilvl 7 先钳 [1,99] 再钳 [0,127]
quality 4 1 low / 2 normal / 3 superior / 4 magic / 5 set / 6 rare / 7 unique / 8 crafted
hasGraphic 1 ItemsTxt +0x23 非 0 才可能为 1
↳ graphic 3 ItemData+0x49
hasAutoAffix 1
↳ autoAffix id 11 ItemData+0x36,AutoMagic.txt 索引,钳 [0,2047]
按 quality 九路跳转(跳转表 @6FD787AC)
↳ low(1) 3 low quality 子类型
↳ superior(3) 3 superior 子类型
↳ magic(4) 11 + 11 prefix id、suffix id(各只有 1 槽 —— 这是 magic 只能 {前缀}/{后缀}/{两者} 的根本原因)
↳ set(5) 12 set item id
↳ rare(6) / crafted(8) 8 + 8 + 6×(1+11) 名 1、名 2,然后 3 前缀 + 3 后缀交替(每组 1 bit 有无 + 11 bit id)
↳ unique(7) 12 unique id(0xFFF = 无)
runeword 12 + 4 仅 runeword 标志置位
personalized name 7×N + 7 bit 0 终止 仅 personalized 标志置位
tome id 5 仅书类
realm data 1 (+96)
defense 11 仅护甲;需减 ItemStatCost 的 Save Add(10)
maxDurability 8 0 = 不可损坏(则不写 current)
curDurability 8 (+1?) 1.13c 后面疑似跟 1 bit 未用位,务必实测
quantity 9 仅 stackable
socket 总数 4 仅 socketed 标志置位
属性列表 9 bit stat id + 每 stat 的 Save Bits(含 Save Param Bits),以 0x1FF 结束 set 物品可能多段(每段一个 0x1FF);runeword 另起一段

实现要求

数据结构至少要覆盖:

version, mode,
location: { slot, x, y, page } | world: { x, y },
code(4B), id(u32), ilvl, quality,
graphic?, autoAffix?,
prefixId?, suffixId?,                    // magic
rareName1?, rareName2?, rareAffixes[6]?, // rare / crafted
setId?, uniqueId?,
runewordId?, runewordParam?,
personalizedName?, tomeId?, realmData?,
defense?, durMax?, durCur?, quantity?,
socketCount?, socketedItems[],
stats: [{ id, param, value }],
rawFlags: u32,                           // 原样保留
unknownBits: ...                         // 未识别段原样保留

陷阱

[!CAUTION]
不要「解析后丢弃未知位」。 必须保留原始 flags 与所有未识别段的原始 bit,回写时原样吐出,否则 .d2s round-trip 不可能无损。

[!CAUTION]
stat 存的是 真实值 + Save Add(来自 ItemStatCost.txt),读写两侧都要处理。负数属性就是靠 Save Add 平移成无符号存的。

[!WARNING]
flags 位图各家解析器的位号口径不一致,上表只列了名字没列位号。必须用 1.13c 真实存档实测标定,不能照抄任何一份第三方文档。

[!NOTE]
仓库已有 src/formats/bitstream.ts,先确认它的位序是 LSB-first 再复用。

验收标准

  • 产出一份字段清单文档(建议放 docs/),每个字段标注位宽、来源偏移、是否可选、依赖哪个 flag
  • Item / ItemBase interface 按清单改完,含 rawFlags 与未识别位的保留字段
  • flags 位图的每一位都用 1.13c 真实存档标定过,位号写进文档
  • 结构评审通过后才允许开始 T-01 ~ T-11
  • npm run typecheck 0 error
  • npx vitest run 全绿
  • npx tsx scripts/verify-items.ts 新增断言通过

溯源

金标准(算法) 1.13c 反汇编:D2Common!6FD77180 / D2Common!6FD592C4(objdump -d,D2Common 基址 0x6FD50000、D2Game 0x6FC10000)
参考 母 issue #25「注意」段落
本 issue 结论强度 标 ✅ 的位宽为逐指令确证;flags 位号与部分尾部字段为公开规范,需实测标定

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

## 问题 M10 一旦开始改 [`src/game/items.ts`](https://git.projectdiablo2.cn/troytt/diablo2-web/src/branch/main/src/game/items.ts) 的 `Item` / `ItemBase` 结构,M13(`.d2s` 存档互通)就被锁死了。`.d2s` 的物品段是**位对齐的变长编码**,字段一旦定错,round-trip 比对就过不去,届时要把整个物品模型翻一遍。 母 issue 已经明确提醒了这一点。本 issue 的任务是**在写任何加载 / 序列化代码之前**,把字段清单一次定死。 ## 金标准数据 **关键事实**:`.d2s` 的物品记录与网络 `0x9C` 包用的是**同一个序列化函数** —— `D2Common!6FD77180`,逐字段调 `WriteBits`(`D2Common!6FD592C4`)。所以从这一个函数就能读出完整字段表。 整体:`"JM"` + 物品数(u16),随后每件物品一条**位对齐变长记录**,**LSB-first 位序、跨字节连续、整条记录不按字节对齐**(下一件紧接着写)。 ### 头部(所有物品) | 字段 | 位宽 | 备注 | |---|---|---| | `"JM"` | 16 | 外层写 | | flags 位图 | 32 | identified / switchIn / switchOut / broken / socketed / new / badEquip / ear / startItem / **simple(compact)** / ethereal / personalized / runeword … | | **version** | **10** | `ItemData+0x30`,钳 `[0,1023]` | | **location/mode** | **3** | `Unit+0x10`,钳 `[0,7]`(0 stored / 1 equipped / 2 belt / 3 ground / 4 cursor / 5 dropping / 6 socketed) | | ↳ mode==3/5(地上) → worldX | **16** | | | ↳ mode==3/5(地上) → worldY | **16** | | | ↳ 否则 → equipped slot | **4** | `ItemData+0x44` | | ↳ 否则 → grid X | **4** | | | ↳ 否则 → grid Y | **4** | ⚠ 部分解析器写 3 bit,**1.13c 这里是 4** | | ↳ 否则 → storage page | **3** | `ItemData+0x45 + 1`(inventory / cube / stash) | | **item code** | **32** | 4 字符打包 dword(如 `"gld "`) | | **已镶嵌件数** | **3** | 钳 `[0,7]` | 若外层传入「简单物品」标志(宝石 / 药水 / 符文等),写完这里再写一个 32 位值就返回 —— 对应 `.d2s` 的 **simple / compact item**。 ### 扩展部分(非 simple item) | 字段 | 位宽 | 备注 | |---|---|---| | **unique id** | **32** | `Unit+0x28`(dwInitSeed)—— **词缀数值靠它复原** | | **ilvl** | **7** | 先钳 `[1,99]` 再钳 `[0,127]` | | **quality** | **4** | 1 low / 2 normal / 3 superior / 4 magic / 5 set / 6 rare / 7 unique / 8 crafted | | hasGraphic | **1** | ItemsTxt `+0x23` 非 0 才可能为 1 | | ↳ graphic | **3** | `ItemData+0x49` | | hasAutoAffix | **1** | | | ↳ autoAffix id | **11** | `ItemData+0x36`,`AutoMagic.txt` 索引,钳 `[0,2047]` | | **按 quality 九路跳转**(跳转表 @`6FD787AC`) | | | | ↳ low(1) | 3 | low quality 子类型 | | ↳ superior(3) | 3 | superior 子类型 | | ↳ magic(4) | 11 + 11 | prefix id、suffix id(**各只有 1 槽** —— 这是 magic 只能 {前缀}/{后缀}/{两者} 的根本原因) | | ↳ set(5) | 12 | set item id | | ↳ rare(6) / crafted(8) | 8 + 8 + 6×(1+11) | 名 1、名 2,然后 3 前缀 + 3 后缀交替(每组 1 bit 有无 + 11 bit id) | | ↳ unique(7) | 12 | unique id(`0xFFF` = 无) | | **runeword** | 12 + 4 | 仅 runeword 标志置位 | | **personalized name** | 7×N + 7 bit 0 终止 | 仅 personalized 标志置位 | | **tome id** | 5 | 仅书类 | | **realm data** | 1 (+96) | | | **defense** | 11 | 仅护甲;需减 `ItemStatCost` 的 `Save Add`(10) | | **maxDurability** | 8 | 0 = 不可损坏(则不写 current) | | **curDurability** | 8 (+1?) | 1.13c 后面疑似跟 1 bit 未用位,**务必实测** | | **quantity** | 9 | 仅 stackable | | **socket 总数** | 4 | 仅 socketed 标志置位 | | **属性列表** | 9 bit stat id + 每 stat 的 `Save Bits`(含 `Save Param Bits`),**以 `0x1FF` 结束** | set 物品可能多段(每段一个 `0x1FF`);runeword 另起一段 | ## 实现要求 数据结构至少要覆盖: ``` version, mode, location: { slot, x, y, page } | world: { x, y }, code(4B), id(u32), ilvl, quality, graphic?, autoAffix?, prefixId?, suffixId?, // magic rareName1?, rareName2?, rareAffixes[6]?, // rare / crafted setId?, uniqueId?, runewordId?, runewordParam?, personalizedName?, tomeId?, realmData?, defense?, durMax?, durCur?, quantity?, socketCount?, socketedItems[], stats: [{ id, param, value }], rawFlags: u32, // 原样保留 unknownBits: ... // 未识别段原样保留 ``` ## 陷阱 > [!CAUTION] > **不要「解析后丢弃未知位」。** 必须保留原始 flags 与所有未识别段的原始 bit,回写时原样吐出,否则 `.d2s` round-trip 不可能无损。 > [!CAUTION] > **stat 存的是 `真实值 + Save Add`**(来自 `ItemStatCost.txt`),读写两侧都要处理。负数属性就是靠 `Save Add` 平移成无符号存的。 > [!WARNING] > **flags 位图各家解析器的位号口径不一致**,上表只列了名字没列位号。必须用 1.13c 真实存档实测标定,不能照抄任何一份第三方文档。 > [!NOTE] > 仓库已有 [`src/formats/bitstream.ts`](https://git.projectdiablo2.cn/troytt/diablo2-web/src/branch/main/src/formats/bitstream.ts),先确认它的位序是 LSB-first 再复用。 ## 验收标准 - [ ] 产出一份字段清单文档(建议放 `docs/`),每个字段标注位宽、来源偏移、是否可选、依赖哪个 flag - [ ] `Item` / `ItemBase` interface 按清单改完,含 `rawFlags` 与未识别位的保留字段 - [ ] flags 位图的每一位都用 1.13c 真实存档标定过,位号写进文档 - [ ] 结构评审通过后**才允许**开始 T-01 ~ T-11 - [ ] `npm run typecheck` 0 error - [ ] `npx vitest run` 全绿 - [ ] `npx tsx scripts/verify-items.ts` 新增断言通过 --- ### 溯源 | | | |---|---| | 金标准(算法) | 1.13c 反汇编:`D2Common!6FD77180` / `D2Common!6FD592C4`(`objdump -d`,D2Common 基址 `0x6FD50000`、D2Game `0x6FC10000`) | | 参考 | 母 issue #25「注意」段落 | | 本 issue 结论强度 | 标 ✅ 的位宽为**逐指令确证**;flags 位号与部分尾部字段为公开规范,**需实测标定** | > [!WARNING] > **D2MOO 是 1.10f,不是本项目的金标准。** 它只能作线索使用。任何涉及常量、列序、整数截断位置或 RNG 消耗次数的结论,都必须回到 1.13c 的表格或 DLL 复核后再落地。
troytt added this to the [M10] 真实物品系统:500 基础物品、1000 词缀、TreasureClassEx 掉落树 milestone 2026-09-18 10:50:43 +00:00
Author
Owner

已完成并通过 D2Common!6FD77180 反汇编确证的 .d2s 位对齐规范冻结(docs/item-d2s-spec.md + src/game/items.ts),合并入 main (commit 24fbc3b)。

已完成并通过 D2Common!6FD77180 反汇编确证的 .d2s 位对齐规范冻结(docs/item-d2s-spec.md + src/game/items.ts),合并入 main (commit 24fbc3b)。
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#88
No description provided.