package memory import ( "slices" "sort" "strings" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/item" "github.com/hectorgimenez/d2go/pkg/data/skill" "github.com/hectorgimenez/d2go/pkg/data/stat" "github.com/hectorgimenez/d2go/pkg/data/state" "github.com/hectorgimenez/d2go/pkg/utils" ) func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverData) data.Inventory { mainPlayer := rawPlayerUnits.GetMainPlayer() baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (4 * 1024) unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8) // Process shared stash data - build a UnitID→page mapping for ALL shared stash units. // Post-patch D2R has 5 shared stash units (3 original shared tabs + DLC tabs). stashUnitIDToPage := make(map[uint]uint) // UnitID → 1-based page number stashPlayerUnitOrder := make([]uint, 0, 6) // order values, sorted stashPlayerUnits := make(map[uint]RawPlayerUnit) // order → unit for _, pu := range rawPlayerUnits { if pu.States.HasState(state.Sharedstash) { order := gd.ReadUInt(pu.Address+0xD8, Uint64) stashPlayerUnitOrder = append(stashPlayerUnitOrder, order) stashPlayerUnits[order] = pu } } slices.Sort(stashPlayerUnitOrder) for i, orderKey := range stashPlayerUnitOrder { uid := uint(stashPlayerUnits[orderKey].UnitID) stashUnitIDToPage[uid] = uint(i + 1) // pages are 1-based } // Gold inventoryGold, _ := mainPlayer.BaseStats.FindStat(stat.Gold, 0) mainPlayerStashedGold, _ := mainPlayer.BaseStats.FindStat(stat.StashGold, 0) stashedGold := [6]int{mainPlayerStashedGold.Value, 0, 0, 0, 0, 0} for i, puKey := range stashPlayerUnitOrder { if i > 5 { break // max 6 shared stash pages } if stashGold, found := stashPlayerUnits[puKey].BaseStats.FindStat(stat.StashGold, 0); found { stashedGold[i+1] = stashGold.Value } } inventory := data.Inventory{ Gold: inventoryGold.Value, StashedGold: stashedGold, SharedStashPages: len(stashPlayerUnitOrder), } belt := data.Belt{} // Early return if not in-game to avoid any crash if mainPlayer.Area == 0 { inventory.Belt = belt return inventory } type socketInfo struct { item *data.Item position int // Store X position } socketedItemsMap := make(map[data.UnitID][]socketInfo, 120) // Up to ~120 items could be socketable baseItemsMap := make(map[data.UnitID]*data.Item, 120) // Same number of potential base items allItems := make([]*data.Item, 0, 480) // max capacity: 400 (stashes) + 40 (inv) + 12 (cube) + 12 (equipped) + 16 (belt) // Pre-allocate buffers for repeated use var itemDataBuffer = make([]byte, 144) var unitDataBuffer = make([]byte, 144) var pathBuffer = make([]byte, 144) // Process all items in a single pass for i := 0; i < 128; i++ { itemOffset := 8 * i itemUnitPtr := uintptr(ReadUIntFromBuffer(unitTableBuffer, uint(itemOffset), Uint64)) for itemUnitPtr > 0 { nextItemPtr := uintptr(gd.Process.ReadUInt(itemUnitPtr+0x158, Uint64)) // Read basic item data into pre-allocated buffer if err := gd.Process.ReadIntoBuffer(itemUnitPtr, itemDataBuffer); err != nil { itemUnitPtr = nextItemPtr continue } itemType := ReadUIntFromBuffer(itemDataBuffer, 0x00, Uint32) // Skip non-item entries early if itemType != 4 { itemUnitPtr = nextItemPtr continue } txtFileNo := ReadUIntFromBuffer(itemDataBuffer, 0x04, Uint32) unitID := ReadUIntFromBuffer(itemDataBuffer, 0x08, Uint32) // itemLoc = 0 in inventory, 1 equipped, 2 in belt, 3 on ground, 4 cursor, 5 dropping, 6 socketed itemLoc := ReadUIntFromBuffer(itemDataBuffer, 0x0C, Uint32) unitDataPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x10, Uint64)) if err := gd.Process.ReadIntoBuffer(unitDataPtr, unitDataBuffer); err != nil { itemUnitPtr = nextItemPtr continue } flags := ReadUIntFromBuffer(unitDataBuffer, 0x18, Uint32) invPage := ReadUIntFromBuffer(unitDataBuffer, 0x55, Uint8) itemQuality := ReadUIntFromBuffer(unitDataBuffer, 0x00, Uint32) itemOwnerNPC := ReadUIntFromBuffer(unitDataBuffer, 0x0C, Uint32) // Link to uniqueitems.txt, setitems.txt txtUniqueSet := int32(gd.Process.ReadUInt(unitDataPtr+0x34, Uint32)) pathPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x38, Uint64)) if err := gd.Process.ReadIntoBuffer(pathPtr, pathBuffer); err != nil { itemUnitPtr = nextItemPtr continue } // Item coordinates (X, Y) itemX := ReadUIntFromBuffer(pathBuffer, 0x10, Uint16) itemY := ReadUIntFromBuffer(pathBuffer, 0x14, Uint16) // Create item structure itm := &data.Item{ ID: int(txtFileNo), UnitID: data.UnitID(unitID), Name: item.GetNameByEnum(txtFileNo), Quality: item.Quality(itemQuality), Position: data.Position{ X: int(itemX), Y: int(itemY), }, IsHovered: hover.IsHovered && hover.UnitType == 4 && hover.UnitID == data.UnitID(unitID), Sockets: make([]data.Item, 0), UniqueSetID: txtUniqueSet, } // Set item properties setProperties(itm, uint32(flags)) // Read rare affixes rarePrefix := int16(gd.Process.ReadUInt(unitDataPtr+0x42, Uint16)) rareSuffix := int16(gd.Process.ReadUInt(unitDataPtr+0x44, Uint16)) //autoAffix := int16(gd.Process.ReadUInt(unitDataPtr+0x46, Uint16)) // Read magic affixes var prefixes [3]int16 var suffixes [3]int16 for i := 0; i < 3; i++ { prefixes[i] = int16(gd.Process.ReadUInt(unitDataPtr+0x48+uintptr(i*2), Uint16)) suffixes[i] = int16(gd.Process.ReadUInt(unitDataPtr+0x4E+uintptr(i*2), Uint16)) } itm.Affixes = data.ItemAffixes{ Rare: struct { Prefix int16 Suffix int16 }{ Prefix: rarePrefix, Suffix: rareSuffix, }, Magic: struct { Prefixes [3]int16 Suffixes [3]int16 }{ Prefixes: prefixes, Suffixes: suffixes, }, } maxAffixReq := 0 if itm.Identified { switch itm.Quality { case item.QualityUnique: // find matching item (uniqueitems.txt) for _, uniqueInfo := range item.UniqueItems { if uniqueInfo.ID == int(txtUniqueSet) { itm.IdentifiedName = uniqueInfo.Name itm.LevelReq = uniqueInfo.LevelReq break } } case item.QualitySet: // find matching item (setitems.txt) for setItemName, setItemInfo := range item.SetItems { if setItemInfo.ID == int(txtUniqueSet) { itm.IdentifiedName = string(setItemName) itm.LevelReq = setItemInfo.LevelReq break } } case item.QualityRare, item.QualityCrafted: // Set item name from rare affixes if prefix, exists := item.RarePrefixDesc[int(rarePrefix)]; exists { if suffix, exists := item.RareSuffixDesc[int(rareSuffix)]; exists { itm.IdentifiedName = prefix.Name + " " + suffix.Name } } // Get level requirements from magic affixes for _, prefixID := range prefixes { if prefix, exists := item.MagicPrefixDesc[int(prefixID)]; exists && prefixID != 0 { if prefix.LevelReq > maxAffixReq { maxAffixReq = prefix.LevelReq } } } for _, suffixID := range suffixes { if suffix, exists := item.MagicSuffixDesc[int(suffixID)]; exists && suffixID != 0 { if suffix.LevelReq > maxAffixReq { maxAffixReq = suffix.LevelReq } } } case item.QualityMagic: var prefixParts []string var suffixParts []string // Get all prefixes for _, prefixID := range prefixes { if prefix, exists := item.MagicPrefixDesc[int(prefixID)]; exists && prefixID != 0 { prefixParts = append(prefixParts, prefix.Name) if prefix.LevelReq > maxAffixReq { maxAffixReq = prefix.LevelReq } } } // Get all suffixes for _, suffixID := range suffixes { if suffix, exists := item.MagicSuffixDesc[int(suffixID)]; exists && suffixID != 0 { suffixParts = append(suffixParts, suffix.Name) if suffix.LevelReq > maxAffixReq { maxAffixReq = suffix.LevelReq } } } // Construct name: prefixes + base name + suffixes var nameParts []string if len(prefixParts) > 0 { nameParts = append(nameParts, prefixParts...) } nameParts = append(nameParts, itm.Desc().Name) if len(suffixParts) > 0 { nameParts = append(nameParts, suffixParts...) } itm.IdentifiedName = strings.Join(nameParts, " ") } } // Set runeword name if applicable if itm.IsRuneword { if runeword, exists := item.RunewordIDMap[prefixes[0]]; exists { itm.RunewordName = runeword } } // Determine item location location := item.LocationUnknown switch itemLoc { case 0: // Check shared stash ownership dynamically via UnitID map. // Also keep legacy checks (ownerNPC 2/3/4) for older D2R versions. if page, ok := stashUnitIDToPage[itemOwnerNPC]; ok { location = item.LocationSharedStash invPage = page } else if itemOwnerNPC == 2 && len(stashUnitIDToPage) == 0 { location = item.LocationSharedStash invPage = 1 } else if itemOwnerNPC == 3 && len(stashUnitIDToPage) == 0 { location = item.LocationSharedStash invPage = 2 } else if itemOwnerNPC == 4 && len(stashUnitIDToPage) == 0 { location = item.LocationSharedStash invPage = 3 } else if itemOwnerNPC == 4294967295 { if 0x00002000&flags != 0 { location = item.LocationVendor } else { // DLC stash tab items: ownerNPC=0xFFFFFFFF, no vendor flag. // Classify by item type: gems, runes, or materials. location = classifyDLCTabItem(txtFileNo) } } else if data.UnitID(itemOwnerNPC) == mainPlayer.UnitID || itemOwnerNPC == 1 { if invPage == 0 { location = item.LocationInventory } else if invPage == 3 { location = item.LocationCube invPage = 0 } else { location = item.LocationStash invPage = 0 } } case 1: isMercItem := (flags & 0x800000) != 0 if data.UnitID(itemOwnerNPC) == mainPlayer.UnitID || itemOwnerNPC == 1 { location = item.LocationEquipped if itm.Type().Code == item.TypeBelt { belt.Name = itm.Name } } else if isMercItem { location = item.LocationMercenary } case 2: if data.UnitID(itemOwnerNPC) == mainPlayer.UnitID || itemOwnerNPC == 1 { location = item.LocationBelt } case 3, 5: location = item.LocationGround case 6: location = item.LocationSocket case 4: location = item.LocationCursor } // Set body location if equipped bodyLoc := item.LocNone equipSlotFlags := uint16(gd.Process.ReadUInt(unitDataPtr+uintptr(0x54), Uint16)) if equipSlotFlags&0xFF00 == 0xFF00 { equipSlot := uint8(equipSlotFlags & 0xFF) switch equipSlot { case 0x01: bodyLoc = item.LocHead case 0x02: bodyLoc = item.LocNeck case 0x03: bodyLoc = item.LocTorso case 0x04: bodyLoc = item.LocLeftArm case 0x05: bodyLoc = item.LocRightArm case 0x06: bodyLoc = item.LocLeftRing case 0x07: bodyLoc = item.LocRightRing case 0x08: bodyLoc = item.LocBelt case 0x09: bodyLoc = item.LocFeet case 0x0A: bodyLoc = item.LocGloves case 0x0B: bodyLoc = item.LocLeftArmSecondary case 0x0C: bodyLoc = item.LocRightArmSecondary } } itm.Location = item.Location{ LocationType: location, BodyLocation: bodyLoc, Page: int(invPage), } // We don't care about the inventory we don't know where they are, probably previous games or random crap if location == item.LocationUnknown { itemUnitPtr = nextItemPtr continue } // Read item stats statsListExPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x88, Uint64)) itm.BaseStats, itm.Stats = gd.getItemStats(statsListExPtr) // // Read stack quantity from UnitData+0x9C (DLC tabs store quantity here instead of in stats) // stackQty := gd.Process.ReadUInt(unitDataPtr+0x9C, Uint32) // found := false // for i := range itm.Stats { // if itm.Stats[i].ID == stat.Quantity && itm.Stats[i].Layer == 0 { // itm.Stats[i].Value = int(stackQty) // found = true // break // } // } // if !found && stackQty > 0 { // itm.Stats = append(itm.Stats, stat.Data{ID: stat.Quantity, Value: int(stackQty), Layer: 0}) // } // Process socket information if location == item.LocationSocket { if itm.Desc().Code == "jew" { // Base requirement for jewels itm.LevelReq = itm.Desc().RequiredLevel // For magic/rare jewels, check affixes if itm.Quality == item.QualityMagic || itm.Quality == item.QualityRare { itm.LevelReq = updateMaxReqFromAffixes(itm.LevelReq, itm.Affixes) } // Rainbow facets } else if itm.Quality == item.QualityUnique { for _, uniqueInfo := range item.UniqueItems { if uniqueInfo.Code == itm.Desc().Code { itm.LevelReq = uniqueInfo.LevelReq break } } } else { // Normal socketed items (runes,gems) just use base requirement itm.LevelReq = itm.Desc().RequiredLevel } itemExtraData := uintptr(gd.Process.ReadUInt(unitDataPtr+0xA0, Uint64)) if itemExtraData != 0 { parentInfoPtr := uintptr(gd.Process.ReadUInt(itemExtraData+0x08, Uint64)) if parentInfoPtr != 0 { // Read parent unit ID directly from base item memory structure if err := gd.Process.ReadIntoBuffer(parentInfoPtr, itemDataBuffer); err == nil { parentUnitID := data.UnitID(ReadUIntFromBuffer(itemDataBuffer, 0x08, Uint32)) socketedItemsMap[parentUnitID] = append(socketedItemsMap[parentUnitID], socketInfo{ item: itm, position: itm.Position.X, }) } } } } else { // Check if item has sockets if numSockets, _ := itm.Stats.FindStat(stat.NumSockets, 0); numSockets.Value > 0 { baseItemsMap[itm.UnitID] = itm } } // Add to appropriate collections if location == item.LocationBelt { belt.Items = append(belt.Items, *itm) } else if location != item.LocationSocket { allItems = append(allItems, itm) } itemUnitPtr = nextItemPtr } } // Link sockets to base items for baseUnitID, baseItem := range baseItemsMap { numSockets, _ := baseItem.Stats.FindStat(stat.NumSockets, 0) if numSockets.Value == 0 { continue } // Get socket list for this base item sockets := socketedItemsMap[baseUnitID] if len(sockets) == 0 { continue } if len(sockets) > numSockets.Value { continue } // Sort by position if we have multiple sockets if len(sockets) > 1 { sort.Slice(sockets, func(i, j int) bool { return sockets[i].position < sockets[j].position }) } // Validate socket positions and build final list in one pass (allow partial sockets) baseItem.Sockets = make([]data.Item, 0, len(sockets)) for i, socket := range sockets { if socket.position != i { baseItem.Sockets = nil // Reset if positions are invalid break } baseItem.Sockets = append(baseItem.Sockets, *socket.item) } } // Build final inventory inventory.AllItems = make([]data.Item, len(allItems)) for i, itm := range allItems { baseDesc := itm.Desc() maxReq := calculateItemLevelReq(itm, baseDesc) // Check magic/rare affixes if itm.Identified && (itm.Quality == item.QualityMagic || itm.Quality == item.QualityRare || itm.Quality == item.QualityCrafted) { maxReq = updateMaxReqFromAffixes(maxReq, itm.Affixes) } // Check socketed items for _, socketItem := range itm.Sockets { socketBaseDesc := socketItem.Desc() socketReq := calculateItemLevelReq(&socketItem, socketBaseDesc) if socketReq > maxReq { maxReq = socketReq } // Check affixes on magic socketed items if socketItem.Quality == item.QualityMagic { maxReq = updateMaxReqFromAffixes(maxReq, socketItem.Affixes) } } itm.LevelReq = maxReq // Update stat 92 (LevelRequire) so we can use with .nip for it, s := range itm.Stats { if s.ID == stat.LevelRequire { itm.Stats[it].Value = maxReq break } } inventory.AllItems[i] = *itm } // Sort items by distance if needed, using pre-calculated distances if len(inventory.AllItems) > 0 { // Pre-calculate distances to avoid recalculating in sort comparisons distances := make([]int, len(inventory.AllItems)) for i, invItem := range inventory.AllItems { distances[i] = utils.DistanceFromPoint(mainPlayer.Position, invItem.Position) } sort.SliceStable(inventory.AllItems, func(i, j int) bool { return distances[i] < distances[j] }) } inventory.Belt = belt return inventory } func (gd *GameReader) getItemStats(statsListExPtr uintptr) (stat.Stats, stat.Stats) { // Initial full and base stats extraction fullStats := gd.getStatsList(statsListExPtr + 0xA8) baseStats := gd.getStatsList(statsListExPtr + 0x30) // Create empty LevelRequire stat .We will update it from inventory if _, found := fullStats.FindStat(stat.LevelRequire, 0); !found { fullStats = append(fullStats, stat.Data{ ID: stat.LevelRequire, Value: 0, Layer: 0, }) } // Flags and last stat list pointers flags := gd.Process.ReadUInt(statsListExPtr+0x1C, Uint64) lastStatsList := uintptr(gd.Process.ReadUInt(statsListExPtr+0x90, Uint64)) // If the special flag isn't set, return the current base and full stats if (flags & 0x80000000) == 0 { return baseStats, fullStats } // Prepare for additional stats processing additionalStats := stat.Stats{} statListPtr := lastStatsList // Traverse the stat lists to accumulate additional stats for statListPtr != 0 { statListFlags := gd.Process.ReadUInt(statListPtr+0x1C, Uint64) // If we hit a condition where no further traversal is needed, break if (0x40 & statListFlags & 0xFFFFDFFF) != 0 { break } // Move to the previous stat list statListPtr = uintptr(gd.Process.ReadUInt(statListPtr+0x48, Uint64)) } // If we found a valid previous stat list if statListPtr != 0 { additionalBaseStats := gd.getStatsList(statListPtr + 0x30) // Add only the additional stats that are not already present in fullStats for _, statM := range additionalBaseStats { if _, found := fullStats.FindStat(statM.ID, statM.Layer); !found { additionalStats = append(additionalStats, statM) } } } // Merge additional stats into fullStats (but not into baseStats) fullStats = append(fullStats, additionalStats...) // Handle base stats from potential modifiers and ensure they don't pollute baseStats statListPtr = lastStatsList for statListPtr != 0 { statListFlags := gd.Process.ReadUInt(statListPtr+0x1C, Uint64) if statListFlags != 0 { modifierBaseStats := gd.getStatsList(statListPtr + 0x30) for _, mStat := range modifierBaseStats { // Update fullStats if existingStat, found := fullStats.FindStat(mStat.ID, mStat.Layer); found { existingStat.Value = mStat.Value } else { fullStats = append(fullStats, mStat) } // Add to baseStats only if this stat qualifies as a base stat if (flags&0x80000000) != 0 && statListFlags == 0x80000000 { if _, found := baseStats.FindStat(mStat.ID, mStat.Layer); !found { baseStats = append(baseStats, mStat) } } } } // Move to the previous stat list statListPtr = uintptr(gd.Process.ReadUInt(statListPtr+0x48, Uint64)) } return baseStats, fullStats } func setProperties(item *data.Item, flags uint32) { if 0x4000000&flags != 0 { item.IsRuneword = true } if 0x1000000&flags != 0 { item.IsNamed = true } if 0x400000&flags != 0 { item.Ethereal = true } if 0x20000&flags != 0 { item.IsStartItem = true } if 0x10000&flags != 0 { item.IsEar = true } if 0x2000&flags != 0 { item.InTradeOrStoreScreen = true // If item is sold to vendor or placed in trade with another player } if 0x800&flags != 0 { item.HasSockets = true } if 0x100&flags != 0 { item.IsBroken = true } if 0x10&flags != 0 { item.Identified = true } // Only jewels and runes, gems don't work if 0x8&flags != 0 { item.IsInSocket = true } if 0x1&flags != 0 { item.HasBeenEquipped = true // Item was recently equipped (same game) } } func updateMaxReqFromAffixes(currentMax int, affixes data.ItemAffixes) int { maxReq := currentMax for _, prefixID := range affixes.Magic.Prefixes { if prefix, exists := item.MagicPrefixDesc[int(prefixID)]; exists && prefixID != 0 { if prefix.LevelReq > maxReq { maxReq = prefix.LevelReq } } } for _, suffixID := range affixes.Magic.Suffixes { if suffix, exists := item.MagicSuffixDesc[int(suffixID)]; exists && suffixID != 0 { if suffix.LevelReq > maxReq { maxReq = suffix.LevelReq } } } return maxReq } func staffModLevelRequirement(itm *data.Item, baseDesc item.Description) int { eligibleTypes := map[string]bool{ item.TypeScepter: true, item.TypeWand: true, item.TypeStaff: true, item.TypeOrb: true, item.TypeHandtoHand: true, item.TypeHandtoHand2: true, item.TypePelt: true, item.TypePrimalHelm: true, } if !eligibleTypes[baseDesc.Type] { return 0 } maxReq := 0 checkStats := func(stats stat.Stats) { for _, s := range stats { if s.ID == stat.SingleSkill { if skill, ok := skill.Skills[skill.ID(s.Layer)]; ok && skill.ReqLevel > maxReq { maxReq = skill.ReqLevel } } } } checkStats(itm.BaseStats) checkStats(itm.Stats) return maxReq } func calculateItemLevelReq(itm *data.Item, baseDesc item.Description) int { // Start with base item's requirement maxReq := baseDesc.RequiredLevel // Handle Crafted items first as they have special calculation rules if itm.Quality == item.QualityCrafted { // Count number of affixes and find highest level requirement affixCount := 0 maxAffixReq := 0 for _, prefixID := range itm.Affixes.Magic.Prefixes { if prefix, exists := item.MagicPrefixDesc[int(prefixID)]; exists && prefixID != 0 { affixCount++ if prefix.LevelReq > maxAffixReq { maxAffixReq = prefix.LevelReq } } } for _, suffixID := range itm.Affixes.Magic.Suffixes { if suffix, exists := item.MagicSuffixDesc[int(suffixID)]; exists && suffixID != 0 { affixCount++ if suffix.LevelReq > maxAffixReq { maxAffixReq = suffix.LevelReq } } } // Base calculation levelReq := maxAffixReq + (3 * affixCount) // Add 10 to final level req levelReq += 10 // Cap at 98 if needed (Maximum possible) if levelReq > 99 { levelReq = 98 } return levelReq } // Handle Unique/Set items and their special upgrade cases if itm.Quality == item.QualityUnique || itm.Quality == item.QualitySet { itemCode := baseDesc.Code normalCode := baseDesc.NormalCode exceptionalCode := baseDesc.UberCode eliteCode := baseDesc.UltraCode // Find the original item tier by looking up the unique/set item var originalCode string if itm.Quality == item.QualityUnique { for _, uniqueInfo := range item.UniqueItems { if uniqueInfo.ID == int(itm.UniqueSetID) { originalCode = uniqueInfo.Code break } } } else { for _, setItemInfo := range item.SetItems { if setItemInfo.ID == int(itm.UniqueSetID) { originalCode = setItemInfo.Code break } } } // Apply upgrade requirements if itemCode == eliteCode { if originalCode == normalCode { // Double upgraded: Normal -> Exceptional -> Elite maxReq += 12 // +5 for exceptional, +7 for elite } else if originalCode == exceptionalCode { // Single upgraded: Exceptional -> Elite maxReq += 7 } } else if itemCode == exceptionalCode && originalCode == normalCode { // Single upgraded: Normal -> Exceptional maxReq += 5 } // Check unique/set specific level requirement if itm.Identified { if itm.Quality == item.QualityUnique { for _, uniqueInfo := range item.UniqueItems { if uniqueInfo.ID == int(itm.UniqueSetID) { if uniqueInfo.LevelReq > maxReq { maxReq = uniqueInfo.LevelReq } break } } } else if itm.Quality == item.QualitySet { for _, setItemInfo := range item.SetItems { if setItemInfo.ID == int(itm.UniqueSetID) { if setItemInfo.LevelReq > maxReq { maxReq = setItemInfo.LevelReq } break } } } } } if staffModReq := staffModLevelRequirement(itm, baseDesc); staffModReq > maxReq { maxReq = staffModReq } return maxReq } // gemTypes is the set of d2go item type codes that correspond to gems. var gemTypes = map[string]bool{ item.TypeAmethyst: true, item.TypeDiamond: true, item.TypeEmerald: true, item.TypeRuby: true, item.TypeSapphire: true, item.TypeTopaz: true, item.TypeSkull: true, } // materialIDs is the set of txt file IDs that the DLC Materials tab auto-sorts. // Keys (pk1-pk3), Organs (dhn, bey, mbr), Token (toa), Essences (tes, ceh, bet, fed). var materialIDs = map[uint]bool{ 662: true, // KeyOfTerror 663: true, // KeyOfHate 664: true, // KeyOfDestruction 665: true, // DiablosHorn 666: true, // BaalsEye 667: true, // MephistosBrain 668: true, // TokenOfAbsolution 669: true, // TwistedEssenceOfSuffering 670: true, // ChargedEssenceOfHatred 671: true, // BurningEssenceOfTerror 672: true, // FesteringEssenceOfDestruction } // classifyDLCTabItem determines which DLC stash tab an item belongs to // based on its txt file number. Items with ownerNPC=0xFFFFFFFF and no // vendor flag are DLC tab items; the game auto-sorts them by category. func classifyDLCTabItem(txtFileNo uint) item.LocationType { if desc, ok := item.Desc[int(txtFileNo)]; ok { if desc.Type == item.TypeRune { return item.LocationRunesTab } if gemTypes[desc.Type] { return item.LocationGemsTab } } if materialIDs[txtFileNo] { return item.LocationMaterialsTab } // Fallback: unknown DLC item — treat as materials tab return item.LocationMaterialsTab }