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 }