package items import ( "fmt" "sync" "time" ) // Global unique ID counter for items var ( nextUniqueID int64 = 1 uniqueIDMux sync.Mutex ) // NextUniqueID generates the next unique ID for an item func NextUniqueID() int64 { uniqueIDMux.Lock() defer uniqueIDMux.Unlock() id := nextUniqueID nextUniqueID++ return id } // Error handling utilities // ItemError represents an item-specific error type ItemError struct { message string } func (e *ItemError) Error() string { return e.message } // NewItemError creates a new item error func NewItemError(message string) *ItemError { return &ItemError{message: message} } // IsItemError checks if an error is an ItemError func IsItemError(err error) bool { _, ok := err.(*ItemError) return ok } // Common item errors var ( ErrItemNotFound = NewItemError("item not found") ErrInvalidItem = NewItemError("invalid item") ErrItemLocked = NewItemError("item is locked") ErrInsufficientSpace = NewItemError("insufficient inventory space") ErrCannotEquip = NewItemError("cannot equip item") ErrCannotTrade = NewItemError("cannot trade item") ErrItemExpired = NewItemError("item has expired") ) // Item creation utilities // NewItem creates a new item instance with default values func NewItem() *Item { return &Item{ Details: ItemCore{ UniqueID: NextUniqueID(), Count: 1, }, GenericInfo: GenericInfo{ Condition: 100, // 100% condition }, Created: time.Now(), GroupedCharIDs: make(map[int32]bool), } } // NewItemFromTemplate creates a new item from a template item func NewItemFromTemplate(template *Item) *Item { if template == nil { return NewItem() } item := &Item{ // Copy basic information LowerName: template.LowerName, Name: template.Name, Description: template.Description, StackCount: template.StackCount, SellPrice: template.SellPrice, SellStatus: template.SellStatus, MaxSellValue: template.MaxSellValue, BrokerPrice: template.BrokerPrice, WeaponType: template.WeaponType, Adornment: template.Adornment, Creator: template.Creator, SellerName: template.SellerName, SellerCharID: template.SellerCharID, SellerHouseID: template.SellerHouseID, Created: time.Now(), GroupedCharIDs: make(map[int32]bool), EffectType: template.EffectType, BookLanguage: template.BookLanguage, SpellID: template.SpellID, SpellTier: template.SpellTier, ItemScript: template.ItemScript, // Copy core data with new unique ID Details: ItemCore{ ItemID: template.Details.ItemID, SOEId: template.Details.SOEId, UniqueID: NextUniqueID(), Count: 1, Tier: template.Details.Tier, Icon: template.Details.Icon, ClassicIcon: template.Details.ClassicIcon, NumSlots: template.Details.NumSlots, RecommendedLevel: template.Details.RecommendedLevel, }, // Copy generic info GenericInfo: template.GenericInfo, } // Copy arrays and slices if template.Classifications != nil { item.Classifications = make([]*Classifications, len(template.Classifications)) copy(item.Classifications, template.Classifications) } if template.ItemStats != nil { item.ItemStats = make([]*ItemStat, len(template.ItemStats)) copy(item.ItemStats, template.ItemStats) } if template.ItemSets != nil { item.ItemSets = make([]*ItemSet, len(template.ItemSets)) copy(item.ItemSets, template.ItemSets) } if template.ItemStringStats != nil { item.ItemStringStats = make([]*ItemStatString, len(template.ItemStringStats)) copy(item.ItemStringStats, template.ItemStringStats) } if template.ItemLevelOverrides != nil { item.ItemLevelOverrides = make([]*ItemLevelOverride, len(template.ItemLevelOverrides)) copy(item.ItemLevelOverrides, template.ItemLevelOverrides) } if template.ItemEffects != nil { item.ItemEffects = make([]*ItemEffect, len(template.ItemEffects)) copy(item.ItemEffects, template.ItemEffects) } if template.BookPages != nil { item.BookPages = make([]*BookPage, len(template.BookPages)) copy(item.BookPages, template.BookPages) } if template.SlotData != nil { item.SlotData = make([]int8, len(template.SlotData)) copy(item.SlotData, template.SlotData) } // Copy type-specific info pointers (deep copy if needed) if template.WeaponInfo != nil { weaponInfo := *template.WeaponInfo item.WeaponInfo = &weaponInfo } if template.RangedInfo != nil { rangedInfo := *template.RangedInfo item.RangedInfo = &rangedInfo } if template.ArmorInfo != nil { armorInfo := *template.ArmorInfo item.ArmorInfo = &armorInfo } if template.AdornmentInfo != nil { adornmentInfo := *template.AdornmentInfo item.AdornmentInfo = &adornmentInfo } if template.BagInfo != nil { bagInfo := *template.BagInfo item.BagInfo = &bagInfo } if template.FoodInfo != nil { foodInfo := *template.FoodInfo item.FoodInfo = &foodInfo } if template.BaubleInfo != nil { baubleInfo := *template.BaubleInfo item.BaubleInfo = &baubleInfo } if template.BookInfo != nil { bookInfo := *template.BookInfo item.BookInfo = &bookInfo } if template.HouseItemInfo != nil { houseItemInfo := *template.HouseItemInfo item.HouseItemInfo = &houseItemInfo } if template.HouseContainerInfo != nil { houseContainerInfo := *template.HouseContainerInfo item.HouseContainerInfo = &houseContainerInfo } if template.SkillInfo != nil { skillInfo := *template.SkillInfo item.SkillInfo = &skillInfo } if template.RecipeBookInfo != nil { recipeBookInfo := *template.RecipeBookInfo if template.RecipeBookInfo.Recipes != nil { recipeBookInfo.Recipes = make([]uint32, len(template.RecipeBookInfo.Recipes)) copy(recipeBookInfo.Recipes, template.RecipeBookInfo.Recipes) } item.RecipeBookInfo = &recipeBookInfo } if template.ItemSetInfo != nil { itemSetInfo := *template.ItemSetInfo item.ItemSetInfo = &itemSetInfo } if template.ThrownInfo != nil { thrownInfo := *template.ThrownInfo item.ThrownInfo = &thrownInfo } return item } // Item validation utilities // ItemValidationResult represents the result of item validation type ItemValidationResult struct { Valid bool `json:"valid"` Errors []string `json:"errors,omitempty"` } // Validate validates the item's data func (item *Item) Validate() *ItemValidationResult { item.mutex.RLock() defer item.mutex.RUnlock() result := &ItemValidationResult{Valid: true} if item.Details.ItemID <= 0 { result.Valid = false result.Errors = append(result.Errors, "invalid item ID") } if item.Name == "" { result.Valid = false result.Errors = append(result.Errors, "item name cannot be empty") } if len(item.Name) > MaxItemNameLength { result.Valid = false result.Errors = append(result.Errors, fmt.Sprintf("item name too long: %d > %d", len(item.Name), MaxItemNameLength)) } if len(item.Description) > MaxItemDescLength { result.Valid = false result.Errors = append(result.Errors, fmt.Sprintf("item description too long: %d > %d", len(item.Description), MaxItemDescLength)) } if item.Details.Count <= 0 { result.Valid = false result.Errors = append(result.Errors, "item count must be positive") } if item.GenericInfo.Condition < 0 || item.GenericInfo.Condition > 100 { result.Valid = false result.Errors = append(result.Errors, "item condition must be between 0 and 100") } return result } // Item utility methods // IsItemLocked checks if the item is locked for any reason func (item *Item) IsItemLocked() bool { item.mutex.RLock() defer item.mutex.RUnlock() return item.Details.ItemLocked } // CheckClass checks if the item can be used by the given adventure/tradeskill class func (item *Item) CheckClass(adventureClass, tradeskillClass int8) bool { item.mutex.RLock() defer item.mutex.RUnlock() // Check if item has no class restrictions (value of 0 means all classes) if item.GenericInfo.AdventureClasses == 0 && item.GenericInfo.TradeskillClasses == 0 { return true } // Check adventure class if item.GenericInfo.AdventureClasses > 0 { adventureClassFlag := int64(1 << uint(adventureClass)) if item.GenericInfo.AdventureClasses&adventureClassFlag == 0 { return false } } // Check tradeskill class if item.GenericInfo.TradeskillClasses > 0 { tradeskillClassFlag := int64(1 << uint(tradeskillClass)) if item.GenericInfo.TradeskillClasses&tradeskillClassFlag == 0 { return false } } return true } // CheckClassLevel checks if the item can be used by the given class at the given level func (item *Item) CheckClassLevel(adventureClass, tradeskillClass int8, playerLevel int16) bool { item.mutex.RLock() defer item.mutex.RUnlock() // First check if the class can use the item if !item.CheckClass(adventureClass, tradeskillClass) { return false } // Check level requirements requiredLevel := item.GenericInfo.AdventureDefaultLevel if requiredLevel > playerLevel { return false } // Check for level overrides specific to this class for _, override := range item.ItemLevelOverrides { if override.AdventureClass == adventureClass || override.TradeskillClass == tradeskillClass { if override.Level > playerLevel { return false } } } return true } // GetWeight returns the weight of the item in tenths func (item *Item) GetWeight() int32 { item.mutex.RLock() defer item.mutex.RUnlock() return item.GenericInfo.Weight } // IsStackable checks if the item can be stacked func (item *Item) IsStackable() bool { item.mutex.RLock() defer item.mutex.RUnlock() return item.StackCount > 1 } // CanStack checks if this item can stack with another item func (item *Item) CanStack(other *Item) bool { if other == nil { return false } item.mutex.RLock() defer item.mutex.RUnlock() other.mutex.RLock() defer other.mutex.RUnlock() // Items must have same ID and be stackable if item.Details.ItemID != other.Details.ItemID || !item.IsStackable() { return false } // Check if conditions are similar (within tolerance) conditionDiff := item.GenericInfo.Condition - other.GenericInfo.Condition if conditionDiff < 0 { conditionDiff = -conditionDiff } if conditionDiff > 10 { // Allow up to 10% condition difference return false } return true } // GetSellPrice returns the sell price of the item func (item *Item) GetSellPrice() int32 { item.mutex.RLock() defer item.mutex.RUnlock() if item.SellPrice > 0 { return item.SellPrice } // Return a fraction of max sell value if no specific sell price is set return item.MaxSellValue / 4 } // HasFlag checks if the item has a specific flag func (item *Item) HasFlag(flag int16) bool { item.mutex.RLock() defer item.mutex.RUnlock() return item.GenericInfo.ItemFlags&flag != 0 } // HasFlag2 checks if the item has a specific flag2 func (item *Item) HasFlag2(flag int16) bool { item.mutex.RLock() defer item.mutex.RUnlock() return item.GenericInfo.ItemFlags2&flag != 0 } // Slot validation utilities // IsValidSlot checks if a slot ID is valid for equipment func IsValidSlot(slot int8) bool { return slot >= 0 && slot < NumSlots } // GetSlotName returns the name of a slot based on its ID func GetSlotName(slot int8) string { switch slot { case EQ2PrimarySlot: return "Primary" case EQ2SecondarySlot: return "Secondary" case EQ2HeadSlot: return "Head" case EQ2ChestSlot: return "Chest" case EQ2ShouldersSlot: return "Shoulders" case EQ2ForearmsSlot: return "Forearms" case EQ2HandsSlot: return "Hands" case EQ2LegsSlot: return "Legs" case EQ2FeetSlot: return "Feet" case EQ2LRingSlot: return "Left Ring" case EQ2RRingSlot: return "Right Ring" case EQ2EarsSlot1: return "Left Ear" case EQ2EarsSlot2: return "Right Ear" case EQ2NeckSlot: return "Neck" case EQ2LWristSlot: return "Left Wrist" case EQ2RWristSlot: return "Right Wrist" case EQ2RangeSlot: return "Range" case EQ2AmmoSlot: return "Ammo" case EQ2WaistSlot: return "Waist" case EQ2CloakSlot: return "Cloak" case EQ2CharmSlot1: return "Charm 1" case EQ2CharmSlot2: return "Charm 2" case EQ2FoodSlot: return "Food" case EQ2DrinkSlot: return "Drink" case EQ2TexturesSlot: return "Textures" case EQ2HairSlot: return "Hair" case EQ2BeardSlot: return "Beard" case EQ2WingsSlot: return "Wings" case EQ2NakedChestSlot: return "Naked Chest" case EQ2NakedLegsSlot: return "Naked Legs" case EQ2BackSlot: return "Back" default: return "Unknown" } } // GetItemTypeName returns the name of an item type func GetItemTypeName(itemType int8) string { switch itemType { case ItemTypeNormal: return "Normal" case ItemTypeWeapon: return "Weapon" case ItemTypeRanged: return "Ranged" case ItemTypeArmor: return "Armor" case ItemTypeShield: return "Shield" case ItemTypeBag: return "Bag" case ItemTypeSkill: return "Skill" case ItemTypeRecipe: return "Recipe" case ItemTypeFood: return "Food" case ItemTypeBauble: return "Bauble" case ItemTypeHouse: return "House" case ItemTypeThrown: return "Thrown" case ItemTypeHouseContainer: return "House Container" case ItemTypeBook: return "Book" case ItemTypeAdornment: return "Adornment" case ItemTypePattern: return "Pattern" case ItemTypeArmorset: return "Armor Set" default: return "Unknown" } }