188 lines
11 KiB
Markdown
188 lines
11 KiB
Markdown
# Diablo II Item Bitstream Specification (.d2s / D2Common)
|
|
|
|
> **Gold Standard Reference**: `D2Common!6FD77180` (Packing / Serialization) and `D2Common!6FD592C4` (Unpacking / Deserialization).
|
|
> **Target Version**: Diablo II 1.10 / 1.13c Lord of Destruction character save (`.d2s`) item list.
|
|
|
|
---
|
|
|
|
## 1. Overview and Architecture
|
|
|
|
Diablo II stores item data inside character save files (`.d2s`), network packets, and memory structures as a **variable-length, bit-aligned stream**. Unlike byte-aligned structures, fields in the item stream are packed tightly with arbitrary bit widths (e.g. 1 bit, 3 bits, 7 bits, 9 bits, 11 bits, 12 bits) without byte padding between fields.
|
|
|
|
### 1.1 Bitstream Conventions
|
|
- **Byte and Bit Ordering**: Little-endian bitstream. Bits are read from lowest to highest within each byte (LSB first: bit 0 of byte 0, bit 1 of byte 0, ..., bit 7 of byte 0, bit 0 of byte 1, etc.).
|
|
- **String Encoding**:
|
|
- Item base code (4 bytes / 32 bits): 4 ASCII characters right-padded with ASCII spaces (`0x20`), stored in byte order.
|
|
- Personalized and Ear names: Null-terminated strings using 7-bit ASCII characters (`0x00` terminator is 7 zero bits).
|
|
- **Two Major Modes**:
|
|
1. **Simple / Compact Item**: Items with the `Simple` flag (bit 11) set (potions, scrolls, gold, gems, runes, keys, simple quest items). Serialization terminates immediately after the item base code and socket count.
|
|
2. **Extended Item**: Items with the `Simple` flag cleared (weapons, armor, jewelry, charms, complex items). Includes full fingerprint, ilvl, quality branch, defense/durability, sockets, and stat list blocks.
|
|
|
|
---
|
|
|
|
## 2. Complete Bitstream Layout
|
|
|
|
The table below outlines the sequential bitstream order executed by `D2Common!6FD592C4` (unpack) and `D2Common!6FD77180` (pack):
|
|
|
|
| Bit Offset (Rel) | Bit Width | Field Name | Type | Condition / Presence | Description |
|
|
|---|---|---|---|---|---|
|
|
| `0` | 16 | `magic` | ASCII | Always | `"JM"` header magic (`0x4A`, `0x4D`) |
|
|
| `16` | 32 | `dwFlags` | Bitmask | Always | Header flags (see §3 for bitmask breakdown) |
|
|
| `48` | 10 | `version` | uint | Always | Item format version (`0` or `101` for 1.10+) |
|
|
| `58` | 3 | `mode` | uint | Always | Location mode (0: Stored, 1: Equipped, 2: Belt, 3: Ground, 4: Cursor, 5: Dropping, 6: Socketed) |
|
|
| `61` | 32 or 15 | **Position Data** | Branch | Always | Dependent on `mode` (see §2.1 below) |
|
|
| — | 32 | `itemCode` | ASCII[4] | Always (except Ear) | 4 ASCII characters right-padded with space (e.g. `"swd "`, `"gld "`) |
|
|
| — | 3 | `socketedCount` | uint | Always (except Ear) | Number of gems/runes/jewels socketed inside this item (0..7) |
|
|
| *CUTOFF* | — | *Simple Item End* | — | `dwFlags.Simple == 1` | **If Simple Item flag is set, serialization STOPS here.** |
|
|
| — | 32 | `uniqueId` | uint32 | Extended item | Item GUID / seed (`dwInitSeed`, item fingerprint) |
|
|
| — | 7 | `ilvl` | uint | Extended item | Item level (0..127) |
|
|
| — | 4 | `quality` | uint | Extended item | Item quality tier (1..8, see §4) |
|
|
| — | 1 | `hasGraphic` | bool | Extended item | 1 if graphic variation index is present |
|
|
| — | 3 | `graphic` | uint | `hasGraphic == 1` | Graphic variant index (0..7) |
|
|
| — | 1 | `hasAutoAffix` | bool | Extended item | 1 if class auto-affix (Automagic) is present |
|
|
| — | 11 | `autoAffixId` | uint | `hasAutoAffix == 1` | Automagic affix ID (from `AutoMagic.txt`) |
|
|
| — | Variable | **Quality Data** | Branch | Extended item | Dependent on `quality` (see §4) |
|
|
| — | 16 | `runewordData` | uint | `dwFlags.Runeword == 1` | 12-bit Runeword ID (`Runes.txt`) + 4-bit parameter |
|
|
| — | Variable | `personalizedName`| String | `dwFlags.Personalized == 1`| 7-bit ASCII characters terminated by `0b0000000` (max 15 chars) |
|
|
| — | 5 | `tomeId` | uint | Code == `"ibk "` or `"tbk "` | Tome ID (5 bits) |
|
|
| — | 1 + 96 | `realmData` | Bytes | Extended item | 1 bit flag; if set, followed by 96 bits of realm data |
|
|
| — | 11 | `defense` | uint | Item kind is Armor | Current armor defense (value - 10 + `Armor.minac`) |
|
|
| — | 8 (+8) | `durability` | uint | Item has Durability | `maxDurability` (8 bits); if max > 0: `currentDurability` (8 bits) |
|
|
| — | 9 | `quantity` | uint | Item is Stackable | Current stack count (e.g. javelins, keys, arrows) |
|
|
| — | 4 | `totalSockets` | uint | `dwFlags.Socketed == 1` | Total number of sockets on the item (1..6) |
|
|
| — | Variable | **Set Bonuses** | Branch | `quality == 5` (Set) | 5 bits property list count; active set bonus stat lists (each terminated by `0x1FF`) |
|
|
| — | Variable | **Base Stat List** | StatList | Extended item | Stat modifier list (9-bit statId + params/values), terminated by `0x1FF` |
|
|
| — | Variable | **Runeword Stats** | StatList | `dwFlags.Runeword == 1` | Extra runeword stat list, terminated by `0x1FF` |
|
|
|
|
---
|
|
|
|
### 2.1 Positioning Branch (Offset 61)
|
|
|
|
The position encoding branches on `mode`:
|
|
|
|
1. **Ground / Dropping Mode (`mode == 3` or `mode == 5`)**:
|
|
- `worldX`: 16 bits (World coordinate X)
|
|
- `worldY`: 16 bits (World coordinate Y)
|
|
- *Total*: 32 bits
|
|
|
|
2. **Equipped, Belt, Stored, Cursor, Socketed Mode (`mode != 3` and `mode != 5`)**:
|
|
- `equippedSlot`: 4 bits (Equipped body slot index)
|
|
- `0`: None
|
|
- `1`: Head (Helm)
|
|
- `2`: Neck (Amulet)
|
|
- `3`: Torso (Armor)
|
|
- `4`: Right Hand (Primary Weapon / Shield)
|
|
- `5`: Left Hand (Secondary Weapon / Shield)
|
|
- `6`: Right Finger (Ring)
|
|
- `7`: Left Finger (Ring)
|
|
- `8`: Waist (Belt)
|
|
- `9`: Feet (Boots)
|
|
- `10`: Hands (Gloves)
|
|
- `11`: Alt Right Hand (Weapon swap)
|
|
- `12`: Alt Left Hand (Weapon swap)
|
|
- `gridX`: 4 bits (Inventory column `0..9`, Belt column `0..3`)
|
|
- `gridY`: 4 bits (Inventory row `0..9`, Belt row `0..3`)
|
|
- `storagePage`: 3 bits:
|
|
- `0`: Inventory
|
|
- `1`: Equipped (Body)
|
|
- `2`: Belt
|
|
- `4`: Horadric Cube
|
|
- `5`: Stash
|
|
- *Total*: 15 bits
|
|
|
|
---
|
|
|
|
## 3. Header Flags Bitmask (`dwFlags`, 32 bits)
|
|
|
|
The 32-bit integer at bit offset 16 controls item state and conditional decoding:
|
|
|
|
| Bit | Hex Value | Name | Description |
|
|
|---|---|---|---|
|
|
| `0` | `0x00000001` | `Identified` | `1` = Item is identified; `0` = Unidentified |
|
|
| `1` | `0x00000002` | `Unk1` | Reserved / unused |
|
|
| `2` | `0x00000004` | `Unk2` | Reserved / unused |
|
|
| `3` | `0x00000008` | `Socketed` | `1` = Item has sockets (and socket count field present) |
|
|
| `4` | `0x00000010` | `NewItem` | `1` = Item picked up or generated since game start |
|
|
| `5` | `0x00000020` | `Unk5` | Reserved / unused |
|
|
| `6` | `0x00000040` | `Unk6` | Reserved / unused |
|
|
| `7` | `0x00000080` | `IsEar` | `1` = Item is a player Ear |
|
|
| `8` | `0x00000100` | `StarterItem` | `1` = Starter / beginner equipment (e.g. cracked sash) |
|
|
| `9` | `0x00000200` | `Unk9` | Reserved / unused |
|
|
| `10` | `0x00000400` | `Unk10` | Reserved / unused |
|
|
| `11` | `0x00000800` | `Simple` | `1` = Compact item (stops after socketed count); `0` = Extended item |
|
|
| `12` | `0x00001000` | `Ethereal` | `1` = Ethereal (cannot be repaired, translucent art, stat bonus) |
|
|
| `13` | `0x00002000` | `Any` | Internal personalization / save flag |
|
|
| `14` | `0x00004000` | `Personalized` | `1` = Personalized name present (Anya quest reward) |
|
|
| `15` | `0x00008000` | `Gamble` | Gambling item flag |
|
|
| `16` | `0x00010000` | `Runeword` | `1` = Active Runeword (runeword data and stats present) |
|
|
| `17..31` | `0xFFFE0000` | `Reserved` | Engine and server internal flags |
|
|
|
|
---
|
|
|
|
## 4. Quality Field and Branches (4 bits)
|
|
|
|
The 4-bit `quality` field dictates the affix and identity layout:
|
|
|
|
| Value | Quality Tier | Encoded Fields |
|
|
|---|---|---|
|
|
| `1` | **Low Quality** | `qualitySubtype` (3 bits: `0` crude, `1` cracked, `2` damaged, `3` low quality) |
|
|
| `2` | **Normal** | None (standard base item) |
|
|
| `3` | **Superior** | `superiorSubtype` (3 bits: attack rating, defense, durability, etc.) |
|
|
| `4` | **Magic** | `prefixId` (11 bits, `MagicPrefix.txt`), `suffixId` (11 bits, `MagicSuffix.txt`) |
|
|
| `5` | **Set** | `setId` (12 bits, index into `SetItems.txt`) |
|
|
| `6` | **Rare** | `rareName1` (8 bits), `rareName2` (8 bits), followed by 6 affix slots (each 1 bit present + 11-bit id) |
|
|
| `7` | **Unique** | `uniqueId` (12 bits, index into `UniqueItems.txt`) |
|
|
| `8` | **Crafted** | `rareName1` (8 bits), `rareName2` (8 bits), followed by 6 affix slots (each 1 bit present + 11-bit id) |
|
|
| `9` | **Tempered** | Rare-style affix structure (expansion prototype remnant) |
|
|
|
|
### 4.1 Rare & Crafted Affix Slots (6 Slots)
|
|
Rare (6) and Crafted (8) items carry exactly 6 affix slots. For each slot `i = 0..5`:
|
|
1. Read `present` (1 bit).
|
|
2. If `present == 1`: read `affixId` (11 bits, index into `MagicPrefix.txt` or `MagicSuffix.txt`).
|
|
3. If `present == 0`: move to next slot.
|
|
|
|
---
|
|
|
|
## 5. Stat Lists and `ItemStatCost.txt` Bit Alignment
|
|
|
|
Diablo II encodes magical bonuses and base stats as an open-ended list of properties, terminated by the 9-bit sentinel `0x1FF` (decimal `511`).
|
|
|
|
### 5.1 Stat Entry Encoding Loop
|
|
1. Read `statId` (9 bits).
|
|
2. If `statId == 0x1FF`: **End of Stat List**.
|
|
3. Lookup `statId` in `ItemStatCost.txt`:
|
|
- `CSvParam` / `Save Param Bits`: If non-zero, read `param` (`CSvParam` bits). Used for skill ID, character class ID, elemental masteries, or charges.
|
|
- `CSvBits` / `Save Bits`: Read unsigned integer of length `CSvBits`.
|
|
- `CSvSigned`: Indicates signed or unsigned interpretation.
|
|
- `Save Add`: Value offset. `finalValue = rawValue - SaveAdd`.
|
|
4. Store `{ statId, statName, param, value }` in item's stat list.
|
|
5. Repeat from Step 1.
|
|
|
|
### 5.2 Multiple Stat Blocks
|
|
Extended items can contain multiple stat list blocks in sequence:
|
|
- **Base Stats Block**: Always present on extended items; ends with `0x1FF`.
|
|
- **Set Item Bonuses**: For Set items, 5 bits indicate bonus properties; each active set bonus tier has its own stat list ending with `0x1FF`.
|
|
- **Runeword Bonuses**: For Runewords (`dwFlags.Runeword == 1`), a dedicated runeword bonus stat list follows, ending with `0x1FF`.
|
|
|
|
---
|
|
|
|
## 6. Player Ear Items (`dwFlags.IsEar == 1`)
|
|
|
|
If bit 7 of `dwFlags` is set:
|
|
- Item code is `"ear "`.
|
|
- Followed by:
|
|
- `classIndex`: 3 bits (0: Amazon, 1: Sorceress, 2: Necromancer, 3: Paladin, 4: Barbarian, 5: Druid, 6: Assassin).
|
|
- `level`: 7 bits (Player level at time of death).
|
|
- `playerName`: 7-bit ASCII characters terminated by `0b0000000`.
|
|
- Ear decoding terminates here; no extended stats or durability are encoded.
|
|
|
|
---
|
|
|
|
## 7. Lossless Round-Trip and Preservation Guarantee
|
|
|
|
To ensure perfect interoperability between this engine, official Diablo II binaries, and third-party save editors (e.g. Hero Editor, Gomule):
|
|
|
|
1. **`rawFlags: number`**: The raw 32-bit flags integer is preserved verbatim so unknown, reserved, or engine-specific flag bits are never stripped or shifted.
|
|
2. **`unknownBits?: Uint8Array`**: Any trailing padding bits, vendor/realm payloads, or unparsed bitstream extensions are retained alongside the structured fields.
|
|
3. **Deterministic Serialization**: When re-packing to bitstream via `D2Common!6FD77180`, fields are written back in the identical sequence and bit alignments specified in this document.
|