Skip to content

Commit ca12282

Browse files
fix: Make stats mutable, fix made up API in readme
1 parent c32f062 commit ca12282

10 files changed

Lines changed: 167 additions & 97 deletions

File tree

README.md

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dotnet build
5454
### Reading a Character Save
5555

5656
```csharp
57+
using D2SSharp.Enums;
5758
using D2SSharp.Model;
5859

5960
// Read save file (uses built-in game data)
@@ -62,9 +63,13 @@ D2Save save = D2Save.Read(saveBytes);
6263

6364
// Access character info
6465
Console.WriteLine($"Character: {save.Character.Preview.Name}");
65-
Console.WriteLine($"Level: {save.Stats.Level}");
66+
Console.WriteLine($"Level: {save.Character.Level}");
6667
Console.WriteLine($"Class: {save.Character.Class}");
6768

69+
// Access player stats
70+
Console.WriteLine($"Strength: {save.Stats.GetStat(StatId.Strength)}");
71+
Console.WriteLine($"Stash Gold: {save.Stats.GetStat(StatId.StashGold)}");
72+
6873
// Iterate items
6974
foreach (var item in save.Items)
7075
{
@@ -75,16 +80,62 @@ foreach (var item in save.Items)
7580
### Modifying and Saving
7681

7782
```csharp
78-
// Modify stats
79-
save.Stats.Strength = 200;
80-
save.Stats.GoldInStash = 2500000;
83+
// Modify player stats
84+
save.Stats.SetStat(StatId.Strength, 200);
85+
save.Stats.SetStat(StatId.StashGold, 2500000);
86+
87+
// Modify character fields directly
88+
save.Character.Level = 99;
8189

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

96+
### Working with Items
97+
98+
```csharp
99+
// Access item stats
100+
foreach (var item in save.Items)
101+
{
102+
// Item properties
103+
Console.WriteLine($"{item.ItemCodeString}: Level {item.ItemLevel}, Quality {item.Quality}");
104+
105+
// Item stats (complete items only)
106+
foreach (var stat in item.Stats)
107+
{
108+
Console.WriteLine($" {stat.Id} = {stat.Value}");
109+
}
110+
111+
// Stats are mutable
112+
var fireStat = item.Stats.FirstOrDefault(s => s.Id == StatId.FireResist);
113+
if (fireStat != null)
114+
fireStat.Value = 50;
115+
116+
// Add new stats
117+
item.Stats.Add(new Stat { Id = StatId.MagicFind, Value = 25 });
118+
119+
// Socketed items
120+
foreach (var socket in item.Sockets)
121+
{
122+
Console.WriteLine($" Socket: {socket?.ItemCodeString}");
123+
}
124+
}
125+
```
126+
127+
### Working with Skills
128+
129+
```csharp
130+
// Skills are indexed 0-29 within each class
131+
// Get/set skill levels by index
132+
save.Skills[0] = 20; // Max out first skill
133+
134+
// Map between skill index and actual skill ID
135+
int skillId = save.Skills.GetSkillId(0); // Index -> skill ID
136+
int index = save.Skills.GetSkillIndex(skillId); // Skill ID -> index
137+
```
138+
88139
### Reading Shared Stash
89140

