eq2go/internal/items/helpers.go

538 lines
13 KiB
Go

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"
}
}