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 }