562 lines
16 KiB
Go
562 lines
16 KiB
Go
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
|
|
} |