11 KiB
Diablo II Item Bitstream Specification (.d2s / D2Common)
Gold Standard Reference:
D2Common!6FD77180(Packing / Serialization) andD2Common!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 (
0x00terminator is 7 zero bits).
- Item base code (4 bytes / 32 bits): 4 ASCII characters right-padded with ASCII spaces (
- Two Major Modes:
- Simple / Compact Item: Items with the
Simpleflag (bit 11) set (potions, scrolls, gold, gems, runes, keys, simple quest items). Serialization terminates immediately after the item base code and socket count. - Extended Item: Items with the
Simpleflag cleared (weapons, armor, jewelry, charms, complex items). Includes full fingerprint, ilvl, quality branch, defense/durability, sockets, and stat list blocks.
- Simple / Compact Item: Items with the
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:
-
Ground / Dropping Mode (
mode == 3ormode == 5):worldX: 16 bits (World coordinate X)worldY: 16 bits (World coordinate Y)- Total: 32 bits
-
Equipped, Belt, Stored, Cursor, Socketed Mode (
mode != 3andmode != 5):equippedSlot: 4 bits (Equipped body slot index)0: None1: 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 column0..9, Belt column0..3)gridY: 4 bits (Inventory row0..9, Belt row0..3)storagePage: 3 bits:0: Inventory1: Equipped (Body)2: Belt4: Horadric Cube5: 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:
- Read
present(1 bit). - If
present == 1: readaffixId(11 bits, index intoMagicPrefix.txtorMagicSuffix.txt). - 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
- Read
statId(9 bits). - If
statId == 0x1FF: End of Stat List. - Lookup
statIdinItemStatCost.txt:CSvParam/Save Param Bits: If non-zero, readparam(CSvParambits). Used for skill ID, character class ID, elemental masteries, or charges.CSvBits/Save Bits: Read unsigned integer of lengthCSvBits.CSvSigned: Indicates signed or unsigned interpretation.Save Add: Value offset.finalValue = rawValue - SaveAdd.
- Store
{ statId, statName, param, value }in item's stat list. - 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 with0x1FF.
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 by0b0000000.
- 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):
rawFlags: number: The raw 32-bit flags integer is preserved verbatim so unknown, reserved, or engine-specific flag bits are never stripped or shifted.unknownBits?: Uint8Array: Any trailing padding bits, vendor/realm payloads, or unparsed bitstream extensions are retained alongside the structured fields.- 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.