Skip to content

Commit d359a51

Browse files
fix: Handle expansion flag migration correctly
1 parent 533079b commit d359a51

2 files changed

Lines changed: 58 additions & 6 deletions

File tree

src/D2SSharp.Tests/IntegrationTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,45 @@ public void Convert_99_To_105_RoundTrip()
489489
Assert.Equal(save105.Character.Class, restored.Character.Class);
490490
}
491491

492+
/// <summary>
493+
/// Tests that expansion sections (MercItems, IronGolem) survive v99→v105→v99 conversion.
494+
/// The v104+ format moves the Expansion flag from CharacterFlags to GameVersion;
495+
/// the reader/writer must use GameVersion for v104+ to avoid losing expansion sections.
496+
/// </summary>
497+
[Fact]
498+
public void Convert_99_To_105_PreservesExpansionSections()
499+
{
500+
var data = File.ReadAllBytes(Path.Combine("Resources", "99", "Roka.d2s"));
501+
var original = D2Save.Read(data);
502+
503+
Assert.True(original.Character.Flags.HasFlag(Enums.CharacterFlags.Expansion), "Source save should be expansion");
504+
Assert.NotNull(original.MercItems);
505+
Assert.NotNull(original.IronGolem);
506+
_output.WriteLine($"Original v{original.Version}: Expansion={original.Character.Flags.HasFlag(Enums.CharacterFlags.Expansion)}, MercItems={original.MercItems != null}, IronGolem={original.IronGolem != null}");
507+
508+
// Convert to v105
509+
var buffer105 = new byte[data.Length * 2];
510+
int written105 = original.Write(buffer105, targetVersion: 105);
511+
512+
// Read back — must still have expansion sections
513+
var save105 = D2Save.Read(buffer105.AsSpan(0, written105));
514+
Assert.Equal(Enums.GameVersion.Expansion, save105.Character.Preview.GameVersion);
515+
Assert.NotNull(save105.MercItems);
516+
Assert.NotNull(save105.IronGolem);
517+
_output.WriteLine($"v105: GameVersion={save105.Character.Preview.GameVersion}, MercItems={save105.MercItems != null}, IronGolem={save105.IronGolem != null}");
518+
519+
// Convert back to v99
520+
var buffer99 = new byte[written105 * 2];
521+
int written99 = save105.Write(buffer99, targetVersion: 99);
522+
523+
// Read back — must still have expansion sections and flag restored
524+
var restored = D2Save.Read(buffer99.AsSpan(0, written99));
525+
Assert.True(restored.Character.Flags.HasFlag(Enums.CharacterFlags.Expansion), "Restored save should have Expansion flag");
526+
Assert.NotNull(restored.MercItems);
527+
Assert.NotNull(restored.IronGolem);
528+
_output.WriteLine($"Restored v{restored.Version}: Expansion={restored.Character.Flags.HasFlag(Enums.CharacterFlags.Expansion)}, MercItems={restored.MercItems != null}, IronGolem={restored.IronGolem != null}");
529+
}
530+
492531
/// <summary>
493532
/// Tests round-trip for the modern v105 shared stash file with chronicle and advanced stash tabs.
494533
/// </summary>

src/D2SSharp/Model/D2Save.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public partial class D2Save
1212
/// <summary>File magic value: 0xAA55AA55</summary>
1313
public const uint Magic = 0xAA55AA55;
1414

15-
/// <summary>Current save version (96 for 1.10+).</summary>
16-
public const uint CurrentVersion = 96;
15+
/// <summary>Current save version (105 for D2R 3.x).</summary>
16+
public const uint CurrentVersion = 105;
1717

1818
/// <summary>Save file version.</summary>
1919
public uint Version { get; set; } = CurrentVersion;
@@ -104,7 +104,7 @@ public static D2Save Read(ReadOnlySpan<byte> data, IExternalData externalData)
104104
save.Corpses = CorpseSection.Read(ref reader, externalData, save.Version);
105105

106106
// Expansion-only sections
107-
if (save.Character.Flags.HasFlag(CharacterFlags.Expansion))
107+
if (save.IsExpansion(save.Version))
108108
{
109109
save.MercItems = MercItemsSection.Read(ref reader, externalData, save.Character.MercData.HasMerc, save.Version);
110110
save.IronGolem = IronGolemSection.Read(ref reader, externalData, save.Version);
@@ -171,7 +171,7 @@ public int Write(Span<byte> buffer, IExternalData externalData, uint? targetVers
171171
Corpses.Write(ref writer, externalData, Version);
172172

173173
// Expansion-only sections
174-
if (Character.Flags.HasFlag(CharacterFlags.Expansion))
174+
if (IsExpansion(Version))
175175
{
176176
(MercItems ?? new MercItemsSection()).Write(ref writer, externalData, Character.MercData.HasMerc, Version);
177177
(IronGolem ?? new IronGolemSection()).Write(ref writer, externalData, Version);
@@ -227,7 +227,7 @@ public int EstimateSize()
227227
}
228228

229229
// Expansion sections
230-
if (Character.Flags.HasFlag(CharacterFlags.Expansion))
230+
if (IsExpansion(Version))
231231
{
232232
// Merc items
233233
size += 4 + CountItemsRecursive(MercItems?.Items) * 200;
@@ -283,6 +283,13 @@ public static bool VerifyChecksum(ReadOnlySpan<byte> data)
283283
return ChecksumCalculator.Verify(data);
284284
}
285285

286+
/// <summary>
287+
/// Returns true if this save is an expansion save, checking GameVersion for v104+ or CharacterFlags for older versions.
288+
/// </summary>
289+
private bool IsExpansion(uint version) => version >= 104
290+
? Character.Preview.GameVersion > GameVersion.Classic
291+
: Character.Flags.HasFlag(CharacterFlags.Expansion);
292+
286293
/// <summary>
287294
/// Prepares the save data for writing to a specific version format.
288295
/// Handles conversion between 1.14 (version 96) and D2R (version 97+) formats,
@@ -381,10 +388,11 @@ private void Convert103To104()
381388
}
382389

383390
// GuildEmblemColor: keep low byte (uint -> byte)
384-
Character.Preview.GuildEmblemColor = Character.Preview.GuildEmblemColor & 0xFF;
391+
Character.Preview.GuildEmblemColor &= 0xFF;
385392

386393
// GameVersion: derive from CharacterFlags
387394
Character.Preview.GameVersion = Character.Flags.HasFlag(CharacterFlags.Expansion) ? GameVersion.Expansion : GameVersion.Classic;
395+
Character.Flags &= ~CharacterFlags.Expansion;
388396

389397
// SaveTimes/Experiences: slots 0-1 already populated, 2-5 remain zero (default)
390398
}
@@ -402,6 +410,11 @@ private void Convert104To103()
402410
: Character.Preview.Name;
403411
}
404412

413+
if (Character.Preview.GameVersion > GameVersion.Classic)
414+
{
415+
Character.Flags |= CharacterFlags.Expansion;
416+
}
417+
405418
// GuildEmblemColor: byte -> uint (zero-extend, already correct)
406419

407420
// SaveTimes/Experiences: only [0] and [1] are used in old format

0 commit comments

Comments
 (0)