simplify items and items/loot
This commit is contained in:
parent
d0c51ea42f
commit
6b3270684f
@ -16,6 +16,8 @@ This document outlines how we successfully simplified the EverQuest II housing p
|
||||
- Groups
|
||||
- Guilds
|
||||
- Heroic Ops
|
||||
- Items
|
||||
- Items/Loot
|
||||
|
||||
## Before: Complex Architecture (8 Files, ~2000+ Lines)
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
538
internal/items/helpers.go
Normal 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"
|
||||
}
|
||||
}
|
@ -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
@ -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
2005
internal/items/items.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
@ -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"
|
||||
)
|
@ -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
|
||||
}
|
@ -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
1162
internal/items/loot/loot.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user