eq2go/internal/items/items.go

2005 lines
59 KiB
Go

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
}