Skip to content

Commit 010ab7c

Browse files
Parse stash versions correctly
1 parent b10e62f commit 010ab7c

3 files changed

Lines changed: 59 additions & 24 deletions

File tree

src/D2SSharp.Tests/IntegrationTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,13 @@ public void RoundTrip_Stash_Saves(IExternalData externalData)
263263
var data = File.ReadAllBytes(Path.Combine("Resources", "99", "SharedStashSoftCoreV2.d2i"));
264264

265265
// Read stash
266-
var stash = D2StashSave.Read(data, externalData, saveVersion: 99);
266+
var stash = D2StashSave.Read(data, externalData);
267267
Assert.NotEmpty(stash);
268268

269269
// Write stash back
270270
var buffer = new byte[data.Length * 2];
271271
var writer = new BitWriter(buffer);
272-
stash.Write(ref writer, externalData, saveVersion: 99);
272+
stash.Write(ref writer, externalData);
273273
int written = writer.BytesWritten;
274274

275275
// Compare

src/D2SSharp/Model/D2StashSave.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ public partial class D2StashSave : List<D2StashTab>
1313
/// </summary>
1414
/// <param name="reader">The bit reader.</param>
1515
/// <param name="externalData">External game data for parsing items.</param>
16-
/// <param name="saveVersion">Save file version (97+ for D2R format).</param>
1716
/// <returns>The parsed stash save containing all tabs.</returns>
18-
public static D2StashSave Read(ref BitReader reader, IExternalData externalData, uint saveVersion)
17+
public static D2StashSave Read(ref BitReader reader, IExternalData externalData)
1918
{
2019
var save = new D2StashSave();
2120

2221
while (reader.BitsRemaining > D2StashTab.HeaderSize * 8)
2322
{
24-
save.Add(D2StashTab.Read(ref reader, externalData, saveVersion));
23+
save.Add(D2StashTab.Read(ref reader, externalData));
2524
}
2625

2726
return save;
@@ -32,25 +31,24 @@ public static D2StashSave Read(ref BitReader reader, IExternalData externalData,
3231
/// </summary>
3332
/// <param name="data">The stash file data.</param>
3433
/// <param name="externalData">External game data for parsing items.</param>
35-
/// <param name="saveVersion">Save file version (97+ for D2R format).</param>
3634
/// <returns>The parsed stash save containing all tabs.</returns>
37-
public static D2StashSave Read(ReadOnlySpan<byte> data, IExternalData externalData, uint saveVersion)
35+
public static D2StashSave Read(ReadOnlySpan<byte> data, IExternalData externalData)
3836
{
3937
var reader = new BitReader(data);
40-
return Read(ref reader, externalData, saveVersion);
38+
return Read(ref reader, externalData);
4139
}
4240

4341
/// <summary>
4442
/// Writes all stash tabs to a BitWriter.
4543
/// </summary>
4644
/// <param name="writer">The bit writer.</param>
4745
/// <param name="externalData">External game data for writing items.</param>
48-
/// <param name="saveVersion">Save file version (97+ for D2R format).</param>
49-
public void Write(ref BitWriter writer, IExternalData externalData, uint saveVersion)
46+
/// <param name="targetVersion">Optional target version for format conversion. If null, uses current Version.</param>
47+
public void Write(ref BitWriter writer, IExternalData externalData, uint? targetVersion = null)
5048
{
5149
foreach (var tab in this)
5250
{
53-
tab.Write(ref writer, externalData, saveVersion);
51+
tab.Write(ref writer, externalData, targetVersion);
5452
}
5553
}
5654
}

src/D2SSharp/Model/D2StashTab.cs

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using D2SSharp.Data;
2+
using D2SSharp.Enums;
23
using D2SSharp.IO;
34

45
namespace D2SSharp.Model;
@@ -14,8 +15,11 @@ public partial class D2StashTab
1415
/// <summary>Tab magic value: 0xAA55AA55</summary>
1516
public const uint Magic = 0xAA55AA55;
1617

17-
/// <summary>Expected header value for D2R stash tabs.</summary>
18-
public const ulong Header = 0x6300000001L;
18+
/// <summary>Stash format.</summary>
19+
public uint StashFormat { get; set; }
20+
21+
/// <summary>Item format in the stash.</summary>
22+
public uint ItemFormat { get; set; }
1923

2024
/// <summary>Gold amount stored in the stash tab.</summary>
2125
public uint Gold { get; set; }
@@ -34,9 +38,8 @@ public partial class D2StashTab
3438
/// </summary>
3539
/// <param name="reader">The bit reader.</param>
3640
/// <param name="externalData">External game data for parsing items.</param>
37-
/// <param name="saveVersion">Save file version (97+ for D2R format).</param>
3841
/// <returns>The parsed stash tab.</returns>
39-
public static D2StashTab Read(ref BitReader reader, IExternalData externalData, uint saveVersion)
42+
public static D2StashTab Read(ref BitReader reader, IExternalData externalData)
4043
{
4144
var tab = new D2StashTab();
4245
int startPosition = reader.BytePosition; // Record start
@@ -45,15 +48,13 @@ public static D2StashTab Read(ref BitReader reader, IExternalData externalData,
4548
if (magic != Magic)
4649
throw new InvalidDataException($"Invalid stash tab magic: 0x{magic:X8}, expected 0x{Magic:X8}");
4750

48-
ulong header = reader.ReadUInt64();
49-
if (header != Header)
50-
throw new InvalidDataException($"Invalid stash tab header: 0x{header:X16}, expected 0x{Header:X16}");
51-
51+
tab.StashFormat = reader.ReadUInt32();
52+
tab.ItemFormat = reader.ReadUInt32();
5253
tab.Gold = reader.ReadUInt32();
5354
ushort size = reader.ReadUInt16(); // Size field (not stored, used for validation)
5455
tab.Season = reader.ReadUInt16();
5556
reader.ReadBytes(tab.Reserved);
56-
tab.Items = ItemsSection.Read(ref reader, externalData, saveVersion);
57+
tab.Items = ItemsSection.Read(ref reader, externalData, tab.ItemFormat);
5758

5859
// Seek to the correct position for the next tab
5960
reader.SetBytePosition(startPosition + size);
@@ -66,13 +67,22 @@ public static D2StashTab Read(ref BitReader reader, IExternalData externalData,
6667
/// </summary>
6768
/// <param name="writer">The bit writer.</param>
6869
/// <param name="externalData">External game data for writing items.</param>
69-
/// <param name="saveVersion">Save file version (97+ for D2R format).</param>
70-
public void Write(ref BitWriter writer, IExternalData externalData, uint saveVersion)
70+
/// <param name="targetVersion">Optional target version for format conversion. If null, uses current Version.</param>
71+
public void Write(ref BitWriter writer, IExternalData externalData, uint? targetVersion = null)
7172
{
73+
uint writeVersion = targetVersion ?? ItemFormat;
74+
75+
// Prepare for version conversion if needed (mutates data for new format)
76+
PrepareForVersion(ItemFormat, writeVersion);
77+
78+
// Update Version permanently (data is now in the new format)
79+
ItemFormat = writeVersion;
80+
7281
int startPosition = writer.BytesWritten;
7382

7483
writer.WriteUInt32(Magic);
75-
writer.WriteUInt64(Header);
84+
writer.WriteUInt32(StashFormat);
85+
writer.WriteUInt32(ItemFormat);
7686
writer.WriteUInt32(Gold);
7787

7888
// Write placeholder for size, will update at end
@@ -81,7 +91,7 @@ public void Write(ref BitWriter writer, IExternalData externalData, uint saveVer
8191
writer.WriteUInt16(Season);
8292
writer.WriteBytes(Reserved);
8393

84-
Items.Write(ref writer, externalData, saveVersion);
94+
Items.Write(ref writer, externalData, writeVersion);
8595

8696
// Calculate this tab's size and write it back
8797
int tabSize = writer.BytesWritten - startPosition;
@@ -90,4 +100,31 @@ public void Write(ref BitWriter writer, IExternalData externalData, uint saveVer
90100
writer.WriteUInt16((ushort)tabSize);
91101
writer.SetBitPosition(currentPosition);
92102
}
103+
104+
/// <summary>
105+
/// Prepares the save data for writing to a specific version format.
106+
/// Handles conversion between 1.14 (version 96) and D2R (version 97+) formats.
107+
/// </summary>
108+
private void PrepareForVersion(uint sourceVersion, uint targetVersion)
109+
{
110+
bool sourceIsD2R = sourceVersion > 96;
111+
bool targetIsD2R = targetVersion > 96;
112+
113+
// Only convert if source and target formats differ
114+
if (!sourceIsD2R && targetIsD2R)
115+
Convert114ToD2R();
116+
}
117+
118+
/// <summary>
119+
/// Converts save data from 1.14 format to D2R format.
120+
/// </summary>
121+
private void Convert114ToD2R()
122+
{
123+
// 3. Zero BodyLocation for stored items (D2R fails otherwise)
124+
foreach (var item in Items)
125+
{
126+
if (item.Position.Mode != ItemMode.Equipped)
127+
item.Position.BodyLocation = BodyLocation.None;
128+
}
129+
}
93130
}

0 commit comments

Comments
 (0)