Code Monkey home page Code Monkey logo

d2s-format's Introduction

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

d2s-format's People

Contributors

grandpagamehacker avatar gucio321 avatar krisives avatar satandidnowrong avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

d2s-format's Issues

[Suggestion]: My C++ version of the checksum algorithm should it help anyone

This is ready to be built in VS2022.

#define _CRT_SECURE_NO_DEPRECATE

#include <iostream>
#include <stdio.h>
#include<string>

using namespace std;

char** argv = NULL;

void main(int argc, char** argv)
{
	if (argv[1] == NULL) {
		cout << "\n\n\tNo .d2s file specified. \n\n\tUsage: d2scs.exe char.d2s\n\n" << endl;
		return;
	}

	FILE* charFile;
	charFile = fopen(argv[1], "r+b");

	// zero out the previous checksum
	char zero[4] = { 0 };
	fseek(charFile, 12, SEEK_SET);
	fwrite(zero, sizeof(char),sizeof(zero), charFile);
	fflush(charFile);
	fseek(charFile, 0, SEEK_SET);

	unsigned char saveFileData[16];
	unsigned long counter = 0;
	unsigned int uSum = 0;

	int n;
	int uVar = 0;
	int byte;

	while ((n = fread(saveFileData, sizeof(char), 16, charFile)) > 0) {
		int i;
		for (i = 0; i < n; i++) {
			// Current byte in charFile		
			byte = (unsigned)saveFileData[i];

			//SAME as uSum  line, just to double check if both produce the same checksum
			if (uVar < 0)
				byte++;
			uVar = byte + uVar * 2;

			//SAME as above block, just to double check if both produce the same checksum
			uSum = ((uSum << 1) | (uSum >> 31)) + (unsigned)saveFileData[i];
		}
		counter += 16;
	}
	
	unsigned long r_uVar = _byteswap_ulong(uVar);
	unsigned long r_uSum = _byteswap_ulong(uSum);

	// write new checksum
	fseek(charFile, 12, SEEK_SET);
	fwrite(&uVar, sizeof(uVar), 1, charFile);
	fflush(charFile);
	fseek(charFile, 0, SEEK_SET);

	printf("\n\n\t%08x checksum written to %s\n\n", r_uVar, argv[1]);

	fclose(charFile);
}

XP offset for 1.12 d2s

Xp is stored at offset 802 as 4 byte little endian unsigned int multiplied by 2.

Eg, if your in-game xp is 79 689 129, then at offset 802 you'll see:
79 689 129 * 2 = 159 378 258 => hex => 52 EB 7F 09

So if you set xp bytes to zeros 00 00 00 00, you have to update char level (offset:43, 1 byte) with 01 and so on.

Can show nodejs code how to reset char level to 1.

Implementation of checksum calculation in javascript

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));
});

[Suggestion]: My PHP version of the checksum algorithm should it help anyone

Hi. Thanks for the great resource. Please add my version of the checksum algorithm in PHP should this help anyone.

<?php
/*
  Diablo 2 D2S Save File Checksum Calculator 
  By Hash Borgir (https://www.psychedelicsdaily.com)
  @date 6/4/2022

 Checksum field is at byte 12. 
 Bytes 12/13/14/15 as a uint32. 
 Set this to 0. 
 After clearing the checksum field add up the values of all the bytes in the file and rotate the running total one bit to the left before adding the next byte.
*/

/**
 * @param string $hex
 * @return string
 */
function swapEndianness(string $hex) {
    return implode('', array_reverse(str_split($hex, 2)));
}

/**
 * Calculate D2S Checksum
 * @param $data
 * @return string
 */
function checksum($fileData) {
    $nSignature = 0;
    foreach ($fileData as $k => $byte) {
        if ($k == 12 || $k == 13 || $k == 14 || $k == 15)  { // skip bytes 12,13,14,15
            $byte = 0;
        }
        $nSignature = ((($nSignature << 1) | ($nSignature >> 31)) + $byte & 0xFFFFFFFF);
    }
    return swapEndianness(str_pad(dechex($nSignature), 8, 0, STR_PAD_LEFT));
}

$filename = "D:\Diablo II\MODS\ironman-dev\save\Sorc.d2s";
$fp = fopen($filename, "rb+");

fseek($fp, 12); // go to byte 12
fwrite($fp, pack('I', 0)); // clear the checksum field uInt32
$fileData = unpack('C*', file_get_contents($filename)); // open file and unpack

fseek($fp, 12); // go to byte 12
fwrite($fp, pack('H8', checksum($fileData))); // write new checksum
fclose($fp);

Fix formatting

Something changed in Markdown now it doesnt render the tables

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.