Skip to content

A C# library for reading and writing Diablo 2 save files (.d2s character saves and .d2i shared stash files)

Notifications You must be signed in to change notification settings

ResurrectedTrader/D2SSharp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

D2SSharp

Build NuGet

A C# library for reading and writing Diablo 2 save files (.d2s character saves and .d2i shared stash files). Supports both the original Diablo 2: Lord of Destruction format (1.10+) and Diablo 2 Resurrected.

Features

  • Full read/write support for character save files (.d2s) and shared stash (.d2i)
  • Supports D2 LOD (version 96) and D2R (version 97+) formats
  • Version conversion between D2 LOD and D2R formats
  • Complete item parsing including stats, sockets, runewords, and set bonuses
  • Full round-tripping support - produces identical outputs, as verified by tests
  • Separate zero-copy overlay API for modifying header fields (name, level, flags, waypoints) without parsing the full save
  • External .txt file support for modded game data
  • Zero external dependencies beyond .NET

Table of Contents

Installation

dotnet add package D2SSharp

Or clone and build from source:

git clone https://github.com/ResurrectedTrader/D2SSharp.git
cd D2SSharp
dotnet build

Usage

Reading a Character Save

using D2SSharp.Model;

// Read save file (uses built-in game data)
byte[] saveBytes = File.ReadAllBytes("MyCharacter.d2s");
D2Save save = D2Save.Read(saveBytes);

// Access character info
Console.WriteLine($"Character: {save.Character.Preview.Name}");
Console.WriteLine($"Level: {save.Stats.Level}");
Console.WriteLine($"Class: {save.Character.Class}");

// Iterate items
foreach (var item in save.Items)
{
    Console.WriteLine($"Item: {item.ItemCodeString} (Quality: {item.Quality})");
}

Modifying and Saving

// Modify stats
save.Stats.Strength = 200;
save.Stats.GoldInStash = 2500000;

// Write back to file
byte[] buffer = new byte[save.EstimateSize()];
int bytesWritten = save.Write(buffer);
File.WriteAllBytes("MyCharacter.d2s", buffer.AsSpan(0, bytesWritten).ToArray());

Reading Shared Stash

byte[] stashBytes = File.ReadAllBytes("SharedStashSoftCoreV2.d2i");
D2StashSave stash = D2StashSave.Read(stashBytes);

foreach (var tab in stash)
{
    Console.WriteLine($"Tab: {tab.Name}, Items: {tab.Items.Count}");
}

Overlay API (Zero-Copy Access)

For simple modifications to the fixed-size header sections, the overlay API provides direct memory access without parsing the entire save file.

using D2SSharp.Model;

byte[] data = File.ReadAllBytes("MyCharacter.d2s");

// Get a reference directly into the byte array
ref var overlay = ref D2SaveLayout.From(data);

// Read fields directly (Name is on D2SaveLayout, handles version differences)
Console.WriteLine($"Name: {overlay.Name}");
Console.WriteLine($"Level: {overlay.Character.Level}");
Console.WriteLine($"Class: {overlay.Character.Class}");

// Modify character
overlay.Name = "NewName";
overlay.Character.MercData.Experience = 1000000;

// Unlock all waypoints
overlay.Waypoints.UnlockAllWaypoints();

// Update checksum and save
D2SaveLayout.UpdateChecksum(data);
File.WriteAllBytes("MyCharacter.d2s", data);

Performance

Benchmarks comparing full parsing vs overlay access (tested on a level 99 character with full inventory):

Operation Time Allocated
Full Deserialize 56.1 μs 225.8 KB
Full Serialize 28.4 μs 122.5 KB
Full Round-Trip 86.5 μs 348.3 KB
Overlay: Read Name 9.8 ns 32 B
Overlay: Modify + Checksum 991 ns 2.3 KB

The overlay API is ~5700x faster for reading character name and ~87x faster for modifying fields and updating the checksum compared to a full round-trip.

Overlay Limitations

