simplify items and items/loot

This commit is contained in:
Sky Johnson 2025-08-29 15:00:01 -05:00
parent d0c51ea42f
commit 6b3270684f
20 changed files with 3707 additions and 9295 deletions

View File

@ -16,6 +16,8 @@ This document outlines how we successfully simplified the EverQuest II housing p
- Groups - Groups
- Guilds - Guilds
- Heroic Ops - Heroic Ops
- Items
- Items/Loot
## Before: Complex Architecture (8 Files, ~2000+ Lines) ## Before: Complex Architecture (8 Files, ~2000+ Lines)

View File

@ -1,562 +0,0 @@
package items
import (
"context"
"fmt"
"log"
"time"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// LoadCharacterItems loads all items for a character from the database
func (idb *ItemDatabase) LoadCharacterItems(charID uint32, masterList *MasterItemList) (*PlayerItemList, *EquipmentItemList, error) {
log.Printf("Loading items for character %d", charID)
inventory := NewPlayerItemList()
equipment := NewEquipmentItemList()
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT item_id, unique_id, inv_slot_id, slot_id, appearance_type, icon, icon2,
count, tier, bag_id, details_count, creator, adornment_slot0, adornment_slot1,
adornment_slot2, group_id, creator_app, random_seed
FROM character_items
WHERE char_id = ?
`
itemCount := 0
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{charID},
ResultFunc: func(stmt *sqlite.Stmt) error {
characterItem, err := idb.scanCharacterItemFromStmt(stmt, masterList)
if err != nil {
log.Printf("Error scanning character item from row: %v", err)
return nil // Continue processing other rows
}
if characterItem == nil {
return nil // Item template not found, continue
}
// Place item in appropriate container based on inv_slot_id
if characterItem.Details.InvSlotID >= 0 && characterItem.Details.InvSlotID < 100 {
// Equipment slots (0-25)
if characterItem.Details.InvSlotID < NumSlots {
equipment.SetItem(int8(characterItem.Details.InvSlotID), characterItem, false)
}
} else {
// Inventory, bank, or special slots
inventory.AddItem(characterItem)
}
itemCount++
return nil
},
})
if err != nil {
return nil, nil, fmt.Errorf("failed to query character items: %w", err)
}
log.Printf("Loaded %d items for character %d", itemCount, charID)
return inventory, equipment, nil
}
// scanCharacterItemFromStmt scans a character item statement and creates an item instance
func (idb *ItemDatabase) scanCharacterItemFromStmt(stmt *sqlite.Stmt, masterList *MasterItemList) (*Item, error) {
itemID := int32(stmt.ColumnInt64(0))
uniqueID := stmt.ColumnInt64(1)
invSlotID := int32(stmt.ColumnInt64(2))
slotID := int32(stmt.ColumnInt64(3))
appearanceType := int8(stmt.ColumnInt64(4))
icon := int16(stmt.ColumnInt64(5))
icon2 := int16(stmt.ColumnInt64(6))
count := int16(stmt.ColumnInt64(7))
tier := int16(stmt.ColumnInt64(8))
bagID := int32(stmt.ColumnInt64(9))
// Skip details_count (column 10) - same as count
var creator string
if stmt.ColumnType(11) != sqlite.TypeNull {
creator = stmt.ColumnText(11)
}
adorn0 := int32(stmt.ColumnInt64(12))
adorn1 := int32(stmt.ColumnInt64(13))
adorn2 := int32(stmt.ColumnInt64(14))
// Skip group_id (column 15) - TODO: implement heirloom groups
// Skip creator_app (column 16) - TODO: implement creator appearance
// Skip random_seed (column 17) - TODO: implement item variations
// Get item template from master list
template := masterList.GetItem(itemID)
if template == nil {
log.Printf("Warning: Item template %d not found for character item", itemID)
return nil, nil
}
// Create item instance from template
item := NewItemFromTemplate(template)
// Update with character-specific data
item.Details.UniqueID = uniqueID
item.Details.InvSlotID = invSlotID
item.Details.SlotID = int16(slotID)
item.Details.AppearanceType = int16(appearanceType)
item.Details.Icon = icon
item.Details.ClassicIcon = icon2
item.Details.Count = count
item.Details.Tier = int8(tier)
item.Details.BagID = bagID
// Set creator if present
if creator != "" {
item.Creator = creator
}
// Set adornment slots
item.Adorn0 = adorn0
item.Adorn1 = adorn1
item.Adorn2 = adorn2
// TODO: Handle group items (heirloom items shared between characters)
// TODO: Handle creator appearance
// TODO: Handle random seed for item variations
return item, nil
}
// SaveCharacterItems saves all items for a character to the database
func (idb *ItemDatabase) SaveCharacterItems(charID uint32, inventory *PlayerItemList, equipment *EquipmentItemList) error {
log.Printf("Saving items for character %d", charID)
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
// Use a savepoint for transaction support
defer sqlitex.Save(conn)(&err)
// Delete existing items for this character
err = sqlitex.Execute(conn, "DELETE FROM character_items WHERE char_id = ?", &sqlitex.ExecOptions{
Args: []any{charID},
})
if err != nil {
return fmt.Errorf("failed to delete existing character items: %w", err)
}
insertQuery := `
INSERT INTO character_items
(char_id, item_id, unique_id, inv_slot_id, slot_id, appearance_type, icon, icon2,
count, tier, bag_id, details_count, creator, adornment_slot0, adornment_slot1,
adornment_slot2, group_id, creator_app, random_seed, created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
itemCount := 0
// Save equipped items
if equipment != nil {
for slotID, item := range equipment.GetAllEquippedItems() {
if item != nil {
if err := idb.saveCharacterItem(conn, insertQuery, charID, item, int32(slotID)); err != nil {
return fmt.Errorf("failed to save equipped item: %w", err)
}
itemCount++
}
}
}
// Save inventory items
if inventory != nil {
allItems := inventory.GetAllItems()
for _, item := range allItems {
if item != nil {
if err := idb.saveCharacterItem(conn, insertQuery, charID, item, item.Details.InvSlotID); err != nil {
return fmt.Errorf("failed to save inventory item: %w", err)
}
itemCount++
}
}
}
log.Printf("Saved %d items for character %d", itemCount, charID)
return nil
}
// saveCharacterItem saves a single character item
func (idb *ItemDatabase) saveCharacterItem(conn *sqlite.Conn, query string, charID uint32, item *Item, invSlotID int32) error {
// Handle null creator
var creator any = nil
if item.Creator != "" {
creator = item.Creator
}
// Handle null creator app
var creatorApp any = nil
// TODO: Set creator app if needed
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{
charID,
item.Details.ItemID,
item.Details.UniqueID,
invSlotID,
item.Details.SlotID,
item.Details.AppearanceType,
item.Details.Icon,
item.Details.ClassicIcon,
item.Details.Count,
item.Details.Tier,
item.Details.BagID,
item.Details.Count, // details_count (same as count for now)
creator,
item.Adorn0,
item.Adorn1,
item.Adorn2,
0, // group_id (TODO: implement heirloom groups)
creatorApp,
0, // random_seed (TODO: implement item variations)
time.Now().Format("2006-01-02 15:04:05"),
},
})
return err
}
// DeleteCharacterItem deletes a specific item from a character's inventory
func (idb *ItemDatabase) DeleteCharacterItem(charID uint32, uniqueID int64) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := "DELETE FROM character_items WHERE char_id = ? AND unique_id = ?"
changes := conn.Changes()
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{charID, uniqueID},
})
if err != nil {
return fmt.Errorf("failed to delete character item: %v", err)
}
rowsAffected := conn.Changes() - changes
if rowsAffected == 0 {
return fmt.Errorf("no item found with unique_id %d for character %d", uniqueID, charID)
}
log.Printf("Deleted item %d for character %d", uniqueID, charID)
return nil
}
// DeleteAllCharacterItems deletes all items for a character
func (idb *ItemDatabase) DeleteAllCharacterItems(charID uint32) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := "DELETE FROM character_items WHERE char_id = ?"
changes := conn.Changes()
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{charID},
})
if err != nil {
return fmt.Errorf("failed to delete character items: %v", err)
}
rowsAffected := conn.Changes() - changes
log.Printf("Deleted %d items for character %d", rowsAffected, charID)
return nil
}
// SaveSingleCharacterItem saves a single character item (for updates)
func (idb *ItemDatabase) SaveSingleCharacterItem(charID uint32, item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
// Use a savepoint for transaction support
defer sqlitex.Save(conn)(&err)
// Handle null creator
var creator any = nil
if item.Creator != "" {
creator = item.Creator
}
// Handle null creator app
var creatorApp any = nil
query := `
INSERT OR REPLACE INTO character_items
(char_id, item_id, unique_id, inv_slot_id, slot_id, appearance_type, icon, icon2,
count, tier, bag_id, details_count, creator, adornment_slot0, adornment_slot1,
adornment_slot2, group_id, creator_app, random_seed, created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{
charID,
item.Details.ItemID,
item.Details.UniqueID,
item.Details.InvSlotID,
item.Details.SlotID,
item.Details.AppearanceType,
item.Details.Icon,
item.Details.ClassicIcon,
item.Details.Count,
item.Details.Tier,
item.Details.BagID,
item.Details.Count, // details_count
creator,
item.Adorn0,
item.Adorn1,
item.Adorn2,
0, // group_id
creatorApp,
0, // random_seed
time.Now().Format("2006-01-02 15:04:05"),
},
})
if err != nil {
return fmt.Errorf("failed to save character item: %v", err)
}
return nil
}
// LoadTemporaryItems loads temporary items that may have expired
func (idb *ItemDatabase) LoadTemporaryItems(charID uint32, masterList *MasterItemList) ([]*Item, error) {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT ci.item_id, ci.unique_id, ci.inv_slot_id, ci.slot_id, ci.appearance_type,
ci.icon, ci.icon2, ci.count, ci.tier, ci.bag_id, ci.details_count,
ci.creator, ci.adornment_slot0, ci.adornment_slot1, ci.adornment_slot2,
ci.group_id, ci.creator_app, ci.random_seed, ci.created
FROM character_items ci
JOIN items i ON ci.item_id = i.id
WHERE ci.char_id = ? AND (i.generic_info_item_flags & ?) > 0
`
var tempItems []*Item
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{charID, Temporary},
ResultFunc: func(stmt *sqlite.Stmt) error {
item, err := idb.scanCharacterItemFromStmt(stmt, masterList)
if err != nil {
log.Printf("Error scanning temporary item: %v", err)
return nil // Continue processing other rows
}
if item != nil {
tempItems = append(tempItems, item)
}
return nil
},
})
if err != nil {
return nil, fmt.Errorf("failed to query temporary items: %w", err)
}
return tempItems, nil
}
// CleanupExpiredItems removes expired temporary items from the database
func (idb *ItemDatabase) CleanupExpiredItems(charID uint32) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
// This would typically check item expiration times and remove expired items
// For now, this is a placeholder implementation
query := `
DELETE FROM character_items
WHERE char_id = ?
AND item_id IN (
SELECT id FROM items
WHERE (generic_info_item_flags & ?) > 0
AND created < datetime('now', '-1 day')
)
`
changes := conn.Changes()
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{charID, Temporary},
})
if err != nil {
return fmt.Errorf("failed to cleanup expired items: %v", err)
}
rowsAffected := conn.Changes() - changes
if rowsAffected > 0 {
log.Printf("Cleaned up %d expired items for character %d", rowsAffected, charID)
}
return nil
}
// UpdateItemLocation updates an item's location in the database
func (idb *ItemDatabase) UpdateItemLocation(charID uint32, uniqueID int64, invSlotID int32, slotID int16, bagID int32) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
UPDATE character_items
SET inv_slot_id = ?, slot_id = ?, bag_id = ?
WHERE char_id = ? AND unique_id = ?
`
changes := conn.Changes()
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{invSlotID, slotID, bagID, charID, uniqueID},
})
if err != nil {
return fmt.Errorf("failed to update item location: %v", err)
}
rowsAffected := conn.Changes() - changes
if rowsAffected == 0 {
return fmt.Errorf("no item found with unique_id %d for character %d", uniqueID, charID)
}
return nil
}
// UpdateItemCount updates an item's count in the database
func (idb *ItemDatabase) UpdateItemCount(charID uint32, uniqueID int64, count int16) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
UPDATE character_items
SET count = ?, details_count = ?
WHERE char_id = ? AND unique_id = ?
`
changes := conn.Changes()
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{count, count, charID, uniqueID},
})
if err != nil {
return fmt.Errorf("failed to update item count: %v", err)
}
rowsAffected := conn.Changes() - changes
if rowsAffected == 0 {
return fmt.Errorf("no item found with unique_id %d for character %d", uniqueID, charID)
}
return nil
}
// GetCharacterItemCount returns the number of items a character has
func (idb *ItemDatabase) GetCharacterItemCount(charID uint32) (int32, error) {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `SELECT COUNT(*) FROM character_items WHERE char_id = ?`
var count int32
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{charID},
ResultFunc: func(stmt *sqlite.Stmt) error {
count = int32(stmt.ColumnInt64(0))
return nil
},
})
if err != nil {
return 0, fmt.Errorf("failed to get character item count: %v", err)
}
return count, nil
}
// GetCharacterItemsByBag returns all items in a specific bag for a character
func (idb *ItemDatabase) GetCharacterItemsByBag(charID uint32, bagID int32, masterList *MasterItemList) ([]*Item, error) {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT item_id, unique_id, inv_slot_id, slot_id, appearance_type, icon, icon2,
count, tier, bag_id, details_count, creator, adornment_slot0, adornment_slot1,
adornment_slot2, group_id, creator_app, random_seed
FROM character_items
WHERE char_id = ? AND bag_id = ?
ORDER BY slot_id
`
var items []*Item
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{charID, bagID},
ResultFunc: func(stmt *sqlite.Stmt) error {
item, err := idb.scanCharacterItemFromStmt(stmt, masterList)
if err != nil {
log.Printf("Error scanning character item from row: %v", err)
return nil // Continue processing other rows
}
if item != nil {
items = append(items, item)
}
return nil
},
})
if err != nil {
return nil, fmt.Errorf("failed to query character items by bag: %v", err)
}
return items, nil
}

View File

@ -1,372 +0,0 @@
package items
import (
"context"
"fmt"
"log"
"strings"
"sync/atomic"
"time"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// ItemDatabase handles all database operations for items
type ItemDatabase struct {
pool *sqlitex.Pool
loadedItems map[int32]bool
}
// NewItemDatabase creates a new item database manager
func NewItemDatabase(pool *sqlitex.Pool) *ItemDatabase {
idb := &ItemDatabase{
pool: pool,
loadedItems: make(map[int32]bool),
}
return idb
}
// LoadItems loads all items from the database into the master item list
func (idb *ItemDatabase) LoadItems(masterList *MasterItemList) error {
// Loading items from database
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT id, soe_id, name, description, icon, icon2, icon_heroic_op, icon_heroic_op2, icon_id,
icon_backdrop, icon_border, icon_tint_red, icon_tint_green, icon_tint_blue, tier,
level, success_sellback, stack_size, generic_info_show_name,
generic_info_item_flags, generic_info_item_flags2, generic_info_creator_flag,
generic_info_condition, generic_info_weight, generic_info_skill_req1,
generic_info_skill_req2, generic_info_skill_min_level, generic_info_item_type,
generic_info_appearance_id, generic_info_appearance_red, generic_info_appearance_green,
generic_info_appearance_blue, generic_info_appearance_highlight_red,
generic_info_appearance_highlight_green, generic_info_appearance_highlight_blue,
generic_info_collectable, generic_info_offers_quest_id, generic_info_part_of_quest_id,
generic_info_max_charges, generic_info_adventure_classes, generic_info_tradeskill_classes,
generic_info_adventure_default_level, generic_info_tradeskill_default_level,
generic_info_usable, generic_info_harvest, generic_info_body_drop,
generic_info_pvp_description, generic_info_merc_only, generic_info_mount_only,
generic_info_set_id, generic_info_collectable_unk, generic_info_transmuted_material,
broker_price, sell_price, max_sell_value, created, script_name, lua_script
FROM items
`
itemCount := 0
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
item, err := idb.scanItemFromStmt(stmt)
if err != nil {
log.Printf("Error scanning item from row: %v", err)
return nil // Continue processing other rows
}
// Load additional item data
if err := idb.loadItemDetails(conn, item); err != nil {
log.Printf("Error loading details for item %d: %v", item.Details.ItemID, err)
return nil // Continue processing other rows
}
masterList.AddItem(item)
idb.loadedItems[item.Details.ItemID] = true
itemCount++
return nil
},
})
if err != nil {
return fmt.Errorf("failed to query items: %w", err)
}
// Loaded items from database
return nil
}
// scanItemFromStmt scans a database statement into an Item struct
func (idb *ItemDatabase) scanItemFromStmt(stmt *sqlite.Stmt) (*Item, error) {
item := &Item{}
item.ItemStats = make([]*ItemStat, 0)
item.ItemEffects = make([]*ItemEffect, 0)
item.ItemStringStats = make([]*ItemStatString, 0)
item.ItemLevelOverrides = make([]*ItemLevelOverride, 0)
item.SlotData = make([]int8, 0)
item.Details.ItemID = int32(stmt.ColumnInt64(0))
item.Details.SOEId = int32(stmt.ColumnInt64(1))
item.Name = stmt.ColumnText(2)
item.Description = stmt.ColumnText(3)
item.Details.Icon = int16(stmt.ColumnInt64(4))
item.Details.ClassicIcon = int16(stmt.ColumnInt64(5))
// Skip icon_heroic_op, icon_heroic_op2, icon_id, icon_backdrop, icon_border as they're duplicates
item.GenericInfo.AppearanceRed = int8(stmt.ColumnInt64(11))
item.GenericInfo.AppearanceGreen = int8(stmt.ColumnInt64(12))
item.GenericInfo.AppearanceBlue = int8(stmt.ColumnInt64(13))
item.Details.Tier = int8(stmt.ColumnInt64(14))
item.Details.RecommendedLevel = int16(stmt.ColumnInt64(15))
item.SellPrice = int32(stmt.ColumnInt64(16))
item.StackCount = int16(stmt.ColumnInt64(17))
item.GenericInfo.ShowName = int8(stmt.ColumnInt64(18))
item.GenericInfo.ItemFlags = int16(stmt.ColumnInt64(19))
item.GenericInfo.ItemFlags2 = int16(stmt.ColumnInt64(20))
item.GenericInfo.CreatorFlag = int8(stmt.ColumnInt64(21))
item.GenericInfo.Condition = int8(stmt.ColumnInt64(22))
item.GenericInfo.Weight = int32(stmt.ColumnInt64(23))
item.GenericInfo.SkillReq1 = int32(stmt.ColumnInt64(24))
item.GenericInfo.SkillReq2 = int32(stmt.ColumnInt64(25))
item.GenericInfo.SkillMin = int16(stmt.ColumnInt64(26))
item.GenericInfo.ItemType = int8(stmt.ColumnInt64(27))
item.GenericInfo.AppearanceID = int16(stmt.ColumnInt64(28))
item.GenericInfo.AppearanceRed = int8(stmt.ColumnInt64(29))
item.GenericInfo.AppearanceGreen = int8(stmt.ColumnInt64(30))
item.GenericInfo.AppearanceBlue = int8(stmt.ColumnInt64(31))
item.GenericInfo.AppearanceHighlightRed = int8(stmt.ColumnInt64(32))
item.GenericInfo.AppearanceHighlightGreen = int8(stmt.ColumnInt64(33))
item.GenericInfo.AppearanceHighlightBlue = int8(stmt.ColumnInt64(34))
item.GenericInfo.Collectable = int8(stmt.ColumnInt64(35))
item.GenericInfo.OffersQuestID = int32(stmt.ColumnInt64(36))
item.GenericInfo.PartOfQuestID = int32(stmt.ColumnInt64(37))
item.GenericInfo.MaxCharges = int16(stmt.ColumnInt64(38))
item.GenericInfo.AdventureClasses = int64(stmt.ColumnInt64(39))
item.GenericInfo.TradeskillClasses = int64(stmt.ColumnInt64(40))
item.GenericInfo.AdventureDefaultLevel = int16(stmt.ColumnInt64(41))
item.GenericInfo.TradeskillDefaultLevel = int16(stmt.ColumnInt64(42))
item.GenericInfo.Usable = int8(stmt.ColumnInt64(43))
item.GenericInfo.Harvest = int8(stmt.ColumnInt64(44))
item.GenericInfo.BodyDrop = int8(stmt.ColumnInt64(45))
// Skip PvP description - assuming it's actually an int8 field based on the error
item.GenericInfo.PvPDescription = int8(stmt.ColumnInt64(46))
item.GenericInfo.MercOnly = int8(stmt.ColumnInt64(47))
item.GenericInfo.MountOnly = int8(stmt.ColumnInt64(48))
item.GenericInfo.SetID = int32(stmt.ColumnInt64(49))
item.GenericInfo.CollectableUnk = int8(stmt.ColumnInt64(50))
item.GenericInfo.TransmutedMaterial = int8(stmt.ColumnInt64(51))
item.BrokerPrice = int64(stmt.ColumnInt64(52))
item.SellPrice = int32(stmt.ColumnInt64(53))
item.MaxSellValue = int32(stmt.ColumnInt64(54))
// Handle created timestamp
if stmt.ColumnType(55) != sqlite.TypeNull {
createdStr := stmt.ColumnText(55)
if created, err := time.Parse("2006-01-02 15:04:05", createdStr); err == nil {
item.Created = created
}
}
// Handle script names
if stmt.ColumnType(56) != sqlite.TypeNull {
item.ItemScript = stmt.ColumnText(56)
}
if stmt.ColumnType(57) != sqlite.TypeNull {
item.ItemScript = stmt.ColumnText(57) // Lua script takes precedence
}
// Set lowercase name for searching
item.LowerName = strings.ToLower(item.Name)
// Generate unique ID
item.Details.UniqueID = NextUniqueItemID()
return item, nil
}
// loadItemDetails loads all additional details for an item
func (idb *ItemDatabase) loadItemDetails(conn *sqlite.Conn, item *Item) error {
// Load item stats
if err := idb.loadItemStats(conn, item); err != nil {
return fmt.Errorf("failed to load stats: %v", err)
}
// Load item effects
if err := idb.loadItemEffects(conn, item); err != nil {
return fmt.Errorf("failed to load effects: %v", err)
}
// Load item appearances
if err := idb.loadItemAppearances(conn, item); err != nil {
return fmt.Errorf("failed to load appearances: %v", err)
}
// Load level overrides
if err := idb.loadItemLevelOverrides(conn, item); err != nil {
return fmt.Errorf("failed to load level overrides: %v", err)
}
// Load modifier strings
if err := idb.loadItemModStrings(conn, item); err != nil {
return fmt.Errorf("failed to load mod strings: %v", err)
}
// Load type-specific details
if err := idb.loadItemTypeDetails(item); err != nil {
return fmt.Errorf("failed to load type details: %v", err)
}
return nil
}
// loadItemStats loads item stat modifications
func (idb *ItemDatabase) loadItemStats(conn *sqlite.Conn, item *Item) error {
query := `
SELECT item_id, stat_type, stat_subtype, value, stat_name, level
FROM item_mod_stats
WHERE item_id = ?
`
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
var stat ItemStat
// Skip item_id (column 0)
stat.StatType = int32(stmt.ColumnInt64(1))
stat.StatSubtype = int16(stmt.ColumnInt64(2))
stat.Value = float32(stmt.ColumnFloat(3))
if stmt.ColumnType(4) != sqlite.TypeNull {
stat.StatName = stmt.ColumnText(4)
}
stat.Level = int8(stmt.ColumnInt64(5))
item.ItemStats = append(item.ItemStats, &stat)
return nil
},
})
return err
}
// loadItemEffects loads item effects and descriptions
func (idb *ItemDatabase) loadItemEffects(conn *sqlite.Conn, item *Item) error {
query := `
SELECT item_id, effect, percentage, subbulletflag
FROM item_effects
WHERE item_id = ?
`
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
var effect ItemEffect
// Skip item_id (column 0)
effect.Effect = stmt.ColumnText(1)
effect.Percentage = int8(stmt.ColumnInt64(2))
effect.SubBulletFlag = int8(stmt.ColumnInt64(3))
item.ItemEffects = append(item.ItemEffects, &effect)
return nil
},
})
return err
}
// loadItemAppearances loads item appearance data
func (idb *ItemDatabase) loadItemAppearances(conn *sqlite.Conn, item *Item) error {
query := `
SELECT item_id, type, red, green, blue, highlight_red, highlight_green, highlight_blue
FROM item_appearances
WHERE item_id = ?
LIMIT 1
`
var foundAppearance bool
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
// Skip item_id (column 0)
item.GenericInfo.AppearanceID = int16(stmt.ColumnInt64(1))
item.GenericInfo.AppearanceRed = int8(stmt.ColumnInt64(2))
item.GenericInfo.AppearanceGreen = int8(stmt.ColumnInt64(3))
item.GenericInfo.AppearanceBlue = int8(stmt.ColumnInt64(4))
item.GenericInfo.AppearanceHighlightRed = int8(stmt.ColumnInt64(5))
item.GenericInfo.AppearanceHighlightGreen = int8(stmt.ColumnInt64(6))
item.GenericInfo.AppearanceHighlightBlue = int8(stmt.ColumnInt64(7))
foundAppearance = true
return nil
},
})
// Stop after first appearance
if foundAppearance {
return err
}
return err
}
// loadItemLevelOverrides loads item level overrides for different classes
func (idb *ItemDatabase) loadItemLevelOverrides(conn *sqlite.Conn, item *Item) error {
query := `
SELECT item_id, adventure_class, tradeskill_class, level
FROM item_levels_override
WHERE item_id = ?
`
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
var override ItemLevelOverride
// Skip item_id (column 0)
override.AdventureClass = int8(stmt.ColumnInt64(1))
override.TradeskillClass = int8(stmt.ColumnInt64(2))
override.Level = int16(stmt.ColumnInt64(3))
item.ItemLevelOverrides = append(item.ItemLevelOverrides, &override)
return nil
},
})
return err
}
// loadItemModStrings loads item modifier strings
func (idb *ItemDatabase) loadItemModStrings(conn *sqlite.Conn, item *Item) error {
query := `
SELECT item_id, stat_string
FROM item_mod_strings
WHERE item_id = ?
`
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
var statString ItemStatString
// Skip item_id (column 0)
statString.StatString = stmt.ColumnText(1)
item.ItemStringStats = append(item.ItemStringStats, &statString)
return nil
},
})
return err
}
// nextUniqueIDCounter is the global counter for unique item IDs
var nextUniqueIDCounter int64 = 1
// NextUniqueItemID generates a unique ID for items (thread-safe)
func NextUniqueItemID() int64 {
return atomic.AddInt64(&nextUniqueIDCounter, 1)
}
// Helper functions for database value parsing (kept for future use)
// Close closes the database pool
func (idb *ItemDatabase) Close() error {
if idb.pool != nil {
return idb.pool.Close()
}
return nil
}

View File

@ -1,562 +0,0 @@
package items
import (
"fmt"
)
// NewEquipmentItemList creates a new equipment item list
func NewEquipmentItemList() *EquipmentItemList {
return &EquipmentItemList{
items: [NumSlots]*Item{},
appearanceType: BaseEquipment,
}
}
// NewEquipmentItemListFromCopy creates a copy of an equipment list
func NewEquipmentItemListFromCopy(source *EquipmentItemList) *EquipmentItemList {
if source == nil {
return NewEquipmentItemList()
}
source.mutex.RLock()
defer source.mutex.RUnlock()
equipment := &EquipmentItemList{
appearanceType: source.appearanceType,
}
// Copy all equipped items
for i, item := range source.items {
if item != nil {
equipment.items[i] = item.Copy()
}
}
return equipment
}
// GetAllEquippedItems returns all equipped items
func (eil *EquipmentItemList) GetAllEquippedItems() []*Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
var equippedItems []*Item
for _, item := range eil.items {
if item != nil {
equippedItems = append(equippedItems, item)
}
}
return equippedItems
}
// ResetPackets resets packet data
func (eil *EquipmentItemList) ResetPackets() {
eil.mutex.Lock()
defer eil.mutex.Unlock()
eil.xorPacket = nil
eil.origPacket = nil
}
// HasItem checks if a specific item ID is equipped
func (eil *EquipmentItemList) HasItem(itemID int32) bool {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
for _, item := range eil.items {
if item != nil && item.Details.ItemID == itemID {
return true
}
}
return false
}
// GetNumberOfItems returns the number of equipped items
func (eil *EquipmentItemList) GetNumberOfItems() int8 {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
count := int8(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.GenericInfo.Weight * int32(item.Details.Count)
}
}
return totalWeight
}
// GetItemFromUniqueID gets an equipped item by unique ID
func (eil *EquipmentItemList) GetItemFromUniqueID(uniqueID int32) *Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
for _, item := range eil.items {
if item != nil && int32(item.Details.UniqueID) == uniqueID {
return item
}
}
return nil
}
// GetItemFromItemID gets an equipped item by item template ID
func (eil *EquipmentItemList) GetItemFromItemID(itemID int32) *Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
for _, item := range eil.items {
if item != nil && item.Details.ItemID == itemID {
return item
}
}
return nil
}
// SetItem sets an item in a specific equipment slot
func (eil *EquipmentItemList) SetItem(slotID int8, item *Item, locked bool) {
if slotID < 0 || slotID >= NumSlots {
return
}
if !locked {
eil.mutex.Lock()
defer eil.mutex.Unlock()
}
eil.items[slotID] = item
if item != nil {
item.Details.SlotID = int16(slotID)
item.Details.AppearanceType = int16(eil.appearanceType)
}
}
// RemoveItem removes an item from a specific slot
func (eil *EquipmentItemList) RemoveItem(slot int8, deleteItem bool) {
if slot < 0 || slot >= NumSlots {
return
}
eil.mutex.Lock()
defer eil.mutex.Unlock()
item := eil.items[slot]
eil.items[slot] = nil
if deleteItem && item != nil {
item.NeedsDeletion = true
}
}
// GetItem gets an item from a specific slot
func (eil *EquipmentItemList) GetItem(slotID int8) *Item {
if slotID < 0 || slotID >= NumSlots {
return nil
}
eil.mutex.RLock()
defer eil.mutex.RUnlock()
return eil.items[slotID]
}
// AddItem adds an item to the equipment (finds appropriate slot)
func (eil *EquipmentItemList) AddItem(slot int8, item *Item) bool {
if item == nil {
return false
}
// Check if the specific slot is requested and valid
if slot >= 0 && slot < NumSlots {
eil.mutex.Lock()
defer eil.mutex.Unlock()
if eil.items[slot] == nil {
eil.items[slot] = item
item.Details.SlotID = int16(slot)
item.Details.AppearanceType = int16(eil.appearanceType)
return true
}
}
// Find a free slot that the item can be equipped in
freeSlot := eil.GetFreeSlot(item, slot, 0)
if freeSlot < NumSlots {
eil.SetItem(freeSlot, item, false)
return true
}
return false
}
// CheckEquipSlot checks if an item can be equipped in a specific slot
func (eil *EquipmentItemList) CheckEquipSlot(item *Item, slot int8) bool {
if item == nil || slot < 0 || slot >= NumSlots {
return false
}
// Check if item has the required slot data
return item.HasSlot(slot, -1)
}
// 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 || slot >= NumSlots {
return false
}
// Check slot compatibility
if !eil.CheckEquipSlot(item, slot) {
return false
}
// Check if slot is already occupied
eil.mutex.RLock()
defer eil.mutex.RUnlock()
return eil.items[slot] == nil
}
// GetFreeSlot finds a free slot for an item
func (eil *EquipmentItemList) GetFreeSlot(item *Item, preferredSlot int8, version int16) int8 {
if item == nil {
return NumSlots // Invalid slot
}
eil.mutex.RLock()
defer eil.mutex.RUnlock()
// If preferred slot is specified and available, use it
if preferredSlot >= 0 && preferredSlot < NumSlots {
if eil.items[preferredSlot] == nil && item.HasSlot(preferredSlot, -1) {
return preferredSlot
}
}
// Search through all possible slots for this item
for slot := int8(0); slot < NumSlots; slot++ {
if eil.items[slot] == nil && item.HasSlot(slot, -1) {
return slot
}
}
return NumSlots // No free slot found
}
// CheckSlotConflict checks for slot conflicts (lore items, etc.)
func (eil *EquipmentItemList) CheckSlotConflict(item *Item, checkLoreOnly bool, loreStackCount *int16) int32 {
if item == nil {
return 0
}
eil.mutex.RLock()
defer eil.mutex.RUnlock()
// Check for lore conflicts
if item.CheckFlag(Lore) || item.CheckFlag(LoreEquip) {
stackCount := int16(0)
for _, equippedItem := range eil.items {
if equippedItem != nil && equippedItem.Details.ItemID == item.Details.ItemID {
stackCount++
}
}
if loreStackCount != nil {
*loreStackCount = stackCount
}
if stackCount > 0 {
return 1 // Lore conflict
}
}
return 0 // No conflict
}
// GetSlotByItem finds the slot an item is equipped in
func (eil *EquipmentItemList) GetSlotByItem(item *Item) int8 {
if item == nil {
return NumSlots
}
eil.mutex.RLock()
defer eil.mutex.RUnlock()
for slot, equippedItem := range eil.items {
if equippedItem == item {
return int8(slot)
}
}
return NumSlots // Not found
}
// CalculateEquipmentBonuses calculates stat bonuses from all equipped items
func (eil *EquipmentItemList) CalculateEquipmentBonuses() *ItemStatsValues {
return eil.CalculateEquipmentBonusesWithEntity(nil)
}
// CalculateEquipmentBonusesWithEntity calculates stat bonuses from all equipped items with entity modifiers
func (eil *EquipmentItemList) CalculateEquipmentBonusesWithEntity(entity Entity) *ItemStatsValues {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
totalBonuses := &ItemStatsValues{}
// We need access to the master item list to calculate bonuses
// This would typically be injected or passed as a parameter
// For now, we'll just accumulate basic stats from the items
for _, item := range eil.items {
if item != nil {
// TODO: Implement item bonus calculation with master item list
// This should call mil.CalculateItemBonusesFromItem(item, entity)
itemBonuses := &ItemStatsValues{} // placeholder
if itemBonuses != nil {
// Add item bonuses to total
totalBonuses.Str += itemBonuses.Str
totalBonuses.Sta += itemBonuses.Sta
totalBonuses.Agi += itemBonuses.Agi
totalBonuses.Wis += itemBonuses.Wis
totalBonuses.Int += itemBonuses.Int
totalBonuses.VsSlash += itemBonuses.VsSlash
totalBonuses.VsCrush += itemBonuses.VsCrush
totalBonuses.VsPierce += itemBonuses.VsPierce
totalBonuses.VsPhysical += itemBonuses.VsPhysical
totalBonuses.VsHeat += itemBonuses.VsHeat
totalBonuses.VsCold += itemBonuses.VsCold
totalBonuses.VsMagic += itemBonuses.VsMagic
totalBonuses.VsMental += itemBonuses.VsMental
totalBonuses.VsDivine += itemBonuses.VsDivine
totalBonuses.VsDisease += itemBonuses.VsDisease
totalBonuses.VsPoison += itemBonuses.VsPoison
totalBonuses.Health += itemBonuses.Health
totalBonuses.Power += itemBonuses.Power
totalBonuses.Concentration += itemBonuses.Concentration
totalBonuses.AbilityModifier += itemBonuses.AbilityModifier
totalBonuses.CriticalMitigation += itemBonuses.CriticalMitigation
totalBonuses.ExtraShieldBlockChance += itemBonuses.ExtraShieldBlockChance
totalBonuses.BeneficialCritChance += itemBonuses.BeneficialCritChance
totalBonuses.CritBonus += itemBonuses.CritBonus
totalBonuses.Potency += itemBonuses.Potency
totalBonuses.HateGainMod += itemBonuses.HateGainMod
totalBonuses.AbilityReuseSpeed += itemBonuses.AbilityReuseSpeed
totalBonuses.AbilityCastingSpeed += itemBonuses.AbilityCastingSpeed
totalBonuses.AbilityRecoverySpeed += itemBonuses.AbilityRecoverySpeed
totalBonuses.SpellReuseSpeed += itemBonuses.SpellReuseSpeed
totalBonuses.SpellMultiAttackChance += itemBonuses.SpellMultiAttackChance
totalBonuses.DPS += itemBonuses.DPS
totalBonuses.AttackSpeed += itemBonuses.AttackSpeed
totalBonuses.MultiAttackChance += itemBonuses.MultiAttackChance
totalBonuses.Flurry += itemBonuses.Flurry
totalBonuses.AEAutoattackChance += itemBonuses.AEAutoattackChance
totalBonuses.Strikethrough += itemBonuses.Strikethrough
totalBonuses.Accuracy += itemBonuses.Accuracy
totalBonuses.OffensiveSpeed += itemBonuses.OffensiveSpeed
totalBonuses.UncontestedParry += itemBonuses.UncontestedParry
totalBonuses.UncontestedBlock += itemBonuses.UncontestedBlock
totalBonuses.UncontestedDodge += itemBonuses.UncontestedDodge
totalBonuses.UncontestedRiposte += itemBonuses.UncontestedRiposte
totalBonuses.SizeMod += itemBonuses.SizeMod
}
}
}
return totalBonuses
}
// SetAppearanceType sets the appearance type (normal or appearance equipment)
func (eil *EquipmentItemList) SetAppearanceType(appearanceType int8) {
eil.mutex.Lock()
defer eil.mutex.Unlock()
eil.appearanceType = appearanceType
// Update all equipped items with new appearance type
for _, item := range eil.items {
if item != nil {
item.Details.AppearanceType = int16(appearanceType)
}
}
}
// GetAppearanceType gets the current appearance type
func (eil *EquipmentItemList) GetAppearanceType() int8 {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
return eil.appearanceType
}
// 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 {
// Validate item
itemResult := item.Validate()
if !itemResult.Valid {
result.Valid = false
for _, err := range itemResult.Errors {
result.Errors = append(result.Errors, fmt.Sprintf("Slot %d: %s", slot, err))
}
}
// Check slot compatibility
if !item.HasSlot(int8(slot), -1) {
result.Valid = false
result.Errors = append(result.Errors, fmt.Sprintf("Item %s cannot be equipped in slot %d", item.Name, slot))
}
}
}
return result
}
// GetEquippedItemsByType returns equipped items of a specific type
func (eil *EquipmentItemList) GetEquippedItemsByType(itemType int8) []*Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
var matchingItems []*Item
for _, item := range eil.items {
if item != nil && item.GenericInfo.ItemType == itemType {
matchingItems = append(matchingItems, item)
}
}
return matchingItems
}
// GetWeapons returns all equipped weapons
func (eil *EquipmentItemList) GetWeapons() []*Item {
return eil.GetEquippedItemsByType(ItemTypeWeapon)
}
// GetArmor returns all equipped armor pieces
func (eil *EquipmentItemList) GetArmor() []*Item {
return eil.GetEquippedItemsByType(ItemTypeArmor)
}
// GetJewelry returns all equipped jewelry
func (eil *EquipmentItemList) GetJewelry() []*Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
var jewelry []*Item
// Check ring slots
if eil.items[EQ2LRingSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2LRingSlot])
}
if eil.items[EQ2RRingSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2RRingSlot])
}
// Check ear slots
if eil.items[EQ2EarsSlot1] != nil {
jewelry = append(jewelry, eil.items[EQ2EarsSlot1])
}
if eil.items[EQ2EarsSlot2] != nil {
jewelry = append(jewelry, eil.items[EQ2EarsSlot2])
}
// Check neck slot
if eil.items[EQ2NeckSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2NeckSlot])
}
// Check wrist slots
if eil.items[EQ2LWristSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2LWristSlot])
}
if eil.items[EQ2RWristSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2RWristSlot])
}
return jewelry
}
// HasWeaponEquipped checks if any weapon is equipped
func (eil *EquipmentItemList) HasWeaponEquipped() bool {
weapons := eil.GetWeapons()
return len(weapons) > 0
}
// HasShieldEquipped checks if a shield is equipped
func (eil *EquipmentItemList) HasShieldEquipped() bool {
item := eil.GetItem(EQ2SecondarySlot)
return item != nil && item.IsShield()
}
// HasTwoHandedWeapon checks if a two-handed weapon is equipped
func (eil *EquipmentItemList) HasTwoHandedWeapon() bool {
primaryItem := eil.GetItem(EQ2PrimarySlot)
if primaryItem != nil && primaryItem.IsWeapon() && primaryItem.WeaponInfo != nil {
return primaryItem.WeaponInfo.WieldType == ItemWieldTypeTwoHand
}
return false
}
// CanDualWield checks if dual wielding is possible with current equipment
func (eil *EquipmentItemList) CanDualWield() bool {
primaryItem := eil.GetItem(EQ2PrimarySlot)
secondaryItem := eil.GetItem(EQ2SecondarySlot)
if primaryItem != nil && secondaryItem != nil {
// Both items must be weapons that can be dual wielded
if primaryItem.IsWeapon() && secondaryItem.IsWeapon() {
if primaryItem.WeaponInfo != nil && secondaryItem.WeaponInfo != nil {
return primaryItem.WeaponInfo.WieldType == ItemWieldTypeDual &&
secondaryItem.WeaponInfo.WieldType == ItemWieldTypeDual
}
}
}
return false
}
// String returns a string representation of the equipment list
func (eil *EquipmentItemList) String() string {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
equippedCount := 0
for _, item := range eil.items {
if item != nil {
equippedCount++
}
}
return fmt.Sprintf("EquipmentItemList{Equipped: %d/%d, AppearanceType: %d}",
equippedCount, NumSlots, eil.appearanceType)
}
func init() {
// Equipment item list system initialized
}

538
internal/items/helpers.go Normal file
View File

@ -0,0 +1,538 @@
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"
}
}

View File

