diablo2-web/docs/item-d2s-spec.md

11 KiB

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.