The overlay API only covers the fixed-size header sections (first 765 bytes):

Section Supported Fields
Header Version, FileSize, Checksum
Character Name, Level, Class, Flags, MercData, Hotkeys, Appearance
Quests All quest flags for all difficulties
Waypoints All waypoint flags for all difficulties
PlayerIntro NPC/Quest intro flags

Not accessible via overlay: Player stats (strength, vitality, gold, etc.), skills, items, corpses, mercenary items, iron golem. These require full parsing with D2Save.Read().

Save Format Versions

Version Game Notes
96 D2 LOD 1.10+ 32-bit item codes, 7-bit strings
97 D2 Resurrected Huffman-encoded item codes, 7-bit strings
98+ D2 Resurrected Huffman-encoded item codes, 8-bit strings

Version Conversion

The library supports converting saves between D2 LOD (1.14) and D2R formats by specifying a target version when writing:

// Read a 1.14 save (version 96)
var save = D2Save.Read(File.ReadAllBytes("old_character.d2s"));

// Write as D2R format (version 99)
byte[] buffer = new byte[save.EstimateSize()];
int written = save.Write(buffer, targetVersion: 99);
File.WriteAllBytes("new_character.d2s", buffer.AsSpan(0, written).ToArray());

Conversion Details

The library handles the following format differences automatically:

1.14 → D2R

Field Conversion
Character.Name Moved to Character.Preview.Name (D2R uses UTF-8 preview name)
Character.Preview.* Populated from equipped items (Head, Torso, LeftHand, RightHand)
Character.Preview.Transform Looked up from Character.AppearanceTints
Character.Preview.FileIndex Extracted from item quality data
Character.Preview.Flags Set to Targeting for primary weapon, 0 for others
Item.Position.BodyLocation Zeroed for non-equipped items (D2R requires this)

D2R → 1.14

Field Conversion
Character.Preview.Name Moved to Character.Name
Character.Preview.* Zeroed (1.14 doesn't use preview items)
Item.Position.BodyLocation Kept as None for stored items (D2R doesn't preserve original equip slot)

Binary Differences

When comparing converted saves to saves created by the game:

Section Reason
Items Item ordering may differ between saves of the same character
Header (Checksum) Differs due to item ordering differences

These differences do not affect gameplay - the converted saves are fully functional.

External Data

The library includes embedded game data tables for versions 96, 97, and 99, which are used automatically. For modded games with custom items/stats, you can provide your own txt files:

using D2SSharp.Data;

// Load from a directory containing version subdirectories
// Directory structure:
//   MyTxtFiles/
//     96/
//       armor.txt, itemstatcost.txt, itemtypes.txt, misc.txt, weapons.txt
//     99/
//       armor.txt, itemstatcost.txt, itemtypes.txt, misc.txt, weapons.txt
var modData = new TxtFileExternalData(@"C:\path\to\MyTxtFiles");

// Or load from a single directory for a specific version
var modData = new TxtFileExternalData(@"C:\path\to\MyTxtFiles\99", version: 99);

// Then use it for reading/writing
var save = D2Save.Read(File.ReadAllBytes("modded.d2s"), modData);

The library selects version data based on exact match with the save file's version. If the required version is not available, an exception is thrown listing the available versions.

Mod Compatibility

The library provides limited support for modded save files:

Supported

  • Custom item data: See External Data section for loading mod-specific .txt files
  • Trailing data: Any bytes after the standard sections are preserved in D2Save.TrailingData and written back during round-trip

Not Supported

  • Missing sections: Save files must contain all required sections. Files with missing or malformed sections (e.g., Expansion flag set but no MercItems/IronGolem sections) will fail to parse
  • Modified section formats: The library expects standard D2/D2R section layouts

Building

dotnet build D2SSharp.sln
dotnet test

Acknowledgments

This project was vibe coded with Claude.

Special thanks to:

License

MIT

About

A C# library for reading and writing Diablo 2 save files (.d2s character saves and .d2i shared stash files)

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages