eq2go/internal/items/character_items_db.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
}