package items import ( "fmt" "log" ) // NewPlayerItemList creates a new player item list func NewPlayerItemList() *PlayerItemList { return &PlayerItemList{ indexedItems: make(map[int32]*Item), items: make(map[int32]map[int8]map[int16]*Item), overflowItems: make([]*Item, 0), } } // SetMaxItemIndex sets and returns the maximum saved item index func (pil *PlayerItemList) SetMaxItemIndex() int32 { pil.mutex.Lock() defer pil.mutex.Unlock() maxIndex := int32(0) for index := range pil.indexedItems { if index > maxIndex { maxIndex = index } } pil.maxSavedIndex = maxIndex return maxIndex } // SharedBankAddAllowed checks if an item can be added to shared bank func (pil *PlayerItemList) SharedBankAddAllowed(item *Item) bool { if item == nil { return false } // Check item flags that prevent shared bank storage if item.CheckFlag(NoTrade) || item.CheckFlag(Attuned) || item.CheckFlag(LoreEquip) { return false } // Check heirloom flag if item.CheckFlag2(Heirloom) { return true // Heirloom items can go in shared bank } return true } // GetItemsFromBagID gets all items from a specific bag func (pil *PlayerItemList) GetItemsFromBagID(bagID int32) []*Item { pil.mutex.RLock() defer pil.mutex.RUnlock() var bagItems []*Item if bagMap, exists := pil.items[bagID]; exists { for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil { bagItems = append(bagItems, item) } } } } return bagItems } // GetItemsInBag gets all items inside a bag item func (pil *PlayerItemList) GetItemsInBag(bag *Item) []*Item { if bag == nil || !bag.IsBag() { return nil } return pil.GetItemsFromBagID(bag.Details.BagID) } // GetBag gets a bag from an inventory slot func (pil *PlayerItemList) GetBag(inventorySlot int8, lock bool) *Item { if lock { pil.mutex.RLock() defer pil.mutex.RUnlock() } // Check main inventory slots for bagID := int32(0); bagID < NumInvSlots; bagID++ { if bagMap, exists := pil.items[bagID]; exists { if slot0Map, exists := bagMap[0]; exists { if item, exists := slot0Map[int16(inventorySlot)]; exists && item != nil && item.IsBag() { return item } } } } return nil } // HasItem checks if the player has a specific item func (pil *PlayerItemList) HasItem(itemID int32, includeBank bool) bool { pil.mutex.RLock() defer pil.mutex.RUnlock() for bagID, bagMap := range pil.items { // Skip bank slots if not including bank if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 { continue } for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil && item.Details.ItemID == itemID { return true } } } } return false } // GetItemFromIndex gets an item by its index func (pil *PlayerItemList) GetItemFromIndex(index int32) *Item { pil.mutex.RLock() defer pil.mutex.RUnlock() if item, exists := pil.indexedItems[index]; exists { return item } return nil } // MoveItem moves an item to a new location func (pil *PlayerItemList) MoveItem(item *Item, invSlot int32, slot int16, appearanceType int8, eraseOld bool) { if item == nil { return } pil.mutex.Lock() defer pil.mutex.Unlock() // Remove from old location if requested if eraseOld { pil.eraseItemInternal(item) } // Update item location item.Details.InvSlotID = invSlot item.Details.SlotID = slot item.Details.AppearanceType = int16(appearanceType) // Add to new location pil.addItemToLocationInternal(item, invSlot, appearanceType, slot) } // MoveItemByIndex moves an item by index to a new location func (pil *PlayerItemList) MoveItemByIndex(toBagID int32, fromIndex int16, to int8, appearanceType int8, charges int8) bool { pil.mutex.Lock() defer pil.mutex.Unlock() // Find item by index var item *Item for _, bagMap := range pil.items { for _, slotMap := range bagMap { for _, foundItem := range slotMap { if foundItem != nil && foundItem.Details.NewIndex == fromIndex { item = foundItem break } } if item != nil { break } } if item != nil { break } } if item == nil { return false } // Remove from old location pil.eraseItemInternal(item) // Update item properties item.Details.BagID = toBagID item.Details.SlotID = int16(to) item.Details.AppearanceType = int16(appearanceType) if charges > 0 { item.Details.Count = int16(charges) } // Add to new location pil.addItemToLocationInternal(item, toBagID, appearanceType, int16(to)) return true } // EraseItem removes an item from the inventory func (pil *PlayerItemList) EraseItem(item *Item) { if item == nil { return } pil.mutex.Lock() defer pil.mutex.Unlock() pil.eraseItemInternal(item) } // eraseItemInternal removes an item from internal storage (assumes lock is held) func (pil *PlayerItemList) eraseItemInternal(item *Item) { if item == nil { return } // Remove from indexed items for index, indexedItem := range pil.indexedItems { if indexedItem == item { delete(pil.indexedItems, index) break } } // Remove from location-based storage if bagMap, exists := pil.items[item.Details.BagID]; exists { if slotMap, exists := bagMap[int8(item.Details.AppearanceType)]; exists { delete(slotMap, item.Details.SlotID) // Clean up empty maps if len(slotMap) == 0 { delete(bagMap, int8(item.Details.AppearanceType)) if len(bagMap) == 0 { delete(pil.items, item.Details.BagID) } } } } // Remove from overflow items for i, overflowItem := range pil.overflowItems { if overflowItem == item { pil.overflowItems = append(pil.overflowItems[:i], pil.overflowItems[i+1:]...) break } } } // GetItemFromUniqueID gets an item by its unique ID func (pil *PlayerItemList) GetItemFromUniqueID(uniqueID int32, includeBank bool, lock bool) *Item { if lock { pil.mutex.RLock() defer pil.mutex.RUnlock() } for bagID, bagMap := range pil.items { // Skip bank slots if not including bank if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 { continue } for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil && int32(item.Details.UniqueID) == uniqueID { return item } } } } // Check overflow items for _, item := range pil.overflowItems { if item != nil && int32(item.Details.UniqueID) == uniqueID { return item } } return nil } // GetItemFromID gets an item by its template ID func (pil *PlayerItemList) GetItemFromID(itemID int32, count int8, includeBank bool, lock bool) *Item { if lock { pil.mutex.RLock() defer pil.mutex.RUnlock() } for bagID, bagMap := range pil.items { // Skip bank slots if not including bank if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 { continue } for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil && item.Details.ItemID == itemID { if count == 0 || item.Details.Count >= int16(count) { return item } } } } } return nil } // GetAllStackCountItemFromID gets the total count of all stacks of an item func (pil *PlayerItemList) GetAllStackCountItemFromID(itemID int32, count int8, includeBank bool, lock bool) int32 { if lock { pil.mutex.RLock() defer pil.mutex.RUnlock() } totalCount := int32(0) for bagID, bagMap := range pil.items { // Skip bank slots if not including bank if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 { continue } for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil && item.Details.ItemID == itemID { totalCount += int32(item.Details.Count) } } } } return totalCount } // AssignItemToFreeSlot assigns an item to the first available free slot func (pil *PlayerItemList) AssignItemToFreeSlot(item *Item, inventoryOnly bool) bool { if item == nil { return false } pil.mutex.Lock() defer pil.mutex.Unlock() var bagID int32 var slot int16 if pil.getFirstFreeSlotInternal(&bagID, &slot, inventoryOnly) { item.Details.BagID = bagID item.Details.SlotID = slot item.Details.AppearanceType = BaseEquipment pil.addItemToLocationInternal(item, bagID, BaseEquipment, slot) return true } return false } // GetNumberOfFreeSlots returns the number of free inventory slots func (pil *PlayerItemList) GetNumberOfFreeSlots() int16 { pil.mutex.RLock() defer pil.mutex.RUnlock() freeSlots := int16(0) // Check main inventory slots for bagID := int32(0); bagID < NumInvSlots; bagID++ { bag := pil.GetBag(int8(bagID), false) if bag != nil && bag.BagInfo != nil { // Count free slots in this bag usedSlots := 0 if bagMap, exists := pil.items[bagID]; exists { for _, slotMap := range bagMap { usedSlots += len(slotMap) } } freeSlots += int16(bag.BagInfo.NumSlots) - int16(usedSlots) } } return freeSlots } // GetNumberOfItems returns the total number of items in inventory func (pil *PlayerItemList) GetNumberOfItems() int16 { pil.mutex.RLock() defer pil.mutex.RUnlock() itemCount := int16(0) for _, bagMap := range pil.items { for _, slotMap := range bagMap { itemCount += int16(len(slotMap)) } } return itemCount } // GetWeight returns the total weight of all items func (pil *PlayerItemList) GetWeight() int32 { pil.mutex.RLock() defer pil.mutex.RUnlock() totalWeight := int32(0) for _, bagMap := range pil.items { for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil { totalWeight += item.GenericInfo.Weight * int32(item.Details.Count) } } } } return totalWeight } // HasFreeSlot checks if there's at least one free slot func (pil *PlayerItemList) HasFreeSlot() bool { return pil.GetNumberOfFreeSlots() > 0 } // HasFreeBagSlot checks if there's a free bag slot in main inventory func (pil *PlayerItemList) HasFreeBagSlot() bool { pil.mutex.RLock() defer pil.mutex.RUnlock() // Check main inventory bag slots for bagSlot := int8(0); bagSlot < NumInvSlots; bagSlot++ { bag := pil.GetBag(bagSlot, false) if bag == nil { return true // Empty bag slot } } return false } // DestroyItem destroys an item by index func (pil *PlayerItemList) DestroyItem(index int16) { pil.mutex.Lock() defer pil.mutex.Unlock() // Find and remove item by index for _, bagMap := range pil.items { for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil && item.Details.NewIndex == index { pil.eraseItemInternal(item) return } } } } } // CanStack checks if an item can be stacked with existing items func (pil *PlayerItemList) CanStack(item *Item, includeBank bool) *Item { if item == nil { return nil } pil.mutex.RLock() defer pil.mutex.RUnlock() for bagID, bagMap := range pil.items { // Skip bank slots if not including bank if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 { continue } for _, slotMap := range bagMap { for _, existingItem := range slotMap { if existingItem != nil && existingItem.Details.ItemID == item.Details.ItemID && existingItem.Details.Count < existingItem.StackCount && existingItem.Details.UniqueID != item.Details.UniqueID { return existingItem } } } } return nil } // GetAllItemsFromID gets all items with a specific ID func (pil *PlayerItemList) GetAllItemsFromID(itemID int32, includeBank bool, lock bool) []*Item { if lock { pil.mutex.RLock() defer pil.mutex.RUnlock() } var matchingItems []*Item for bagID, bagMap := range pil.items { // Skip bank slots if not including bank if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 { continue } for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil && item.Details.ItemID == itemID { matchingItems = append(matchingItems, item) } } } } return matchingItems } // RemoveItem removes an item from inventory func (pil *PlayerItemList) RemoveItem(item *Item, deleteItem bool, lock bool) { if item == nil { return } if lock { pil.mutex.Lock() defer pil.mutex.Unlock() } pil.eraseItemInternal(item) if deleteItem { // Mark item for deletion item.NeedsDeletion = true } } // AddItem adds an item to the inventory func (pil *PlayerItemList) AddItem(item *Item) bool { if item == nil { return false } pil.mutex.Lock() defer pil.mutex.Unlock() // Try to stack with existing items first stackableItem := pil.CanStack(item, false) if stackableItem != nil { // Stack with existing item stackableItem.Details.Count += item.Details.Count if stackableItem.Details.Count > stackableItem.StackCount { // Handle overflow overflow := stackableItem.Details.Count - stackableItem.StackCount stackableItem.Details.Count = stackableItem.StackCount item.Details.Count = overflow // Continue to add the overflow as a new item } else { return true // Successfully stacked } } // Try to assign to free slot var bagID int32 var slot int16 if pil.getFirstFreeSlotInternal(&bagID, &slot, true) { item.Details.BagID = bagID item.Details.SlotID = slot item.Details.AppearanceType = BaseEquipment pil.addItemToLocationInternal(item, bagID, BaseEquipment, slot) return true } // Add to overflow if no free slots return pil.AddOverflowItem(item) } // GetItem gets an item from a specific location func (pil *PlayerItemList) GetItem(bagSlot int32, slot int16, appearanceType int8) *Item { pil.mutex.RLock() defer pil.mutex.RUnlock() if bagMap, exists := pil.items[bagSlot]; exists { if slotMap, exists := bagMap[appearanceType]; exists { if item, exists := slotMap[slot]; exists { return item } } } return nil } // GetAllItems returns all items in the inventory func (pil *PlayerItemList) GetAllItems() map[int32]*Item { pil.mutex.RLock() defer pil.mutex.RUnlock() // Return a copy of indexed items allItems := make(map[int32]*Item) for index, item := range pil.indexedItems { allItems[index] = item } return allItems } // HasFreeBankSlot checks if there's a free bank slot func (pil *PlayerItemList) HasFreeBankSlot() bool { pil.mutex.RLock() defer pil.mutex.RUnlock() // Check bank bag slots for bagSlot := int32(BankSlot1); bagSlot <= BankSlot8; bagSlot++ { if _, exists := pil.items[bagSlot]; !exists { return true } } return false } // FindFreeBankSlot finds the first free bank slot func (pil *PlayerItemList) FindFreeBankSlot() int8 { pil.mutex.RLock() defer pil.mutex.RUnlock() for bagSlot := int32(BankSlot1); bagSlot <= BankSlot8; bagSlot++ { if _, exists := pil.items[bagSlot]; !exists { return int8(bagSlot - BankSlot1) } } return -1 } // GetFirstFreeSlot gets the first free slot coordinates func (pil *PlayerItemList) GetFirstFreeSlot(bagID *int32, slot *int16) bool { pil.mutex.RLock() defer pil.mutex.RUnlock() return pil.getFirstFreeSlotInternal(bagID, slot, true) } // getFirstFreeSlotInternal gets the first free slot (assumes lock is held) func (pil *PlayerItemList) getFirstFreeSlotInternal(bagID *int32, slot *int16, inventoryOnly bool) bool { // Check main inventory bags first for bagSlotID := int32(0); bagSlotID < NumInvSlots; bagSlotID++ { bag := pil.GetBag(int8(bagSlotID), false) if bag != nil && bag.BagInfo != nil { // Check slots in this bag bagMap := pil.items[bagSlotID] if bagMap == nil { bagMap = make(map[int8]map[int16]*Item) pil.items[bagSlotID] = bagMap } slotMap := bagMap[BaseEquipment] if slotMap == nil { slotMap = make(map[int16]*Item) bagMap[BaseEquipment] = slotMap } for slotID := int16(0); slotID < int16(bag.BagInfo.NumSlots); slotID++ { if _, exists := slotMap[slotID]; !exists { *bagID = bagSlotID *slot = slotID return true } } } } // Check bank bags if not inventory only if !inventoryOnly { for bagSlotID := int32(BankSlot1); bagSlotID <= BankSlot8; bagSlotID++ { bag := pil.GetBankBag(int8(bagSlotID-BankSlot1), false) if bag != nil && bag.BagInfo != nil { bagMap := pil.items[bagSlotID] if bagMap == nil { bagMap = make(map[int8]map[int16]*Item) pil.items[bagSlotID] = bagMap } slotMap := bagMap[BaseEquipment] if slotMap == nil { slotMap = make(map[int16]*Item) bagMap[BaseEquipment] = slotMap } for slotID := int16(0); slotID < int16(bag.BagInfo.NumSlots); slotID++ { if _, exists := slotMap[slotID]; !exists { *bagID = bagSlotID *slot = slotID return true } } } } } return false } // GetFirstFreeBankSlot gets the first free bank slot coordinates func (pil *PlayerItemList) GetFirstFreeBankSlot(bagID *int32, slot *int16) bool { pil.mutex.RLock() defer pil.mutex.RUnlock() return pil.getFirstFreeSlotInternal(bagID, slot, false) } // GetBankBag gets a bank bag by slot func (pil *PlayerItemList) GetBankBag(inventorySlot int8, lock bool) *Item { if lock { pil.mutex.RLock() defer pil.mutex.RUnlock() } bagID := int32(BankSlot1) + int32(inventorySlot) if bagMap, exists := pil.items[bagID]; exists { if slotMap, exists := bagMap[0]; exists { if item, exists := slotMap[0]; exists && item != nil && item.IsBag() { return item } } } return nil } // AddOverflowItem adds an item to overflow storage func (pil *PlayerItemList) AddOverflowItem(item *Item) bool { if item == nil { return false } pil.mutex.Lock() defer pil.mutex.Unlock() pil.overflowItems = append(pil.overflowItems, item) return true } // GetOverflowItem gets the first overflow item func (pil *PlayerItemList) GetOverflowItem() *Item { pil.mutex.RLock() defer pil.mutex.RUnlock() if len(pil.overflowItems) > 0 { return pil.overflowItems[0] } return nil } // RemoveOverflowItem removes an item from overflow storage func (pil *PlayerItemList) RemoveOverflowItem(item *Item) { if item == nil { return } pil.mutex.Lock() defer pil.mutex.Unlock() for i, overflowItem := range pil.overflowItems { if overflowItem == item { pil.overflowItems = append(pil.overflowItems[:i], pil.overflowItems[i+1:]...) break } } } // GetOverflowItemList returns all overflow items func (pil *PlayerItemList) GetOverflowItemList() []*Item { pil.mutex.RLock() defer pil.mutex.RUnlock() // Return a copy of the overflow list overflowCopy := make([]*Item, len(pil.overflowItems)) copy(overflowCopy, pil.overflowItems) return overflowCopy } // ResetPackets resets packet data func (pil *PlayerItemList) ResetPackets() { pil.mutex.Lock() defer pil.mutex.Unlock() pil.xorPacket = nil pil.origPacket = nil pil.packetCount = 0 } // CheckSlotConflict checks for slot conflicts (lore items, etc.) func (pil *PlayerItemList) CheckSlotConflict(item *Item, checkLoreOnly bool, lockMutex bool, loreStackCount *int16) int32 { if item == nil { return 0 } if lockMutex { pil.mutex.RLock() defer pil.mutex.RUnlock() } // Check for lore conflicts if item.CheckFlag(Lore) || item.CheckFlag(LoreEquip) { stackCount := int16(0) for _, bagMap := range pil.items { for _, slotMap := range bagMap { for _, existingItem := range slotMap { if existingItem != nil && existingItem.Details.ItemID == item.Details.ItemID { stackCount++ } } } } if loreStackCount != nil { *loreStackCount = stackCount } if stackCount > 0 { return 1 // Lore conflict } } return 0 // No conflict } // GetItemCountInBag returns the number of items in a bag func (pil *PlayerItemList) GetItemCountInBag(bag *Item) int32 { if bag == nil || !bag.IsBag() { return 0 } pil.mutex.RLock() defer pil.mutex.RUnlock() count := int32(0) if bagMap, exists := pil.items[bag.Details.BagID]; exists { for _, slotMap := range bagMap { count += int32(len(slotMap)) } } return count } // GetFirstNewItem gets the index of the first new item func (pil *PlayerItemList) GetFirstNewItem() int16 { pil.mutex.RLock() defer pil.mutex.RUnlock() for _, bagMap := range pil.items { for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil && item.Details.NewItem { return item.Details.NewIndex } } } } return -1 } // GetNewItemByIndex gets a new item by its index func (pil *PlayerItemList) GetNewItemByIndex(index int16) int16 { pil.mutex.RLock() defer pil.mutex.RUnlock() for _, bagMap := range pil.items { for _, slotMap := range bagMap { for _, item := range slotMap { if item != nil && item.Details.NewItem && item.Details.NewIndex == index { return index } } } } return -1 } // addItemToLocationInternal adds an item to a specific location (assumes lock is held) func (pil *PlayerItemList) addItemToLocationInternal(item *Item, bagID int32, appearanceType int8, slot int16) { if item == nil { return } // Ensure bag map exists if pil.items[bagID] == nil { pil.items[bagID] = make(map[int8]map[int16]*Item) } // Ensure appearance type map exists if pil.items[bagID][appearanceType] == nil { pil.items[bagID][appearanceType] = make(map[int16]*Item) } // Add item to location pil.items[bagID][appearanceType][slot] = item // Add to indexed items if item.Details.Index > 0 { pil.indexedItems[int32(item.Details.Index)] = item } } // IsItemInSlotType checks if an item is in a specific slot type func (pil *PlayerItemList) IsItemInSlotType(item *Item, slotType InventorySlotType, lockItems bool) bool { if item == nil { return false } if lockItems { pil.mutex.RLock() defer pil.mutex.RUnlock() } bagID := item.Details.BagID switch slotType { case BaseInventory: return bagID >= 0 && bagID < NumInvSlots case Bank: return bagID >= BankSlot1 && bagID <= BankSlot8 case SharedBank: // TODO: Implement shared bank slot detection return false case Overflow: // Check if item is in overflow list for _, overflowItem := range pil.overflowItems { if overflowItem == item { return true } } return false } return false } // String returns a string representation of the player item list func (pil *PlayerItemList) String() string { pil.mutex.RLock() defer pil.mutex.RUnlock() return fmt.Sprintf("PlayerItemList{Items: %d, Overflow: %d, MaxIndex: %d}", len(pil.indexedItems), len(pil.overflowItems), pil.maxSavedIndex) } func init() { log.Printf("Player item list system initialized") }