Skip to content

Commit aecea2a

Browse files
feat: Speedup huffman, type descendancy checks and paired stats lookups
1 parent 8b1ec44 commit aecea2a

3 files changed

Lines changed: 92 additions & 43 deletions

File tree

src/D2SSharp/Data/TxtFileExternalData.cs

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ private static VersionData BuildVersionData(ItemStatCostEntry[] itemStatCost, It
148148
ItemStatCost = itemStatCost,
149149
ItemTypes = itemTypes,
150150
Items = items,
151-
ItemCodeToIndex = BuildItemCodeIndex(items)
151+
ItemCodeToIndex = BuildItemCodeIndex(items),
152+
TypeDescendants = BuildTypeDescendants(itemTypes)
152153
};
153154
}
154155

@@ -507,54 +508,86 @@ public ItemTypeInfo GetItemInfo(int itemIndex, uint saveVersion)
507508
hasVariableGfx = data.ItemTypes[typeIndex].VarInvGfx > 0;
508509
}
509510

511+
uint typeCode = typeIndex < data.ItemTypes.Length ? data.ItemTypes[typeIndex].Code : 0;
512+
510513
return new ItemTypeInfo(
511514
CompactSave: item.CompactSave != 0,
512515
HasVariableGfx: hasVariableGfx,
513-
IsArmor: IsTypeOrDescendant(data.ItemTypes, typeIndex, ItemTypeCodes.Armor),
514-
IsWeapon: IsTypeOrDescendant(data.ItemTypes, typeIndex, ItemTypeCodes.Weapon),
515-
IsGold: IsTypeOrDescendant(data.ItemTypes, typeIndex, ItemTypeCodes.Gold),
516+
IsArmor: data.IsDescendant(ItemTypeCodes.Armor, typeCode),
517+
IsWeapon: data.IsDescendant(ItemTypeCodes.Weapon, typeCode),
518+
IsGold: data.IsDescendant(ItemTypeCodes.Gold, typeCode),
516519
IsStackable: item.Stackable != 0,
517-
IsCharm: IsTypeOrDescendant(data.ItemTypes, typeIndex, ItemTypeCodes.Charm),
518-
IsBodyPart: IsTypeOrDescendant(data.ItemTypes, typeIndex, ItemTypeCodes.BodyPart),
519-
IsPlayerBodyPart: IsTypeOrDescendant(data.ItemTypes, typeIndex, ItemTypeCodes.PlayerBodyPart),
520-
IsScrollOrBook: IsTypeOrDescendant(data.ItemTypes, typeIndex, ItemTypeCodes.Scroll) ||
521-
IsTypeOrDescendant(data.ItemTypes, typeIndex, ItemTypeCodes.Book),
520+
IsCharm: data.IsDescendant(ItemTypeCodes.Charm, typeCode),
521+
IsBodyPart: data.IsDescendant(ItemTypeCodes.BodyPart, typeCode),
522+
IsPlayerBodyPart: data.IsDescendant(ItemTypeCodes.PlayerBodyPart, typeCode),
523+
IsScrollOrBook: data.IsDescendant(ItemTypeCodes.Scroll, typeCode) ||
524+
data.IsDescendant(ItemTypeCodes.Book, typeCode),
522525
IsQuest: item.Quest != 0,
523526
QuestDiffCheck: item.QuestDiffCheck != 0
524527
);
525528
}
526529

527-
/// <summary>
528-
/// Checks if the given type index is or descends from the target type code.
529-
/// Traverses the equiv1/equiv2 parent chain.
530-
/// </summary>
531-
private static bool IsTypeOrDescendant(ItemTypeEntry[] itemTypes, int typeIndex, uint targetTypeCode)
530+
private static Dictionary<uint, HashSet<uint>> BuildTypeDescendants(ItemTypeEntry[] itemTypes)
532531
{
533-
// Use a visited set to prevent infinite loops
534-
var visited = new HashSet<int>();
535-
var stack = new Stack<int>();
536-
stack.Push(typeIndex);
532+
var children = new List<int>[itemTypes.Length];
533+
for (int i = 0; i < itemTypes.Length; i++)
534+
{
535+
children[i] = [];
536+
}
537537

538-
while (stack.Count > 0)
538+
for (int i = 0; i < itemTypes.Length; i++)
539539
{
540-
var current = stack.Pop();
541-
if (current < 0 || current >= itemTypes.Length || !visited.Add(current))
542-
continue;
540+
ref readonly var itemType = ref itemTypes[i];
541+
if (itemType.Equiv1 != 0 && itemType.Equiv1 != 0xFFFF)
542+
children[itemType.Equiv1].Add(i);
543+
if (itemType.Equiv2 != 0 && itemType.Equiv2 != 0xFFFF)
544+
children[itemType.Equiv2].Add(i);
545+
}
543546

544-
ref readonly var itemType = ref itemTypes[current];
547+
var descendantsByIndex = new HashSet<uint>[itemTypes.Length];
545548

546-
// Check if this type matches the target
547-
if (itemType.Code == targetTypeCode)
548-
return true;
549+
HashSet<uint> ComputeDescendants(int index)
550+
{
551+
var cached = descendantsByIndex[index];
552+
if (cached != null)
553+
return cached;
549554

550-
// Add parent types to check
551-
if (itemType.Equiv1 != 0 && itemType.Equiv1 != 0xFFFF)
552-
stack.Push(itemType.Equiv1);
553-
if (itemType.Equiv2 != 0 && itemType.Equiv2 != 0xFFFF)
554-
stack.Push(itemType.Equiv2);
555+
var set = new HashSet<uint>();
556+
uint code = itemTypes[index].Code;
557+
if (code != 0)
558+
set.Add(code);
559+
560+
foreach (int child in children[index])
561+
{
562+
uint childCode = itemTypes[child].Code;
563+
if (childCode != 0)
564+
set.Add(childCode);
565+
set.UnionWith(ComputeDescendants(child));
566+
}
567+
568+
descendantsByIndex[index] = set;
569+
return set;
555570
}
556571

557-
return false;
572+
var result = new Dictionary<uint, HashSet<uint>>();
573+
for (int i = 0; i < itemTypes.Length; i++)
574+
{
575+
uint code = itemTypes[i].Code;
576+
if (code == 0)
577+
continue;
578+
579+
var set = ComputeDescendants(i);
580+
if (!result.TryGetValue(code, out var existing))
581+
{
582+
result[code] = set;
583+
}
584+
else
585+
{
586+
existing.UnionWith(set);
587+
}
588+
}
589+
590+
return result;
558591
}
559592

