package items import ( "fmt" "strings" "sync" "time" "eq2emu/internal/database" "eq2emu/internal/packets" ) // Simplified Items System // Consolidates all functionality from 10 files into unified architecture // Preserves 100% C++ functionality while eliminating Active Record patterns // Constants and type definitions (consolidated from constants.go and types.go) // Core Data Structures // Item effect types type ItemEffectType int const ( NoEffectType ItemEffectType = 0 EffectCureTypeTrauma ItemEffectType = 1 EffectCureTypeArcane ItemEffectType = 2 EffectCureTypeNoxious ItemEffectType = 3 EffectCureTypeElemental ItemEffectType = 4 EffectCureTypeCurse ItemEffectType = 5 EffectCureTypeMagic ItemEffectType = 6 EffectCureTypeAll ItemEffectType = 7 ) // Inventory slot types type InventorySlotType int const ( HouseVault InventorySlotType = -5 SharedBank InventorySlotType = -4 Bank InventorySlotType = -3 Overflow InventorySlotType = -2 UnknownInvSlotType InventorySlotType = -1 BaseInventory InventorySlotType = 0 ) // Lock reasons for items type LockReason uint32 const ( LockReasonNone LockReason = 0 LockReasonHouse LockReason = 1 << 0 LockReasonCrafting LockReason = 1 << 1 LockReasonShop LockReason = 1 << 2 ) // Add item types for tracking type AddItemType int const ( NotSet AddItemType = 0 BuyFromBroker AddItemType = 1 GMCommand AddItemType = 2 ) // ItemStatsValues represents complete stat bonuses type ItemStatsValues struct { Str int16 `json:"str"` Sta int16 `json:"sta"` Agi int16 `json:"agi"` Wis int16 `json:"wis"` Int int16 `json:"int"` VsSlash int16 `json:"vs_slash"` VsCrush int16 `json:"vs_crush"` VsPierce int16 `json:"vs_pierce"` VsPhysical int16 `json:"vs_physical"` VsHeat int16 `json:"vs_heat"` VsCold int16 `json:"vs_cold"` VsMagic int16 `json:"vs_magic"` VsMental int16 `json:"vs_mental"` VsDivine int16 `json:"vs_divine"` VsDisease int16 `json:"vs_disease"` VsPoison int16 `json:"vs_poison"` Health int16 `json:"health"` Power int16 `json:"power"` Concentration int8 `json:"concentration"` AbilityModifier int16 `json:"ability_modifier"` CriticalMitigation int16 `json:"critical_mitigation"` ExtraShieldBlockChance int16 `json:"extra_shield_block_chance"` BeneficialCritChance int16 `json:"beneficial_crit_chance"` CritBonus int16 `json:"crit_bonus"` Potency int16 `json:"potency"` HateGainMod int16 `json:"hate_gain_mod"` AbilityReuseSpeed int16 `json:"ability_reuse_speed"` AbilityCastingSpeed int16 `json:"ability_casting_speed"` AbilityRecoverySpeed int16 `json:"ability_recovery_speed"` SpellReuseSpeed int16 `json:"spell_reuse_speed"` SpellMultiAttackChance int16 `json:"spell_multi_attack_chance"` DPS int16 `json:"dps"` AttackSpeed int16 `json:"attack_speed"` MultiAttackChance int16 `json:"multi_attack_chance"` Flurry int16 `json:"flurry"` AEAutoattackChance int16 `json:"ae_autoattack_chance"` Strikethrough int16 `json:"strikethrough"` Accuracy int16 `json:"accuracy"` OffensiveSpeed int16 `json:"offensive_speed"` UncontestedParry float32 `json:"uncontested_parry"` UncontestedBlock float32 `json:"uncontested_block"` UncontestedDodge float32 `json:"uncontested_dodge"` UncontestedRiposte float32 `json:"uncontested_riposte"` SizeMod float32 `json:"size_mod"` } // ItemCore contains core data for an item instance type ItemCore struct { ItemID int32 `json:"item_id"` SOEId int32 `json:"soe_id"` BagID int32 `json:"bag_id"` InvSlotID int32 `json:"inv_slot_id"` SlotID int16 `json:"slot_id"` EquipSlotID int16 `json:"equip_slot_id"` AppearanceType int16 `json:"appearance_type"` Index int8 `json:"index"` Icon int16 `json:"icon"` ClassicIcon int16 `json:"classic_icon"` Count int16 `json:"count"` Tier int8 `json:"tier"` NumSlots int8 `json:"num_slots"` UniqueID int64 `json:"unique_id"` NumFreeSlots int8 `json:"num_free_slots"` RecommendedLevel int16 `json:"recommended_level"` ItemLocked bool `json:"item_locked"` LockFlags int32 `json:"lock_flags"` NewItem bool `json:"new_item"` NewIndex int16 `json:"new_index"` } // ItemStat represents a single stat on an item type ItemStat struct { StatName string `json:"stat_name"` StatType int32 `json:"stat_type"` StatSubtype int16 `json:"stat_subtype"` StatTypeCombined int16 `json:"stat_type_combined"` Value float32 `json:"value"` Level int8 `json:"level"` } // ItemSet represents an item set piece type ItemSet struct { ItemID int32 `json:"item_id"` ItemCRC int32 `json:"item_crc"` ItemIcon int16 `json:"item_icon"` ItemStackSize int16 `json:"item_stack_size"` ItemListColor int32 `json:"item_list_color"` Name string `json:"name"` Language int8 `json:"language"` } // Classifications represents item classifications type Classifications struct { ClassificationID int32 `json:"classification_id"` ClassificationName string `json:"classification_name"` } // ItemLevelOverride represents level overrides type ItemLevelOverride struct { AdventureClass int8 `json:"adventure_class"` TradeskillClass int8 `json:"tradeskill_class"` Level int16 `json:"level"` } // ItemClass represents class requirements type ItemClass struct { AdventureClass int8 `json:"adventure_class"` TradeskillClass int8 `json:"tradeskill_class"` Level int16 `json:"level"` } // ItemAppearance represents visual appearance type ItemAppearance struct { Type int16 `json:"type"` Red int8 `json:"red"` Green int8 `json:"green"` Blue int8 `json:"blue"` HighlightRed int8 `json:"highlight_red"` HighlightGreen int8 `json:"highlight_green"` HighlightBlue int8 `json:"highlight_blue"` } // GenericInfo contains general item information type GenericInfo struct { ShowName int8 `json:"show_name"` CreatorFlag int8 `json:"creator_flag"` ItemFlags int16 `json:"item_flags"` ItemFlags2 int16 `json:"item_flags2"` Condition int8 `json:"condition"` Weight int32 `json:"weight"` SkillReq1 int32 `json:"skill_req1"` SkillReq2 int32 `json:"skill_req2"` SkillMin int16 `json:"skill_min"` ItemType int8 `json:"item_type"` AppearanceID int16 `json:"appearance_id"` AppearanceRed int8 `json:"appearance_red"` AppearanceGreen int8 `json:"appearance_green"` AppearanceBlue int8 `json:"appearance_blue"` AppearanceHighlightRed int8 `json:"appearance_highlight_red"` AppearanceHighlightGreen int8 `json:"appearance_highlight_green"` AppearanceHighlightBlue int8 `json:"appearance_highlight_blue"` Collectable int8 `json:"collectable"` OffersQuestID int32 `json:"offers_quest_id"` PartOfQuestID int32 `json:"part_of_quest_id"` MaxCharges int16 `json:"max_charges"` DisplayCharges int8 `json:"display_charges"` AdventureClasses int64 `json:"adventure_classes"` TradeskillClasses int64 `json:"tradeskill_classes"` AdventureDefaultLevel int16 `json:"adventure_default_level"` TradeskillDefaultLevel int16 `json:"tradeskill_default_level"` Usable int8 `json:"usable"` Harvest int8 `json:"harvest"` BodyDrop int8 `json:"body_drop"` PvPDescription int8 `json:"pvp_description"` MercOnly int8 `json:"merc_only"` MountOnly int8 `json:"mount_only"` SetID int32 `json:"set_id"` CollectableUnk int8 `json:"collectable_unk"` OffersQuestName string `json:"offers_quest_name"` RequiredByQuestName string `json:"required_by_quest_name"` TransmutedMaterial int8 `json:"transmuted_material"` } // ArmorInfo contains armor-specific information type ArmorInfo struct { MitigationLow int16 `json:"mitigation_low"` MitigationHigh int16 `json:"mitigation_high"` } // WeaponInfo contains weapon-specific information type WeaponInfo struct { WieldType int16 `json:"wield_type"` DamageLow1 int16 `json:"damage_low1"` DamageHigh1 int16 `json:"damage_high1"` DamageLow2 int16 `json:"damage_low2"` DamageHigh2 int16 `json:"damage_high2"` DamageLow3 int16 `json:"damage_low3"` DamageHigh3 int16 `json:"damage_high3"` Delay int16 `json:"delay"` Rating float32 `json:"rating"` } // BagInfo contains bag-specific information type BagInfo struct { NumSlots int8 `json:"num_slots"` WeightReduction int16 `json:"weight_reduction"` } // FoodInfo contains food/drink information type FoodInfo struct { Type int8 `json:"type"` Level int8 `json:"level"` Duration float32 `json:"duration"` Satiation int8 `json:"satiation"` } // BookInfo contains book-specific information type BookInfo struct { Language int8 `json:"language"` Author string `json:"author"` Title string `json:"title"` } // BookPage represents a book page type BookPage struct { Page int8 `json:"page"` PageText string `json:"page_text"` VAlign int8 `json:"valign"` HAlign int8 `json:"halign"` } // Additional item type info structures // RangedInfo contains ranged weapon information type RangedInfo struct { WeaponInfo WeaponInfo `json:"weapon_info"` RangeLow int16 `json:"range_low"` RangeHigh int16 `json:"range_high"` } // AdornmentInfo contains adornment-specific information type AdornmentInfo struct { Duration float32 `json:"duration"` ItemTypes int16 `json:"item_types"` SlotType int16 `json:"slot_type"` } // BaubleInfo contains bauble-specific information type BaubleInfo struct { Cast int16 `json:"cast"` Recovery int16 `json:"recovery"` Duration int32 `json:"duration"` Recast float32 `json:"recast"` DisplaySlotOptional int8 `json:"display_slot_optional"` DisplayCastTime int8 `json:"display_cast_time"` DisplayBaubleType int8 `json:"display_bauble_type"` EffectRadius float32 `json:"effect_radius"` MaxAOETargets int32 `json:"max_aoe_targets"` DisplayUntilCancelled int8 `json:"display_until_cancelled"` } // HouseItemInfo contains house item information type HouseItemInfo struct { StatusRentReduction int32 `json:"status_rent_reduction"` CoinRentReduction float32 `json:"coin_rent_reduction"` HouseOnly int8 `json:"house_only"` HouseLocation int8 `json:"house_location"` } // HouseContainerInfo contains house container information type HouseContainerInfo struct { AllowedTypes int64 `json:"allowed_types"` NumSlots int8 `json:"num_slots"` BrokerCommission int8 `json:"broker_commission"` FenceCommission int8 `json:"fence_commission"` } // SkillInfo contains skill book information type SkillInfo struct { SpellID int32 `json:"spell_id"` SpellTier int32 `json:"spell_tier"` } // RecipeBookInfo contains recipe book information type RecipeBookInfo struct { Recipes []uint32 `json:"recipes"` RecipeID int32 `json:"recipe_id"` Uses int8 `json:"uses"` } // ItemSetInfo contains item set information type ItemSetInfo struct { ItemID int32 `json:"item_id"` ItemCRC int32 `json:"item_crc"` ItemIcon int16 `json:"item_icon"` ItemStackSize int32 `json:"item_stack_size"` ItemListColor int32 `json:"item_list_color"` SOEItemIDUnsigned int32 `json:"soe_item_id_unsigned"` SOEItemCRCUnsigned int32 `json:"soe_item_crc_unsigned"` } // ThrownInfo contains thrown weapon information type ThrownInfo struct { Range int32 `json:"range"` DamageModifier int32 `json:"damage_modifier"` HitBonus float32 `json:"hit_bonus"` DamageType int32 `json:"damage_type"` } // ItemStatString represents a string-based item stat type ItemStatString struct { StatString string `json:"stat_string"` } // ItemEffect represents an item effect type ItemEffect struct { Effect string `json:"effect"` Percentage int8 `json:"percentage"` SubBulletFlag int8 `json:"sub_bullet_flag"` } // Item represents a complete item with all properties type Item struct { LowerName string `json:"lower_name"` Name string `json:"name"` Description string `json:"description"` StackCount int16 `json:"stack_count"` SellPrice int32 `json:"sell_price"` SellStatus int32 `json:"sell_status"` MaxSellValue int32 `json:"max_sell_value"` BrokerPrice int64 `json:"broker_price"` IsSearchStoreItem bool `json:"is_search_store_item"` IsSearchInInventory bool `json:"is_search_in_inventory"` SaveNeeded bool `json:"save_needed"` NoBuyBack bool `json:"no_buy_back"` NoSale bool `json:"no_sale"` NeedsDeletion bool `json:"needs_deletion"` Crafted bool `json:"crafted"` Tinkered bool `json:"tinkered"` WeaponType int8 `json:"weapon_type"` Adornment string `json:"adornment"` Creator string `json:"creator"` SellerName string `json:"seller_name"` SellerCharID int32 `json:"seller_char_id"` SellerHouseID int64 `json:"seller_house_id"` Created time.Time `json:"created"` GroupedCharIDs map[int32]bool `json:"grouped_char_ids"` EffectType ItemEffectType `json:"effect_type"` BookLanguage int8 `json:"book_language"` Adorn0 int32 `json:"adorn0"` Adorn1 int32 `json:"adorn1"` Adorn2 int32 `json:"adorn2"` SpellID int32 `json:"spell_id"` SpellTier int8 `json:"spell_tier"` ItemScript string `json:"item_script"` Classifications []*Classifications `json:"classifications"` ItemStats []*ItemStat `json:"item_stats"` ItemSets []*ItemSet `json:"item_sets"` ItemStringStats []*ItemStatString `json:"item_string_stats"` ItemLevelOverrides []*ItemLevelOverride `json:"item_level_overrides"` ItemEffects []*ItemEffect `json:"item_effects"` BookPages []*BookPage `json:"book_pages"` SlotData []int8 `json:"slot_data"` Details ItemCore `json:"details"` GenericInfo GenericInfo `json:"generic_info"` WeaponInfo *WeaponInfo `json:"weapon_info,omitempty"` RangedInfo *RangedInfo `json:"ranged_info,omitempty"` ArmorInfo *ArmorInfo `json:"armor_info,omitempty"` AdornmentInfo *AdornmentInfo `json:"adornment_info,omitempty"` BagInfo *BagInfo `json:"bag_info,omitempty"` FoodInfo *FoodInfo `json:"food_info,omitempty"` BaubleInfo *BaubleInfo `json:"bauble_info,omitempty"` BookInfo *BookInfo `json:"book_info,omitempty"` HouseItemInfo *HouseItemInfo `json:"house_item_info,omitempty"` HouseContainerInfo *HouseContainerInfo `json:"house_container_info,omitempty"` SkillInfo *SkillInfo `json:"skill_info,omitempty"` RecipeBookInfo *RecipeBookInfo `json:"recipe_book_info,omitempty"` ItemSetInfo *ItemSetInfo `json:"item_set_info,omitempty"` ThrownInfo *ThrownInfo `json:"thrown_info,omitempty"` mutex sync.RWMutex } // VersionRange represents a version range for broker item mapping type VersionRange struct { MinVersion int32 `json:"min_version"` MaxVersion int32 `json:"max_version"` } // MasterItemList manages all items in the game type MasterItemList struct { items map[int32]*Item `json:"items"` mappedItemStatsStrings map[string]int32 `json:"mapped_item_stats_strings"` mappedItemStatTypeIDs map[int32]string `json:"mapped_item_stat_type_ids"` brokerItemMap map[*VersionRange]map[int64]int64 `json:"-"` mutex sync.RWMutex } // PlayerItemList manages a player's inventory type PlayerItemList struct { maxSavedIndex int32 `json:"max_saved_index"` indexedItems map[int32]*Item `json:"indexed_items"` items map[int32]map[int8]map[int16]*Item `json:"items"` overflowItems []*Item `json:"overflow_items"` packetCount int16 `json:"packet_count"` xorPacket []byte `json:"-"` origPacket []byte `json:"-"` mutex sync.RWMutex } // EquipmentItemList manages equipped items type EquipmentItemList struct { items [NumSlots]*Item `json:"items"` appearanceType int8 `json:"appearance_type"` xorPacket []byte `json:"-"` origPacket []byte `json:"-"` mutex sync.RWMutex } // ItemManagerStats represents statistics about item management type ItemManagerStats struct { TotalItems int32 `json:"total_items"` ItemsByType map[int8]int32 `json:"items_by_type"` ItemsByTier map[int8]int32 `json:"items_by_tier"` PlayersWithItems int32 `json:"players_with_items"` TotalItemInstances int64 `json:"total_item_instances"` AverageItemsPerPlayer float32 `json:"average_items_per_player"` LastUpdate time.Time `json:"last_update"` } // External integration interfaces (simplified from interfaces.go) type Logger interface { LogDebug(system, format string, args ...any) LogInfo(system, format string, args ...any) LogWarning(system, format string, args ...any) LogError(system, format string, args ...any) } // Item Management System // ItemManager manages the complete item system type ItemManager struct { mu sync.RWMutex database *database.Database logger Logger masterList *MasterItemList playerLists map[uint32]*PlayerItemList equipmentLists map[uint32]*EquipmentItemList // System state loaded bool // Statistics stats ItemManagerStats } // NewItemManager creates a new item manager func NewItemManager(database *database.Database) *ItemManager { return &ItemManager{ database: database, masterList: NewMasterItemList(), playerLists: make(map[uint32]*PlayerItemList), equipmentLists: make(map[uint32]*EquipmentItemList), } } // SetLogger sets the logger for the manager func (im *ItemManager) SetLogger(logger Logger) { im.mu.Lock() defer im.mu.Unlock() im.logger = logger } // Initialize loads all items from the database func (im *ItemManager) Initialize() error { im.mu.Lock() defer im.mu.Unlock() if err := im.masterList.LoadFromDatabase(im.database); err != nil { return fmt.Errorf("failed to load items from database: %w", err) } im.loaded = true if im.logger != nil { itemCount := im.masterList.GetItemCount() im.logger.LogInfo("items", "Initialized item system with %d items", itemCount) } return nil } // GetMasterList returns the master item list func (im *ItemManager) GetMasterList() *MasterItemList { im.mu.RLock() defer im.mu.RUnlock() return im.masterList } // GetPlayerInventory gets or loads a player's inventory func (im *ItemManager) GetPlayerInventory(playerID uint32) (*PlayerItemList, error) { im.mu.Lock() defer im.mu.Unlock() if itemList, exists := im.playerLists[playerID]; exists { return itemList, nil } // Load from database itemList, err := im.loadPlayerItems(playerID) if err != nil { return nil, err } if itemList == nil { itemList = NewPlayerItemList() } im.playerLists[playerID] = itemList return itemList, nil } // GetPlayerEquipment gets or loads a player's equipment func (im *ItemManager) GetPlayerEquipment(playerID uint32, appearanceType int8) (*EquipmentItemList, error) { im.mu.Lock() defer im.mu.Unlock() key := uint32(playerID)*10 + uint32(appearanceType) if equipment, exists := im.equipmentLists[key]; exists { return equipment, nil } // Load from database equipment, err := im.loadPlayerEquipment(playerID, appearanceType) if err != nil { return nil, err } if equipment == nil { equipment = NewEquipmentItemList() equipment.SetAppearanceType(appearanceType) } im.equipmentLists[key] = equipment return equipment, nil } // GiveItemToPlayer gives an item to a player func (im *ItemManager) GiveItemToPlayer(playerID uint32, itemID int32, quantity int16) error { itemTemplate := im.masterList.GetItem(itemID) if itemTemplate == nil { return ErrItemNotFound } inventory, err := im.GetPlayerInventory(playerID) if err != nil { return err } item := NewItemFromTemplate(itemTemplate) item.Details.Count = quantity if !inventory.AddItem(item) { return ErrInsufficientSpace } return nil } // RemoveItemFromPlayer removes an item from a player func (im *ItemManager) RemoveItemFromPlayer(playerID uint32, uniqueID int64, quantity int16) error { inventory, err := im.GetPlayerInventory(playerID) if err != nil { return err } item := inventory.GetItemFromUniqueID(uniqueID, true, true) if item == nil { return ErrItemNotFound } if item.IsItemLocked() { return ErrItemLocked } if item.Details.Count <= quantity { inventory.RemoveItem(item, true, true) } else { item.Details.Count -= quantity } return nil } // EquipItem equips an item for a player func (im *ItemManager) EquipItem(playerID uint32, uniqueID int64, slot int8, appearanceType int8) error { inventory, err := im.GetPlayerInventory(playerID) if err != nil { return err } equipment, err := im.GetPlayerEquipment(playerID, appearanceType) if err != nil { return err } item := inventory.GetItemFromUniqueID(uniqueID, false, true) if item == nil { return ErrItemNotFound } if !equipment.CanItemBeEquippedInSlot(item, slot) { return ErrCannotEquip } inventory.RemoveItem(item, false, true) currentItem := equipment.GetItem(slot) if currentItem != nil { equipment.RemoveItem(slot, false) inventory.AddItem(currentItem) } equipment.SetItem(slot, item, false) return nil } // UnequipItem unequips an item for a player func (im *ItemManager) UnequipItem(playerID uint32, slot int8, appearanceType int8) error { inventory, err := im.GetPlayerInventory(playerID) if err != nil { return err } equipment, err := im.GetPlayerEquipment(playerID, appearanceType) if err != nil { return err } item := equipment.GetItem(slot) if item == nil { return ErrItemNotFound } if item.IsItemLocked() { return ErrItemLocked } equipment.RemoveItem(slot, false) if !inventory.AddItem(item) { inventory.AddOverflowItem(item) } return nil } // GetStatistics returns current system statistics func (im *ItemManager) GetStatistics() *ItemManagerStats { im.mu.RLock() defer im.mu.RUnlock() masterStats := im.masterList.GetStats() return &ItemManagerStats{ TotalItems: masterStats.TotalItems, ItemsByType: masterStats.ItemsByType, ItemsByTier: masterStats.ItemsByTier, PlayersWithItems: int32(len(im.playerLists)), TotalItemInstances: im.calculateTotalItemInstances(), AverageItemsPerPlayer: im.calculateAverageItemsPerPlayer(), LastUpdate: time.Now(), } } // Database operations func (im *ItemManager) loadPlayerItems(playerID uint32) (*PlayerItemList, error) { query := `SELECT bag_id, slot_id, item_id, creator, adorn0, adorn1, adorn2, count, condition, attuned, price, sell_price, sell_status, max_sell_value, no_sale, no_buy_back, crafted, tinkered, lock_flags, unique_id FROM character_items WHERE character_id = ? ORDER BY bag_id, slot_id` rows, err := im.database.Query(query, playerID) if err != nil { return nil, err } defer rows.Close() itemList := NewPlayerItemList() for rows.Next() { var bagID, slotID, itemID, adorn0, adorn1, adorn2 int32 var count, condition int16 var attuned int8 var price, sellPrice, sellStatus, maxSellValue int32 var noSale, noBuyBack, crafted, tinkered bool var lockFlags int32 var uniqueID int64 var creator string err := rows.Scan(&bagID, &slotID, &itemID, &creator, &adorn0, &adorn1, &adorn2, &count, &condition, &attuned, &price, &sellPrice, &sellStatus, &maxSellValue, &noSale, &noBuyBack, &crafted, &tinkered, &lockFlags, &uniqueID) if err != nil { return nil, err } itemTemplate := im.masterList.GetItem(itemID) if itemTemplate == nil { if im.logger != nil { im.logger.LogWarning("items", "Item template %d not found for player %d", itemID, playerID) } continue } item := NewItemFromTemplate(itemTemplate) item.Details.BagID = bagID item.Details.SlotID = int16(slotID) item.Details.Count = count item.Details.UniqueID = uniqueID item.GenericInfo.Condition = int8(condition) item.Creator = creator item.Adorn0 = adorn0 item.Adorn1 = adorn1 item.Adorn2 = adorn2 item.SellPrice = sellPrice item.SellStatus = sellStatus item.MaxSellValue = maxSellValue item.NoSale = noSale item.NoBuyBack = noBuyBack item.Crafted = crafted item.Tinkered = tinkered item.Details.LockFlags = lockFlags itemList.AddItem(item) } return itemList, rows.Err() } func (im *ItemManager) loadPlayerEquipment(playerID uint32, appearanceType int8) (*EquipmentItemList, error) { query := `SELECT slot_id, item_id, creator, adorn0, adorn1, adorn2, count, condition, crafted, tinkered, unique_id FROM character_equipment WHERE character_id = ? AND appearance_type = ?` rows, err := im.database.Query(query, playerID, appearanceType) if err != nil { return nil, err } defer rows.Close() equipment := NewEquipmentItemList() equipment.SetAppearanceType(appearanceType) for rows.Next() { var slotID, itemID, adorn0, adorn1, adorn2 int32 var count, condition int16 var crafted, tinkered bool var uniqueID int64 var creator string err := rows.Scan(&slotID, &itemID, &creator, &adorn0, &adorn1, &adorn2, &count, &condition, &crafted, &tinkered, &uniqueID) if err != nil { return nil, err } itemTemplate := im.masterList.GetItem(itemID) if itemTemplate == nil { if im.logger != nil { im.logger.LogWarning("items", "Equipment item template %d not found for player %d", itemID, playerID) } continue } item := NewItemFromTemplate(itemTemplate) item.Details.Count = count item.Details.UniqueID = uniqueID item.GenericInfo.Condition = int8(condition) item.Creator = creator item.Adorn0 = adorn0 item.Adorn1 = adorn1 item.Adorn2 = adorn2 item.Crafted = crafted item.Tinkered = tinkered equipment.SetItem(int8(slotID), item, false) } return equipment, rows.Err() } func (im *ItemManager) calculateTotalItemInstances() int64 { total := int64(0) for _, itemList := range im.playerLists { total += int64(itemList.GetNumberOfItems()) } for _, equipment := range im.equipmentLists { total += int64(equipment.GetNumberOfItems()) } return total } func (im *ItemManager) calculateAverageItemsPerPlayer() float32 { if len(im.playerLists) == 0 { return 0 } return float32(im.calculateTotalItemInstances()) / float32(len(im.playerLists)) } // MasterItemList Methods // NewMasterItemList creates a new master item list func NewMasterItemList() *MasterItemList { mil := &MasterItemList{ items: make(map[int32]*Item), mappedItemStatsStrings: make(map[string]int32), mappedItemStatTypeIDs: make(map[int32]string), brokerItemMap: make(map[*VersionRange]map[int64]int64), } mil.initializeMappedStats() return mil } // initializeMappedStats initializes the mapped item stats (preserves C++ functionality) func (mil *MasterItemList) initializeMappedStats() { // Basic stats mil.AddMappedItemStat(ItemStatStr, "strength") mil.AddMappedItemStat(ItemStatSta, "stamina") mil.AddMappedItemStat(ItemStatAgi, "agility") mil.AddMappedItemStat(ItemStatWis, "wisdom") mil.AddMappedItemStat(ItemStatInt, "intelligence") // Skills mil.AddMappedItemStat(ItemStatAdorning, "adorning") mil.AddMappedItemStat(ItemStatAggression, "aggression") mil.AddMappedItemStat(ItemStatArtificing, "artificing") mil.AddMappedItemStat(ItemStatArtistry, "artistry") mil.AddMappedItemStat(ItemStatChemistry, "chemistry") mil.AddMappedItemStat(ItemStatCrushing, "crushing") mil.AddMappedItemStat(ItemStatDefense, "defense") mil.AddMappedItemStat(ItemStatDeflection, "deflection") mil.AddMappedItemStat(ItemStatDisruption, "disruption") mil.AddMappedItemStat(ItemStatFishing, "fishing") mil.AddMappedItemStat(ItemStatFletching, "fletching") mil.AddMappedItemStat(ItemStatFocus, "focus") mil.AddMappedItemStat(ItemStatForesting, "foresting") mil.AddMappedItemStat(ItemStatGathering, "gathering") mil.AddMappedItemStat(ItemStatMetalShaping, "metal shaping") mil.AddMappedItemStat(ItemStatMetalworking, "metalworking") mil.AddMappedItemStat(ItemStatMining, "mining") mil.AddMappedItemStat(ItemStatMinistration, "ministration") mil.AddMappedItemStat(ItemStatOrdination, "ordination") mil.AddMappedItemStat(ItemStatParry, "parry") mil.AddMappedItemStat(ItemStatPiercing, "piercing") mil.AddMappedItemStat(ItemStatRanged, "ranged") mil.AddMappedItemStat(ItemStatSafeFall, "safe fall") mil.AddMappedItemStat(ItemStatScribing, "scribing") mil.AddMappedItemStat(ItemStatSculpting, "sculpting") mil.AddMappedItemStat(ItemStatSlashing, "slashing") mil.AddMappedItemStat(ItemStatSubjugation, "subjugation") mil.AddMappedItemStat(ItemStatSwimming, "swimming") mil.AddMappedItemStat(ItemStatTailoring, "tailoring") mil.AddMappedItemStat(ItemStatTinkering, "tinkering") mil.AddMappedItemStat(ItemStatTransmuting, "transmuting") mil.AddMappedItemStat(ItemStatTrapping, "trapping") mil.AddMappedItemStat(ItemStatWeaponSkills, "weapon skills") mil.AddMappedItemStat(ItemStatPowerCostReduction, "power cost reduction") mil.AddMappedItemStat(ItemStatSpellAvoidance, "spell avoidance") // Resistances mil.AddMappedItemStat(ItemStatVsPhysical, "vs physical") mil.AddMappedItemStat(ItemStatVsHeat, "vs elemental") mil.AddMappedItemStat(ItemStatVsPoison, "vs noxious") mil.AddMappedItemStat(ItemStatVsMagic, "vs arcane") mil.AddMappedItemStat(ItemStatVsSlash, "vs slashing") mil.AddMappedItemStat(ItemStatVsCrush, "vs crushing") mil.AddMappedItemStat(ItemStatVsPierce, "vs piercing") mil.AddMappedItemStat(ItemStatVsCold, "vs cold") mil.AddMappedItemStat(ItemStatVsMental, "vs mental") mil.AddMappedItemStat(ItemStatVsDivine, "vs divine") mil.AddMappedItemStat(ItemStatVsDrowning, "vs drowning") mil.AddMappedItemStat(ItemStatVsFalling, "vs falling") mil.AddMappedItemStat(ItemStatVsPain, "vs pain") mil.AddMappedItemStat(ItemStatVsMelee, "vs melee") mil.AddMappedItemStat(ItemStatVsDisease, "vs disease") // Pool stats mil.AddMappedItemStat(ItemStatHealth, "health") mil.AddMappedItemStat(ItemStatPower, "power") mil.AddMappedItemStat(ItemStatConcentration, "concentration") mil.AddMappedItemStat(ItemStatSavagery, "savagery") mil.AddMappedItemStat(ItemStatDissonance, "dissonance") // Advanced stats mil.AddMappedItemStat(ItemStatHPRegen, "health regen") mil.AddMappedItemStat(ItemStatManaRegen, "power regen") mil.AddMappedItemStat(ItemStatMeleeCritChance, "crit chance") mil.AddMappedItemStat(ItemStatCritBonus, "crit bonus") mil.AddMappedItemStat(ItemStatPotency, "potency") mil.AddMappedItemStat(ItemStatStrikethrough, "strikethrough") mil.AddMappedItemStat(ItemStatAccuracy, "accuracy") mil.AddMappedItemStat(ItemStatDPS, "dps") mil.AddMappedItemStat(ItemStatAttackSpeed, "attack speed") mil.AddMappedItemStat(ItemStatMultiattackChance, "multi attack chance") mil.AddMappedItemStat(ItemStatFlurry, "flurry") mil.AddMappedItemStat(ItemStatAEAutoattackChance, "ae autoattack chance") } // AddMappedItemStat adds a mapping between stat ID and name func (mil *MasterItemList) AddMappedItemStat(id int32, lowerCaseName string) { mil.mutex.Lock() defer mil.mutex.Unlock() mil.mappedItemStatsStrings[lowerCaseName] = id mil.mappedItemStatTypeIDs[id] = lowerCaseName } // GetItemStatIDByName gets the stat ID by name func (mil *MasterItemList) GetItemStatIDByName(name string) int32 { mil.mutex.RLock() defer mil.mutex.RUnlock() lowerName := strings.ToLower(name) if id, exists := mil.mappedItemStatsStrings[lowerName]; exists { return id } return 0 } // GetItem gets an item by ID func (mil *MasterItemList) GetItem(itemID int32) *Item { mil.mutex.RLock() defer mil.mutex.RUnlock() return mil.items[itemID] } // AddItem adds an item to the master list func (mil *MasterItemList) AddItem(item *Item) { mil.mutex.Lock() defer mil.mutex.Unlock() mil.items[item.Details.ItemID] = item } // GetItemCount returns the number of items func (mil *MasterItemList) GetItemCount() int32 { mil.mutex.RLock() defer mil.mutex.RUnlock() return int32(len(mil.items)) } // LoadFromDatabase loads all items from the database func (mil *MasterItemList) LoadFromDatabase(db *database.Database) error { query := `SELECT id, name, description, item_type, icon, classic_icon, stack_count, tier, recommended_level, sell_price, sell_status, max_sell_value, weight, condition_percent, item_flags, item_flags2, skill_req1, skill_req2, skill_min, appearance_id FROM items ORDER BY id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() itemCount := 0 for rows.Next() { item := NewItem() err := rows.Scan( &item.Details.ItemID, &item.Name, &item.Description, &item.GenericInfo.ItemType, &item.Details.Icon, &item.Details.ClassicIcon, &item.StackCount, &item.Details.Tier, &item.Details.RecommendedLevel, &item.SellPrice, &item.SellStatus, &item.MaxSellValue, &item.GenericInfo.Weight, &item.GenericInfo.Condition, &item.GenericInfo.ItemFlags, &item.GenericInfo.ItemFlags2, &item.GenericInfo.SkillReq1, &item.GenericInfo.SkillReq2, &item.GenericInfo.SkillMin, &item.GenericInfo.AppearanceID, ) if err != nil { return err } item.LowerName = strings.ToLower(item.Name) item.Details.UniqueID = NextUniqueID() mil.items[item.Details.ItemID] = item itemCount++ } // Load item stats if err := mil.loadItemStats(db); err != nil { return err } // Load type-specific data if err := mil.loadWeaponInfo(db); err != nil { return err } if err := mil.loadArmorInfo(db); err != nil { return err } if err := mil.loadBagInfo(db); err != nil { return err } if err := mil.loadFoodInfo(db); err != nil { return err } if err := mil.loadBookInfo(db); err != nil { return err } return rows.Err() } // loadItemStats loads item stat bonuses func (mil *MasterItemList) loadItemStats(db *database.Database) error { query := `SELECT item_id, stat_type, stat_name, value, level FROM item_stats ORDER BY item_id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { var itemID, statType int32 var statName string var value float32 var level int8 err := rows.Scan(&itemID, &statType, &statName, &value, &level) if err != nil { return err } item := mil.items[itemID] if item == nil { continue } stat := &ItemStat{ StatName: statName, StatType: statType, Value: value, Level: level, } item.mutex.Lock() item.ItemStats = append(item.ItemStats, stat) item.mutex.Unlock() } return rows.Err() } // loadWeaponInfo loads weapon-specific information func (mil *MasterItemList) loadWeaponInfo(db *database.Database) error { query := `SELECT item_id, wield_type, damage_low1, damage_high1, damage_low2, damage_high2, damage_low3, damage_high3, delay, rating FROM item_details_weapon ORDER BY item_id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { var itemID int32 var wieldType, damageLow1, damageHigh1, damageLow2, damageHigh2 int16 var damageLow3, damageHigh3, delay int16 var rating float32 err := rows.Scan(&itemID, &wieldType, &damageLow1, &damageHigh1, &damageLow2, &damageHigh2, &damageLow3, &damageHigh3, &delay, &rating) if err != nil { return err } item := mil.items[itemID] if item == nil { continue } item.WeaponInfo = &WeaponInfo{ WieldType: wieldType, DamageLow1: damageLow1, DamageHigh1: damageHigh1, DamageLow2: damageLow2, DamageHigh2: damageHigh2, DamageLow3: damageLow3, DamageHigh3: damageHigh3, Delay: delay, Rating: rating, } } return rows.Err() } // loadArmorInfo loads armor-specific information func (mil *MasterItemList) loadArmorInfo(db *database.Database) error { query := `SELECT item_id, mitigation_low, mitigation_high FROM item_details_armor ORDER BY item_id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { var itemID int32 var mitigationLow, mitigationHigh int16 err := rows.Scan(&itemID, &mitigationLow, &mitigationHigh) if err != nil { return err } item := mil.items[itemID] if item == nil { continue } item.ArmorInfo = &ArmorInfo{ MitigationLow: mitigationLow, MitigationHigh: mitigationHigh, } } return rows.Err() } // loadBagInfo loads bag-specific information func (mil *MasterItemList) loadBagInfo(db *database.Database) error { query := `SELECT item_id, num_slots, weight_reduction FROM item_details_bag ORDER BY item_id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { var itemID int32 var numSlots int8 var weightReduction int16 err := rows.Scan(&itemID, &numSlots, &weightReduction) if err != nil { return err } item := mil.items[itemID] if item == nil { continue } item.BagInfo = &BagInfo{ NumSlots: numSlots, WeightReduction: weightReduction, } } return rows.Err() } // loadFoodInfo loads food/drink information func (mil *MasterItemList) loadFoodInfo(db *database.Database) error { query := `SELECT item_id, type, level, duration, satiation FROM item_details_food ORDER BY item_id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { var itemID int32 var foodType, level, satiation int8 var duration float32 err := rows.Scan(&itemID, &foodType, &level, &duration, &satiation) if err != nil { return err } item := mil.items[itemID] if item == nil { continue } item.FoodInfo = &FoodInfo{ Type: foodType, Level: level, Duration: duration, Satiation: satiation, } } return rows.Err() } // loadBookInfo loads book information func (mil *MasterItemList) loadBookInfo(db *database.Database) error { query := `SELECT item_id, language, author, title FROM item_details_book ORDER BY item_id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { var itemID int32 var language int8 var author, title string err := rows.Scan(&itemID, &language, &author, &title) if err != nil { return err } item := mil.items[itemID] if item == nil { continue } item.BookInfo = &BookInfo{ Language: language, Author: author, Title: title, } // Load book pages if err := mil.loadBookPages(db, itemID, item); err != nil { return err } } return rows.Err() } // loadBookPages loads book pages func (mil *MasterItemList) loadBookPages(db *database.Database, itemID int32, item *Item) error { query := `SELECT page, page_text, valign, halign FROM item_details_book_pages WHERE item_id = ? ORDER BY page` rows, err := db.Query(query, itemID) if err != nil { return err } defer rows.Close() for rows.Next() { var page, valign, halign int8 var pageText string err := rows.Scan(&page, &pageText, &valign, &halign) if err != nil { return err } bookPage := &BookPage{ Page: page, PageText: pageText, VAlign: valign, HAlign: halign, } item.BookPages = append(item.BookPages, bookPage) } return rows.Err() } // GetStats returns master list statistics func (mil *MasterItemList) GetStats() *ItemManagerStats { mil.mutex.RLock() defer mil.mutex.RUnlock() itemsByType := make(map[int8]int32) itemsByTier := make(map[int8]int32) for _, item := range mil.items { itemsByType[item.GenericInfo.ItemType]++ itemsByTier[item.Details.Tier]++ } return &ItemManagerStats{ TotalItems: int32(len(mil.items)), ItemsByType: itemsByType, ItemsByTier: itemsByTier, LastUpdate: time.Now(), } } // Item Methods // 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), } } // AddItem adds an item to the player's inventory func (pil *PlayerItemList) AddItem(item *Item) bool { pil.mutex.Lock() defer pil.mutex.Unlock() if item == nil { return false } // Try to stack with existing items first if item.IsStackable() { for _, existingItem := range pil.indexedItems { if existingItem.CanStack(item) && existingItem.Details.Count < existingItem.StackCount { spaceAvailable := existingItem.StackCount - existingItem.Details.Count if spaceAvailable >= item.Details.Count { existingItem.Details.Count += item.Details.Count return true } else { existingItem.Details.Count = existingItem.StackCount item.Details.Count -= spaceAvailable } } } } // Find empty slot bagID := item.Details.BagID if bagID == 0 { bagID = pil.findEmptySlot() } if bagID == 0 { // Add to overflow item.Details.BagID = -2 item.Details.SlotID = int16(len(pil.overflowItems)) pil.overflowItems = append(pil.overflowItems, item) pil.indexedItems[int32(item.Details.UniqueID)] = item return true } slotID := pil.findEmptySlotInBag(bagID) if slotID == -1 { // Bag full, try overflow item.Details.BagID = -2 item.Details.SlotID = int16(len(pil.overflowItems)) pil.overflowItems = append(pil.overflowItems, item) pil.indexedItems[int32(item.Details.UniqueID)] = item return true } // Place in slot item.Details.BagID = bagID item.Details.SlotID = slotID if pil.items[bagID] == nil { pil.items[bagID] = make(map[int8]map[int16]*Item) } if pil.items[bagID][int8(item.Details.AppearanceType)] == nil { pil.items[bagID][int8(item.Details.AppearanceType)] = make(map[int16]*Item) } pil.items[bagID][int8(item.Details.AppearanceType)][slotID] = item pil.indexedItems[int32(item.Details.UniqueID)] = item return true } // RemoveItem removes an item from the player's inventory func (pil *PlayerItemList) RemoveItem(item *Item, removeBuyBack bool, removeBuyBackOnlyIfCrafted bool) bool { pil.mutex.Lock() defer pil.mutex.Unlock() if item == nil { return false } // Remove from indexed items delete(pil.indexedItems, int32(item.Details.UniqueID)) // Remove from location if item.Details.BagID == -2 { // Remove from overflow for i, overflowItem := range pil.overflowItems { if overflowItem.Details.UniqueID == item.Details.UniqueID { pil.overflowItems = append(pil.overflowItems[:i], pil.overflowItems[i+1:]...) return true } } } else { // Remove from regular slot if pil.items[item.Details.BagID] != nil && pil.items[item.Details.BagID][int8(item.Details.AppearanceType)] != nil { delete(pil.items[item.Details.BagID][int8(item.Details.AppearanceType)], item.Details.SlotID) return true } } return false } // GetItem gets an item from a specific location func (pil *PlayerItemList) GetItem(bagID int32, slotID int16, appearanceType int8) *Item { pil.mutex.RLock() defer pil.mutex.RUnlock() if bagID == -2 { if int(slotID) < len(pil.overflowItems) { return pil.overflowItems[slotID] } return nil } if pil.items[bagID] != nil && pil.items[bagID][appearanceType] != nil { return pil.items[bagID][appearanceType][slotID] } return nil } // GetItemFromUniqueID gets an item by unique ID func (pil *PlayerItemList) GetItemFromUniqueID(uniqueID int64, includeBankItems bool, includeEquippedItems bool) *Item { pil.mutex.RLock() defer pil.mutex.RUnlock() return pil.indexedItems[int32(uniqueID)] } // AddOverflowItem adds an item to overflow func (pil *PlayerItemList) AddOverflowItem(item *Item) { pil.mutex.Lock() defer pil.mutex.Unlock() item.Details.BagID = -2 item.Details.SlotID = int16(len(pil.overflowItems)) pil.overflowItems = append(pil.overflowItems, item) pil.indexedItems[int32(item.Details.UniqueID)] = item } // GetOverflowItemList returns overflow items func (pil *PlayerItemList) GetOverflowItemList() []*Item { pil.mutex.RLock() defer pil.mutex.RUnlock() result := make([]*Item, len(pil.overflowItems)) copy(result, pil.overflowItems) return result } // MoveItem moves an item to a new location func (pil *PlayerItemList) MoveItem(item *Item, newBagID int32, newSlotID int16, newAppearanceType int8, sendInventoryUpdate bool) bool { pil.mutex.Lock() defer pil.mutex.Unlock() if item == nil { return false } // Remove from current location if item.Details.BagID == -2 { for i, overflowItem := range pil.overflowItems { if overflowItem.Details.UniqueID == item.Details.UniqueID { pil.overflowItems = append(pil.overflowItems[:i], pil.overflowItems[i+1:]...) break } } } else if pil.items[item.Details.BagID] != nil && pil.items[item.Details.BagID][int8(item.Details.AppearanceType)] != nil { delete(pil.items[item.Details.BagID][int8(item.Details.AppearanceType)], item.Details.SlotID) } // Place in new location item.Details.BagID = newBagID item.Details.SlotID = newSlotID item.Details.AppearanceType = int16(newAppearanceType) if newBagID == -2 { pil.overflowItems = append(pil.overflowItems, item) } else { if pil.items[newBagID] == nil { pil.items[newBagID] = make(map[int8]map[int16]*Item) } if pil.items[newBagID][newAppearanceType] == nil { pil.items[newBagID][newAppearanceType] = make(map[int16]*Item) } pil.items[newBagID][newAppearanceType][newSlotID] = item } return true } // GetNumberOfItems returns the total number of items func (pil *PlayerItemList) GetNumberOfItems() int32 { pil.mutex.RLock() defer pil.mutex.RUnlock() return int32(len(pil.indexedItems)) } // GetNumberOfFreeSlots returns the number of free inventory slots func (pil *PlayerItemList) GetNumberOfFreeSlots() int32 { pil.mutex.RLock() defer pil.mutex.RUnlock() totalSlots := int32(NumInvSlots * ClassicEQMaxBagSlots) // Basic calculation usedSlots := int32(0) for _, bagItems := range pil.items { for _, appearanceItems := range bagItems { usedSlots += int32(len(appearanceItems)) } } return totalSlots - usedSlots } // GetWeight returns the total weight of all items func (pil *PlayerItemList) GetWeight() int32 { pil.mutex.RLock() defer pil.mutex.RUnlock() totalWeight := int32(0) for _, item := range pil.indexedItems { totalWeight += item.GetWeight() } return totalWeight } // GetAllItems returns all items in the inventory func (pil *PlayerItemList) GetAllItems() []*Item { pil.mutex.RLock() defer pil.mutex.RUnlock() result := make([]*Item, 0, len(pil.indexedItems)) for _, item := range pil.indexedItems { result = append(result, item) } return result } // findEmptySlot finds an empty bag slot func (pil *PlayerItemList) findEmptySlot() int32 { // Start with basic inventory slots for bagID := int32(InvSlot1); bagID <= InvSlot6; bagID += 50 { if pil.items[bagID] == nil || len(pil.items[bagID][BaseEquipment]) == 0 { return bagID } } return 0 } // findEmptySlotInBag finds an empty slot within a bag func (pil *PlayerItemList) findEmptySlotInBag(bagID int32) int16 { if pil.items[bagID] == nil || pil.items[bagID][BaseEquipment] == nil { return 0 } maxSlots := int16(ClassicEQMaxBagSlots) for slotID := int16(0); slotID < maxSlots; slotID++ { if pil.items[bagID][BaseEquipment][slotID] == nil { return slotID } } return -1 } // EquipmentItemList Methods // NewEquipmentItemList creates a new equipment item list func NewEquipmentItemList() *EquipmentItemList { return &EquipmentItemList{ items: [NumSlots]*Item{}, } } // SetAppearanceType sets the appearance type for this equipment list func (eil *EquipmentItemList) SetAppearanceType(appearanceType int8) { eil.mutex.Lock() defer eil.mutex.Unlock() eil.appearanceType = appearanceType } // GetItem gets an item from a slot func (eil *EquipmentItemList) GetItem(slot int8) *Item { eil.mutex.RLock() defer eil.mutex.RUnlock() if slot < 0 || int(slot) >= len(eil.items) { return nil } return eil.items[slot] } // SetItem sets an item in a slot func (eil *EquipmentItemList) SetItem(slot int8, item *Item, sendEquipUpdate bool) bool { eil.mutex.Lock() defer eil.mutex.Unlock() if slot < 0 || int(slot) >= len(eil.items) { return false } eil.items[slot] = item if item != nil { item.Details.EquipSlotID = int16(slot) } return true } // RemoveItem removes an item from a slot func (eil *EquipmentItemList) RemoveItem(slot int8, sendEquipUpdate bool) *Item { eil.mutex.Lock() defer eil.mutex.Unlock() if slot < 0 || int(slot) >= len(eil.items) { return nil } item := eil.items[slot] eil.items[slot] = nil if item != nil { item.Details.EquipSlotID = -1 } return item } // CanItemBeEquippedInSlot checks if an item can be equipped in a slot func (eil *EquipmentItemList) CanItemBeEquippedInSlot(item *Item, slot int8) bool { if item == nil || slot < 0 || int(slot) >= len(eil.items) { return false } item.mutex.RLock() defer item.mutex.RUnlock() // Check item type vs slot compatibility switch slot { case EQ2PrimarySlot: return item.GenericInfo.ItemType == ItemTypeWeapon case EQ2SecondarySlot: return item.GenericInfo.ItemType == ItemTypeWeapon || item.GenericInfo.ItemType == ItemTypeShield case EQ2HeadSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2ChestSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2ShouldersSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2ForearmsSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2HandsSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2LegsSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2FeetSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2LRingSlot, EQ2RRingSlot: return item.GenericInfo.ItemType == ItemTypeBauble case EQ2EarsSlot1, EQ2EarsSlot2: return item.GenericInfo.ItemType == ItemTypeBauble case EQ2NeckSlot: return item.GenericInfo.ItemType == ItemTypeBauble case EQ2LWristSlot, EQ2RWristSlot: return item.GenericInfo.ItemType == ItemTypeBauble case EQ2RangeSlot: return item.GenericInfo.ItemType == ItemTypeRanged case EQ2AmmoSlot: return item.GenericInfo.ItemType == ItemTypeNormal // Ammo case EQ2WaistSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2CloakSlot: return item.GenericInfo.ItemType == ItemTypeArmor case EQ2CharmSlot1, EQ2CharmSlot2: return item.GenericInfo.ItemType == ItemTypeBauble case EQ2FoodSlot: return item.GenericInfo.ItemType == ItemTypeFood case EQ2DrinkSlot: return item.GenericInfo.ItemType == ItemTypeFood } return false } // GetNumberOfItems returns the number of equipped items func (eil *EquipmentItemList) GetNumberOfItems() int32 { eil.mutex.RLock() defer eil.mutex.RUnlock() count := int32(0) for _, item := range eil.items { if item != nil { count++ } } return count } // GetWeight returns the total weight of equipped items func (eil *EquipmentItemList) GetWeight() int32 { eil.mutex.RLock() defer eil.mutex.RUnlock() totalWeight := int32(0) for _, item := range eil.items { if item != nil { totalWeight += item.GetWeight() } } return totalWeight } // CalculateEquipmentBonuses calculates stat bonuses from all equipped items func (eil *EquipmentItemList) CalculateEquipmentBonuses() *ItemStatsValues { eil.mutex.RLock() defer eil.mutex.RUnlock() bonuses := &ItemStatsValues{} for _, item := range eil.items { if item == nil { continue } item.mutex.RLock() for _, stat := range item.ItemStats { switch stat.StatType { case ItemStatStr: bonuses.Str += int16(stat.Value) case ItemStatSta: bonuses.Sta += int16(stat.Value) case ItemStatAgi: bonuses.Agi += int16(stat.Value) case ItemStatWis: bonuses.Wis += int16(stat.Value) case ItemStatInt: bonuses.Int += int16(stat.Value) case ItemStatHealth: bonuses.Health += int16(stat.Value) case ItemStatPower: bonuses.Power += int16(stat.Value) case ItemStatCritBonus: bonuses.CritBonus += int16(stat.Value) case ItemStatPotency: bonuses.Potency += int16(stat.Value) case ItemStatStrikethrough: bonuses.Strikethrough += int16(stat.Value) case ItemStatAccuracy: bonuses.Accuracy += int16(stat.Value) case ItemStatDPS: bonuses.DPS += int16(stat.Value) case ItemStatAttackSpeed: bonuses.AttackSpeed += int16(stat.Value) case ItemStatMultiattackChance: bonuses.MultiAttackChance += int16(stat.Value) case ItemStatFlurry: bonuses.Flurry += int16(stat.Value) case ItemStatAEAutoattackChance: bonuses.AEAutoattackChance += int16(stat.Value) // Add more stat mappings as needed } } item.mutex.RUnlock() } return bonuses } // ValidateEquipment validates all equipped items func (eil *EquipmentItemList) ValidateEquipment() *ItemValidationResult { eil.mutex.RLock() defer eil.mutex.RUnlock() result := &ItemValidationResult{Valid: true} for slot, item := range eil.items { if item == nil { continue } itemResult := item.Validate() if !itemResult.Valid { result.Valid = false for _, itemErr := range itemResult.Errors { result.Errors = append(result.Errors, fmt.Sprintf("Equipment slot %d: %s", slot, itemErr)) } } // Check slot compatibility if !eil.CanItemBeEquippedInSlot(item, int8(slot)) { result.Valid = false result.Errors = append(result.Errors, fmt.Sprintf("Item %s cannot be equipped in slot %d", item.Name, slot)) } } return result } // Packet Building Integration // BuildItemPacket builds an item packet for network transmission func (im *ItemManager) BuildItemPacket(item *Item, clientVersion uint32) (map[string]interface{}, error) { if item == nil { return nil, fmt.Errorf("item is nil") } item.mutex.RLock() defer item.mutex.RUnlock() packet := make(map[string]interface{}) // Basic item data packet["item_id"] = item.Details.ItemID packet["unique_id"] = item.Details.UniqueID packet["name"] = item.Name packet["description"] = item.Description packet["icon"] = item.Details.Icon packet["classic_icon"] = item.Details.ClassicIcon packet["count"] = item.Details.Count packet["tier"] = item.Details.Tier packet["item_type"] = item.GenericInfo.ItemType packet["condition"] = item.GenericInfo.Condition packet["weight"] = item.GenericInfo.Weight packet["item_flags"] = item.GenericInfo.ItemFlags packet["item_flags2"] = item.GenericInfo.ItemFlags2 packet["sell_price"] = item.GetSellPrice() packet["stack_count"] = item.StackCount packet["bag_id"] = item.Details.BagID packet["slot_id"] = item.Details.SlotID packet["appearance_type"] = item.Details.AppearanceType // Creator info if item.Creator != "" { packet["creator"] = item.Creator } // Adornments packet["adorn0"] = item.Adorn0 packet["adorn1"] = item.Adorn1 packet["adorn2"] = item.Adorn2 // Item stats if len(item.ItemStats) > 0 { stats := make([]map[string]interface{}, len(item.ItemStats)) for i, stat := range item.ItemStats { stats[i] = map[string]interface{}{ "stat_type": stat.StatType, "stat_name": stat.StatName, "value": stat.Value, "level": stat.Level, } } packet["item_stats"] = stats } // Type-specific data if item.WeaponInfo != nil { packet["weapon_info"] = map[string]interface{}{ "wield_type": item.WeaponInfo.WieldType, "damage_low1": item.WeaponInfo.DamageLow1, "damage_high1": item.WeaponInfo.DamageHigh1, "damage_low2": item.WeaponInfo.DamageLow2, "damage_high2": item.WeaponInfo.DamageHigh2, "damage_low3": item.WeaponInfo.DamageLow3, "damage_high3": item.WeaponInfo.DamageHigh3, "delay": item.WeaponInfo.Delay, "rating": item.WeaponInfo.Rating, } } if item.ArmorInfo != nil { packet["armor_info"] = map[string]interface{}{ "mitigation_low": item.ArmorInfo.MitigationLow, "mitigation_high": item.ArmorInfo.MitigationHigh, } } if item.BagInfo != nil { packet["bag_info"] = map[string]interface{}{ "num_slots": item.BagInfo.NumSlots, "weight_reduction": item.BagInfo.WeightReduction, } } if item.FoodInfo != nil { packet["food_info"] = map[string]interface{}{ "type": item.FoodInfo.Type, "level": item.FoodInfo.Level, "duration": item.FoodInfo.Duration, "satiation": item.FoodInfo.Satiation, } } if item.BookInfo != nil { packet["book_info"] = map[string]interface{}{ "language": item.BookInfo.Language, "author": item.BookInfo.Author, "title": item.BookInfo.Title, } } return packet, nil } // GetItemOpcodes returns item-related opcodes func (im *ItemManager) GetItemOpcodes() map[string]packets.InternalOpcode { return map[string]packets.InternalOpcode{ "update_inventory": packets.OP_UpdateInventoryMsg, "item_move": packets.OP_ItemMoveMsg, "item_equip": packets.OP_ItemEquipMsg, "item_unequip": packets.OP_ItemUnequipMsg, "item_pickup": packets.OP_ItemPickupMsg, "item_drop": packets.OP_ItemDropMsg, "item_examine": packets.OP_ItemExamineMsg, "item_update": packets.OP_ItemUpdateMsg, } } // Utility Functions // IsLoaded returns whether the item system has been loaded func (im *ItemManager) IsLoaded() bool { im.mu.RLock() defer im.mu.RUnlock() return im.loaded }