Skip to content

krisives/d2s-format

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 

Repository files navigation

Buy Me A
Coffee

Diablo II Save File Format

Diablo II stores your game character on disk as a .d2s file. This is a binary file format that encodes all of the stats, items, name, and other pieces of data.

Integers are stored in little endian byte order, which is the native byte ordering on a x86 architecture Diablo II is based on.

Header

Each .d2s file starts with a 765 byte header, after which data is of variable length.

Hex Byte Length Desc
0x00 0 4 Signature (0xaa55aa55)
0x04 4 4 Version ID
0x08 8 4 File size
0x0C 12 4 Checksum
0x10 16 4 Active Weapon
0x14 20 16 Character Name
0x24 36 1 Character Status
0x25 37 1 Character Progression
0x26 38 2 ?
0x28 40 1 Character Class
0x29 41 2 ?
0x2B 43 1 Level
0x2C 44 4 ?
0x30 48 4 Time
0x34 52 4 ?
0x38 56 64 Hotkeys
0x78 120 4 Left Mouse
0x7C 124 4 Right Mouse
0x80 128 4 Left Mouse (weapon switch)
0x84 132 4 Right Mouse (weapon switch)
0x88 136 32 Character Menu Appearance
0xA8 168 3 Difficulty
0xAB 171 4 Map
0xAF 175 2 ?
0xB1 177 2 Merc dead?
0xB3 179 4 Merc seed?
0xB7 183 2 Merc Name ID
0xB9 185 2 Merc Type
0xBB 187 4 Merc Experience
0xBF 191 144 ?
0x14F 335 298 Quest
0x279 633 81 Waypoint
0x2CA 714 51 NPC
0x2FD 765 Stats
Items

Versions

File version. The following values are known:

  • 71 is 1.00 through v1.06
  • 87 is 1.07 or Expansion Set v1.08
  • 89 is standard game v1.08
  • 92 is v1.09 (both the standard game and the Expansion Set.)
  • 96 is v1.10+

Checksum

To calculate the checksum set the value of it in the .d2s data to be zero and iterate through all the bytes in the data calculating a 32-bit checksum:

code in C
    sum = (sum << 1) + data[i];
code in JavaScript

source: #5

const fs = require("fs");
const path = require("path");
const file = path.join(process.cwd(), "path_to_save.d2s");

function calculateSum(data) {
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    let ch = data[i];
    if (i >= 12 && i < 16) {
      ch = 0;
    }
    ch += sum < 0;
    sum = (sum << 1) + ch;
  }

  return sum;
}

function littleToBigEndian(number) {
  return new DataView(
    Int32Array.of(
      new DataView(Int32Array.of(number).buffer).getUint32(0, true)
    ).buffer
  );
}

function ashex(buffer) {
  return buffer.getUint32(0, false).toString(16);
}

async function readSafeFile() {
  return await new Promise((resolve, reject) => {
    fs.readFile(file, (err, data) => {
      if (err) return reject(err);
      return resolve(data);
    });
  });
}

async function writeCheckSumToSafeFile(data) {
  return await new Promise((resolve, reject) => {
    fs.writeFile(file, data, err => {
      if (err) reject(err);
      resolve();
    });
  });
}

readSafeFile().then(data => {
  const sum = calculateSum(data);
  const bufferSum = littleToBigEndian(sum);
  const hex = ashex(bufferSum);
  const newData = data;
  for (let i = 0; i < 4; i++) {
    newData[12 + i] = bufferSum.getInt8(i);
  }
  writeCheckSumToSafeFile(newData).then(() => console.log(hex));
});
code in golang

source: https://github.com/gucio321/d2d2s/blob/66f91e2af7b3949ca7f279aae397bd8904519e2d/pkg/d2s/d2s.go#L397

// CalculateChecksum calculates a checksum and saves in a byte slice 
func CalculateChecksum(data *[]byte) {
        var sum uint32
        for i := range *data {
                sum = ((sum << 1) % math.MaxUint32) | (sum >> (int32Size*byteLen - 1))

                sum += uint32((*data)[i])
        }

        sumBytes := make([]byte, int32Size)
        binary.LittleEndian.PutUint32(sumBytes, sum)

        const (
                int32Size = 4
                checksumPosition = 12
        )
        for i := 0; i < int32Size; i++ {
                (*data)[checksumPosition+i] = sumBytes[i]
        }
}

If the checksum is invalid, Diablo II will not open the save file.

Active Weapon

TODO

Character Name

Character names are store as an array of 16 characters which contain a null terminated string padded with 0x00 for the remaining bytes. Characters are stored as 8-bit ASCII, but remember that valid must follow these rules:

  • Must be 2-15 in length
  • Must begin with a letter
  • May contain up to one hyphen (-) or underscore (_)
  • May contain letters