560593
// Internal data structures
@@ -564,6 +597,14 @@ private class VersionData
564597
public required ItemTypeEntry[] ItemTypes { get; init; }
565598
public required ItemEntry[] Items { get; init; }
566599
public required Dictionary<uint, int> ItemCodeToIndex { get; init; }
600+
public required Dictionary<uint, HashSet<uint>> TypeDescendants { get; init; }
601+
602+
public bool IsDescendant(uint targetTypeCode, uint typeCode)
603+
{
604+
return typeCode != 0 &&
605+
TypeDescendants.TryGetValue(targetTypeCode, out var set) &&
606+
set.Contains(typeCode);
607+
}
567608
}
568609

569610
private struct ItemStatCostEntry

src/D2SSharp/HuffmanEncoding.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,27 @@ private static readonly (char Symbol, uint Bits, int Length)[] Table =
5353
// Lookup table for encoding: indexed by character, returns (bits in LSB order, length)
5454
private static readonly (uint Bits, int Length)[] EncodeLookup;
5555

56+
// Lookup tables for decoding: indexed by code length, maps MSB-first code to symbol
57+
private static readonly Dictionary<uint, char>[] DecodeLookup;
58+
5659
// Maximum code length in the table
5760
private const int MaxCodeLength = 9;
5861

5962
static HuffmanEncoding()
6063
{
6164
// Build encode lookup table (supports ASCII characters up to 127)
6265
EncodeLookup = new (uint Bits, int Length)[128];
66+
DecodeLookup = new Dictionary<uint, char>[MaxCodeLength + 1];
67+
for (int i = 0; i <= MaxCodeLength; i++)
68+
{
69+
DecodeLookup[i] = new Dictionary<uint, char>();
70+
}
71+
6372
foreach (var (symbol, bits, length) in Table)
6473
{
6574
// Reverse bits from MSB-first to LSB-first for writing
6675
EncodeLookup[symbol] = (ReverseBits(bits, length), length);
76+
DecodeLookup[length][bits] = symbol;
6777
}
6878
}
6979

@@ -124,18 +134,11 @@ public static uint Decode(ref BitReader reader)
124134
code = (code << 1) | bit;
125135
length++;
126136

127-
// Check if this code matches any entry
128-
foreach (var (symbol, bits, len) in Table)
137+
if (DecodeLookup[length].TryGetValue(code, out var symbol))
129138
{
130-
if (len == length && bits == code)
131-
{
132-
decoded = symbol;
133-
break;
134-
}
135-
}
136-
137-
if (decoded.HasValue)
139+
decoded = symbol;
138140
break;
141+
}
139142
}
140143

141144
if (!decoded.HasValue)

src/D2SSharp/Model/Item.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,11 @@ private static void WriteStatList(ref BitWriter writer, List<Stat> stats, IExter
788788
{
789789
// Build a set of paired follower stats so we don't write them individually
790790
var writtenStats = new HashSet<StatId>();
791+
var statMap = new Dictionary<StatId, Stat>(stats.Count);
792+
foreach (var stat in stats)
793+
{
794+
statMap[stat.Id] = stat;
795+
}
791796

792797
foreach (var stat in stats)
793798
{
@@ -826,7 +831,7 @@ private static void WriteStatList(ref BitWriter writer, List<Stat> stats, IExter
826831
var pairedStatIds = stat.Id.GetPairedStats();
827832
foreach (StatId pairedStatId in pairedStatIds)
828833
{
829-
var pairedStat = stats.Single(s => s.Id == pairedStatId);
834+
statMap.TryGetValue(pairedStatId, out var pairedStat);
830835
var pairedInfo = externalData.GetStatInfo(pairedStatId, saveVersion);
831836

832837
writtenStats.Add(pairedStatId);

0 commit comments

Comments
 (0)