851 lines
25 KiB
Go
851 lines
25 KiB
Go
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 > 4 {
|
|
break // max 5 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, 800) // max capacity: 600 (stashes) + 91 (DLC tabs) + 40 (inv) + 12 (cube) + 12 (equipped) + 16 (belt) + headroom
|
|
|
|
// 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)
|
|
|
|
// stack quantity as item property
|
|
stackQty := gd.Process.ReadUInt(unitDataPtr+0x9C, Uint32)
|
|
itm.StackedQuantity = int(stackQty)
|
|
|
|
// 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
|
|
}
|