@ -1,742 +0,0 @@
package items
import (
"fmt"
"sync"
"time"
)
// SpellManager defines the interface for spell-related operations needed by items
type SpellManager interface {
// GetSpell retrieves spell information by ID and tier
GetSpell(spellID uint32, tier int8) (Spell, error)
// GetSpellsBySkill gets spells associated with a skill
GetSpellsBySkill(skillID uint32) ([]uint32, error)
// ValidateSpellID checks if a spell ID is valid
ValidateSpellID(spellID uint32) bool
}
// PlayerManager defines the interface for player-related operations needed by items
type PlayerManager interface {
// GetPlayer retrieves player information by ID
GetPlayer(playerID uint32) (Player, error)
// GetPlayerLevel gets a player's current level
GetPlayerLevel(playerID uint32) (int16, error)
// GetPlayerClass gets a player's adventure class
GetPlayerClass(playerID uint32) (int8, error)
// GetPlayerRace gets a player's race
GetPlayerRace(playerID uint32) (int8, error)
// SendMessageToPlayer sends a message to a player
SendMessageToPlayer(playerID uint32, channel int8, message string) error
// GetPlayerName gets a player's name
GetPlayerName(playerID uint32) (string, error)
}
// PacketManager defines the interface for packet-related operations
type PacketManager interface {
// SendPacketToPlayer sends a packet to a specific player
SendPacketToPlayer(playerID uint32, packetData []byte) error
// QueuePacketForPlayer queues a packet for delayed sending
QueuePacketForPlayer(playerID uint32, packetData []byte) error
// GetClientVersion gets the client version for a player
GetClientVersion(playerID uint32) (int16, error)
// SerializeItem serializes an item for network transmission
SerializeItem(item *Item, clientVersion int16, player Player) ([]byte, error)
}
// RuleManager defines the interface for rules/configuration access
type RuleManager interface {
// GetBool retrieves a boolean rule value
GetBool(category, rule string) bool
// GetInt32 retrieves an int32 rule value
GetInt32(category, rule string) int32
// GetFloat retrieves a float rule value
GetFloat(category, rule string) float32
// GetString retrieves a string rule value
GetString(category, rule string) string
}
// DatabaseService defines the interface for item persistence operations
type DatabaseService interface {
// LoadItems loads all item templates from the database
LoadItems(masterList *MasterItemList) error
// SaveItem saves an item template to the database
SaveItem(item *Item) error
// DeleteItem removes an item template from the database
DeleteItem(itemID int32) error
// LoadPlayerItems loads a player's inventory from the database
LoadPlayerItems(playerID uint32) (*PlayerItemList, error)
// SavePlayerItems saves a player's inventory to the database
SavePlayerItems(playerID uint32, itemList *PlayerItemList) error
// LoadPlayerEquipment loads a player's equipment from the database
LoadPlayerEquipment(playerID uint32, appearanceType int8) (*EquipmentItemList, error)
// SavePlayerEquipment saves a player's equipment to the database
SavePlayerEquipment(playerID uint32, equipment *EquipmentItemList) error
// LoadItemStats loads item stat mappings from the database
LoadItemStats() (map[string]int32, map[int32]string, error)
// SaveItemStat saves an item stat mapping to the database
SaveItemStat(statID int32, statName string) error
}
// QuestManager defines the interface for quest-related item operations
type QuestManager interface {
// CheckQuestPrerequisites checks if a player meets quest prerequisites for an item
CheckQuestPrerequisites(playerID uint32, questID int32) bool
// GetQuestRewards gets quest rewards for an item
GetQuestRewards(questID int32) ([]*QuestRewardData, error)
// IsQuestItem checks if an item is a quest item
IsQuestItem(itemID int32) bool
}
// BrokerManager defines the interface for broker/marketplace operations
type BrokerManager interface {
// SearchItems searches for items on the broker
SearchItems(criteria *ItemSearchCriteria) ([]*Item, error)
// ListItem lists an item on the broker
ListItem(playerID uint32, item *Item, price int64) error
// BuyItem purchases an item from the broker
BuyItem(playerID uint32, itemID int32, sellerID uint32) error
// GetItemPrice gets the current market price for an item
GetItemPrice(itemID int32) (int64, error)
}
// CraftingManager defines the interface for crafting-related item operations
type CraftingManager interface {
// CanCraftItem checks if a player can craft an item
CanCraftItem(playerID uint32, itemID int32) bool
// GetCraftingRequirements gets crafting requirements for an item
GetCraftingRequirements(itemID int32) ([]CraftingRequirement, error)
// CraftItem handles item crafting
CraftItem(playerID uint32, itemID int32, quality int8) (*Item, error)
}
// HousingManager defines the interface for housing-related item operations
type HousingManager interface {
// CanPlaceItem checks if an item can be placed in a house
CanPlaceItem(playerID uint32, houseID int32, item *Item) bool
// PlaceItem places an item in a house
PlaceItem(playerID uint32, houseID int32, item *Item, location HouseLocation) error
// RemoveItem removes an item from a house
RemoveItem(playerID uint32, houseID int32, itemID int32) error
// GetHouseItems gets all items in a house
GetHouseItems(houseID int32) ([]*Item, error)
}
// LootManager defines the interface for loot-related operations
type LootManager interface {
// GenerateLoot generates loot for a loot table
GenerateLoot(lootTableID int32, playerLevel int16) ([]*Item, error)
// DistributeLoot distributes loot to players
DistributeLoot(items []*Item, playerIDs []uint32, lootMethod int8) error
// CanLootItem checks if a player can loot an item
CanLootItem(playerID uint32, item *Item) bool
}
// Data structures used by the interfaces
// Spell represents a spell in the game
type Spell interface {
GetID() uint32
GetName() string
GetIcon() uint32
GetIconBackdrop() uint32
GetTier() int8
GetDescription() string
}
// Player represents a player in the game
type Player interface {
GetID() uint32
GetName() string
GetLevel() int16
GetAdventureClass() int8
GetTradeskillClass() int8
GetRace() int8
GetGender() int8
GetAlignment() int8
}
// Entity represents an entity (player or NPC) that can have items
type Entity interface {
GetID() uint32
GetName() string
GetLevel() int16
GetRace() int8
GetGender() int8
GetAlignment() int8
IsPlayer() bool
IsNPC() bool
// GetStatValueByName gets a stat value by name for item calculations
GetStatValueByName(statName string) float64
// GetSkillValueByName gets a skill value by name for item calculations
GetSkillValueByName(skillName string) int32
}
// CraftingRequirement represents a crafting requirement
type CraftingRequirement struct {
ItemID int32 `json:"item_id"`
Quantity int16 `json:"quantity"`
Skill int32 `json:"skill"`
Level int16 `json:"level"`
}
// HouseLocation represents a location within a house
type HouseLocation struct {
X float32 `json:"x"`
Y float32 `json:"y"`
Z float32 `json:"z"`
Heading float32 `json:"heading"`
Pitch float32 `json:"pitch"`
Roll float32 `json:"roll"`
Location int8 `json:"location"` // 0=floor, 1=ceiling, 2=wall
}
// ItemSystemAdapter provides a high-level interface to the complete item system
type ItemSystemAdapter struct {
masterList *MasterItemList
playerLists map[uint32]*PlayerItemList
equipmentLists map[uint32]*EquipmentItemList
spellManager SpellManager
playerManager PlayerManager
packetManager PacketManager
ruleManager RuleManager
databaseService DatabaseService
questManager QuestManager
brokerManager BrokerManager
craftingManager CraftingManager
housingManager HousingManager
lootManager LootManager
mutex sync.RWMutex
}
// NewItemSystemAdapter creates a new item system adapter with all dependencies
func NewItemSystemAdapter(
masterList *MasterItemList,
spellManager SpellManager,
playerManager PlayerManager,
packetManager PacketManager,
ruleManager RuleManager,
databaseService DatabaseService,
questManager QuestManager,
brokerManager BrokerManager,
craftingManager CraftingManager,
housingManager HousingManager,
lootManager LootManager,
) *ItemSystemAdapter {
return &ItemSystemAdapter{
masterList: masterList,
playerLists: make(map[uint32]*PlayerItemList),
equipmentLists: make(map[uint32]*EquipmentItemList),
spellManager: spellManager,
playerManager: playerManager,
packetManager: packetManager,
ruleManager: ruleManager,
databaseService: databaseService,
questManager: questManager,
brokerManager: brokerManager,
craftingManager: craftingManager,
housingManager: housingManager,
lootManager: lootManager,
}
}
// Initialize sets up the item system (loads items from database, etc.)
func (isa *ItemSystemAdapter) Initialize() error {
// Load items from database
err := isa.databaseService.LoadItems(isa.masterList)
if err != nil {
return err
}
// Load item stat mappings
statsStrings, statsIDs, err := isa.databaseService.LoadItemStats()
if err != nil {
return err
}
isa.masterList.mutex.Lock()
isa.masterList.mappedItemStatsStrings = statsStrings
isa.masterList.mappedItemStatTypeIDs = statsIDs
isa.masterList.mutex.Unlock()
return nil
}
// GetPlayerInventory gets or loads a player's inventory
func (isa *ItemSystemAdapter) GetPlayerInventory(playerID uint32) (*PlayerItemList, error) {
isa.mutex.Lock()
defer isa.mutex.Unlock()
if itemList, exists := isa.playerLists[playerID]; exists {
return itemList, nil
}
// Load from database
itemList, err := isa.databaseService.LoadPlayerItems(playerID)
if err != nil {
return nil, err
}
if itemList == nil {
itemList = NewPlayerItemList()
}
isa.playerLists[playerID] = itemList
return itemList, nil
}
// GetPlayerEquipment gets or loads a player's equipment
func (isa *ItemSystemAdapter) GetPlayerEquipment(playerID uint32, appearanceType int8) (*EquipmentItemList, error) {
isa.mutex.Lock()
defer isa.mutex.Unlock()
key := uint32(playerID)*10 + uint32(appearanceType)
if equipment, exists := isa.equipmentLists[key]; exists {
return equipment, nil
}
// Load from database
equipment, err := isa.databaseService.LoadPlayerEquipment(playerID, appearanceType)
if err != nil {
return nil, err
}
if equipment == nil {
equipment = NewEquipmentItemList()
equipment.SetAppearanceType(appearanceType)
}
isa.equipmentLists[key] = equipment
return equipment, nil
}
// SavePlayerData saves a player's item data
func (isa *ItemSystemAdapter) SavePlayerData(playerID uint32) error {
isa.mutex.RLock()
defer isa.mutex.RUnlock()
// Save inventory
if itemList, exists := isa.playerLists[playerID]; exists {
err := isa.databaseService.SavePlayerItems(playerID, itemList)
if err != nil {
return err
}
}
// Save equipment (both normal and appearance)
for key, equipment := range isa.equipmentLists {
if key/10 == playerID {
err := isa.databaseService.SavePlayerEquipment(playerID, equipment)
if err != nil {
return err
}
}
}
return nil
}
// GiveItemToPlayer gives an item to a player
func (isa *ItemSystemAdapter) GiveItemToPlayer(playerID uint32, itemID int32, quantity int16, addType AddItemType) error {
// Get item template
itemTemplate := isa.masterList.GetItem(itemID)
if itemTemplate == nil {
return ErrItemNotFound
}
// Create item instance
item := NewItemFromTemplate(itemTemplate)
item.Details.Count = quantity
// Get player inventory
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
// Try to add item to inventory
if !inventory.AddItem(item) {
return ErrInsufficientSpace
}
// Send update to player
player, err := isa.playerManager.GetPlayer(playerID)
if err != nil {
return err
}
clientVersion, _ := isa.packetManager.GetClientVersion(playerID)
packetData, err := isa.packetManager.SerializeItem(item, clientVersion, player)
if err != nil {
return err
}
return isa.packetManager.SendPacketToPlayer(playerID, packetData)
}
// RemoveItemFromPlayer removes an item from a player
func (isa *ItemSystemAdapter) RemoveItemFromPlayer(playerID uint32, uniqueID int32, quantity int16) error {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
item := inventory.GetItemFromUniqueID(uniqueID, true, true)
if item == nil {
return ErrItemNotFound
}
// Check if item can be removed
if item.IsItemLocked() {
return ErrItemLocked
}
if item.Details.Count <= quantity {
// Remove entire stack
inventory.RemoveItem(item, true, true)
} else {
// Reduce quantity
item.Details.Count -= quantity
}
return nil
}
// EquipItem equips an item for a player
func (isa *ItemSystemAdapter) EquipItem(playerID uint32, uniqueID int32, slot int8, appearanceType int8) error {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
equipment, err := isa.GetPlayerEquipment(playerID, appearanceType)
if err != nil {
return err
}
// Get item from inventory
item := inventory.GetItemFromUniqueID(uniqueID, false, true)
if item == nil {
return ErrItemNotFound
}
// Check if item can be equipped
if !equipment.CanItemBeEquippedInSlot(item, slot) {
return ErrCannotEquip
}
// Check class/race/level requirements
player, err := isa.playerManager.GetPlayer(playerID)
if err != nil {
return err
}
if !item.CheckClass(player.GetAdventureClass(), player.GetTradeskillClass()) {
return ErrCannotEquip
}
if !item.CheckClassLevel(player.GetAdventureClass(), player.GetTradeskillClass(), player.GetLevel()) {
return ErrCannotEquip
}
// Remove from inventory
inventory.RemoveItem(item, false, true)
// Check if slot is occupied and unequip current item
currentItem := equipment.GetItem(slot)
if currentItem != nil {
equipment.RemoveItem(slot, false)
inventory.AddItem(currentItem)
}
// Equip new item
equipment.SetItem(slot, item, false)
return nil
}
// UnequipItem unequips an item for a player
func (isa *ItemSystemAdapter) UnequipItem(playerID uint32, slot int8, appearanceType int8) error {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
equipment, err := isa.GetPlayerEquipment(playerID, appearanceType)
if err != nil {
return err
}
// Get equipped item
item := equipment.GetItem(slot)
if item == nil {
return ErrItemNotFound
}
// Check if item can be unequipped
if item.IsItemLocked() {
return ErrItemLocked
}
// Remove from equipment
equipment.RemoveItem(slot, false)
// Add to inventory
if !inventory.AddItem(item) {
// Inventory full, add to overflow
inventory.AddOverflowItem(item)
}
return nil
}
// MoveItem moves an item within a player's inventory
func (isa *ItemSystemAdapter) MoveItem(playerID uint32, fromBagID int32, fromSlot int16, toBagID int32, toSlot int16, appearanceType int8) error {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
// Get item from source location
item := inventory.GetItem(fromBagID, fromSlot, appearanceType)
if item == nil {
return ErrItemNotFound
}
// Check if item is locked
if item.IsItemLocked() {
return ErrItemLocked
}
// Move item
inventory.MoveItem(item, toBagID, toSlot, appearanceType, true)
return nil
}
// SearchBrokerItems searches for items on the broker
func (isa *ItemSystemAdapter) SearchBrokerItems(criteria *ItemSearchCriteria) ([]*Item, error) {
if isa.brokerManager == nil {
return nil, fmt.Errorf("broker manager not available")
}
return isa.brokerManager.SearchItems(criteria)
}
// CraftItem handles item crafting
func (isa *ItemSystemAdapter) CraftItem(playerID uint32, itemID int32, quality int8) (*Item, error) {
if isa.craftingManager == nil {
return nil, fmt.Errorf("crafting manager not available")
}
// Check if player can craft the item
if !isa.craftingManager.CanCraftItem(playerID, itemID) {
return nil, fmt.Errorf("player cannot craft this item")
}
// Craft the item
return isa.craftingManager.CraftItem(playerID, itemID, quality)
}
// GetPlayerItemStats returns statistics about a player's items
func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]any, error) {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return nil, err
}
equipment, err := isa.GetPlayerEquipment(playerID, BaseEquipment)
if err != nil {
return nil, err
}
// Calculate equipment bonuses
bonuses := equipment.CalculateEquipmentBonuses()
return map[string]any{
"player_id": playerID,
"total_items": inventory.GetNumberOfItems(),
"equipped_items": equipment.GetNumberOfItems(),
"inventory_weight": inventory.GetWeight(),
"equipment_weight": equipment.GetWeight(),
"free_slots": inventory.GetNumberOfFreeSlots(),
"overflow_items": len(inventory.GetOverflowItemList()),
"stat_bonuses": bonuses,
"last_update": time.Now(),
}, nil
}
// GetSystemStats returns comprehensive statistics about the item system
func (isa *ItemSystemAdapter) GetSystemStats() map[string]any {
isa.mutex.RLock()
defer isa.mutex.RUnlock()
masterStats := isa.masterList.GetStats()
return map[string]any{
"total_item_templates": masterStats.TotalItems,
"items_by_type": masterStats.ItemsByType,
"items_by_tier": masterStats.ItemsByTier,
"active_players": len(isa.playerLists),
"cached_inventories": len(isa.playerLists),
"cached_equipment": len(isa.equipmentLists),
"last_update": time.Now(),
}
}
// ClearPlayerData removes cached data for a player (e.g., when they log out)
func (isa *ItemSystemAdapter) ClearPlayerData(playerID uint32) {
isa.mutex.Lock()
defer isa.mutex.Unlock()
// Remove inventory
delete(isa.playerLists, playerID)
// Remove equipment
keysToDelete := make([]uint32, 0)
for key := range isa.equipmentLists {
if key/10 == playerID {
keysToDelete = append(keysToDelete, key)
}
}
for _, key := range keysToDelete {
delete(isa.equipmentLists, key)
}
}
// ValidatePlayerItems validates all items for a player
func (isa *ItemSystemAdapter) ValidatePlayerItems(playerID uint32) *ItemValidationResult {
result := &ItemValidationResult{Valid: true}
// Validate inventory
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
result.Valid = false
result.Errors = append(result.Errors, fmt.Sprintf("Failed to load inventory: %v", err))
return result
}
allItems := inventory.GetAllItems()
for index, item := range allItems {
itemResult := item.Validate()
if !itemResult.Valid {
result.Valid = false
for _, itemErr := range itemResult.Errors {
result.Errors = append(result.Errors, fmt.Sprintf("Inventory item %d: %s", index, itemErr))
}
}
}
// Validate equipment
equipment, err := isa.GetPlayerEquipment(playerID, BaseEquipment)
if err != nil {
result.Valid = false
result.Errors = append(result.Errors, fmt.Sprintf("Failed to load equipment: %v", err))
return result
}
equipResult := equipment.ValidateEquipment()
if !equipResult.Valid {
result.Valid = false
result.Errors = append(result.Errors, equipResult.Errors...)
}
return result
}
// MockImplementations for testing
// MockSpellManager is a mock implementation of SpellManager for testing
type MockSpellManager struct {
spells map[uint32]MockSpell
}
// MockSpell is a mock implementation of Spell for testing
type MockSpell struct {
id uint32
name string
icon uint32
iconBackdrop uint32
tier int8
description string
}
func (ms MockSpell) GetID() uint32 { return ms.id }
func (ms MockSpell) GetName() string { return ms.name }
func (ms MockSpell) GetIcon() uint32 { return ms.icon }
func (ms MockSpell) GetIconBackdrop() uint32 { return ms.iconBackdrop }
func (ms MockSpell) GetTier() int8 { return ms.tier }
func (ms MockSpell) GetDescription() string { return ms.description }
func (msm *MockSpellManager) GetSpell(spellID uint32, tier int8) (Spell, error) {
if spell, exists := msm.spells[spellID]; exists {
return spell, nil
}
return nil, fmt.Errorf("spell not found: %d", spellID)
}
func (msm *MockSpellManager) GetSpellsBySkill(skillID uint32) ([]uint32, error) {
return []uint32{}, nil
}
func (msm *MockSpellManager) ValidateSpellID(spellID uint32) bool {
_, exists := msm.spells[spellID]
return exists
}
// NewMockSpellManager creates a new mock spell manager
func NewMockSpellManager() *MockSpellManager {
return &MockSpellManager{
spells: make(map[uint32]MockSpell),
}
}
// AddMockSpell adds a mock spell for testing
func (msm *MockSpellManager) AddMockSpell(id uint32, name string, icon uint32, tier int8, description string) {
msm.spells[id] = MockSpell{
id: id,
name: name,
icon: icon,
iconBackdrop: icon + 1000,
tier: tier,
description: description,
}
}
func init() {
// Item system interfaces initialized
}

File diff suppressed because it is too large Load Diff

View File