90141
```csharp
0 Bytes
Binary file not shown.

src/D2SSharp.Tests/RoundTripTests.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ public void RoundTrip_CompleteItem_Magic()
7777
item.Quality = ItemQuality.Magic;
7878
item.Flags |= ItemFlags.Identified;
7979
item.QualityData = new MagicQualityData { PrefixId = 100, SuffixId = 50 };
80-
item.Stats.Add(new Stat(StatId.FireResist, 0, 20)); // fire resist
81-
item.Stats.Add(new Stat(StatId.IncreasedAttackSpeed, 0, 10)); // IAS
80+
item.Stats.Add(new Stat { Id = StatId.FireResist, Value = 20 }); // fire resist
81+
item.Stats.Add(new Stat { Id = StatId.IncreasedAttackSpeed, Value = 10 }); // IAS
8282
AssertItemRoundTrip(item);
8383
}
8484

@@ -98,8 +98,8 @@ public void RoundTrip_CompleteItem_Rare()
9898
Prefixes = [100, 0, 200],
9999
Suffixes = [50, 75, 0]
100100
};
101-
item.Stats.Add(new Stat(StatId.FireResist, 0, 30));
102-
item.Stats.Add(new Stat(StatId.LightningResist, 0, 25));
101+
item.Stats.Add(new Stat { Id = StatId.FireResist, Value = 30 });
102+
item.Stats.Add(new Stat { Id = StatId.LightningResist, Value = 25 });
103103
AssertItemRoundTrip(item);
104104
}
105105

@@ -115,9 +115,9 @@ public void RoundTrip_CompleteItem_ColdDamage()
115115
item.QualityData = new MagicQualityData { PrefixId = 100, SuffixId = 0 };
116116

117117
// Cold damage is a paired stat: coldmindam triggers coldmaxdam and coldlength
118-
item.Stats.Add(new Stat(StatId.ColdMinDamage, 0, 5));
119-
item.Stats.Add(new Stat(StatId.ColdMaxDamage, 0, 10));
120-
item.Stats.Add(new Stat(StatId.ColdLength, 0, 50));
118+
item.Stats.Add(new Stat { Id = StatId.ColdMinDamage, Value = 5 });
119+
item.Stats.Add(new Stat { Id = StatId.ColdMaxDamage, Value = 10 });
120+
item.Stats.Add(new Stat { Id = StatId.ColdLength, Value = 50 });
121121

122122
AssertItemRoundTrip(item);
123123
}
@@ -134,9 +134,9 @@ public void RoundTrip_CompleteItem_PoisonDamage()
134134
item.QualityData = new MagicQualityData { PrefixId = 150, SuffixId = 0 };
135135

136136
// Poison damage is a paired stat
137-
item.Stats.Add(new Stat(StatId.PoisonMinDamage, 0, 10));
138-
item.Stats.Add(new Stat(StatId.PoisonMaxDamage, 0, 20));
139-
item.Stats.Add(new Stat(StatId.PoisonLength, 0, 100));
137+
item.Stats.Add(new Stat { Id = StatId.PoisonMinDamage, Value = 10 });
138+
item.Stats.Add(new Stat { Id = StatId.PoisonMaxDamage, Value = 20 });
139+
item.Stats.Add(new Stat { Id = StatId.PoisonLength, Value = 100 });
140140

141141
AssertItemRoundTrip(item);
142142
}
@@ -178,10 +178,10 @@ public void RoundTrip_CompleteItem_Runeword()
178178
// Runeword stats
179179
item.RunewordStats =
180180
[
181-
new Stat(StatId.FireResist, 0, 35),
182-
new Stat(StatId.LightningResist, 0, 35),
183-
new Stat(StatId.ColdResist, 0, 35),
184-
new Stat(StatId.PoisonResist, 0, 35)
181+
new Stat { Id = StatId.FireResist, Value = 35 },
182+
new Stat { Id = StatId.LightningResist, Value = 35 },
183+
new Stat { Id = StatId.ColdResist, Value = 35 },
184+
new Stat { Id = StatId.PoisonResist, Value = 35 }
185185
];
186186

187187
AssertItemRoundTrip(item);
@@ -239,11 +239,11 @@ public void RoundTrip_CompleteItem_SetWithBonuses()
239239
item.SetItemMask = 0b00101; // Bits 0 and 2 set
240240

241241
// Main stats
242-
item.Stats.Add(new Stat(StatId.FireResist, 0, 15));
242+
item.Stats.Add(new Stat { Id = StatId.FireResist, Value = 15 });
243243

244244
// Set bonus lists (for bits 0 and 2)
245-
item.SetBonusStats.Add([new Stat(StatId.LightningResist, 0, 10)]);
246-
item.SetBonusStats.Add([new Stat(StatId.ColdResist, 0, 10)]);
245+
item.SetBonusStats.Add([new Stat { Id = StatId.LightningResist, Value = 10 }]);
246+
item.SetBonusStats.Add([new Stat { Id = StatId.ColdResist, Value = 10 }]);
247247

248248
AssertItemRoundTrip(item);
249249
}
@@ -483,7 +483,7 @@ private Item GenerateRandomItem(Random random)
483483
{
484484
StatId statId = statIds[random.Next(statIds.Length)];
485485
int value = random.Next(1, 50);
486-
item.Stats.Add(new Stat(statId, 0, value));
486+
item.Stats.Add(new Stat { Id = statId, Value = value });
487487
}
488488

489489
// Occasionally add realm data (old format: 2 x uint32)

src/D2SSharp.Tests/SizeEstimationTests.cs

Lines changed: 54 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -243,29 +243,29 @@ private static Item CreateRarePersonalizedRing(StorePage page, int x, int y, str
243243
Stats =
244244
[
245245
// 4 attributes (displays as "+X to all attributes")
246-
new Stat(StatId.Strength, 0, 20),
247-
new Stat(StatId.Dexterity, 0, 20),
248-
new Stat(StatId.Vitality, 0, 20),
249-
new Stat(StatId.Energy, 0, 20),
246+
new Stat { Id = StatId.Strength, Value = 20 },
247+
new Stat { Id = StatId.Dexterity, Value = 20 },
248+
new Stat { Id = StatId.Vitality, Value = 20 },
249+
new Stat { Id = StatId.Energy, Value = 20 },
250250
// 4 resists (displays as "+X% to all resistances")
251-
new Stat(StatId.FireResist, 0, 30),
252-
new Stat(StatId.LightningResist, 0, 30),
253-
new Stat(StatId.ColdResist, 0, 30),
254-
new Stat(StatId.PoisonResist, 0, 30),
251+
new Stat { Id = StatId.FireResist, Value = 30 },
252+
new Stat { Id = StatId.LightningResist, Value = 30 },
253+
new Stat { Id = StatId.ColdResist, Value = 30 },
254+
new Stat { Id = StatId.PoisonResist, Value = 30 },
255255
// Fire damage (displays as "+X-Y fire damage")
256-
new Stat(StatId.FireMinDamage, 0, 10),
257-
new Stat(StatId.FireMaxDamage, 0, 20),
256+
new Stat { Id = StatId.FireMinDamage, Value = 10 },
257+
new Stat { Id = StatId.FireMaxDamage, Value = 20 },
258258
// Lightning damage (displays as "+X-Y lightning damage")
259-
new Stat(StatId.LightningMinDamage, 0, 1),
260-
new Stat(StatId.LightningMaxDamage, 0, 30),
259+
new Stat { Id = StatId.LightningMinDamage, Value = 1 },
260+
new Stat { Id = StatId.LightningMaxDamage, Value = 30 },
261261
// Cold damage (displays as "+X-Y cold damage")
262-
new Stat(StatId.ColdMinDamage, 0, 10),
263-
new Stat(StatId.ColdMaxDamage, 0, 20),
264-
new Stat(StatId.ColdLength, 0, 100),
262+
new Stat { Id = StatId.ColdMinDamage, Value = 10 },
263+
new Stat { Id = StatId.ColdMaxDamage, Value = 20 },
264+
new Stat { Id = StatId.ColdLength, Value = 100 },
265265
// Poison damage (displays as "+X poison damage over Y seconds")
266-
new Stat(StatId.PoisonMinDamage, 0, 50),
267-
new Stat(StatId.PoisonMaxDamage, 0, 50),
268-
new Stat(StatId.PoisonLength, 0, 150)
266+
new Stat { Id = StatId.PoisonMinDamage, Value = 50 },
267+
new Stat { Id = StatId.PoisonMaxDamage, Value = 50 },
268+
new Stat { Id = StatId.PoisonLength, Value = 150 }
269269
]
270270
};
271271
}
@@ -300,29 +300,29 @@ private static Item CreateSocketedPersonalizedWand(StorePage page, int x, int y,
300300
Stats =
301301
[
302302
// 4 attributes (displays as "+X to all attributes")
303-
new Stat(StatId.Strength, 0, 20),
304-
new Stat(StatId.Dexterity, 0, 20),
305-
new Stat(StatId.Vitality, 0, 20),
306-
new Stat(StatId.Energy, 0, 20),
303+
new Stat { Id = StatId.Strength, Value = 20 },
304+
new Stat { Id = StatId.Dexterity, Value = 20 },
305+
new Stat { Id = StatId.Vitality, Value = 20 },
306+
new Stat { Id = StatId.Energy, Value = 20 },
307307
// 4 resists (displays as "+X% to all resistances")
308-
new Stat(StatId.FireResist, 0, 30),
309-
new Stat(StatId.LightningResist, 0, 30),
310-
new Stat(StatId.ColdResist, 0, 30),
311-
new Stat(StatId.PoisonResist, 0, 30),
308+
new Stat { Id = StatId.FireResist, Value = 30 },
309+
new Stat { Id = StatId.LightningResist, Value = 30 },
310+
new Stat { Id = StatId.ColdResist, Value = 30 },
311+
new Stat { Id = StatId.PoisonResist, Value = 30 },
312312
// Fire damage (displays as "+X-Y fire damage")
313-
new Stat(StatId.FireMinDamage, 0, 10),
314-
new Stat(StatId.FireMaxDamage, 0, 20),
313+
new Stat { Id = StatId.FireMinDamage, Value = 10 },
314+
new Stat { Id = StatId.FireMaxDamage, Value = 20 },
315315
// Lightning damage (displays as "+X-Y lightning damage")
316-
new Stat(StatId.LightningMinDamage, 0, 1),
317-
new Stat(StatId.LightningMaxDamage, 0, 30),
316+
new Stat { Id = StatId.LightningMinDamage, Value = 1 },
317+
new Stat { Id = StatId.LightningMaxDamage, Value = 30 },
318318
// Cold damage (displays as "+X-Y cold damage")
319-
new Stat(StatId.ColdMinDamage, 0, 10),
320-
new Stat(StatId.ColdMaxDamage, 0, 20),
321-
new Stat(StatId.ColdLength, 0, 100),
319+
new Stat { Id = StatId.ColdMinDamage, Value = 10 },
320+
new Stat { Id = StatId.ColdMaxDamage, Value = 20 },
321+
new Stat { Id = StatId.ColdLength, Value = 100 },
322322
// Poison damage (displays as "+X poison damage over Y seconds")
323-
new Stat(StatId.PoisonMinDamage, 0, 50),
324-
new Stat(StatId.PoisonMaxDamage, 0, 50),
325-
new Stat(StatId.PoisonLength, 0, 150)
323+
new Stat { Id = StatId.PoisonMinDamage, Value = 50 },
324+
new Stat { Id = StatId.PoisonMaxDamage, Value = 50 },
325+
new Stat { Id = StatId.PoisonLength, Value = 150 }
326326
],
327327
Sockets =
328328
[
@@ -359,29 +359,29 @@ private static Item CreateRareJewel()
359359
Stats =
360360
[
361361
// 4 attributes (displays as "+X to all attributes")
362-
new Stat(StatId.Strength, 0, 15),
363-
new Stat(StatId.Dexterity, 0, 15),
364-
new Stat(StatId.Vitality, 0, 15),
365-
new Stat(StatId.Energy, 0, 15),
362+
new Stat { Id = StatId.Strength, Value = 15 },
363+
new Stat { Id = StatId.Dexterity, Value = 15 },
364+
new Stat { Id = StatId.Vitality, Value = 15 },
365+
new Stat { Id = StatId.Energy, Value = 15 },
366366
// 4 resists (displays as "+X% to all resistances")
367-
new Stat(StatId.FireResist, 0, 30),
368-
new Stat(StatId.LightningResist, 0, 30),
369-
new Stat(StatId.ColdResist, 0, 30),
370-
new Stat(StatId.PoisonResist, 0, 30),
367+
new Stat { Id = StatId.FireResist, Value = 30 },
368+
new Stat { Id = StatId.LightningResist, Value = 30 },
369+
new Stat { Id = StatId.ColdResist, Value = 30 },
370+
new Stat { Id = StatId.PoisonResist, Value = 30 },
371371
// Fire damage (displays as "+X-Y fire damage")
372-
new Stat(StatId.FireMinDamage, 0, 5),
373-
new Stat(StatId.FireMaxDamage, 0, 10),
372+
new Stat { Id = StatId.FireMinDamage, Value = 5 },
373+
new Stat { Id = StatId.FireMaxDamage, Value = 10 },
374374
// Lightning damage (displays as "+X-Y lightning damage")
375-
new Stat(StatId.LightningMinDamage, 0, 1),
376-
new Stat(StatId.LightningMaxDamage, 0, 15),
375+
new Stat { Id = StatId.LightningMinDamage, Value = 1 },
376+
new Stat { Id = StatId.LightningMaxDamage, Value = 15 },
377377
// Cold damage (displays as "+X-Y cold damage")
378-
new Stat(StatId.ColdMinDamage, 0, 5),
379-
new Stat(StatId.ColdMaxDamage, 0, 10),
380-
new Stat(StatId.ColdLength, 0, 50),
378+
new Stat { Id = StatId.ColdMinDamage, Value = 5 },
379+
new Stat { Id = StatId.ColdMaxDamage, Value = 10 },
380+
new Stat { Id = StatId.ColdLength, Value = 50 },
381381
// Poison damage (displays as "+X poison damage over Y seconds")
382-
new Stat(StatId.PoisonMinDamage, 0, 25),
383-
new Stat(StatId.PoisonMaxDamage, 0, 25),
384-
new Stat(StatId.PoisonLength, 0, 75)
382+
new Stat { Id = StatId.PoisonMinDamage, Value = 25 },
383+
new Stat { Id = StatId.PoisonMaxDamage, Value = 25 },
384+
new Stat { Id = StatId.PoisonLength, Value = 75 }
385385
]
386386
};
387387
}

src/D2SSharp/Model/CharacterIdentity.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ namespace D2SSharp.Model;
55
/// Stored as two little-endian uint32 values on wire.
66
/// Note: the field order (which uint32 is account vs character) may be swapped.
77
/// </summary>
8-
/// <param name="GameAccountId">32-bit game account ID.</param>
9-
/// <param name="CharacterId">32-bit character ID.</param>
10-
public readonly record struct CharacterIdentity(uint GameAccountId, uint CharacterId);
8+
public record struct CharacterIdentity
9+
{
10+
/// <summary>32-bit game account ID.</summary>
11+
public uint GameAccountId { get; set; }
12+
13+
/// <summary>32-bit character ID.</summary>
14+
public uint CharacterId { get; set; }
15+
}

src/D2SSharp/Model/ChronicleSection.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ private static ChronicleEntry ReadEntry(ref BitReader reader)
108108
uint itemId = reader.ReadUInt32();
109109
ushort source = reader.ReadUInt16();
110110
uint timestamp = reader.ReadUInt32();
111-
return new ChronicleEntry(itemId, source, timestamp);
111+
return new ChronicleEntry { ItemId = itemId, Source = source, Timestamp = timestamp };
112112
}
113113

114114
private static void WriteEntry(ref BitWriter writer, ChronicleEntry entry)
@@ -122,7 +122,14 @@ private static void WriteEntry(ref BitWriter writer, ChronicleEntry entry)
122122
/// <summary>
123123
/// A single chronicle entry tracking a found item.
124124
/// </summary>
125-
/// <param name="ItemId">items.txt row index for set/unique items; runeword string index for runewords.</param>
126-
/// <param name="Source">Source identifier. Non-zero for set/unique items, 0 for runewords.</param>
127-
/// <param name="Timestamp">When the chronicle entry was created.</param>
128-
public readonly record struct ChronicleEntry(uint ItemId, ushort Source, uint Timestamp);
125+
public record struct ChronicleEntry
126+
{
127+
/// <summary>items.txt row index for set/unique items; runeword string index for runewords.</summary>
128+
public uint ItemId { get; set; }
129+
130+
/// <summary>Source identifier. Non-zero for set/unique items, 0 for runewords.</summary>
131+
public ushort Source { get; set; }
132+
133+
/// <summary>When the chronicle entry was created.</summary>
134+
public uint Timestamp { get; set; }
135+
}

src/D2SSharp/Model/DemonSection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ private static List<Stat> ReadDemonStats(ReadOnlySpan<byte> statBlob, IExternalD
265265
// Demon stats always use 32-bit raw values
266266
long value = statReader.ReadBits(32);
267267

268-
stats.Add(new Stat(statId, layer, value));
268+
stats.Add(new Stat { Id = statId, Layer = layer, Value = value });
269269
}
270270

271271
return stats;

0 commit comments

Comments
 (0)