• Stars
    star
    121
  • Rank 293,924 (Top 6 %)
  • Language
  • Created over 8 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Diablo II Save File Format (.d2s format)

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.

Byte Length Desc
0 4 Signature (0xaa55aa55)
4 4 Version ID
8 4 File size
12 4 Checksum
16 4 Active Weapon
20 16 Character Name
36 1 Character Status
37 1 Character Progression
38 2 ?
40 1 Character Class
41 2 ?
43 1 Level
44 4 ?
48 4 Time
52 4 ?
56 64 Hotkeys
120 4 Left Mouse
124 4 Right Mouse
128 4 Left Mouse (weapon switch)
132 4 Right Mouse (weapon switch)
136 32 Character Menu Appearance
168 3 Difficulty
171 4 Map
175 2 ?
177 2 Merc dead?
179 4 Merc seed?
183 2 Merc Name ID
185 2 Merc Type
187 4 Merc Experience
191 144 ?
335 298 Quest
633 81 Waypoint
714 51 NPC
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