@ -1,722 +0,0 @@
package items
import (
"context"
"fmt"
"log"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// loadItemTypeDetails loads type-specific details for an item based on its type
func (idb *ItemDatabase) loadItemTypeDetails(item *Item) error {
switch item.GenericInfo.ItemType {
case ItemTypeWeapon:
return idb.loadWeaponDetails(item)
case ItemTypeRanged:
return idb.loadRangedWeaponDetails(item)
case ItemTypeArmor:
return idb.loadArmorDetails(item)
case ItemTypeShield:
return idb.loadShieldDetails(item)
case ItemTypeBag:
return idb.loadBagDetails(item)
case ItemTypeSkill:
return idb.loadSkillDetails(item)
case ItemTypeRecipe:
return idb.loadRecipeBookDetails(item)
case ItemTypeFood:
return idb.loadFoodDetails(item)
case ItemTypeBauble:
return idb.loadBaubleDetails(item)
case ItemTypeHouse:
return idb.loadHouseItemDetails(item)
case ItemTypeThrown:
return idb.loadThrownWeaponDetails(item)
case ItemTypeHouseContainer:
return idb.loadHouseContainerDetails(item)
case ItemTypeBook:
return idb.loadBookDetails(item)
case ItemTypeAdornment:
return idb.loadAdornmentDetails(item)
}
// No specific type details needed for this item type
return nil
}
// loadWeaponDetails loads weapon-specific information
func (idb *ItemDatabase) loadWeaponDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT wield_type, damage_low1, damage_high1, damage_low2, damage_high2,
damage_low3, damage_high3, delay_hundredths, rating
FROM item_details_weapon
WHERE item_id = ?
`
weapon := &WeaponInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
weapon.WieldType = int16(stmt.ColumnInt64(0))
weapon.DamageLow1 = int16(stmt.ColumnInt64(1))
weapon.DamageHigh1 = int16(stmt.ColumnInt64(2))
weapon.DamageLow2 = int16(stmt.ColumnInt64(3))
weapon.DamageHigh2 = int16(stmt.ColumnInt64(4))
weapon.DamageLow3 = int16(stmt.ColumnInt64(5))
weapon.DamageHigh3 = int16(stmt.ColumnInt64(6))
weapon.Delay = int16(stmt.ColumnInt64(7))
weapon.Rating = float32(stmt.ColumnFloat(8))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load weapon details: %v", err)
}
if found {
item.WeaponInfo = weapon
}
return nil
}
// loadRangedWeaponDetails loads ranged weapon information
func (idb *ItemDatabase) loadRangedWeaponDetails(item *Item) error {
// First load weapon info
if err := idb.loadWeaponDetails(item); err != nil {
return err
}
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT range_low, range_high
FROM item_details_range
WHERE item_id = ?
`
ranged := &RangedInfo{
WeaponInfo: *item.WeaponInfo, // Copy weapon info
}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
ranged.RangeLow = int16(stmt.ColumnInt64(0))
ranged.RangeHigh = int16(stmt.ColumnInt64(1))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load ranged weapon details: %v", err)
}
if found {
item.RangedInfo = ranged
item.WeaponInfo = nil // Clear weapon info since we have ranged info
}
return nil
}
// loadArmorDetails loads armor mitigation information
func (idb *ItemDatabase) loadArmorDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT mitigation_low, mitigation_high
FROM item_details_armor
WHERE item_id = ?
`
armor := &ArmorInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
armor.MitigationLow = int16(stmt.ColumnInt64(0))
armor.MitigationHigh = int16(stmt.ColumnInt64(1))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load armor details: %v", err)
}
if found {
item.ArmorInfo = armor
}
return nil
}
// loadShieldDetails loads shield information
func (idb *ItemDatabase) loadShieldDetails(item *Item) error {
// Load armor details first
if err := idb.loadArmorDetails(item); err != nil {
return err
}
if item.ArmorInfo != nil {
shield := &ShieldInfo{
ArmorInfo: *item.ArmorInfo,
}
item.ArmorInfo = nil // Clear armor info
// Note: In Go we don't have ShieldInfo, just use ArmorInfo
item.ArmorInfo = &shield.ArmorInfo
}
return nil
}
// loadBagDetails loads bag information
func (idb *ItemDatabase) loadBagDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT num_slots, weight_reduction
FROM item_details_bag
WHERE item_id = ?
`
bag := &BagInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
bag.NumSlots = int8(stmt.ColumnInt64(0))
bag.WeightReduction = int16(stmt.ColumnInt64(1))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load bag details: %v", err)
}
if found {
item.BagInfo = bag
}
return nil
}
// loadSkillDetails loads skill book information
func (idb *ItemDatabase) loadSkillDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT spell_id, spell_tier
FROM item_details_skill
WHERE item_id = ?
`
skill := &SkillInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
skill.SpellID = int32(stmt.ColumnInt64(0))
skill.SpellTier = int32(stmt.ColumnInt64(1))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load skill details: %v", err)
}
if found {
item.SkillInfo = skill
item.SpellID = skill.SpellID
item.SpellTier = int8(skill.SpellTier)
}
return nil
}
// loadRecipeBookDetails loads recipe book information
func (idb *ItemDatabase) loadRecipeBookDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT recipe_id, uses
FROM item_details_recipe_book
WHERE item_id = ?
`
recipe := &RecipeBookInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
recipeID := int32(stmt.ColumnInt64(0))
recipe.Uses = int8(stmt.ColumnInt64(1))
recipe.RecipeID = recipeID
recipe.Recipes = []uint32{uint32(recipeID)} // Add the single recipe
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load recipe book details: %v", err)
}
if found {
item.RecipeBookInfo = recipe
}
return nil
}
// loadFoodDetails loads food/drink information
func (idb *ItemDatabase) loadFoodDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT type, level, duration, satiation
FROM item_details_food
WHERE item_id = ?
`
food := &FoodInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
food.Type = int8(stmt.ColumnInt64(0))
food.Level = int8(stmt.ColumnInt64(1))
food.Duration = float32(stmt.ColumnFloat(2))
food.Satiation = int8(stmt.ColumnInt64(3))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load food details: %v", err)
}
if found {
item.FoodInfo = food
}
return nil
}
// loadBaubleDetails loads bauble information
func (idb *ItemDatabase) loadBaubleDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT cast, recovery, duration, recast, display_slot_optional,
display_cast_time, display_bauble_type, effect_radius,
max_aoe_targets, display_until_cancelled
FROM item_details_bauble
WHERE item_id = ?
`
bauble := &BaubleInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
bauble.Cast = int16(stmt.ColumnInt64(0))
bauble.Recovery = int16(stmt.ColumnInt64(1))
bauble.Duration = int32(stmt.ColumnInt64(2))
bauble.Recast = float32(stmt.ColumnFloat(3))
bauble.DisplaySlotOptional = int8(stmt.ColumnInt64(4))
bauble.DisplayCastTime = int8(stmt.ColumnInt64(5))
bauble.DisplayBaubleType = int8(stmt.ColumnInt64(6))
bauble.EffectRadius = float32(stmt.ColumnFloat(7))
bauble.MaxAOETargets = int32(stmt.ColumnInt64(8))
bauble.DisplayUntilCancelled = int8(stmt.ColumnInt64(9))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load bauble details: %v", err)
}
if found {
item.BaubleInfo = bauble
}
return nil
}
// loadHouseItemDetails loads house item information
func (idb *ItemDatabase) loadHouseItemDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT status_rent_reduction, coin_rent_reduction, house_only, house_location
FROM item_details_house
WHERE item_id = ?
`
house := &HouseItemInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
house.StatusRentReduction = int32(stmt.ColumnInt64(0))
house.CoinRentReduction = float32(stmt.ColumnFloat(1))
house.HouseOnly = int8(stmt.ColumnInt64(2))
// @TODO: Fix HouseLocation field type - should be string, not int8
// house.HouseLocation = stmt.ColumnText(3) // Type mismatch - needs struct field type fix
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load house item details: %v", err)
}
if found {
item.HouseItemInfo = house
}
return nil
}
// loadThrownWeaponDetails loads thrown weapon information
func (idb *ItemDatabase) loadThrownWeaponDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT range_val, damage_modifier, hit_bonus, damage_type
FROM item_details_thrown
WHERE item_id = ?
`
thrown := &ThrownInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
thrown.Range = int32(stmt.ColumnInt64(0))
thrown.DamageModifier = int32(stmt.ColumnInt64(1))
thrown.HitBonus = float32(stmt.ColumnFloat(2))
thrown.DamageType = int32(stmt.ColumnInt64(3))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load thrown weapon details: %v", err)
}
if found {
item.ThrownInfo = thrown
}
return nil
}
// loadHouseContainerDetails loads house container information
func (idb *ItemDatabase) loadHouseContainerDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT allowed_types, num_slots, broker_commission, fence_commission
FROM item_details_house_container
WHERE item_id = ?
`
container := &HouseContainerInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
container.AllowedTypes = int64(stmt.ColumnInt64(0))
container.NumSlots = int8(stmt.ColumnInt64(1))
container.BrokerCommission = int8(stmt.ColumnInt64(2))
container.FenceCommission = int8(stmt.ColumnInt64(3))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load house container details: %v", err)
}
if found {
item.HouseContainerInfo = container
}
return nil
}
// loadBookDetails loads book information
func (idb *ItemDatabase) loadBookDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT language, author, title
FROM item_details_book
WHERE item_id = ?
`
book := &BookInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
book.Language = int8(stmt.ColumnInt64(0))
book.Author = stmt.ColumnText(1)
book.Title = stmt.ColumnText(2)
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load book details: %v", err)
}
if found {
item.BookInfo = book
item.BookLanguage = book.Language
// Load book pages
if err := idb.loadBookPages(item); err != nil {
log.Printf("Error loading book pages for item %d: %v", item.Details.ItemID, err)
}
}
return nil
}
// loadBookPages loads book page content
func (idb *ItemDatabase) loadBookPages(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT page, page_text, page_text_valign, page_text_halign
FROM item_details_book_pages
WHERE item_id = ?
ORDER BY page
`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
var page BookPage
page.Page = int8(stmt.ColumnInt64(0))
page.PageText = stmt.ColumnText(1)
page.VAlign = int8(stmt.ColumnInt64(2))
page.HAlign = int8(stmt.ColumnInt64(3))
item.BookPages = append(item.BookPages, &page)
return nil
},
})
return err
}
// loadAdornmentDetails loads adornment information
func (idb *ItemDatabase) loadAdornmentDetails(item *Item) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT duration, item_types, slot_type
FROM item_details_adornments
WHERE item_id = ?
`
adornment := &AdornmentInfo{}
found := false
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{item.Details.ItemID},
ResultFunc: func(stmt *sqlite.Stmt) error {
adornment.Duration = float32(stmt.ColumnFloat(0))
adornment.ItemTypes = int16(stmt.ColumnInt64(1))
adornment.SlotType = int16(stmt.ColumnInt64(2))
found = true
return nil
},
})
if err != nil {
return fmt.Errorf("failed to load adornment details: %v", err)
}
if found {
item.AdornmentInfo = adornment
}
return nil
}
// LoadItemSets loads item set information
func (idb *ItemDatabase) LoadItemSets(masterList *MasterItemList) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT item_id, item_crc, item_icon, item_stack_size, item_list_color
FROM reward_crate_items
ORDER BY item_id
`
itemSets := make(map[int32][]*ItemSet)
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
var itemSet ItemSet
itemSet.ItemID = int32(stmt.ColumnInt64(0))
itemSet.ItemCRC = int32(stmt.ColumnInt64(1))
itemSet.ItemIcon = int16(stmt.ColumnInt64(2))
itemSet.ItemStackSize = int16(stmt.ColumnInt64(3))
itemSet.ItemListColor = int32(stmt.ColumnInt64(4))
// Add to item sets map
itemSets[itemSet.ItemID] = append(itemSets[itemSet.ItemID], &itemSet)
return nil
},
})
if err != nil {
return fmt.Errorf("failed to query item sets: %v", err)
}
// Associate item sets with items
for itemID, sets := range itemSets {
item := masterList.GetItem(itemID)
if item != nil {
item.ItemSets = sets
}
}
log.Printf("Loaded item sets for %d items", len(itemSets))
return nil
}
// LoadItemClassifications loads item classifications
func (idb *ItemDatabase) LoadItemClassifications(masterList *MasterItemList) error {
ctx := context.Background()
conn, err := idb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer idb.pool.Put(conn)
query := `
SELECT item_id, classification_id, classification_name
FROM item_classifications
ORDER BY item_id
`
classifications := make(map[int32][]*Classifications)
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
itemID := int32(stmt.ColumnInt64(0))
var classification Classifications
classification.ClassificationID = int32(stmt.ColumnInt64(1))
classification.ClassificationName = stmt.ColumnText(2)
classifications[itemID] = append(classifications[itemID], &classification)
return nil
},
})
if err != nil {
return fmt.Errorf("failed to query item classifications: %v", err)
}
// Associate classifications with items
for itemID, classifs := range classifications {
item := masterList.GetItem(itemID)
if item != nil {
item.Classifications = classifs
}
}
log.Printf("Loaded classifications for %d items", len(classifications))
return nil
}

2005
internal/items/items.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,518 +0,0 @@
package loot
import (
"fmt"
"log"
"time"
"eq2emu/internal/items"
)
// ChestInteraction represents the different ways a player can interact with a chest
type ChestInteraction int8
const (
ChestInteractionView ChestInteraction = iota
ChestInteractionLoot
ChestInteractionLootAll
ChestInteractionDisarm
ChestInteractionLockpick
ChestInteractionClose
)
// String returns the string representation of ChestInteraction
func (ci ChestInteraction) String() string {
switch ci {
case ChestInteractionView:
return "view"
case ChestInteractionLoot:
return "loot"
case ChestInteractionLootAll:
return "loot_all"
case ChestInteractionDisarm:
return "disarm"
case ChestInteractionLockpick:
return "lockpick"
case ChestInteractionClose:
return "close"
default:
return "unknown"
}
}
// ChestInteractionResult represents the result of a chest interaction
type ChestInteractionResult struct {
Success bool `json:"success"`
Result int8 `json:"result"` // ChestResult constant
Message string `json:"message"` // Message to display to player
Items []*items.Item `json:"items"` // Items received
Coins int32 `json:"coins"` // Coins received
Experience int32 `json:"experience"` // Experience gained (for disarming/lockpicking)
ChestEmpty bool `json:"chest_empty"` // Whether chest is now empty
ChestClosed bool `json:"chest_closed"` // Whether chest should be closed
}
// ChestService handles treasure chest interactions and management
type ChestService struct {
lootManager *LootManager
playerService PlayerService
zoneService ZoneService
}
// PlayerService interface for player-related operations
type PlayerService interface {
GetPlayerPosition(playerID uint32) (x, y, z, heading float32, zoneID int32, err error)
IsPlayerInCombat(playerID uint32) bool
CanPlayerCarryItems(playerID uint32, itemCount int) bool
AddItemsToPlayer(playerID uint32, items []*items.Item) error
AddCoinsToPlayer(playerID uint32, coins int32) error
GetPlayerSkillValue(playerID uint32, skillName string) int32
AddPlayerExperience(playerID uint32, experience int32, skillName string) error
SendMessageToPlayer(playerID uint32, message string) error
}
// ZoneService interface for zone-related operations
type ZoneService interface {
GetZoneRule(zoneID int32, ruleName string) (any, error)
SpawnObjectInZone(zoneID int32, appearanceID int32, x, y, z, heading float32, name string, commands []string) (int32, error)
RemoveObjectFromZone(zoneID int32, objectID int32) error
GetDistanceBetweenPoints(x1, y1, z1, x2, y2, z2 float32) float32
}
// NewChestService creates a new chest service
func NewChestService(lootManager *LootManager, playerService PlayerService, zoneService ZoneService) *ChestService {
return &ChestService{
lootManager: lootManager,
playerService: playerService,
zoneService: zoneService,
}
}
// CreateTreasureChestFromLoot creates a treasure chest at the specified location with the given loot
func (cs *ChestService) CreateTreasureChestFromLoot(spawnID int32, zoneID int32, x, y, z, heading float32,
lootResult *LootResult, lootRights []uint32) (*TreasureChest, error) {
// Check if treasure chests are enabled in this zone
enabled, err := cs.zoneService.GetZoneRule(zoneID, ConfigTreasureChestEnabled)
if err != nil {
log.Printf("%s Failed to check treasure chest rule for zone %d: %v", LogPrefixChest, zoneID, err)
} else if enabled == false {
log.Printf("%s Treasure chests disabled in zone %d", LogPrefixChest, zoneID)
return nil, nil // Not an error, just disabled
}
// Don't create chest if no loot
if lootResult.IsEmpty() {
log.Printf("%s No loot to put in treasure chest for spawn %d", LogPrefixChest, spawnID)
return nil, nil
}
// Filter items by tier (only common+ items go in chests, matching C++ ITEM_TAG_COMMON)
filteredItems := make([]*items.Item, 0)
for _, item := range lootResult.GetItems() {
if item.Details.Tier >= LootTierCommon {
filteredItems = append(filteredItems, item)
}
}
// Update loot result with filtered items
filteredResult := &LootResult{
Items: filteredItems,
Coins: lootResult.GetCoins(),
}
// Don't create chest if no qualifying items and no coins
if filteredResult.IsEmpty() {
log.Printf("%s No qualifying loot for treasure chest (tier >= %d) for spawn %d",
LogPrefixChest, LootTierCommon, spawnID)
return nil, nil
}
// Create the chest
chest, err := cs.lootManager.CreateTreasureChest(spawnID, zoneID, x, y, z, heading, filteredResult, lootRights)
if err != nil {
return nil, fmt.Errorf("failed to create treasure chest: %v", err)
}
// Spawn the chest object in the zone
chestCommands := []string{"loot", "disarm"} // TODO: Add "lockpick" if chest is locked
objectID, err := cs.zoneService.SpawnObjectInZone(zoneID, chest.AppearanceID, x, y, z, heading,
"Treasure Chest", chestCommands)
if err != nil {
log.Printf("%s Failed to spawn chest object in zone: %v", LogPrefixChest, err)
// Continue anyway, chest exists in memory
} else {
log.Printf("%s Spawned treasure chest object %d in zone %d", LogPrefixChest, objectID, zoneID)
}
return chest, nil
}
// HandleChestInteraction processes a player's interaction with a treasure chest
func (cs *ChestService) HandleChestInteraction(chestID int32, playerID uint32,
interaction ChestInteraction, itemUniqueID int64) *ChestInteractionResult {
result := &ChestInteractionResult{
Success: false,
Items: make([]*items.Item, 0),
}
// Get the chest
chest := cs.lootManager.GetTreasureChest(chestID)
if chest == nil {
result.Result = ChestResultFailed
result.Message = "Treasure chest not found"
return result
}
// Basic validation
if validationResult := cs.validateChestInteraction(chest, playerID); validationResult != nil {
return validationResult
}
// Process the specific interaction
switch interaction {
case ChestInteractionView:
return cs.handleViewChest(chest, playerID)
case ChestInteractionLoot:
return cs.handleLootItem(chest, playerID, itemUniqueID)
case ChestInteractionLootAll:
return cs.handleLootAll(chest, playerID)
case ChestInteractionDisarm:
return cs.handleDisarmChest(chest, playerID)
case ChestInteractionLockpick:
return cs.handleLockpickChest(chest, playerID)
case ChestInteractionClose:
return cs.handleCloseChest(chest, playerID)
default:
result.Result = ChestResultFailed
result.Message = "Unknown chest interaction"
return result
}
}
// validateChestInteraction performs basic validation for chest interactions
func (cs *ChestService) validateChestInteraction(chest *TreasureChest, playerID uint32) *ChestInteractionResult {
// Check loot rights
if !chest.HasLootRights(playerID) {
return &ChestInteractionResult{
Success: false,
Result: ChestResultNoRights,
Message: "You do not have rights to loot this chest",
}
}
// Check if player is in combat
if cs.playerService.IsPlayerInCombat(playerID) {
return &ChestInteractionResult{
Success: false,
Result: ChestResultInCombat,
Message: "You cannot loot while in combat",
}
}
// Check distance
px, py, pz, _, pZoneID, err := cs.playerService.GetPlayerPosition(playerID)
if err != nil {
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: "Failed to get player position",
}
}
if pZoneID != chest.ZoneID {
return &ChestInteractionResult{
Success: false,
Result: ChestResultTooFar,
Message: "You are too far from the chest",
}
}
distance := cs.zoneService.GetDistanceBetweenPoints(px, py, pz, chest.X, chest.Y, chest.Z)
if distance > 10.0 { // TODO: Make this configurable
return &ChestInteractionResult{
Success: false,
Result: ChestResultTooFar,
Message: "You are too far from the chest",
}
}
// Check if chest is locked
if chest.IsLocked {
return &ChestInteractionResult{
Success: false,
Result: ChestResultLocked,
Message: "The chest is locked",
}
}
// Check if chest is trapped
if chest.IsDisarmable {
return &ChestInteractionResult{
Success: false,
Result: ChestResultTrapped,
Message: "The chest appears to be trapped",
}
}
return nil // Validation passed
}
// handleViewChest handles viewing chest contents
func (cs *ChestService) handleViewChest(chest *TreasureChest, playerID uint32) *ChestInteractionResult {
if chest.LootResult.IsEmpty() {
return &ChestInteractionResult{
Success: true,
Result: ChestResultEmpty,
Message: "The chest is empty",
ChestEmpty: true,
}
}
return &ChestInteractionResult{
Success: true,
Result: ChestResultSuccess,
Message: fmt.Sprintf("The chest contains %d items and %d coins",
len(chest.LootResult.GetItems()), chest.LootResult.GetCoins()),
Items: chest.LootResult.GetItems(),
Coins: chest.LootResult.GetCoins(),
}
}
// handleLootItem handles looting a specific item from the chest
func (cs *ChestService) handleLootItem(chest *TreasureChest, playerID uint32, itemUniqueID int64) *ChestInteractionResult {
// Check if player can carry more items
if !cs.playerService.CanPlayerCarryItems(playerID, 1) {
return &ChestInteractionResult{
Success: false,
Result: ChestResultCantCarry,
Message: "Your inventory is full",
}
}
// Loot the specific item
item, err := cs.lootManager.LootChestItem(chest.ID, playerID, itemUniqueID)
if err != nil {
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: fmt.Sprintf("Failed to loot item: %v", err),
}
}
// Add item to player's inventory
if err := cs.playerService.AddItemsToPlayer(playerID, []*items.Item{item}); err != nil {
log.Printf("%s Failed to add looted item to player %d: %v", LogPrefixChest, playerID, err)
// TODO: Put item back in chest?
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: "Failed to add item to inventory",
}
}
// Send message to player
message := fmt.Sprintf("You looted %s", item.Name)
cs.playerService.SendMessageToPlayer(playerID, message)
return &ChestInteractionResult{
Success: true,
Result: ChestResultSuccess,
Message: message,
Items: []*items.Item{item},
ChestEmpty: cs.lootManager.IsChestEmpty(chest.ID),
}
}
// handleLootAll handles looting all items and coins from the chest
func (cs *ChestService) handleLootAll(chest *TreasureChest, playerID uint32) *ChestInteractionResult {
lootResult, err := cs.lootManager.LootChestAll(chest.ID, playerID)
if err != nil {
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: fmt.Sprintf("Failed to loot chest: %v", err),
}
}
if lootResult.IsEmpty() {
return &ChestInteractionResult{
Success: true,
Result: ChestResultEmpty,
Message: "The chest is empty",
ChestEmpty: true,
}
}
// Check if player can carry all items
if !cs.playerService.CanPlayerCarryItems(playerID, len(lootResult.Items)) {
// TODO: Partial loot or put items back?
return &ChestInteractionResult{
Success: false,
Result: ChestResultCantCarry,
Message: "Your inventory is full",
}
}
// Add items to player's inventory
if len(lootResult.Items) > 0 {
if err := cs.playerService.AddItemsToPlayer(playerID, lootResult.Items); err != nil {
log.Printf("%s Failed to add looted items to player %d: %v", LogPrefixChest, playerID, err)
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: "Failed to add items to inventory",
}
}
}
// Add coins to player
if lootResult.Coins > 0 {
if err := cs.playerService.AddCoinsToPlayer(playerID, lootResult.Coins); err != nil {
log.Printf("%s Failed to add looted coins to player %d: %v", LogPrefixChest, playerID, err)
}
}
// Send message to player
message := fmt.Sprintf("You looted %d items and %d coins", len(lootResult.Items), lootResult.Coins)
cs.playerService.SendMessageToPlayer(playerID, message)
return &ChestInteractionResult{
Success: true,
Result: ChestResultSuccess,
Message: message,
Items: lootResult.Items,
Coins: lootResult.Coins,
ChestEmpty: true,
}
}
// handleDisarmChest handles disarming a trapped chest
func (cs *ChestService) handleDisarmChest(chest *TreasureChest, playerID uint32) *ChestInteractionResult {
if !chest.IsDisarmable {
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: "This chest is not trapped",
}
}
// Get player's disarm skill
disarmSkill := cs.playerService.GetPlayerSkillValue(playerID, "Disarm Trap")
// Calculate success chance (simplified)
successChance := float32(disarmSkill) - float32(chest.DisarmDifficulty)
if successChance < 0 {
successChance = 0
} else if successChance > 95 {
successChance = 95
}
// Roll for success
roll := float32(time.Now().UnixNano() % 100) // Simple random
if roll > successChance {
// Failed disarm - could trigger trap effects here
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: "You failed to disarm the trap",
}
}
// Success - disarm the trap
chest.IsDisarmable = false
// Give experience
experience := int32(chest.DisarmDifficulty * 10) // 10 exp per difficulty point
cs.playerService.AddPlayerExperience(playerID, experience, "Disarm Trap")
message := "You successfully disarmed the trap"
cs.playerService.SendMessageToPlayer(playerID, message)
return &ChestInteractionResult{
Success: true,
Result: ChestResultSuccess,
Message: message,
Experience: experience,
}
}
// handleLockpickChest handles picking a locked chest
func (cs *ChestService) handleLockpickChest(chest *TreasureChest, playerID uint32) *ChestInteractionResult {
if !chest.IsLocked {
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: "This chest is not locked",
}
}
// Get player's lockpicking skill
lockpickSkill := cs.playerService.GetPlayerSkillValue(playerID, "Pick Lock")
// Calculate success chance (simplified)
successChance := float32(lockpickSkill) - float32(chest.LockpickDifficulty)
if successChance < 0 {
successChance = 0
} else if successChance > 95 {
successChance = 95
}
// Roll for success
roll := float32(time.Now().UnixNano() % 100) // Simple random
if roll > successChance {
return &ChestInteractionResult{
Success: false,
Result: ChestResultFailed,
Message: "You failed to pick the lock",
}
}
// Success - unlock the chest
chest.IsLocked = false
// Give experience
experience := int32(chest.LockpickDifficulty * 10) // 10 exp per difficulty point
cs.playerService.AddPlayerExperience(playerID, experience, "Pick Lock")
message := "You successfully picked the lock"
cs.playerService.SendMessageToPlayer(playerID, message)
return &ChestInteractionResult{
Success: true,
Result: ChestResultSuccess,
Message: message,
Experience: experience,
}
}
// handleCloseChest handles closing the chest interface
func (cs *ChestService) handleCloseChest(chest *TreasureChest, playerID uint32) *ChestInteractionResult {
return &ChestInteractionResult{
Success: true,
Result: ChestResultSuccess,
Message: "Closed chest",
ChestClosed: true,
}
}
// CleanupEmptyChests removes empty chests from zones
func (cs *ChestService) CleanupEmptyChests(zoneID int32) {
chests := cs.lootManager.GetZoneChests(zoneID)
for _, chest := range chests {
if chest.LootResult.IsEmpty() {
// Remove from zone
cs.zoneService.RemoveObjectFromZone(zoneID, chest.ID)
// Remove from loot manager
cs.lootManager.RemoveTreasureChest(chest.ID)
}
}
}
// GetPlayerChestList returns a list of chests a player can access
func (cs *ChestService) GetPlayerChestList(playerID uint32) []*TreasureChest {
return cs.lootManager.GetPlayerChests(playerID)
}

View File

@ -1,199 +0,0 @@
package loot
// Loot tier constants based on EQ2 item quality system
const (
LootTierTrash int8 = 0 // Gray items
LootTierCommon int8 = 1 // White items
LootTierUncommon int8 = 2 // Green items
LootTierTreasured int8 = 3 // Blue items
LootTierRare int8 = 4 // Purple items
LootTierLegendary int8 = 5 // Orange items
LootTierFabled int8 = 6 // Yellow items
LootTierMythical int8 = 7 // Red items
LootTierArtifact int8 = 8 // Artifact items
LootTierRelic int8 = 9 // Relic items
LootTierUltimate int8 = 10 // Ultimate items
)
// Chest appearance IDs from the C++ implementation
const (
ChestAppearanceSmall int32 = 4034 // Small chest for common+ items
ChestAppearanceTreasure int32 = 5864 // Treasure chest for treasured+ items
ChestAppearanceOrnate int32 = 5865 // Ornate chest for legendary+ items
ChestAppearanceExquisite int32 = 4015 // Exquisite chest for fabled+ items
)
// Loot generation constants
const (
DefaultMaxLootItems int16 = 6 // Default maximum items per loot
DefaultLootDropProbability float32 = 100.0 // Default probability for loot to drop
DefaultCoinProbability float32 = 50.0 // Default probability for coin drops
MaxGlobalLootTables int = 1000 // Maximum number of global loot tables
)
// Database table names
const (
TableLootTable = "loottable"
TableLootDrop = "lootdrop"
TableSpawnLoot = "spawn_loot"
TableLootGlobal = "loot_global"
TableLootTables = "loot_tables" // Alternative name
TableLootDrops = "loot_drops" // Alternative name
TableSpawnLootList = "spawn_loot_list" // Alternative name
)
// Database column names for loot tables
const (
ColLootTableID = "id"
ColLootTableName = "name"
ColLootTableMinCoin = "mincoin"
ColLootTableMaxCoin = "maxcoin"
ColLootTableMaxItems = "maxlootitems"
ColLootTableDropProb = "lootdrop_probability"
ColLootTableCoinProb = "coin_probability"
)
// Database column names for loot drops
const (
ColLootDropTableID = "loot_table_id"
ColLootDropItemID = "item_id"
ColLootDropCharges = "item_charges"
ColLootDropEquip = "equip_item"
ColLootDropProb = "probability"
ColLootDropQuestID = "no_drop_quest_completed_id"
)
// Database column names for spawn loot
const (
ColSpawnLootSpawnID = "spawn_id"
ColSpawnLootTableID = "loottable_id"
)
// Database column names for global loot
const (
ColGlobalLootType = "type"
ColGlobalLootTable = "loot_table"
ColGlobalLootValue1 = "value1"
ColGlobalLootValue2 = "value2"
ColGlobalLootValue3 = "value3"
ColGlobalLootValue4 = "value4"
)
// Loot flags and special values
const (
LootFlagNoTrade uint32 = 1 << 0 // Item cannot be traded
LootFlagHeirloom uint32 = 1 << 1 // Item is heirloom (account bound)
LootFlagTemporary uint32 = 1 << 2 // Item is temporary
LootFlagNoValue uint32 = 1 << 3 // Item has no coin value
LootFlagNoZone uint32 = 1 << 4 // Item cannot leave zone
LootFlagNoDestroy uint32 = 1 << 5 // Item cannot be destroyed
LootFlagCrafted uint32 = 1 << 6 // Item is crafted
LootFlagArtisan uint32 = 1 << 7 // Item requires artisan skill
LootFlagAntique uint32 = 1 << 8 // Item is antique
LootFlagMagic uint32 = 1 << 9 // Item is magic
LootFlagLegendary uint32 = 1 << 10 // Item is legendary
LootFlagDroppable uint32 = 1 << 11 // Item can be dropped
LootFlagEquipped uint32 = 1 << 12 // Item starts equipped
LootFlagVisible uint32 = 1 << 13 // Item is visible
LootFlagUnique uint32 = 1 << 14 // Only one can be owned
LootFlagLore uint32 = 1 << 15 // Item has lore restrictions
)
// Special loot table IDs
const (
LootTableIDNone int32 = 0 // No loot table
LootTableIDGlobal int32 = -1 // Global loot table marker
LootTableIDLevel int32 = -2 // Level-based global loot
LootTableIDRace int32 = -3 // Race-based global loot
LootTableIDZone int32 = -4 // Zone-based global loot
)
// Loot command types
const (
LootCommandView = "view" // View chest contents
LootCommandTake = "take" // Take specific item
LootCommandTakeAll = "take_all" // Take all items
LootCommandClose = "close" // Close loot window
LootCommandDisarm = "disarm" // Disarm chest trap
LootCommandLockpick = "lockpick" // Pick chest lock
)
// Chest interaction results
const (
ChestResultSuccess = 0 // Operation successful
ChestResultLocked = 1 // Chest is locked
ChestResultTrapped = 2 // Chest is trapped
ChestResultNoRights = 3 // No loot rights
ChestResultEmpty = 4 // Chest is empty
ChestResultFailed = 5 // Operation failed
ChestResultCantCarry = 6 // Cannot carry more items
ChestResultTooFar = 7 // Too far from chest
ChestResultInCombat = 8 // Cannot loot while in combat
)
// Loot distribution methods
const (
LootDistributionNone = 0 // No automatic distribution
LootDistributionFreeForAll = 1 // Anyone can loot
LootDistributionRoundRobin = 2 // Round robin distribution
LootDistributionMasterLoot = 3 // Master looter decides
LootDistributionNeedGreed = 4 // Need before greed system
LootDistributionLotto = 5 // Random lotto system
)
// Loot quality thresholds for different distribution methods
const (
NeedGreedThreshold int8 = LootTierTreasured // Blue+ items use need/greed
MasterLootThreshold int8 = LootTierRare // Purple+ items go to master looter
LottoThreshold int8 = LootTierLegendary // Orange+ items use lotto system
)
// Chest spawn duration and cleanup
const (
ChestDespawnTime = 300 // Seconds before chest despawns (5 minutes)
ChestCleanupTime = 600 // Seconds before chest is force-cleaned (10 minutes)
MaxChestsPerZone = 100 // Maximum number of chests per zone
MaxChestsPerPlayer = 10 // Maximum number of chests a player can have loot rights to
)
// Probability calculation constants
const (
ProbabilityMax float32 = 100.0 // Maximum probability percentage
ProbabilityMin float32 = 0.0 // Minimum probability percentage
ProbabilityDefault float32 = 50.0 // Default probability for items
)
// Error messages
const (
ErrLootTableNotFound = "loot table not found"
ErrNoLootRights = "no loot rights for this chest"
ErrChestLocked = "chest is locked"
ErrChestTrapped = "chest is trapped"
ErrInventoryFull = "inventory is full"
ErrTooFarFromChest = "too far from chest"
ErrInCombat = "cannot loot while in combat"
ErrInvalidLootTable = "invalid loot table"
ErrInvalidItem = "invalid item in loot table"
ErrDatabaseError = "database error during loot operation"
)
// Logging prefixes
const (
LogPrefixLoot = "[LOOT]"
LogPrefixChest = "[CHEST]"
LogPrefixDatabase = "[LOOT-DB]"
LogPrefixGeneration = "[LOOT-GEN]"
)
// Configuration keys for loot system
const (
ConfigTreasureChestEnabled = "treasure_chest_enabled"
ConfigGlobalLootEnabled = "global_loot_enabled"
ConfigLootStatisticsEnabled = "loot_statistics_enabled"
ConfigChestDespawnTime = "chest_despawn_time"
ConfigMaxChestsPerZone = "max_chests_per_zone"
ConfigDefaultLootProbability = "default_loot_probability"
ConfigDefaultCoinProbability = "default_coin_probability"
ConfigLootDistanceCheck = "loot_distance_check"
ConfigLootCombatCheck = "loot_combat_check"
)

View File

@ -1,612 +0,0 @@
package loot
import (
"context"
"fmt"
"log"
"sync"
"time"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// LootDatabase handles all database operations for the loot system
type LootDatabase struct {
pool *sqlitex.Pool
lootTables map[int32]*LootTable
spawnLoot map[int32][]int32 // spawn_id -> []loot_table_id
globalLoot []*GlobalLoot
mutex sync.RWMutex
}
// NewLootDatabase creates a new loot database manager
func NewLootDatabase(pool *sqlitex.Pool) *LootDatabase {
ldb := &LootDatabase{
pool: pool,
lootTables: make(map[int32]*LootTable),
spawnLoot: make(map[int32][]int32),
globalLoot: make([]*GlobalLoot, 0),
}
return ldb
}
// LoadAllLootData loads all loot data from the database
func (ldb *LootDatabase) LoadAllLootData() error {
log.Printf("%s Loading loot data from database...", LogPrefixDatabase)
// Load loot tables first
if err := ldb.loadLootTables(); err != nil {
return fmt.Errorf("failed to load loot tables: %v", err)
}
// Load loot drops for each table
if err := ldb.loadLootDrops(); err != nil {
return fmt.Errorf("failed to load loot drops: %v", err)
}
// Load spawn loot assignments
if err := ldb.loadSpawnLoot(); err != nil {
return fmt.Errorf("failed to load spawn loot: %v", err)
}
// Load global loot configuration
if err := ldb.loadGlobalLoot(); err != nil {
return fmt.Errorf("failed to load global loot: %v", err)
}
ldb.mutex.RLock()
tableCount := len(ldb.lootTables)
spawnCount := len(ldb.spawnLoot)
globalCount := len(ldb.globalLoot)
ldb.mutex.RUnlock()
log.Printf("%s Loaded %d loot tables, %d spawn assignments, %d global loot entries",
LogPrefixDatabase, tableCount, spawnCount, globalCount)
return nil
}
// loadLootTables loads all loot tables from the database
func (ldb *LootDatabase) loadLootTables() error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
query := `
SELECT id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability
FROM loottable
ORDER BY id
`
ldb.mutex.Lock()
defer ldb.mutex.Unlock()
// Clear existing tables
ldb.lootTables = make(map[int32]*LootTable)
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
table := &LootTable{
Drops: make([]*LootDrop, 0),
}
table.ID = int32(stmt.ColumnInt64(0))
table.Name = stmt.ColumnText(1)
table.MinCoin = int32(stmt.ColumnInt64(2))
table.MaxCoin = int32(stmt.ColumnInt64(3))
table.MaxLootItems = int16(stmt.ColumnInt64(4))
table.LootDropProbability = float32(stmt.ColumnFloat(5))
table.CoinProbability = float32(stmt.ColumnFloat(6))
ldb.lootTables[table.ID] = table
return nil
},
})
return err
}
// loadLootDrops loads all loot drops for the loaded loot tables
func (ldb *LootDatabase) loadLootDrops() error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
query := `
SELECT loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id
FROM lootdrop
WHERE loot_table_id = ?
ORDER BY probability DESC
`
ldb.mutex.Lock()
defer ldb.mutex.Unlock()
for tableID, table := range ldb.lootTables {
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{tableID},
ResultFunc: func(stmt *sqlite.Stmt) error {
drop := &LootDrop{}
drop.LootTableID = int32(stmt.ColumnInt64(0))
drop.ItemID = int32(stmt.ColumnInt64(1))
drop.ItemCharges = int16(stmt.ColumnInt64(2))
equipItem := int8(stmt.ColumnInt64(3))
drop.Probability = float32(stmt.ColumnFloat(4))
drop.NoDropQuestCompletedID = int32(stmt.ColumnInt64(5))
drop.EquipItem = equipItem == 1
table.Drops = append(table.Drops, drop)
return nil
},
})
if err != nil {
log.Printf("%s Failed to query loot drops for table %d: %v", LogPrefixDatabase, tableID, err)
}
}
return nil
}
// loadSpawnLoot loads spawn to loot table assignments
func (ldb *LootDatabase) loadSpawnLoot() error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
query := `
SELECT spawn_id, loottable_id
FROM spawn_loot
ORDER BY spawn_id
`
ldb.mutex.Lock()
defer ldb.mutex.Unlock()
// Clear existing spawn loot
ldb.spawnLoot = make(map[int32][]int32)
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
spawnID := int32(stmt.ColumnInt64(0))
lootTableID := int32(stmt.ColumnInt64(1))
ldb.spawnLoot[spawnID] = append(ldb.spawnLoot[spawnID], lootTableID)
return nil
},
})
return err
}
// loadGlobalLoot loads global loot configuration
func (ldb *LootDatabase) loadGlobalLoot() error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
query := `
SELECT type, loot_table, value1, value2, value3, value4
FROM loot_global
ORDER BY type, value1
`
ldb.mutex.Lock()
defer ldb.mutex.Unlock()
// Clear existing global loot
ldb.globalLoot = make([]*GlobalLoot, 0)
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
lootType := stmt.ColumnText(0)
tableID := int32(stmt.ColumnInt64(1))
value1 := int32(stmt.ColumnInt64(2))
value2 := int32(stmt.ColumnInt64(3))
value3 := int32(stmt.ColumnInt64(4))
// value4 := int32(stmt.ColumnInt64(5)) // unused
global := &GlobalLoot{
TableID: tableID,
}
// Parse loot type and values
switch lootType {
case "level":
global.Type = GlobalLootTypeLevel
global.MinLevel = int8(value1)
global.MaxLevel = int8(value2)
global.LootTier = value3
case "race":
global.Type = GlobalLootTypeRace
global.Race = int16(value1)
global.LootTier = value2
case "zone":
global.Type = GlobalLootTypeZone
global.ZoneID = value1
global.LootTier = value2
default:
log.Printf("%s Unknown global loot type: %s", LogPrefixDatabase, lootType)
return nil // Continue processing
}
ldb.globalLoot = append(ldb.globalLoot, global)
return nil
},
})
return err
}
// GetLootTable returns a loot table by ID (thread-safe)
func (ldb *LootDatabase) GetLootTable(tableID int32) *LootTable {
ldb.mutex.RLock()
defer ldb.mutex.RUnlock()
return ldb.lootTables[tableID]
}
// GetSpawnLootTables returns all loot table IDs for a spawn (thread-safe)
func (ldb *LootDatabase) GetSpawnLootTables(spawnID int32) []int32 {
ldb.mutex.RLock()
defer ldb.mutex.RUnlock()
tables := ldb.spawnLoot[spawnID]
if tables == nil {
return nil
}
// Return a copy to prevent external modification
result := make([]int32, len(tables))
copy(result, tables)
return result
}
// GetGlobalLootTables returns applicable global loot tables for given parameters
func (ldb *LootDatabase) GetGlobalLootTables(level int16, race int16, zoneID int32) []*GlobalLoot {
ldb.mutex.RLock()
defer ldb.mutex.RUnlock()
var result []*GlobalLoot
for _, global := range ldb.globalLoot {
switch global.Type {
case GlobalLootTypeLevel:
if level >= int16(global.MinLevel) && level <= int16(global.MaxLevel) {
result = append(result, global)
}
case GlobalLootTypeRace:
if race == global.Race {
result = append(result, global)
}
case GlobalLootTypeZone:
if zoneID == global.ZoneID {
result = append(result, global)
}
}
}
return result
}
// AddLootTable adds a new loot table to the database
func (ldb *LootDatabase) AddLootTable(table *LootTable) error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
// Use a savepoint for transaction support
defer sqlitex.Save(conn)(&err)
query := `INSERT INTO loottable (id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability) VALUES (?, ?, ?, ?, ?, ?, ?)`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{
table.ID,
table.Name,
table.MinCoin,
table.MaxCoin,
table.MaxLootItems,
table.LootDropProbability,
table.CoinProbability,
},
})
if err != nil {
return fmt.Errorf("failed to insert loot table: %v", err)
}
// Add drops if any
for _, drop := range table.Drops {
if err := ldb.addLootDropWithConn(conn, drop); err != nil {
log.Printf("%s Failed to add loot drop for table %d: %v", LogPrefixDatabase, table.ID, err)
}
}
// Update in-memory cache
ldb.mutex.Lock()
ldb.lootTables[table.ID] = table
ldb.mutex.Unlock()
log.Printf("%s Added loot table %d (%s) with %d drops", LogPrefixDatabase, table.ID, table.Name, len(table.Drops))
return nil
}
// AddLootDrop adds a new loot drop to the database
func (ldb *LootDatabase) AddLootDrop(drop *LootDrop) error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
return ldb.addLootDropWithConn(conn, drop)
}
// addLootDropWithConn adds a loot drop using an existing connection
func (ldb *LootDatabase) addLootDropWithConn(conn *sqlite.Conn, drop *LootDrop) error {
equipItem := int8(0)
if drop.EquipItem {
equipItem = 1
}
query := `INSERT INTO lootdrop (loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id) VALUES (?, ?, ?, ?, ?, ?)`
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{
drop.LootTableID,
drop.ItemID,
drop.ItemCharges,
equipItem,
drop.Probability,
drop.NoDropQuestCompletedID,
},
})
return err
}
// UpdateLootTable updates an existing loot table
func (ldb *LootDatabase) UpdateLootTable(table *LootTable) error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
// Use a savepoint for transaction support
defer sqlitex.Save(conn)(&err)
updateQuery := `UPDATE loottable SET name = ?, mincoin = ?, maxcoin = ?, maxlootitems = ?, lootdrop_probability = ?, coin_probability = ? WHERE id = ?`
err = sqlitex.Execute(conn, updateQuery, &sqlitex.ExecOptions{
Args: []any{
table.Name,
table.MinCoin,
table.MaxCoin,
table.MaxLootItems,
table.LootDropProbability,
table.CoinProbability,
table.ID,
},
})
if err != nil {
return fmt.Errorf("failed to update loot table: %v", err)
}
// Update drops - delete old ones and insert new ones
if err := ldb.deleteLootDropsWithConn(conn, table.ID); err != nil {
log.Printf("%s Failed to delete old loot drops for table %d: %v", LogPrefixDatabase, table.ID, err)
}
for _, drop := range table.Drops {
if err := ldb.addLootDropWithConn(conn, drop); err != nil {
log.Printf("%s Failed to add updated loot drop for table %d: %v", LogPrefixDatabase, table.ID, err)
}
}
// Update in-memory cache
ldb.mutex.Lock()
ldb.lootTables[table.ID] = table
ldb.mutex.Unlock()
return nil
}
// DeleteLootTable removes a loot table and all its drops
func (ldb *LootDatabase) DeleteLootTable(tableID int32) error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
// Use a savepoint for transaction support
defer sqlitex.Save(conn)(&err)
// Delete drops first
if err := ldb.deleteLootDropsWithConn(conn, tableID); err != nil {
return fmt.Errorf("failed to delete loot drops: %v", err)
}
// Delete table
query := `DELETE FROM loottable WHERE id = ?`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{tableID},
})
if err != nil {
return fmt.Errorf("failed to delete loot table: %v", err)
}
// Remove from in-memory cache
ldb.mutex.Lock()
delete(ldb.lootTables, tableID)
ldb.mutex.Unlock()
return nil
}
// DeleteLootDrops removes all drops for a loot table
func (ldb *LootDatabase) DeleteLootDrops(tableID int32) error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
return ldb.deleteLootDropsWithConn(conn, tableID)
}
// deleteLootDropsWithConn removes all drops for a loot table using an existing connection
func (ldb *LootDatabase) deleteLootDropsWithConn(conn *sqlite.Conn, tableID int32) error {
query := `DELETE FROM lootdrop WHERE loot_table_id = ?`
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{tableID},
})
return err
}
// AddSpawnLoot assigns a loot table to a spawn
func (ldb *LootDatabase) AddSpawnLoot(spawnID, tableID int32) error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
query := `INSERT OR REPLACE INTO spawn_loot (spawn_id, loottable_id) VALUES (?, ?)`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{spawnID, tableID},
})
if err != nil {
return fmt.Errorf("failed to insert spawn loot: %v", err)
}
// Update in-memory cache
ldb.mutex.Lock()
ldb.spawnLoot[spawnID] = append(ldb.spawnLoot[spawnID], tableID)
ldb.mutex.Unlock()
return nil
}
// DeleteSpawnLoot removes all loot table assignments for a spawn
func (ldb *LootDatabase) DeleteSpawnLoot(spawnID int32) error {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
query := `DELETE FROM spawn_loot WHERE spawn_id = ?`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{spawnID},
})
if err != nil {
return fmt.Errorf("failed to delete spawn loot: %v", err)
}
// Remove from in-memory cache
ldb.mutex.Lock()
delete(ldb.spawnLoot, spawnID)
ldb.mutex.Unlock()
return nil
}
// GetLootStatistics returns database statistics
func (ldb *LootDatabase) GetLootStatistics() (map[string]any, error) {
ctx := context.Background()
conn, err := ldb.pool.Take(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get database connection: %w", err)
}
defer ldb.pool.Put(conn)
stats := make(map[string]any)
// Count loot tables
var count int
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM loottable", &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
count = int(stmt.ColumnInt64(0))
return nil
},
})
if err == nil {
stats["loot_tables"] = count
}
// Count loot drops
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM lootdrop", &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
count = int(stmt.ColumnInt64(0))
return nil
},
})
if err == nil {
stats["loot_drops"] = count
}
// Count spawn loot assignments
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM spawn_loot", &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
count = int(stmt.ColumnInt64(0))
return nil
},
})
if err == nil {
stats["spawn_loot_assignments"] = count
}
// In-memory statistics
ldb.mutex.RLock()
stats["cached_loot_tables"] = len(ldb.lootTables)
stats["cached_spawn_assignments"] = len(ldb.spawnLoot)
stats["cached_global_loot"] = len(ldb.globalLoot)
ldb.mutex.RUnlock()
stats["loaded_at"] = time.Now().Format(time.RFC3339)
return stats, nil
}
// ReloadLootData reloads all loot data from the database
func (ldb *LootDatabase) ReloadLootData() error {
log.Printf("%s Reloading loot data from database...", LogPrefixDatabase)
return ldb.LoadAllLootData()
}
// Close closes the database pool
func (ldb *LootDatabase) Close() error {
if ldb.pool != nil {
return ldb.pool.Close()
}
return nil
}

View File

@ -1,433 +0,0 @@
package loot
import (
"context"
"fmt"
"log"
// @TODO: Fix MasterItemListService type import - temporarily commented out
// "eq2emu/internal/items"
"zombiezen.com/go/sqlite/sqlitex"
)
// LootSystem represents the complete loot system integration
type LootSystem struct {
Database *LootDatabase
Manager *LootManager
ChestService *ChestService
PacketService *LootPacketService
}
// LootSystemConfig holds configuration for the loot system
type LootSystemConfig struct {
DatabasePool *sqlitex.Pool
// @TODO: Fix MasterItemListService type import
ItemMasterList any // was items.MasterItemListService
PlayerService PlayerService
ZoneService ZoneService
ClientService ClientService
ItemPacketBuilder ItemPacketBuilder
StartCleanupTimer bool
}
// NewLootSystem creates a complete loot system with all components
func NewLootSystem(config *LootSystemConfig) (*LootSystem, error) {
if config.DatabasePool == nil {
return nil, fmt.Errorf("database pool is required")
}
if config.ItemMasterList == nil {
return nil, fmt.Errorf("item master list is required")
}
// Create database layer
database := NewLootDatabase(config.DatabasePool)
// Load loot data
if err := database.LoadAllLootData(); err != nil {
return nil, fmt.Errorf("failed to load loot data: %v", err)
}
// Create loot manager
manager := NewLootManager(database, config.ItemMasterList)
// Create chest service (optional - requires player and zone services)
var chestService *ChestService
if config.PlayerService != nil && config.ZoneService != nil {
chestService = NewChestService(manager, config.PlayerService, config.ZoneService)
}
// Create packet service (optional - requires client and item packet builder)
var packetService *LootPacketService
if config.ClientService != nil && config.ItemPacketBuilder != nil {
packetBuilder := NewLootPacketBuilder(config.ItemPacketBuilder)
packetService = NewLootPacketService(packetBuilder, config.ClientService)
}
// Start cleanup timer if requested
if config.StartCleanupTimer {
manager.StartCleanupTimer()
}
system := &LootSystem{
Database: database,
Manager: manager,
ChestService: chestService,
PacketService: packetService,
}
log.Printf("%s Loot system initialized successfully", LogPrefixLoot)
return system, nil
}
// GenerateAndCreateChest generates loot for a spawn and creates a treasure chest
func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, z, heading float32,
context *LootContext) (*TreasureChest, error) {
if ls.ChestService == nil {
return nil, fmt.Errorf("chest service not available")
}
// Generate loot
lootResult, err := ls.Manager.GenerateLoot(spawnID, context)
if err != nil {
return nil, fmt.Errorf("failed to generate loot: %v", err)
}
// Don't create chest if no loot
if lootResult.IsEmpty() {
log.Printf("%s No loot generated for spawn %d, not creating chest", LogPrefixLoot, spawnID)
return nil, nil
}
// Create treasure chest
chest, err := ls.ChestService.CreateTreasureChestFromLoot(spawnID, zoneID, x, y, z, heading,
lootResult, context.GroupMembers)
if err != nil {
return nil, fmt.Errorf("failed to create treasure chest: %v", err)
}
return chest, nil
}
// HandlePlayerLootInteraction handles a player's interaction with a chest and sends appropriate packets
func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32,
interaction ChestInteraction, itemUniqueID int64) error {
if ls.ChestService == nil {
return fmt.Errorf("chest service not available")
}
// Handle the interaction
result := ls.ChestService.HandleChestInteraction(chestID, playerID, interaction, itemUniqueID)
// Send response packet if packet service is available
if ls.PacketService != nil {
if err := ls.PacketService.SendLootResponse(result, playerID); err != nil {
log.Printf("%s Failed to send loot response packet: %v", LogPrefixLoot, err)
}
// Send updated loot window if chest is still open and has items
if result.Success && !result.ChestClosed {
chest := ls.Manager.GetTreasureChest(chestID)
if chest != nil && !chest.LootResult.IsEmpty() {
if err := ls.PacketService.SendLootUpdate(chest, playerID); err != nil {
log.Printf("%s Failed to send loot update packet: %v", LogPrefixLoot, err)
}
} else if chest != nil && chest.LootResult.IsEmpty() {
// Send stopped looting packet for empty chest
if err := ls.PacketService.SendStoppedLooting(chestID, playerID); err != nil {
log.Printf("%s Failed to send stopped looting packet: %v", LogPrefixLoot, err)
}
}
}
}
// Log the interaction
log.Printf("%s Player %d %s chest %d: %s",
LogPrefixLoot, playerID, interaction.String(), chestID, result.Message)
return nil
}
// ShowChestToPlayer sends the loot window to a player
func (ls *LootSystem) ShowChestToPlayer(chestID int32, playerID uint32) error {
if ls.PacketService == nil {
return fmt.Errorf("packet service not available")
}
chest := ls.Manager.GetTreasureChest(chestID)
if chest == nil {
return fmt.Errorf("chest %d not found", chestID)
}
// Check loot rights
if !chest.HasLootRights(playerID) {
return fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID)
}
// Send loot update packet
return ls.PacketService.SendLootUpdate(chest, playerID)
}
// GetSystemStatistics returns comprehensive statistics about the loot system
func (ls *LootSystem) GetSystemStatistics() (map[string]any, error) {
stats := make(map[string]any)
// Database statistics
if dbStats, err := ls.Database.GetLootStatistics(); err == nil {
stats["database"] = dbStats
}
// Manager statistics
stats["generation"] = ls.Manager.GetStatistics()
// Active chests count
chestCount := 0
ls.Manager.mutex.RLock()
chestCount = len(ls.Manager.treasureChests)
ls.Manager.mutex.RUnlock()
stats["active_chests"] = chestCount
return stats, nil
}
// ReloadAllData reloads all loot data from the database
func (ls *LootSystem) ReloadAllData() error {
log.Printf("%s Reloading all loot system data", LogPrefixLoot)
return ls.Database.LoadAllLootData()
}
// Shutdown gracefully shuts down the loot system
func (ls *LootSystem) Shutdown() error {
log.Printf("%s Shutting down loot system", LogPrefixLoot)
// Close database connections
if err := ls.Database.Close(); err != nil {
log.Printf("%s Error closing database: %v", LogPrefixLoot, err)
return err
}
// Clear active chests
ls.Manager.mutex.Lock()
ls.Manager.treasureChests = make(map[int32]*TreasureChest)
ls.Manager.mutex.Unlock()
log.Printf("%s Loot system shutdown complete", LogPrefixLoot)
return nil
}
// AddLootTableWithDrops adds a complete loot table with drops in a single transaction
func (ls *LootSystem) AddLootTableWithDrops(table *LootTable) error {
return ls.Database.AddLootTable(table)
}
// CreateQuickLootTable creates a simple loot table with basic parameters
func (ls *LootSystem) CreateQuickLootTable(tableID int32, name string, items []QuickLootItem,
minCoin, maxCoin int32, maxItems int16) error {
table := &LootTable{
ID: tableID,
Name: name,
MinCoin: minCoin,
MaxCoin: maxCoin,
MaxLootItems: maxItems,
LootDropProbability: DefaultLootDropProbability,
CoinProbability: DefaultCoinProbability,
Drops: make([]*LootDrop, len(items)),
}
for i, item := range items {
table.Drops[i] = &LootDrop{
LootTableID: tableID,
ItemID: item.ItemID,
ItemCharges: item.Charges,
EquipItem: item.AutoEquip,
Probability: item.Probability,
}
}
return ls.AddLootTableWithDrops(table)
}
// QuickLootItem represents a simple loot item for quick table creation
type QuickLootItem struct {
ItemID int32
Charges int16
Probability float32
AutoEquip bool
}
// AssignLootToSpawns assigns a loot table to multiple spawns
func (ls *LootSystem) AssignLootToSpawns(tableID int32, spawnIDs []int32) error {
for _, spawnID := range spawnIDs {
if err := ls.Database.AddSpawnLoot(spawnID, tableID); err != nil {
return fmt.Errorf("failed to assign loot table %d to spawn %d: %v", tableID, spawnID, err)
}
}
log.Printf("%s Assigned loot table %d to %d spawns", LogPrefixLoot, tableID, len(spawnIDs))
return nil
}
// CreateGlobalLevelLoot creates global loot for a level range
func (ls *LootSystem) CreateGlobalLevelLoot(minLevel, maxLevel int8, tableID int32, tier int32) error {
global := &GlobalLoot{
Type: GlobalLootTypeLevel,
MinLevel: minLevel,
MaxLevel: maxLevel,
TableID: tableID,
LootTier: tier,
}
// Insert into database
ctx := context.Background()
conn, err := ls.Database.pool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to get database connection: %w", err)
}
defer ls.Database.pool.Put(conn)
query := `INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4) VALUES (?, ?, ?, ?, ?, ?)`
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
Args: []any{"level", tableID, minLevel, maxLevel, tier, 0},
})
if err != nil {
return fmt.Errorf("failed to insert global level loot: %v", err)
}
// Add to in-memory cache
ls.Database.mutex.Lock()
ls.Database.globalLoot = append(ls.Database.globalLoot, global)
ls.Database.mutex.Unlock()
log.Printf("%s Created global level loot for levels %d-%d using table %d",
LogPrefixLoot, minLevel, maxLevel, tableID)
return nil
}
// GetActiveChestsInZone returns all active chests in a specific zone
func (ls *LootSystem) GetActiveChestsInZone(zoneID int32) []*TreasureChest {
return ls.Manager.GetZoneChests(zoneID)
}
// CleanupZoneChests removes all chests from a specific zone
func (ls *LootSystem) CleanupZoneChests(zoneID int32) {
chests := ls.Manager.GetZoneChests(zoneID)
for _, chest := range chests {
ls.Manager.RemoveTreasureChest(chest.ID)
// Remove from zone if chest service is available
if ls.ChestService != nil {
ls.ChestService.zoneService.RemoveObjectFromZone(zoneID, chest.ID)
}
}
log.Printf("%s Cleaned up %d chests from zone %d", LogPrefixLoot, len(chests), zoneID)
}
// ValidateItemsInLootTables checks that all items in loot tables exist in the item master list
func (ls *LootSystem) ValidateItemsInLootTables() []ValidationError {
var errors []ValidationError
ls.Database.mutex.RLock()
defer ls.Database.mutex.RUnlock()
for tableID, table := range ls.Database.lootTables {
for _, drop := range table.Drops {
// @TODO: Fix MasterItemListService type import - itemMasterList method calls disabled
// item := ls.Manager.itemMasterList.GetItem(drop.ItemID)
var item any = nil
if item == nil {
errors = append(errors, ValidationError{
Type: "missing_item",
TableID: tableID,
ItemID: drop.ItemID,
Description: fmt.Sprintf("Item %d in loot table %d (%s) does not exist", drop.ItemID, tableID, table.Name),
})
}
}
}
if len(errors) > 0 {
log.Printf("%s Found %d validation errors in loot tables", LogPrefixLoot, len(errors))
}
return errors
}
// ValidationError represents a loot system validation error
type ValidationError struct {
Type string `json:"type"`
TableID int32 `json:"table_id"`
ItemID int32 `json:"item_id,omitempty"`
Description string `json:"description"`
}
// GetLootPreview generates a preview of potential loot without actually creating it
func (ls *LootSystem) GetLootPreview(spawnID int32, context *LootContext) (*LootPreview, error) {
tableIDs := ls.Database.GetSpawnLootTables(spawnID)
globalLoot := ls.Database.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID)
for _, global := range globalLoot {
tableIDs = append(tableIDs, global.TableID)
}
preview := &LootPreview{
SpawnID: spawnID,
TableIDs: tableIDs,
PossibleItems: make([]*LootPreviewItem, 0),
MinCoins: 0,
MaxCoins: 0,
}
for _, tableID := range tableIDs {
table := ls.Database.GetLootTable(tableID)
if table == nil {
continue
}
preview.MinCoins += table.MinCoin
preview.MaxCoins += table.MaxCoin
for _, drop := range table.Drops {
// @TODO: Fix MasterItemListService type import - itemMasterList method calls disabled
// item := ls.Manager.itemMasterList.GetItem(drop.ItemID)
var item any = nil
if item == nil {
continue
}
// @TODO: Fix MasterItemListService type import - preview item creation disabled
previewItem := &LootPreviewItem{
ItemID: drop.ItemID,
ItemName: "[DISABLED - TYPE IMPORT ISSUE]",
Probability: drop.Probability,
Tier: 0, // item.Details.Tier disabled
}
preview.PossibleItems = append(preview.PossibleItems, previewItem)
}
}
return preview, nil
}
// LootPreview represents a preview of potential loot
type LootPreview struct {
SpawnID int32 `json:"spawn_id"`
TableIDs []int32 `json:"table_ids"`
PossibleItems []*LootPreviewItem `json:"possible_items"`
MinCoins int32 `json:"min_coins"`
MaxCoins int32 `json:"max_coins"`
}
// LootPreviewItem represents a potential loot item in a preview
type LootPreviewItem struct {
ItemID int32 `json:"item_id"`
ItemName string `json:"item_name"`
Probability float32 `json:"probability"`
Tier int8 `json:"tier"`
}

1162
internal/items/loot/loot.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,493 +0,0 @@
package loot
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
"eq2emu/internal/items"
)
// LootManager handles all loot generation and management
type LootManager struct {
database *LootDatabase
// @TODO: Fix MasterItemListService type import
itemMasterList any // was items.MasterItemListService
statistics *LootStatistics
treasureChests map[int32]*TreasureChest // chest_id -> TreasureChest
chestIDCounter int32
random *rand.Rand
mutex sync.RWMutex
}
// NewLootManager creates a new loot manager
// @TODO: Fix MasterItemListService type import
func NewLootManager(database *LootDatabase, itemMasterList any) *LootManager {
return &LootManager{
database: database,
itemMasterList: itemMasterList,
statistics: NewLootStatistics(),
treasureChests: make(map[int32]*TreasureChest),
chestIDCounter: 1,
random: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
// GenerateLoot generates loot for a spawn based on its loot table assignments
func (lm *LootManager) GenerateLoot(spawnID int32, context *LootContext) (*LootResult, error) {
log.Printf("%s Generating loot for spawn %d", LogPrefixGeneration, spawnID)
result := &LootResult{
Items: make([]*items.Item, 0),
Coins: 0,
}
// Get loot tables for this spawn
tableIDs := lm.database.GetSpawnLootTables(spawnID)
// Also check for global loot tables
globalLoot := lm.database.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID)
for _, global := range globalLoot {
tableIDs = append(tableIDs, global.TableID)
}
if len(tableIDs) == 0 {
log.Printf("%s No loot tables found for spawn %d", LogPrefixGeneration, spawnID)
return result, nil
}
// Process each loot table
for _, tableID := range tableIDs {
if err := lm.processLootTable(tableID, context, result); err != nil {
log.Printf("%s Error processing loot table %d: %v", LogPrefixGeneration, tableID, err)
continue
}
}
// Record statistics
if len(tableIDs) > 0 {
lm.statistics.RecordLoot(tableIDs[0], result) // Use first table for stats
}
log.Printf("%s Generated %d items and %d coins for spawn %d",
LogPrefixGeneration, len(result.Items), result.Coins, spawnID)
return result, nil
}
// processLootTable processes a single loot table and adds results to the loot result
func (lm *LootManager) processLootTable(tableID int32, context *LootContext, result *LootResult) error {
table := lm.database.GetLootTable(tableID)
if table == nil {
return fmt.Errorf("loot table %d not found", tableID)
}
lm.mutex.Lock()
defer lm.mutex.Unlock()
// Check if loot should drop at all
if !lm.rollProbability(table.LootDropProbability) {
log.Printf("%s Loot table %d failed drop probability check", LogPrefixGeneration, tableID)
return nil
}
// Generate coins if probability succeeds
if lm.rollProbability(table.CoinProbability) {
coins := lm.generateCoins(table.MinCoin, table.MaxCoin)
result.AddCoins(coins)
log.Printf("%s Generated %d coins from table %d", LogPrefixGeneration, coins, tableID)
}
// Generate items
itemsGenerated := 0
maxItems := int(table.MaxLootItems)
if maxItems <= 0 {
maxItems = int(DefaultMaxLootItems)
}
// Process each loot drop
for _, drop := range table.Drops {
// Check if we've hit the max item limit
if itemsGenerated >= maxItems {
break
}
// Check quest requirement
if drop.NoDropQuestCompletedID > 0 {
if !context.CompletedQuests[drop.NoDropQuestCompletedID] {
continue // Player hasn't completed required quest
}
}
// Roll probability for this drop
if !lm.rollProbability(drop.Probability) {
continue
}
// @TODO: Fix MasterItemListService type import - itemMasterList method calls disabled
// Get item template
// itemTemplate := lm.itemMasterList.GetItem(drop.ItemID)
// if itemTemplate == nil {
// log.Printf("%s Item template %d not found for loot drop", LogPrefixGeneration, drop.ItemID)
// continue
// }
var itemTemplate any = nil
if itemTemplate == nil {
log.Printf("%s Item template %d not found for loot drop (disabled due to type import issue)", LogPrefixGeneration, drop.ItemID)
continue
}
// @TODO: Fix MasterItemListService type import - item creation disabled
// Create item instance
// item := items.NewItemFromTemplate(itemTemplate)
var item *items.Item = nil
// Set charges if specified
if drop.ItemCharges > 0 {
item.Details.Count = drop.ItemCharges
}
// Mark as equipped if specified
if drop.EquipItem {
// This would be handled by the caller when distributing loot
// For now, we just note it in the item
}
result.AddItem(item)
itemsGenerated++
log.Printf("%s Generated item %d (%s) from table %d",
LogPrefixGeneration, drop.ItemID, item.Name, tableID)
}
return nil
}
// rollProbability rolls a probability check (0-100%)
func (lm *LootManager) rollProbability(probability float32) bool {
if probability <= 0 {
return false
}
if probability >= 100.0 {
return true
}
roll := lm.random.Float32() * 100.0
return roll <= probability
}
// generateCoins generates a random coin amount between min and max
func (lm *LootManager) generateCoins(minCoin, maxCoin int32) int32 {
if minCoin >= maxCoin {
return minCoin
}
return minCoin + lm.random.Int31n(maxCoin-minCoin+1)
}
// CreateTreasureChest creates a treasure chest for loot
func (lm *LootManager) CreateTreasureChest(spawnID int32, zoneID int32, x, y, z, heading float32,
lootResult *LootResult, lootRights []uint32) (*TreasureChest, error) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
// Generate unique chest ID
chestID := lm.chestIDCounter
lm.chestIDCounter++
// Determine chest appearance based on highest item tier
highestTier := lm.getHighestItemTier(lootResult.GetItems())
appearance := GetChestAppearance(highestTier)
chest := &TreasureChest{
ID: chestID,
SpawnID: spawnID,
ZoneID: zoneID,
X: x,
Y: y,
Z: z,
Heading: heading,
AppearanceID: appearance.AppearanceID,
LootResult: lootResult,
Created: time.Now(),
LootRights: make([]uint32, len(lootRights)),
IsDisarmable: false, // TODO: Implement trap system
IsLocked: false, // TODO: Implement lock system
}
// Copy loot rights
copy(chest.LootRights, lootRights)
// Store chest
lm.treasureChests[chestID] = chest
// Record statistics
lm.statistics.RecordChest()
log.Printf("%s Created treasure chest %d (%s) at (%.2f, %.2f, %.2f) with %d items and %d coins",
LogPrefixChest, chestID, appearance.Name, x, y, z,
len(lootResult.GetItems()), lootResult.GetCoins())
return chest, nil
}
// getHighestItemTier finds the highest tier among items
func (lm *LootManager) getHighestItemTier(items []*items.Item) int8 {
var highest int8 = LootTierCommon
for _, item := range items {
if item.Details.Tier > highest {
highest = item.Details.Tier
}
}
return highest
}
// GetTreasureChest returns a treasure chest by ID
func (lm *LootManager) GetTreasureChest(chestID int32) *TreasureChest {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
return lm.treasureChests[chestID]
}
// RemoveTreasureChest removes a treasure chest
func (lm *LootManager) RemoveTreasureChest(chestID int32) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
delete(lm.treasureChests, chestID)
log.Printf("%s Removed treasure chest %d", LogPrefixChest, chestID)
}
// LootChestItem removes a specific item from a chest
func (lm *LootManager) LootChestItem(chestID int32, playerID uint32, itemUniqueID int64) (*items.Item, error) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
chest := lm.treasureChests[chestID]
if chest == nil {
return nil, fmt.Errorf("treasure chest %d not found", chestID)
}
// Check loot rights
if !chest.HasLootRights(playerID) {
return nil, fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID)
}
// Find and remove the item
lootItems := chest.LootResult.GetItems()
for i, item := range lootItems {
if item.Details.UniqueID == itemUniqueID {
// Remove item from slice
chest.LootResult.mutex.Lock()
chest.LootResult.Items = append(chest.LootResult.Items[:i], chest.LootResult.Items[i+1:]...)
chest.LootResult.mutex.Unlock()
log.Printf("%s Player %d looted item %d (%s) from chest %d",
LogPrefixChest, playerID, item.Details.ItemID, item.Name, chestID)
return item, nil
}
}
return nil, fmt.Errorf("item %d not found in chest %d", itemUniqueID, chestID)
}
// LootChestCoins removes coins from a chest
func (lm *LootManager) LootChestCoins(chestID int32, playerID uint32) (int32, error) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
chest := lm.treasureChests[chestID]
if chest == nil {
return 0, fmt.Errorf("treasure chest %d not found", chestID)
}
// Check loot rights
if !chest.HasLootRights(playerID) {
return 0, fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID)
}
coins := chest.LootResult.GetCoins()
if coins <= 0 {
return 0, nil
}
// Remove coins from chest
chest.LootResult.mutex.Lock()
chest.LootResult.Coins = 0
chest.LootResult.mutex.Unlock()
log.Printf("%s Player %d looted %d coins from chest %d",
LogPrefixChest, playerID, coins, chestID)
return coins, nil
}
// LootChestAll removes all items and coins from a chest
func (lm *LootManager) LootChestAll(chestID int32, playerID uint32) (*LootResult, error) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
chest := lm.treasureChests[chestID]
if chest == nil {
return nil, fmt.Errorf("treasure chest %d not found", chestID)
}
// Check loot rights
if !chest.HasLootRights(playerID) {
return nil, fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID)
}
// Get all loot
result := &LootResult{
Items: chest.LootResult.GetItems(),
Coins: chest.LootResult.GetCoins(),
}
// Clear chest loot
chest.LootResult.mutex.Lock()
chest.LootResult.Items = make([]*items.Item, 0)
chest.LootResult.Coins = 0
chest.LootResult.mutex.Unlock()
log.Printf("%s Player %d looted all (%d items, %d coins) from chest %d",
LogPrefixChest, playerID, len(result.Items), result.Coins, chestID)
return result, nil
}
// IsChestEmpty checks if a chest has no loot
func (lm *LootManager) IsChestEmpty(chestID int32) bool {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
chest := lm.treasureChests[chestID]
if chest == nil {
return true
}
return chest.LootResult.IsEmpty()
}
// CleanupExpiredChests removes chests that have been around too long
func (lm *LootManager) CleanupExpiredChests() {
lm.mutex.Lock()
defer lm.mutex.Unlock()
now := time.Now()
var expired []int32
for chestID, chest := range lm.treasureChests {
age := now.Sub(chest.Created).Seconds()
// Remove empty chests after ChestDespawnTime
if chest.LootResult.IsEmpty() && age > ChestDespawnTime {
expired = append(expired, chestID)
}
// Force remove all chests after ChestCleanupTime
if age > ChestCleanupTime {
expired = append(expired, chestID)
}
}
for _, chestID := range expired {
delete(lm.treasureChests, chestID)
log.Printf("%s Cleaned up expired chest %d", LogPrefixChest, chestID)
}
if len(expired) > 0 {
log.Printf("%s Cleaned up %d expired chests", LogPrefixChest, len(expired))
}
}
// GetZoneChests returns all chests in a specific zone
func (lm *LootManager) GetZoneChests(zoneID int32) []*TreasureChest {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
var chests []*TreasureChest
for _, chest := range lm.treasureChests {
if chest.ZoneID == zoneID {
chests = append(chests, chest)
}
}
return chests
}
// GetPlayerChests returns all chests a player has loot rights to
func (lm *LootManager) GetPlayerChests(playerID uint32) []*TreasureChest {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
var chests []*TreasureChest
for _, chest := range lm.treasureChests {
if chest.HasLootRights(playerID) {
chests = append(chests, chest)
}
}
return chests
}
// GetStatistics returns loot generation statistics
func (lm *LootManager) GetStatistics() LootStatistics {
return lm.statistics.GetStatistics()
}
// ReloadLootData reloads loot data from the database
func (lm *LootManager) ReloadLootData() error {
log.Printf("%s Reloading loot data...", LogPrefixLoot)
return lm.database.ReloadLootData()
}
// AddLootTable adds a new loot table
func (lm *LootManager) AddLootTable(table *LootTable) error {
log.Printf("%s Adding loot table %d (%s)", LogPrefixLoot, table.ID, table.Name)
return lm.database.AddLootTable(table)
}
// UpdateLootTable updates an existing loot table
func (lm *LootManager) UpdateLootTable(table *LootTable) error {
log.Printf("%s Updating loot table %d (%s)", LogPrefixLoot, table.ID, table.Name)
return lm.database.UpdateLootTable(table)
}
// DeleteLootTable removes a loot table
func (lm *LootManager) DeleteLootTable(tableID int32) error {
log.Printf("%s Deleting loot table %d", LogPrefixLoot, tableID)
return lm.database.DeleteLootTable(tableID)
}
// AssignSpawnLoot assigns a loot table to a spawn
func (lm *LootManager) AssignSpawnLoot(spawnID, tableID int32) error {
log.Printf("%s Assigning loot table %d to spawn %d", LogPrefixLoot, tableID, spawnID)
return lm.database.AddSpawnLoot(spawnID, tableID)
}
// RemoveSpawnLoot removes loot table assignments from a spawn
func (lm *LootManager) RemoveSpawnLoot(spawnID int32) error {
log.Printf("%s Removing loot assignments from spawn %d", LogPrefixLoot, spawnID)
return lm.database.DeleteSpawnLoot(spawnID)
}
// StartCleanupTimer starts a background timer to clean up expired chests
func (lm *LootManager) StartCleanupTimer() {
go func() {
ticker := time.NewTicker(5 * time.Minute) // Clean up every 5 minutes
defer ticker.Stop()
for range ticker.C {
lm.CleanupExpiredChests()
}
}()
log.Printf("%s Started chest cleanup timer", LogPrefixLoot)
}

View File

@ -1,464 +0,0 @@
package loot
import (
"fmt"
"log"
"eq2emu/internal/items"
)
// PacketBuilder interface for building loot-related packets
type PacketBuilder interface {
BuildUpdateLootPacket(chest *TreasureChest, playerID uint32, clientVersion int32) ([]byte, error)
BuildLootItemPacket(item *items.Item, playerID uint32, clientVersion int32) ([]byte, error)
BuildStoppedLootingPacket(chestID int32, playerID uint32, clientVersion int32) ([]byte, error)
BuildLootResponsePacket(result *ChestInteractionResult, clientVersion int32) ([]byte, error)
}
// LootPacketBuilder builds loot-related packets for client communication
type LootPacketBuilder struct {
itemPacketBuilder ItemPacketBuilder
}
// ItemPacketBuilder interface for building item-related packet data
type ItemPacketBuilder interface {
BuildItemData(item *items.Item, clientVersion int32) ([]byte, error)
GetItemAppearanceData(item *items.Item) (int32, int16, int16, int16, int16, int16, int16)
}
// NewLootPacketBuilder creates a new loot packet builder
func NewLootPacketBuilder(itemPacketBuilder ItemPacketBuilder) *LootPacketBuilder {
return &LootPacketBuilder{
itemPacketBuilder: itemPacketBuilder,
}
}
// BuildUpdateLootPacket builds an UpdateLoot packet to show chest contents to a player
func (lpb *LootPacketBuilder) BuildUpdateLootPacket(chest *TreasureChest, playerID uint32, clientVersion int32) ([]byte, error) {
log.Printf("%s Building UpdateLoot packet for chest %d, player %d, version %d",
LogPrefixLoot, chest.ID, playerID, clientVersion)
// Start with base packet structure
packet := &LootPacketData{
PacketType: "UpdateLoot",
ChestID: chest.ID,
SpawnID: chest.SpawnID,
PlayerID: playerID,
ClientVersion: clientVersion,
}
// Add loot items
lootItems := chest.LootResult.GetItems()
packet.ItemCount = int16(len(lootItems))
packet.Items = make([]*LootItemData, len(lootItems))
for i, item := range lootItems {
itemData, err := lpb.buildLootItemData(item, clientVersion)
if err != nil {
log.Printf("%s Failed to build item data for item %d: %v", LogPrefixLoot, item.Details.ItemID, err)
continue
}
packet.Items[i] = itemData
}
// Add coin information
packet.Coins = chest.LootResult.GetCoins()
// Build packet based on client version
return lpb.buildVersionSpecificLootPacket(packet)
}
// buildLootItemData builds loot item data for a specific item
func (lpb *LootPacketBuilder) buildLootItemData(item *items.Item, clientVersion int32) (*LootItemData, error) {
// Get item appearance data
appearanceID, red, green, blue, highlightRed, highlightGreen, highlightBlue :=
lpb.itemPacketBuilder.GetItemAppearanceData(item)
return &LootItemData{
ItemID: item.Details.ItemID,
UniqueID: item.Details.UniqueID,
Name: item.Name,
Count: item.Details.Count,
Tier: item.Details.Tier,
Icon: item.Details.Icon,
AppearanceID: appearanceID,
Red: red,
Green: green,
Blue: blue,
HighlightRed: highlightRed,
HighlightGreen: highlightGreen,
HighlightBlue: highlightBlue,
ItemType: item.GenericInfo.ItemType,
NoTrade: (int32(item.GenericInfo.ItemFlags) & int32(LootFlagNoTrade)) != 0,
Heirloom: (int32(item.GenericInfo.ItemFlags) & int32(LootFlagHeirloom)) != 0,
Lore: (int32(item.GenericInfo.ItemFlags) & int32(LootFlagLore)) != 0,
}, nil
}
// buildVersionSpecificLootPacket builds the actual packet bytes based on client version
func (lpb *LootPacketBuilder) buildVersionSpecificLootPacket(packet *LootPacketData) ([]byte, error) {
switch {
case packet.ClientVersion >= 60114:
return lpb.buildLootPacketV60114(packet)
case packet.ClientVersion >= 1193:
return lpb.buildLootPacketV1193(packet)
case packet.ClientVersion >= 546:
return lpb.buildLootPacketV546(packet)
case packet.ClientVersion >= 373:
return lpb.buildLootPacketV373(packet)
default:
return lpb.buildLootPacketV1(packet)
}
}
// buildLootPacketV60114 builds loot packet for client version 60114+
func (lpb *LootPacketBuilder) buildLootPacketV60114(packet *LootPacketData) ([]byte, error) {
// This is the most recent packet format with all features
buffer := NewPacketBuffer()
// Packet header
buffer.WriteInt32(packet.ChestID)
buffer.WriteInt32(packet.SpawnID)
buffer.WriteInt16(packet.ItemCount)
buffer.WriteInt32(packet.Coins)
// Loot options
buffer.WriteInt8(1) // loot_all_enabled
buffer.WriteInt8(1) // auto_loot_enabled
buffer.WriteInt8(0) // loot_timeout (0 = no timeout)
// Item array
for _, item := range packet.Items {
if item == nil {
continue
}
buffer.WriteInt32(item.ItemID)
buffer.WriteInt64(item.UniqueID)
buffer.WriteString(item.Name)
buffer.WriteInt16(item.Count)
buffer.WriteInt8(item.Tier)
buffer.WriteInt16(item.Icon)
buffer.WriteInt32(item.AppearanceID)
buffer.WriteInt16(item.Red)
buffer.WriteInt16(item.Green)
buffer.WriteInt16(item.Blue)
buffer.WriteInt16(item.HighlightRed)
buffer.WriteInt16(item.HighlightGreen)
buffer.WriteInt16(item.HighlightBlue)
buffer.WriteInt8(item.ItemType)
buffer.WriteBool(item.NoTrade)
buffer.WriteBool(item.Heirloom)
buffer.WriteBool(item.Lore)
// Extended item data for newer clients
buffer.WriteInt32(0) // adornment_slot0
buffer.WriteInt32(0) // adornment_slot1
buffer.WriteInt32(0) // adornment_slot2
}
return buffer.GetBytes(), nil
}
// buildLootPacketV1193 builds loot packet for client version 1193+
func (lpb *LootPacketBuilder) buildLootPacketV1193(packet *LootPacketData) ([]byte, error) {
buffer := NewPacketBuffer()
buffer.WriteInt32(packet.ChestID)
buffer.WriteInt32(packet.SpawnID)
buffer.WriteInt16(packet.ItemCount)
buffer.WriteInt32(packet.Coins)
buffer.WriteInt8(1) // loot_all_enabled
for _, item := range packet.Items {
if item == nil {
continue
}
buffer.WriteInt32(item.ItemID)
buffer.WriteInt64(item.UniqueID)
buffer.WriteString(item.Name)
buffer.WriteInt16(item.Count)
buffer.WriteInt8(item.Tier)
buffer.WriteInt16(item.Icon)
buffer.WriteInt32(item.AppearanceID)
buffer.WriteInt16(item.Red)
buffer.WriteInt16(item.Green)
buffer.WriteInt16(item.Blue)
buffer.WriteInt8(item.ItemType)
buffer.WriteBool(item.NoTrade)
buffer.WriteBool(item.Heirloom)
}
return buffer.GetBytes(), nil
}
// buildLootPacketV546 builds loot packet for client version 546+
func (lpb *LootPacketBuilder) buildLootPacketV546(packet *LootPacketData) ([]byte, error) {
buffer := NewPacketBuffer()
buffer.WriteInt32(packet.ChestID)
buffer.WriteInt32(packet.SpawnID)
buffer.WriteInt16(packet.ItemCount)
buffer.WriteInt32(packet.Coins)
for _, item := range packet.Items {
if item == nil {
continue
}
buffer.WriteInt32(item.ItemID)
buffer.WriteInt64(item.UniqueID)
buffer.WriteString(item.Name)
buffer.WriteInt16(item.Count)
buffer.WriteInt8(item.Tier)
buffer.WriteInt16(item.Icon)
buffer.WriteInt8(item.ItemType)
buffer.WriteBool(item.NoTrade)
}
return buffer.GetBytes(), nil
}
// buildLootPacketV373 builds loot packet for client version 373+
func (lpb *LootPacketBuilder) buildLootPacketV373(packet *LootPacketData) ([]byte, error) {
buffer := NewPacketBuffer()
buffer.WriteInt32(packet.ChestID)
buffer.WriteInt16(packet.ItemCount)
buffer.WriteInt32(packet.Coins)
for _, item := range packet.Items {
if item == nil {
continue
}
buffer.WriteInt32(item.ItemID)
buffer.WriteString(item.Name)
buffer.WriteInt16(item.Count)
buffer.WriteInt16(item.Icon)
buffer.WriteInt8(item.ItemType)
}
return buffer.GetBytes(), nil
}
// buildLootPacketV1 builds loot packet for client version 1 (oldest)
func (lpb *LootPacketBuilder) buildLootPacketV1(packet *LootPacketData) ([]byte, error) {
buffer := NewPacketBuffer()
buffer.WriteInt32(packet.ChestID)
buffer.WriteInt16(packet.ItemCount)
for _, item := range packet.Items {
if item == nil {
continue
}
buffer.WriteInt32(item.ItemID)
buffer.WriteString(item.Name)
buffer.WriteInt16(item.Count)
}
return buffer.GetBytes(), nil
}
// BuildLootItemPacket builds a packet for when a player loots a specific item
func (lpb *LootPacketBuilder) BuildLootItemPacket(item *items.Item, playerID uint32, clientVersion int32) ([]byte, error) {
log.Printf("%s Building LootItem packet for item %d, player %d", LogPrefixLoot, item.Details.ItemID, playerID)
buffer := NewPacketBuffer()
// Basic loot item response
buffer.WriteInt32(item.Details.ItemID)
buffer.WriteInt64(item.Details.UniqueID)
buffer.WriteString(item.Name)
buffer.WriteInt16(item.Details.Count)
buffer.WriteInt8(1) // success flag
return buffer.GetBytes(), nil
}
// BuildStoppedLootingPacket builds a packet when player stops looting
func (lpb *LootPacketBuilder) BuildStoppedLootingPacket(chestID int32, playerID uint32, clientVersion int32) ([]byte, error) {
log.Printf("%s Building StoppedLooting packet for chest %d, player %d", LogPrefixLoot, chestID, playerID)
buffer := NewPacketBuffer()
buffer.WriteInt32(chestID)
return buffer.GetBytes(), nil
}
// BuildLootResponsePacket builds a response packet for chest interactions
func (lpb *LootPacketBuilder) BuildLootResponsePacket(result *ChestInteractionResult, clientVersion int32) ([]byte, error) {
buffer := NewPacketBuffer()
// Result code and message
buffer.WriteInt8(result.Result)
buffer.WriteBool(result.Success)
buffer.WriteString(result.Message)
// Items received
buffer.WriteInt16(int16(len(result.Items)))
for _, item := range result.Items {
buffer.WriteInt32(item.Details.ItemID)
buffer.WriteString(item.Name)
buffer.WriteInt16(item.Details.Count)
}
// Coins received
buffer.WriteInt32(result.Coins)
// Experience gained
buffer.WriteInt32(result.Experience)
// Status flags
buffer.WriteBool(result.ChestEmpty)
buffer.WriteBool(result.ChestClosed)
return buffer.GetBytes(), nil
}
// LootPacketData represents the data structure for loot packets
type LootPacketData struct {
PacketType string
ChestID int32
SpawnID int32
PlayerID uint32
ClientVersion int32
ItemCount int16
Items []*LootItemData
Coins int32
}
// LootItemData represents an item in a loot packet
type LootItemData struct {
ItemID int32
UniqueID int64
Name string
Count int16
Tier int8
Icon int16
AppearanceID int32
Red int16
Green int16
Blue int16
HighlightRed int16
HighlightGreen int16
HighlightBlue int16
ItemType int8
NoTrade bool
Heirloom bool
Lore bool
}
// PacketBuffer is a simple buffer for building packet data
type PacketBuffer struct {
data []byte
}
// NewPacketBuffer creates a new packet buffer
func NewPacketBuffer() *PacketBuffer {
return &PacketBuffer{
data: make([]byte, 0, 1024),
}
}
// WriteInt8 writes an 8-bit integer
func (pb *PacketBuffer) WriteInt8(value int8) {
pb.data = append(pb.data, byte(value))
}
// WriteInt16 writes a 16-bit integer
func (pb *PacketBuffer) WriteInt16(value int16) {
pb.data = append(pb.data, byte(value), byte(value>>8))
}
// WriteInt32 writes a 32-bit integer
func (pb *PacketBuffer) WriteInt32(value int32) {
pb.data = append(pb.data,
byte(value), byte(value>>8), byte(value>>16), byte(value>>24))
}
// WriteInt64 writes a 64-bit integer
func (pb *PacketBuffer) WriteInt64(value int64) {
pb.data = append(pb.data,
byte(value), byte(value>>8), byte(value>>16), byte(value>>24),
byte(value>>32), byte(value>>40), byte(value>>48), byte(value>>56))
}
// WriteBool writes a boolean as a single byte
func (pb *PacketBuffer) WriteBool(value bool) {
if value {
pb.data = append(pb.data, 1)
} else {
pb.data = append(pb.data, 0)
}
}
// WriteString writes a null-terminated string
func (pb *PacketBuffer) WriteString(value string) {
pb.data = append(pb.data, []byte(value)...)
pb.data = append(pb.data, 0) // null terminator
}
// GetBytes returns the current buffer data
func (pb *PacketBuffer) GetBytes() []byte {
return pb.data
}
// LootPacketService provides high-level packet building services
type LootPacketService struct {
packetBuilder *LootPacketBuilder
clientService ClientService
}
// ClientService interface for client-related operations
type ClientService interface {
GetClientVersion(playerID uint32) int32
SendPacketToPlayer(playerID uint32, packetType string, data []byte) error
}
// NewLootPacketService creates a new loot packet service
func NewLootPacketService(packetBuilder *LootPacketBuilder, clientService ClientService) *LootPacketService {
return &LootPacketService{
packetBuilder: packetBuilder,
clientService: clientService,
}
}
// SendLootUpdate sends a loot update packet to a player
func (lps *LootPacketService) SendLootUpdate(chest *TreasureChest, playerID uint32) error {
clientVersion := lps.clientService.GetClientVersion(playerID)
packet, err := lps.packetBuilder.BuildUpdateLootPacket(chest, playerID, clientVersion)
if err != nil {
return fmt.Errorf("failed to build loot update packet: %v", err)
}
return lps.clientService.SendPacketToPlayer(playerID, "UpdateLoot", packet)
}
// SendLootResponse sends a loot interaction response to a player
func (lps *LootPacketService) SendLootResponse(result *ChestInteractionResult, playerID uint32) error {
clientVersion := lps.clientService.GetClientVersion(playerID)
packet, err := lps.packetBuilder.BuildLootResponsePacket(result, clientVersion)
if err != nil {
return fmt.Errorf("failed to build loot response packet: %v", err)
}
return lps.clientService.SendPacketToPlayer(playerID, "LootResponse", packet)
}
// SendStoppedLooting sends a stopped looting packet to a player
func (lps *LootPacketService) SendStoppedLooting(chestID int32, playerID uint32) error {
clientVersion := lps.clientService.GetClientVersion(playerID)
packet, err := lps.packetBuilder.BuildStoppedLootingPacket(chestID, playerID, clientVersion)
if err != nil {
return fmt.Errorf("failed to build stopped looting packet: %v", err)
}
return lps.clientService.SendPacketToPlayer(playerID, "StoppedLooting", packet)
}

View File

@ -1,321 +0,0 @@
package loot
import (
"sync"
"time"
"eq2emu/internal/items"
)
// LootTable represents a complete loot table with its drops
type LootTable struct {
ID int32 `json:"id"`
Name string `json:"name"`
MinCoin int32 `json:"min_coin"`
MaxCoin int32 `json:"max_coin"`
MaxLootItems int16 `json:"max_loot_items"`
LootDropProbability float32 `json:"loot_drop_probability"`
CoinProbability float32 `json:"coin_probability"`
Drops []*LootDrop `json:"drops"`
mutex sync.RWMutex
}
// LootDrop represents an individual item that can drop from a loot table
type LootDrop struct {
LootTableID int32 `json:"loot_table_id"`
ItemID int32 `json:"item_id"`
ItemCharges int16 `json:"item_charges"`
EquipItem bool `json:"equip_item"`
Probability float32 `json:"probability"`
NoDropQuestCompletedID int32 `json:"no_drop_quest_completed_id"`
}
// GlobalLoot represents global loot configuration based on level, race, or zone
type GlobalLoot struct {
Type GlobalLootType `json:"type"`
MinLevel int8 `json:"min_level"`
MaxLevel int8 `json:"max_level"`
Race int16 `json:"race"`
ZoneID int32 `json:"zone_id"`
TableID int32 `json:"table_id"`
LootTier int32 `json:"loot_tier"`
}
// GlobalLootType represents the type of global loot
type GlobalLootType int8
const (
GlobalLootTypeLevel GlobalLootType = iota
GlobalLootTypeRace
GlobalLootTypeZone
)
// String returns the string representation of GlobalLootType
func (t GlobalLootType) String() string {
switch t {
case GlobalLootTypeLevel:
return "level"
case GlobalLootTypeRace:
return "race"
case GlobalLootTypeZone:
return "zone"
default:
return "unknown"
}
}
// LootResult represents the result of loot generation
type LootResult struct {
Items []*items.Item `json:"items"`
Coins int32 `json:"coins"`
mutex sync.RWMutex
}
// AddItem adds an item to the loot result (thread-safe)
func (lr *LootResult) AddItem(item *items.Item) {
lr.mutex.Lock()
defer lr.mutex.Unlock()
lr.Items = append(lr.Items, item)
}
// AddCoins adds coins to the loot result (thread-safe)
func (lr *LootResult) AddCoins(coins int32) {
lr.mutex.Lock()
defer lr.mutex.Unlock()
lr.Coins += coins
}
// GetItems returns a copy of the items slice (thread-safe)
func (lr *LootResult) GetItems() []*items.Item {
lr.mutex.RLock()
defer lr.mutex.RUnlock()
result := make([]*items.Item, len(lr.Items))
copy(result, lr.Items)
return result
}
// GetCoins returns the coin amount (thread-safe)
func (lr *LootResult) GetCoins() int32 {
lr.mutex.RLock()
defer lr.mutex.RUnlock()
return lr.Coins
}
// IsEmpty returns true if the loot result has no items or coins
func (lr *LootResult) IsEmpty() bool {
lr.mutex.RLock()
defer lr.mutex.RUnlock()
return len(lr.Items) == 0 && lr.Coins == 0
}
// TreasureChest represents a treasure chest spawn containing loot
type TreasureChest struct {
ID int32 `json:"id"`
SpawnID int32 `json:"spawn_id"`
ZoneID int32 `json:"zone_id"`
X float32 `json:"x"`
Y float32 `json:"y"`
Z float32 `json:"z"`
Heading float32 `json:"heading"`
AppearanceID int32 `json:"appearance_id"`
LootResult *LootResult `json:"loot_result"`
Created time.Time `json:"created"`
LootRights []uint32 `json:"loot_rights"` // Player IDs with loot rights
IsDisarmable bool `json:"is_disarmable"` // Can be disarmed
IsLocked bool `json:"is_locked"` // Requires key or lockpicking
DisarmDifficulty int16 `json:"disarm_difficulty"` // Difficulty for disarming
LockpickDifficulty int16 `json:"lockpick_difficulty"` // Difficulty for lockpicking
mutex sync.RWMutex
}
// HasLootRights checks if a player has rights to loot this chest
func (tc *TreasureChest) HasLootRights(playerID uint32) bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
// If no specific loot rights, anyone can loot
if len(tc.LootRights) == 0 {
return true
}
for _, id := range tc.LootRights {
if id == playerID {
return true
}
}
return false
}
// AddLootRights adds a player to the loot rights list
func (tc *TreasureChest) AddLootRights(playerID uint32) {
tc.mutex.Lock()
defer tc.mutex.Unlock()
// Check if already has rights
for _, id := range tc.LootRights {
if id == playerID {
return
}
}
tc.LootRights = append(tc.LootRights, playerID)
}
// ChestAppearance represents different chest appearances based on loot tier
type ChestAppearance struct {
AppearanceID int32 `json:"appearance_id"`
Name string `json:"name"`
MinTier int8 `json:"min_tier"`
MaxTier int8 `json:"max_tier"`
}
// Predefined chest appearances based on C++ implementation
var (
SmallChest = &ChestAppearance{AppearanceID: 4034, Name: "Small Chest", MinTier: 1, MaxTier: 2}
TreasureChestAppearance = &ChestAppearance{AppearanceID: 5864, Name: "Treasure Chest", MinTier: 3, MaxTier: 4}
OrnateChest = &ChestAppearance{AppearanceID: 5865, Name: "Ornate Chest", MinTier: 5, MaxTier: 6}
ExquisiteChest = &ChestAppearance{AppearanceID: 4015, Name: "Exquisite Chest", MinTier: 7, MaxTier: 10}
)
// GetChestAppearance returns the appropriate chest appearance based on loot tier
func GetChestAppearance(highestTier int8) *ChestAppearance {
if highestTier >= ExquisiteChest.MinTier {
return ExquisiteChest
}
if highestTier >= OrnateChest.MinTier {
return OrnateChest
}
if highestTier >= TreasureChestAppearance.MinTier {
return TreasureChestAppearance
}
return SmallChest
}
// LootContext provides context for loot generation
type LootContext struct {
PlayerLevel int16 `json:"player_level"`
PlayerRace int16 `json:"player_race"`
ZoneID int32 `json:"zone_id"`
KillerID uint32 `json:"killer_id"`
GroupMembers []uint32 `json:"group_members"`
CompletedQuests map[int32]bool `json:"completed_quests"`
LootMethod GroupLootMethod `json:"loot_method"`
}
// GroupLootMethod represents different group loot distribution methods
type GroupLootMethod int8
const (
GroupLootMethodFreeForAll GroupLootMethod = iota
GroupLootMethodRoundRobin
GroupLootMethodMasterLooter
GroupLootMethodNeed
GroupLootMethodLotto
)
// String returns the string representation of GroupLootMethod
func (glm GroupLootMethod) String() string {
switch glm {
case GroupLootMethodFreeForAll:
return "free_for_all"
case GroupLootMethodRoundRobin:
return "round_robin"
case GroupLootMethodMasterLooter:
return "master_looter"
case GroupLootMethodNeed:
return "need_greed"
case GroupLootMethodLotto:
return "lotto"
default:
return "unknown"
}
}
// LootEntry represents a complete loot entry with all associated data
type LootEntry struct {
SpawnID int32 `json:"spawn_id"`
LootTableID int32 `json:"loot_table_id"`
TableName string `json:"table_name"`
Priority int16 `json:"priority"`
}
// LootStatistics tracks loot generation statistics
type LootStatistics struct {
TotalLoots int64 `json:"total_loots"`
TotalItems int64 `json:"total_items"`
TotalCoins int64 `json:"total_coins"`
TreasureChests int64 `json:"treasure_chests"`
ItemsByTier map[int8]int64 `json:"items_by_tier"`
LootsByTable map[int32]int64 `json:"loots_by_table"`
AverageItemsPerLoot float32 `json:"average_items_per_loot"`
AverageCoinsPerLoot float32 `json:"average_coins_per_loot"`
mutex sync.RWMutex
}
// NewLootStatistics creates a new loot statistics tracker
func NewLootStatistics() *LootStatistics {
return &LootStatistics{
ItemsByTier: make(map[int8]int64),
LootsByTable: make(map[int32]int64),
}
}
// RecordLoot records statistics for a loot generation
func (ls *LootStatistics) RecordLoot(tableID int32, result *LootResult) {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.TotalLoots++
ls.LootsByTable[tableID]++
items := result.GetItems()
ls.TotalItems += int64(len(items))
ls.TotalCoins += int64(result.GetCoins())
// Track items by tier
for _, item := range items {
ls.ItemsByTier[item.Details.Tier]++
}
// Update averages
if ls.TotalLoots > 0 {
ls.AverageItemsPerLoot = float32(ls.TotalItems) / float32(ls.TotalLoots)
ls.AverageCoinsPerLoot = float32(ls.TotalCoins) / float32(ls.TotalLoots)
}
}
// RecordChest records a treasure chest creation
func (ls *LootStatistics) RecordChest() {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.TreasureChests++
}
// GetStatistics returns a copy of the current statistics
func (ls *LootStatistics) GetStatistics() LootStatistics {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
// Create deep copy
copy := LootStatistics{
TotalLoots: ls.TotalLoots,
TotalItems: ls.TotalItems,
TotalCoins: ls.TotalCoins,
TreasureChests: ls.TreasureChests,
AverageItemsPerLoot: ls.AverageItemsPerLoot,
AverageCoinsPerLoot: ls.AverageCoinsPerLoot,
ItemsByTier: make(map[int8]int64),
LootsByTable: make(map[int32]int64),
}
for tier, count := range ls.ItemsByTier {
copy.ItemsByTier[tier] = count
}
for tableID, count := range ls.LootsByTable {
copy.LootsByTable[tableID] = count
}
return copy
}

View File

@ -1,713 +0,0 @@
package items
import (
"fmt"
"log"
"strings"
"time"
)
// 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),
}
// Initialize mapped item stats
mil.initializeMappedStats()
return mil
}
// initializeMappedStats initializes the mapped item stats
func (mil *MasterItemList) initializeMappedStats() {
// Add all the mapped item stats as in the C++ constructor
// Basic stats
mil.AddMappedItemStat(ItemStatStr, "strength")
mil.AddMappedItemStat(ItemStatSta, "stamina")
mil.AddMappedItemStat(ItemStatAgi, "agility")
mil.AddMappedItemStat(ItemStatWis, "wisdom")
mil.AddMappedItemStat(ItemStatInt, "intelligence")
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")
}
// 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
// log.Printf("Added stat mapping: %s -> %d", lowerCaseName, id)
}
// 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
}
// GetMappedStatCount returns the number of mapped stats (for debugging)
func (mil *MasterItemList) GetMappedStatCount() int {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
return len(mil.mappedItemStatsStrings)
}
// GetItemStatNameByID gets the stat name by ID
func (mil *MasterItemList) GetItemStatNameByID(id int32) string {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
if name, exists := mil.mappedItemStatTypeIDs[id]; exists {
return name
}
return ""
}
// AddItem adds an item to the master list
func (mil *MasterItemList) AddItem(item *Item) {
if item == nil {
return
}
mil.mutex.Lock()
defer mil.mutex.Unlock()
mil.items[item.Details.ItemID] = item
// Added item to master list
}
// GetItem retrieves an item by ID
func (mil *MasterItemList) GetItem(itemID int32) *Item {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
if item, exists := mil.items[itemID]; exists {
return item.Copy() // Return a copy to prevent external modifications
}
return nil
}
// GetItemByName retrieves an item by name (case-insensitive)
func (mil *MasterItemList) GetItemByName(name string) *Item {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
lowerName := strings.ToLower(name)
for _, item := range mil.items {
if strings.ToLower(item.Name) == lowerName {
return item.Copy()
}
}
return nil
}
// IsBag checks if an item ID represents a bag
func (mil *MasterItemList) IsBag(itemID int32) bool {
item := mil.GetItem(itemID)
if item == nil {
return false
}
return item.IsBag()
}
// RemoveAll removes all items from the master list
func (mil *MasterItemList) RemoveAll() {
mil.mutex.Lock()
defer mil.mutex.Unlock()
count := len(mil.items)
mil.items = make(map[int32]*Item)
log.Printf("Removed %d items from master list", count)
}
// GetItemCount returns the total number of items
func (mil *MasterItemList) GetItemCount() int {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
return len(mil.items)
}
// CalculateItemBonuses calculates the stat bonuses for an item
func (mil *MasterItemList) CalculateItemBonuses(itemID int32) *ItemStatsValues {
item := mil.GetItem(itemID)
if item == nil {
return nil
}
return mil.CalculateItemBonusesFromItem(item, nil)
}
// CalculateItemBonusesWithEntity calculates the stat bonuses for an item with entity-specific modifiers
func (mil *MasterItemList) CalculateItemBonusesWithEntity(itemID int32, entity Entity) *ItemStatsValues {
item := mil.GetItem(itemID)
if item == nil {
return nil
}
return mil.CalculateItemBonusesFromItem(item, entity)
}
// CalculateItemBonusesFromItem calculates stat bonuses from an item instance
func (mil *MasterItemList) CalculateItemBonusesFromItem(item *Item, entity Entity) *ItemStatsValues {
if item == nil {
return nil
}
item.mutex.RLock()
defer item.mutex.RUnlock()
values := &ItemStatsValues{}
// Process all item stats
for _, stat := range item.ItemStats {
switch stat.StatType {
case ItemStatStr:
values.Str += int16(stat.Value)
case ItemStatSta:
values.Sta += int16(stat.Value)
case ItemStatAgi:
values.Agi += int16(stat.Value)
case ItemStatWis:
values.Wis += int16(stat.Value)
case ItemStatInt:
values.Int += int16(stat.Value)
case ItemStatVsSlash:
values.VsSlash += int16(stat.Value)
case ItemStatVsCrush:
values.VsCrush += int16(stat.Value)
case ItemStatVsPierce:
values.VsPierce += int16(stat.Value)
case ItemStatVsPhysical:
values.VsPhysical += int16(stat.Value)
case ItemStatVsHeat:
values.VsHeat += int16(stat.Value)
case ItemStatVsCold:
values.VsCold += int16(stat.Value)
case ItemStatVsMagic:
values.VsMagic += int16(stat.Value)
case ItemStatVsMental:
values.VsMental += int16(stat.Value)
case ItemStatVsDivine:
values.VsDivine += int16(stat.Value)
case ItemStatVsDisease:
values.VsDisease += int16(stat.Value)
case ItemStatVsPoison:
values.VsPoison += int16(stat.Value)
case ItemStatHealth:
values.Health += int16(stat.Value)
case ItemStatPower:
values.Power += int16(stat.Value)
case ItemStatConcentration:
values.Concentration += int8(stat.Value)
case ItemStatAbilityModifier:
values.AbilityModifier += int16(stat.Value)
case ItemStatCriticalMitigation:
values.CriticalMitigation += int16(stat.Value)
case ItemStatExtraShieldBlockChance:
values.ExtraShieldBlockChance += int16(stat.Value)
case ItemStatBeneficialCritChance:
values.BeneficialCritChance += int16(stat.Value)
case ItemStatCritBonus:
values.CritBonus += int16(stat.Value)
case ItemStatPotency:
values.Potency += int16(stat.Value)
case ItemStatHateGainMod:
values.HateGainMod += int16(stat.Value)
case ItemStatAbilityReuseSpeed:
values.AbilityReuseSpeed += int16(stat.Value)
case ItemStatAbilityCastingSpeed:
values.AbilityCastingSpeed += int16(stat.Value)
case ItemStatAbilityRecoverySpeed:
values.AbilityRecoverySpeed += int16(stat.Value)
case ItemStatSpellReuseSpeed:
values.SpellReuseSpeed += int16(stat.Value)
case ItemStatSpellMultiAttackChance:
values.SpellMultiAttackChance += int16(stat.Value)
case ItemStatDPS:
values.DPS += int16(stat.Value)
case ItemStatAttackSpeed:
values.AttackSpeed += int16(stat.Value)
case ItemStatMultiattackChance:
values.MultiAttackChance += int16(stat.Value)
case ItemStatFlurry:
values.Flurry += int16(stat.Value)
case ItemStatAEAutoattackChance:
values.AEAutoattackChance += int16(stat.Value)
case ItemStatStrikethrough:
values.Strikethrough += int16(stat.Value)
case ItemStatAccuracy:
values.Accuracy += int16(stat.Value)
case ItemStatOffensiveSpeed:
values.OffensiveSpeed += int16(stat.Value)
case ItemStatUncontestedParry:
values.UncontestedParry += stat.Value
case ItemStatUncontestedBlock:
values.UncontestedBlock += stat.Value
case ItemStatUncontestedDodge:
values.UncontestedDodge += stat.Value
case ItemStatUncontestedRiposte:
values.UncontestedRiposte += stat.Value
case ItemStatSizeMod:
values.SizeMod += stat.Value
}
}
return values
}
// Broker-related methods
// AddBrokerItemMapRange adds a broker item mapping range
func (mil *MasterItemList) AddBrokerItemMapRange(minVersion int32, maxVersion int32, clientBitmask int64, serverBitmask int64) {
mil.mutex.Lock()
defer mil.mutex.Unlock()
// Find existing range
var targetRange *VersionRange
for versionRange := range mil.brokerItemMap {
if versionRange.MinVersion == minVersion && versionRange.MaxVersion == maxVersion {
targetRange = versionRange
break
}
}
// Create new range if not found
if targetRange == nil {
targetRange = &VersionRange{
MinVersion: minVersion,
MaxVersion: maxVersion,
}
mil.brokerItemMap[targetRange] = make(map[int64]int64)
}
mil.brokerItemMap[targetRange][clientBitmask] = serverBitmask
}
// FindBrokerItemMapVersionRange finds a broker item map by version range
func (mil *MasterItemList) FindBrokerItemMapVersionRange(minVersion int32, maxVersion int32) map[int64]int64 {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
for versionRange, mapping := range mil.brokerItemMap {
// Check if min and max version are both in range
if versionRange.MinVersion <= minVersion && maxVersion <= versionRange.MaxVersion {
return mapping
}
// Check if the min version is in range, but max range is 0
if versionRange.MinVersion <= minVersion && versionRange.MaxVersion == 0 {
return mapping
}
// Check if min version is 0 and max_version has a cap
if versionRange.MinVersion == 0 && maxVersion <= versionRange.MaxVersion {
return mapping
}
}
return nil
}
// FindBrokerItemMapByVersion finds a broker item map by specific version
func (mil *MasterItemList) FindBrokerItemMapByVersion(version int32) map[int64]int64 {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
var defaultMapping map[int64]int64
for versionRange, mapping := range mil.brokerItemMap {
// Check for default range (0,0)
if versionRange.MinVersion == 0 && versionRange.MaxVersion == 0 {
defaultMapping = mapping
continue
}
// Check if version is in range
if version >= versionRange.MinVersion && version <= versionRange.MaxVersion {
return mapping
}
}
return defaultMapping
}
// ShouldAddItemBrokerType checks if an item should be added to broker by type
func (mil *MasterItemList) ShouldAddItemBrokerType(item *Item, itemType int64) bool {
if item == nil {
return false
}
switch itemType {
case ItemBrokerTypeAdornment:
return item.IsAdornment()
case ItemBrokerTypeAmmo:
return item.IsAmmo()
case ItemBrokerTypeAttuneable:
return item.CheckFlag(Attuneable)
case ItemBrokerTypeBag:
return item.IsBag()
case ItemBrokerTypeBauble:
return item.IsBauble()
case ItemBrokerTypeBook:
return item.IsBook()
case ItemBrokerTypeChainarmor:
return item.IsChainArmor()
case ItemBrokerTypeCloak:
return item.IsCloak()
case ItemBrokerTypeClotharmor:
return item.IsClothArmor()
case ItemBrokerTypeCollectable:
return item.IsCollectable()
case ItemBrokerTypeCrushweapon:
return item.IsCrushWeapon()
case ItemBrokerTypeDrink:
return item.IsFoodDrink()
case ItemBrokerTypeFood:
return item.IsFoodFood()
case ItemBrokerTypeHouseitem:
return item.IsHouseItem()
case ItemBrokerTypeJewelry:
return item.IsJewelry()
case ItemBrokerTypeLeatherarmor:
return item.IsLeatherArmor()
case ItemBrokerTypeLore:
return item.CheckFlag(Lore)
case ItemBrokerTypeMisc:
return item.IsMisc()
case ItemBrokerTypePierceweapon:
return item.IsPierceWeapon()
case ItemBrokerTypePlatearmor:
return item.IsPlateArmor()
case ItemBrokerTypePoison:
return item.IsPoison()
case ItemBrokerTypePotion:
return item.IsPotion()
case ItemBrokerTypeRecipebook:
return item.IsRecipeBook()
case ItemBrokerTypeSalesdisplay:
return item.IsSalesDisplay()
case ItemBrokerTypeShield:
return item.IsShield()
case ItemBrokerTypeSlashweapon:
return item.IsSlashWeapon()
case ItemBrokerTypeSpellscroll:
return item.IsSpellScroll()
case ItemBrokerTypeTinkered:
return item.IsTinkered()
case ItemBrokerTypeTradeskill:
return item.IsTradeskill()
}
return false
}
// ShouldAddItemBrokerSlot checks if an item should be added to broker by slot
func (mil *MasterItemList) ShouldAddItemBrokerSlot(item *Item, slotType int64) bool {
if item == nil {
return false
}
switch slotType {
case ItemBrokerSlotPrimary:
return item.HasSlot(EQ2PrimarySlot, -1)
case ItemBrokerSlotPrimary2H:
return item.HasSlot(EQ2PrimarySlot, -1) || item.HasSlot(EQ2SecondarySlot, -1)
case ItemBrokerSlotSecondary:
return item.HasSlot(EQ2SecondarySlot, -1)
case ItemBrokerSlotHead:
return item.HasSlot(EQ2HeadSlot, -1)
case ItemBrokerSlotChest:
return item.HasSlot(EQ2ChestSlot, -1)
case ItemBrokerSlotShoulders:
return item.HasSlot(EQ2ShouldersSlot, -1)
case ItemBrokerSlotForearms:
return item.HasSlot(EQ2ForearmsSlot, -1)
case ItemBrokerSlotHands:
return item.HasSlot(EQ2HandsSlot, -1)
case ItemBrokerSlotLegs:
return item.HasSlot(EQ2LegsSlot, -1)
case ItemBrokerSlotFeet:
return item.HasSlot(EQ2FeetSlot, -1)
case ItemBrokerSlotRing:
return item.HasSlot(EQ2LRingSlot, EQ2RRingSlot)
case ItemBrokerSlotEars:
return item.HasSlot(EQ2EarsSlot1, EQ2EarsSlot2)
case ItemBrokerSlotNeck:
return item.HasSlot(EQ2NeckSlot, -1)
case ItemBrokerSlotWrist:
return item.HasSlot(EQ2LWristSlot, EQ2RWristSlot)
case ItemBrokerSlotRangeWeapon:
return item.HasSlot(EQ2RangeSlot, -1)
case ItemBrokerSlotAmmo:
return item.HasSlot(EQ2AmmoSlot, -1)
case ItemBrokerSlotWaist:
return item.HasSlot(EQ2WaistSlot, -1)
case ItemBrokerSlotCloak:
return item.HasSlot(EQ2CloakSlot, -1)
case ItemBrokerSlotCharm:
return item.HasSlot(EQ2CharmSlot1, EQ2CharmSlot2)
case ItemBrokerSlotFood:
return item.HasSlot(EQ2FoodSlot, -1)
case ItemBrokerSlotDrink:
return item.HasSlot(EQ2DrinkSlot, -1)
}
return false
}
// ShouldAddItemBrokerStat checks if an item should be added to broker by stat
func (mil *MasterItemList) ShouldAddItemBrokerStat(item *Item, statType int64) bool {
if item == nil {
return false
}
// Check if the item has the requested stat type
for _, stat := range item.ItemStats {
switch statType {
case ItemBrokerStatTypeStr:
if stat.StatType == ItemStatStr {
return true
}
case ItemBrokerStatTypeSta:
if stat.StatType == ItemStatSta {
return true
}
case ItemBrokerStatTypeAgi:
if stat.StatType == ItemStatAgi {
return true
}
case ItemBrokerStatTypeWis:
if stat.StatType == ItemStatWis {
return true
}
case ItemBrokerStatTypeInt:
if stat.StatType == ItemStatInt {
return true
}
case ItemBrokerStatTypeHealth:
if stat.StatType == ItemStatHealth {
return true
}
case ItemBrokerStatTypePower:
if stat.StatType == ItemStatPower {
return true
}
case ItemBrokerStatTypePotency:
if stat.StatType == ItemStatPotency {
return true
}
case ItemBrokerStatTypeCritical:
if stat.StatType == ItemStatMeleeCritChance || stat.StatType == ItemStatBeneficialCritChance {
return true
}
case ItemBrokerStatTypeAttackspeed:
if stat.StatType == ItemStatAttackSpeed {
return true
}
case ItemBrokerStatTypeDPS:
if stat.StatType == ItemStatDPS {
return true
}
// Add more stat type checks as needed
}
}
return false
}
// GetItems searches for items based on criteria
func (mil *MasterItemList) GetItems(criteria *ItemSearchCriteria) []*Item {
if criteria == nil {
return nil
}
mil.mutex.RLock()
defer mil.mutex.RUnlock()
var results []*Item
for _, item := range mil.items {
if mil.matchesCriteria(item, criteria) {
results = append(results, item.Copy())
}
}
return results
}
// matchesCriteria checks if an item matches the search criteria
func (mil *MasterItemList) matchesCriteria(item *Item, criteria *ItemSearchCriteria) bool {
// Name matching
if criteria.Name != "" {
if !strings.Contains(strings.ToLower(item.Name), strings.ToLower(criteria.Name)) {
return false
}
}
// Price range
if criteria.MinPrice > 0 && item.BrokerPrice < criteria.MinPrice {
return false
}
if criteria.MaxPrice > 0 && item.BrokerPrice > criteria.MaxPrice {
return false
}
// Tier range
if criteria.MinTier > 0 && item.Details.Tier < criteria.MinTier {
return false
}
if criteria.MaxTier > 0 && item.Details.Tier > criteria.MaxTier {
return false
}
// Level range
if criteria.MinLevel > 0 && item.Details.RecommendedLevel < criteria.MinLevel {
return false
}
if criteria.MaxLevel > 0 && item.Details.RecommendedLevel > criteria.MaxLevel {
return false
}
// Item type matching
if criteria.ItemType != 0 {
if !mil.ShouldAddItemBrokerType(item, criteria.ItemType) {
return false
}
}
// Location type matching (slot compatibility)
if criteria.LocationType != 0 {
if !mil.ShouldAddItemBrokerSlot(item, criteria.LocationType) {
return false
}
}
// Broker type matching (stat requirements)
if criteria.BrokerType != 0 {
if !mil.ShouldAddItemBrokerStat(item, criteria.BrokerType) {
return false
}
}
// Seller matching
if criteria.Seller != "" {
if !strings.Contains(strings.ToLower(item.SellerName), strings.ToLower(criteria.Seller)) {
return false
}
}
// Adornment matching
if criteria.Adornment != "" {
if !strings.Contains(strings.ToLower(item.Adornment), strings.ToLower(criteria.Adornment)) {
return false
}
}
return true
}
// GetStats returns statistics about the master item list
func (mil *MasterItemList) GetStats() *ItemManagerStats {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
stats := &ItemManagerStats{
TotalItems: int32(len(mil.items)),
ItemsByType: make(map[int8]int32),
ItemsByTier: make(map[int8]int32),
LastUpdate: time.Now(),
}
// Count items by type and tier
for _, item := range mil.items {
stats.ItemsByType[item.GenericInfo.ItemType]++
stats.ItemsByTier[item.Details.Tier]++
}
return stats
}
// Validate validates the master item list
func (mil *MasterItemList) Validate() *ItemValidationResult {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
result := &ItemValidationResult{Valid: true}
for itemID, item := range mil.items {
itemResult := item.Validate()
if !itemResult.Valid {
result.Valid = false
for _, err := range itemResult.Errors {
result.Errors = append(result.Errors, fmt.Sprintf("Item %d: %s", itemID, err))
}
}
}
return result
}
// Size returns the number of items in the master list
func (mil *MasterItemList) Size() int {
return mil.GetItemCount()
}
// Clear removes all items from the master list
func (mil *MasterItemList) Clear() {
mil.RemoveAll()
}
func init() {
// Master item list system initialized
}

View File

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

View File

@ -1,572 +0,0 @@
package items
import (
"sync"
"time"
)
// 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 how items were added
type AddItemType int
const (
NotSet AddItemType = 0
BuyFromBroker AddItemType = 1
GMCommand AddItemType = 2
)
// ItemStatsValues represents the complete stat bonuses from an item
type ItemStatsValues struct {
// Base stats
Str int16 `json:"str"`
Sta int16 `json:"sta"`
Agi int16 `json:"agi"`
Wis int16 `json:"wis"`
Int int16 `json:"int"`
// Resistances
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"`
// Pools
Health int16 `json:"health"`
Power int16 `json:"power"`
Concentration int8 `json:"concentration"`
// Abilities and damage
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"`
// Uncontested stats
UncontestedParry float32 `json:"uncontested_parry"`
UncontestedBlock float32 `json:"uncontested_block"`
UncontestedDodge float32 `json:"uncontested_dodge"`
UncontestedRiposte float32 `json:"uncontested_riposte"`
// Other
SizeMod float32 `json:"size_mod"`
}
// ItemCore contains the 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"` // used when a bag is equipped
AppearanceType int16 `json:"appearance_type"` // 0 for combat armor, 1 for appearance armor
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 for specific classes
type ItemLevelOverride struct {
AdventureClass int8 `json:"adventure_class"`
TradeskillClass int8 `json:"tradeskill_class"`
Level int16 `json:"level"`
}
// ItemClass represents class requirements for an item
type ItemClass struct {
AdventureClass int8 `json:"adventure_class"`
TradeskillClass int8 `json:"tradeskill_class"`
Level int16 `json:"level"`
}
// ItemAppearance represents visual appearance data
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"`
}
// QuestRewardData represents quest reward information
type QuestRewardData struct {
QuestID int32 `json:"quest_id"`
IsTemporary bool `json:"is_temporary"`
Description string `json:"description"`
IsCollection bool `json:"is_collection"`
HasDisplayed bool `json:"has_displayed"`
TmpCoin int64 `json:"tmp_coin"`
TmpStatus int32 `json:"tmp_status"`
DbSaved bool `json:"db_saved"`
DbIndex int32 `json:"db_index"`
}
// Generic_Info 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"` // num/10
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"`
}
// AdornmentInfo contains adornment-specific information
type AdornmentInfo struct {
Duration float32 `json:"duration"`
ItemTypes int16 `json:"item_types"`
SlotType int16 `json:"slot_type"`
}
// 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"`
}
// ShieldInfo contains shield-specific information
type ShieldInfo struct {
ArmorInfo ArmorInfo `json:"armor_info"`
}
// RangedInfo contains ranged weapon information
type RangedInfo struct {
WeaponInfo WeaponInfo `json:"weapon_info"`
RangeLow int16 `json:"range_low"`
RangeHigh int16 `json:"range_high"`
}
// 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"` // 0=water, 1=food
Level int8 `json:"level"`
Duration float32 `json:"duration"`
Satiation int8 `json:"satiation"`
}
// 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"`
}
// BookInfo contains book-specific information
type BookInfo struct {
Language int8 `json:"language"`
Author string `json:"author"`
Title string `json:"title"`
}
// BookInfoPages represents a book page
type BookInfoPages struct {
Page int8 `json:"page"`
PageText string `json:"page_text"`
PageTextVAlign int8 `json:"page_text_valign"`
PageTextHAlign int8 `json:"page_text_halign"`
}
// SkillInfo contains skill book information
type SkillInfo struct {
SpellID int32 `json:"spell_id"`
SpellTier int32 `json:"spell_tier"`
}
// 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"` // 0 = floor, 1 = ceiling, 2 = wall
}
// 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"`
}
// 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"`
}
// ItemEffect represents an item effect
type ItemEffect struct {
Effect string `json:"effect"`
Percentage int8 `json:"percentage"`
SubBulletFlag int8 `json:"sub_bullet_flag"`
}
// 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"`
}
// ItemStatString represents a string-based item stat
type ItemStatString struct {
StatString string `json:"stat_string"`
}
// Item represents a complete item with all its properties
type Item struct {
// Basic item information
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"`
// Search and state flags
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"`
// Item metadata
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"`
// Adornment slots
Adorn0 int32 `json:"adorn0"`
Adorn1 int32 `json:"adorn1"`
Adorn2 int32 `json:"adorn2"`
// Spell information
SpellID int32 `json:"spell_id"`
SpellTier int8 `json:"spell_tier"`
ItemScript string `json:"item_script"`
// Collections and arrays
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"`
// Core item data
Details ItemCore `json:"details"`
GenericInfo GenericInfo `json:"generic_info"`
// Type-specific information (pointers to allow nil for unused types)
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"`
BookInfoPages *BookInfoPages `json:"book_info_pages,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"`
// Thread safety
mutex sync.RWMutex
}
// 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:"-"` // Complex type, exclude from JSON
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"`
}
// 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:"-"` // Exclude from JSON
origPacket []byte `json:"-"` // Exclude from JSON
mutex sync.RWMutex
}
// EquipmentItemList manages equipped items for a character
type EquipmentItemList struct {
items [NumSlots]*Item `json:"items"`
appearanceType int8 `json:"appearance_type"` // 0 for normal equip, 1 for appearance
xorPacket []byte `json:"-"` // Exclude from JSON
origPacket []byte `json:"-"` // Exclude from 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"`
}
// ItemSearchCriteria represents search criteria for items
type ItemSearchCriteria struct {
Name string `json:"name"`
ItemType int64 `json:"item_type"`
LocationType int64 `json:"location_type"`
BrokerType int64 `json:"broker_type"`
MinPrice int64 `json:"min_price"`
MaxPrice int64 `json:"max_price"`
MinSkill int8 `json:"min_skill"`
MaxSkill int8 `json:"max_skill"`
Seller string `json:"seller"`
Adornment string `json:"adornment"`
MinTier int8 `json:"min_tier"`
MaxTier int8 `json:"max_tier"`
MinLevel int16 `json:"min_level"`
MaxLevel int16 `json:"max_level"`
ItemClass int8 `json:"item_class"`
AdditionalCriteria map[string]string `json:"additional_criteria"`
}
// ItemValidationResult represents the result of item validation
type ItemValidationResult struct {
Valid bool `json:"valid"`
Errors []string `json:"errors,omitempty"`
}
// 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")
)