diff --git a/pkg/data/item/location.go b/pkg/data/item/location.go index 18624ae..eb64ac0 100644 --- a/pkg/data/item/location.go +++ b/pkg/data/item/location.go @@ -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" diff --git a/pkg/data/items.go b/pkg/data/items.go index 8f593ad..2b0816c 100644 --- a/pkg/data/items.go +++ b/pkg/data/items.go @@ -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) { diff --git a/pkg/memory/item.go b/pkg/memory/item.go index a1d495b..44f06bb 100644 --- a/pkg/memory/item.go +++ b/pkg/memory/item.go @@ -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 +} diff --git a/pkg/memory/process.go b/pkg/memory/process.go index 2b2b91e..a7f6575 100644 --- a/pkg/memory/process.go +++ b/pkg/memory/process.go @@ -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)