You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add support for D2R save format versions 100-105 with restructured
header, new sections, and updated item serialization.
Header changes (v104+):
- PreviewData expanded to 228 bytes with SaveTimes[6], Experiences[6],
GameMode field, and 96-byte UTF-8 Name
- Character Name field removed (now only in PreviewData)
New sections:
- DemonSection (v103+, "lf" magic) for summoned creature persistence
- ChronicleSection (0xC0EAEDC0) tracking found set/unique/runeword items
- StashTabType enum (Normal, AdvancedStash, Chronicle) for StashFormat 2
Item serialization (v105+):
- Quantity uses 1-bit presence flag for ALL items, not just stackable
- Chronicle recipient data (v100+) with CharacterIdentity pairs
- AdvancedStashStackSize for advanced stash tab items (v100+)
New enums: GameMode, DemonType, StashTabType, CharacterClass.Warlock,
and stat IDs 207/361-367.
Version conversion extended across v96↔v97+, v<=103↔v104+, and
v<=104↔v105+ boundaries.
Copy file name to clipboardExpand all lines: CLAUDE.md
+58-8Lines changed: 58 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
5
5
## Project Overview
6
6
7
-
D2SSharp is a C# library for reading and writing Diablo 2 save files (.d2s character saves and .d2i shared stash files). It supports both the original D2 LOD format (version 96) and the Diablo 2 Resurrected format (version 97+).
7
+
D2SSharp is a C# library for reading and writing Diablo 2 save files (.d2s character saves and .d2i shared stash files). It supports the original D2 LOD format (version 96), Diablo 2 Resurrected format (version 97+), and the latest D2R format (version 105) with its restructured header and new item serialization.
8
8
9
9
## Build Commands
10
10
@@ -26,9 +26,9 @@ dotnet test --logger "console;verbosity=detailed"
26
26
27
27
### Core Components
28
28
29
-
-**D2Save** (`Model/D2Save.cs`): Root model for character save files. Contains all sections: Character, Quests, Waypoints, Skills, Items, Corpses, MercItems, IronGolem.
29
+
-**D2Save** (`Model/D2Save.cs`): Root model for character save files. Contains all sections: Character, Quests, Waypoints, Skills, Items, Corpses, MercItems, IronGolem, Demon (v103+).
30
30
31
-
-**D2StashSave** (`Model/D2StashSave.cs`): Model for shared stash files. Contains a list of D2StashTab entries.
31
+
-**D2StashSave** (`Model/D2StashSave.cs`): Model for shared stash files. Contains a list of D2StashTab entries. Each tab has a `StashTabType` (Normal, AdvancedStash, Chronicle) that determines its body content. Chronicle tabs track found set/unique/runeword items.
32
32
33
33
-**Item** (`Model/Item.cs`): Complete item model with all properties. Handles both compact save format (quest items, gold, gems, runes) and complete format (equipment with stats).
34
34
@@ -39,7 +39,7 @@ dotnet test --logger "console;verbosity=detailed"
39
39
The library uses embedded game data tables to parse items. For standard saves, no explicit external data is needed:
40
40
41
41
```csharp
42
-
// Read/write using built-in embedded data (versions 96, 97, 99)
42
+
// Read/write using built-in embedded data (versions 96, 97, 99, 105)
43
43
varsave=D2Save.Read(bytes);
44
44
intwritten=save.Write(buffer);
45
45
```
@@ -58,7 +58,7 @@ var save = D2Save.Read(bytes, modData);
58
58
intwritten=save.Write(buffer, modData);
59
59
```
60
60
61
-
-**TxtFileExternalData.Default**: Shared default instance with embedded data for versions 96, 97, 99
61
+
-**TxtFileExternalData.Default**: Shared default instance with embedded data for versions 96, 97, 99, 105
62
62
-**IExternalData** (`Data/IExternalData.cs`): Interface for providing stat and item type information
63
63
64
64
The external data provides:
@@ -71,6 +71,22 @@ The external data provides:
71
71
The library uses `saveVersion` to distinguish formats:
72
72
-**Version 96**: Original D2 LOD format (32-bit item codes, 7-bit strings, 10-bit item format)
73
73
-**Version 97+**: D2R format (Huffman-encoded item codes, 8-bit strings, compact 3-bit item format, 4-field realm data)
-**Version 103+**: DemonSection ("lf" magic) after IronGolem
76
+
-**Version 104+**: New header format (403 bytes), Name moved from Character to PreviewData, expanded SaveTimes/Experiences arrays, GameMode field
77
+
-**Version 105+**: Item quantity uses 1-bit presence flag for ALL items (not just stackable)
78
+
79
+
### Shared Stash Tab Format
80
+
81
+
Each stash tab has a 64-byte header: Magic(4) + StashFormat(4) + ItemFormat(4) + Gold(4) + Size(2) + Season(2) + TabType(1) + Reserved(43).
82
+
83
+
-**StashFormat < 2**: TabType is forced to Normal on read (game ignores the byte). Chronicle tabs are skipped entirely on write.
84
+
-**StashFormat >= 2**: TabType determines the tab body content:
85
+
-`Normal` (0): Items (JM section)
86
+
-`AdvancedStash` (1): Items with stackable support (JM section)
87
+
-`Chronicle` (2): Chronicle section (magic 0xC0EAEDC0) tracking found set/unique/runeword items
88
+
89
+
The chronicle tab writer in the game has a size calculation bug (`add ax, 40h` at 0x140311e98) that adds 64 bytes of stale buffer data to the tab size. The reader ignores these bytes. The library preserves them in `ChronicleSection.TrailingData` for byte-exact round-trip.
The library supports converting between 1.14 (version 96) and D2R (version 97+) formats via `D2Save.Write(buffer, targetVersion)`.
109
+
The library supports converting between formats via `D2Save.Write(buffer, targetVersion)`. Handles three boundaries: v96↔v97+ (1.14↔D2R), v<=103↔v104+ (old↔new header), and v<=104↔v105+ (item quantity format).
94
110
95
111
Key conversion logic in `D2Save.PrepareForVersion()`:
96
112
@@ -111,7 +127,7 @@ Key conversion logic in `D2Save.PrepareForVersion()`:
111
127
112
128
## Mod Compatibility
113
129
114
-
-**TrailingData**: `D2Save.TrailingData` captures any bytes after the IronGolem section. These are preserved during round-trip for mod compatibility.
130
+
-**TrailingData**: `D2Save.TrailingData` captures any bytes after the last known section (IronGolem or Demon). These are preserved during round-trip for mod compatibility.
115
131
-**TxtFileExternalData**: Load mod-specific .txt files for custom items/stats by providing a directory with version subdirectories.
116
132
- Files with missing sections (e.g., Expansion flag set but no MercItems/IronGolem) will fail to parse.
117
133
@@ -132,10 +148,43 @@ Each section defines a `Magic` constant used for validation. Useful for debuggin
132
148
| MercItemsSection | "jf" (0x666A) |`6A 66`|
133
149
| IronGolemSection | "kf" (0x666B) |`6B 66`|
134
150
| Item (v96 only) | "JM" (0x4D4A) |`4A 4D`|
151
+
| DemonSection (v103+) | "lf" (0x666C) |`6C 66`|
152
+
| D2StashTab header | 0xAA55AA55 |`55 AA 55 AA`|
153
+
| ChronicleSection | 0xC0EAEDC0 |`C0 ED EA C0`|
154
+
155
+
## Overlay API
156
+
157
+
The overlay API (`D2SaveOverlay.cs`) provides zero-copy, blittable struct access to the fixed-size header sections of save files via `MemoryMarshal.AsRef`. This allows direct read/write of header fields without parsing the full save.
158
+
159
+
Two layout structs exist for different save versions:
160
+
161
+
-**`D2SaveLayout`** (765 bytes): For v<=103 saves. Character section includes 16-byte Name field and 144-byte PreviewData.
162
+
-**`D2SaveLayoutV104`** (833 bytes): For v>=104 saves. Character section removes Name (now in PreviewData) and expands PreviewData to 228 bytes (+68 bytes total).
163
+
164
+
```csharp
165
+
vardata=File.ReadAllBytes("save.d2s");
166
+
167
+
// Check version first, then use the appropriate layout
168
+
uintversion=BitConverter.ToUInt32(data, 4);
169
+
if (version>=104)
170
+
{
171
+
refvaroverlay=refD2SaveLayoutV104.From(data);
172
+
overlay.Character.Level=99;
173
+
D2SaveLayoutV104.UpdateChecksum(data);
174
+
}
175
+
else
176
+
{
177
+
refvaroverlay=refD2SaveLayout.From(data);
178
+
overlay.Character.Level=99;
179
+
D2SaveLayout.UpdateChecksum(data);
180
+
}
181
+
```
182
+
183
+
Both layouts provide a `Name` property that handles version-aware name access, `From()` for validation, and `UpdateChecksum()` for recalculating the checksum after modifications. Using the wrong layout for a version throws `InvalidDataException`.
135
184
136
185
## Testing
137
186
138
-
All tests use `TxtFileExternalData.Default` which provides embedded stat/item info for versions 96, 97, and 99. For modded saves, use a custom `TxtFileExternalData` instance loaded from mod-specific .txt files.
187
+
All tests use `TxtFileExternalData.Default` which provides embedded stat/item info for versions 96, 97, 99, and 105. For modded saves, use a custom `TxtFileExternalData` instance loaded from mod-specific .txt files.
139
188
140
189
Round-trip tests verify that read -> write produces identical bytes.
141
190
@@ -145,6 +194,7 @@ Resources are organized by save version:
145
194
-`Resources/96/` - D2 1.14 format saves (version 96)
146
195
-`Resources/97/` - D2R format saves (version 97)
147
196
-`Resources/99/` - D2R 1.5+ format saves (version 99)
197
+
-`Resources/105/` - D2R latest format saves (version 105) with new header and item format
148
198
-`Resources/Modded/` - Modded saves with custom .txt files in `Txt/99/`
- DemonSection support (v103+) for summoned creature persistence
14
16
- Full round-tripping support - produces identical outputs, as verified by tests
15
-
- Separate [zero-copy overlay API](#overlay-api-zero-copy-access) for modifying header fields (name, level, flags, waypoints) without parsing the full save
17
+
- Separate [zero-copy overlay API](#overlay-api-zero-copy-access) for modifying header fields (name, level, flags, waypoints) without parsing the full save
For simple modifications to the fixed-size header sections, the overlay API provides direct memory access without parsing the entire save file.
113
+
For simple modifications to the fixed-size header sections, the overlay API provides direct memory access without parsing the entire save file. Two layout structs exist because v104+ saves have a different header layout:
114
+
115
+
-**`D2SaveLayout`** (765 bytes) — for saves with version <= 103
116
+
-**`D2SaveLayoutV104`** (833 bytes) — for saves with version >= 104
117
+
118
+
Using the wrong layout for a version throws `InvalidDataException`.
| 104 | D2R 2.x | New header layout (833 bytes), Name moved to PreviewData, GameMode field |
199
+
| 105 | D2R 2.x | Item quantity uses 1-bit presence flag for all items |
162
200
163
201
## Version Conversion
164
202
165
-
The library supports converting saves between D2 LOD (1.14) and D2R formats by specifying a target version when writing:
203
+
The library supports converting saves between formats by specifying a target version when writing. Conversion is handled across three boundaries: v96↔v97+ (1.14↔D2R), v<=103↔v104+ (old↔new header), and v<=104↔v105+ (item quantity format).
@@ -197,6 +235,20 @@ The library handles the following format differences automatically:
197
235
|`Character.Preview.*`| Zeroed (1.14 doesn't use preview items) |
198
236
|`Item.Position.BodyLocation`| Kept as `None` for stored items (D2R doesn't preserve original equip slot) |
199
237
238
+
#### v<=103 ↔ v104+ (Header Layout)
239
+
240
+
| Field | Conversion |
241
+
|-------|------------|
242
+
|`Character.Name`| Removed in v104+; name is only in `Character.Preview.Name`|
243
+
|`PreviewData`| Expands from 144 to 228 bytes: `SaveTimes[6]`, `Experiences[6]`, `GameMode`|
244
+
|`DemonSection`| Added in v103+; initialized empty when upgrading from earlier versions |
245
+
246
+
#### v<=104 ↔ v105+ (Item Format)
247
+
248
+
| Field | Conversion |
249
+
|-------|------------|
250
+
|`Item.Quantity`| v105+ uses a 1-bit presence flag for all items, not just stackable |
251
+
200
252
#### Binary Differences
201
253
202
254
When comparing converted saves to saves created by the game:
@@ -210,7 +262,7 @@ These differences do not affect gameplay - the converted saves are fully functio
210
262
211
263
## External Data
212
264
213
-
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:
265
+
The library includes embedded game data tables for versions 96, 97, 99, and 105, which are used automatically. For modded games with custom items/stats, you can provide your own txt files:
0 commit comments