Character Status

This is a 8-bit field:

Bit Desc
0 ?
1 ?
2 Hardcore
3 Died
4 ?
5 Expansion
6 ?
7 ?

Character Progression

TODO

Character Class

ID Class
0 Amazon
1 Sorceress
2 Necromancer
3 Paladin
4 Barbarian
5 Druid
6 Assassin

Level

This level value is visible only in character select screen and must be the same as this in Stats section.

Hotkeys

TODO

Character Menu Appearance

32 byte structure which defines how the character looks in the menu Does not change in-game look

Difficulty

3 bytes of data that indicates which of the three difficulties the character has unlocked. Each byte is representitive of one of the difficulties. In this order: Normal, Nightmare, and Hell. Each byte is a bitfield structured like this:

7 6 5 4 3 2, 1, 0
Active? Unknown Unknown Unknown Unknown Which act (0-4)?

Map

TODO

Quest

TODO

Waypoint

Waypoint data starts with 2 chars "WS" and 6 unknown bytes, always = {0x01, 0x00, 0x00, 0x00, 0x50, 0x00}

Three structures are in place for each difficulty, at offsets 641, 665 and 689.

The contents of this structure are as follows

byte bytesize contents
0 2 bytes {0x02, 0x01} Unknown purpose
2 5 bytes Waypoint bitfield in order of least significant bit
7 17 bytes unknown

In the waypoint bitfield, a bit value of 1 means that the waypoint is enabled It is in an order from lowest to highest, so 0 is Rogue encampment (ACT I) etc. The first waypoint in each difficulty is always activated.

NPC

TODO

Stats

TODO (9-bit encoding)

Items

Items are stored in lists described by this header:

Byte Size Desc
0 2 "JM"
2 2 Item Count

After this come N items. Each item starts with a basic 14-byte structure. Many fields in this structure are not "byte-aligned" and are described by their bit position and sizes.

Bit Size Desc
0 16 "JM" (separate from the list header)
16 4 ?
20 1 Identified
21 6 ?
27 1 Socketed
28 1 ?
29 1 Picked up since last save
30 2 ?
32 1 Ear
33 1 Starter Gear
34 3 ?
37 1 Compact
38 1 Ethereal
39 1 ?
40 1 Personalized
41 1 ?
42 1 Runeword
43 15 ?
58 3 Parent
61 4 Equipped
65 4 Column
69 3 Row
72 1 ?
73 3 Stash
76 4 ?
80 24 Type code (3 letters)
108 Extended Item Data

Extended Item Data

If the item is marked as Compact (bit 37 is set) no extended item information will exist and the item is finished.

Items with extended information store bits based on information in the item header. For example, an item marked as Socketed will store an extra 3-bit integer encoding how many sockets the item has.

Bit Size Desc
108 Sockets
Custom Graphics
Class Specific
Quality
Mods

Custom Graphics

Custom graphics are denoted by a single bit, which if set means a 3-bit number for the graphic index follows. If the bit is not set the 3-bits are not present.

Bit Size Desc
0 1 Item has custom graphics
1 3 Alternate graphic index

Class Specific

Class items like Barbarian helms or Amazon bows have special properties specific to those kinds of items. If the first bit is empty the remaining 11 bits will not be present.

Bit Size Desc
0 1 Item has class specific data
1 11 Class specific bits

Quality

Item quality is encoded as a 4-bit integer.

Low Quality

Mods

After each item is a list of mods. The list is a series of key value pairs where the key is a 9-bit number and the value depends on the key. The list ends when key 511 (0x1ff) is found which is all 9-bits being set.

Using the file ItemStatCost.txt as a tab-delimited CSV file you can extract the ID column which maps to the 9-bit keys. The columns Save Bits and Param Bits describe how large the mod is.

The only exception is min-max style modifiers which use the next row in the CSV to store the "max" portion of the mod. The bit sizes of these two can be different and you should sum them to get the total size.

Runeword

TODO

Parent

All items are located somewhere and have a "parent" which can be another item, such as when inserting a jewel.

Value Desc
0 Stored
1 Equipped
2 Belt
4 Cursor
6 Item

For items that are "stored" a 3-bit integer encoded starting at bit 73 describes where to store the item:

Value Desc
1 Inventory
4 Horadric Cube
5 Stash

Equipped

Items that are equipped describe their slot:

Value Slot
1 Helmet
2 Amulet
3 Armor
4 Weapon (Right)
5 Weapon (Left)
6 Ring (Right)
7 Ring (Left)
8 Belt
9 Boots
10 Gloves
11 Alternate Weapon (Right)
12 Alternate Weapon (Left)

Sockets