Prefix,Suffix,LevelReq Socketed items on bases, bodyloc, Merc equipment (#74)

* Prefix,Suffix, Socketed with base, bodyloc, Merc equipment

Will add better description later:

Item that has socketed items now display their linked runes/jewels/gems in accurate order

Rare prefix, rare suffix id
Magic prefix1,2,3  magic suffix1,2,3
Prefix1 also used to tell wich runeword it is
Spirit 20635, cta 20519 , infinity 20566 see : getlocalestring.txt

Mercenary equipment now displaying in inventory list . Also has custom location : mercenary instead of equipped

Bodyloc :
	// Body locations
	LocNone              LocationType = "none"
	LocHead              LocationType = "head"
	LocNeck              LocationType = "neck"
	LocTorso             LocationType = "torso"
	LocLeftArm           LocationType = "left_arm"
	LocRightArm          LocationType = "right_arm"
	LocLeftRing          LocationType = "left_ring"
	LocRightRing         LocationType = "right_ring"
	LocBelt              LocationType = "belt"
	LocFeet              LocationType = "feet"
	LocGloves            LocationType = "gloves"
	LocLeftArmSecondary  LocationType = "left_arm_secondary"
	LocRightArmSecondary LocationType = "right_arm_secondary"

	Note that if you begin game on secondary slot it will be left_arm  then primary will be  left_arm_secondary (inversed):  With pr 62 (activeweaponslot) we will be able to solve this potential issue.

* small optimization

* Socketed items to base from pointer

new helper functions.
AutoAffix offset.

* remove helpers fnc, flag isEquipped to HasBeenEquipped

* Runewords names mapped

* helper function not needed

* Optimization

* adjust pre-allocations

* readd removed itemloc comment

* Addex prefix/suffix to rules .nip

new structure:
[Type], [Quality],[Class], [Name], [Flag], [Color], [Prefix], [Suffix]  #  Stats

Exemple : [name] == ring && [quality] == rare && [suffix] == 174 #

Any magic ring with suffix 'of The Apprentice'  ( 10 faster cast rate)

Will be useful for trophy items with fool/cruel  [prefix] == 1416 [prefix] == 1273

* LevelReq + Affixes mapping rare/magic/base/runewords. Nip parser LevelReq

* Unique and set items mapping + LevelReq
This commit is contained in:
elb
2025-02-16 16:02:03 -05:00
committed by GitHub
parent 09dfe3c65d
commit 120c2ba144
21 changed files with 6092 additions and 84 deletions

View File

@@ -3,6 +3,7 @@ package memory
import (
"slices"
"sort"
"strings"
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/item"
@@ -16,8 +17,9 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (4 * 1024)
unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8)
// Process shared stash data
stashPlayerUnits := make(map[uint]RawPlayerUnit)
stashPlayerUnitOrder := make([]uint, 0)
stashPlayerUnitOrder := make([]uint, 0, 3) // Pre-allocate with expected size
for _, pu := range rawPlayerUnits {
if pu.States.HasState(state.Sharedstash) {
order := gd.ReadUInt(pu.Address+0xD8, Uint64)
@@ -30,14 +32,15 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
// Gold
inventoryGold, _ := mainPlayer.BaseStats.FindStat(stat.Gold, 0)
mainPlayerStashedGold, _ := mainPlayer.BaseStats.FindStat(stat.StashGold, 0)
stashedGold := [4]int{}
stashedGold[0] = mainPlayerStashedGold.Value
stashedGold := [4]int{mainPlayerStashedGold.Value, 0, 0, 0}
for i, puKey := range stashPlayerUnitOrder {
if i > 2 {
break
}
stashGold, _ := stashPlayerUnits[puKey].BaseStats.FindStat(stat.StashGold, 0)
stashedGold[i+1] = stashGold.Value
if stashGold, found := stashPlayerUnits[puKey].BaseStats.FindStat(stat.StashGold, 0); found {
stashedGold[i+1] = stashGold.Value
}
}
inventory := data.Inventory{
@@ -45,97 +48,240 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
StashedGold: stashedGold,
}
belt := data.Belt{}
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 {
itemDataBuffer := gd.Process.ReadBytesFromMemory(itemUnitPtr, 144)
itemType := ReadUIntFromBuffer(itemDataBuffer, 0x00, Uint32)
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)
if itemType != 4 {
itemUnitPtr = uintptr(gd.Process.ReadUInt(itemUnitPtr+0x158, 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))
unitDataBuffer := gd.Process.ReadBytesFromMemory(unitDataPtr, 144)
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)
// Item coordinates (X, Y)
// Link to uniqueitems.txt, setitems.txt
txtUniqueSet := int32(gd.Process.ReadUInt(unitDataPtr+0x34, Uint32))
pathPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x38, Uint64))
pathBuffer := gd.Process.ReadBytesFromMemory(pathPtr, 144)
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)
name := item.GetNameByEnum(txtFileNo)
itemHovered := false
if hover.IsHovered && hover.UnitType == 4 && hover.UnitID == data.UnitID(unitID) {
itemHovered = true
}
itm := data.Item{
// Create item structure
itm := &data.Item{
ID: int(txtFileNo),
UnitID: data.UnitID(unitID),
Name: name,
Name: item.GetNameByEnum(txtFileNo),
Quality: item.Quality(itemQuality),
Position: data.Position{
X: int(itemX),
Y: int(itemY),
},
IsHovered: itemHovered,
IsHovered: hover.IsHovered && hover.UnitType == 4 && hover.UnitID == data.UnitID(unitID),
Sockets: make([]data.Item, 0),
UniqueSetID: txtUniqueSet,
}
setProperties(&itm, uint32(flags))
// 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:
// 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, string(itm.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:
if itemOwnerNPC == 2 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[0]].UnitID) {
location = item.LocationSharedStash
invPage = 1
break
}
if itemOwnerNPC == 3 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[1]].UnitID) {
} else if itemOwnerNPC == 3 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[1]].UnitID) {
location = item.LocationSharedStash
invPage = 2
break
}
if itemOwnerNPC == 4 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[2]].UnitID) {
} else if itemOwnerNPC == 4 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[2]].UnitID) {
location = item.LocationSharedStash
invPage = 3
break
}
if 0x00002000&flags != 0 && itemOwnerNPC == 4294967295 {
} else if 0x00002000&flags != 0 && itemOwnerNPC == 4294967295 {
location = item.LocationVendor
break
}
if data.UnitID(itemOwnerNPC) == mainPlayer.UnitID || itemOwnerNPC == 1 {
} else if data.UnitID(itemOwnerNPC) == mainPlayer.UnitID || itemOwnerNPC == 1 {
if invPage == 0 {
location = item.LocationInventory
break
}
if invPage == 3 {
} else if invPage == 3 {
location = item.LocationCube
invPage = 0
break
} else {
location = item.LocationStash
invPage = 0
}
location = item.LocationStash
invPage = 0
break
}
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 {
@@ -149,39 +295,195 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
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 {
// Item Stats
statsListExPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x88, Uint64))
baseStats, stats := gd.getItemStats(statsListExPtr)
itm.Stats = stats
itm.BaseStats = baseStats
if location == item.LocationUnknown {
itemUnitPtr = nextItemPtr
continue
}
if location == item.LocationBelt {
belt.Items = append(belt.Items, itm)
// Read item stats
statsListExPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x88, Uint64))
itm.BaseStats, itm.Stats = gd.getItemStats(statsListExPtr)
// Process socket information
if location == item.LocationSocket {
// Set level requirement for socketed item
if itm.Quality == item.QualityUnique {
for _, uniqueInfo := range item.UniqueItems {
if uniqueInfo.Code == itm.Desc().Code {
itm.LevelReq = uniqueInfo.LevelReq
break
}
}
} else {
inventory.AllItems = append(inventory.AllItems, itm)
// Normal socketed items (like runes) just use base requirement
itm.LevelReq = item.Desc[itm.ID].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
}
}
itemUnitPtr = uintptr(gd.Process.ReadUInt(itemUnitPtr+0x158, Uint64))
// 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) != 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
baseItem.Sockets = make([]data.Item, 0, numSockets.Value)
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 {
maxReq := item.Desc[itm.ID].RequiredLevel
// Check unique/set level requirements
if itm.Quality == item.QualityUnique {
for _, uniqueInfo := range item.UniqueItems {
if uniqueInfo.ID == int(itm.UniqueSetID) && itm.Identified {
if uniqueInfo.LevelReq > maxReq {
maxReq = uniqueInfo.LevelReq
}
itm.IdentifiedName = uniqueInfo.Name
break
}
}
} else if itm.Quality == item.QualitySet {
for setItemName, setItemInfo := range item.SetItems {
if setItemInfo.ID == int(itm.UniqueSetID) && itm.Identified {
if setItemInfo.LevelReq > maxReq {
maxReq = setItemInfo.LevelReq
}
itm.IdentifiedName = string(setItemName)
break
}
}
}
// Check socketed items
for _, socketItem := range itm.Sockets {
if socketItem.LevelReq > maxReq {
maxReq = socketItem.LevelReq
}
// Check affixes on magic socketed items
if socketItem.Quality == item.QualityMagic {
maxReq = updateMaxReqFromAffixes(maxReq, socketItem.Affixes)
}
}
// Check item's own affixes for magic/rare items
if itm.Identified && (itm.Quality == item.QualityMagic || itm.Quality == item.QualityRare) {
maxReq = updateMaxReqFromAffixes(maxReq, itm.Affixes)
}
itm.LevelReq = maxReq
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
sort.SliceStable(inventory.AllItems, func(i, j int) bool {
distanceI := utils.DistanceFromPoint(mainPlayer.Position, inventory.AllItems[i].Position)
distanceJ := utils.DistanceFromPoint(mainPlayer.Position, inventory.AllItems[j].Position)
return distanceI < distanceJ
})
return inventory
}
@@ -282,7 +584,7 @@ func setProperties(item *data.Item, flags uint32) {
item.IsEar = true
}
if 0x2000&flags != 0 {
item.InTradeOrStoreScreen = true
item.InTradeOrStoreScreen = true // If item is sold to vendor or placed in trade with another player
}
if 0x800&flags != 0 {
item.HasSockets = true
@@ -298,6 +600,25 @@ func setProperties(item *data.Item, flags uint32) {
item.IsInSocket = true
}
if 0x1&flags != 0 {
item.IsEquipped = true
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
}