add new location types and enhance inventory structure for shared stash support and DLC support.

This commit is contained in:
crazywh1t3
2026-02-13 22:28:16 +02:00
parent 828db2029f
commit 211abf01f0
4 changed files with 102 additions and 29 deletions

View File

@@ -10,18 +10,21 @@ type Location struct {
const (
// Storage locations
LocationUnknown LocationType = "unknown"
LocationInventory LocationType = "inventory"
LocationStash LocationType = "stash"
LocationSharedStash LocationType = "shared_stash"
LocationBelt LocationType = "belt"
LocationCube LocationType = "cube"
LocationVendor LocationType = "vendor"
LocationGround LocationType = "ground"
LocationSocket LocationType = "socket"
LocationCursor LocationType = "cursor"
LocationEquipped LocationType = "equipped"
LocationMercenary LocationType = "mercenary"
LocationUnknown LocationType = "unknown"
LocationInventory LocationType = "inventory"
LocationStash LocationType = "stash"
LocationSharedStash LocationType = "shared_stash"
LocationBelt LocationType = "belt"
LocationCube LocationType = "cube"
LocationVendor LocationType = "vendor"
LocationGround LocationType = "ground"
LocationSocket LocationType = "socket"
LocationCursor LocationType = "cursor"
LocationEquipped LocationType = "equipped"
LocationMercenary LocationType = "mercenary"
LocationGemsTab LocationType = "gems_tab"
LocationMaterialsTab LocationType = "materials_tab"
LocationRunesTab LocationType = "runes_tab"
// Body locations
LocNone LocationType = "none"

View File

@@ -8,10 +8,11 @@ import (
)
type Inventory struct {
Belt Belt
AllItems []Item
Gold int
StashedGold [4]int
Belt Belt
AllItems []Item
Gold int
StashedGold [6]int // [0]=personal, [1..5]=shared stash pages
SharedStashPages int // Number of shared stash units detected (3=non-DLC, 5+=DLC)
}
func (i Inventory) Find(name item.Name, locations ...item.LocationType) (Item, bool) {

View File

@@ -19,9 +19,11 @@ 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, 3) // Pre-allocate with expected size
// 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)
@@ -30,15 +32,19 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
}
}
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 := [4]int{mainPlayerStashedGold.Value, 0, 0, 0}
stashedGold := [6]int{mainPlayerStashedGold.Value, 0, 0, 0, 0, 0}
for i, puKey := range stashPlayerUnitOrder {
if i > 2 {
break
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
@@ -46,8 +52,9 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
}
inventory := data.Inventory{
Gold: inventoryGold.Value,
StashedGold: stashedGold,
Gold: inventoryGold.Value,
StashedGold: stashedGold,
SharedStashPages: len(stashPlayerUnitOrder),
}
belt := data.Belt{}
@@ -259,17 +266,28 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
location := item.LocationUnknown
switch itemLoc {
case 0:
if itemOwnerNPC == 2 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[0]].UnitID) {
// 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 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[1]].UnitID) {
} else if itemOwnerNPC == 3 && len(stashUnitIDToPage) == 0 {
location = item.LocationSharedStash
invPage = 2
} else if itemOwnerNPC == 4 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[2]].UnitID) {
} else if itemOwnerNPC == 4 && len(stashUnitIDToPage) == 0 {
location = item.LocationSharedStash
invPage = 3
} else if 0x00002000&flags != 0 && itemOwnerNPC == 4294967295 {
location = item.LocationVendor
} 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
@@ -781,3 +799,49 @@ func calculateItemLevelReq(itm *data.Item, baseDesc item.Description) int {
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
}

View File

@@ -72,6 +72,11 @@ func (p *Process) Close() error {
return windows.CloseHandle(p.handler)
}
// ModuleBaseAddress returns the base address of the D2R module.
func (p *Process) ModuleBaseAddress() uintptr {
return p.moduleBaseAddressPtr
}
func getGameModule() (ModuleInfo, error) {
processes := make([]uint32, 2048)
length := uint32(0)